telemetry-dev 0.2.2__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.
@@ -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.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/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
@@ -85,6 +86,12 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
85
86
 
86
87
  Explicit `init()` arguments take precedence over environment variables.
87
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
+
88
95
  ## API reference
89
96
 
90
97
  | Name | Description |
@@ -62,6 +62,12 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
62
62
 
63
63
  Explicit `init()` arguments take precedence over environment variables.
64
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
+
65
71
  ## API reference
66
72
 
67
73
  | Name | Description |
@@ -1,9 +1,10 @@
1
1
  [project]
2
2
  name = "telemetry-dev"
3
- version = "0.2.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/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.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/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
- for key, child in mapping.items():
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
 
@@ -33,6 +34,7 @@ from ._config import (
33
34
  SDK_VERSION,
34
35
  LogLevelOption,
35
36
  ResolvedConfig,
37
+ SessionMode,
36
38
  configure_logger,
37
39
  logger,
38
40
  report_error,
@@ -123,6 +125,7 @@ class Client:
123
125
  *,
124
126
  config: ResolvedConfig,
125
127
  enabled: bool,
128
+ session_mode: SessionMode = "explicit",
126
129
  register_global: bool = False,
127
130
  export_mode: ExportMode = "batched",
128
131
  capture_input: bool = True,
@@ -145,6 +148,9 @@ class Client:
145
148
  self.mask = mask
146
149
  self.max_attribute_length = max_attribute_length
147
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
+ )
148
154
  self._shutdown = False
149
155
  self._atexit_registered = False
150
156
 
@@ -316,6 +322,7 @@ def init(
316
322
  environment: str | None = None,
317
323
  service_name: str | None = None,
318
324
  enabled: bool = True,
325
+ session_mode: SessionMode = "explicit",
319
326
  register_global: bool = False,
320
327
  export_mode: ExportMode = "batched",
321
328
  log_level: LogLevelOption = "warn",
@@ -348,9 +355,12 @@ def init(
348
355
  logger.debug("telemetry-dev disabled: no API key")
349
356
 
350
357
  try:
358
+ if session_mode not in ("explicit", "process"):
359
+ raise ValueError(f"invalid session_mode: {session_mode!r}")
351
360
  client = Client(
352
361
  config=config,
353
362
  enabled=effective_enabled,
363
+ session_mode=session_mode,
354
364
  register_global=register_global,
355
365
  export_mode=export_mode,
356
366
  capture_input=capture_input,
@@ -22,6 +22,7 @@ def _resolve_version() -> str:
22
22
  SDK_VERSION = _resolve_version()
23
23
 
24
24
  LogLevelOption = Literal["debug", "info", "warn", "error", "silent"]
25
+ SessionMode = Literal["explicit", "process"]
25
26
 
26
27
  logger = logging.getLogger("telemetry_dev")
27
28
 
@@ -78,6 +78,13 @@ def propagated_attributes(context: Context | None = None) -> dict[str, Attribute
78
78
  return {}
79
79
 
80
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
+
81
88
  @contextmanager
82
89
  def propagate_attributes(
83
90
  *,
@@ -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 propagated_attributes().items():
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=otel_context.get_current(),
69
+ context=context,
58
70
  severity_number=SeverityNumber(severity),
59
71
  severity_text=normalized.upper(),
60
72
  body=body,
@@ -19,6 +19,7 @@ from ._client import Client, get_client
19
19
  from ._config import logger
20
20
  from ._context import (
21
21
  context_from_parent,
22
+ context_with_propagated_attributes,
22
23
  propagated_attributes,
23
24
  traceparent_of,
24
25
  with_session_parent,
@@ -317,10 +318,17 @@ class SpanHandle:
317
318
  """Handle around an OTel span. Use as a context manager to activate the span in the
318
319
  current context, or keep it detached and call .end() manually."""
319
320
 
320
- def __init__(self, span: Span, client: Client | None, state: _SpanState | None) -> None:
321
+ def __init__(
322
+ self,
323
+ span: Span,
324
+ client: Client | None,
325
+ state: _SpanState | None,
326
+ context: Context | None = None,
327
+ ) -> None:
321
328
  self.span = span
322
329
  self._client = client
323
330
  self._state = state
331
+ self._context = context
324
332
  self._context_token: Token[Context] | None = None
325
333
  self._ended = False
326
334
 
@@ -480,7 +488,9 @@ class SpanHandle:
480
488
 
481
489
  def __enter__(self) -> SpanHandle:
482
490
  if self._client is not None and self._state is not None:
483
- self._context_token = otel_context.attach(trace.set_span_in_context(self.span))
491
+ self._context_token = otel_context.attach(
492
+ trace.set_span_in_context(self.span, self._context)
493
+ )
484
494
  return self
485
495
 
486
496
  def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]:
@@ -593,6 +603,11 @@ def start_span(
593
603
  if isinstance(explicit_session_id, str)
594
604
  else propagated.get(ATTR_SESSION_ID)
595
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)
596
611
  ctx = with_session_parent(
597
612
  ctx, session_id if isinstance(session_id, str) else None, client.config.api_key
598
613
  )
@@ -605,7 +620,7 @@ def start_span(
605
620
  start_time=_to_ns(start_time),
606
621
  )
607
622
  _SPAN_STATES[span] = state
608
- handle = SpanHandle(span, client, state)
623
+ handle = SpanHandle(span, client, state, ctx)
609
624
  # The processor stamps propagation on start; explicit fields still win.
610
625
  if attrs:
611
626
  span.set_attributes(attrs)