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.
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/PKG-INFO +92 -9
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/README.md +90 -8
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/pyproject.toml +2 -1
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/pyproject.toml.orig +2 -1
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/__init__.py +18 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_client.py +10 -70
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_config.py +6 -2
- telemetry_dev-0.2.12/src/telemetry_dev/_export.py +100 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_metrics.py +12 -9
- telemetry_dev-0.2.12/src/telemetry_dev/_prompts.py +679 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_semconv.py +5 -0
- telemetry_dev-0.2.12/src/telemetry_dev/_serialize.py +220 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_spans.py +30 -12
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/otel.py +6 -0
- telemetry_dev-0.2.10/src/telemetry_dev/_serialize.py +0 -102
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/LICENSE +0 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_capture.py +0 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_context.py +0 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_logs.py +0 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_observe.py +0 -0
- {telemetry_dev-0.2.10 → telemetry_dev-0.2.12}/src/telemetry_dev/_processor.py +0 -0
- {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.
|
|
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` |
|
|
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
|
|
177
|
-
|
|
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,
|
|
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
|
|
182
|
-
|
|
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` |
|
|
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
|
|
153
|
-
|
|
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,
|
|
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
|
|
158
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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"
|
|
82
|
-
logger.error("telemetry-dev: %s", message, exc_info=exc)
|
|
86
|
+
logger.debug("telemetry-dev on_error hook raised")
|