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.
Files changed (35) hide show
  1. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/PKG-INFO +1 -1
  2. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/components.py +4 -5
  3. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/odigos_sampler.py +49 -31
  4. odigos_opentelemetry_python-1.0.84/initializer/odigos_sampler_helpers.py +22 -0
  5. odigos_opentelemetry_python-1.0.84/initializer/version.py +1 -0
  6. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/PKG-INFO +1 -1
  7. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/SOURCES.txt +1 -0
  8. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/config.py +1 -1
  9. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/http_client.py +18 -18
  10. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/pyproject.toml +1 -1
  11. odigos_opentelemetry_python-1.0.82/initializer/version.py +0 -1
  12. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/LICENSE +0 -0
  13. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/README.md +0 -0
  14. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/__init__.py +0 -0
  15. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/distro/__init__.py +0 -0
  16. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/distro/distro.py +0 -0
  17. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/distro/instrumentation_registry.py +0 -0
  18. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/envs.py +0 -0
  19. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/exit_hook.py +0 -0
  20. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/lib_handling.py +0 -0
  21. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/post_instrument.py +0 -0
  22. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/process_resource.py +0 -0
  23. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/schemas/__init__.py +0 -0
  24. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/initializer/schemas/sampling.py +0 -0
  25. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/dependency_links.txt +0 -0
  26. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/entry_points.txt +0 -0
  27. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/requires.txt +0 -0
  28. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/top_level.txt +0 -0
  29. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/__init__.py +0 -0
  30. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/anyvalue_pb2.py +0 -0
  31. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/health_status.py +0 -0
  32. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_pb2.py +0 -0
  33. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_registry.py +0 -0
  34. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/opamp/utils.py +0 -0
  35. {odigos_opentelemetry_python-1.0.82 → odigos_opentelemetry_python-1.0.84}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odigos-opentelemetry-python
3
- Version: 1.0.82
3
+ Version: 1.0.84
4
4
  Summary: Odigos Initializer for Python OpenTelemetry Components
5
5
  Author-email: odigos-io <support@odigos.io>
6
6
  Requires-Python: >=3.9
@@ -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 sample_config is present
271
- if hasattr(conf, 'sample_config') and conf.sample_config:
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.sample_config and 'headSampling' in conf.sample_config['traces']:
275
- head_sampling_config = conf.sample_config['traces']['headSampling']
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 Any, Mapping, Optional, Sequence
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
- @staticmethod
134
- def _get_attribute(span_attributes: Mapping[str, Any], *keys: str):
135
- """Return the first non-None value found for the given attribute keys."""
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
- span_route = span_attributes.get(http_attributes_semconv.HTTP_ROUTE)
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 = self._get_attribute(span_attributes, http_attributes_semconv.HTTP_REQUEST_METHOD, "http.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 = self._get_attribute(span_attributes, "http.target", "url.path")
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(self, http_client_rule: HeadSamplingHttpClientOperationMatcher, span_attributes: Mapping[str, Any]) -> bool:
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" / "http.host" → new semconv: "server.address"
174
- server_address = self._get_attribute(span_attributes, server_attributes_semconv.SERVER_ADDRESS, "net.peer.name", "http.host")
175
- url_template = span_attributes.get("url.template")
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 = self._get_attribute(span_attributes, http_attributes_semconv.HTTP_REQUEST_METHOD, "http.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
- if http_client_rule.get("templatedPath") and http_client_rule["templatedPath"] != url_template:
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
- if http_client_rule.get("templatedPathPrefix") and not (url_template or "").startswith(http_client_rule["templatedPathPrefix"]):
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
- Path param segments ({...}) in the rule are treated as wildcards.
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 len(rule_parts) != len(span_parts):
201
- return False
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
- # Traverse both rule and span segments
204
- for i in range(len(rule_parts)):
205
- # If we hit a path param in the rule, it matches any span segment
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 rule_parts[i] != span_parts[i]:
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odigos-opentelemetry-python
3
- Version: 1.0.82
3
+ Version: 1.0.84
4
4
  Summary: Odigos Initializer for Python OpenTelemetry Components
5
5
  Author-email: odigos-io <support@odigos.io>
6
6
  Requires-Python: >=3.9
@@ -7,6 +7,7 @@ initializer/envs.py
7
7
  initializer/exit_hook.py
8
8
  initializer/lib_handling.py
9
9
  initializer/odigos_sampler.py
10
+ initializer/odigos_sampler_helpers.py
10
11
  initializer/post_instrument.py
11
12
  initializer/process_resource.py
12
13
  initializer/version.py
@@ -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
- sample_config: Dict[str, Any] = field(default_factory=dict)
30
+ container_config: Dict[str, Any] = field(default_factory=dict)
31
31
  span_metrics_mode: Optional[str] = None
32
32
 
33
33
 
@@ -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 as e:
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
- inner = decoded_map.get("", None)
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
- sample_config = decoded_map.get("container_config", None)
315
- if sample_config is not None:
316
- config.sample_config = sample_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
- try:
319
- config.span_metrics_mode = sample_config['traces']['headSampling']['spanMetricsMode']
320
- except (KeyError, TypeError):
321
- pass
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, 'sample_config') or not remote_config.sample_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.sample_config or 'headSampling' not in remote_config.sample_config['traces']:
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.sample_config['traces']['headSampling'] or None
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"""
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odigos-opentelemetry-python"
7
- version = "1.0.82"
7
+ version = "1.0.84"
8
8
  description = "Odigos Initializer for Python OpenTelemetry Components"
9
9
  requires-python = ">=3.9"
10
10
  authors = [
@@ -1 +0,0 @@
1
- VERSION = "1.0.82"