odigos-opentelemetry-python 1.0.83__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.83 → odigos_opentelemetry_python-1.0.84}/PKG-INFO +1 -1
  2. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/odigos_sampler.py +49 -31
  3. odigos_opentelemetry_python-1.0.84/initializer/odigos_sampler_helpers.py +22 -0
  4. odigos_opentelemetry_python-1.0.84/initializer/version.py +1 -0
  5. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/PKG-INFO +1 -1
  6. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/SOURCES.txt +1 -0
  7. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/pyproject.toml +1 -1
  8. odigos_opentelemetry_python-1.0.83/initializer/version.py +0 -1
  9. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/LICENSE +0 -0
  10. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/README.md +0 -0
  11. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/__init__.py +0 -0
  12. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/components.py +0 -0
  13. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/distro/__init__.py +0 -0
  14. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/distro/distro.py +0 -0
  15. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/distro/instrumentation_registry.py +0 -0
  16. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/envs.py +0 -0
  17. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/exit_hook.py +0 -0
  18. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/lib_handling.py +0 -0
  19. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/post_instrument.py +0 -0
  20. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/process_resource.py +0 -0
  21. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/schemas/__init__.py +0 -0
  22. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/initializer/schemas/sampling.py +0 -0
  23. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/dependency_links.txt +0 -0
  24. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/entry_points.txt +0 -0
  25. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/requires.txt +0 -0
  26. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/odigos_opentelemetry_python.egg-info/top_level.txt +0 -0
  27. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/__init__.py +0 -0
  28. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/anyvalue_pb2.py +0 -0
  29. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/config.py +0 -0
  30. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/health_status.py +0 -0
  31. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/http_client.py +0 -0
  32. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_pb2.py +0 -0
  33. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/opamp_registry.py +0 -0
  34. {odigos_opentelemetry_python-1.0.83 → odigos_opentelemetry_python-1.0.84}/opamp/utils.py +0 -0
  35. {odigos_opentelemetry_python-1.0.83 → 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.83
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
@@ -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.83
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odigos-opentelemetry-python"
7
- version = "1.0.83"
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.83"