telemetry-dev 0.2.10__tar.gz → 0.2.12__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 (22) hide show
  1. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/PKG-INFO +92 -9
  2. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/README.md +90 -8
  3. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/pyproject.toml +2 -1
  4. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/pyproject.toml.orig +2 -1
  5. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/__init__.py +18 -0
  6. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_client.py +10 -70
  7. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_config.py +6 -2
  8. telemetry_dev-0.2.12/src/telemetry_dev/_export.py +100 -0
  9. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_metrics.py +12 -9
  10. telemetry_dev-0.2.12/src/telemetry_dev/_prompts.py +679 -0
  11. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_semconv.py +5 -0
  12. telemetry_dev-0.2.12/src/telemetry_dev/_serialize.py +220 -0
  13. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_spans.py +30 -12
  14. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/otel.py +6 -0
  15. telemetry_dev-0.2.10/src/telemetry_dev/_serialize.py +0 -102
  16. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/LICENSE +0 -0
  17. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_capture.py +0 -0
  18. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_context.py +0 -0
  19. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_logs.py +0 -0
  20. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_observe.py +0 -0
  21. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_processor.py +0 -0
  22. {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telemetry-dev
3
- Version: 0.2.10
3
+ Version: 0.2.12
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
@@ -17,6 +17,7 @@ Classifier: Typing :: Typed
17
17
  Requires-Dist: opentelemetry-api>=1.39.0,<2
18
18
  Requires-Dist: opentelemetry-sdk>=1.39.0,<2
19
19
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.39.0,<2
20
+ Requires-Dist: rfc8785>=0.1.4
20
21
  Requires-Python: >=3.10
21
22
  Project-URL: Homepage, https://telemetry.dev
22
23
  Project-URL: Repository, https://github.com/telemetry-dev/telemetry
@@ -80,7 +81,7 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
80
81
  | Variable | Default | Purpose |
81
82
  | --- | --- | --- |
82
83
  | `TELEMETRY_DEV_API_KEY` | — | Ingest key (`td_live_...`). Absent = SDK is a no-op. |
83
- | `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Ingest base URL (trailing slashes stripped). |
84
+ | `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Base URL for telemetry export and `get_prompt` (trailing slashes stripped). |
84
85
  | `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
85
86
  | `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
86
87
  | `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
@@ -107,6 +108,8 @@ later `init()` generates a new ID. It is not persisted across processes.
107
108
  | `propagate_attributes(*, user_id=, session_id=, metadata=)` | Context manager stamping `user.id` / `gen_ai.conversation.id` / `td.metadata.*` on every span and log record started inside (threads/asyncio included via contextvars). |
108
109
  | `log(message, *, level="info", event_name=None, attributes=None)` | Emit an OTLP log record to `/v1/logs`, correlated with the current trace. Levels: `debug`/`info`/`warn`/`error` (`"warning"` is accepted as an alias of `warn`). |
109
110
  | `get_traceparent() -> str \| None` | W3C traceparent of the current context. |
111
+ | `get_prompt(name, *, label=, version=, type="chat", cache_ttl_seconds=60, fallback=, api_key=, base_url=)` / `get_prompt_async(...)` | Fetch a prompt by label or version; see [Prompts](#prompts). |
112
+ | `with_prompt(prompt)` | Context manager recording the prompt on each model call started inside. |
110
113
  | `flush(timeout_s=10.0)` / `shutdown(timeout_s=10.0)` | Force-flush / tear down traces + logs + metrics. Shutdown also runs atexit unless `disable_atexit=True`. |
111
114
  | `TelemetrySpanProcessor` / `telemetry_dev.otel.create_telemetry_span_exporter` | Bring-your-own-OTel helpers (below). |
112
115
  | `MaskContext`, `Usage`, `SpanHandle`, `Client`, `NOT_GIVEN` | Supporting types. |
@@ -151,6 +154,17 @@ the core SDK with provider integrations to enable the new metric.
151
154
  `SpanContext`), `start_time`, and per-call `capture_input` / `capture_output` overrides;
152
155
  `end()` additionally accepts `end_time`.
153
156
 
157
+ Core SDK error recording keeps failure status, error type, and an exception event. It includes
158
+ `exception.message` and `exception.stacktrace` only when both effective `capture_input` and
159
+ `capture_output` are true, including per-span overrides: exceptions can echo either inputs or
160
+ outputs. The policy is per span: when an error propagates to an enclosing span whose capture is on,
161
+ that span records its message and stack. Included fields pass through `mask` with their exception
162
+ attribute keys, then serialization and truncation. A truncated `exception.stacktrace` keeps its
163
+ end, where Python normally puts the innermost frame and the exception line. Mask and serialization failures drop the affected field and report generic errors
164
+ to `on_error` and SDK diagnostics without the original message, traceback, or exception chain.
165
+ The original application error is raised unchanged. These controls do not scrub error type names,
166
+ metadata, identifiers, resource attributes, or events written directly through raw OpenTelemetry APIs.
167
+
154
168
  ### init() options
155
169
 
156
170
  | Option | Default | Purpose |
@@ -166,20 +180,22 @@ the core SDK with provider integrations to enable the new metric.
166
180
  | `max_attribute_length` | `65536` | Non-negative integer per-content-attribute cap. Invalid values call `on_error`, do not raise into application code, and install a disabled client; truncated values use the ASCII `...[truncated]` marker, or its prefix when the cap is shorter than the marker. The cap also applies, with the marker, to string metadata and log attributes; any other string attribute is cut at the cap without a marker, except that a cap of `0` leaves those at 65536. |
167
181
  | `span_filter` | `None` | Export predicate `Callable[[ReadableSpan], bool]`. |
168
182
  | `sampler` | OTel environment configuration | OTel `Sampler` that overrides environment settings. Session roots use its root policy. Real parents keep their sampling decisions. |
169
- | `on_error` | `None` | Receives internal SDK errors; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never raises them into application code. |
183
+ | `on_error` | `None` | Receives internal SDK errors; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never raises them into application code. A series of failed exports, for example during an ingest outage, is one report to `on_error` and the `telemetry_dev` logger. After an export reaches the ingest, the next failure is a new report. |
170
184
  | `disable_atexit` | `False` | Skip the automatic atexit shutdown. |
171
185
  | `timeout` | `10.0` | OTLP HTTP timeout in seconds. |
172
186
  | `span_exporter`, `log_exporter`, `metric_reader` | `None` | Test seams / offline mode; any of them enables the client without an API key. |
173
187
 
174
188
  `capture_input=False` and `capture_output=False` omit captured content. Provider integrations may
175
189
  still record non-content metadata such as response IDs and models, finish reasons, usage, timing,
176
- and errors. Capture flags do not gate stop sequences, tool descriptions and definitions (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes,
177
- or exception messages and stack traces; redact those separately when needed.
190
+ and error types. Capture flags do not gate stop sequences, tool descriptions and definitions
191
+ (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes;
192
+ redact those separately when needed.
178
193
 
179
- `mask` receives input and output content, system instructions, log messages, and content that
194
+ `mask` receives input and output content, system instructions, log messages, captured exception
195
+ messages and stack traces, and content that
180
196
  integrations pass to `SpanHandle.serialize_content`. `MaskContext.key` names the attribute being
181
- written. It does not receive span or propagated `metadata`, raw `attributes`, log attributes, or
182
- exception messages and stack traces; redact those separately.
197
+ written. It does not receive span or propagated `metadata`, raw `attributes`, or log attributes;
198
+ redact those separately. Log masking is independent of span capture flags.
183
199
 
184
200
  Session roots share trace IDs from the API key and session ID. The hash matches the TypeScript SDK.
185
201
  The SDK uses the configured OTel sampling policy, not the synthetic parent's flags.
@@ -187,6 +203,73 @@ For example, `init(sampler=ParentBased(TraceIdRatioBased(0.1)))` samples session
187
203
  These classes come from `opentelemetry.sdk.trace.sampling`.
188
204
  The built-in Python and TypeScript ratio samplers can select different sessions because the OTel algorithms differ.
189
205
 
206
+ ## Prompts
207
+
208
+ `get_prompt` fetches a prompt that you manage in telemetry.dev. Fetch it by its label, so that you
209
+ deploy a version when you move the label, without a change to the code:
210
+
211
+ ```python
212
+ import telemetry_dev
213
+ from openai import OpenAI
214
+ from telemetry_dev_openai import wrap_openai
215
+
216
+ telemetry_dev.init()
217
+ client = wrap_openai(OpenAI())
218
+
219
+ prompt = telemetry_dev.get_prompt("support/reply", label="production")
220
+ messages = prompt.compile(customer_message=customer_message, tone=tone)
221
+
222
+ with telemetry_dev.with_prompt(prompt):
223
+ response = client.chat.completions.create(
224
+ model=prompt.model["name"] if prompt.model else "gpt-4o", messages=messages
225
+ )
226
+
227
+ # Pin one version, as in a test:
228
+ pinned = telemetry_dev.get_prompt("support/reply", version=13)
229
+ ```
230
+
231
+ - Without `label` and `version`, `get_prompt` fetches the `production` label. A text prompt needs
232
+ `type="text"`; `get_prompt` raises for a prompt of the other type.
233
+ - `compile(variables)` or `compile(**variables)` puts the values in place of `{{name}}`. A missing
234
+ value or `None` becomes an empty text, and a value that is not a string becomes JSON. In a chat
235
+ prompt, a placeholder message becomes the list of messages in its variable. A text prompt
236
+ compiles to a string.
237
+ - The prompt also has `model`, `parameters`, `tools`, and `response_format` of its version. The
238
+ keys inside them are the API's (`maxTokens`, `topP`, ...).
239
+ - `get_prompt` keeps each prompt. When the copy is older than `cache_ttl_seconds` (default 60), a
240
+ call returns it at once and fetches a new copy in a background thread. Only a call with no copy
241
+ waits for a fetch, for 10 seconds at most, also when the network stops. Calls for the same
242
+ prompt share one fetch. `get_prompt_async` runs the same function in a worker thread.
243
+ - If a fetch fails, `get_prompt` returns the last copy that it fetched and reports the error
244
+ through `on_error`. With no copy, it returns the `fallback` (`version` is `None` and
245
+ `is_fallback` is `True`). With no fallback, it raises `PromptError` whose `code` tells why, for
246
+ example `prompt_not_found`, `label_not_found`, `insufficient_scope`, `missing_api_key`, or
247
+ `timeout`. A response with a different status and no API error gives `http_<status>`, for
248
+ example `http_302` for a redirect. `get_prompt` does not send the request to the location of a
249
+ redirect, thus your key does not go to a different host. With no copy, the calls of the next 10
250
+ seconds (or the TTL, if it is shorter) get the same result without a fetch. This is the one SDK
251
+ function that can raise: your app cannot run without its prompt.
252
+ - `get_prompt` uses the API key and base URL of an enabled `init()`, or `TELEMETRY_DEV_API_KEY`
253
+ and `TELEMETRY_DEV_BASE_URL`, or its own `api_key` and `base_url`. It reads the same host that
254
+ telemetry goes to, `https://ingest.telemetry.dev` by default. The key needs the read scope for
255
+ prompts.
256
+ - When a label sends part of the traffic to a second version, each session keeps one version: the
257
+ choice uses the session id of `propagate_attributes` (or of `session_mode="process"`). Without a
258
+ session id, each call chooses at random.
259
+
260
+ `with_prompt(prompt)` records the prompt on each model call that starts inside it:
261
+ `td.prompt.name`, `td.prompt.version`, and `td.prompt.label` (only for a prompt fetched by label).
262
+ When input capture is on, `td.prompt.variables` holds the JSON of the variables of the last
263
+ `compile()` of that prompt object before `with_prompt` starts, or of the last `compile()` inside
264
+ it, after `mask` and truncation. If threads or tasks that run at the same time share one prompt
265
+ object, call `compile()` in `with_prompt`, or call `get_prompt` in each thread or task.
266
+ `get_prompt` returns a new object from the cache. If you do not, a thread can record the variables
267
+ of a different thread. Model calls are the spans of type `generation` (operations `chat`,
268
+ `text_completion`, and `generate_content`), including the spans of the provider integrations:
269
+ OpenAI, Anthropic, Amazon Bedrock, Google GenAI, LiteLLM, and OpenRouter. Spans from other
270
+ OpenTelemetry tracers do not get the attributes. Messages from `compile()` that you pass to a
271
+ model call outside `with_prompt` are not recorded.
272
+
190
273
  ## Auto-metrics
191
274
 
192
275
  Ended spans automatically record histograms (DELTA temporality, exported every 60s):
@@ -253,7 +336,7 @@ from telemetry_dev import TelemetrySpanProcessor
253
336
  | `metrics` | `True` | Auto-record GenAI duration/token histograms for exported spans when an API key is available. |
254
337
  | `service_name` | `OTEL_SERVICE_NAME` or `unknown_service` | `service.name` on the metrics resource. |
255
338
  | `environment` | `TELEMETRY_DEV_ENVIRONMENT` or `production` | `deployment.environment.name` on the metrics resource. |
256
- | `on_error` | `None` | Receives internal processor errors; errors are never raised into your code. |
339
+ | `on_error` | `None` | Receives export errors of the telemetry.dev exporters and internal processor errors; errors are never raised into your code. A series of failed exports is one report. After an export reaches the ingest, the next failure is a new report. |
257
340
  | `span_exporter` | `None` | Advanced/test seam replacing the telemetry.dev OTLP trace exporter. |
258
341
 
259
342
  Without an API key and without `span_exporter`, `TelemetrySpanProcessor()` is an inert no-op:
@@ -56,7 +56,7 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
56
56
  | Variable | Default | Purpose |
57
57
  | --- | --- | --- |
58
58
  | `TELEMETRY_DEV_API_KEY` | — | Ingest key (`td_live_...`). Absent = SDK is a no-op. |
59
- | `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Ingest base URL (trailing slashes stripped). |
59
+ | `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Base URL for telemetry export and `get_prompt` (trailing slashes stripped). |
60
60
  | `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
61
61
  | `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
62
62
  | `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
@@ -83,6 +83,8 @@ later `init()` generates a new ID. It is not persisted across processes.
83
83
  | `propagate_attributes(*, user_id=, session_id=, metadata=)` | Context manager stamping `user.id` / `gen_ai.conversation.id` / `td.metadata.*` on every span and log record started inside (threads/asyncio included via contextvars). |
84
84
  | `log(message, *, level="info", event_name=None, attributes=None)` | Emit an OTLP log record to `/v1/logs`, correlated with the current trace. Levels: `debug`/`info`/`warn`/`error` (`"warning"` is accepted as an alias of `warn`). |
85
85
  | `get_traceparent() -> str \| None` | W3C traceparent of the current context. |
86
+ | `get_prompt(name, *, label=, version=, type="chat", cache_ttl_seconds=60, fallback=, api_key=, base_url=)` / `get_prompt_async(...)` | Fetch a prompt by label or version; see [Prompts](#prompts). |
87
+ | `with_prompt(prompt)` | Context manager recording the prompt on each model call started inside. |
86
88
  | `flush(timeout_s=10.0)` / `shutdown(timeout_s=10.0)` | Force-flush / tear down traces + logs + metrics. Shutdown also runs atexit unless `disable_atexit=True`. |
87
89
  | `TelemetrySpanProcessor` / `telemetry_dev.otel.create_telemetry_span_exporter` | Bring-your-own-OTel helpers (below). |
88
90
  | `MaskContext`, `Usage`, `SpanHandle`, `Client`, `NOT_GIVEN` | Supporting types. |
@@ -127,6 +129,17 @@ the core SDK with provider integrations to enable the new metric.
127
129
  `SpanContext`), `start_time`, and per-call `capture_input` / `capture_output` overrides;
128
130
  `end()` additionally accepts `end_time`.
129
131
 
132
+ Core SDK error recording keeps failure status, error type, and an exception event. It includes
133
+ `exception.message` and `exception.stacktrace` only when both effective `capture_input` and
134
+ `capture_output` are true, including per-span overrides: exceptions can echo either inputs or
135
+ outputs. The policy is per span: when an error propagates to an enclosing span whose capture is on,
136
+ that span records its message and stack. Included fields pass through `mask` with their exception
137
+ attribute keys, then serialization and truncation. A truncated `exception.stacktrace` keeps its
138
+ end, where Python normally puts the innermost frame and the exception line. Mask and serialization failures drop the affected field and report generic errors
139
+ to `on_error` and SDK diagnostics without the original message, traceback, or exception chain.
140
+ The original application error is raised unchanged. These controls do not scrub error type names,
141
+ metadata, identifiers, resource attributes, or events written directly through raw OpenTelemetry APIs.
142
+
130
143
  ### init() options
131
144
 
132
145
  | Option | Default | Purpose |
@@ -142,20 +155,22 @@ the core SDK with provider integrations to enable the new metric.
142
155
  | `max_attribute_length` | `65536` | Non-negative integer per-content-attribute cap. Invalid values call `on_error`, do not raise into application code, and install a disabled client; truncated values use the ASCII `...[truncated]` marker, or its prefix when the cap is shorter than the marker. The cap also applies, with the marker, to string metadata and log attributes; any other string attribute is cut at the cap without a marker, except that a cap of `0` leaves those at 65536. |
143
156
  | `span_filter` | `None` | Export predicate `Callable[[ReadableSpan], bool]`. |
144
157
  | `sampler` | OTel environment configuration | OTel `Sampler` that overrides environment settings. Session roots use its root policy. Real parents keep their sampling decisions. |
145
- | `on_error` | `None` | Receives internal SDK errors; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never raises them into application code. |
158
+ | `on_error` | `None` | Receives internal SDK errors; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never raises them into application code. A series of failed exports, for example during an ingest outage, is one report to `on_error` and the `telemetry_dev` logger. After an export reaches the ingest, the next failure is a new report. |
146
159
  | `disable_atexit` | `False` | Skip the automatic atexit shutdown. |
147
160
  | `timeout` | `10.0` | OTLP HTTP timeout in seconds. |
148
161
  | `span_exporter`, `log_exporter`, `metric_reader` | `None` | Test seams / offline mode; any of them enables the client without an API key. |
149
162
 
150
163
  `capture_input=False` and `capture_output=False` omit captured content. Provider integrations may
151
164
  still record non-content metadata such as response IDs and models, finish reasons, usage, timing,
152
- and errors. Capture flags do not gate stop sequences, tool descriptions and definitions (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes,
153
- or exception messages and stack traces; redact those separately when needed.
165
+ and error types. Capture flags do not gate stop sequences, tool descriptions and definitions
166
+ (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes;
167
+ redact those separately when needed.
154
168
 
155
- `mask` receives input and output content, system instructions, log messages, and content that
169
+ `mask` receives input and output content, system instructions, log messages, captured exception
170
+ messages and stack traces, and content that
156
171
  integrations pass to `SpanHandle.serialize_content`. `MaskContext.key` names the attribute being
157
- written. It does not receive span or propagated `metadata`, raw `attributes`, log attributes, or
158
- exception messages and stack traces; redact those separately.
172
+ written. It does not receive span or propagated `metadata`, raw `attributes`, or log attributes;
173
+ redact those separately. Log masking is independent of span capture flags.
159
174
 
160
175
  Session roots share trace IDs from the API key and session ID. The hash matches the TypeScript SDK.
161
176
  The SDK uses the configured OTel sampling policy, not the synthetic parent's flags.
@@ -163,6 +178,73 @@ For example, `init(sampler=ParentBased(TraceIdRatioBased(0.1)))` samples session
163
178
  These classes come from `opentelemetry.sdk.trace.sampling`.
164
179
  The built-in Python and TypeScript ratio samplers can select different sessions because the OTel algorithms differ.
165
180
 
181
+ ## Prompts
182
+
183
+ `get_prompt` fetches a prompt that you manage in telemetry.dev. Fetch it by its label, so that you
184
+ deploy a version when you move the label, without a change to the code:
185
+
186
+ ```python
187
+ import telemetry_dev
188
+ from openai import OpenAI
189
+ from telemetry_dev_openai import wrap_openai
190
+
191
+ telemetry_dev.init()
192
+ client = wrap_openai(OpenAI())
193
+
194
+ prompt = telemetry_dev.get_prompt("support/reply", label="production")
195
+ messages = prompt.compile(customer_message=customer_message, tone=tone)
196
+
197
+ with telemetry_dev.with_prompt(prompt):
198
+ response = client.chat.completions.create(
199
+ model=prompt.model["name"] if prompt.model else "gpt-4o", messages=messages
200
+ )
201
+
202
+ # Pin one version, as in a test:
203
+ pinned = telemetry_dev.get_prompt("support/reply", version=13)
204
+ ```
205
+
206
+ - Without `label` and `version`, `get_prompt` fetches the `production` label. A text prompt needs
207
+ `type="text"`; `get_prompt` raises for a prompt of the other type.
208
+ - `compile(variables)` or `compile(**variables)` puts the values in place of `{{name}}`. A missing
209
+ value or `None` becomes an empty text, and a value that is not a string becomes JSON. In a chat
210
+ prompt, a placeholder message becomes the list of messages in its variable. A text prompt
211
+ compiles to a string.
212
+ - The prompt also has `model`, `parameters`, `tools`, and `response_format` of its version. The
213
+ keys inside them are the API's (`maxTokens`, `topP`, ...).
214
+ - `get_prompt` keeps each prompt. When the copy is older than `cache_ttl_seconds` (default 60), a
215
+ call returns it at once and fetches a new copy in a background thread. Only a call with no copy
216
+ waits for a fetch, for 10 seconds at most, also when the network stops. Calls for the same
217
+ prompt share one fetch. `get_prompt_async` runs the same function in a worker thread.
218
+ - If a fetch fails, `get_prompt` returns the last copy that it fetched and reports the error
219
+ through `on_error`. With no copy, it returns the `fallback` (`version` is `None` and
220
+ `is_fallback` is `True`). With no fallback, it raises `PromptError` whose `code` tells why, for
221
+ example `prompt_not_found`, `label_not_found`, `insufficient_scope`, `missing_api_key`, or
222
+ `timeout`. A response with a different status and no API error gives `http_<status>`, for
223
+ example `http_302` for a redirect. `get_prompt` does not send the request to the location of a
224
+ redirect, thus your key does not go to a different host. With no copy, the calls of the next 10
225
+ seconds (or the TTL, if it is shorter) get the same result without a fetch. This is the one SDK
226
+ function that can raise: your app cannot run without its prompt.
227
+ - `get_prompt` uses the API key and base URL of an enabled `init()`, or `TELEMETRY_DEV_API_KEY`
228
+ and `TELEMETRY_DEV_BASE_URL`, or its own `api_key` and `base_url`. It reads the same host that
229
+ telemetry goes to, `https://ingest.telemetry.dev` by default. The key needs the read scope for
230
+ prompts.
231
+ - When a label sends part of the traffic to a second version, each session keeps one version: the
232
+ choice uses the session id of `propagate_attributes` (or of `session_mode="process"`). Without a
233
+ session id, each call chooses at random.
234
+
235
+ `with_prompt(prompt)` records the prompt on each model call that starts inside it:
236
+ `td.prompt.name`, `td.prompt.version`, and `td.prompt.label` (only for a prompt fetched by label).
237
+ When input capture is on, `td.prompt.variables` holds the JSON of the variables of the last
238
+ `compile()` of that prompt object before `with_prompt` starts, or of the last `compile()` inside
239
+ it, after `mask` and truncation. If threads or tasks that run at the same time share one prompt
240
+ object, call `compile()` in `with_prompt`, or call `get_prompt` in each thread or task.
241
+ `get_prompt` returns a new object from the cache. If you do not, a thread can record the variables
242
+ of a different thread. Model calls are the spans of type `generation` (operations `chat`,
243
+ `text_completion`, and `generate_content`), including the spans of the provider integrations:
244
+ OpenAI, Anthropic, Amazon Bedrock, Google GenAI, LiteLLM, and OpenRouter. Spans from other
245
+ OpenTelemetry tracers do not get the attributes. Messages from `compile()` that you pass to a
246
+ model call outside `with_prompt` are not recorded.
247
+
166
248
  ## Auto-metrics
167
249
 
168
250
  Ended spans automatically record histograms (DELTA temporality, exported every 60s):
@@ -229,7 +311,7 @@ from telemetry_dev import TelemetrySpanProcessor
229
311
  | `metrics` | `True` | Auto-record GenAI duration/token histograms for exported spans when an API key is available. |
230
312
  | `service_name` | `OTEL_SERVICE_NAME` or `unknown_service` | `service.name` on the metrics resource. |
231
313
  | `environment` | `TELEMETRY_DEV_ENVIRONMENT` or `production` | `deployment.environment.name` on the metrics resource. |
232
- | `on_error` | `None` | Receives internal processor errors; errors are never raised into your code. |
314
+ | `on_error` | `None` | Receives export errors of the telemetry.dev exporters and internal processor errors; errors are never raised into your code. A series of failed exports is one report. After an export reaches the ingest, the next failure is a new report. |
233
315
  | `span_exporter` | `None` | Advanced/test seam replacing the telemetry.dev OTLP trace exporter. |
234
316
 
235
317
  Without an API key and without `span_exporter`, `TelemetrySpanProcessor()` is an inert no-op:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "telemetry-dev"
3
- version = "0.2.10"
3
+ version = "0.2.12"
4
4
  description = "telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "opentelemetry-api>=1.39.0,<2",
29
29
  "opentelemetry-sdk>=1.39.0,<2",
30
30
  "opentelemetry-exporter-otlp-proto-http>=1.39.0,<2",
31
+ "rfc8785>=0.1.4",
31
32
  ]
