telemetry-dev 0.1.0__py3-none-any.whl
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/__init__.py +55 -0
- telemetry_dev/_client.py +390 -0
- telemetry_dev/_config.py +81 -0
- telemetry_dev/_context.py +101 -0
- telemetry_dev/_logs.py +65 -0
- telemetry_dev/_metrics.py +120 -0
- telemetry_dev/_observe.py +142 -0
- telemetry_dev/_processor.py +77 -0
- telemetry_dev/_semconv.py +115 -0
- telemetry_dev/_serialize.py +103 -0
- telemetry_dev/_spans.py +676 -0
- telemetry_dev/otel.py +186 -0
- telemetry_dev/py.typed +0 -0
- telemetry_dev-0.1.0.dist-info/METADATA +235 -0
- telemetry_dev-0.1.0.dist-info/RECORD +16 -0
- telemetry_dev-0.1.0.dist-info/WHEEL +4 -0
telemetry_dev/_spans.py
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
5
|
+
from contextvars import Token
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Literal, TypedDict
|
|
9
|
+
from weakref import WeakKeyDictionary
|
|
10
|
+
|
|
11
|
+
from opentelemetry import context as otel_context
|
|
12
|
+
from opentelemetry import trace
|
|
13
|
+
from opentelemetry.context import Context
|
|
14
|
+
from opentelemetry.sdk.trace import Span as SdkSpan
|
|
15
|
+
from opentelemetry.trace import Span, SpanContext, Status, StatusCode
|
|
16
|
+
from opentelemetry.util import types as otel_types
|
|
17
|
+
|
|
18
|
+
from ._client import Client, get_client
|
|
19
|
+
from ._config import logger
|
|
20
|
+
from ._context import context_from_parent, traceparent_of
|
|
21
|
+
from ._semconv import (
|
|
22
|
+
ATTR_AGENT_ID,
|
|
23
|
+
ATTR_AGENT_NAME,
|
|
24
|
+
ATTR_COST,
|
|
25
|
+
ATTR_ERROR_TYPE,
|
|
26
|
+
ATTR_FINISH_REASONS,
|
|
27
|
+
ATTR_OPERATION,
|
|
28
|
+
ATTR_OUTPUT_TYPE,
|
|
29
|
+
ATTR_PROVIDER,
|
|
30
|
+
ATTR_REQUEST_MODEL,
|
|
31
|
+
ATTR_RESPONSE_ID,
|
|
32
|
+
ATTR_RESPONSE_MODEL,
|
|
33
|
+
ATTR_SYSTEM_INSTRUCTIONS,
|
|
34
|
+
ATTR_TIME_TO_FIRST_CHUNK,
|
|
35
|
+
ATTR_TOOL_CALL_ID,
|
|
36
|
+
ATTR_TOOL_DESCRIPTION,
|
|
37
|
+
ATTR_TOOL_NAME,
|
|
38
|
+
METADATA_PREFIX,
|
|
39
|
+
RESERVED_METADATA_KEYS,
|
|
40
|
+
SAMPLING_ATTRS,
|
|
41
|
+
SEVERITY,
|
|
42
|
+
SPAN_TYPE_TO_OPERATION,
|
|
43
|
+
USAGE_ATTRS,
|
|
44
|
+
SpanType,
|
|
45
|
+
input_key,
|
|
46
|
+
output_key,
|
|
47
|
+
)
|
|
48
|
+
from ._serialize import AttributeValue, coerce_attr_value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _NotGiven:
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return "NOT_GIVEN"
|
|
54
|
+
|
|
55
|
+
def __bool__(self) -> bool:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
NOT_GIVEN: Any = _NotGiven()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Usage(TypedDict, total=False):
|
|
63
|
+
"""Token usage — exactly these six fields are emitted; unknown keys are dropped."""
|
|
64
|
+
|
|
65
|
+
input_tokens: int
|
|
66
|
+
output_tokens: int
|
|
67
|
+
total_tokens: int
|
|
68
|
+
cache_read_input_tokens: int
|
|
69
|
+
cache_creation_input_tokens: int
|
|
70
|
+
reasoning_output_tokens: int
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
ParentType = str | Context | SpanContext | None
|
|
74
|
+
TimeInput = datetime | int | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class _SpanState:
|
|
79
|
+
operation: str
|
|
80
|
+
capture_input: bool
|
|
81
|
+
capture_output: bool
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_SPAN_STATES: WeakKeyDictionary[Span, _SpanState] = WeakKeyDictionary()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _to_ns(value: TimeInput) -> int | None:
|
|
88
|
+
if value is None:
|
|
89
|
+
return None
|
|
90
|
+
if isinstance(value, datetime):
|
|
91
|
+
return int(value.timestamp() * 1_000_000_000)
|
|
92
|
+
return int(value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _record_error_on_span(span: Span, error: BaseException) -> None:
|
|
96
|
+
error_type = type(error).__name__
|
|
97
|
+
stacktrace = "".join(traceback.format_exception(type(error), error, error.__traceback__))
|
|
98
|
+
span.set_attribute(ATTR_ERROR_TYPE, error_type)
|
|
99
|
+
span.add_event(
|
|
100
|
+
"exception",
|
|
101
|
+
{
|
|
102
|
+
"exception.type": error_type,
|
|
103
|
+
"exception.message": str(error),
|
|
104
|
+
"exception.stacktrace": stacktrace,
|
|
105
|
+
"log.severity_number": SEVERITY["error"],
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _metadata_attrs(
|
|
112
|
+
metadata: Mapping[str, Any],
|
|
113
|
+
max_len: int,
|
|
114
|
+
on_error: Callable[[BaseException], None] | None,
|
|
115
|
+
) -> dict[str, AttributeValue]:
|
|
116
|
+
attrs: dict[str, AttributeValue] = {}
|
|
117
|
+
for key, value in metadata.items():
|
|
118
|
+
if key in RESERVED_METADATA_KEYS:
|
|
119
|
+
logger.debug(
|
|
120
|
+
"telemetry-dev: dropping reserved metadata key %r "
|
|
121
|
+
"(use propagate_attributes user_id/session_id instead)",
|
|
122
|
+
key,
|
|
123
|
+
)
|
|
124
|
+
continue
|
|
125
|
+
attr = coerce_attr_value(
|
|
126
|
+
value,
|
|
127
|
+
max_len=max_len,
|
|
128
|
+
key=f"{METADATA_PREFIX}{key}",
|
|
129
|
+
on_error=on_error,
|
|
130
|
+
)
|
|
131
|
+
if attr is not None:
|
|
132
|
+
attrs[f"{METADATA_PREFIX}{key}"] = attr
|
|
133
|
+
return attrs
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _usage_attrs(usage: Mapping[str, Any]) -> dict[str, AttributeValue]:
|
|
137
|
+
attrs: dict[str, AttributeValue] = {}
|
|
138
|
+
for key, value in usage.items():
|
|
139
|
+
attr = USAGE_ATTRS.get(key)
|
|
140
|
+
if attr is None:
|
|
141
|
+
logger.debug("telemetry-dev: dropping unknown usage key %r", key)
|
|
142
|
+
continue
|
|
143
|
+
if isinstance(value, int | float) and not isinstance(value, bool):
|
|
144
|
+
attrs[attr] = value
|
|
145
|
+
return attrs
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _apply_fields(client: Client, span: Span, state: _SpanState, fields: dict[str, Any]) -> None:
|
|
149
|
+
"""Map the shared snake_case field set onto gen_ai.* attributes; raw `attributes` merge last."""
|
|
150
|
+
attrs: dict[str, otel_types.AttributeValue] = {}
|
|
151
|
+
|
|
152
|
+
name = fields.get("name")
|
|
153
|
+
if name is not None:
|
|
154
|
+
span.update_name(name)
|
|
155
|
+
|
|
156
|
+
for field, attr in (
|
|
157
|
+
("model", ATTR_REQUEST_MODEL),
|
|
158
|
+
("provider", ATTR_PROVIDER),
|
|
159
|
+
("response_model", ATTR_RESPONSE_MODEL),
|
|
160
|
+
("response_id", ATTR_RESPONSE_ID),
|
|
161
|
+
("output_type", ATTR_OUTPUT_TYPE),
|
|
162
|
+
("tool_name", ATTR_TOOL_NAME),
|
|
163
|
+
("tool_call_id", ATTR_TOOL_CALL_ID),
|
|
164
|
+
("tool_description", ATTR_TOOL_DESCRIPTION),
|
|
165
|
+
("agent_name", ATTR_AGENT_NAME),
|
|
166
|
+
("agent_id", ATTR_AGENT_ID),
|
|
167
|
+
):
|
|
168
|
+
value = fields.get(field)
|
|
169
|
+
if value is not None:
|
|
170
|
+
attrs[attr] = value
|
|
171
|
+
|
|
172
|
+
finish_reason = fields.get("finish_reason")
|
|
173
|
+
if finish_reason:
|
|
174
|
+
attrs[ATTR_FINISH_REASONS] = [finish_reason]
|
|
175
|
+
|
|
176
|
+
usage = fields.get("usage")
|
|
177
|
+
if usage is not None:
|
|
178
|
+
attrs.update(_usage_attrs(usage))
|
|
179
|
+
|
|
180
|
+
cost_usd = fields.get("cost_usd")
|
|
181
|
+
if cost_usd is not None:
|
|
182
|
+
attrs[ATTR_COST] = float(cost_usd)
|
|
183
|
+
|
|
184
|
+
for field, attr in SAMPLING_ATTRS.items():
|
|
185
|
+
value = fields.get(field)
|
|
186
|
+
if value is not None:
|
|
187
|
+
attrs[attr] = list(value) if field == "stop_sequences" else value
|
|
188
|
+
|
|
189
|
+
time_to_first_chunk_ms = fields.get("time_to_first_chunk_ms")
|
|
190
|
+
if time_to_first_chunk_ms is not None:
|
|
191
|
+
attrs[ATTR_TIME_TO_FIRST_CHUNK] = time_to_first_chunk_ms / 1000
|
|
192
|
+
|
|
193
|
+
metadata = fields.get("metadata")
|
|
194
|
+
if metadata is not None:
|
|
195
|
+
attrs.update(_metadata_attrs(metadata, client.max_attribute_length, client.on_error))
|
|
196
|
+
|
|
197
|
+
if "input" in fields and state.capture_input:
|
|
198
|
+
key = input_key(state.operation)
|
|
199
|
+
serialized = client.serialize(fields["input"], key)
|
|
200
|
+
if serialized is not None:
|
|
201
|
+
attrs[key] = serialized
|
|
202
|
+
system_instructions = fields.get("system_instructions")
|
|
203
|
+
if system_instructions is not None and state.capture_input:
|
|
204
|
+
serialized = client.serialize(system_instructions, ATTR_SYSTEM_INSTRUCTIONS)
|
|
205
|
+
if serialized is not None:
|
|
206
|
+
attrs[ATTR_SYSTEM_INSTRUCTIONS] = serialized
|
|
207
|
+
if "output" in fields and state.capture_output:
|
|
208
|
+
key = output_key(state.operation)
|
|
209
|
+
serialized = client.serialize(fields["output"], key)
|
|
210
|
+
if serialized is not None:
|
|
211
|
+
attrs[key] = serialized
|
|
212
|
+
|
|
213
|
+
raw = fields.get("attributes")
|
|
214
|
+
if raw is not None:
|
|
215
|
+
for key, value in raw.items():
|
|
216
|
+
attr = coerce_attr_value(
|
|
217
|
+
value,
|
|
218
|
+
max_len=client.max_attribute_length,
|
|
219
|
+
key=key,
|
|
220
|
+
on_error=client.on_error,
|
|
221
|
+
)
|
|
222
|
+
if attr is not None:
|
|
223
|
+
attrs[key] = attr
|
|
224
|
+
|
|
225
|
+
if attrs:
|
|
226
|
+
span.set_attributes(attrs)
|
|
227
|
+
|
|
228
|
+
error = fields.get("error")
|
|
229
|
+
if error is not None:
|
|
230
|
+
_record_error_on_span(span, error)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _collect_fields(
|
|
234
|
+
*,
|
|
235
|
+
input: Any = NOT_GIVEN,
|
|
236
|
+
output: Any = NOT_GIVEN,
|
|
237
|
+
name: str | None = None,
|
|
238
|
+
model: str | None = None,
|
|
239
|
+
provider: str | None = None,
|
|
240
|
+
system_instructions: Any = None,
|
|
241
|
+
response_model: str | None = None,
|
|
242
|
+
response_id: str | None = None,
|
|
243
|
+
output_type: str | None = None,
|
|
244
|
+
finish_reason: str | None = None,
|
|
245
|
+
usage: Usage | Mapping[str, int] | None = None,
|
|
246
|
+
cost_usd: float | None = None,
|
|
247
|
+
temperature: float | None = None,
|
|
248
|
+
top_p: float | None = None,
|
|
249
|
+
top_k: float | None = None,
|
|
250
|
+
max_tokens: int | None = None,
|
|
251
|
+
stop_sequences: Sequence[str] | None = None,
|
|
252
|
+
seed: int | None = None,
|
|
253
|
+
frequency_penalty: float | None = None,
|
|
254
|
+
presence_penalty: float | None = None,
|
|
255
|
+
time_to_first_chunk_ms: float | None = None,
|
|
256
|
+
tool_name: str | None = None,
|
|
257
|
+
tool_call_id: str | None = None,
|
|
258
|
+
tool_description: str | None = None,
|
|
259
|
+
agent_name: str | None = None,
|
|
260
|
+
agent_id: str | None = None,
|
|
261
|
+
metadata: Mapping[str, Any] | None = None,
|
|
262
|
+
attributes: Mapping[str, AttributeValue] | None = None,
|
|
263
|
+
error: BaseException | None = None,
|
|
264
|
+
) -> dict[str, Any]:
|
|
265
|
+
fields: dict[str, Any] = {}
|
|
266
|
+
if input is not NOT_GIVEN:
|
|
267
|
+
fields["input"] = input
|
|
268
|
+
if output is not NOT_GIVEN:
|
|
269
|
+
fields["output"] = output
|
|
270
|
+
for key, value in (
|
|
271
|
+
("name", name),
|
|
272
|
+
("model", model),
|
|
273
|
+
("provider", provider),
|
|
274
|
+
("system_instructions", system_instructions),
|
|
275
|
+
("response_model", response_model),
|
|
276
|
+
("response_id", response_id),
|
|
277
|
+
("output_type", output_type),
|
|
278
|
+
("finish_reason", finish_reason),
|
|
279
|
+
("usage", usage),
|
|
280
|
+
("cost_usd", cost_usd),
|
|
281
|
+
("temperature", temperature),
|
|
282
|
+
("top_p", top_p),
|
|
283
|
+
("top_k", top_k),
|
|
284
|
+
("max_tokens", max_tokens),
|
|
285
|
+
("stop_sequences", stop_sequences),
|
|
286
|
+
("seed", seed),
|
|
287
|
+
("frequency_penalty", frequency_penalty),
|
|
288
|
+
("presence_penalty", presence_penalty),
|
|
289
|
+
("time_to_first_chunk_ms", time_to_first_chunk_ms),
|
|
290
|
+
("tool_name", tool_name),
|
|
291
|
+
("tool_call_id", tool_call_id),
|
|
292
|
+
("tool_description", tool_description),
|
|
293
|
+
("agent_name", agent_name),
|
|
294
|
+
("agent_id", agent_id),
|
|
295
|
+
("metadata", metadata),
|
|
296
|
+
("attributes", attributes),
|
|
297
|
+
("error", error),
|
|
298
|
+
):
|
|
299
|
+
if value is not None:
|
|
300
|
+
fields[key] = value
|
|
301
|
+
return fields
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class SpanHandle:
|
|
305
|
+
"""Handle around an OTel span. Use as a context manager to activate the span in the
|
|
306
|
+
current context, or keep it detached and call .end() manually."""
|
|
307
|
+
|
|
308
|
+
def __init__(self, span: Span, client: Client | None, state: _SpanState | None) -> None:
|
|
309
|
+
self.span = span
|
|
310
|
+
self._client = client
|
|
311
|
+
self._state = state
|
|
312
|
+
self._context_token: Token[Context] | None = None
|
|
313
|
+
self._ended = False
|
|
314
|
+
|
|
315
|
+
def _recording(self) -> bool:
|
|
316
|
+
return (
|
|
317
|
+
self._client is not None
|
|
318
|
+
and self._state is not None
|
|
319
|
+
and not self._ended
|
|
320
|
+
and self.span.is_recording()
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def update(
|
|
324
|
+
self,
|
|
325
|
+
*,
|
|
326
|
+
name: str | None = None,
|
|
327
|
+
input: Any = NOT_GIVEN,
|
|
328
|
+
output: Any = NOT_GIVEN,
|
|
329
|
+
model: str | None = None,
|
|
330
|
+
provider: str | None = None,
|
|
331
|
+
system_instructions: Any = None,
|
|
332
|
+
response_model: str | None = None,
|
|
333
|
+
response_id: str | None = None,
|
|
334
|
+
output_type: str | None = None,
|
|
335
|
+
finish_reason: str | None = None,
|
|
336
|
+
usage: Usage | Mapping[str, int] | None = None,
|
|
337
|
+
cost_usd: float | None = None,
|
|
338
|
+
temperature: float | None = None,
|
|
339
|
+
top_p: float | None = None,
|
|
340
|
+
top_k: float | None = None,
|
|
341
|
+
max_tokens: int | None = None,
|
|
342
|
+
stop_sequences: Sequence[str] | None = None,
|
|
343
|
+
seed: int | None = None,
|
|
344
|
+
frequency_penalty: float | None = None,
|
|
345
|
+
presence_penalty: float | None = None,
|
|
346
|
+
time_to_first_chunk_ms: float | None = None,
|
|
347
|
+
tool_name: str | None = None,
|
|
348
|
+
tool_call_id: str | None = None,
|
|
349
|
+
tool_description: str | None = None,
|
|
350
|
+
agent_name: str | None = None,
|
|
351
|
+
agent_id: str | None = None,
|
|
352
|
+
metadata: Mapping[str, Any] | None = None,
|
|
353
|
+
attributes: Mapping[str, AttributeValue] | None = None,
|
|
354
|
+
error: BaseException | None = None,
|
|
355
|
+
) -> SpanHandle:
|
|
356
|
+
if not self._recording():
|
|
357
|
+
return self
|
|
358
|
+
assert self._client is not None and self._state is not None
|
|
359
|
+
try:
|
|
360
|
+
fields = _collect_fields(
|
|
361
|
+
name=name,
|
|
362
|
+
input=input,
|
|
363
|
+
output=output,
|
|
364
|
+
model=model,
|
|
365
|
+
provider=provider,
|
|
366
|
+
system_instructions=system_instructions,
|
|
367
|
+
response_model=response_model,
|
|
368
|
+
response_id=response_id,
|
|
369
|
+
output_type=output_type,
|
|
370
|
+
finish_reason=finish_reason,
|
|
371
|
+
usage=usage,
|
|
372
|
+
cost_usd=cost_usd,
|
|
373
|
+
temperature=temperature,
|
|
374
|
+
top_p=top_p,
|
|
375
|
+
top_k=top_k,
|
|
376
|
+
max_tokens=max_tokens,
|
|
377
|
+
stop_sequences=stop_sequences,
|
|
378
|
+
seed=seed,
|
|
379
|
+
frequency_penalty=frequency_penalty,
|
|
380
|
+
presence_penalty=presence_penalty,
|
|
381
|
+
time_to_first_chunk_ms=time_to_first_chunk_ms,
|
|
382
|
+
tool_name=tool_name,
|
|
383
|
+
tool_call_id=tool_call_id,
|
|
384
|
+
tool_description=tool_description,
|
|
385
|
+
agent_name=agent_name,
|
|
386
|
+
agent_id=agent_id,
|
|
387
|
+
metadata=metadata,
|
|
388
|
+
attributes=attributes,
|
|
389
|
+
error=error,
|
|
390
|
+
)
|
|
391
|
+
_apply_fields(self._client, self.span, self._state, fields)
|
|
392
|
+
except BaseException as exc:
|
|
393
|
+
self._client.report("SpanHandle.update failed", exc)
|
|
394
|
+
return self
|
|
395
|
+
|
|
396
|
+
def end(
|
|
397
|
+
self,
|
|
398
|
+
*,
|
|
399
|
+
name: str | None = None,
|
|
400
|
+
input: Any = NOT_GIVEN,
|
|
401
|
+
output: Any = NOT_GIVEN,
|
|
402
|
+
model: str | None = None,
|
|
403
|
+
provider: str | None = None,
|
|
404
|
+
system_instructions: Any = None,
|
|
405
|
+
response_model: str | None = None,
|
|
406
|
+
response_id: str | None = None,
|
|
407
|
+
output_type: str | None = None,
|
|
408
|
+
finish_reason: str | None = None,
|
|
409
|
+
usage: Usage | Mapping[str, int] | None = None,
|
|
410
|
+
cost_usd: float | None = None,
|
|
411
|
+
temperature: float | None = None,
|
|
412
|
+
top_p: float | None = None,
|
|
413
|
+
top_k: float | None = None,
|
|
414
|
+
max_tokens: int | None = None,
|
|
415
|
+
stop_sequences: Sequence[str] | None = None,
|
|
416
|
+
seed: int | None = None,
|
|
417
|
+
frequency_penalty: float | None = None,
|
|
418
|
+
presence_penalty: float | None = None,
|
|
419
|
+
time_to_first_chunk_ms: float | None = None,
|
|
420
|
+
tool_name: str | None = None,
|
|
421
|
+
tool_call_id: str | None = None,
|
|
422
|
+
tool_description: str | None = None,
|
|
423
|
+
agent_name: str | None = None,
|
|
424
|
+
agent_id: str | None = None,
|
|
425
|
+
metadata: Mapping[str, Any] | None = None,
|
|
426
|
+
attributes: Mapping[str, AttributeValue] | None = None,
|
|
427
|
+
error: BaseException | None = None,
|
|
428
|
+
end_time: TimeInput = None,
|
|
429
|
+
) -> None:
|
|
430
|
+
if not self._recording():
|
|
431
|
+
return
|
|
432
|
+
self.update(
|
|
433
|
+
name=name,
|
|
434
|
+
input=input,
|
|
435
|
+
output=output,
|
|
436
|
+
model=model,
|
|
437
|
+
provider=provider,
|
|
438
|
+
system_instructions=system_instructions,
|
|
439
|
+
response_model=response_model,
|
|
440
|
+
response_id=response_id,
|
|
441
|
+
output_type=output_type,
|
|
442
|
+
finish_reason=finish_reason,
|
|
443
|
+
usage=usage,
|
|
444
|
+
cost_usd=cost_usd,
|
|
445
|
+
temperature=temperature,
|
|
446
|
+
top_p=top_p,
|
|
447
|
+
top_k=top_k,
|
|
448
|
+
max_tokens=max_tokens,
|
|
449
|
+
stop_sequences=stop_sequences,
|
|
450
|
+
seed=seed,
|
|
451
|
+
frequency_penalty=frequency_penalty,
|
|
452
|
+
presence_penalty=presence_penalty,
|
|
453
|
+
time_to_first_chunk_ms=time_to_first_chunk_ms,
|
|
454
|
+
tool_name=tool_name,
|
|
455
|
+
tool_call_id=tool_call_id,
|
|
456
|
+
tool_description=tool_description,
|
|
457
|
+
agent_name=agent_name,
|
|
458
|
+
agent_id=agent_id,
|
|
459
|
+
metadata=metadata,
|
|
460
|
+
attributes=attributes,
|
|
461
|
+
error=error,
|
|
462
|
+
)
|
|
463
|
+
self._ended = True
|
|
464
|
+
self.span.end(_to_ns(end_time))
|
|
465
|
+
|
|
466
|
+
def traceparent(self) -> str | None:
|
|
467
|
+
return traceparent_of(self.span.get_span_context())
|
|
468
|
+
|
|
469
|
+
def __enter__(self) -> SpanHandle:
|
|
470
|
+
if self._client is not None and self._state is not None:
|
|
471
|
+
self._context_token = otel_context.attach(trace.set_span_in_context(self.span))
|
|
472
|
+
return self
|
|
473
|
+
|
|
474
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]:
|
|
475
|
+
if self._context_token is not None:
|
|
476
|
+
otel_context.detach(self._context_token)
|
|
477
|
+
self._context_token = None
|
|
478
|
+
if not self._ended and self._recording():
|
|
479
|
+
if isinstance(exc, BaseException):
|
|
480
|
+
self.end(error=exc)
|
|
481
|
+
else:
|
|
482
|
+
self.end()
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _noop_handle() -> SpanHandle:
|
|
487
|
+
return SpanHandle(trace.INVALID_SPAN, None, None)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def start_span(
|
|
491
|
+
name: str,
|
|
492
|
+
*,
|
|
493
|
+
type: SpanType = "span",
|
|
494
|
+
input: Any = NOT_GIVEN,
|
|
495
|
+
output: Any = NOT_GIVEN,
|
|
496
|
+
model: str | None = None,
|
|
497
|
+
provider: str | None = None,
|
|
498
|
+
system_instructions: Any = None,
|
|
499
|
+
response_model: str | None = None,
|
|
500
|
+
response_id: str | None = None,
|
|
501
|
+
output_type: str | None = None,
|
|
502
|
+
finish_reason: str | None = None,
|
|
503
|
+
usage: Usage | Mapping[str, int] | None = None,
|
|
504
|
+
cost_usd: float | None = None,
|
|
505
|
+
temperature: float | None = None,
|
|
506
|
+
top_p: float | None = None,
|
|
507
|
+
top_k: float | None = None,
|
|
508
|
+
max_tokens: int | None = None,
|
|
509
|
+
stop_sequences: Sequence[str] | None = None,
|
|
510
|
+
seed: int | None = None,
|
|
511
|
+
frequency_penalty: float | None = None,
|
|
512
|
+
presence_penalty: float | None = None,
|
|
513
|
+
time_to_first_chunk_ms: float | None = None,
|
|
514
|
+
tool_name: str | None = None,
|
|
515
|
+
tool_call_id: str | None = None,
|
|
516
|
+
tool_description: str | None = None,
|
|
517
|
+
agent_name: str | None = None,
|
|
518
|
+
agent_id: str | None = None,
|
|
519
|
+
metadata: Mapping[str, Any] | None = None,
|
|
520
|
+
attributes: Mapping[str, AttributeValue] | None = None,
|
|
521
|
+
parent: ParentType = None,
|
|
522
|
+
start_time: TimeInput = None,
|
|
523
|
+
capture_input: bool | None = None,
|
|
524
|
+
capture_output: bool | None = None,
|
|
525
|
+
) -> SpanHandle:
|
|
526
|
+
"""Start a telemetry.dev span. Entering the returned handle (``with``) activates it in the
|
|
527
|
+
current context; without ``with`` it is a detached handle that must be ended via .end()."""
|
|
528
|
+
client = get_client()
|
|
529
|
+
if client is None or not client.enabled or client.tracer is None:
|
|
530
|
+
return _noop_handle()
|
|
531
|
+
try:
|
|
532
|
+
operation = SPAN_TYPE_TO_OPERATION.get(type)
|
|
533
|
+
if operation is None:
|
|
534
|
+
logger.debug("telemetry-dev: unknown span type %r; using 'span'", type)
|
|
535
|
+
operation = SPAN_TYPE_TO_OPERATION["span"]
|
|
536
|
+
state = _SpanState(
|
|
537
|
+
operation=operation,
|
|
538
|
+
capture_input=client.capture_input if capture_input is None else capture_input,
|
|
539
|
+
capture_output=client.capture_output if capture_output is None else capture_output,
|
|
540
|
+
)
|
|
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
|
+
fields = _collect_fields(
|
|
555
|
+
input=input,
|
|
556
|
+
output=output,
|
|
557
|
+
model=model,
|
|
558
|
+
provider=provider,
|
|
559
|
+
system_instructions=system_instructions,
|
|
560
|
+
response_model=response_model,
|
|
561
|
+
response_id=response_id,
|
|
562
|
+
output_type=output_type,
|
|
563
|
+
finish_reason=finish_reason,
|
|
564
|
+
usage=usage,
|
|
565
|
+
cost_usd=cost_usd,
|
|
566
|
+
temperature=temperature,
|
|
567
|
+
top_p=top_p,
|
|
568
|
+
top_k=top_k,
|
|
569
|
+
max_tokens=max_tokens,
|
|
570
|
+
stop_sequences=stop_sequences,
|
|
571
|
+
seed=seed,
|
|
572
|
+
frequency_penalty=frequency_penalty,
|
|
573
|
+
presence_penalty=presence_penalty,
|
|
574
|
+
time_to_first_chunk_ms=time_to_first_chunk_ms,
|
|
575
|
+
tool_name=tool_name,
|
|
576
|
+
tool_call_id=tool_call_id,
|
|
577
|
+
tool_description=tool_description,
|
|
578
|
+
agent_name=agent_name,
|
|
579
|
+
agent_id=agent_id,
|
|
580
|
+
metadata=metadata,
|
|
581
|
+
attributes=attributes,
|
|
582
|
+
)
|
|
583
|
+
if fields:
|
|
584
|
+
_apply_fields(client, span, state, fields)
|
|
585
|
+
return handle
|
|
586
|
+
except BaseException as exc:
|
|
587
|
+
client.report("start_span failed", exc)
|
|
588
|
+
return _noop_handle()
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def update_current_span(
|
|
592
|
+
*,
|
|
593
|
+
name: str | None = None,
|
|
594
|
+
input: Any = NOT_GIVEN,
|
|
595
|
+
output: Any = NOT_GIVEN,
|
|
596
|
+
model: str | None = None,
|
|
597
|
+
provider: str | None = None,
|
|
598
|
+
system_instructions: Any = None,
|
|
599
|
+
response_model: str | None = None,
|
|
600
|
+
response_id: str | None = None,
|
|
601
|
+
output_type: str | None = None,
|
|
602
|
+
finish_reason: str | None = None,
|
|
603
|
+
usage: Usage | Mapping[str, int] | None = None,
|
|
604
|
+
cost_usd: float | None = None,
|
|
605
|
+
temperature: float | None = None,
|
|
606
|
+
top_p: float | None = None,
|
|
607
|
+
top_k: float | None = None,
|
|
608
|
+
max_tokens: int | None = None,
|
|
609
|
+
stop_sequences: Sequence[str] | None = None,
|
|
610
|
+
seed: int | None = None,
|
|
611
|
+
frequency_penalty: float | None = None,
|
|
612
|
+
presence_penalty: float | None = None,
|
|
613
|
+
time_to_first_chunk_ms: float | None = None,
|
|
614
|
+
tool_name: str | None = None,
|
|
615
|
+
tool_call_id: str | None = None,
|
|
616
|
+
tool_description: str | None = None,
|
|
617
|
+
agent_name: str | None = None,
|
|
618
|
+
agent_id: str | None = None,
|
|
619
|
+
metadata: Mapping[str, Any] | None = None,
|
|
620
|
+
attributes: Mapping[str, AttributeValue] | None = None,
|
|
621
|
+
error: BaseException | None = None,
|
|
622
|
+
) -> None:
|
|
623
|
+
"""Apply the update field set to the currently active span; no-ops when there is none."""
|
|
624
|
+
client = get_client()
|
|
625
|
+
if client is None or not client.enabled:
|
|
626
|
+
return
|
|
627
|
+
span = trace.get_current_span()
|
|
628
|
+
if not span.is_recording():
|
|
629
|
+
return
|
|
630
|
+
state = _SPAN_STATES.get(span)
|
|
631
|
+
if state is None:
|
|
632
|
+
operation = "function"
|
|
633
|
+
if isinstance(span, SdkSpan):
|
|
634
|
+
current = (span.attributes or {}).get(ATTR_OPERATION)
|
|
635
|
+
if isinstance(current, str):
|
|
636
|
+
operation = current
|
|
637
|
+
state = _SpanState(
|
|
638
|
+
operation=operation,
|
|
639
|
+
capture_input=client.capture_input,
|
|
640
|
+
capture_output=client.capture_output,
|
|
641
|
+
)
|
|
642
|
+
try:
|
|
643
|
+
fields = _collect_fields(
|
|
644
|
+
name=name,
|
|
645
|
+
input=input,
|
|
646
|
+
output=output,
|
|
647
|
+
model=model,
|
|
648
|
+
provider=provider,
|
|
649
|
+
system_instructions=system_instructions,
|
|
650
|
+
response_model=response_model,
|
|
651
|
+
response_id=response_id,
|
|
652
|
+
output_type=output_type,
|
|
653
|
+
finish_reason=finish_reason,
|
|
654
|
+
usage=usage,
|
|
655
|
+
cost_usd=cost_usd,
|
|
656
|
+
temperature=temperature,
|
|
657
|
+
top_p=top_p,
|
|
658
|
+
top_k=top_k,
|
|
659
|
+
max_tokens=max_tokens,
|
|
660
|
+
stop_sequences=stop_sequences,
|
|
661
|
+
seed=seed,
|
|
662
|
+
frequency_penalty=frequency_penalty,
|
|
663
|
+
presence_penalty=presence_penalty,
|
|
664
|
+
time_to_first_chunk_ms=time_to_first_chunk_ms,
|
|
665
|
+
tool_name=tool_name,
|
|
666
|
+
tool_call_id=tool_call_id,
|
|
667
|
+
tool_description=tool_description,
|
|
668
|
+
agent_name=agent_name,
|
|
669
|
+
agent_id=agent_id,
|
|
670
|
+
metadata=metadata,
|
|
671
|
+
attributes=attributes,
|
|
672
|
+
error=error,
|
|
673
|
+
)
|
|
674
|
+
_apply_fields(client, span, state, fields)
|
|
675
|
+
except BaseException as exc:
|
|
676
|
+
client.report("update_current_span failed", exc)
|