odigos-opentelemetry-python 1.0.81__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.81 → odigos_opentelemetry_python-1.0.82}/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/components.py +47 -35
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/odigos_sampler.py +18 -3
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/schemas/sampling.py +1 -0
- odigos_opentelemetry_python-1.0.82/initializer/version.py +1 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/config.py +13 -6
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/http_client.py +90 -75
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/pyproject.toml +4 -1
- odigos_opentelemetry_python-1.0.81/initializer/version.py +0 -1
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/LICENSE +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/README.md +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/distro/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/distro/distro.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/distro/instrumentation_registry.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/envs.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/exit_hook.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/lib_handling.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/post_instrument.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/process_resource.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/initializer/schemas/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/SOURCES.txt +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/dependency_links.txt +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/entry_points.txt +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/requires.txt +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/odigos_opentelemetry_python.egg-info/top_level.txt +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/anyvalue_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/health_status.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_registry.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/utils.py +0 -0
- {odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/setup.cfg +0 -0
{odigos_opentelemetry_python-1.0.81 → 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
|
|
@@ -6,7 +6,7 @@ from opentelemetry.context import Context
|
|
|
6
6
|
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult, _get_parent_trace_state
|
|
7
7
|
from opentelemetry.semconv.attributes import http_attributes as http_attributes_semconv
|
|
8
8
|
from opentelemetry.semconv.attributes import server_attributes as server_attributes_semconv
|
|
9
|
-
from opentelemetry.trace import Link, SpanKind, TraceState
|
|
9
|
+
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
|
|
10
10
|
from opentelemetry.util.types import Attributes
|
|
11
11
|
|
|
12
12
|
from initializer.schemas.sampling import (
|
|
@@ -30,6 +30,11 @@ class OdigosSampler(Sampler):
|
|
|
30
30
|
self._lock = Lock()
|
|
31
31
|
self._config: Optional[HeadSamplingConfig] = None
|
|
32
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
|
|
37
|
+
|
|
33
38
|
def _trace_id_based_sampling(self, trace_id: int, percentage_to_sample: float) -> bool:
|
|
34
39
|
"""Mimic OpenTelemetry's trace ID-based sampling logic with 64-bit range."""
|
|
35
40
|
# Since percentage to sample is in percents and we need a fraction, divide by 100
|
|
@@ -52,6 +57,17 @@ class OdigosSampler(Sampler):
|
|
|
52
57
|
) -> "SamplingResult":
|
|
53
58
|
attributes = attributes or {}
|
|
54
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))
|
|
55
71
|
# sampler_logger.debug(f'Running Should_sample a span with the following attributes: {attributes}')
|
|
56
72
|
|
|
57
73
|
if self._config is None:
|
|
@@ -109,7 +125,7 @@ class OdigosSampler(Sampler):
|
|
|
109
125
|
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=trace_state)
|
|
110
126
|
else:
|
|
111
127
|
# sampler_logger.debug(f'Trace [{trace_id}] is dropped with lowest percentage {lowest_percentage}')
|
|
112
|
-
return SamplingResult(
|
|
128
|
+
return SamplingResult(self._not_sampled_decision(), trace_state=trace_state)
|
|
113
129
|
|
|
114
130
|
# sampler_logger.debug('No noisy operation matched, sampling the trace')
|
|
115
131
|
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=_get_parent_trace_state(parent_context))
|
|
@@ -199,7 +215,6 @@ class OdigosSampler(Sampler):
|
|
|
199
215
|
|
|
200
216
|
def update_config(self, new_config):
|
|
201
217
|
with self._lock:
|
|
202
|
-
# sampler_logger.debug(f'Updating the configuration with the new configuration: {new_config}')
|
|
203
218
|
self._config = new_config
|
|
204
219
|
|
|
205
220
|
def build_tracestate(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "1.0.82"
|
|
@@ -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.81 → 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
|
-
container_config = utils.get_container_config(first_message_server_to_agent.remote_config.config.config_map)
|
|
151
|
-
self.signals = utils.parse_first_message_signals(container_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
|
|
@@ -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 = [
|
|
@@ -145,6 +145,9 @@ extend-exclude = [
|
|
|
145
145
|
# Default rule set: pycodestyle errors (E) + pyflakes (F).
|
|
146
146
|
select = ["E", "F"]
|
|
147
147
|
|
|
148
|
+
[tool.ruff.lint.per-file-ignores]
|
|
149
|
+
"initializer/components.py" = ["E402"]
|
|
150
|
+
|
|
148
151
|
[tool.ruff.format]
|
|
149
152
|
quote-style = "preserve"
|
|
150
153
|
indent-style = "space"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "1.0.81"
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.81 → 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.81 → odigos_opentelemetry_python-1.0.82}/initializer/envs.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.81 → 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
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/anyvalue_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/health_status.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.81 → odigos_opentelemetry_python-1.0.82}/opamp/opamp_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|