odigos-opentelemetry-python 1.0.82__tar.gz → 1.0.84__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.82 → odigos_opentelemetry_python-1.0.84}/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/components.py +4 -5
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/odigos_sampler.py +49 -31
- odigos_opentelemetry_python-1.0.84/initializer/odigos_sampler_helpers.py +22 -0
- odigos_opentelemetry_python-1.0.84/initializer/version.py +1 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/PKG-INFO +1 -1
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/SOURCES.txt +1 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/config.py +1 -1
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/http_client.py +18 -18
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/pyproject.toml +1 -1
- odigos_opentelemetry_python-1.0.82/initializer/version.py +0 -1
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/LICENSE +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/README.md +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/distro/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/distro/distro.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/distro/instrumentation_registry.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/envs.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/exit_hook.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/lib_handling.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/post_instrument.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/process_resource.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/schemas/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/schemas/sampling.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/dependency_links.txt +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/entry_points.txt +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/requires.txt +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/top_level.txt +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/__init__.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/anyvalue_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/health_status.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_pb2.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_registry.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/utils.py +0 -0
- {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/setup.cfg +0 -0
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/components.py
RENAMED
|
@@ -267,12 +267,11 @@ def update_agent_config(conf: Config):
|
|
|
267
267
|
if provider is None:
|
|
268
268
|
return
|
|
269
269
|
|
|
270
|
-
# Update sampler configuration if
|
|
271
|
-
if hasattr(conf, '
|
|
272
|
-
# Extract headSampling configuration from traces.headSampling
|
|
270
|
+
# Update sampler configuration if container_config is present
|
|
271
|
+
if hasattr(conf, 'container_config') and conf.container_config:
|
|
273
272
|
head_sampling_config = None
|
|
274
|
-
if 'traces' in conf.
|
|
275
|
-
head_sampling_config = conf.
|
|
273
|
+
if 'traces' in conf.container_config and 'headSampling' in conf.container_config['traces']:
|
|
274
|
+
head_sampling_config = conf.container_config['traces']['headSampling']
|
|
276
275
|
|
|
277
276
|
# Apply config to sampler - sampler will always exist when this callback is called
|
|
278
277
|
# because update_agent_config is only called from OpAMP worker thread after sampler creation
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from threading import Lock
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Mapping, Optional, Sequence
|
|
4
|
+
from urllib.parse import urlsplit
|
|
4
5
|
|
|
5
6
|
from opentelemetry.context import Context
|
|
6
7
|
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult, _get_parent_trace_state
|
|
7
8
|
from opentelemetry.semconv.attributes import http_attributes as http_attributes_semconv
|
|
8
9
|
from opentelemetry.semconv.attributes import server_attributes as server_attributes_semconv
|
|
10
|
+
from opentelemetry.semconv.attributes import url_attributes as url_attributes_semconv
|
|
9
11
|
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
|
|
10
|
-
from opentelemetry.util.types import Attributes
|
|
12
|
+
from opentelemetry.util.types import Attributes, AttributeValue
|
|
11
13
|
|
|
14
|
+
from initializer.odigos_sampler_helpers import get_attribute, strip_port
|
|
12
15
|
from initializer.schemas.sampling import (
|
|
13
16
|
HeadSamplingConfig,
|
|
14
17
|
HeadSamplingHttpClientOperationMatcher,
|
|
@@ -130,25 +133,19 @@ class OdigosSampler(Sampler):
|
|
|
130
133
|
# sampler_logger.debug('No noisy operation matched, sampling the trace')
|
|
131
134
|
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=_get_parent_trace_state(parent_context))
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for key in keys:
|
|
137
|
-
value = span_attributes.get(key)
|
|
138
|
-
if value is not None:
|
|
139
|
-
return value
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
def _match_http_server_sample_rule(self, http_server_rule: HeadSamplingHttpServerOperationMatcher, span_attributes: Mapping[str, Any]) -> bool:
|
|
136
|
+
def _match_http_server_sample_rule(
|
|
137
|
+
self, http_server_rule: HeadSamplingHttpServerOperationMatcher, span_attributes: Mapping[str, AttributeValue]
|
|
138
|
+
) -> bool:
|
|
143
139
|
"""
|
|
144
140
|
Try to match all the http server's attributes to the definitions of the noisy action
|
|
145
141
|
"""
|
|
146
|
-
|
|
142
|
+
raw_route = span_attributes.get(http_attributes_semconv.HTTP_ROUTE)
|
|
143
|
+
span_route: Optional[str] = raw_route if isinstance(raw_route, str) else None
|
|
147
144
|
# Old semconv: "http.method" → new semconv: "http.request.method"
|
|
148
|
-
span_method =
|
|
145
|
+
span_method = get_attribute(span_attributes, http_attributes_semconv.HTTP_REQUEST_METHOD, "http.method")
|
|
149
146
|
|
|
150
147
|
# Some instrumentations send target instead of route
|
|
151
|
-
target =
|
|
148
|
+
target = get_attribute(span_attributes, "http.target", "url.path")
|
|
152
149
|
|
|
153
150
|
if http_server_rule.get("route") and http_server_rule["route"] != span_route:
|
|
154
151
|
# If we have route in the span attributes, evaluate it first (use template matching
|
|
@@ -166,46 +163,67 @@ class OdigosSampler(Sampler):
|
|
|
166
163
|
|
|
167
164
|
return True
|
|
168
165
|
|
|
169
|
-
def _match_http_client_sample_rule(
|
|
166
|
+
def _match_http_client_sample_rule(
|
|
167
|
+
self,
|
|
168
|
+
http_client_rule: HeadSamplingHttpClientOperationMatcher,
|
|
169
|
+
span_attributes: Mapping[str, AttributeValue],
|
|
170
|
+
) -> bool:
|
|
170
171
|
"""
|
|
171
172
|
Try to match all the http client's attributes to the definitions of the noisy action
|
|
172
173
|
"""
|
|
173
|
-
# Old semconv: "net.peer.name"
|
|
174
|
-
server_address =
|
|
175
|
-
|
|
174
|
+
# Old semconv: "http.host"/"net.peer.name" → new semconv: "server.address"
|
|
175
|
+
server_address = get_attribute(span_attributes, server_attributes_semconv.SERVER_ADDRESS, "net.peer.name", "http.host")
|
|
176
|
+
if server_address:
|
|
177
|
+
server_address = strip_port(server_address)
|
|
178
|
+
|
|
179
|
+
# Old semconv for url is a target that we need to get the client_path from → new semconv: "url.template"/"url.path"
|
|
180
|
+
client_path = get_attribute(span_attributes, "url.template", url_attributes_semconv.URL_PATH)
|
|
181
|
+
if not client_path:
|
|
182
|
+
# Old semconv: "http.target" is a path (may carry a query); "http.url"/"url.full" is a full URL we parse the path out of.
|
|
183
|
+
target = get_attribute(span_attributes, "http.target", "url.full", "http.url")
|
|
184
|
+
client_path = urlsplit(target).path if target else None
|
|
185
|
+
|
|
176
186
|
# Old semconv: "http.method" → new semconv: "http.request.method"
|
|
177
|
-
method =
|
|
187
|
+
method = get_attribute(span_attributes, http_attributes_semconv.HTTP_REQUEST_METHOD, "http.method")
|
|
178
188
|
|
|
179
189
|
if http_client_rule.get("serverAddress") and http_client_rule["serverAddress"] != server_address:
|
|
180
190
|
return False
|
|
181
|
-
|
|
191
|
+
templated_path = http_client_rule.get("templatedPath")
|
|
192
|
+
if templated_path and not self._match_rule_route_to_span(templated_path, client_path or ""):
|
|
182
193
|
return False
|
|
183
|
-
|
|
194
|
+
templated_path_prefix = http_client_rule.get("templatedPathPrefix")
|
|
195
|
+
if templated_path_prefix and not self._match_rule_route_to_span(templated_path_prefix, client_path or "", route_has_prefix=True):
|
|
184
196
|
return False
|
|
185
197
|
if http_client_rule.get("method") and http_client_rule["method"] != method:
|
|
186
198
|
return False
|
|
187
199
|
|
|
188
200
|
return True
|
|
189
201
|
|
|
190
|
-
def _match_rule_route_to_span(self, rule_route: str, span_value: str) -> bool:
|
|
202
|
+
def _match_rule_route_to_span(self, rule_route: str, span_value: str, route_has_prefix: bool = False) -> bool:
|
|
191
203
|
"""
|
|
192
204
|
Match a sampling rule route (e.g. /item/{id}) against a span value, which can be either
|
|
193
205
|
a templated route (e.g. /item/{item_id}) or a full URL target (e.g. /item/123?param=hello).
|
|
194
|
-
|
|
206
|
+
Wildcard segments ({...}, :name, *) in the rule match any single span segment.
|
|
207
|
+
With prefix=True the span may have extra trailing segments (prefix match).
|
|
195
208
|
"""
|
|
196
209
|
span_value = span_value.split('?')[0]
|
|
197
210
|
rule_parts = [part for part in rule_route.split('/') if part]
|
|
198
211
|
span_parts = [part for part in span_value.split('/') if part]
|
|
199
212
|
|
|
200
|
-
if
|
|
201
|
-
|
|
213
|
+
if route_has_prefix:
|
|
214
|
+
# Prefix match: span may have extra trailing segments, but not fewer.
|
|
215
|
+
if len(span_parts) < len(rule_parts):
|
|
216
|
+
return False
|
|
217
|
+
else:
|
|
218
|
+
# Exact match: segment counts must be identical.
|
|
219
|
+
if len(span_parts) != len(rule_parts):
|
|
220
|
+
return False
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if rule_parts[i].startswith('{'):
|
|
222
|
+
for rule_segment, span_segment in zip(rule_parts, span_parts):
|
|
223
|
+
# Wildcard segment ({text}, :name, *) matches any single span segment
|
|
224
|
+
if rule_segment == '*' or (rule_segment.startswith('{') and rule_segment.endswith('}')) or rule_segment.startswith(':'):
|
|
207
225
|
continue
|
|
208
|
-
if
|
|
226
|
+
if rule_segment != span_segment:
|
|
209
227
|
return False
|
|
210
228
|
|
|
211
229
|
return True
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Mapping, Optional
|
|
2
|
+
from urllib.parse import urlsplit
|
|
3
|
+
|
|
4
|
+
from opentelemetry.util.types import AttributeValue
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def strip_port(host: str) -> str:
|
|
8
|
+
parsed = urlsplit(f'//{host}')
|
|
9
|
+
return parsed.hostname or host
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_attribute(span_attributes: Mapping[str, AttributeValue], *keys: str) -> Optional[str]:
|
|
13
|
+
"""Return the first string value found for the given attribute keys.
|
|
14
|
+
|
|
15
|
+
Span attribute values can be str/bool/int/float or sequences of those; the HTTP
|
|
16
|
+
semconv keys we look up are always strings, so non-str values are ignored.
|
|
17
|
+
"""
|
|
18
|
+
for key in keys:
|
|
19
|
+
value = span_attributes.get(key)
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
return value
|
|
22
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "1.0.84"
|
|
@@ -27,7 +27,7 @@ class CodeAttributes(DefaultNoneMixin):
|
|
|
27
27
|
@dataclass
|
|
28
28
|
class Config(DefaultNoneMixin):
|
|
29
29
|
code_attributes: CodeAttributes = field(default_factory=CodeAttributes)
|
|
30
|
-
|
|
30
|
+
container_config: Dict[str, Any] = field(default_factory=dict)
|
|
31
31
|
span_metrics_mode: Optional[str] = None
|
|
32
32
|
|
|
33
33
|
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/http_client.py
RENAMED
|
@@ -23,7 +23,7 @@ from initializer.process_resource import PROCESS_VPID
|
|
|
23
23
|
from google.protobuf.json_format import MessageToDict
|
|
24
24
|
from google.protobuf.message import DecodeError
|
|
25
25
|
|
|
26
|
-
from opamp.config import from_dict, Config
|
|
26
|
+
from opamp.config import from_dict, Config, CodeAttributes
|
|
27
27
|
|
|
28
28
|
# Setup the logger
|
|
29
29
|
opamp_logger = logging.getLogger('odigos')
|
|
@@ -183,8 +183,7 @@ class OpAMPHTTPClient:
|
|
|
183
183
|
self.opamp_connection_event.error = False
|
|
184
184
|
return
|
|
185
185
|
|
|
186
|
-
except Exception
|
|
187
|
-
print(f"[send_first_message_with_retry] Error: {e}", flush=True)
|
|
186
|
+
except Exception:
|
|
188
187
|
pass
|
|
189
188
|
|
|
190
189
|
if attempt < max_retries:
|
|
@@ -304,21 +303,22 @@ class OpAMPHTTPClient:
|
|
|
304
303
|
# If not JSON, just keep it as raw bytes
|
|
305
304
|
decoded_map[key] = body_bytes
|
|
306
305
|
|
|
307
|
-
|
|
308
|
-
if inner is None:
|
|
309
|
-
config = Config() # Return default values for config
|
|
310
|
-
else:
|
|
311
|
-
config = from_dict(Config, inner)
|
|
306
|
+
config = Config()
|
|
312
307
|
|
|
313
308
|
# Extract container_config if present
|
|
314
|
-
|
|
315
|
-
if
|
|
316
|
-
config.
|
|
309
|
+
container_config = decoded_map.get("container_config", None)
|
|
310
|
+
if container_config is not None:
|
|
311
|
+
config.container_config = container_config
|
|
317
312
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
313
|
+
traces = container_config.get('traces', {})
|
|
314
|
+
|
|
315
|
+
span_metrics_mode = traces.get('headSampling', {}).get('spanMetricsMode')
|
|
316
|
+
if span_metrics_mode is not None:
|
|
317
|
+
config.span_metrics_mode = span_metrics_mode
|
|
318
|
+
|
|
319
|
+
code_attrs_raw = traces.get('codeAttributes')
|
|
320
|
+
if isinstance(code_attrs_raw, dict):
|
|
321
|
+
config.code_attributes = from_dict(CodeAttributes, code_attrs_raw)
|
|
322
322
|
|
|
323
323
|
return config
|
|
324
324
|
|
|
@@ -456,14 +456,14 @@ class OpAMPHTTPClient:
|
|
|
456
456
|
|
|
457
457
|
def _extract_sampler_config(self, remote_config):
|
|
458
458
|
"""Extract sampler config from remote config, same logic as update_agent_config"""
|
|
459
|
-
if not hasattr(remote_config, '
|
|
459
|
+
if not hasattr(remote_config, 'container_config') or not remote_config.container_config:
|
|
460
460
|
return None
|
|
461
461
|
|
|
462
462
|
# Extract headSampling configuration from traces.headSampling
|
|
463
|
-
if 'traces' not in remote_config.
|
|
463
|
+
if 'traces' not in remote_config.container_config or 'headSampling' not in remote_config.container_config['traces']:
|
|
464
464
|
return None
|
|
465
465
|
|
|
466
|
-
return remote_config.
|
|
466
|
+
return remote_config.container_config['traces']['headSampling'] or None
|
|
467
467
|
|
|
468
468
|
def get_initial_sampler_config(self):
|
|
469
469
|
"""Get the sampler config from the initial OpAMP message"""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "1.0.82"
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/envs.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/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.82 → odigos_opentelemetry_python-1.0.84}/opamp/anyvalue_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/health_status.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_pb2.py
RENAMED
|
File without changes
|
{odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|