odigos-opentelemetry-python 1.0.80__tar.gz → 1.0.82__tar.gz
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.
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/components.py +47 -35
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/odigos_sampler.py +107 -36
- odigos_opentelemetry_python-1.0.82/initializer/schemas/sampling.py +40 -0
- odigos_opentelemetry_python-1.0.82/initializer/version.py +1 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/SOURCES.txt +2 -0
- odigos_opentelemetry_python-1.0.82/opamp/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/config.py +13 -6
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/http_client.py +90 -75
- odigos_opentelemetry_python-1.0.82/opamp/utils.py +26 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/pyproject.toml +41 -1
- odigos_opentelemetry_python-1.0.80/initializer/version.py +0 -1
- odigos_opentelemetry_python-1.0.80/opamp/utils.py +0 -26
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/LICENSE +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/README.md +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/distro/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/distro/distro.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/distro/instrumentation_registry.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/envs.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/exit_hook.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/lib_handling.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/post_instrument.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/process_resource.py +0 -0
- {odigos_opentelemetry_python-1.0.80/opamp → odigos_opentelemetry_python-1.0.82/initializer/schemas}/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/dependency_links.txt +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/entry_points.txt +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/requires.txt +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/top_level.txt +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/anyvalue_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/health_status.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_registry.py +0 -0
- {odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/setup.cfg +0 -0
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/components.py
RENAMED
|
@@ -27,7 +27,6 @@ from opentelemetry.sdk.resources import OTELResourceDetector
|
|
|
27
27
|
from opentelemetry.sdk.trace import TracerProvider
|
|
28
28
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
29
29
|
from opentelemetry.trace import set_tracer_provider, get_tracer_provider
|
|
30
|
-
from opentelemetry.sdk.trace.sampling import ParentBased
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
# Move Odigos agent paths to the end of sys.path so the application's packages are
|
|
@@ -48,19 +47,19 @@ from opamp.config import Config
|
|
|
48
47
|
from opamp import opamp_registry
|
|
49
48
|
from .distro import instrumentation_registry
|
|
50
49
|
|
|
51
|
-
|
|
52
50
|
from opamp.http_client import OpAMPHTTPClient, MockOpAMPClient
|
|
51
|
+
from opamp.utils import parse_first_message_signals
|
|
53
52
|
|
|
54
53
|
MINIMUM_PYTHON_SUPPORTED_VERSION = (3, 9)
|
|
55
54
|
|
|
56
55
|
_odigos_sampler = None
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
def initialize_components(trace_exporters
|
|
58
|
+
def initialize_components(trace_exporters=False, span_processor=None):
|
|
60
59
|
# Ensure each child process after fork gets a fresh OpAMP client
|
|
61
60
|
# and correct process.* resource attributes.
|
|
62
61
|
os.register_at_fork(
|
|
63
|
-
|
|
62
|
+
after_in_child=_after_in_child,
|
|
64
63
|
) # pylint: disable=protected-access
|
|
65
64
|
|
|
66
65
|
handle_instrumenation_of_sub_processes()
|
|
@@ -69,24 +68,29 @@ def initialize_components(trace_exporters = False, span_processor = None):
|
|
|
69
68
|
client = None
|
|
70
69
|
|
|
71
70
|
try:
|
|
72
|
-
|
|
73
71
|
client = start_opamp_client(opamp_connection_event)
|
|
74
72
|
|
|
75
73
|
## In case of error, e.g during the first connection to the OpAMP server,
|
|
76
74
|
# we will use the default value which is enable traces only.
|
|
77
75
|
if opamp_connection_event.error:
|
|
76
|
+
container_config = None
|
|
78
77
|
supported_signals = {"traceSignal": True}
|
|
79
|
-
initial_sampler_config = None
|
|
78
|
+
initial_sampler_config = None
|
|
79
|
+
initial_remote_config = None
|
|
80
80
|
else:
|
|
81
|
-
|
|
81
|
+
container_config = client.container_config
|
|
82
|
+
supported_signals = parse_first_message_signals(container_config)
|
|
82
83
|
|
|
83
|
-
# Get initial
|
|
84
|
+
# Get initial configs directly from client (only when connection is successful)
|
|
84
85
|
try:
|
|
85
86
|
initial_sampler_config = client.get_initial_sampler_config()
|
|
86
87
|
except AttributeError:
|
|
87
|
-
# Fallback to None to avoid errors
|
|
88
88
|
initial_sampler_config = None
|
|
89
89
|
|
|
90
|
+
try:
|
|
91
|
+
initial_remote_config = client.get_initial_remote_config()
|
|
92
|
+
except AttributeError:
|
|
93
|
+
initial_remote_config = None
|
|
90
94
|
|
|
91
95
|
handle_django_instrumentation()
|
|
92
96
|
|
|
@@ -96,15 +100,20 @@ def initialize_components(trace_exporters = False, span_processor = None):
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
base_resource = (
|
|
99
|
-
OdigosProcessResourceDetector(client.pid).detect()
|
|
100
|
-
.merge(OTELResourceDetector().detect())
|
|
101
|
-
.merge(Resource.create(auto_resource))
|
|
103
|
+
OdigosProcessResourceDetector(client.pid).detect().merge(OTELResourceDetector().detect()).merge(Resource.create(auto_resource))
|
|
102
104
|
)
|
|
103
105
|
|
|
104
106
|
# Wrap in proxy so child processes can refresh automatically
|
|
105
107
|
resource = ProxyResource(base_resource, OdigosProcessResourceDetector)
|
|
106
108
|
|
|
107
|
-
odigos_sampler = initialize_traces_if_enabled(
|
|
109
|
+
odigos_sampler = initialize_traces_if_enabled(
|
|
110
|
+
trace_exporters,
|
|
111
|
+
resource,
|
|
112
|
+
span_processor,
|
|
113
|
+
supported_signals,
|
|
114
|
+
initial_sampler_config,
|
|
115
|
+
initial_remote_config,
|
|
116
|
+
)
|
|
108
117
|
if odigos_sampler is not None:
|
|
109
118
|
client.sampler = odigos_sampler
|
|
110
119
|
|
|
@@ -119,16 +128,22 @@ def initialize_components(trace_exporters = False, span_processor = None):
|
|
|
119
128
|
# Make sure the distro modules are reloaded even if an exception is raised.
|
|
120
129
|
reload_distro_modules()
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
|
|
132
|
+
def initialize_traces_if_enabled(
|
|
133
|
+
trace_exporters,
|
|
134
|
+
resource,
|
|
135
|
+
span_processor=None,
|
|
136
|
+
signals=None,
|
|
137
|
+
initial_sampler_config=None,
|
|
138
|
+
initial_remote_config=None,
|
|
139
|
+
):
|
|
123
140
|
global _odigos_sampler
|
|
124
141
|
# In case collectors signals support receiving traces, initialize the tracer provider with the exporters
|
|
125
142
|
traces_enabled = signals.get("traceSignal", False)
|
|
126
143
|
|
|
127
144
|
if traces_enabled:
|
|
128
|
-
|
|
129
145
|
odigos_sampler = OdigosSampler()
|
|
130
146
|
_odigos_sampler = odigos_sampler
|
|
131
|
-
sampler = ParentBased(odigos_sampler)
|
|
132
147
|
|
|
133
148
|
# Apply initial sampler config if provided (from OpAMP first message)
|
|
134
149
|
if initial_sampler_config is not None:
|
|
@@ -136,7 +151,7 @@ def initialize_traces_if_enabled(trace_exporters, resource, span_processor = Non
|
|
|
136
151
|
|
|
137
152
|
# Exporting using exporters
|
|
138
153
|
if trace_exporters:
|
|
139
|
-
provider = TracerProvider(resource=resource, sampler=
|
|
154
|
+
provider = TracerProvider(resource=resource, sampler=odigos_sampler)
|
|
140
155
|
id_generator_name = sdk_config._get_id_generator()
|
|
141
156
|
id_generator = sdk_config._import_id_generator(id_generator_name)
|
|
142
157
|
provider.id_generator = id_generator
|
|
@@ -147,21 +162,26 @@ def initialize_traces_if_enabled(trace_exporters, resource, span_processor = Non
|
|
|
147
162
|
|
|
148
163
|
for _, exporter_class in exporters.items():
|
|
149
164
|
exporter_args = {}
|
|
150
|
-
provider.add_span_processor(
|
|
151
|
-
BatchSpanProcessor(exporter_class(**exporter_args))
|
|
152
|
-
)
|
|
165
|
+
provider.add_span_processor(BatchSpanProcessor(exporter_class(**exporter_args)))
|
|
153
166
|
|
|
154
167
|
# Exporting using EBPF
|
|
155
168
|
else:
|
|
156
|
-
provider = TracerProvider(resource=resource, sampler=
|
|
169
|
+
provider = TracerProvider(resource=resource, sampler=odigos_sampler)
|
|
157
170
|
set_tracer_provider(provider)
|
|
158
171
|
if span_processor is not None:
|
|
159
172
|
provider.add_span_processor(span_processor)
|
|
160
173
|
|
|
174
|
+
# Bootstrap processor config from the first OpAMP message.
|
|
175
|
+
# The update_conf_cb fires before the TracerProvider exists,
|
|
176
|
+
# so the processor misses the initial config; apply it now.
|
|
177
|
+
if initial_remote_config is not None and span_processor is not None and hasattr(span_processor, 'update_config'):
|
|
178
|
+
span_processor.update_config(initial_remote_config)
|
|
179
|
+
|
|
161
180
|
return odigos_sampler
|
|
162
181
|
|
|
163
182
|
return None
|
|
164
183
|
|
|
184
|
+
|
|
165
185
|
def initialize_metrics_if_enabled(resource, signals):
|
|
166
186
|
# Currently we're not exporting metrics using the OTEL exporter.
|
|
167
187
|
metrics_enabled = False
|
|
@@ -198,7 +218,6 @@ def start_opamp_client(event):
|
|
|
198
218
|
|
|
199
219
|
python_version_supported = is_supported_python_version()
|
|
200
220
|
|
|
201
|
-
|
|
202
221
|
client.start(python_version_supported)
|
|
203
222
|
|
|
204
223
|
# Wait for the opamp first message to be received
|
|
@@ -230,6 +249,7 @@ def start_opamp_client(event):
|
|
|
230
249
|
|
|
231
250
|
return client
|
|
232
251
|
|
|
252
|
+
|
|
233
253
|
def update_agent_config(conf: Config):
|
|
234
254
|
"""
|
|
235
255
|
The function `update_agent_config` checks for a tracer provider and updates its configuration using
|
|
@@ -254,20 +274,14 @@ def update_agent_config(conf: Config):
|
|
|
254
274
|
if 'traces' in conf.sample_config and 'headSampling' in conf.sample_config['traces']:
|
|
255
275
|
head_sampling_config = conf.sample_config['traces']['headSampling']
|
|
256
276
|
|
|
257
|
-
if head_sampling_config and (
|
|
258
|
-
head_sampling_config.get('noisyOperations') is not None
|
|
259
|
-
):
|
|
260
|
-
valid_config = head_sampling_config
|
|
261
|
-
else:
|
|
262
|
-
valid_config = None
|
|
263
|
-
|
|
264
277
|
# Apply config to sampler - sampler will always exist when this callback is called
|
|
265
278
|
# because update_agent_config is only called from OpAMP worker thread after sampler creation
|
|
266
279
|
from opamp import opamp_registry
|
|
280
|
+
|
|
267
281
|
client = opamp_registry.get_client()
|
|
268
282
|
|
|
269
283
|
if client and hasattr(client, 'sampler') and client.sampler:
|
|
270
|
-
client.sampler.update_config(
|
|
284
|
+
client.sampler.update_config(head_sampling_config)
|
|
271
285
|
else:
|
|
272
286
|
# This should not happen in normal operation
|
|
273
287
|
pass
|
|
@@ -281,19 +295,17 @@ def update_agent_config(conf: Config):
|
|
|
281
295
|
# If you attached only one BatchSpanProcessor, _active_span_processor
|
|
282
296
|
# *is* that object. If you attached several, it is a MultiSpanProcessor
|
|
283
297
|
# that wraps them in ._span_processors.
|
|
284
|
-
processors = (
|
|
285
|
-
active_proc._span_processors
|
|
286
|
-
if hasattr(active_proc, "_span_processors")
|
|
287
|
-
else [active_proc]
|
|
288
|
-
)
|
|
298
|
+
processors = active_proc._span_processors if hasattr(active_proc, "_span_processors") else [active_proc]
|
|
289
299
|
|
|
290
300
|
for proc in processors:
|
|
291
301
|
if hasattr(proc, "update_config"):
|
|
292
302
|
proc.update_config(conf)
|
|
293
303
|
|
|
304
|
+
|
|
294
305
|
def is_supported_python_version():
|
|
295
306
|
return sys.version_info >= MINIMUM_PYTHON_SUPPORTED_VERSION
|
|
296
307
|
|
|
308
|
+
|
|
297
309
|
def handle_instrumenation_of_sub_processes():
|
|
298
310
|
# Resolves a path management issue introduced by OpenTelemetry's sitecustomize implementation.
|
|
299
311
|
# OpenTelemetry removed auto_instrumentation from the path to address a specific bug
|
|
@@ -1,85 +1,137 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from threading import Lock
|
|
3
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
4
|
+
|
|
5
|
+
from opentelemetry.context import Context
|
|
6
|
+
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult, _get_parent_trace_state
|
|
1
7
|
from opentelemetry.semconv.attributes import http_attributes as http_attributes_semconv
|
|
2
8
|
from opentelemetry.semconv.attributes import server_attributes as server_attributes_semconv
|
|
3
|
-
from opentelemetry.
|
|
4
|
-
from
|
|
5
|
-
import logging
|
|
9
|
+
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
|
|
10
|
+
from opentelemetry.util.types import Attributes
|
|
6
11
|
|
|
12
|
+
from initializer.schemas.sampling import (
|
|
13
|
+
HeadSamplingConfig,
|
|
14
|
+
HeadSamplingHttpClientOperationMatcher,
|
|
15
|
+
HeadSamplingHttpServerOperationMatcher,
|
|
16
|
+
HeadSamplingOperationMatcher,
|
|
17
|
+
NoisyOperation,
|
|
18
|
+
)
|
|
7
19
|
|
|
8
20
|
# Setup the Sampler logger
|
|
9
21
|
sampler_logger = logging.getLogger('odigos')
|
|
10
22
|
|
|
11
|
-
class OdigosSampler(Sampler):
|
|
12
23
|
|
|
24
|
+
class OdigosSampler(Sampler):
|
|
13
25
|
# For compatibility with 64 bit trace IDs, the sampler checks the 64
|
|
14
26
|
# low-order bits of the trace ID to decide whether to sample a given trace.
|
|
15
27
|
TRACE_ID_LIMIT = (1 << 64) - 1
|
|
16
28
|
|
|
17
29
|
def __init__(self):
|
|
18
30
|
self._lock = Lock()
|
|
19
|
-
self._config = None
|
|
31
|
+
self._config: Optional[HeadSamplingConfig] = None
|
|
32
|
+
|
|
33
|
+
def _not_sampled_decision(self) -> Decision:
|
|
34
|
+
if self._config and self._config.get('spanMetricsMode') == "all-spans":
|
|
35
|
+
return Decision.RECORD_ONLY
|
|
36
|
+
return Decision.DROP
|
|
20
37
|
|
|
21
38
|
def _trace_id_based_sampling(self, trace_id: int, percentage_to_sample: float) -> bool:
|
|
22
39
|
"""Mimic OpenTelemetry's trace ID-based sampling logic with 64-bit range."""
|
|
23
40
|
# Since percentage to sample is in percents and we need a fraction, divide by 100
|
|
24
41
|
fraction = percentage_to_sample / 100
|
|
25
|
-
|
|
42
|
+
|
|
26
43
|
# Calculate the bound based on the fraction using OpenTelemetry's approach
|
|
27
44
|
bound = round(fraction * (self.TRACE_ID_LIMIT + 1))
|
|
28
45
|
# Apply the TRACE_ID_LIMIT mask to ensure 64-bit range and compare it to the bound
|
|
29
46
|
return (trace_id & self.TRACE_ID_LIMIT) < bound
|
|
30
47
|
|
|
31
|
-
def should_sample(
|
|
48
|
+
def should_sample(
|
|
49
|
+
self,
|
|
50
|
+
parent_context: Optional[Context],
|
|
51
|
+
trace_id: int,
|
|
52
|
+
name: str,
|
|
53
|
+
kind: Optional[SpanKind] = None,
|
|
54
|
+
attributes: Attributes = None,
|
|
55
|
+
links: Optional[Sequence["Link"]] = None,
|
|
56
|
+
trace_state: Optional[TraceState] = None,
|
|
57
|
+
) -> "SamplingResult":
|
|
32
58
|
attributes = attributes or {}
|
|
33
59
|
with self._lock:
|
|
60
|
+
parent_span_context = get_current_span(parent_context).get_span_context()
|
|
61
|
+
|
|
62
|
+
# Parent-based logic taken from upstream ParentBased sampler so we can
|
|
63
|
+
# decide to DROP or RECORD_ONLY based on the recordMode configuration.
|
|
64
|
+
# Remote/local parent distinction is removed because both paths used the
|
|
65
|
+
# same not-sampled decision.
|
|
66
|
+
if parent_span_context is not None and parent_span_context.is_valid:
|
|
67
|
+
if parent_span_context.trace_flags.sampled:
|
|
68
|
+
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=_get_parent_trace_state(parent_context))
|
|
69
|
+
else:
|
|
70
|
+
return SamplingResult(self._not_sampled_decision(), trace_state=_get_parent_trace_state(parent_context))
|
|
34
71
|
# sampler_logger.debug(f'Running Should_sample a span with the following attributes: {attributes}')
|
|
35
72
|
|
|
36
73
|
if self._config is None:
|
|
37
74
|
# sampler_logger.debug('No configuration is set, returning RECORD_AND_SAMPLE')
|
|
38
75
|
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=_get_parent_trace_state(parent_context))
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
|
|
77
|
+
is_dry_run: bool = self._config.get('dryRun', False)
|
|
78
|
+
noisy_operations: list[NoisyOperation] = self._config.get('noisyOperations', [])
|
|
79
|
+
# Track the rule with the lowest percentageAtMost so the tracestate entry reports the actual deciding rule's id (first-wins on ties)
|
|
80
|
+
winning_operation: Optional[NoisyOperation] = None
|
|
42
81
|
|
|
43
82
|
for noisy_operation in noisy_operations:
|
|
44
83
|
if noisy_operation.get("disabled"):
|
|
45
84
|
continue
|
|
46
85
|
|
|
47
|
-
percentage_to_sample = noisy_operation.get("percentageAtMost", 0.0)
|
|
86
|
+
percentage_to_sample: float = noisy_operation.get("percentageAtMost", 0.0)
|
|
48
87
|
|
|
49
|
-
http_operation_sample = noisy_operation.get("operation")
|
|
50
|
-
if not http_operation_sample:
|
|
51
|
-
continue
|
|
88
|
+
http_operation_sample: Optional[HeadSamplingOperationMatcher] = noisy_operation.get("operation")
|
|
52
89
|
|
|
53
90
|
# sampler_logger.debug(f'Evaluating noisy operation: {noisy_operation}')
|
|
54
91
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if
|
|
58
|
-
matched =
|
|
92
|
+
# No operation matcher -> rule applies workload-wide (matches every span)
|
|
93
|
+
# This supports Sampling rules that scope by sourceScopes (workload) without specific HTTP matcher
|
|
94
|
+
if not http_operation_sample:
|
|
95
|
+
matched = True
|
|
96
|
+
else: # When we have a granular operation, check it vs the server/client rules specified in the noisy operation
|
|
97
|
+
matched = False
|
|
98
|
+
if http_server_sample := http_operation_sample.get("httpServer"):
|
|
99
|
+
matched = self._match_http_server_sample_rule(http_server_sample, attributes) or matched
|
|
59
100
|
|
|
60
|
-
|
|
61
|
-
|
|
101
|
+
if http_client_sample := http_operation_sample.get("httpClient"):
|
|
102
|
+
matched = self._match_http_client_sample_rule(http_client_sample, attributes) or matched
|
|
62
103
|
|
|
63
104
|
# sampler_logger.debug(f'Noisy operation matched: {matched}, percentage: {percentage_to_sample}')
|
|
64
|
-
if matched
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
if matched and (winning_operation is None or percentage_to_sample < winning_operation.get("percentageAtMost", 0.0)):
|
|
106
|
+
winning_operation = noisy_operation
|
|
107
|
+
|
|
108
|
+
if winning_operation is not None:
|
|
109
|
+
# Build tracestate from the winning rule and attach it to both decisions.
|
|
110
|
+
# Even on DROP the SpanContext (and its trace_state) is propagated to downstream services via the tracestate header,
|
|
111
|
+
# enabling consistent head-sampling and tail-sampler decisions across the trace.
|
|
112
|
+
lowest_percentage = winning_operation.get("percentageAtMost", 0.0)
|
|
113
|
+
sampled = self._trace_id_based_sampling(trace_id, lowest_percentage)
|
|
114
|
+
|
|
115
|
+
dry_run_sampled = sampled if is_dry_run else None
|
|
116
|
+
trace_state = self.build_tracestate(parent_context, winning_operation, dry_run_sampled)
|
|
117
|
+
|
|
118
|
+
# In dry-run mode, never actually drop spans
|
|
119
|
+
if is_dry_run:
|
|
120
|
+
# sampler_logger.debug(f'Dry run [{trace_id}] would_be_sampled={sampled} (lowest_percentage={lowest_percentage})')
|
|
121
|
+
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=trace_state)
|
|
122
|
+
|
|
123
|
+
if sampled:
|
|
72
124
|
# sampler_logger.debug(f'Trace [{trace_id}] is sampled with lowest percentage {lowest_percentage}')
|
|
73
|
-
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=
|
|
125
|
+
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=trace_state)
|
|
74
126
|
else:
|
|
75
127
|
# sampler_logger.debug(f'Trace [{trace_id}] is dropped with lowest percentage {lowest_percentage}')
|
|
76
|
-
return SamplingResult(
|
|
128
|
+
return SamplingResult(self._not_sampled_decision(), trace_state=trace_state)
|
|
77
129
|
|
|
78
|
-
# sampler_logger.debug(
|
|
130
|
+
# sampler_logger.debug('No noisy operation matched, sampling the trace')
|
|
79
131
|
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=_get_parent_trace_state(parent_context))
|
|
80
132
|
|
|
81
133
|
@staticmethod
|
|
82
|
-
def _get_attribute(span_attributes:
|
|
134
|
+
def _get_attribute(span_attributes: Mapping[str, Any], *keys: str):
|
|
83
135
|
"""Return the first non-None value found for the given attribute keys."""
|
|
84
136
|
for key in keys:
|
|
85
137
|
value = span_attributes.get(key)
|
|
@@ -87,7 +139,7 @@ class OdigosSampler(Sampler):
|
|
|
87
139
|
return value
|
|
88
140
|
return None
|
|
89
141
|
|
|
90
|
-
def _match_http_server_sample_rule(self, http_server_rule:
|
|
142
|
+
def _match_http_server_sample_rule(self, http_server_rule: HeadSamplingHttpServerOperationMatcher, span_attributes: Mapping[str, Any]) -> bool:
|
|
91
143
|
"""
|
|
92
144
|
Try to match all the http server's attributes to the definitions of the noisy action
|
|
93
145
|
"""
|
|
@@ -106,7 +158,7 @@ class OdigosSampler(Sampler):
|
|
|
106
158
|
# If we have target, try to match it with the route in the sampling rule
|
|
107
159
|
if target:
|
|
108
160
|
return self._match_rule_route_to_span(http_server_rule["route"], target)
|
|
109
|
-
|
|
161
|
+
|
|
110
162
|
if http_server_rule.get("routePrefix") and not (span_route or "").startswith(http_server_rule["routePrefix"]):
|
|
111
163
|
return False
|
|
112
164
|
if http_server_rule.get("method") and http_server_rule["method"] != span_method:
|
|
@@ -114,7 +166,7 @@ class OdigosSampler(Sampler):
|
|
|
114
166
|
|
|
115
167
|
return True
|
|
116
168
|
|
|
117
|
-
def _match_http_client_sample_rule(self, http_client_rule:
|
|
169
|
+
def _match_http_client_sample_rule(self, http_client_rule: HeadSamplingHttpClientOperationMatcher, span_attributes: Mapping[str, Any]) -> bool:
|
|
118
170
|
"""
|
|
119
171
|
Try to match all the http client's attributes to the definitions of the noisy action
|
|
120
172
|
"""
|
|
@@ -158,11 +210,30 @@ class OdigosSampler(Sampler):
|
|
|
158
210
|
|
|
159
211
|
return True
|
|
160
212
|
|
|
161
|
-
|
|
162
213
|
def get_description(self):
|
|
163
214
|
return "OdigosSampler"
|
|
164
215
|
|
|
165
216
|
def update_config(self, new_config):
|
|
166
217
|
with self._lock:
|
|
167
|
-
# sampler_logger.debug(f'Updating the configuration with the new configuration: {new_config}')
|
|
168
218
|
self._config = new_config
|
|
219
|
+
|
|
220
|
+
def build_tracestate(
|
|
221
|
+
self,
|
|
222
|
+
parent_context: Optional[Context],
|
|
223
|
+
noisy_operation: NoisyOperation,
|
|
224
|
+
dry_run_sampled: Optional[bool] = None,
|
|
225
|
+
) -> TraceState:
|
|
226
|
+
tracestate = _get_parent_trace_state(parent_context)
|
|
227
|
+
|
|
228
|
+
if not tracestate:
|
|
229
|
+
tracestate = TraceState()
|
|
230
|
+
|
|
231
|
+
# Round percentage to 2 decimal places for byte-identical tracestate values
|
|
232
|
+
percentage = round(noisy_operation.get("percentageAtMost", 0.0), 2)
|
|
233
|
+
noisy_op_tracestate_value = f"c:n;dr.p:{percentage};dr.id:{noisy_operation['id']}"
|
|
234
|
+
|
|
235
|
+
if dry_run_sampled is not None:
|
|
236
|
+
noisy_op_tracestate_value += ";dry:t" if dry_run_sampled else ";dry:f"
|
|
237
|
+
tracestate = tracestate.add("odigos", noisy_op_tracestate_value)
|
|
238
|
+
|
|
239
|
+
return tracestate
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Typed shapes for the head-sampling configuration sent by the Odigos OpAMP server.
|
|
2
|
+
|
|
3
|
+
Mirrors the Go schema in odigos/common/api/sampling (matchers_head.go, sampling.go).
|
|
4
|
+
All fields are `omitempty` on the Go side (except NoisyOperation.id), so every
|
|
5
|
+
key is treated as optional on the wire — hence `total=False` on every TypedDict.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HeadSamplingHttpServerOperationMatcher(TypedDict, total=False):
|
|
12
|
+
route: str
|
|
13
|
+
routePrefix: str
|
|
14
|
+
method: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HeadSamplingHttpClientOperationMatcher(TypedDict, total=False):
|
|
18
|
+
serverAddress: str
|
|
19
|
+
templatedPath: str
|
|
20
|
+
templatedPathPrefix: str
|
|
21
|
+
method: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HeadSamplingOperationMatcher(TypedDict, total=False):
|
|
25
|
+
httpServer: HeadSamplingHttpServerOperationMatcher
|
|
26
|
+
httpClient: HeadSamplingHttpClientOperationMatcher
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoisyOperation(TypedDict, total=False):
|
|
30
|
+
id: str
|
|
31
|
+
name: str
|
|
32
|
+
disabled: bool
|
|
33
|
+
operation: HeadSamplingOperationMatcher
|
|
34
|
+
percentageAtMost: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HeadSamplingConfig(TypedDict, total=False):
|
|
38
|
+
dryRun: bool
|
|
39
|
+
spanMetricsMode: str
|
|
40
|
+
noisyOperations: list[NoisyOperation]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "1.0.82"
|
|
@@ -13,6 +13,8 @@ initializer/version.py
|
|
|
13
13
|
initializer/distro/__init__.py
|
|
14
14
|
initializer/distro/distro.py
|
|
15
15
|
initializer/distro/instrumentation_registry.py
|
|
16
|
+
initializer/schemas/__init__.py
|
|
17
|
+
initializer/schemas/sampling.py
|
|
16
18
|
odigos_opentelemetry_python.egg-info/PKG-INFO
|
|
17
19
|
odigos_opentelemetry_python.egg-info/SOURCES.txt
|
|
18
20
|
odigos_opentelemetry_python.egg-info/dependency_links.txt
|
|
File without changes
|
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from dataclasses import dataclass, fields, field
|
|
3
|
-
from typing import Any, Dict, Type, TypeVar, get_type_hints
|
|
3
|
+
from typing import Any, Dict, Optional, Type, TypeVar, get_type_hints
|
|
4
4
|
|
|
5
5
|
T = TypeVar("T")
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
### Configuration structs ###
|
|
8
9
|
class DefaultNoneMixin:
|
|
9
|
-
#__getattr__ is only invoked when the normal attribute lookup (__dict__, class attributes, etc.) fails.
|
|
10
|
+
# __getattr__ is only invoked when the normal attribute lookup (__dict__, class attributes, etc.) fails.
|
|
10
11
|
# Prevent raising a ValueError when accessing a non existing key
|
|
11
12
|
# It'll save us multiple try catch blocks in the future
|
|
12
13
|
def __getattr__(self, name: str):
|
|
13
14
|
return None
|
|
14
15
|
|
|
16
|
+
|
|
15
17
|
@dataclass
|
|
16
18
|
class CodeAttributes(DefaultNoneMixin):
|
|
17
|
-
column:
|
|
18
|
-
file_path:
|
|
19
|
-
function:
|
|
19
|
+
column: bool = False
|
|
20
|
+
file_path: bool = False
|
|
21
|
+
function: bool = False
|
|
20
22
|
line_number: bool = False
|
|
21
|
-
namespace:
|
|
23
|
+
namespace: bool = False
|
|
22
24
|
stack_trace: bool = False
|
|
23
25
|
|
|
26
|
+
|
|
24
27
|
@dataclass
|
|
25
28
|
class Config(DefaultNoneMixin):
|
|
26
29
|
code_attributes: CodeAttributes = field(default_factory=CodeAttributes)
|
|
27
30
|
sample_config: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
span_metrics_mode: Optional[str] = None
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
### Helpers ###
|
|
30
35
|
|
|
36
|
+
|
|
31
37
|
def camel_to_snake(name: str) -> str:
|
|
32
38
|
"""Convert camelCase or PascalCase to snake_case."""
|
|
33
39
|
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
|
34
40
|
return s1.lower()
|
|
35
41
|
|
|
42
|
+
|
|
36
43
|
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
|
|
37
44
|
"""
|
|
38
45
|
Instantiate the dataclass `cls` from `data`:
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/http_client.py
RENAMED
|
@@ -33,31 +33,32 @@ _debugger_instance = None
|
|
|
33
33
|
env_var_mappings = {
|
|
34
34
|
"ODIGOS_WORKLOAD_NAMESPACE": ResourceAttributes.K8S_NAMESPACE_NAME,
|
|
35
35
|
"ODIGOS_CONTAINER_NAME": ResourceAttributes.K8S_CONTAINER_NAME,
|
|
36
|
-
"ODIGOS_POD_NAME": ResourceAttributes.K8S_POD_NAME
|
|
36
|
+
"ODIGOS_POD_NAME": ResourceAttributes.K8S_POD_NAME,
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
|
|
39
40
|
class OpAMPHTTPClient:
|
|
40
41
|
def __init__(self, opamp_connection_event, condition: threading.Condition):
|
|
41
42
|
self.server_host = os.getenv('ODIGOS_OPAMP_SERVER_HOST')
|
|
42
43
|
self.server_url = f"http://{self.server_host}/v1/opamp"
|
|
43
|
-
self.
|
|
44
|
+
self.container_config = {}
|
|
44
45
|
self.running = True
|
|
45
46
|
self.condition = condition
|
|
46
47
|
self.opamp_connection_event = opamp_connection_event
|
|
47
48
|
self.next_sequence_num = 0
|
|
48
49
|
self.instance_uid = uuid7().__str__()
|
|
49
50
|
self.remote_config_status = None
|
|
50
|
-
self.sampler = None
|
|
51
|
+
self.sampler = None # OdigosSampler instance
|
|
51
52
|
self.pid = os.getpid()
|
|
52
|
-
self.update_conf_cb = None
|
|
53
|
-
self._initial_sampler_config = None
|
|
53
|
+
self.update_conf_cb = None # Callback for configuration updates in the processor
|
|
54
|
+
self._initial_sampler_config = None # Store initial sampler config for direct access
|
|
55
|
+
self._initial_remote_config = None # Store initial full Config for processor bootstrap
|
|
54
56
|
|
|
55
57
|
def __repr__(self):
|
|
56
58
|
return f"<OpAMPHTTPClient instance_uid={self.instance_uid} pid={self.pid}>"
|
|
57
59
|
|
|
58
60
|
def start(self, python_version_supported: bool = None):
|
|
59
61
|
if not python_version_supported:
|
|
60
|
-
|
|
61
62
|
python_version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
|
|
62
63
|
error_message = f"Opentelemetry SDK require Python in version 3.8 or higher [{python_version} is not supported]"
|
|
63
64
|
|
|
@@ -101,7 +102,11 @@ class OpAMPHTTPClient:
|
|
|
101
102
|
agent_disconnect = self.get_agent_disconnect()
|
|
102
103
|
agent_failure_message.agent_disconnect.CopyFrom(agent_disconnect)
|
|
103
104
|
|
|
104
|
-
agent_health = self.get_agent_health(
|
|
105
|
+
agent_health = self.get_agent_health(
|
|
106
|
+
component_health=component_health,
|
|
107
|
+
last_error=error_message,
|
|
108
|
+
status=AgentHealthStatus.AGENT_FAILURE.value,
|
|
109
|
+
)
|
|
105
110
|
agent_failure_message.health.CopyFrom(agent_health)
|
|
106
111
|
|
|
107
112
|
return agent_failure_message
|
|
@@ -116,7 +121,11 @@ class OpAMPHTTPClient:
|
|
|
116
121
|
agent_disconnect = self.get_agent_disconnect()
|
|
117
122
|
first_disconnect_message.agent_disconnect.CopyFrom(agent_disconnect)
|
|
118
123
|
|
|
119
|
-
agent_health = self.get_agent_health(
|
|
124
|
+
agent_health = self.get_agent_health(
|
|
125
|
+
component_health=False,
|
|
126
|
+
last_error=error_message,
|
|
127
|
+
status=AgentHealthStatus.UNSUPPORTED_RUNTIME_VERSION.value,
|
|
128
|
+
)
|
|
120
129
|
first_disconnect_message.health.CopyFrom(agent_health)
|
|
121
130
|
|
|
122
131
|
self.send_agent_to_server_message(first_disconnect_message)
|
|
@@ -127,9 +136,19 @@ class OpAMPHTTPClient:
|
|
|
127
136
|
for attempt in range(1, max_retries + 1):
|
|
128
137
|
try:
|
|
129
138
|
# Send first message to OpAMP server, Health is false as the component is not initialized
|
|
130
|
-
agent_health = self.get_agent_health(
|
|
139
|
+
agent_health = self.get_agent_health(
|
|
140
|
+
component_health=False,
|
|
141
|
+
last_error="Connecting to Odigos configuration server",
|
|
142
|
+
status=AgentHealthStatus.STARTING.value,
|
|
143
|
+
)
|
|
131
144
|
agent_description = self.get_agent_description()
|
|
132
|
-
first_message_server_to_agent = self.send_agent_to_server_message(
|
|
145
|
+
first_message_server_to_agent = self.send_agent_to_server_message(
|
|
146
|
+
opamp_pb2.AgentToServer(
|
|
147
|
+
agent_description=agent_description,
|
|
148
|
+
health=agent_health,
|
|
149
|
+
remote_config_status=opamp_pb2.RemoteConfigStatus(),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
133
152
|
|
|
134
153
|
# Check if the response of the first message is empty
|
|
135
154
|
# It may happen if OpAMPServer is not available
|
|
@@ -147,12 +166,12 @@ class OpAMPHTTPClient:
|
|
|
147
166
|
# The default config is preloaded when the EBPFSpanProcessor is initialized
|
|
148
167
|
pass
|
|
149
168
|
|
|
150
|
-
|
|
151
|
-
self.signals = utils.parse_first_message_signals(sdk_config)
|
|
169
|
+
self.container_config = utils.get_container_config(first_message_server_to_agent.remote_config.config.config_map)
|
|
152
170
|
|
|
153
|
-
# Store initial
|
|
171
|
+
# Store initial configs for direct access (TracerProvider doesn't exist yet)
|
|
154
172
|
remote_config = self.get_remote_config(first_message_server_to_agent)
|
|
155
173
|
self._initial_sampler_config = self._extract_sampler_config(remote_config)
|
|
174
|
+
self._initial_remote_config = remote_config
|
|
156
175
|
|
|
157
176
|
# Send healthy message to OpAMP server
|
|
158
177
|
agent_health = self.get_agent_health(component_health=True, status=AgentHealthStatus.HEALTHY.value)
|
|
@@ -165,7 +184,7 @@ class OpAMPHTTPClient:
|
|
|
165
184
|
return
|
|
166
185
|
|
|
167
186
|
except Exception as e:
|
|
168
|
-
|
|
187
|
+
print(f"[send_first_message_with_retry] Error: {e}", flush=True)
|
|
169
188
|
pass
|
|
170
189
|
|
|
171
190
|
if attempt < max_retries:
|
|
@@ -179,10 +198,19 @@ class OpAMPHTTPClient:
|
|
|
179
198
|
|
|
180
199
|
# If OpAMP server is throttling, it might not respond in the OpAMP client timeout period (5s).
|
|
181
200
|
# In this case, we're trying to send a message to the OpAMP server to indicate that OpAMP is not available.
|
|
182
|
-
agent_health = self.get_agent_health(
|
|
183
|
-
|
|
201
|
+
agent_health = self.get_agent_health(
|
|
202
|
+
component_health=False,
|
|
203
|
+
last_error="Failed to connect to Odigos configuration server - dynamic configuration unavailable",
|
|
204
|
+
status=AgentHealthStatus.NO_CONNECTION_TO_OPAMP_SERVER.value,
|
|
205
|
+
)
|
|
184
206
|
agent_description = self.get_agent_description()
|
|
185
|
-
self.send_agent_to_server_message(
|
|
207
|
+
self.send_agent_to_server_message(
|
|
208
|
+
opamp_pb2.AgentToServer(
|
|
209
|
+
agent_description=agent_description,
|
|
210
|
+
health=agent_health,
|
|
211
|
+
remote_config_status=opamp_pb2.RemoteConfigStatus(),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
186
214
|
|
|
187
215
|
def worker(self):
|
|
188
216
|
while self.running:
|
|
@@ -218,8 +246,7 @@ class OpAMPHTTPClient:
|
|
|
218
246
|
# The default config is preloaded when the EBPFSpanProcessor is initialized
|
|
219
247
|
pass
|
|
220
248
|
|
|
221
|
-
except requests_odigos.RequestException
|
|
222
|
-
# opamp_logger.error(f"Error fetching data: {e}")
|
|
249
|
+
except requests_odigos.RequestException:
|
|
223
250
|
pass
|
|
224
251
|
self.condition.wait(30)
|
|
225
252
|
|
|
@@ -231,16 +258,9 @@ class OpAMPHTTPClient:
|
|
|
231
258
|
component_health_map = {}
|
|
232
259
|
|
|
233
260
|
for lib_name, lib_details in instrumented_libraries.items():
|
|
234
|
-
component_health_map[lib_name] = opamp_pb2.ComponentHealth(
|
|
235
|
-
healthy=True,
|
|
236
|
-
status=json.dumps(lib_details)
|
|
237
|
-
)
|
|
261
|
+
component_health_map[lib_name] = opamp_pb2.ComponentHealth(healthy=True, status=json.dumps(lib_details))
|
|
238
262
|
|
|
239
|
-
health_msg = opamp_pb2.ComponentHealth(
|
|
240
|
-
healthy=True,
|
|
241
|
-
status="instrumentation libraries report",
|
|
242
|
-
component_health_map=component_health_map
|
|
243
|
-
)
|
|
263
|
+
health_msg = opamp_pb2.ComponentHealth(healthy=True, status="instrumentation libraries report", component_health_map=component_health_map)
|
|
244
264
|
|
|
245
265
|
self.send_agent_to_server_message(opamp_pb2.AgentToServer(health=health_msg))
|
|
246
266
|
|
|
@@ -286,7 +306,7 @@ class OpAMPHTTPClient:
|
|
|
286
306
|
|
|
287
307
|
inner = decoded_map.get("", None)
|
|
288
308
|
if inner is None:
|
|
289
|
-
config = Config()
|
|
309
|
+
config = Config() # Return default values for config
|
|
290
310
|
else:
|
|
291
311
|
config = from_dict(Config, inner)
|
|
292
312
|
|
|
@@ -295,18 +315,22 @@ class OpAMPHTTPClient:
|
|
|
295
315
|
if sample_config is not None:
|
|
296
316
|
config.sample_config = sample_config
|
|
297
317
|
|
|
318
|
+
try:
|
|
319
|
+
config.span_metrics_mode = sample_config['traces']['headSampling']['spanMetricsMode']
|
|
320
|
+
except (KeyError, TypeError):
|
|
321
|
+
pass
|
|
322
|
+
|
|
298
323
|
return config
|
|
299
324
|
|
|
300
|
-
def send_heartbeat(self) -> opamp_pb2.ServerToAgent:
|
|
325
|
+
def send_heartbeat(self) -> opamp_pb2.ServerToAgent: # type: ignore
|
|
301
326
|
# opamp_logger.debug("Sending heartbeat to OpAMP server...")
|
|
302
327
|
try:
|
|
303
328
|
agent_to_server = opamp_pb2.AgentToServer(remote_config_status=self.remote_config_status)
|
|
304
329
|
return self.send_agent_to_server_message(agent_to_server)
|
|
305
|
-
except requests_odigos.RequestException
|
|
306
|
-
# opamp_logger.error(f"Error sending heartbeat to OpAMP server: {e}")
|
|
330
|
+
except requests_odigos.RequestException:
|
|
307
331
|
pass
|
|
308
332
|
|
|
309
|
-
def get_agent_description(self) -> opamp_pb2.AgentDescription:
|
|
333
|
+
def get_agent_description(self) -> opamp_pb2.AgentDescription: # type: ignore
|
|
310
334
|
# The "DISABLE_OPAMP_CLIENT" environment variable is defined only in our VMs environments.
|
|
311
335
|
# Here we use it exclusively to distinguish between virtual machine and Kubernetes environments.
|
|
312
336
|
#
|
|
@@ -325,42 +349,24 @@ class OpAMPHTTPClient:
|
|
|
325
349
|
process_id_key = PROCESS_VPID
|
|
326
350
|
|
|
327
351
|
identifying_attributes = [
|
|
328
|
-
anyvalue_pb2.KeyValue(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
),
|
|
332
|
-
anyvalue_pb2.KeyValue(
|
|
333
|
-
key=process_id_key,
|
|
334
|
-
value=anyvalue_pb2.AnyValue(int_value=self.pid)
|
|
335
|
-
),
|
|
336
|
-
anyvalue_pb2.KeyValue(
|
|
337
|
-
key=ResourceAttributes.TELEMETRY_SDK_LANGUAGE,
|
|
338
|
-
value=anyvalue_pb2.AnyValue(string_value="python")
|
|
339
|
-
)
|
|
352
|
+
anyvalue_pb2.KeyValue(key=ResourceAttributes.SERVICE_INSTANCE_ID, value=anyvalue_pb2.AnyValue(string_value=self.instance_uid)),
|
|
353
|
+
anyvalue_pb2.KeyValue(key=process_id_key, value=anyvalue_pb2.AnyValue(int_value=self.pid)),
|
|
354
|
+
anyvalue_pb2.KeyValue(key=ResourceAttributes.TELEMETRY_SDK_LANGUAGE, value=anyvalue_pb2.AnyValue(string_value="python")),
|
|
340
355
|
]
|
|
341
356
|
|
|
342
357
|
# Add additional attributes from environment variables
|
|
343
358
|
for env_var, attribute_key in env_var_mappings.items():
|
|
344
359
|
value = os.environ.get(env_var)
|
|
345
360
|
if value:
|
|
346
|
-
identifying_attributes.append(
|
|
347
|
-
anyvalue_pb2.KeyValue(
|
|
348
|
-
key=attribute_key,
|
|
349
|
-
value=anyvalue_pb2.AnyValue(string_value=value)
|
|
350
|
-
)
|
|
351
|
-
)
|
|
361
|
+
identifying_attributes.append(anyvalue_pb2.KeyValue(key=attribute_key, value=anyvalue_pb2.AnyValue(string_value=value)))
|
|
352
362
|
|
|
353
|
-
return opamp_pb2.AgentDescription(
|
|
354
|
-
identifying_attributes=identifying_attributes,
|
|
355
|
-
non_identifying_attributes=[]
|
|
356
|
-
)
|
|
363
|
+
return opamp_pb2.AgentDescription(identifying_attributes=identifying_attributes, non_identifying_attributes=[])
|
|
357
364
|
|
|
358
|
-
def get_agent_disconnect(self) -> opamp_pb2.AgentDisconnect:
|
|
365
|
+
def get_agent_disconnect(self) -> opamp_pb2.AgentDisconnect: # type: ignore
|
|
359
366
|
return opamp_pb2.AgentDisconnect()
|
|
360
367
|
|
|
361
|
-
def get_agent_health(self, component_health: bool = None, last_error
|
|
362
|
-
health = opamp_pb2.ComponentHealth(
|
|
363
|
-
)
|
|
368
|
+
def get_agent_health(self, component_health: bool = None, last_error: str = None, status: str = None) -> opamp_pb2.ComponentHealth: # type: ignore
|
|
369
|
+
health = opamp_pb2.ComponentHealth()
|
|
364
370
|
if component_health is not None:
|
|
365
371
|
health.healthy = component_health
|
|
366
372
|
if last_error is not None:
|
|
@@ -370,8 +376,7 @@ class OpAMPHTTPClient:
|
|
|
370
376
|
|
|
371
377
|
return health
|
|
372
378
|
|
|
373
|
-
|
|
374
|
-
def send_agent_to_server_message(self, message: opamp_pb2.AgentToServer) -> opamp_pb2.ServerToAgent: # type: ignore
|
|
379
|
+
def send_agent_to_server_message(self, message: opamp_pb2.AgentToServer) -> opamp_pb2.ServerToAgent: # type: ignore
|
|
375
380
|
|
|
376
381
|
message.instance_uid = self.instance_uid.encode('utf-8')
|
|
377
382
|
message.sequence_num = self.next_sequence_num
|
|
@@ -417,10 +422,20 @@ class OpAMPHTTPClient:
|
|
|
417
422
|
self.running = False
|
|
418
423
|
# opamp_logger.info("Sending agent disconnect message to OpAMP server...")
|
|
419
424
|
if custom_failure_message:
|
|
420
|
-
disconnect_message = self.get_agent_failure_disconnect_message(
|
|
425
|
+
disconnect_message = self.get_agent_failure_disconnect_message(
|
|
426
|
+
error_message=custom_failure_message,
|
|
427
|
+
component_health=component_health,
|
|
428
|
+
)
|
|
421
429
|
else:
|
|
422
|
-
agent_health = self.get_agent_health(
|
|
423
|
-
|
|
430
|
+
agent_health = self.get_agent_health(
|
|
431
|
+
component_health=component_health,
|
|
432
|
+
last_error="Python runtime is exiting",
|
|
433
|
+
status=AgentHealthStatus.TERMINATED.value,
|
|
434
|
+
)
|
|
435
|
+
disconnect_message = opamp_pb2.AgentToServer(
|
|
436
|
+
agent_disconnect=opamp_pb2.AgentDisconnect(),
|
|
437
|
+
health=agent_health,
|
|
438
|
+
)
|
|
424
439
|
|
|
425
440
|
with self.condition:
|
|
426
441
|
self.condition.notify_all()
|
|
@@ -428,7 +443,7 @@ class OpAMPHTTPClient:
|
|
|
428
443
|
|
|
429
444
|
self.send_agent_to_server_message(disconnect_message)
|
|
430
445
|
|
|
431
|
-
def update_remote_config_status(self, server_to_agent: opamp_pb2.ServerToAgent) -> bool:
|
|
446
|
+
def update_remote_config_status(self, server_to_agent: opamp_pb2.ServerToAgent) -> bool: # type: ignore
|
|
432
447
|
if server_to_agent.HasField("remote_config"):
|
|
433
448
|
remote_config_hash = server_to_agent.remote_config.config_hash
|
|
434
449
|
remote_config_status = opamp_pb2.RemoteConfigStatus(last_remote_config_hash=remote_config_hash)
|
|
@@ -448,20 +463,16 @@ class OpAMPHTTPClient:
|
|
|
448
463
|
if 'traces' not in remote_config.sample_config or 'headSampling' not in remote_config.sample_config['traces']:
|
|
449
464
|
return None
|
|
450
465
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
# Check if we have valid sampling config (same validation as update_agent_config)
|
|
454
|
-
if head_sampling_config and (
|
|
455
|
-
head_sampling_config.get('noisyOperations') is not None
|
|
456
|
-
):
|
|
457
|
-
return head_sampling_config
|
|
458
|
-
else:
|
|
459
|
-
return None
|
|
466
|
+
return remote_config.sample_config['traces']['headSampling'] or None
|
|
460
467
|
|
|
461
468
|
def get_initial_sampler_config(self):
|
|
462
469
|
"""Get the sampler config from the initial OpAMP message"""
|
|
463
470
|
return self._initial_sampler_config
|
|
464
471
|
|
|
472
|
+
def get_initial_remote_config(self):
|
|
473
|
+
"""Get the full Config from the initial OpAMP message"""
|
|
474
|
+
return self._initial_remote_config
|
|
475
|
+
|
|
465
476
|
|
|
466
477
|
# Mock client class for non-OpAMP installations
|
|
467
478
|
# This class simulates the OpAMP client when the OpAMP server is not available.
|
|
@@ -469,7 +480,7 @@ class OpAMPHTTPClient:
|
|
|
469
480
|
class MockOpAMPClient:
|
|
470
481
|
def __init__(self, opamp_connection_event, *args, **kwargs):
|
|
471
482
|
self.pid = os.getpid()
|
|
472
|
-
self.
|
|
483
|
+
self.container_config = {"traces": {}}
|
|
473
484
|
self.sampler = None # For compatibility
|
|
474
485
|
opamp_connection_event.event.set()
|
|
475
486
|
|
|
@@ -477,5 +488,9 @@ class MockOpAMPClient:
|
|
|
477
488
|
"""Mock client has no sampler config"""
|
|
478
489
|
return None
|
|
479
490
|
|
|
491
|
+
def get_initial_remote_config(self):
|
|
492
|
+
"""Mock client has no remote config"""
|
|
493
|
+
return None
|
|
494
|
+
|
|
480
495
|
def shutdown(self, custom_failure_message=None):
|
|
481
496
|
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
def get_container_config(config_map):
|
|
4
|
+
"""
|
|
5
|
+
Extracts and parses the container configuration from the config map.
|
|
6
|
+
"""
|
|
7
|
+
if "container_config" not in config_map:
|
|
8
|
+
return {}
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
return json.loads(config_map["container_config"].body)
|
|
12
|
+
except json.JSONDecodeError:
|
|
13
|
+
return {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_first_message_signals(container_config): # type: ignore
|
|
17
|
+
"""
|
|
18
|
+
Parses the trace, logs, and metrics signals from the container config.
|
|
19
|
+
Signal is considered enabled if its key exists in the config.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"traceSignal": "traces" in container_config,
|
|
24
|
+
"metricsSignal": "metrics" in container_config,
|
|
25
|
+
"logsSignal": "logs" in container_config,
|
|
26
|
+
}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "odigos-opentelemetry-python"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.82"
|
|
8
8
|
description = "Odigos Initializer for Python OpenTelemetry Components"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
authors = [
|
|
@@ -92,6 +92,13 @@ test = [
|
|
|
92
92
|
"pytest >= 8.0",
|
|
93
93
|
]
|
|
94
94
|
|
|
95
|
+
[dependency-groups]
|
|
96
|
+
dev = [
|
|
97
|
+
"ruff >= 0.15.12",
|
|
98
|
+
"ty >= 0.0.33",
|
|
99
|
+
"pre-commit >= 3.5.0",
|
|
100
|
+
]
|
|
101
|
+
|
|
95
102
|
[project.entry-points.opentelemetry_post_instrument]
|
|
96
103
|
odigos_post = "initializer.post_instrument:run_post_hook"
|
|
97
104
|
|
|
@@ -122,3 +129,36 @@ include = [
|
|
|
122
129
|
"opentelemetry",
|
|
123
130
|
"opentelemetry.*",
|
|
124
131
|
]
|
|
132
|
+
|
|
133
|
+
[tool.ruff]
|
|
134
|
+
line-length = 150
|
|
135
|
+
target-version = "py39"
|
|
136
|
+
extend-exclude = [
|
|
137
|
+
".venv",
|
|
138
|
+
"dist",
|
|
139
|
+
"build",
|
|
140
|
+
"*.egg-info",
|
|
141
|
+
"tests/integration/configurator",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
[tool.ruff.lint]
|
|
145
|
+
# Default rule set: pycodestyle errors (E) + pyflakes (F).
|
|
146
|
+
select = ["E", "F"]
|
|
147
|
+
|
|
148
|
+
[tool.ruff.lint.per-file-ignores]
|
|
149
|
+
"initializer/components.py" = ["E402"]
|
|
150
|
+
|
|
151
|
+
[tool.ruff.format]
|
|
152
|
+
quote-style = "preserve"
|
|
153
|
+
indent-style = "space"
|
|
154
|
+
|
|
155
|
+
[tool.ty.environment]
|
|
156
|
+
python-version = "3.9"
|
|
157
|
+
|
|
158
|
+
[tool.ty.src]
|
|
159
|
+
exclude = [
|
|
160
|
+
".venv",
|
|
161
|
+
"dist",
|
|
162
|
+
"build",
|
|
163
|
+
"tests/integration/configurator",
|
|
164
|
+
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "1.0.80"
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
def get_sdk_config(config_map):
|
|
4
|
-
"""
|
|
5
|
-
Extracts and parses the SDK configuration from the config map.
|
|
6
|
-
"""
|
|
7
|
-
if "SDK" not in config_map:
|
|
8
|
-
return {}
|
|
9
|
-
|
|
10
|
-
try:
|
|
11
|
-
return json.loads(config_map["SDK"].body)
|
|
12
|
-
except json.JSONDecodeError:
|
|
13
|
-
return {}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def parse_first_message_signals(sdk_config): # type: ignore
|
|
18
|
-
"""
|
|
19
|
-
Parses the trace, logs, and metrics signals configuration from the SDK config.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
"traceSignal": sdk_config.get("traceSignal", {}).get("enabled", False),
|
|
24
|
-
"metricsSignal": sdk_config.get("metricsSignal", {}).get("enabled", False),
|
|
25
|
-
"logsSignal": sdk_config.get("logsSignal", {}).get("enabled", False),
|
|
26
|
-
}
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/envs.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/initializer/exit_hook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/anyvalue_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/health_status.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.80 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_registry.py
RENAMED
|
File without changes
|
|
File without changes
|