odigos-opentelemetry-python 1.0.45__tar.gz → 1.0.46__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.45/odigos_opentelemetry_python.egg-info → odigos_opentelemetry_python-1.0.46}/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/components.py +77 -37
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46/odigos_opentelemetry_python.egg-info}/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/odigos_opentelemetry_python.egg-info/SOURCES.txt +1 -0
- odigos_opentelemetry_python-1.0.46/opamp/config.py +58 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/http_client.py +139 -64
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/utils.py +6 -6
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/setup.py +1 -1
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/LICENSE +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/README.md +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/envs.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/exit_hook.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/lib_handling.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/odigos_sampler.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/process_resource.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/sampling_operators.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/version.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/odigos_opentelemetry_python.egg-info/dependency_links.txt +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/odigos_opentelemetry_python.egg-info/requires.txt +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/odigos_opentelemetry_python.egg-info/top_level.txt +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/anyvalue_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/health_status.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/opamp_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/setup.cfg +0 -0
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/components.py
RENAMED
|
@@ -6,13 +6,14 @@ import threading
|
|
|
6
6
|
import atexit
|
|
7
7
|
import sys
|
|
8
8
|
import os
|
|
9
|
+
|
|
9
10
|
import opentelemetry.sdk._configuration as sdk_config
|
|
10
11
|
from .process_resource import OdigosProcessResourceDetector
|
|
11
12
|
from opentelemetry.sdk.resources import Resource
|
|
12
13
|
from opentelemetry.sdk.resources import OTELResourceDetector
|
|
13
14
|
from opentelemetry.sdk.trace import TracerProvider
|
|
14
15
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
15
|
-
from opentelemetry.trace import set_tracer_provider
|
|
16
|
+
from opentelemetry.trace import set_tracer_provider, get_tracer_provider
|
|
16
17
|
|
|
17
18
|
from .version import VERSION
|
|
18
19
|
from .exit_hook import ExitHooks
|
|
@@ -21,6 +22,8 @@ from .envs import set_static_otel_env
|
|
|
21
22
|
from .odigos_sampler import OdigosSampler
|
|
22
23
|
from opentelemetry.sdk.trace.sampling import ParentBased
|
|
23
24
|
|
|
25
|
+
from opamp.config import Config
|
|
26
|
+
|
|
24
27
|
# Reorder the python sys.path to ensure that the user application's dependencies take precedence over the agent's dependencies.
|
|
25
28
|
# This is necessary because the user application's dependencies may be incompatible with those used by the agent.
|
|
26
29
|
reorder_python_path()
|
|
@@ -29,35 +32,35 @@ from opamp.http_client import OpAMPHTTPClient, MockOpAMPClient
|
|
|
29
32
|
|
|
30
33
|
MINIMUM_PYTHON_SUPPORTED_VERSION = (3, 8)
|
|
31
34
|
|
|
35
|
+
|
|
32
36
|
def initialize_components(trace_exporters = False, span_processor = None):
|
|
33
37
|
# In case of forking, the OpAMP client should be started in the child process.
|
|
34
38
|
# e.g when using gunicorn/celery with multiple workers.
|
|
35
39
|
os.register_at_fork(
|
|
36
40
|
after_in_child=lambda: start_opamp_client(threading.Event()),
|
|
37
41
|
) # pylint: disable=protected-access
|
|
38
|
-
|
|
42
|
+
|
|
39
43
|
handle_instrumenation_of_sub_processes()
|
|
40
44
|
|
|
41
45
|
resource_attributes_event = threading.Event()
|
|
42
46
|
client = None
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
try:
|
|
45
|
-
|
|
49
|
+
|
|
46
50
|
client = start_opamp_client(resource_attributes_event)
|
|
47
51
|
resource_attributes_event.wait(timeout=30) # Wait for the resource attributes to be received for 30 seconds
|
|
48
52
|
|
|
49
53
|
supported_signals = client.signals
|
|
50
54
|
received_value = client.resource_attributes
|
|
51
|
-
|
|
52
|
-
if received_value:
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
if received_value:
|
|
54
57
|
handle_django_instrumentation()
|
|
55
|
-
|
|
58
|
+
|
|
56
59
|
auto_resource = {
|
|
57
60
|
"telemetry.distro.name": "odigos",
|
|
58
61
|
"telemetry.distro.version": VERSION,
|
|
59
62
|
}
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
auto_resource.update(received_value)
|
|
62
65
|
|
|
63
66
|
resource = OdigosProcessResourceDetector(client.pid).detect() \
|
|
@@ -70,15 +73,15 @@ def initialize_components(trace_exporters = False, span_processor = None):
|
|
|
70
73
|
|
|
71
74
|
initialize_metrics_if_enabled(resource, supported_signals)
|
|
72
75
|
initialize_logging_if_enabled(resource, supported_signals)
|
|
73
|
-
|
|
74
|
-
else:
|
|
76
|
+
|
|
77
|
+
else:
|
|
75
78
|
raise Exception("Did not receive resource attributes from the OpAMP server.")
|
|
76
|
-
|
|
79
|
+
|
|
77
80
|
except Exception as e:
|
|
78
81
|
if client is not None:
|
|
79
82
|
client.shutdown(custom_failure_message=str(e))
|
|
80
83
|
raise
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
finally:
|
|
83
86
|
# Make sure the distro modules are reloaded even if an exception is raised.
|
|
84
87
|
reload_distro_modules()
|
|
@@ -86,21 +89,21 @@ def initialize_components(trace_exporters = False, span_processor = None):
|
|
|
86
89
|
def initialize_traces_if_enabled(trace_exporters, resource, span_processor = None, signals = None):
|
|
87
90
|
# In case collectors signals support receiving traces, initialize the tracer provider with the exporters
|
|
88
91
|
traces_enabled = signals.get("traceSignal", False)
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
if traces_enabled:
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
odigos_sampler = OdigosSampler()
|
|
93
96
|
sampler = ParentBased(odigos_sampler)
|
|
94
|
-
|
|
97
|
+
|
|
95
98
|
# Exporting using exporters
|
|
96
99
|
if trace_exporters:
|
|
97
100
|
provider = TracerProvider(resource=resource, sampler=sampler)
|
|
98
101
|
id_generator_name = sdk_config._get_id_generator()
|
|
99
|
-
id_generator = sdk_config._import_id_generator(id_generator_name)
|
|
102
|
+
id_generator = sdk_config._import_id_generator(id_generator_name)
|
|
100
103
|
provider.id_generator = id_generator
|
|
101
|
-
|
|
104
|
+
|
|
102
105
|
set_tracer_provider(provider)
|
|
103
|
-
|
|
106
|
+
|
|
104
107
|
exporters, _, _ = sdk_config._import_exporters(["otlp_proto_http"], [], [])
|
|
105
108
|
|
|
106
109
|
for _, exporter_class in exporters.items():
|
|
@@ -108,14 +111,14 @@ def initialize_traces_if_enabled(trace_exporters, resource, span_processor = Non
|
|
|
108
111
|
provider.add_span_processor(
|
|
109
112
|
BatchSpanProcessor(exporter_class(**exporter_args))
|
|
110
113
|
)
|
|
111
|
-
|
|
114
|
+
|
|
112
115
|
# Exporting using EBPF
|
|
113
116
|
else:
|
|
114
117
|
provider = TracerProvider(resource=resource, sampler=sampler)
|
|
115
118
|
set_tracer_provider(provider)
|
|
116
119
|
if span_processor is not None:
|
|
117
120
|
provider.add_span_processor(span_processor)
|
|
118
|
-
|
|
121
|
+
|
|
119
122
|
return odigos_sampler
|
|
120
123
|
|
|
121
124
|
return None
|
|
@@ -123,17 +126,17 @@ def initialize_traces_if_enabled(trace_exporters, resource, span_processor = Non
|
|
|
123
126
|
def initialize_metrics_if_enabled(resource, signals):
|
|
124
127
|
# Currently we're not exporting metrics using the OTEL exporter.
|
|
125
128
|
metrics_enabled = False
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
if metrics_enabled:
|
|
128
131
|
_, exporters, _ = sdk_config._import_exporters([], ["otlp_proto_http"], [])
|
|
129
|
-
|
|
132
|
+
|
|
130
133
|
sdk_config._init_metrics(exporters, resource)
|
|
131
134
|
|
|
132
135
|
|
|
133
136
|
def initialize_logging_if_enabled(resource, signals):
|
|
134
137
|
# Logs are collected using the filelog receiver; currently, there is no need to export them using the OTEL exporter.
|
|
135
138
|
logging_enabled = False
|
|
136
|
-
|
|
139
|
+
|
|
137
140
|
if logging_enabled:
|
|
138
141
|
_, _, exporters = sdk_config._import_exporters([], [], ["otlp_proto_http"])
|
|
139
142
|
sdk_config._init_logging(exporters, resource)
|
|
@@ -142,20 +145,23 @@ def initialize_logging_if_enabled(resource, signals):
|
|
|
142
145
|
def start_opamp_client(event):
|
|
143
146
|
if os.getenv('DISABLE_OPAMP_CLIENT', 'false').strip().lower() == 'true':
|
|
144
147
|
return MockOpAMPClient(event)
|
|
145
|
-
|
|
148
|
+
|
|
146
149
|
# Setting static otel envs such as log correlation and exporter protocols
|
|
147
150
|
set_static_otel_env()
|
|
148
|
-
|
|
151
|
+
|
|
149
152
|
condition = threading.Condition(threading.Lock())
|
|
150
153
|
client = OpAMPHTTPClient(event, condition)
|
|
151
|
-
|
|
154
|
+
|
|
155
|
+
# Register the configuration update callback for the processor
|
|
156
|
+
client.register_config_update_cb(update_agent_config)
|
|
157
|
+
|
|
152
158
|
python_version_supported = is_supported_python_version()
|
|
153
159
|
|
|
154
160
|
client.start(python_version_supported)
|
|
155
|
-
|
|
161
|
+
|
|
156
162
|
hooks = ExitHooks()
|
|
157
163
|
hooks.hook()
|
|
158
|
-
|
|
164
|
+
|
|
159
165
|
def shutdown():
|
|
160
166
|
if hooks.exit_code is not None and hooks.exit_code != 0:
|
|
161
167
|
client.shutdown("Program exit with code: " + str(hooks.exit_code))
|
|
@@ -169,31 +175,65 @@ def start_opamp_client(event):
|
|
|
169
175
|
|
|
170
176
|
return client
|
|
171
177
|
|
|
178
|
+
def update_agent_config(conf: Config):
|
|
179
|
+
"""
|
|
180
|
+
The function `update_agent_config` checks for a tracer provider and updates its configuration using
|
|
181
|
+
processors if available.
|
|
182
|
+
It searches specifically for the EBPFSpanProcessor processes (Looking for the update_config attribute).
|
|
183
|
+
|
|
184
|
+
:param conf: The `conf` parameter in the `update_agent_config` function is of type `Config`. It is
|
|
185
|
+
used to update the configuration settings of the agent
|
|
186
|
+
:type conf: Config
|
|
187
|
+
:return: If the `provider` is `None` or if `active_proc` is `None`, then the function will return
|
|
188
|
+
without performing any further actions.
|
|
189
|
+
"""
|
|
190
|
+
provider = get_tracer_provider()
|
|
191
|
+
if provider is None:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# The provider’s public API does not expose processors directly,
|
|
195
|
+
# but the private _active_span_processor attribute always exists.
|
|
196
|
+
active_proc = getattr(provider, "_active_span_processor", None)
|
|
197
|
+
if active_proc is None:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# If you attached only one BatchSpanProcessor, _active_span_processor
|
|
201
|
+
# *is* that object. If you attached several, it is a MultiSpanProcessor
|
|
202
|
+
# that wraps them in ._span_processors.
|
|
203
|
+
processors = (
|
|
204
|
+
active_proc._span_processors
|
|
205
|
+
if hasattr(active_proc, "_span_processors")
|
|
206
|
+
else [active_proc]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
for proc in processors:
|
|
210
|
+
if hasattr(proc, "update_config"):
|
|
211
|
+
proc.update_config(conf)
|
|
172
212
|
|
|
173
213
|
def is_supported_python_version():
|
|
174
214
|
return sys.version_info >= MINIMUM_PYTHON_SUPPORTED_VERSION
|
|
175
215
|
|
|
176
216
|
def handle_instrumenation_of_sub_processes():
|
|
177
217
|
# Resolves a path management issue introduced by OpenTelemetry's sitecustomize implementation.
|
|
178
|
-
# OpenTelemetry removed auto_instrumentation from the path to address a specific bug
|
|
218
|
+
# OpenTelemetry removed auto_instrumentation from the path to address a specific bug
|
|
179
219
|
# (ref: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1050).
|
|
180
|
-
# This modification does not impact our use case, as we utilize auto_instrumentation as a package
|
|
220
|
+
# This modification does not impact our use case, as we utilize auto_instrumentation as a package
|
|
181
221
|
# and do not rely on opentelemetry-instrument for running instrumentations.
|
|
182
|
-
#
|
|
222
|
+
#
|
|
183
223
|
# We are reintroducing the path to enable parallel execution with other agent that inject themselves via sitecustomize.
|
|
184
|
-
# We've observed that agents attempt to import user-defined sitecustomize.py [OpenTelemetry one in this case] prior to their own execution
|
|
224
|
+
# We've observed that agents attempt to import user-defined sitecustomize.py [OpenTelemetry one in this case] prior to their own execution
|
|
185
225
|
# to prevent application logic disruption.
|
|
186
|
-
#
|
|
226
|
+
#
|
|
187
227
|
# Given OpenTelemetry's path removal during Distro creation, we must manually restore the path.
|
|
188
228
|
# This addresses cases where applications using os.exec* are not properly instrumented.
|
|
189
|
-
#
|
|
229
|
+
#
|
|
190
230
|
# Note: This is a temporary solution and should be refactored when:
|
|
191
231
|
# - The environment override writer is removed
|
|
192
232
|
# - Webhook integration is implemented
|
|
193
233
|
# - A custom distro creation mechanism is developed
|
|
194
234
|
auto_instrumentation_path = "/var/odigos/python/opentelemetry/instrumentation/auto_instrumentation"
|
|
195
235
|
python_path = os.getenv("PYTHONPATH", "")
|
|
196
|
-
|
|
236
|
+
|
|
197
237
|
if auto_instrumentation_path not in python_path:
|
|
198
238
|
new_python_path = f"{python_path}:{auto_instrumentation_path}" if python_path else auto_instrumentation_path
|
|
199
|
-
os.environ["PYTHONPATH"] = new_python_path
|
|
239
|
+
os.environ["PYTHONPATH"] = new_python_path
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass, fields, field
|
|
3
|
+
from typing import Any, Dict, Type, TypeVar, get_type_hints
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
### Configuration structs ###
|
|
8
|
+
class DefaultNoneMixin:
|
|
9
|
+
#__getattr__ is only invoked when the normal attribute lookup (__dict__, class attributes, etc.) fails.
|
|
10
|
+
# Prevent raising a ValueError when accessing a non existing key
|
|
11
|
+
# It'll save us multiple try catch blocks in the future
|
|
12
|
+
def __getattr__(self, name: str):
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CodeAttributes(DefaultNoneMixin):
|
|
17
|
+
column: bool = False
|
|
18
|
+
file_path: bool = False
|
|
19
|
+
function: bool = False
|
|
20
|
+
line_number: bool = False
|
|
21
|
+
namespace: bool = False
|
|
22
|
+
stack_trace: bool = False
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Config(DefaultNoneMixin):
|
|
26
|
+
code_attributes: CodeAttributes = field(default_factory=CodeAttributes)
|
|
27
|
+
|
|
28
|
+
### Helpers ###
|
|
29
|
+
|
|
30
|
+
def camel_to_snake(name: str) -> str:
|
|
31
|
+
"""Convert camelCase or PascalCase to snake_case."""
|
|
32
|
+
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
|
33
|
+
return s1.lower()
|
|
34
|
+
|
|
35
|
+
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
|
|
36
|
+
"""
|
|
37
|
+
Instantiate the dataclass `cls` from `data`:
|
|
38
|
+
- Converts JSON camelCase keys to snake_case field names
|
|
39
|
+
- Ignores unknown keys
|
|
40
|
+
- Uses defaults for any missing fields
|
|
41
|
+
- Recurses if a field’s type is itself a dataclass
|
|
42
|
+
"""
|
|
43
|
+
hints = get_type_hints(cls)
|
|
44
|
+
field_names = {f.name for f in fields(cls)}
|
|
45
|
+
init_args: Dict[str, Any] = {}
|
|
46
|
+
for raw_key, raw_val in data.items():
|
|
47
|
+
key = camel_to_snake(raw_key)
|
|
48
|
+
if key not in field_names:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
field_type = hints.get(key)
|
|
52
|
+
# If the field type is another dataclass, recurse
|
|
53
|
+
if hasattr(field_type, "__dataclass_fields__") and isinstance(raw_val, dict):
|
|
54
|
+
init_args[key] = from_dict(field_type, raw_val)
|
|
55
|
+
else:
|
|
56
|
+
init_args[key] = raw_val
|
|
57
|
+
|
|
58
|
+
return cls(**init_args)
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/http_client.py
RENAMED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
+
import json
|
|
3
4
|
import time
|
|
5
|
+
import base64
|
|
4
6
|
import threading
|
|
5
7
|
import requests_odigos
|
|
6
8
|
import logging
|
|
@@ -18,9 +20,14 @@ from opamp import opamp_pb2, anyvalue_pb2, utils
|
|
|
18
20
|
from opamp.health_status import AgentHealthStatus
|
|
19
21
|
from initializer.process_resource import PROCESS_VPID
|
|
20
22
|
|
|
23
|
+
from google.protobuf.json_format import MessageToDict
|
|
24
|
+
|
|
25
|
+
from opamp.config import from_dict, Config
|
|
26
|
+
|
|
21
27
|
# Setup the logger
|
|
22
28
|
opamp_logger = logging.getLogger('odigos')
|
|
23
29
|
|
|
30
|
+
_debugger_instance = None
|
|
24
31
|
|
|
25
32
|
env_var_mappings = {
|
|
26
33
|
"ODIGOS_WORKLOAD_NAMESPACE": ResourceAttributes.K8S_NAMESPACE_NAME,
|
|
@@ -42,72 +49,72 @@ class OpAMPHTTPClient:
|
|
|
42
49
|
self.remote_config_status = None
|
|
43
50
|
self.sampler = None # OdigosSampler instance
|
|
44
51
|
self.pid = os.getpid()
|
|
52
|
+
self.update_conf_cb = None # Callback for configuration updates in the processor
|
|
45
53
|
|
|
46
54
|
|
|
47
55
|
def start(self, python_version_supported: bool = None):
|
|
48
56
|
if not python_version_supported:
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
python_version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
|
|
51
59
|
error_message = f"Opentelemetry SDK require Python in version 3.8 or higher [{python_version} is not supported]"
|
|
52
|
-
|
|
60
|
+
|
|
53
61
|
# opamp_logger.warning(f"{error_message}, sending disconnect message to OpAMP server...")
|
|
54
62
|
self.send_unsupported_version_disconnect_message(error_message=error_message)
|
|
55
63
|
self.event.set()
|
|
56
64
|
return
|
|
57
|
-
|
|
65
|
+
|
|
58
66
|
self.client_thread = threading.Thread(target=self.run, name="OpAMPClientThread", daemon=True)
|
|
59
67
|
self.client_thread.start()
|
|
60
|
-
|
|
68
|
+
|
|
61
69
|
def run(self):
|
|
62
70
|
try:
|
|
63
71
|
if not self.mandatory_env_vars_set():
|
|
64
72
|
self.event.set()
|
|
65
73
|
return
|
|
66
|
-
|
|
74
|
+
|
|
67
75
|
self.send_first_message_with_retry()
|
|
68
76
|
self.event.set()
|
|
69
|
-
|
|
77
|
+
|
|
70
78
|
self.worker()
|
|
71
|
-
|
|
79
|
+
|
|
72
80
|
except Exception as e:
|
|
73
81
|
# opamp_logger.error(f"Error running OpAMP client: {e}")
|
|
74
|
-
failure_message = self.get_agent_failure_disconnect_message(error_message=str(e))
|
|
82
|
+
failure_message = self.get_agent_failure_disconnect_message(error_message=str(e))
|
|
75
83
|
self.send_agent_to_server_message(failure_message)
|
|
76
|
-
|
|
84
|
+
|
|
77
85
|
# Exiting the opamp thread and set the event to notify the main thread
|
|
78
86
|
self.event.set()
|
|
79
87
|
sys.exit()
|
|
80
88
|
|
|
81
89
|
def get_agent_failure_disconnect_message(self, error_message: str, component_health: bool = False) -> None:
|
|
82
90
|
agent_failure_message = opamp_pb2.AgentToServer()
|
|
83
|
-
|
|
91
|
+
|
|
84
92
|
agent_disconnect = self.get_agent_disconnect()
|
|
85
93
|
agent_failure_message.agent_disconnect.CopyFrom(agent_disconnect)
|
|
86
|
-
|
|
94
|
+
|
|
87
95
|
agent_health = self.get_agent_health(component_health=component_health, last_error=error_message, status=AgentHealthStatus.AGENT_FAILURE.value)
|
|
88
96
|
agent_failure_message.health.CopyFrom(agent_health)
|
|
89
|
-
|
|
97
|
+
|
|
90
98
|
return agent_failure_message
|
|
91
|
-
|
|
99
|
+
|
|
92
100
|
def send_unsupported_version_disconnect_message(self, error_message: str) -> None:
|
|
93
101
|
first_disconnect_message = opamp_pb2.AgentToServer()
|
|
94
|
-
|
|
102
|
+
|
|
95
103
|
agent_description = self.get_agent_description()
|
|
96
|
-
|
|
104
|
+
|
|
97
105
|
first_disconnect_message.agent_description.CopyFrom(agent_description)
|
|
98
|
-
|
|
106
|
+
|
|
99
107
|
agent_disconnect = self.get_agent_disconnect()
|
|
100
108
|
first_disconnect_message.agent_disconnect.CopyFrom(agent_disconnect)
|
|
101
|
-
|
|
109
|
+
|
|
102
110
|
agent_health = self.get_agent_health(component_health=False, last_error=error_message, status=AgentHealthStatus.UNSUPPORTED_RUNTIME_VERSION.value)
|
|
103
111
|
first_disconnect_message.health.CopyFrom(agent_health)
|
|
104
|
-
|
|
112
|
+
|
|
105
113
|
self.send_agent_to_server_message(first_disconnect_message)
|
|
106
|
-
|
|
114
|
+
|
|
107
115
|
def send_first_message_with_retry(self) -> None:
|
|
108
116
|
max_retries = 5
|
|
109
117
|
delay = 2
|
|
110
|
-
|
|
111
118
|
for attempt in range(1, max_retries + 1):
|
|
112
119
|
try:
|
|
113
120
|
# Send first message to OpAMP server, Health is false as the component is not initialized
|
|
@@ -117,28 +124,36 @@ class OpAMPHTTPClient:
|
|
|
117
124
|
|
|
118
125
|
# Check if the response of the first message is empty
|
|
119
126
|
# It may happen if OpAMPServer is not available
|
|
120
|
-
if first_message_server_to_agent.ListFields():
|
|
121
|
-
self.update_remote_config_status(
|
|
122
|
-
|
|
127
|
+
if first_message_server_to_agent.ListFields():
|
|
128
|
+
if self.update_remote_config_status(server_to_agent):
|
|
129
|
+
if server_to_agent.HasField("remote_config") and self.update_conf_cb:
|
|
130
|
+
try:
|
|
131
|
+
remote_config = self.get_remote_config(server_to_agent)
|
|
132
|
+
self.update_conf_cb(remote_config)
|
|
133
|
+
except Exception:
|
|
134
|
+
# Catch exception and don't update the config
|
|
135
|
+
# The default config is preloaded when the EBPFSpanProcessor is initialized
|
|
136
|
+
pass
|
|
137
|
+
|
|
123
138
|
sdk_config = utils.get_sdk_config(first_message_server_to_agent.remote_config.config.config_map)
|
|
124
139
|
self.resource_attributes = utils.parse_first_message_to_resource_attributes(sdk_config, opamp_logger)
|
|
125
140
|
self.signals = utils.parse_first_message_signals(sdk_config, opamp_logger)
|
|
126
|
-
|
|
141
|
+
|
|
127
142
|
# Send healthy message to OpAMP server
|
|
128
143
|
# opamp_logger.info("Reporting healthy to OpAMP server...")
|
|
129
144
|
agent_health = self.get_agent_health(component_health=True, status=AgentHealthStatus.HEALTHY.value)
|
|
130
145
|
self.send_agent_to_server_message(opamp_pb2.AgentToServer(health=agent_health))
|
|
131
|
-
|
|
146
|
+
|
|
132
147
|
# Return if the first message was successfully sent
|
|
133
148
|
return
|
|
134
|
-
|
|
149
|
+
|
|
135
150
|
except Exception as e:
|
|
136
151
|
# opamp_logger.error(f"Attempt {attempt}/{max_retries} failed. Error sending full state to OpAMP server: {e}")
|
|
137
152
|
pass
|
|
138
|
-
|
|
153
|
+
|
|
139
154
|
if attempt < max_retries:
|
|
140
155
|
time.sleep(delay)
|
|
141
|
-
|
|
156
|
+
|
|
142
157
|
# If all attempts failed, raise exception before starting the worker
|
|
143
158
|
raise Exception(f"Error sending first message to OpAMP server after {max_retries} attempts")
|
|
144
159
|
|
|
@@ -149,28 +164,86 @@ class OpAMPHTTPClient:
|
|
|
149
164
|
try:
|
|
150
165
|
server_to_agent = self.send_heartbeat()
|
|
151
166
|
if self.update_remote_config_status(server_to_agent):
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
if server_to_agent.HasField("remote_config") and self.update_conf_cb:
|
|
168
|
+
try:
|
|
169
|
+
remote_config = self.get_remote_config(server_to_agent)
|
|
170
|
+
self.update_conf_cb(remote_config)
|
|
171
|
+
except Exception:
|
|
172
|
+
# Catch exception and don't update the config
|
|
173
|
+
# The default config is preloaded when the EBPFSpanProcessor is initialized
|
|
174
|
+
pass
|
|
155
175
|
|
|
156
176
|
if server_to_agent.flags & opamp_pb2.ServerToAgentFlags_ReportFullState:
|
|
157
177
|
# opamp_logger.info("Received request to report full state")
|
|
158
|
-
|
|
178
|
+
|
|
159
179
|
agent_description = self.get_agent_description()
|
|
160
180
|
agent_health = self.get_agent_health(component_health=True, status=AgentHealthStatus.HEALTHY.value)
|
|
161
181
|
agent_to_server = opamp_pb2.AgentToServer(agent_description=agent_description, health=agent_health)
|
|
162
|
-
|
|
182
|
+
|
|
163
183
|
server_to_agent = self.send_agent_to_server_message(agent_to_server)
|
|
164
|
-
|
|
165
|
-
self.update_remote_config_status(server_to_agent)
|
|
166
|
-
|
|
184
|
+
|
|
185
|
+
if self.update_remote_config_status(server_to_agent):
|
|
186
|
+
if server_to_agent.HasField("remote_config") and self.update_conf_cb:
|
|
187
|
+
try:
|
|
188
|
+
remote_config = self.get_remote_config(server_to_agent)
|
|
189
|
+
self.update_conf_cb(remote_config)
|
|
190
|
+
except Exception:
|
|
191
|
+
# Catch exception and don't update the config
|
|
192
|
+
# The default config is preloaded when the EBPFSpanProcessor is initialized
|
|
193
|
+
pass
|
|
194
|
+
|
|
167
195
|
except requests_odigos.RequestException as e:
|
|
168
196
|
# opamp_logger.error(f"Error fetching data: {e}")
|
|
169
197
|
pass
|
|
170
198
|
self.condition.wait(30)
|
|
171
199
|
|
|
200
|
+
def get_remote_config(self, message) -> dict:
|
|
201
|
+
"""
|
|
202
|
+
Given a Protobuf message with a 'remote_config' field,
|
|
203
|
+
decode all bytes and base64-encoded subfields into a clean Python dict.
|
|
204
|
+
"""
|
|
205
|
+
proto_dict = MessageToDict(message, preserving_proto_field_name=True)
|
|
206
|
+
|
|
207
|
+
remote_config = proto_dict.get("remote_config")
|
|
208
|
+
if not remote_config:
|
|
209
|
+
raise ValueError("No 'remote_config' field found in the provided message.")
|
|
210
|
+
|
|
211
|
+
config_map = remote_config.get("config", {}).get("config_map", {})
|
|
212
|
+
if not config_map:
|
|
213
|
+
raise ValueError("No 'config_map' found inside 'remote_config'.")
|
|
214
|
+
|
|
215
|
+
decoded_map = {}
|
|
216
|
+
|
|
217
|
+
for key, item in config_map.items():
|
|
218
|
+
body_base64 = item.get("body")
|
|
219
|
+
content_type = item.get("content_type")
|
|
220
|
+
|
|
221
|
+
if not body_base64:
|
|
222
|
+
decoded_map[key] = None
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
body_bytes = base64.b64decode(body_base64)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
raise ValueError(f"Failed to base64 decode body for key {key}: {e}")
|
|
229
|
+
|
|
230
|
+
if content_type == "application/json":
|
|
231
|
+
try:
|
|
232
|
+
body_json = json.loads(body_bytes.decode('utf-8'))
|
|
233
|
+
decoded_map[key] = body_json
|
|
234
|
+
except Exception as e:
|
|
235
|
+
raise ValueError(f"Failed to JSON-decode body for key {key}: {e}")
|
|
236
|
+
else:
|
|
237
|
+
# If not JSON, just keep it as raw bytes
|
|
238
|
+
decoded_map[key] = body_bytes
|
|
239
|
+
|
|
240
|
+
inner = decoded_map.get("", None)
|
|
241
|
+
if inner is None:
|
|
242
|
+
return Config() # Return default values for config
|
|
243
|
+
return from_dict(Config, inner)
|
|
244
|
+
|
|
172
245
|
def send_heartbeat(self) -> opamp_pb2.ServerToAgent: # type: ignore
|
|
173
|
-
# opamp_logger.debug("Sending heartbeat to OpAMP server...")
|
|
246
|
+
# opamp_logger.debug("Sending heartbeat to OpAMP server...")
|
|
174
247
|
try:
|
|
175
248
|
agent_to_server = opamp_pb2.AgentToServer(remote_config_status=self.remote_config_status)
|
|
176
249
|
return self.send_agent_to_server_message(agent_to_server)
|
|
@@ -179,23 +252,23 @@ class OpAMPHTTPClient:
|
|
|
179
252
|
pass
|
|
180
253
|
|
|
181
254
|
def get_agent_description(self) -> opamp_pb2.AgentDescription: # type: ignore
|
|
182
|
-
# The "DISABLE_OPAMP_CLIENT" environment variable is defined only in our VMs environments.
|
|
255
|
+
# The "DISABLE_OPAMP_CLIENT" environment variable is defined only in our VMs environments.
|
|
183
256
|
# Here we use it exclusively to distinguish between virtual machine and Kubernetes environments.
|
|
184
257
|
#
|
|
185
|
-
# - If "DISABLE_OPAMP_CLIENT" is set to "true", it indicates that the service is running in a VM,
|
|
258
|
+
# - If "DISABLE_OPAMP_CLIENT" is set to "true", it indicates that the service is running in a VM,
|
|
186
259
|
# so we use "PROCESS_PID" for identification.
|
|
187
|
-
# - Otherwise, the service is assumed to be running in a K8s environment,
|
|
260
|
+
# - Otherwise, the service is assumed to be running in a K8s environment,
|
|
188
261
|
# and we use "PROCESS_VPID" instead.
|
|
189
262
|
#
|
|
190
263
|
# This ensures the correct process identification mechanism is applied based on the runtime environment.
|
|
191
264
|
|
|
192
265
|
# Add additional attributes from environment variables
|
|
193
|
-
|
|
266
|
+
|
|
194
267
|
if os.getenv("DISABLE_OPAMP_CLIENT", "false").strip().lower() == "true":
|
|
195
268
|
process_id_key = ResourceAttributes.PROCESS_PID
|
|
196
269
|
else:
|
|
197
270
|
process_id_key = PROCESS_VPID
|
|
198
|
-
|
|
271
|
+
|
|
199
272
|
identifying_attributes = [
|
|
200
273
|
anyvalue_pb2.KeyValue(
|
|
201
274
|
key=ResourceAttributes.SERVICE_INSTANCE_ID,
|
|
@@ -221,15 +294,15 @@ class OpAMPHTTPClient:
|
|
|
221
294
|
value=anyvalue_pb2.AnyValue(string_value=value)
|
|
222
295
|
)
|
|
223
296
|
)
|
|
224
|
-
|
|
297
|
+
|
|
225
298
|
return opamp_pb2.AgentDescription(
|
|
226
299
|
identifying_attributes=identifying_attributes,
|
|
227
300
|
non_identifying_attributes=[]
|
|
228
301
|
)
|
|
229
|
-
|
|
302
|
+
|
|
230
303
|
def get_agent_disconnect(self) -> opamp_pb2.AgentDisconnect: # type: ignore
|
|
231
304
|
return opamp_pb2.AgentDisconnect()
|
|
232
|
-
|
|
305
|
+
|
|
233
306
|
def get_agent_health(self, component_health: bool = None, last_error : str = None, status: str = None) -> opamp_pb2.ComponentHealth: # type: ignore
|
|
234
307
|
health = opamp_pb2.ComponentHealth(
|
|
235
308
|
)
|
|
@@ -239,24 +312,24 @@ class OpAMPHTTPClient:
|
|
|
239
312
|
health.last_error = last_error
|
|
240
313
|
if status is not None:
|
|
241
314
|
health.status = status
|
|
242
|
-
|
|
315
|
+
|
|
243
316
|
return health
|
|
244
|
-
|
|
245
|
-
|
|
317
|
+
|
|
318
|
+
|
|
246
319
|
def send_agent_to_server_message(self, message: opamp_pb2.AgentToServer) -> opamp_pb2.ServerToAgent: # type: ignore
|
|
247
|
-
|
|
320
|
+
|
|
248
321
|
message.instance_uid = self.instance_uid.encode('utf-8')
|
|
249
|
-
message.sequence_num = self.next_sequence_num
|
|
322
|
+
message.sequence_num = self.next_sequence_num
|
|
250
323
|
if self.remote_config_status:
|
|
251
324
|
message.remote_config_status.CopyFrom(self.remote_config_status)
|
|
252
|
-
|
|
325
|
+
|
|
253
326
|
self.next_sequence_num += 1
|
|
254
327
|
message_bytes = message.SerializeToString()
|
|
255
|
-
|
|
328
|
+
|
|
256
329
|
headers = {
|
|
257
330
|
"Content-Type": "application/x-protobuf",
|
|
258
331
|
}
|
|
259
|
-
|
|
332
|
+
|
|
260
333
|
try:
|
|
261
334
|
agent_message = attach(set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True))
|
|
262
335
|
response = requests_odigos.post(self.server_url, data=message_bytes, headers=headers, timeout=5)
|
|
@@ -269,7 +342,7 @@ class OpAMPHTTPClient:
|
|
|
269
342
|
return opamp_pb2.ServerToAgent()
|
|
270
343
|
finally:
|
|
271
344
|
detach(agent_message)
|
|
272
|
-
|
|
345
|
+
|
|
273
346
|
server_to_agent = opamp_pb2.ServerToAgent()
|
|
274
347
|
try:
|
|
275
348
|
server_to_agent.ParseFromString(response.content)
|
|
@@ -277,19 +350,19 @@ class OpAMPHTTPClient:
|
|
|
277
350
|
# opamp_logger.error(f"Error parsing response from OpAMP server: {e}")
|
|
278
351
|
return opamp_pb2.ServerToAgent()
|
|
279
352
|
return server_to_agent
|
|
280
|
-
|
|
353
|
+
|
|
281
354
|
def mandatory_env_vars_set(self):
|
|
282
355
|
mandatory_env_vars = {
|
|
283
356
|
"ODIGOS_OPAMP_SERVER_HOST": self.server_host,
|
|
284
357
|
}
|
|
285
|
-
|
|
358
|
+
|
|
286
359
|
for env_var, value in mandatory_env_vars.items():
|
|
287
360
|
if not value:
|
|
288
361
|
# opamp_logger.error(f"{env_var} environment variable not set")
|
|
289
362
|
return False
|
|
290
|
-
|
|
291
|
-
return True
|
|
292
|
-
|
|
363
|
+
|
|
364
|
+
return True
|
|
365
|
+
|
|
293
366
|
def shutdown(self, custom_failure_message: str = None, component_health: bool = False):
|
|
294
367
|
self.running = False
|
|
295
368
|
# opamp_logger.info("Sending agent disconnect message to OpAMP server...")
|
|
@@ -298,22 +371,24 @@ class OpAMPHTTPClient:
|
|
|
298
371
|
else:
|
|
299
372
|
agent_health = self.get_agent_health(component_health=component_health, last_error="Python runtime is exiting", status=AgentHealthStatus.TERMINATED.value)
|
|
300
373
|
disconnect_message = opamp_pb2.AgentToServer(agent_disconnect=opamp_pb2.AgentDisconnect(), health=agent_health)
|
|
301
|
-
|
|
374
|
+
|
|
302
375
|
with self.condition:
|
|
303
376
|
self.condition.notify_all()
|
|
304
377
|
self.client_thread.join()
|
|
305
|
-
|
|
378
|
+
|
|
306
379
|
self.send_agent_to_server_message(disconnect_message)
|
|
307
|
-
|
|
380
|
+
|
|
308
381
|
def update_remote_config_status(self, server_to_agent: opamp_pb2.ServerToAgent) -> bool: # type: ignore
|
|
309
382
|
if server_to_agent.HasField("remote_config"):
|
|
310
383
|
remote_config_hash = server_to_agent.remote_config.config_hash
|
|
311
384
|
remote_config_status = opamp_pb2.RemoteConfigStatus(last_remote_config_hash=remote_config_hash)
|
|
312
385
|
self.remote_config_status = remote_config_status
|
|
313
386
|
return True
|
|
314
|
-
|
|
315
387
|
return False
|
|
316
388
|
|
|
389
|
+
def register_config_update_cb(self, cb) -> None:
|
|
390
|
+
self.update_conf_cb = cb
|
|
391
|
+
|
|
317
392
|
|
|
318
393
|
# Mock client class for non-OpAMP installations
|
|
319
394
|
# This class simulates the OpAMP client when the OpAMP server is not available.
|
|
@@ -325,4 +400,4 @@ class MockOpAMPClient:
|
|
|
325
400
|
event.set()
|
|
326
401
|
|
|
327
402
|
def shutdown(self, custom_failure_message=None):
|
|
328
|
-
pass
|
|
403
|
+
pass
|
|
@@ -12,28 +12,28 @@ def get_sdk_config(config_map):
|
|
|
12
12
|
return json.loads(config_map["SDK"].body)
|
|
13
13
|
except json.JSONDecodeError:
|
|
14
14
|
return {}
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
|
|
17
17
|
def parse_first_message_to_resource_attributes(sdk_config, logger: logging.Logger) -> dict: # type: ignore
|
|
18
18
|
'''
|
|
19
19
|
Parses remote resource attributes from the SDK config.
|
|
20
20
|
'''
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
remote_resource_attributes = sdk_config.get('remoteResourceAttributes', [])
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
if not remote_resource_attributes:
|
|
25
25
|
# logger.error('missing "remoteResourceAttributes" section in OpAMP server remote config on first server to agent message')
|
|
26
26
|
return {}
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
return {item['key']: item['value'] for item in remote_resource_attributes}
|
|
29
29
|
|
|
30
30
|
def parse_first_message_signals(sdk_config, logger: logging.Logger): # type: ignore
|
|
31
31
|
"""
|
|
32
32
|
Parses the trace, logs, and metrics signals configuration from the SDK config.
|
|
33
33
|
"""
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
return {
|
|
36
36
|
"traceSignal": sdk_config.get("traceSignal", {}).get("enabled", False),
|
|
37
37
|
"metricsSignal": sdk_config.get("metricsSignal", {}).get("enabled", False),
|
|
38
38
|
"logsSignal": sdk_config.get("logsSignal", {}).get("enabled", False),
|
|
39
|
-
}
|
|
39
|
+
}
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/__init__.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/envs.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/exit_hook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/initializer/version.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/anyvalue_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/health_status.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.45 → odigos_opentelemetry_python-1.0.46}/opamp/opamp_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|