32
33
 
33
34
  [[project.authors]]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "telemetry-dev"
3
- version = "0.2.10"
3
+ version = "0.2.12"
4
4
  description = "telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "opentelemetry-api>=1.39.0,<2",
23
23
  "opentelemetry-sdk>=1.39.0,<2",
24
24
  "opentelemetry-exporter-otlp-proto-http>=1.39.0,<2",
25
+ "rfc8785>=0.1.4",
25
26
  ]
26
27
 
27
28
  [project.urls]
@@ -19,6 +19,16 @@ from ._config import LogLevelOption
19
19
  from ._context import get_traceparent, propagate_attributes
20
20
  from ._logs import log
21
21
  from ._observe import observe
22
+ from ._prompts import (
23
+ ChatPrompt,
24
+ Prompt,
25
+ PromptError,
26
+ PromptModel,
27
+ TextPrompt,
28
+ get_prompt,
29
+ get_prompt_async,
30
+ with_prompt,
31
+ )
22
32
  from ._semconv import LogLevel, SpanType
23
33
  from ._serialize import AttributeValue, Mask, MaskContext
24
34
  from ._spans import (
@@ -34,18 +44,25 @@ __all__ = [
34
44
  "NOT_GIVEN",
35
45
  "AttributeValue",
36
46
  "CaptureBudget",
47
+ "ChatPrompt",
37
48
  "Client",
38
49
  "LogLevel",
39
50
  "LogLevelOption",
40
51
  "Mask",
41
52
  "MaskContext",
53
+ "Prompt",
54
+ "PromptError",
55
+ "PromptModel",
42
56
  "SpanHandle",
43
57
  "SpanType",
44
58
  "TelemetrySpanProcessor",
59
+ "TextPrompt",
45
60
  "Usage",
46
61
  "__version__",
47
62
  "flush",
48
63
  "get_client",
64
+ "get_prompt",
65
+ "get_prompt_async",
49
66
  "get_traceparent",
50
67
  "init",
51
68
  "log",
@@ -54,4 +71,5 @@ __all__ = [
54
71
  "shutdown",
55
72
  "start_span",
56
73
  "update_current_span",
74
+ "with_prompt",
57
75
  ]
@@ -15,7 +15,6 @@ from opentelemetry.sdk._logs import LoggerProvider
15
15
  from opentelemetry.sdk._logs.export import (
16
16
  BatchLogRecordProcessor,
17
17
  LogRecordExporter,
18
- LogRecordExportResult,
19
18
  SimpleLogRecordProcessor,
20
19
  )
21
20
  from opentelemetry.sdk.metrics import Histogram, MeterProvider
@@ -26,7 +25,7 @@ from opentelemetry.sdk.metrics.export import (
26
25
  )
27
26
  from opentelemetry.sdk.resources import Resource
28
27
  from opentelemetry.sdk.trace import ReadableSpan, SpanLimits, TracerProvider
29
- from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
28
+ from opentelemetry.sdk.trace.export import SpanExporter
30
29
  from opentelemetry.sdk.trace.sampling import Sampler
31
30
  from opentelemetry.trace import Tracer
32
31
 
@@ -41,6 +40,7 @@ from ._config import (
41
40
  resolve_config,
42
41
  )
43
42
  from ._context import SessionSampler
43
+ from ._export import ExportFailures, ReportingLogExporter, ReportingSpanExporter
44
44
  from ._metrics import (
45
45
  GuardedOTLPMetricExporter,
46
46
  MetricsRecorder,
@@ -60,68 +60,6 @@ def _default_scope_filter(span: ReadableSpan) -> bool:
60
60
  return scope is not None and scope.name == SCOPE_NAME
61
61
 
62
62
 
63
- class _ReportingSpanExporter(SpanExporter):
64
- """Surfaces transport failures to on_error (TS parity: exporters route errors to onError,
65
- the stock OTLP exporter only logs them)."""
66
-
67
- def __init__(
68
- self, inner: SpanExporter, on_error: Callable[[BaseException], None] | None
69
- ) -> None:
70
- self._inner = inner
71
- self._on_error = on_error
72
-
73
- def export(self, spans: Any) -> SpanExportResult:
74
- try:
75
- result = self._inner.export(spans)
76
- except BaseException as exc:
77
- report_error(self._on_error, "span export failed", exc)
78
- return SpanExportResult.FAILURE
79
- if result is SpanExportResult.FAILURE:
80
- report_error(
81
- self._on_error,
82
- "span export failed",
83
- RuntimeError("OTLP span export failed (check the API key and ingest URL)"),
84
- )
85
- return result
86
-
87
- def force_flush(self, timeout_millis: int = 30000) -> bool:
88
- return self._inner.force_flush(timeout_millis)
89
-
90
- def shutdown(self) -> None:
91
- self._inner.shutdown()
92
-
93
-
94
- class _ReportingLogExporter(LogRecordExporter):
95
- """Log-exporter twin of _ReportingSpanExporter."""
96
-
97
- def __init__(
98
- self, inner: LogRecordExporter, on_error: Callable[[BaseException], None] | None
99
- ) -> None:
100
- self._inner = inner
101
- self._on_error = on_error
102
-
103
- def export(self, batch: Any) -> LogRecordExportResult:
104
- try:
105
- result = self._inner.export(batch)
106
- except BaseException as exc:
107
- report_error(self._on_error, "log export failed", exc)
108
- return LogRecordExportResult.FAILURE
109
- if result is LogRecordExportResult.FAILURE:
110
- report_error(
111
- self._on_error,
112
- "log export failed",
113
- RuntimeError("OTLP log export failed (check the API key and ingest URL)"),
114
- )
115
- return result
116
-
117
- def force_flush(self, timeout_millis: int = 30000) -> bool:
118
- inner_flush = getattr(self._inner, "force_flush", None)
119
- return inner_flush(timeout_millis) if inner_flush is not None else True
120
-
121
- def shutdown(self) -> None:
122
- self._inner.shutdown()
123
-
124
-
125
63
  class Client:
126
64
  """Holds the telemetry pipelines for one init() call. Obtain via telemetry_dev.init()."""
127
65
 
@@ -195,6 +133,7 @@ class Client:
195
133
  ),
196
134
  )
197
135
  self._tracer_provider.sampler = SessionSampler(self._tracer_provider.sampler)
136
+ failures = ExportFailures(on_error)
198
137
 
199
138
  # Without an api key (enabled via a test seam), never construct real network
200
139
  # exporters — they would POST to the ingest with a bogus Authorization header.
@@ -206,7 +145,7 @@ class Client:
206
145
  compression=Compression.Gzip,
207
146
  timeout=timeout,
208
147
  preferred_temporality={Histogram: AggregationTemporality.DELTA},
209
- on_error=on_error,
148
+ failures=failures,
210
149
  )
