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 +0 -0
- dash0/opentelemetry/__init__.py +0 -0
- dash0/opentelemetry/_environment_variables.py +41 -0
- dash0/opentelemetry/configurator.py +77 -0
- dash0/opentelemetry/distro.py +218 -0
- dash0/opentelemetry/injector/__init__.py +0 -0
- dash0/opentelemetry/injector/sitecustomize.py +282 -0
- dash0/opentelemetry/resource.py +239 -0
- dash0/opentelemetry/settings.py +33 -0
- dash0/opentelemetry/version.py +1 -0
- dash0_opentelemetry-0.3.1.dist-info/METADATA +253 -0
- dash0_opentelemetry-0.3.1.dist-info/RECORD +14 -0
- dash0_opentelemetry-0.3.1.dist-info/WHEEL +4 -0
- dash0_opentelemetry-0.3.1.dist-info/entry_points.txt +10 -0
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,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
|