dash0-opentelemetry 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dash0/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,41 @@
1
+ """Environment variables read and written by the Dash0 distribution.
2
+
3
+ Grouped here so the distro, the configurator and the resource detection share a
4
+ single definition of each name.
5
+ """
6
+
7
+ # --- Dash0-specific knobs (mirrors the Node.js distribution) ---
8
+
9
+ DASH0_DISABLE = "DASH0_DISABLE"
10
+ """When set to ``true`` the distribution does nothing at all."""
11
+
12
+ DASH0_OTEL_COLLECTOR_BASE_URL = "DASH0_OTEL_COLLECTOR_BASE_URL"
13
+ """Base URL of the collector to export to. Required; without it the
14
+ distribution disables itself, because there is nowhere to send telemetry."""
15
+
16
+ DASH0_AUTOMATIC_SERVICE_NAME = "DASH0_AUTOMATIC_SERVICE_NAME"
17
+ """Set to ``false`` to opt out of deriving a fallback ``service.name``."""
18
+
19
+ DASH0_BOOTSTRAP_SPAN = "DASH0_BOOTSTRAP_SPAN"
20
+ """When set, a single span with this name is emitted once at startup."""
21
+
22
+ DASH0_FLUSH_ON_SIGTERM_SIGINT = "DASH0_FLUSH_ON_SIGTERM_SIGINT"
23
+ """Set to ``true`` to flush telemetry on SIGTERM/SIGINT before exiting."""
24
+
25
+
26
+ # --- Standard OpenTelemetry variables the distribution defaults ---
27
+
28
+ OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER"
29
+ OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER"
30
+ OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER"
31
+ OTEL_EXPORTER_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL"
32
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
33
+ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
34
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"
35
+ OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT"
36
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
37
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
38
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
39
+ OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME"
40
+ OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES"
41
+ OTEL_SDK_DISABLED = "OTEL_SDK_DISABLED"
@@ -0,0 +1,77 @@
1
+ """SDK configurator for the Dash0 OpenTelemetry distribution.
2
+
3
+ Registered as the single ``opentelemetry_configurator`` entry point. It runs
4
+ after :class:`~dash0.opentelemetry.distro.Dash0Distro` has set the environment,
5
+ so it lets the standard OpenTelemetry SDK configurator build the providers,
6
+ exporters and Resource from that environment, then adds two Dash0 behaviors from
7
+ the Node.js distribution: an optional startup ("bootstrap") span and optional
8
+ graceful flushing on SIGTERM/SIGINT.
9
+
10
+ Normal-exit flushing is already handled by the SDK: the tracer/meter/logger
11
+ providers register ``atexit`` shutdown hooks. The signal handling below covers
12
+ container termination, where ``atexit`` does not run by default.
13
+ """
14
+
15
+ import signal
16
+ from logging import getLogger
17
+ from os import environ, getpid, kill
18
+
19
+ from opentelemetry import _logs, metrics, trace
20
+ from opentelemetry.sdk._configuration import _OTelSDKConfigurator
21
+
22
+ from ._environment_variables import (
23
+ DASH0_BOOTSTRAP_SPAN,
24
+ DASH0_FLUSH_ON_SIGTERM_SIGINT,
25
+ )
26
+ from .settings import evaluate_gate, is_true
27
+
28
+ _logger = getLogger(__name__)
29
+
30
+
31
+ class Dash0Configurator(_OTelSDKConfigurator):
32
+ """Configure the OpenTelemetry SDK for the Dash0 distribution."""
33
+
34
+ def _configure(self, **kwargs):
35
+ enabled, reason = evaluate_gate()
36
+ if not enabled:
37
+ _logger.debug(
38
+ "Dash0 distribution disabled (%s); skipping SDK configuration",
39
+ reason,
40
+ )
41
+ return
42
+
43
+ super()._configure(**kwargs)
44
+
45
+ bootstrap_span_name = environ.get(DASH0_BOOTSTRAP_SPAN)
46
+ if bootstrap_span_name:
47
+ trace.get_tracer("dash0-python-distribution").start_span(
48
+ bootstrap_span_name
49
+ ).end()
50
+
51
+ if is_true(environ.get(DASH0_FLUSH_ON_SIGTERM_SIGINT)):
52
+
53
+ def _flush_and_reraise(signum, _frame):
54
+ for provider in (
55
+ trace.get_tracer_provider(),
56
+ metrics.get_meter_provider(),
57
+ _logs.get_logger_provider(),
58
+ ):
59
+ shutdown = getattr(provider, "shutdown", None)
60
+ if shutdown is not None:
61
+ try:
62
+ shutdown()
63
+ except Exception: # pylint: disable=broad-except
64
+ _logger.exception("dash0: error shutting down %s", provider)
65
+ # Restore the default disposition and re-raise so the process
66
+ # terminates with the expected exit semantics.
67
+ signal.signal(signum, signal.SIG_DFL)
68
+ kill(getpid(), signum)
69
+
70
+ try:
71
+ signal.signal(signal.SIGTERM, _flush_and_reraise)
72
+ signal.signal(signal.SIGINT, _flush_and_reraise)
73
+ except ValueError:
74
+ # signal handlers can only be installed from the main thread
75
+ _logger.debug(
76
+ "dash0: not on main thread, SIGTERM/SIGINT flush not installed"
77
+ )
@@ -0,0 +1,218 @@
1
+ """Dash0 OpenTelemetry distribution.
2
+
3
+ Provides :class:`Dash0Distro`, the ``opentelemetry_distro`` entry point activated
4
+ by ``opentelemetry-instrument`` when the Dash0 distribution is installed. It runs
5
+ before the configurator, so it is where the distribution:
6
+
7
+ * gates itself off if disabled or if no collector endpoint is configured;
8
+ * selects the pure-Python OTLP exporters (no native dependencies, safe to
9
+ inject) matching the configured OTLP protocol — ``http/protobuf`` by
10
+ default, ``grpc`` when requested — and points them at the Dash0 collector;
11
+ * injects the detected resource attributes (distro name/version, a fallback
12
+ service name, and the Kubernetes pod UID) so the SDK's Resource picks them up;
13
+ * activates every instrumentor defensively, so a single failing instrumentor
14
+ cannot abort auto-instrumentation of an injected process.
15
+
16
+ This mirrors the Dash0 Node.js distribution, adapted to Python's distro/
17
+ configurator machinery.
18
+ """
19
+
20
+ from logging import getLogger
21
+ from os import environ
22
+ from urllib.parse import urlparse, urlunparse
23
+
24
+ from opentelemetry.instrumentation.distro import BaseDistro
25
+
26
+ from ._environment_variables import (
27
+ DASH0_OTEL_COLLECTOR_BASE_URL,
28
+ OTEL_EXPORTER_OTLP_ENDPOINT,
29
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
30
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
31
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
32
+ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
33
+ OTEL_EXPORTER_OTLP_PROTOCOL,
34
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
35
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
36
+ OTEL_LOGS_EXPORTER,
37
+ OTEL_METRICS_EXPORTER,
38
+ OTEL_SDK_DISABLED,
39
+ OTEL_TRACES_EXPORTER,
40
+ )
41
+ from .resource import apply_detected_resource_attributes
42
+ from .settings import evaluate_gate
43
+ from .version import __version__
44
+
45
+ _logger = getLogger(__name__)
46
+
47
+ _GRPC_PROTOCOL = "grpc"
48
+ _HTTP_PROTOCOL = "http/protobuf"
49
+
50
+ # Entry-point names under which the pure-Python (pyproto) exporters register
51
+ # themselves, keyed by OTLP protocol. In an injected tree only the pyproto
52
+ # exporters ship under these names, so they resolve to the pure-Python
53
+ # implementations rather than the protobuf/grpcio ones.
54
+ _PYPROTO_EXPORTERS_BY_PROTOCOL = {
55
+ _HTTP_PROTOCOL: "otlp_proto_http",
56
+ _GRPC_PROTOCOL: "otlp_proto_grpc",
57
+ }
58
+
59
+ _DEFAULT_PROTOCOL = _HTTP_PROTOCOL
60
+
61
+ # Default OTLP receiver port per protocol. OTLP/gRPC and OTLP/HTTP conventionally
62
+ # listen on different ports, so a single collector base URL cannot address both.
63
+ _OTLP_DEFAULT_PORT = {_GRPC_PROTOCOL: 4317, _HTTP_PROTOCOL: 4318}
64
+
65
+ # Per-signal wiring: the exporter-selection variable, the per-signal protocol
66
+ # override, the per-signal endpoint override, and the OTLP/HTTP export path.
67
+ _SIGNALS = (
68
+ (
69
+ OTEL_TRACES_EXPORTER,
70
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
71
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
72
+ "v1/traces",
73
+ ),
74
+ (
75
+ OTEL_METRICS_EXPORTER,
76
+ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
77
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
78
+ "v1/metrics",
79
+ ),
80
+ (
81
+ OTEL_LOGS_EXPORTER,
82
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
83
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
84
+ "v1/logs",
85
+ ),
86
+ )
87
+
88
+
89
+ def _per_signal_endpoint_override(base_endpoint, protocol, signal_path):
90
+ """Return the per-signal endpoint a signal on ``protocol`` needs, or ``None``
91
+ when the shared ``OTEL_EXPORTER_OTLP_ENDPOINT`` already resolves correctly.
92
+
93
+ OTLP/gRPC (default port 4317, address used as ``host:port``) and OTLP/HTTP
94
+ (default port 4318, signal path appended) cannot share one endpoint. When a
95
+ signal's protocol differs from the transport ``base_endpoint`` points at —
96
+ detected by its port being the *other* supported protocol's default — the
97
+ port is rewritten to this protocol's default so the single Dash0 collector
98
+ base URL still routes every signal correctly. For an HTTP signal the path is
99
+ appended here, because the exporter appends it only to the shared endpoint,
100
+ not to a per-signal one. A custom or absent port is left untouched (the
101
+ collector is assumed to serve that protocol there, or the user supplies an
102
+ explicit per-signal endpoint, which is always honored).
103
+ """
104
+ parsed = urlparse(base_endpoint)
105
+ try:
106
+ current_port = parsed.port
107
+ except ValueError:
108
+ return None
109
+ other_protocol_ports = {
110
+ port for candidate, port in _OTLP_DEFAULT_PORT.items() if candidate != protocol
111
+ }
112
+ if current_port not in other_protocol_ports:
113
+ return None
114
+ host = parsed.hostname or ""
115
+ if ":" in host: # bracket an IPv6 literal so the port stays unambiguous
116
+ host = f"[{host}]"
117
+ netloc = f"{host}:{_OTLP_DEFAULT_PORT[protocol]}"
118
+ if "@" in parsed.netloc: # keep userinfo credentials, verbatim
119
+ netloc = f"{parsed.netloc.rsplit('@', 1)[0]}@{netloc}"
120
+ path = parsed.path
121
+ if protocol == _HTTP_PROTOCOL:
122
+ separator = "" if path.endswith("/") else "/"
123
+ path = path + separator + signal_path
124
+ return urlunparse(parsed._replace(netloc=netloc, path=path))
125
+
126
+
127
+ # Instrumentor entry-point names that must never be activated by this
128
+ # distribution. Empty by default; the standard OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
129
+ # variable is also honored by the auto-instrumentation loader.
130
+ _DISABLED_INSTRUMENTORS = frozenset()
131
+
132
+
133
+ class Dash0Distro(BaseDistro):
134
+ """OpenTelemetry distribution configured for Dash0."""
135
+
136
+ def _configure(self, **kwargs):
137
+ enabled, reason = evaluate_gate()
138
+ if not enabled:
139
+ # Make the SDK a no-op so nothing is exported, matching the Node.js
140
+ # distribution's "do nothing" behavior when disabled/misconfigured.
141
+ environ[OTEL_SDK_DISABLED] = "true"
142
+ _logger.warning(
143
+ "Dash0 distribution disabled: %s. No telemetry will be sent.",
144
+ reason,
145
+ )
146
+ return
147
+
148
+ default_protocol = environ.setdefault(
149
+ OTEL_EXPORTER_OTLP_PROTOCOL, _DEFAULT_PROTOCOL
150
+ )
151
+ environ.setdefault(
152
+ OTEL_EXPORTER_OTLP_ENDPOINT,
153
+ environ[DASH0_OTEL_COLLECTOR_BASE_URL],
154
+ )
155
+ # The effective shared endpoint (a user-set value is honored above), used
156
+ # to derive per-signal endpoints when signals resolve to different OTLP
157
+ # transports.
158
+ base_endpoint = environ[OTEL_EXPORTER_OTLP_ENDPOINT]
159
+ for (
160
+ exporter_variable,
161
+ protocol_variable,
162
+ endpoint_variable,
163
+ signal_path,
164
+ ) in _SIGNALS:
165
+ protocol = environ.get(protocol_variable, default_protocol)
166
+ exporter = _PYPROTO_EXPORTERS_BY_PROTOCOL.get(protocol)
167
+ if exporter is None:
168
+ _logger.warning(
169
+ "dash0: unsupported OTLP protocol %r in %s; defaulting %s to"
170
+ " the %r exporter",
171
+ protocol,
172
+ protocol_variable
173
+ if protocol_variable in environ
174
+ else OTEL_EXPORTER_OTLP_PROTOCOL,
175
+ exporter_variable,
176
+ _PYPROTO_EXPORTERS_BY_PROTOCOL[_DEFAULT_PROTOCOL],
177
+ )
178
+ protocol = _DEFAULT_PROTOCOL
179
+ exporter = _PYPROTO_EXPORTERS_BY_PROTOCOL[protocol]
180
+ selected_exporter = environ.setdefault(exporter_variable, exporter)
181
+ if selected_exporter != exporter:
182
+ # The user picked this signal's exporter directly; the distro
183
+ # cannot know which transport it speaks, so deriving an endpoint
184
+ # from the protocol variables could redirect a working exporter
185
+ # to the wrong port.
186
+ continue
187
+ signal_endpoint = _per_signal_endpoint_override(
188
+ base_endpoint, protocol, signal_path
189
+ )
190
+ if signal_endpoint is not None and endpoint_variable not in environ:
191
+ environ[endpoint_variable] = signal_endpoint
192
+ _logger.debug(
193
+ "dash0: shared endpoint %r targets the other OTLP transport;"
194
+ " derived %s=%s for protocol %r",
195
+ base_endpoint,
196
+ endpoint_variable,
197
+ signal_endpoint,
198
+ protocol,
199
+ )
200
+
201
+ apply_detected_resource_attributes(__version__)
202
+
203
+ def load_instrumentor(self, entry_point, **kwargs):
204
+ enabled, _ = evaluate_gate()
205
+ if not enabled:
206
+ return
207
+ if entry_point.name in _DISABLED_INSTRUMENTORS:
208
+ _logger.debug(
209
+ "dash0: instrumentor %s is disabled, skipping", entry_point.name
210
+ )
211
+ return
212
+ try:
213
+ super().load_instrumentor(entry_point, **kwargs)
214
+ except Exception: # pylint: disable=broad-except
215
+ _logger.exception(
216
+ "dash0: instrumentor %s failed to load, continuing without it",
217
+ entry_point.name,
218
+ )
File without changes
@@ -0,0 +1,282 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Bootstrap script for injector-based activation of the Dash0 OpenTelemetry distribution for Python.
3
+ #
4
+ # This file ships inside the dash0-opentelemetry wheel, but it is not meant to be imported from its packaged
5
+ # location (importing it executes it). Consumers that build an injectable tree — such as the dash0-operator's
6
+ # instrumentation image build — install the distribution into a self-contained directory
7
+ # (`pip install --target <dir> dash0-opentelemetry`) and copy this file from its packaged location to
8
+ # `<dir>/sitecustomize.py`. The OpenTelemetry injector (https://github.com/open-telemetry/opentelemetry-injector) then
9
+ # prepends `<dir>` to the PYTHONPATH environment variable of the processes to instrument, so that Python's `site`
10
+ # machinery imports this script on interpreter startup. The script initializes the OpenTelemetry
11
+ # auto-instrumentation after a set of safety checks, and deactivates itself (also for child processes) when any of
12
+ # them fails.
13
+ #
14
+ # The dependency-conflict check reads `<dir>/all-dependencies.txt`, a flattened list of every requirement of the
15
+ # injected tree (one PEP 508 requirement per line). The consumer generates that file when building `<dir>`; the
16
+ # dash0-operator does so with pipdeptree (see images/instrumentation/python/extract-flattened-requirements.sh in the
17
+ # dash0-operator repository).
18
+ #
19
+ # IMPORTANT: This file must be valid Python 2.7+. The injector prepends its directory to the PYTHONPATH of any Python
20
+ # process, including processes running interpreters this distribution does not support; this script must at least
21
+ # parse there, so it can deactivate itself gracefully instead of crashing the process. The PEP-263 encoding
22
+ # declaration above must stay on the first or second line: without it, Python 2.7 rejects the file's non-ASCII
23
+ # characters at parse time.
24
+
25
+ from __future__ import print_function
26
+ import os
27
+ from os.path import dirname
28
+ import sys
29
+ from sys import path, version, version_info, stderr
30
+
31
+ required_python_major_version = 3
32
+ minimum_python_minor_version = 10
33
+
34
+ # Packages of the OpenTelemetry API layer. Applications legitimately depend on them for manual instrumentation
35
+ # without being auto-instrumented, so finding them in the application does not indicate double instrumentation.
36
+ # (Version conflicts with them are still caught by the dependency conflict check.)
37
+ double_instrumentation_check_excluded_packages = [
38
+ "opentelemetry-api",
39
+ "opentelemetry-semantic-conventions",
40
+ ]
41
+
42
+ debug_enabled = os.environ.get("OTEL_INJECTOR_LOG_LEVEL") == "debug"
43
+
44
+
45
+ def _log_as_json_to_stderr(level, message):
46
+ log_body = '{{"level": "{}", "message": "{}", "logger_name": "dash0"'.format(level, message)
47
+ if level == "warn":
48
+ # All warnings are Dash0 telemetry collection issues, hence adding the respective marker.
49
+ log_body += ', "dash0.monitoring.telemetry_collection_issue": true'
50
+ log_body += '}'
51
+ print(log_body, file=stderr)
52
+
53
+
54
+ def _log_warn(message):
55
+ _log_as_json_to_stderr("warn", message)
56
+
57
+
58
+ def _log_debug(message):
59
+ if debug_enabled:
60
+ _log_as_json_to_stderr("debug", message)
61
+
62
+
63
+ _log_debug("running sitecustomize.py")
64
+ _log_debug("PYTHONPATH: {}".format(os.environ.get("PYTHONPATH")))
65
+
66
+
67
+ def _print_cannot_auto_instrument_message(reason):
68
+ if hasattr(sys, "argv"):
69
+ # If sys.argv is available, add the full command line (" ".join(sys.argv)) to the log message, so users know
70
+ # which Python process this is about.
71
+ _log_warn("cannot auto-instrument Python process: {} [{}]".format(reason, " ".join(sys.argv)))
72
+ else:
73
+ _log_warn("cannot auto-instrument Python process: {}".format(reason))
74
+
75
+
76
+ def _self_deactivate(current_site):
77
+ # Starting child processes is quite common in Python (e.g. gunicorn etc.), and in particular, the OpenTelemetry
78
+ # instrumentation wrapper (e.g. opentelemetry-instrument python app.py, see
79
+ # https://opentelemetry.io/docs/zero-code/python) does this. When self-deactivating, we need to make sure that we
80
+ # do not only self-deactivate for the current process but also directly deactivate the OpenTelemetry injector's
81
+ # auto-instrumentation for Python for child processes.
82
+ # Failing to do so, in particular when the application is already instrumented with the opentelemetry-instrument
83
+ # wrapper, might crash the child process in case of conflicting opentelemetry-* dependency versions. The reason is
84
+ # that the child process started by opentelemetry-instrument will run e.g.
85
+ # https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v0.53b0/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py
86
+ # in the version brought in by the application. But _load.py then loads other opentelemetry-* dependencies from
87
+ # /__otel_auto_instrumentation/agents/python/glibc/opentelemetry/instrumentation/auto_instrumentation/_load.py,
88
+ # that is, from the packages that we provide. This happens _before_ this sitecustomize.py script runs in the child
89
+ # process. Hence, we cannot rely on sitecustomize.py to self-deactivate in the child process, but must enforce
90
+ # self-deactivation via environment variables.
91
+
92
+ # Remove this site from PYTHONPATH so child processes do not attempt to load packages from us. PYTHONPATH entries
93
+ # are separated by os.pathsep (":" on POSIX).
94
+ current_pythonpath = os.environ.get("PYTHONPATH", "")
95
+ pythonpath_entries = [entry for entry in current_pythonpath.split(os.pathsep) if entry != current_site]
96
+ new_pythonpath = os.pathsep.join(pythonpath_entries)
97
+ _log_debug('setting PYTHONPATH in _self_deactivate: "{}"'.format(new_pythonpath))
98
+ os.environ["PYTHONPATH"] = new_pythonpath
99
+
100
+ # The OpenTelemetry injector will also run for child processes, and it would bring back the PYTHONPATH modification
101
+ # which we have just removed. Instruct it to not do that by disabling Python auto-instrumentation.
102
+ _log_debug("clearing PYTHON_AUTO_INSTRUMENTATION_AGENT_PATH_PREFIX in _self_deactivate")
103
+ os.environ["PYTHON_AUTO_INSTRUMENTATION_AGENT_PATH_PREFIX"] = ""
104
+
105
+ if current_site in path:
106
+ # Remove this site from the _current_ Python process, so our packages do not interfere with the application's
107
+ # dependencies.
108
+ path.remove(current_site)
109
+
110
+
111
+ def _shipped_opentelemetry_package_names(current_site):
112
+ # The OpenTelemetry packages this distribution ships: the injected tree at current_site is exactly the pinned
113
+ # dependency closure of dash0-opentelemetry (see its pyproject.toml), so enumerate the packages installed
114
+ # there instead of maintaining a hardcoded list. Only the OpenTelemetry-related packages count for the double
115
+ # instrumentation check; overlap on the general-purpose support packages we also ship (wrapt, psutil, ...) is
116
+ # common and is handled by the dependency version conflict check instead.
117
+ import importlib.metadata
118
+ names = set()
119
+ for dist in importlib.metadata.distributions(path=[current_site]):
120
+ name = dist.metadata["Name"]
121
+ if name is None:
122
+ continue
123
+ name = name.lower()
124
+ if name in double_instrumentation_check_excluded_packages:
125
+ continue
126
+ if name.startswith(("opentelemetry-", "dash0-")):
127
+ names.add(name)
128
+ return names
129
+
130
+
131
+ def _check_for_double_instrumentation(current_site):
132
+ import importlib.metadata
133
+ packages_we_ship = _shipped_opentelemetry_package_names(current_site)
134
+ offending_packages = []
135
+ for dist in importlib.metadata.distributions():
136
+ name = dist.metadata["Name"]
137
+ if name is not None and name.lower() in packages_we_ship:
138
+ offending_packages.append(str(dist._path))
139
+ if offending_packages:
140
+ _self_deactivate(current_site)
141
+ _print_cannot_auto_instrument_message(
142
+ "The application has OpenTelemetry dependencies which indicate that it is already instrumented. The " +
143
+ "following problematic dependencies have been found: {}. ".format(", ".join(offending_packages)) +
144
+ "Skipping the Dash0 Python auto-instrumentation to avoid double instrumentation. Remove the mentioned "
145
+ "dependencies and make sure the opentelemetry-instrument wrapper executable is not used if you want to " +
146
+ "use Dash0's Python auto-instrumentation")
147
+ return True
148
+ return False
149
+
150
+
151
+ def _read_all_dependencies():
152
+ """Read all flattened dependencies from all-dependencies.txt. Returns list of requirement strings or None on error."""
153
+ dependencies_file = os.path.join(dirname(__file__), "all-dependencies.txt")
154
+ requirements_to_check = []
155
+ try:
156
+ with open(dependencies_file, "r") as f:
157
+ for line in f:
158
+ line = line.strip()
159
+ # Skip empty lines and comments
160
+ if not line or line.startswith("#"):
161
+ continue
162
+ requirements_to_check.append(line)
163
+ return requirements_to_check
164
+ except (IOError, OSError):
165
+ return None
166
+
167
+
168
+ def _check_dependency_version_conflict(req_string, version_conflicts):
169
+ """Check for dependency version conflict for a given requirement.
170
+
171
+ Args:
172
+ req_string: Requirement string (e.g., "package-name >=1.0.0")
173
+ version_conflicts: Dictionary to accumulate version conflicts (modified in place)
174
+ """
175
+ import importlib.metadata
176
+ from packaging.requirements import Requirement
177
+ from packaging.version import Version
178
+
179
+ _log_debug("_check_dependency_version_conflict({})".format(req_string))
180
+ req = Requirement(req_string)
181
+
182
+ # Skip extras/markers for simplicity in conflict detection
183
+ if req.marker and not req.marker.evaluate():
184
+ return
185
+
186
+ if req.name == "pip":
187
+ # A lot of applications depend on pip implicitly, without actually importing it, ignore pip for the dependency
188
+ # conflict check.
189
+ return
190
+
191
+ try:
192
+ installed_distribution = importlib.metadata.distribution(req.name)
193
+ installed_version = Version(installed_distribution.version)
194
+ _log_debug("installed_version: {}".format(installed_version))
195
+
196
+ # Check if installed version satisfies the requirement
197
+ if req.specifier and installed_version not in req.specifier:
198
+ _log_debug("adding version conflict for {}".format(req.name))
199
+ version_conflicts[req.name] = {
200
+ "version_required": str(req.specifier),
201
+ "version_found": str(installed_version),
202
+ }
203
+ except importlib.metadata.PackageNotFoundError:
204
+ _log_debug("adding version error for {}".format(req.name))
205
+ version_conflicts[req.name] = {"error": "required package not found"}
206
+
207
+
208
+ def import_distro():
209
+ _log_debug("checking Python version")
210
+ current_site = dirname(__file__)
211
+
212
+ # We cannot use `sys.version_info.major` or other named attributes, as they only got introduced only in Python 3.1.
213
+ if version_info[0] != required_python_major_version or version_info[1] < minimum_python_minor_version:
214
+ _self_deactivate(current_site)
215
+ _print_cannot_auto_instrument_message("unsupported Python version: {}".format(version))
216
+ return
217
+ _log_debug("found eligible Python version: {}".format(version_info))
218
+
219
+ # The Dash0 Python distribution requires DASH0_OTEL_COLLECTOR_BASE_URL to activate and derives its OTLP endpoint
220
+ # from it. The Dash0 operator sets OTEL_EXPORTER_OTLP_ENDPOINT; bridge it here so the distribution activates
221
+ # without requiring operator-side changes.
222
+ if not os.environ.get("DASH0_OTEL_COLLECTOR_BASE_URL"):
223
+ otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
224
+ if otlp_endpoint:
225
+ _log_debug("bridging OTEL_EXPORTER_OTLP_ENDPOINT to DASH0_OTEL_COLLECTOR_BASE_URL: {}".format(otlp_endpoint))
226
+ os.environ["DASH0_OTEL_COLLECTOR_BASE_URL"] = otlp_endpoint
227
+
228
+ _log_debug("checking for double instrumentation")
229
+
230
+ # Temporarily remove this site to be able to check for problematic dependencies in all other available sites.
231
+ # Without the current site it is easier to differentiate between "our" dependencies and dependencies brought in by
232
+ # the application under monitoring.
233
+ # Maintenance note: After checking for double instrumentation scenarios, we add back the current site, via
234
+ # path.append(current_site). The remove here together with the append also deliberately reorders sys.path to put
235
+ # this site last, before evaluating conflicting dependency versions. See below for more details on that.
236
+ path.remove(current_site)
237
+
238
+ if _check_for_double_instrumentation(current_site):
239
+ return
240
+
241
+ _log_debug("no double instrumentation detected")
242
+ _log_debug("checking for dependency conflicts")
243
+
244
+ # This "path.append(current_site)" together with "path.remove(current_site)" executed a couple of lines above
245
+ # effectively reorders sys.path to put this site last. This is necessary to evaluate potential conflicting
246
+ # dependency versions.
247
+ #
248
+ # We will leave this reordering in effect when importing and initializing opentelemetry.instrumentation. Since we
249
+ # have already ruled out dependency conflicts, the order should not matter, but with this re-ordering we make sure
250
+ # the application's package versions will win over the package versions we bring (in case there are overlapping
251
+ # dependencies).
252
+ path.append(current_site)
253
+
254
+ version_conflicts = {}
255
+ requirements_to_check = _read_all_dependencies()
256
+ if requirements_to_check is None:
257
+ _self_deactivate(current_site)
258
+ _print_cannot_auto_instrument_message("cannot read all-dependencies.txt for dependency conflict checking")
259
+ return
260
+
261
+ for req_string in requirements_to_check:
262
+ _check_dependency_version_conflict(req_string, version_conflicts)
263
+ if version_conflicts:
264
+ break
265
+
266
+ if not version_conflicts:
267
+ try:
268
+ _log_debug("importing and initializing the Python auto-instrumentation now")
269
+ from opentelemetry.instrumentation import auto_instrumentation
270
+ auto_instrumentation.initialize()
271
+ except Exception as e:
272
+ _self_deactivate(current_site)
273
+ _print_cannot_auto_instrument_message(
274
+ "error when importing/initializing the Python OpenTelemetry auto-instrumentation: {}: {}".format(
275
+ type(e).__name__, e))
276
+ else:
277
+ # Remove this site for good, we do not want to trigger dependency conflict issues.
278
+ _self_deactivate(current_site)
279
+ _print_cannot_auto_instrument_message("dependency conflicts: {}".format(version_conflicts))
280
+
281
+
282
+ import_distro()
@@ -0,0 +1,239 @@
1
+ """Resource detection for the Dash0 distribution.
2
+
3
+ Ports the two custom detectors the Node.js distribution ships (a Kubernetes pod
4
+ UID detector and a service-name fallback) plus the ``telemetry.distro.*``
5
+ attributes, and injects them into the standard OpenTelemetry environment
6
+ variables *before* the SDK builds its Resource.
7
+
8
+ Injecting via ``OTEL_RESOURCE_ATTRIBUTES``/``OTEL_SERVICE_NAME`` (rather than
9
+ registering SDK resource detectors) keeps this working across OTel SDK versions:
10
+ the distro's ``_configure`` runs before the configurator, and the SDK's own
11
+ ``Resource.create`` reads these variables.
12
+
13
+ Declarative configuration (``OTEL_CONFIG_FILE``) deliberately ignores those
14
+ environment variables, so the same attributes are also exposed as standard SDK
15
+ resource detectors, registered under the ``opentelemetry_resource_detector``
16
+ entry-point group (see ``pyproject.toml``):
17
+ :class:`Dash0DistributionResourceDetector`,
18
+ :class:`Dash0KubernetesResourceDetector` and
19
+ :class:`Dash0ServiceNameResourceDetector`.
20
+ """
21
+
22
+ import re
23
+ import sys
24
+ from os import environ, path
25
+
26
+ from opentelemetry.sdk.resources import (
27
+ Resource,
28
+ ResourceDetector,
29
+ ServiceInstanceIdResourceDetector,
30
+ )
31
+
32
+ from ._environment_variables import (
33
+ DASH0_AUTOMATIC_SERVICE_NAME,
34
+ OTEL_RESOURCE_ATTRIBUTES,
35
+ OTEL_SERVICE_NAME,
36
+ )
37
+ from .settings import is_false
38
+ from .version import __version__
39
+
40
+ _SERVICE_NAME = "service.name"
41
+ _SERVICE_VERSION = "service.version"
42
+ _K8S_POD_UID = "k8s.pod.uid"
43
+ _DISTRO_NAME = "telemetry.distro.name"
44
+ _DISTRO_VERSION = "telemetry.distro.version"
45
+
46
+ _ETC_HOSTS = "/etc/hosts"
47
+ _K8S_HOSTS_MARKER = "# Kubernetes-managed hosts file"
48
+ _CGROUP_V1_MOUNTINFO = "/proc/self/mountinfo"
49
+ _CGROUP_V2_CGROUP = "/proc/self/cgroup"
50
+ _POD_MOUNT_MARKER = "/pods/"
51
+ _POD_UID_LEN = 36
52
+ _CONTAINER_ID_LEN = 64
53
+ _POD_LABEL = "pod"
54
+ # matches cgroup v2 slices like kubepods-pode462ffed_94ce_4806_a52e_d2726f448f15.slice
55
+ _POD_UID_IN_CGROUP = re.compile(
56
+ r"^[a-z_-]*pod(?P<uid>[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}"
57
+ r"[-_][0-9a-f]{4}[-_][0-9a-f]{12})\.slice$"
58
+ )
59
+
60
+
61
+ def distribution_resource_attributes(version):
62
+ attributes = {_DISTRO_NAME: "dash0-python"}
63
+ if version:
64
+ attributes[_DISTRO_VERSION] = version
65
+ return attributes
66
+
67
+
68
+ def _read_candidate_lines(filename, minimum_length):
69
+ try:
70
+ with open(filename, encoding="utf8") as file:
71
+ content = file.read()
72
+ except OSError:
73
+ return []
74
+ return [
75
+ line.strip()
76
+ for line in content.splitlines()
77
+ if len(line.strip()) > minimum_length
78
+ ]
79
+
80
+
81
+ def running_in_kubernetes():
82
+ try:
83
+ with open(_ETC_HOSTS, encoding="utf8") as file:
84
+ first_line = file.readline()
85
+ except OSError:
86
+ return False
87
+ return first_line.startswith(_K8S_HOSTS_MARKER)
88
+
89
+
90
+ def pod_uid_from_cgroup_v1():
91
+ for line in _read_candidate_lines(_CGROUP_V1_MOUNTINFO, _POD_UID_LEN):
92
+ marker_index = line.find(_POD_MOUNT_MARKER)
93
+ if marker_index > 0:
94
+ after_marker = line[marker_index + len(_POD_MOUNT_MARKER) :]
95
+ if len(after_marker) >= _POD_UID_LEN:
96
+ return after_marker[:_POD_UID_LEN]
97
+ return None
98
+
99
+
100
+ def pod_uid_from_cgroup_v2():
101
+ for line in _read_candidate_lines(_CGROUP_V2_CGROUP, _CONTAINER_ID_LEN):
102
+ segments = line.split("/")
103
+ if len(segments) <= 2:
104
+ continue
105
+ penultimate = segments[-2]
106
+ expected_length = _POD_UID_LEN + len(_POD_LABEL)
107
+ if penultimate.startswith(_POD_LABEL) and len(penultimate) == expected_length:
108
+ return penultimate[len(_POD_LABEL) :]
109
+ match = _POD_UID_IN_CGROUP.match(penultimate)
110
+ if match:
111
+ return match.group("uid").replace("_", "-")
112
+ return None
113
+
114
+
115
+ def detect_kubernetes_pod_uid():
116
+ if not running_in_kubernetes():
117
+ return None
118
+ return pod_uid_from_cgroup_v1() or pod_uid_from_cgroup_v2()
119
+
120
+
121
+ def _service_name_already_configured():
122
+ if environ.get(OTEL_SERVICE_NAME, "").strip():
123
+ return True
124
+ for pair in environ.get(OTEL_RESOURCE_ATTRIBUTES, "").split(","):
125
+ key, _, value = pair.partition("=")
126
+ if key.strip() == _SERVICE_NAME and value.strip().strip('"'):
127
+ return True
128
+ return False
129
+
130
+
131
+ def detect_fallback_service_name():
132
+ """Best-effort service name from the running program.
133
+
134
+ The Node.js distribution reads ``package.json``; Python has no universal
135
+ equivalent, so this derives a name from the entrypoint script.
136
+ """
137
+ argv0 = sys.argv[0] if sys.argv else ""
138
+ if not argv0:
139
+ return None
140
+ name = path.basename(argv0)
141
+ if name.endswith(".py"):
142
+ name = name[: -len(".py")]
143
+ return name or None
144
+
145
+
146
+ def _merge_into_resource_attributes(attributes):
147
+ existing = environ.get(OTEL_RESOURCE_ATTRIBUTES, "")
148
+ present_keys = {
149
+ pair.partition("=")[0].strip() for pair in existing.split(",") if pair.strip()
150
+ }
151
+ additions = [
152
+ f"{key}={value}" for key, value in attributes.items() if key not in present_keys
153
+ ]
154
+ if not additions:
155
+ return
156
+ parts = [existing] if existing.strip() else []
157
+ parts.extend(additions)
158
+ environ[OTEL_RESOURCE_ATTRIBUTES] = ",".join(parts)
159
+
160
+
161
+ def apply_detected_resource_attributes(version):
162
+ if not is_false(environ.get(DASH0_AUTOMATIC_SERVICE_NAME)) and (
163
+ not _service_name_already_configured()
164
+ ):
165
+ fallback = detect_fallback_service_name()
166
+ if fallback:
167
+ environ[OTEL_SERVICE_NAME] = fallback
168
+
169
+ attributes = distribution_resource_attributes(version)
170
+ pod_uid = detect_kubernetes_pod_uid()
171
+ if pod_uid:
172
+ attributes[_K8S_POD_UID] = pod_uid
173
+ _merge_into_resource_attributes(attributes)
174
+
175
+
176
+ def _detected_service_name():
177
+ """The ``service.name`` the environment-variable path would produce.
178
+
179
+ ``OTEL_SERVICE_NAME`` wins (mirroring the declarative ``service`` detector,
180
+ which also reads it); the entrypoint-derived fallback applies only when no
181
+ service name is configured anywhere in the environment and
182
+ ``DASH0_AUTOMATIC_SERVICE_NAME`` is not ``false``. A ``service.name``
183
+ embedded in ``OTEL_RESOURCE_ATTRIBUTES`` suppresses the fallback but is not
184
+ resurrected here — declarative configuration ignores that variable by
185
+ design.
186
+ """
187
+ explicit = environ.get(OTEL_SERVICE_NAME, "").strip()
188
+ if explicit:
189
+ return explicit
190
+ if is_false(environ.get(DASH0_AUTOMATIC_SERVICE_NAME)):
191
+ return None
192
+ if _service_name_already_configured():
193
+ return None
194
+ return detect_fallback_service_name()
195
+
196
+
197
+ class Dash0DistributionResourceDetector(ResourceDetector):
198
+ """Reports ``telemetry.distro.name`` and ``telemetry.distro.version``.
199
+
200
+ The environment-variable injection above does not survive declarative
201
+ configuration: when ``OTEL_CONFIG_FILE`` is set, the declarative resource
202
+ builder ignores ``OTEL_RESOURCE_ATTRIBUTES``/``OTEL_SERVICE_NAME`` and only
203
+ runs detectors named in the config file, resolved via the
204
+ ``opentelemetry_resource_detector`` entry-point group. This detector and
205
+ its two siblings below are registered there (``dash0_distribution``,
206
+ ``dash0_kubernetes``, ``dash0_service_name``) and work with
207
+ ``OTEL_EXPERIMENTAL_RESOURCE_DETECTORS`` as well.
208
+ """
209
+
210
+ def detect(self):
211
+ return Resource(distribution_resource_attributes(__version__))
212
+
213
+
214
+ class Dash0KubernetesResourceDetector(ResourceDetector):
215
+ """Reports ``k8s.pod.uid``, detected from ``/etc/hosts`` and the cgroups."""
216
+
217
+ def detect(self):
218
+ pod_uid = detect_kubernetes_pod_uid()
219
+ if pod_uid:
220
+ return Resource({_K8S_POD_UID: pod_uid})
221
+ return Resource.get_empty()
222
+
223
+
224
+ class Dash0ServiceNameResourceDetector(ResourceDetector):
225
+ """Service detection: the upstream behavior plus the Dash0 fallback.
226
+
227
+ Wraps what upstream service detection provides — a stable
228
+ ``service.instance.id`` (via the SDK's ``ServiceInstanceIdResourceDetector``)
229
+ and ``service.name`` from ``OTEL_SERVICE_NAME`` — and adds the distro's
230
+ entrypoint-derived ``service.name`` fallback when no service name is
231
+ configured (see :func:`_detected_service_name`).
232
+ """
233
+
234
+ def detect(self):
235
+ attributes = dict(ServiceInstanceIdResourceDetector().detect().attributes)
236
+ service_name = _detected_service_name()
237
+ if service_name:
238
+ attributes[_SERVICE_NAME] = service_name
239
+ return Resource(attributes)
@@ -0,0 +1,33 @@
1
+ """Enablement gate for the Dash0 distribution.
2
+
3
+ Mirrors the two hard gates the Node.js distribution checks before doing
4
+ anything: an explicit kill switch, and a required collector endpoint (there is
5
+ no point instrumenting if there is nowhere to send the telemetry).
6
+ """
7
+
8
+ from os import environ
9
+
10
+ from ._environment_variables import (
11
+ DASH0_DISABLE,
12
+ DASH0_OTEL_COLLECTOR_BASE_URL,
13
+ )
14
+
15
+
16
+ def is_true(value):
17
+ return value is not None and value.strip().lower() == "true"
18
+
19
+
20
+ def is_false(value):
21
+ return value is not None and value.strip().lower() == "false"
22
+
23
+
24
+ def evaluate_gate():
25
+ """Return ``(enabled, reason)``.
26
+
27
+ ``reason`` is a human-readable explanation when disabled, else ``None``.
28
+ """
29
+ if is_true(environ.get(DASH0_DISABLE)):
30
+ return False, f"{DASH0_DISABLE} is set to true"
31
+ if not environ.get(DASH0_OTEL_COLLECTOR_BASE_URL):
32
+ return False, f"{DASH0_OTEL_COLLECTOR_BASE_URL} is not set"
33
+ return True, None
@@ -0,0 +1 @@
1
+ __version__ = "0.3.1"
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: dash0-opentelemetry
3
+ Version: 0.3.1
4
+ Summary: Dash0 OpenTelemetry distribution for Python
5
+ Author: Dash0
6
+ License-Expression: Apache-2.0
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: asgiref==3.12.1
18
+ Requires-Dist: dash0-opentelemetry-exporter-otlp-pyproto-grpc==1.44.0.post1
19
+ Requires-Dist: dash0-opentelemetry-exporter-otlp-pyproto-http==1.44.0.post1
20
+ Requires-Dist: opentelemetry-api==1.44.0
21
+ Requires-Dist: opentelemetry-instrumentation-aio-pika==0.65b0
22
+ Requires-Dist: opentelemetry-instrumentation-aiohttp-client==0.65b0
23
+ Requires-Dist: opentelemetry-instrumentation-aiohttp-server==0.65b0
24
+ Requires-Dist: opentelemetry-instrumentation-aiokafka==0.65b0
25
+ Requires-Dist: opentelemetry-instrumentation-aiopg==0.65b0
26
+ Requires-Dist: opentelemetry-instrumentation-asgi==0.65b0
27
+ Requires-Dist: opentelemetry-instrumentation-asyncclick==0.65b0
28
+ Requires-Dist: opentelemetry-instrumentation-asyncio==0.65b0
29
+ Requires-Dist: opentelemetry-instrumentation-asyncpg==0.65b0
30
+ Requires-Dist: opentelemetry-instrumentation-boto3sqs==0.65b0
31
+ Requires-Dist: opentelemetry-instrumentation-botocore==0.65b0
32
+ Requires-Dist: opentelemetry-instrumentation-cassandra==0.65b0
33
+ Requires-Dist: opentelemetry-instrumentation-celery==0.65b0
34
+ Requires-Dist: opentelemetry-instrumentation-click==0.65b0
35
+ Requires-Dist: opentelemetry-instrumentation-confluent-kafka==0.65b0
36
+ Requires-Dist: opentelemetry-instrumentation-dbapi==0.65b0
37
+ Requires-Dist: opentelemetry-instrumentation-django==0.65b0
38
+ Requires-Dist: opentelemetry-instrumentation-exceptions==0.65b0
39
+ Requires-Dist: opentelemetry-instrumentation-falcon==0.65b0
40
+ Requires-Dist: opentelemetry-instrumentation-fastapi==0.65b0
41
+ Requires-Dist: opentelemetry-instrumentation-flask==0.65b0
42
+ Requires-Dist: opentelemetry-instrumentation-grpc==0.65b0
43
+ Requires-Dist: opentelemetry-instrumentation-httpx==0.65b0
44
+ Requires-Dist: opentelemetry-instrumentation-jinja2==0.65b0
45
+ Requires-Dist: opentelemetry-instrumentation-kafka-python==0.65b0
46
+ Requires-Dist: opentelemetry-instrumentation-logging==0.65b0
47
+ Requires-Dist: opentelemetry-instrumentation-mysql==0.65b0
48
+ Requires-Dist: opentelemetry-instrumentation-mysqlclient==0.65b0
49
+ Requires-Dist: opentelemetry-instrumentation-openai-v2==2.3b0
50
+ Requires-Dist: opentelemetry-instrumentation-pika==0.65b0
51
+ Requires-Dist: opentelemetry-instrumentation-psycopg2==0.65b0
52
+ Requires-Dist: opentelemetry-instrumentation-psycopg==0.65b0
53
+ Requires-Dist: opentelemetry-instrumentation-pymemcache==0.65b0
54
+ Requires-Dist: opentelemetry-instrumentation-pymongo==0.65b0
55
+ Requires-Dist: opentelemetry-instrumentation-pymssql==0.65b0
56
+ Requires-Dist: opentelemetry-instrumentation-pymysql==0.65b0
57
+ Requires-Dist: opentelemetry-instrumentation-pyramid==0.65b0
58
+ Requires-Dist: opentelemetry-instrumentation-redis==0.65b0
59
+ Requires-Dist: opentelemetry-instrumentation-remoulade==0.65b0
60
+ Requires-Dist: opentelemetry-instrumentation-requests==0.65b0
61
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy==0.65b0
62
+ Requires-Dist: opentelemetry-instrumentation-sqlite3==0.65b0
63
+ Requires-Dist: opentelemetry-instrumentation-starlette==0.65b0
64
+ Requires-Dist: opentelemetry-instrumentation-structlog==0.65b0
65
+ Requires-Dist: opentelemetry-instrumentation-system-metrics==0.65b0
66
+ Requires-Dist: opentelemetry-instrumentation-threading==0.65b0
67
+ Requires-Dist: opentelemetry-instrumentation-tornado==0.65b0
68
+ Requires-Dist: opentelemetry-instrumentation-tortoiseorm==0.65b0
69
+ Requires-Dist: opentelemetry-instrumentation-urllib3==0.65b0
70
+ Requires-Dist: opentelemetry-instrumentation-urllib==0.65b0
71
+ Requires-Dist: opentelemetry-instrumentation-vertexai==2.2b0
72
+ Requires-Dist: opentelemetry-instrumentation-wsgi==0.65b0
73
+ Requires-Dist: opentelemetry-instrumentation==0.65b0
74
+ Requires-Dist: opentelemetry-propagator-aws-xray==1.0.2
75
+ Requires-Dist: opentelemetry-sdk==1.44.0
76
+ Requires-Dist: opentelemetry-semantic-conventions==0.65b0
77
+ Requires-Dist: opentelemetry-util-genai==0.2b0
78
+ Requires-Dist: opentelemetry-util-http==0.65b0
79
+ Requires-Dist: packaging==26.2
80
+ Requires-Dist: psutil==7.2.2
81
+ Requires-Dist: typing-extensions==4.16.0
82
+ Requires-Dist: wrapt==1.17.3
83
+ Description-Content-Type: text/x-rst
84
+
85
+ ===================
86
+ dash0-opentelemetry
87
+ ===================
88
+
89
+ The Dash0 OpenTelemetry distribution for Python. It is the Python counterpart of
90
+ the Dash0 Node.js distribution (``@dash0/opentelemetry``), adapted to Python's
91
+ distro/configurator machinery, and is meant to be injected into a process (for
92
+ example by the OpenTelemetry injector) with no code changes.
93
+
94
+ Entry points
95
+ ============
96
+
97
+ ``Dash0Distro`` (``opentelemetry_distro``)
98
+ Runs first, before the configurator. It:
99
+
100
+ * **Gates** the distribution: does nothing if ``DASH0_DISABLE=true`` or if
101
+ ``DASH0_OTEL_COLLECTOR_BASE_URL`` is unset (nowhere to send telemetry). In
102
+ either case it sets ``OTEL_SDK_DISABLED=true`` and skips instrumentation.
103
+ * Defaults all three signals to the pure-Python OTLP/HTTP exporter
104
+ (``otlp_proto_http``, ``http/protobuf``) and points
105
+ ``OTEL_EXPORTER_OTLP_ENDPOINT`` at ``DASH0_OTEL_COLLECTOR_BASE_URL``. The
106
+ pure-Python exporter has no native dependencies, which is what makes the
107
+ distribution safe to inject onto an arbitrary process' ``PYTHONPATH``.
108
+ When a signal's OTLP protocol does not match the transport the shared
109
+ endpoint targets — its port is the *other* protocol's conventional
110
+ default (``4317`` gRPC / ``4318`` HTTP) — a per-signal
111
+ ``OTEL_EXPORTER_OTLP_<SIGNAL>_ENDPOINT`` is derived by rewriting the
112
+ port (appending the ``v1/<signal>`` path for HTTP), so mixed-protocol
113
+ setups and a base URL on the "wrong" default port still export
114
+ correctly. Custom ports and explicit per-signal endpoints are always
115
+ left untouched.
116
+ * Injects detected **resource attributes** into ``OTEL_RESOURCE_ATTRIBUTES``/
117
+ ``OTEL_SERVICE_NAME`` (see below) so the SDK's Resource picks them up.
118
+ * Overrides ``load_instrumentor`` to activate each instrumentor
119
+ **defensively** — a disabled or failing instrumentor is skipped and logged
120
+ rather than aborting auto-instrumentation of the host process.
121
+
122
+ ``Dash0Configurator`` (``opentelemetry_configurator``)
123
+ Runs after the distro. Delegates to the standard OpenTelemetry SDK
124
+ configurator to build the providers/exporters/Resource from the environment,
125
+ then adds two behaviors from the Node.js distribution:
126
+
127
+ * an optional **bootstrap span** emitted once at startup
128
+ (``DASH0_BOOTSTRAP_SPAN=<name>``);
129
+ * optional **graceful flush** on ``SIGTERM``/``SIGINT``
130
+ (``DASH0_FLUSH_ON_SIGTERM_SIGINT=true``) — normal-exit flushing is already
131
+ handled by the SDK's ``atexit`` provider shutdown.
132
+
133
+ Resource detectors (``opentelemetry_resource_detector``)
134
+ The detected resource attributes (see below) as standard SDK resource
135
+ detectors, one per concern so each can be used (or omitted) independently:
136
+
137
+ * ``dash0_distribution`` — ``telemetry.distro.name``/``telemetry.distro.version``;
138
+ * ``dash0_kubernetes`` — ``k8s.pod.uid``;
139
+ * ``dash0_service_name`` — wraps the upstream service detection (a
140
+ process-stable ``service.instance.id`` and ``service.name`` from
141
+ ``OTEL_SERVICE_NAME``) and adds the distro's entrypoint-derived
142
+ ``service.name`` fallback, so there is no need to also list the built-in
143
+ ``service`` detector.
144
+
145
+ They can be referenced via ``OTEL_EXPERIMENTAL_RESOURCE_DETECTORS``, or
146
+ explicitly in a declarative config file under
147
+ ``resource.detection/development.detectors``.
148
+
149
+ Instrumentations
150
+ ================
151
+
152
+ The distribution depends on the full upstream auto-instrumentation set — every
153
+ package enumerated by the ``bootstrap_gen`` module of
154
+ ``opentelemetry-instrumentation`` (the list behind ``opentelemetry-bootstrap``)
155
+ — pinned to a single contrib version, so injected processes need no bootstrap
156
+ step. Instrumentations only activate when their target library is present,
157
+ which makes carrying all of them safe.
158
+
159
+ Deviations from the upstream list:
160
+
161
+ * ``opentelemetry-propagator-aws-xray`` is shipped although upstream's
162
+ bootstrap list only covers instrumentations (the Dash0 operator's
163
+ instrumentation image always carried it).
164
+ * ``opentelemetry-exporter-prometheus``,
165
+ ``opentelemetry-propagator-ot-trace``, and
166
+ ``opentelemetry-instrumentation-aws-lambda`` are deliberately not shipped.
167
+
168
+ ``tests/test_instrumentations.py`` cross-checks the dependency block against
169
+ the installed upstream list, so a contrib version bump that adds or removes
170
+ instrumentations fails CI until the block is re-synced. Dependabot keeps the
171
+ pins themselves moving (all ``opentelemetry-*`` packages are grouped into one
172
+ update, since the contrib instrumentations only resolve when they share one
173
+ contrib version).
174
+
175
+ Resource detection
176
+ ==================
177
+
178
+ Ported from the Node.js distribution's custom detectors:
179
+
180
+ * **Kubernetes pod UID** (``k8s.pod.uid``): confirms it is running in Kubernetes
181
+ (via ``/etc/hosts``), then extracts the pod UID from cgroup v1
182
+ (``/proc/self/mountinfo``) or cgroup v2 (``/proc/self/cgroup``).
183
+ * **Service-name fallback**: if ``OTEL_SERVICE_NAME`` / ``service.name`` are not
184
+ set (and ``DASH0_AUTOMATIC_SERVICE_NAME`` is not ``false``), derives a name
185
+ from the entrypoint script. (Node.js reads ``package.json``; Python has no
186
+ universal equivalent, so this is a best-effort analog.)
187
+ * **Distribution attributes**: ``telemetry.distro.name=dash0-python`` and
188
+ ``telemetry.distro.version``.
189
+
190
+ Existing attributes are never overridden.
191
+
192
+ Dependency policy
193
+ =================
194
+
195
+ The distribution ships a curated set of upstream OpenTelemetry packages, pinned
196
+ exactly in its ``pyproject.toml``: each release is validated against precisely
197
+ those versions, and the code assumes them. Version bumps are deliberate,
198
+ standalone changes. The pins cover the *entire* installed tree: every package
199
+ the distribution pulls in transitively is also declared directly with an exact
200
+ pin, so no version is left to transitive resolution. CI enforces both rules via
201
+ ``scripts/check_pinned_dependencies.py`` (in-repo workspace members are exempt,
202
+ as their version is fixed by the checkout).
203
+
204
+ Injector bootstrap (``sitecustomize.py``)
205
+ =========================================
206
+
207
+ The wheel ships the bootstrap script used for ``PYTHONPATH``-based injection at
208
+ ``dash0/opentelemetry/injector/sitecustomize.py``. It is not meant to be
209
+ imported from that location (importing it executes it); instead, a consumer
210
+ building an injectable tree — such as the dash0-operator's instrumentation
211
+ image —
212
+
213
+ 1. installs the distribution into a self-contained directory:
214
+ ``pip install --target <dir> dash0-opentelemetry``;
215
+ 2. copies the script to ``<dir>/sitecustomize.py``;
216
+ 3. generates ``<dir>/all-dependencies.txt``, a flattened list of every
217
+ requirement of the installed tree (one PEP 508 requirement per line).
218
+
219
+ The `OpenTelemetry injector
220
+ <https://github.com/open-telemetry/opentelemetry-injector>`_ then prepends
221
+ ``<dir>`` to the ``PYTHONPATH`` of the processes to instrument, so Python's
222
+ ``site`` machinery runs the script on interpreter startup. The script
223
+ initializes the auto-instrumentation only after a set of safety checks —
224
+ supported interpreter version, no OpenTelemetry packages already shipped with
225
+ the application (double instrumentation), no version conflicts between the
226
+ application's dependencies and ``all-dependencies.txt`` — and otherwise
227
+ deactivates the injection gracefully, for the current process and its children.
228
+ It also bridges ``OTEL_EXPORTER_OTLP_ENDPOINT`` to
229
+ ``DASH0_OTEL_COLLECTOR_BASE_URL`` when only the former is set. The script stays
230
+ valid Python 2.7+ so it can self-deactivate instead of crashing on interpreters
231
+ the distribution does not support.
232
+
233
+ Environment variables
234
+ =====================
235
+
236
+ ============================================ ====================================================
237
+ Variable Effect
238
+ ============================================ ====================================================
239
+ ``DASH0_OTEL_COLLECTOR_BASE_URL`` (required) Collector base URL; also sets the OTLP endpoint.
240
+ ``DASH0_DISABLE`` ``true`` disables the distribution entirely.
241
+ ``DASH0_AUTOMATIC_SERVICE_NAME`` ``false`` opts out of the service-name fallback.
242
+ ``DASH0_BOOTSTRAP_SPAN`` Emit one span with this name at startup.
243
+ ``DASH0_FLUSH_ON_SIGTERM_SIGINT`` ``true`` flushes telemetry on SIGTERM/SIGINT.
244
+ ============================================ ====================================================
245
+
246
+ Status
247
+ ======
248
+
249
+ Prototype. The ``otlp_proto_http`` exporter is resolved from the in-repo pyproto
250
+ workspace member, published as ``dash0-opentelemetry-exporter-otlp-pyproto-http``
251
+ on the Dash0 package index and on public PyPI. Not yet done: the injector's
252
+ per-``libc`` packaging and the integration/injection test matrix. Unit tests do
253
+ not require a running collector.
@@ -0,0 +1,14 @@
1
+ dash0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ dash0/opentelemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ dash0/opentelemetry/_environment_variables.py,sha256=o4MMVOrtvLifcznY-4MOMuRdcZfW1JHVuPfZ-JkuXsQ,1861
4
+ dash0/opentelemetry/configurator.py,sha256=nCw8_EadlYWKoMRp3SFZ62qYJDKcQPtzbXW859VVc_8,3025
5
+ dash0/opentelemetry/distro.py,sha256=wwTy3bMabxgGkHe0hgxIBy77Gd7FjwuGrk4ewSO9CZU,8901
6
+ dash0/opentelemetry/resource.py,sha256=9SrSjhwIdUC1dUiFkVisI5KF2guI7gekDdiK2h76BAQ,8429
7
+ dash0/opentelemetry/settings.py,sha256=Yxt55kicpAI5j24ENpBXh5p4Am6ePagRlHwlDSuN9gs,956
8
+ dash0/opentelemetry/version.py,sha256=r4xAFihOf72W9TD-lpMi6ntWSTKTP2SlzKP1ytkjRbI,22
9
+ dash0/opentelemetry/injector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ dash0/opentelemetry/injector/sitecustomize.py,sha256=bUM4ODkUpXmPnpLpMsuQD2z8DQW24UTg3KAecLshpsc,14681
11
+ dash0_opentelemetry-0.3.1.dist-info/METADATA,sha256=puaYAfLg5AfhASuRq9W0rdju8TkUeXXHzK3cfdu0qHw,13388
12
+ dash0_opentelemetry-0.3.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
13
+ dash0_opentelemetry-0.3.1.dist-info/entry_points.txt,sha256=LXwsgDWxHJdaSa3V5OMibCQPpiNVMmSwiS97DwEFNhA,441
14
+ dash0_opentelemetry-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,10 @@
1
+ [opentelemetry_configurator]
2
+ dash0 = dash0.opentelemetry.configurator:Dash0Configurator
3
+
4
+ [opentelemetry_distro]
5
+ dash0 = dash0.opentelemetry.distro:Dash0Distro
6
+
7
+ [opentelemetry_resource_detector]
8
+ dash0_distribution = dash0.opentelemetry.resource:Dash0DistributionResourceDetector
9
+ dash0_kubernetes = dash0.opentelemetry.resource:Dash0KubernetesResourceDetector
10
+ dash0_service_name = dash0.opentelemetry.resource:Dash0ServiceNameResourceDetector