211
150
  metric_exporter.output_chunks = self._output_chunks
212
151
  metric_reader = PeriodicExportingMetricReader(
@@ -221,14 +160,14 @@ class Client:
221
160
  )
222
161
 
223
162
  if span_exporter is None:
224
- span_exporter = _ReportingSpanExporter(
163
+ span_exporter = ReportingSpanExporter(
225
164
  OTLPSpanExporter(
226
165
  endpoint=f"{config.base_url}/v1/traces",
227
166
  headers=headers,
228
167
  compression=Compression.Gzip,
229
168
  timeout=timeout,
230
169
  ),
231
- on_error,
170
+ failures,
232
171
  )
233
172
  if span_filter is None and register_global:
234
173
  span_filter = _default_scope_filter
@@ -243,14 +182,14 @@ class Client:
243
182
  self.tracer = self._tracer_provider.get_tracer(SCOPE_NAME, SDK_VERSION)
244
183
 
245
184
  if log_exporter is None and config.api_key is not None:
246
- log_exporter = _ReportingLogExporter(
185
+ log_exporter = ReportingLogExporter(
247
186
  OTLPLogExporter(
248
187
  endpoint=f"{config.base_url}/v1/logs",
249
188
  headers=headers,
250
189
  compression=Compression.Gzip,
251
190
  timeout=timeout,
252
191
  ),
253
- on_error,
192
+ failures,
254
193
  )
255
194
  if log_exporter is not None:
256
195
  self._logger_provider = LoggerProvider(resource=resource, shutdown_on_exit=False)
@@ -277,13 +216,14 @@ class Client:
277
216
  def report(self, message: str, exc: BaseException) -> None:
278
217
  report_error(self.on_error, message, exc)
279
218
 
280
- def serialize(self, value: Any, key: str) -> str | None:
219
+ def serialize(self, value: Any, key: str, *, keep_end: bool = False) -> str | None:
281
220
  return serialize_content(
282
221
  value,
283
222
  key=key,
284
223
  mask=self.mask,
285
224
  max_len=self.max_attribute_length,
286
225
  on_error=self.on_error,
226
+ keep_end=keep_end,
287
227
  )
288
228
 
289
229
  def flush(self, timeout_s: float = 10.0) -> None:
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  import os
5
5
  import re
6
+ import traceback
6
7
  from collections.abc import Callable
7
8
  from dataclasses import dataclass
8
9
  from typing import Literal
@@ -74,9 +75,12 @@ def report_error(
74
75
  exc: BaseException,
75
76
  ) -> None:
76
77
  """Route an internal SDK error to the on_error hook + leveled logger; never raises."""
78
+ # Snapshot before callbacks can re-raise exc and attach application exception chains.
79
+ if logger.isEnabledFor(logging.ERROR):
80
+ trace = "".join(traceback.format_exception(exc)).rstrip()
81
+ logger.error("telemetry-dev: %s\n%s", message, trace)
77
82
  if on_error is not None:
78
83
  try:
79
84
  on_error(exc)
80
85
  except BaseException:
81
- logger.debug("telemetry-dev on_error hook raised", exc_info=True)
82
- logger.error("telemetry-dev: %s", message, exc_info=exc)
86
+ logger.debug("telemetry-dev on_error hook raised")