telemetry-dev 0.2.1__tar.gz → 0.2.3__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.
- telemetry_dev-0.2.3/LICENSE +21 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/PKG-INFO +18 -2
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/README.md +15 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/pyproject.toml +3 -2
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/pyproject.toml.orig +3 -2
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_capture.py +2 -27
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_client.py +17 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_config.py +1 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_context.py +84 -2
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_logs.py +16 -4
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_spans.py +65 -23
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/__init__.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_metrics.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_observe.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_processor.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_semconv.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/_serialize.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/otel.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.3}/src/telemetry_dev/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 telemetry.dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telemetry-dev
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics
|
|
5
5
|
Keywords: telemetry,opentelemetry,llm,genai,tracing,observability
|
|
6
6
|
Author: telemetry.dev
|
|
7
7
|
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Classifier: Development Status :: 4 - Beta
|
|
9
10
|
Classifier: Intended Audience :: Developers
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -18,7 +19,7 @@ Requires-Dist: opentelemetry-sdk>=1.35.0,<2
|
|
|
18
19
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.35.0,<2
|
|
19
20
|
Requires-Python: >=3.10
|
|
20
21
|
Project-URL: Homepage, https://telemetry.dev
|
|
21
|
-
Project-URL: Repository, https://github.com/telemetry-dev/
|
|
22
|
+
Project-URL: Repository, https://github.com/telemetry-dev/sdks
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
24
25
|
# telemetry-dev
|
|
@@ -80,9 +81,17 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
80
81
|
| `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Ingest base URL (trailing slashes stripped). |
|
|
81
82
|
| `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
|
|
82
83
|
| `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
|
|
84
|
+
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
|
|
85
|
+
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Probability for `traceidratio` / `parentbased_traceidratio`. |
|
|
83
86
|
|
|
84
87
|
Explicit `init()` arguments take precedence over environment variables.
|
|
85
88
|
|
|
89
|
+
Root spans remain independent by default. `telemetry_dev.init(session_mode="process")` groups
|
|
90
|
+
otherwise uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK
|
|
91
|
+
initialization. Explicit and propagated session IDs take precedence, and valid parent trace
|
|
92
|
+
context is preserved. The generated session ends when the client is shut down or replaced; a
|
|
93
|
+
later `init()` generates a new ID. It is not persisted across processes.
|
|
94
|
+
|
|
86
95
|
## API reference
|
|
87
96
|
|
|
88
97
|
| Name | Description |
|
|
@@ -137,11 +146,18 @@ Explicit `init()` arguments take precedence over environment variables.
|
|
|
137
146
|
| `mask` | `None` | `Callable[[Any, MaskContext], Any]` redaction hook, runs before JSON serialization on input/output/log messages (`MaskContext.key` is the attribute being written). Not applied to correlation identifiers. |
|
|
138
147
|
| `max_attribute_length` | `65536` | Per-content-attribute cap; truncated values get an ASCII `...[truncated]` marker appended. |
|
|
139
148
|
| `span_filter` | `None` | Export predicate `Callable[[ReadableSpan], bool]`. |
|
|
149
|
+
| `sampler` | OTel environment configuration | OTel `Sampler` that overrides environment settings. Session roots use its root policy. Real parents keep their sampling decisions. |
|
|
140
150
|
| `on_error` | `None` | Receives every internal SDK error; the SDK never raises. |
|
|
141
151
|
| `disable_atexit` | `False` | Skip the automatic atexit shutdown. |
|
|
142
152
|
| `timeout` | `10.0` | OTLP HTTP timeout in seconds. |
|
|
143
153
|
| `span_exporter`, `log_exporter`, `metric_reader` | `None` | Test seams / offline mode; any of them enables the client without an API key. |
|
|
144
154
|
|
|
155
|
+
Session roots share trace IDs from the API key and session ID. The hash matches the TypeScript SDK.
|
|
156
|
+
The SDK uses the configured OTel sampling policy, not the synthetic parent's flags.
|
|
157
|
+
For example, `init(sampler=ParentBased(TraceIdRatioBased(0.1)))` samples sessions with OTel's ratio sampler.
|
|
158
|
+
These classes come from `opentelemetry.sdk.trace.sampling`.
|
|
159
|
+
The built-in Python and TypeScript ratio samplers can select different sessions because the OTel algorithms differ.
|
|
160
|
+
|
|
145
161
|
## Auto-metrics
|
|
146
162
|
|
|
147
163
|
Ended spans automatically record two histograms (DELTA temporality, exported every 60s):
|
|
@@ -57,9 +57,17 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
57
57
|
| `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Ingest base URL (trailing slashes stripped). |
|
|
58
58
|
| `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
|
|
59
59
|
| `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
|
|
60
|
+
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
|
|
61
|
+
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Probability for `traceidratio` / `parentbased_traceidratio`. |
|
|
60
62
|
|
|
61
63
|
Explicit `init()` arguments take precedence over environment variables.
|
|
62
64
|
|
|
65
|
+
Root spans remain independent by default. `telemetry_dev.init(session_mode="process")` groups
|
|
66
|
+
otherwise uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK
|
|
67
|
+
initialization. Explicit and propagated session IDs take precedence, and valid parent trace
|
|
68
|
+
context is preserved. The generated session ends when the client is shut down or replaced; a
|
|
69
|
+
later `init()` generates a new ID. It is not persisted across processes.
|
|
70
|
+
|
|
63
71
|
## API reference
|
|
64
72
|
|
|
65
73
|
| Name | Description |
|
|
@@ -114,11 +122,18 @@ Explicit `init()` arguments take precedence over environment variables.
|
|
|
114
122
|
| `mask` | `None` | `Callable[[Any, MaskContext], Any]` redaction hook, runs before JSON serialization on input/output/log messages (`MaskContext.key` is the attribute being written). Not applied to correlation identifiers. |
|
|
115
123
|
| `max_attribute_length` | `65536` | Per-content-attribute cap; truncated values get an ASCII `...[truncated]` marker appended. |
|
|
116
124
|
| `span_filter` | `None` | Export predicate `Callable[[ReadableSpan], bool]`. |
|
|
125
|
+
| `sampler` | OTel environment configuration | OTel `Sampler` that overrides environment settings. Session roots use its root policy. Real parents keep their sampling decisions. |
|
|
117
126
|
| `on_error` | `None` | Receives every internal SDK error; the SDK never raises. |
|
|
118
127
|
| `disable_atexit` | `False` | Skip the automatic atexit shutdown. |
|
|
119
128
|
| `timeout` | `10.0` | OTLP HTTP timeout in seconds. |
|
|
120
129
|
| `span_exporter`, `log_exporter`, `metric_reader` | `None` | Test seams / offline mode; any of them enables the client without an API key. |
|
|
121
130
|
|
|
131
|
+
Session roots share trace IDs from the API key and session ID. The hash matches the TypeScript SDK.
|
|
132
|
+
The SDK uses the configured OTel sampling policy, not the synthetic parent's flags.
|
|
133
|
+
For example, `init(sampler=ParentBased(TraceIdRatioBased(0.1)))` samples sessions with OTel's ratio sampler.
|
|
134
|
+
These classes come from `opentelemetry.sdk.trace.sampling`.
|
|
135
|
+
The built-in Python and TypeScript ratio samplers can select different sessions because the OTel algorithms differ.
|
|
136
|
+
|
|
122
137
|
## Auto-metrics
|
|
123
138
|
|
|
124
139
|
Ended spans automatically record two histograms (DELTA temporality, exported every 60s):
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "telemetry-dev"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
7
8
|
requires-python = ">=3.10"
|
|
8
9
|
keywords = [
|
|
9
10
|
"telemetry",
|
|
@@ -34,7 +35,7 @@ name = "telemetry.dev"
|
|
|
34
35
|
|
|
35
36
|
[project.urls]
|
|
36
37
|
Homepage = "https://telemetry.dev"
|
|
37
|
-
Repository = "https://github.com/telemetry-dev/
|
|
38
|
+
Repository = "https://github.com/telemetry-dev/sdks"
|
|
38
39
|
|
|
39
40
|
[dependency-groups]
|
|
40
41
|
dev = [
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "telemetry-dev"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
7
8
|
requires-python = ">=3.10"
|
|
8
9
|
authors = [{ name = "telemetry.dev" }]
|
|
9
10
|
keywords = ["telemetry", "opentelemetry", "llm", "genai", "tracing", "observability"]
|
|
@@ -25,7 +26,7 @@ dependencies = [
|
|
|
25
26
|
|
|
26
27
|
[project.urls]
|
|
27
28
|
Homepage = "https://telemetry.dev"
|
|
28
|
-
Repository = "https://github.com/telemetry-dev/
|
|
29
|
+
Repository = "https://github.com/telemetry-dev/sdks"
|
|
29
30
|
|
|
30
31
|
[dependency-groups]
|
|
31
32
|
dev = [
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Iterable
|
|
6
|
+
from itertools import chain
|
|
6
7
|
from typing import Final, cast
|
|
7
8
|
|
|
8
9
|
_DEFAULT_MAX_BYTES: Final = 64 * 1024
|
|
@@ -133,33 +134,7 @@ class CaptureBudget:
|
|
|
133
134
|
mapping = cast("dict[object, object]", attributes)
|
|
134
135
|
|
|
135
136
|
if mapping is not None:
|
|
136
|
-
|
|
137
|
-
measured = cls._measure(
|
|
138
|
-
key,
|
|
139
|
-
remaining_bytes=remaining_bytes - byte_count,
|
|
140
|
-
remaining_items=remaining_items - item_count,
|
|
141
|
-
depth=depth + 1,
|
|
142
|
-
seen=seen,
|
|
143
|
-
)
|
|
144
|
-
if measured is None:
|
|
145
|
-
return None
|
|
146
|
-
child_bytes, child_items = measured
|
|
147
|
-
byte_count += child_bytes
|
|
148
|
-
item_count += child_items
|
|
149
|
-
|
|
150
|
-
measured = cls._measure(
|
|
151
|
-
child,
|
|
152
|
-
remaining_bytes=remaining_bytes - byte_count,
|
|
153
|
-
remaining_items=remaining_items - item_count,
|
|
154
|
-
depth=depth + 1,
|
|
155
|
-
seen=seen,
|
|
156
|
-
)
|
|
157
|
-
if measured is None:
|
|
158
|
-
return None
|
|
159
|
-
child_bytes, child_items = measured
|
|
160
|
-
byte_count += child_bytes
|
|
161
|
-
item_count += child_items
|
|
162
|
-
return byte_count, item_count
|
|
137
|
+
children = chain.from_iterable(mapping.items())
|
|
163
138
|
|
|
164
139
|
for child in children:
|
|
165
140
|
measured = cls._measure(
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import atexit
|
|
4
4
|
import threading
|
|
5
|
+
import uuid
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -26,17 +27,20 @@ from opentelemetry.sdk.metrics.export import (
|
|
|
26
27
|
from opentelemetry.sdk.resources import Resource
|
|
27
28
|
from opentelemetry.sdk.trace import ReadableSpan, SpanLimits, TracerProvider
|
|
28
29
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
30
|
+
from opentelemetry.sdk.trace.sampling import Sampler
|
|
29
31
|
from opentelemetry.trace import Tracer
|
|
30
32
|
|
|
31
33
|
from ._config import (
|
|
32
34
|
SDK_VERSION,
|
|
33
35
|
LogLevelOption,
|
|
34
36
|
ResolvedConfig,
|
|
37
|
+
SessionMode,
|
|
35
38
|
configure_logger,
|
|
36
39
|
logger,
|
|
37
40
|
report_error,
|
|
38
41
|
resolve_config,
|
|
39
42
|
)
|
|
43
|
+
from ._context import SessionSampler
|
|
40
44
|
from ._metrics import GuardedOTLPMetricExporter, MetricsRecorder
|
|
41
45
|
from ._processor import ExportMode, StampingSpanProcessor
|
|
42
46
|
from ._semconv import SCOPE_NAME
|
|
@@ -121,6 +125,7 @@ class Client:
|
|
|
121
125
|
*,
|
|
122
126
|
config: ResolvedConfig,
|
|
123
127
|
enabled: bool,
|
|
128
|
+
session_mode: SessionMode = "explicit",
|
|
124
129
|
register_global: bool = False,
|
|
125
130
|
export_mode: ExportMode = "batched",
|
|
126
131
|
capture_input: bool = True,
|
|
@@ -128,6 +133,7 @@ class Client:
|
|
|
128
133
|
mask: Mask | None = None,
|
|
129
134
|
max_attribute_length: int = 65536,
|
|
130
135
|
span_filter: Callable[[ReadableSpan], bool] | None = None,
|
|
136
|
+
sampler: Sampler | None = None,
|
|
131
137
|
on_error: Callable[[BaseException], None] | None = None,
|
|
132
138
|
disable_atexit: bool = False,
|
|
133
139
|
timeout: float = 10.0,
|
|
@@ -142,6 +148,9 @@ class Client:
|
|
|
142
148
|
self.mask = mask
|
|
143
149
|
self.max_attribute_length = max_attribute_length
|
|
144
150
|
self.on_error = on_error
|
|
151
|
+
self._process_session_id: str | None = (
|
|
152
|
+
str(uuid.uuid4()) if enabled and session_mode == "process" else None
|
|
153
|
+
)
|
|
145
154
|
self._shutdown = False
|
|
146
155
|
self._atexit_registered = False
|
|
147
156
|
|
|
@@ -170,12 +179,14 @@ class Client:
|
|
|
170
179
|
# the backstop can use the same cap for raw attributes set outside the funnel.
|
|
171
180
|
self._tracer_provider = TracerProvider(
|
|
172
181
|
resource=resource,
|
|
182
|
+
sampler=sampler,
|
|
173
183
|
shutdown_on_exit=False,
|
|
174
184
|
span_limits=SpanLimits(
|
|
175
185
|
max_attribute_length=max_attribute_length,
|
|
176
186
|
max_span_attribute_length=max_attribute_length,
|
|
177
187
|
),
|
|
178
188
|
)
|
|
189
|
+
self._tracer_provider.sampler = SessionSampler(self._tracer_provider.sampler)
|
|
179
190
|
|
|
180
191
|
# Without an api key (enabled via a test seam), never construct real network
|
|
181
192
|
# exporters — they would POST to the ingest with a bogus Authorization header.
|
|
@@ -311,6 +322,7 @@ def init(
|
|
|
311
322
|
environment: str | None = None,
|
|
312
323
|
service_name: str | None = None,
|
|
313
324
|
enabled: bool = True,
|
|
325
|
+
session_mode: SessionMode = "explicit",
|
|
314
326
|
register_global: bool = False,
|
|
315
327
|
export_mode: ExportMode = "batched",
|
|
316
328
|
log_level: LogLevelOption = "warn",
|
|
@@ -319,6 +331,7 @@ def init(
|
|
|
319
331
|
mask: Mask | None = None,
|
|
320
332
|
max_attribute_length: int = 65536,
|
|
321
333
|
span_filter: Callable[[ReadableSpan], bool] | None = None,
|
|
334
|
+
sampler: Sampler | None = None,
|
|
322
335
|
on_error: Callable[[BaseException], None] | None = None,
|
|
323
336
|
disable_atexit: bool = False,
|
|
324
337
|
timeout: float = 10.0,
|
|
@@ -342,9 +355,12 @@ def init(
|
|
|
342
355
|
logger.debug("telemetry-dev disabled: no API key")
|
|
343
356
|
|
|
344
357
|
try:
|
|
358
|
+
if session_mode not in ("explicit", "process"):
|
|
359
|
+
raise ValueError(f"invalid session_mode: {session_mode!r}")
|
|
345
360
|
client = Client(
|
|
346
361
|
config=config,
|
|
347
362
|
enabled=effective_enabled,
|
|
363
|
+
session_mode=session_mode,
|
|
348
364
|
register_global=register_global,
|
|
349
365
|
export_mode=export_mode,
|
|
350
366
|
capture_input=capture_input,
|
|
@@ -352,6 +368,7 @@ def init(
|
|
|
352
368
|
mask=mask,
|
|
353
369
|
max_attribute_length=max_attribute_length,
|
|
354
370
|
span_filter=span_filter,
|
|
371
|
+
sampler=sampler,
|
|
355
372
|
on_error=on_error,
|
|
356
373
|
disable_atexit=disable_atexit,
|
|
357
374
|
timeout=timeout,
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import hashlib
|
|
4
|
+
from collections.abc import Generator, Mapping, Sequence
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from typing import Any, cast
|
|
6
7
|
|
|
7
8
|
from opentelemetry import context as otel_context
|
|
8
9
|
from opentelemetry import trace
|
|
9
10
|
from opentelemetry.context import Context
|
|
10
|
-
from opentelemetry.trace import
|
|
11
|
+
from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult
|
|
12
|
+
from opentelemetry.trace import (
|
|
13
|
+
Link,
|
|
14
|
+
NonRecordingSpan,
|
|
15
|
+
SpanContext,
|
|
16
|
+
SpanKind,
|
|
17
|
+
TraceFlags,
|
|
18
|
+
TraceState,
|
|
19
|
+
)
|
|
11
20
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
21
|
+
from opentelemetry.util.types import Attributes
|
|
12
22
|
|
|
13
23
|
from ._config import logger
|
|
14
24
|
from ._semconv import (
|
|
@@ -26,6 +36,41 @@ _PROPAGATOR = TraceContextTextMapPropagator()
|
|
|
26
36
|
ParentInput = str | Context | SpanContext | None
|
|
27
37
|
|
|
28
38
|
|
|
39
|
+
class _SessionParent(NonRecordingSpan):
|
|
40
|
+
def __init__(self, span_context: SpanContext, context: Context) -> None:
|
|
41
|
+
super().__init__(span_context)
|
|
42
|
+
self.parent_context = context
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SessionSampler(Sampler):
|
|
46
|
+
"""Pair with with_session_parent to sample roots using the provider's real policy."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, inner: Sampler) -> None:
|
|
49
|
+
self.inner = inner
|
|
50
|
+
|
|
51
|
+
def should_sample(
|
|
52
|
+
self,
|
|
53
|
+
parent_context: Context | None,
|
|
54
|
+
trace_id: int,
|
|
55
|
+
name: str,
|
|
56
|
+
kind: SpanKind | None = None,
|
|
57
|
+
attributes: Attributes = None,
|
|
58
|
+
links: Sequence[Link] | None = None,
|
|
59
|
+
trace_state: TraceState | None = None,
|
|
60
|
+
) -> SamplingResult:
|
|
61
|
+
parent = trace.get_current_span(parent_context)
|
|
62
|
+
if isinstance(parent, _SessionParent):
|
|
63
|
+
parent_context = trace.set_span_in_context(
|
|
64
|
+
trace.get_current_span(parent.parent_context), parent_context
|
|
65
|
+
)
|
|
66
|
+
return self.inner.should_sample(
|
|
67
|
+
parent_context, trace_id, name, kind, attributes, links, trace_state
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def get_description(self) -> str:
|
|
71
|
+
return f"SessionSampler{{{self.inner.get_description()}}}"
|
|
72
|
+
|
|
73
|
+
|
|
29
74
|
def propagated_attributes(context: Context | None = None) -> dict[str, AttributeValue]:
|
|
30
75
|
value = otel_context.get_value(_PROPAGATED_KEY, context=context)
|
|
31
76
|
if isinstance(value, Mapping):
|
|
@@ -33,6 +78,13 @@ def propagated_attributes(context: Context | None = None) -> dict[str, Attribute
|
|
|
33
78
|
return {}
|
|
34
79
|
|
|
35
80
|
|
|
81
|
+
def context_with_propagated_attributes(
|
|
82
|
+
attributes: Mapping[str, AttributeValue], context: Context | None = None
|
|
83
|
+
) -> Context:
|
|
84
|
+
base = context if context is not None else otel_context.get_current()
|
|
85
|
+
return otel_context.set_value(_PROPAGATED_KEY, attributes, context=base)
|
|
86
|
+
|
|
87
|
+
|
|
36
88
|
@contextmanager
|
|
37
89
|
def propagate_attributes(
|
|
38
90
|
*,
|
|
@@ -99,3 +151,33 @@ def context_from_parent(parent: ParentInput) -> Context | None:
|
|
|
99
151
|
if ambient:
|
|
100
152
|
ctx = otel_context.set_value(_PROPAGATED_KEY, ambient, context=ctx)
|
|
101
153
|
return ctx
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def session_span_context(api_key: str | None, session_id: str) -> SpanContext:
|
|
157
|
+
"""Deterministic remote parent for a session, byte-identical to the TS `sessionSpanContext`:
|
|
158
|
+
trace id = SHA-256(api_key ‖ 0x00 ‖ session_id)[0:16], parent span id = digest[16:24].
|
|
159
|
+
The flags are not a sampling decision; pair with_session_parent with SessionSampler."""
|
|
160
|
+
text = f"{api_key or ''}\0{session_id}"
|
|
161
|
+
# Match TextEncoder: surrogate pairs combine, lone surrogates become U+FFFD.
|
|
162
|
+
text = text.encode("utf-16-le", "surrogatepass").decode("utf-16-le", "replace")
|
|
163
|
+
digest = hashlib.sha256(text.encode()).digest()
|
|
164
|
+
return SpanContext(
|
|
165
|
+
trace_id=int.from_bytes(digest[:16], "big"),
|
|
166
|
+
span_id=int.from_bytes(digest[16:24], "big"),
|
|
167
|
+
is_remote=True,
|
|
168
|
+
trace_flags=TraceFlags(TraceFlags.DEFAULT),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def with_session_parent(
|
|
173
|
+
context: Context | None, session_id: str | None, api_key: str | None
|
|
174
|
+
) -> Context | None:
|
|
175
|
+
"""Parent a root under the session; requires SessionSampler on the provider."""
|
|
176
|
+
if not api_key or not session_id:
|
|
177
|
+
return context
|
|
178
|
+
if trace.get_current_span(context).get_span_context().is_valid:
|
|
179
|
+
return context
|
|
180
|
+
base = context if context is not None else otel_context.get_current()
|
|
181
|
+
return trace.set_span_in_context(
|
|
182
|
+
_SessionParent(session_span_context(api_key, session_id), base), base
|
|
183
|
+
)
|
|
@@ -7,8 +7,8 @@ from opentelemetry._logs import SeverityNumber
|
|
|
7
7
|
|
|
8
8
|
from ._client import get_client
|
|
9
9
|
from ._config import logger
|
|
10
|
-
from ._context import propagated_attributes
|
|
11
|
-
from ._semconv import SEVERITY, LogLevel
|
|
10
|
+
from ._context import context_with_propagated_attributes, propagated_attributes
|
|
11
|
+
from ._semconv import ATTR_SESSION_ID, SEVERITY, LogLevel
|
|
12
12
|
from ._serialize import AttributeValue, coerce_attr_value
|
|
13
13
|
|
|
14
14
|
|
|
@@ -41,9 +41,21 @@ def log(
|
|
|
41
41
|
)
|
|
42
42
|
if attr is not None:
|
|
43
43
|
attrs[key] = attr
|
|
44
|
+
propagated = propagated_attributes()
|
|
45
|
+
process_session_id = client._process_session_id # pyright: ignore[reportPrivateUsage]
|
|
46
|
+
explicit_session = attributes is not None and ATTR_SESSION_ID in attributes
|
|
47
|
+
if (
|
|
48
|
+
not explicit_session
|
|
49
|
+
and ATTR_SESSION_ID not in propagated
|
|
50
|
+
and process_session_id is not None
|
|
51
|
+
):
|
|
52
|
+
propagated[ATTR_SESSION_ID] = process_session_id
|
|
53
|
+
context = otel_context.get_current()
|
|
54
|
+
if propagated:
|
|
55
|
+
context = context_with_propagated_attributes(propagated, context)
|
|
44
56
|
# Propagated correlation attrs win on key collisions, matching the span-processor
|
|
45
57
|
# stamping model and the TypeScript SDK.
|
|
46
|
-
for key, value in
|
|
58
|
+
for key, value in propagated.items():
|
|
47
59
|
attr = coerce_attr_value(
|
|
48
60
|
value,
|
|
49
61
|
max_len=client.max_attribute_length,
|
|
@@ -54,7 +66,7 @@ def log(
|
|
|
54
66
|
attrs[key] = attr
|
|
55
67
|
body = client.serialize(message, "log.message") or ""
|
|
56
68
|
client.otel_logger.emit(
|
|
57
|
-
context=
|
|
69
|
+
context=context,
|
|
58
70
|
severity_number=SeverityNumber(severity),
|
|
59
71
|
severity_text=normalized.upper(),
|
|
60
72
|
body=body,
|
|
@@ -17,7 +17,13 @@ from opentelemetry.util import types as otel_types
|
|
|
17
17
|
|
|
18
18
|
from ._client import Client, get_client
|
|
19
19
|
from ._config import logger
|
|
20
|
-
from ._context import
|
|
20
|
+
from ._context import (
|
|
21
|
+
context_from_parent,
|
|
22
|
+
context_with_propagated_attributes,
|
|
23
|
+
propagated_attributes,
|
|
24
|
+
traceparent_of,
|
|
25
|
+
with_session_parent,
|
|
26
|
+
)
|
|
21
27
|
from ._semconv import (
|
|
22
28
|
ATTR_AGENT_ID,
|
|
23
29
|
ATTR_AGENT_NAME,
|
|
@@ -30,6 +36,7 @@ from ._semconv import (
|
|
|
30
36
|
ATTR_REQUEST_MODEL,
|
|
31
37
|
ATTR_RESPONSE_ID,
|
|
32
38
|
ATTR_RESPONSE_MODEL,
|
|
39
|
+
ATTR_SESSION_ID,
|
|
33
40
|
ATTR_SYSTEM_INSTRUCTIONS,
|
|
34
41
|
ATTR_TIME_TO_FIRST_CHUNK,
|
|
35
42
|
ATTR_TOOL_CALL_ID,
|
|
@@ -145,14 +152,12 @@ def _usage_attrs(usage: Mapping[str, Any]) -> dict[str, AttributeValue]:
|
|
|
145
152
|
return attrs
|
|
146
153
|
|
|
147
154
|
|
|
148
|
-
def
|
|
155
|
+
def _fields_to_attributes(
|
|
156
|
+
client: Client, state: _SpanState, fields: dict[str, Any]
|
|
157
|
+
) -> dict[str, otel_types.AttributeValue]:
|
|
149
158
|
"""Map the shared snake_case field set onto gen_ai.* attributes; raw `attributes` merge last."""
|
|
150
159
|
attrs: dict[str, otel_types.AttributeValue] = {}
|
|
151
160
|
|
|
152
|
-
name = fields.get("name")
|
|
153
|
-
if name is not None:
|
|
154
|
-
span.update_name(name)
|
|
155
|
-
|
|
156
161
|
for field, attr in (
|
|
157
162
|
("model", ATTR_REQUEST_MODEL),
|
|
158
163
|
("provider", ATTR_PROVIDER),
|
|
@@ -222,6 +227,14 @@ def _apply_fields(client: Client, span: Span, state: _SpanState, fields: dict[st
|
|
|
222
227
|
if attr is not None:
|
|
223
228
|
attrs[key] = attr
|
|
224
229
|
|
|
230
|
+
return attrs
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _apply_fields(client: Client, span: Span, state: _SpanState, fields: dict[str, Any]) -> None:
|
|
234
|
+
name = fields.get("name")
|
|
235
|
+
if name is not None:
|
|
236
|
+
span.update_name(name)
|
|
237
|
+
attrs = _fields_to_attributes(client, state, fields)
|
|
225
238
|
if attrs:
|
|
226
239
|
span.set_attributes(attrs)
|
|
227
240
|
|
|
@@ -305,10 +318,17 @@ class SpanHandle:
|
|
|
305
318
|
"""Handle around an OTel span. Use as a context manager to activate the span in the
|
|
306
319
|
current context, or keep it detached and call .end() manually."""
|
|
307
320
|
|
|
308
|
-
def __init__(
|
|
321
|
+
def __init__(
|
|
322
|
+
self,
|
|
323
|
+
span: Span,
|
|
324
|
+
client: Client | None,
|
|
325
|
+
state: _SpanState | None,
|
|
326
|
+
context: Context | None = None,
|
|
327
|
+
) -> None:
|
|
309
328
|
self.span = span
|
|
310
329
|
self._client = client
|
|
311
330
|
self._state = state
|
|
331
|
+
self._context = context
|
|
312
332
|
self._context_token: Token[Context] | None = None
|
|
313
333
|
self._ended = False
|
|
314
334
|
|
|
@@ -468,7 +488,9 @@ class SpanHandle:
|
|
|
468
488
|
|
|
469
489
|
def __enter__(self) -> SpanHandle:
|
|
470
490
|
if self._client is not None and self._state is not None:
|
|
471
|
-
self._context_token = otel_context.attach(
|
|
491
|
+
self._context_token = otel_context.attach(
|
|
492
|
+
trace.set_span_in_context(self.span, self._context)
|
|
493
|
+
)
|
|
472
494
|
return self
|
|
473
495
|
|
|
474
496
|
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]:
|
|
@@ -538,19 +560,6 @@ def start_span(
|
|
|
538
560
|
capture_input=client.capture_input if capture_input is None else capture_input,
|
|
539
561
|
capture_output=client.capture_output if capture_output is None else capture_output,
|
|
540
562
|
)
|
|
541
|
-
initial: dict[str, otel_types.AttributeValue] = {ATTR_OPERATION: operation}
|
|
542
|
-
if operation == "execute_tool":
|
|
543
|
-
initial[ATTR_TOOL_NAME] = name
|
|
544
|
-
elif operation == "invoke_agent":
|
|
545
|
-
initial[ATTR_AGENT_NAME] = name
|
|
546
|
-
span = client.tracer.start_span(
|
|
547
|
-
name,
|
|
548
|
-
context=context_from_parent(parent),
|
|
549
|
-
attributes=initial,
|
|
550
|
-
start_time=_to_ns(start_time),
|
|
551
|
-
)
|
|
552
|
-
_SPAN_STATES[span] = state
|
|
553
|
-
handle = SpanHandle(span, client, state)
|
|
554
563
|
fields = _collect_fields(
|
|
555
564
|
input=input,
|
|
556
565
|
output=output,
|
|
@@ -580,8 +589,41 @@ def start_span(
|
|
|
580
589
|
metadata=metadata,
|
|
581
590
|
attributes=attributes,
|
|
582
591
|
)
|
|
583
|
-
|
|
584
|
-
|
|
592
|
+
attrs = _fields_to_attributes(client, state, fields)
|
|
593
|
+
initial: dict[str, otel_types.AttributeValue] = {ATTR_OPERATION: operation}
|
|
594
|
+
if operation == "execute_tool":
|
|
595
|
+
initial[ATTR_TOOL_NAME] = name
|
|
596
|
+
elif operation == "invoke_agent":
|
|
597
|
+
initial[ATTR_AGENT_NAME] = name
|
|
598
|
+
ctx = context_from_parent(parent)
|
|
599
|
+
propagated = propagated_attributes(ctx)
|
|
600
|
+
explicit_session_id = (attributes or {}).get(ATTR_SESSION_ID)
|
|
601
|
+
session_id = (
|
|
602
|
+
explicit_session_id
|
|
603
|
+
if isinstance(explicit_session_id, str)
|
|
604
|
+
else propagated.get(ATTR_SESSION_ID)
|
|
605
|
+
)
|
|
606
|
+
if not isinstance(session_id, str):
|
|
607
|
+
session_id = client._process_session_id # pyright: ignore[reportPrivateUsage]
|
|
608
|
+
if isinstance(session_id, str):
|
|
609
|
+
propagated[ATTR_SESSION_ID] = session_id
|
|
610
|
+
ctx = context_with_propagated_attributes(propagated, ctx)
|
|
611
|
+
ctx = with_session_parent(
|
|
612
|
+
ctx, session_id if isinstance(session_id, str) else None, client.config.api_key
|
|
613
|
+
)
|
|
614
|
+
initial.update(propagated)
|
|
615
|
+
initial.update(attrs)
|
|
616
|
+
span = client.tracer.start_span(
|
|
617
|
+
name,
|
|
618
|
+
context=ctx,
|
|
619
|
+
attributes=initial,
|
|
620
|
+
start_time=_to_ns(start_time),
|
|
621
|
+
)
|
|
622
|
+
_SPAN_STATES[span] = state
|
|
623
|
+
handle = SpanHandle(span, client, state, ctx)
|
|
624
|
+
# The processor stamps propagation on start; explicit fields still win.
|
|
625
|
+
if attrs:
|
|
626
|
+
span.set_attributes(attrs)
|
|
585
627
|
return handle
|
|
586
628
|
except BaseException as exc:
|
|
587
629
|
client.report("start_span failed", exc)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|