1import datetime
2import logging
3import os
4import platform
5import re
6import sys
7from typing import Any
8from typing import Dict
9from typing import TYPE_CHECKING
10from typing import Union
11
12import ddtrace
13from ddtrace.internal.writer import AgentWriter
14from ddtrace.internal.writer import LogWriter
15from ddtrace.sampler import DatadogSampler
16
17from .logger import get_logger
18
19
20if TYPE_CHECKING:
21    from ddtrace import Tracer
22
23
24logger = get_logger(__name__)
25
26
27def in_venv():
28    # type: () -> bool
29    # Works with both venv and virtualenv
30    # https://stackoverflow.com/a/42580137
31    return (
32        "VIRTUAL_ENV" in os.environ
33        or hasattr(sys, "real_prefix")
34        or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
35    )
36
37
38def tags_to_str(tags):
39    # type: (Dict[str, Any]) -> str
40    # Turn a dict of tags to a string "k1:v1,k2:v2,..."
41    return ",".join(["%s:%s" % (k, v) for k, v in tags.items()])
42
43
44def collect(tracer):
45    # type: (Tracer) -> Dict[str, Any]
46    """Collect system and library information into a serializable dict."""
47
48    import pkg_resources
49
50    from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker
51
52    if isinstance(tracer.writer, LogWriter):
53        agent_url = "AGENTLESS"
54        agent_error = None
55    elif isinstance(tracer.writer, AgentWriter):
56        writer = tracer.writer
57        agent_url = writer.agent_url
58        try:
59            writer.write([])
60            writer.flush_queue(raise_exc=True)
61        except Exception as e:
62            agent_error = "Agent not reachable at %s. Exception raised: %s" % (agent_url, str(e))
63        else:
64            agent_error = None
65    else:
66        agent_url = "CUSTOM"
67        agent_error = None
68
69    sampler_rules = None
70    if isinstance(tracer.sampler, DatadogSampler):
71        sampler_rules = [str(rule) for rule in tracer.sampler.rules]
72
73    is_venv = in_venv()
74
75    packages_available = {p.project_name: p.version for p in pkg_resources.working_set}
76    integration_configs = {}  # type: Dict[str, Union[Dict[str, Any], str]]
77    for module, enabled in ddtrace._monkey.PATCH_MODULES.items():
78        # TODO: this check doesn't work in all cases... we need a mapping
79        #       between the module and the library name.
80        module_available = module in packages_available
81        module_instrumented = module in ddtrace._monkey._PATCHED_MODULES
82        module_imported = module in sys.modules
83
84        if enabled:
85            # Note that integration configs aren't added until the integration
86            # module is imported. This typically occurs as a side-effect of
87            # patch().
88            # This also doesn't load work in all cases since we don't always
89            # name the configuration entry the same as the integration module
90            # name :/
91            config = ddtrace.config._config.get(module, "N/A")
92        else:
93            config = None
94
95        if module_available:
96            integration_configs[module] = dict(
97                enabled=enabled,
98                instrumented=module_instrumented,
99                module_available=module_available,
100                module_version=packages_available[module],
101                module_imported=module_imported,
102                config=config,
103            )
104        else:
105            # Use N/A here to avoid the additional clutter of an entire
106            # config dictionary for a module that isn't available.
107            integration_configs[module] = "N/A"
108
109    pip_version = packages_available.get("pip", "N/A")
110
111    return dict(
112        # Timestamp UTC ISO 8601
113        date=datetime.datetime.utcnow().isoformat(),
114        # eg. "Linux", "Darwin"
115        os_name=platform.system(),
116        # eg. 12.5.0
117        os_version=platform.release(),
118        is_64_bit=sys.maxsize > 2 ** 32,
119        architecture=platform.architecture()[0],
120        vm=platform.python_implementation(),
121        version=ddtrace.__version__,
122        lang="python",
123        lang_version=platform.python_version(),
124        pip_version=pip_version,
125        in_virtual_env=is_venv,
126        agent_url=agent_url,
127        agent_error=agent_error,
128        env=ddtrace.config.env or "",
129        is_global_tracer=tracer == ddtrace.tracer,
130        enabled_env_setting=os.getenv("DATADOG_TRACE_ENABLED"),
131        tracer_enabled=tracer.enabled,
132        sampler_type=type(tracer.sampler).__name__ if tracer.sampler else "N/A",
133        priority_sampler_type=type(tracer.priority_sampler).__name__ if tracer.priority_sampler else "N/A",
134        sampler_rules=sampler_rules,
135        service=ddtrace.config.service or "",
136        debug=ddtrace.tracer.log.isEnabledFor(logging.DEBUG),
137        enabled_cli="ddtrace" in os.getenv("PYTHONPATH", ""),
138        analytics_enabled=ddtrace.config.analytics_enabled,
139        log_injection_enabled=ddtrace.config.logs_injection,
140        health_metrics_enabled=ddtrace.config.health_metrics_enabled,
141        runtime_metrics_enabled=RuntimeWorker.enabled,
142        dd_version=ddtrace.config.version or "",
143        priority_sampling_enabled=tracer.priority_sampler is not None,
144        global_tags=os.getenv("DD_TAGS", ""),
145        tracer_tags=tags_to_str(tracer.tags),
146        integrations=integration_configs,
147        partial_flush_enabled=tracer._partial_flush_enabled,
148        partial_flush_min_spans=tracer._partial_flush_min_spans,
149    )
150
151
152def pretty_collect(tracer, color=True):
153    class bcolors:
154        HEADER = "\033[95m"
155        OKBLUE = "\033[94m"
156        OKCYAN = "\033[96m"
157        OKGREEN = "\033[92m"
158        WARNING = "\033[93m"
159        FAIL = "\033[91m"
160        ENDC = "\033[0m"
161        BOLD = "\033[1m"
162
163    info = collect(tracer)
164
165    info_pretty = """{blue}{bold}Tracer Configurations:{end}
166    Tracer enabled: {tracer_enabled}
167    Debug logging: {debug}
168    Writing traces to: {agent_url}
169    Agent error: {agent_error}
170    App Analytics enabled(deprecated): {analytics_enabled}
171    Log injection enabled: {log_injection_enabled}
172    Health metrics enabled: {health_metrics_enabled}
173    Priority sampling enabled: {priority_sampling_enabled}
174    Partial flushing enabled: {partial_flush_enabled}
175    Partial flush minimum number of spans: {partial_flush_min_spans}
176    {green}{bold}Tagging:{end}
177    DD Service: {service}
178    DD Env: {env}
179    DD Version: {dd_version}
180    Global Tags: {global_tags}
181    Tracer Tags: {tracer_tags}""".format(
182        tracer_enabled=info.get("tracer_enabled"),
183        debug=info.get("debug"),
184        agent_url=info.get("agent_url") or "Not writing at the moment, is your tracer running?",
185        agent_error=info.get("agent_error") or "None",
186        analytics_enabled=info.get("analytics_enabled"),
187        log_injection_enabled=info.get("log_injection_enabled"),
188        health_metrics_enabled=info.get("health_metrics_enabled"),
189        priority_sampling_enabled=info.get("priority_sampling_enabled"),
190        partial_flush_enabled=info.get("partial_flush_enabled"),
191        partial_flush_min_spans=info.get("partial_flush_min_spans") or "Not set",
192        service=info.get("service") or "None",
193        env=info.get("env") or "None",
194        dd_version=info.get("dd_version") or "None",
195        global_tags=info.get("global_tags") or "None",
196        tracer_tags=info.get("tracer_tags") or "None",
197        blue=bcolors.OKBLUE,
198        green=bcolors.OKGREEN,
199        bold=bcolors.BOLD,
200        end=bcolors.ENDC,
201    )
202
203    summary = "{0}{1}Summary{2}".format(bcolors.OKCYAN, bcolors.BOLD, bcolors.ENDC)
204
205    if info.get("agent_error"):
206        summary += (
207            "\n\n{fail}ERROR: It looks like you have an agent error: '{agent_error}'\n If you're experiencing"
208            " a connection error, please make sure you've followed the setup for your particular environment so that "
209            "the tracer and Datadog agent are configured properly to connect, and that the Datadog agent is running: "
210            "https://ddtrace.readthedocs.io/en/stable/troubleshooting.html#failed-to-send-traces-connectionrefused"
211            "error"
212            "\nIf your issue is not a connection error then please reach out to support for further assistance:"
213            " https://docs.datadoghq.com/help/{end}"
214        ).format(fail=bcolors.FAIL, agent_error=info.get("agent_error"), end=bcolors.ENDC)
215
216    if not info.get("service"):
217        summary += (
218            "\n\n{warning}WARNING SERVICE NOT SET: It is recommended that a service tag be set for all traced"
219            " applications. For more information please see"
220            " https://ddtrace.readthedocs.io/en/stable/troubleshooting.html{end}"
221        ).format(warning=bcolors.WARNING, end=bcolors.ENDC)
222
223    if not info.get("env"):
224        summary += (
225            "\n\n{warning}WARNING ENV NOT SET: It is recommended that an env tag be set for all traced"
226            " applications. For more information please see "
227            "https://ddtrace.readthedocs.io/en/stable/troubleshooting.html{end}"
228        ).format(warning=bcolors.WARNING, end=bcolors.ENDC)
229
230    if not info.get("dd_version"):
231        summary += (
232            "\n\n{warning}WARNING VERSION NOT SET: It is recommended that a version tag be set for all traced"
233            " applications. For more information please see"
234            " https://ddtrace.readthedocs.io/en/stable/troubleshooting.html{end}"
235        ).format(warning=bcolors.WARNING, end=bcolors.ENDC)
236
237    info_pretty += "\n\n" + summary
238
239    if color is False:
240        return escape_ansi(info_pretty)
241
242    return info_pretty
243
244
245def escape_ansi(line):
246    ansi_escape = re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]")
247    return ansi_escape.sub("", line)
248