Telemetry
Pass a tracer, get GenAI spans for every model and tool call.
Recipes instruments the session for you. Pass a tracer and every model call, tool call and agent run is emitted as OpenTelemetry GenAI semantic-convention spans, so a recipe is observable in any OTLP backend without writing instrumentation and without a vendor SDK in the package.
The span tree
One invoke_agent span per run, with a chat span per model call and an execute_tool span per tool call nested under it.
A delegated run inherits the conversation id and derives its own agent identity, so one trace shows the lead and everything it orchestrated while gen_ai.agent.name still tells them apart. If you already wrap a user turn in your own span, pass runSpans: false and getParentContext to keep your topology and let the chat and tool spans land under it.
What each span carries
| Chat span | Attribute |
|---|---|
| Identity | gen_ai.conversation.id, gen_ai.agent.id, gen_ai.agent.name |
| Model | gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model |
| Request | gen_ai.request.temperature, .max_tokens, .reasoning.level, .stream |
| Content | gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, gen_ai.tool.definitions |
| Result | gen_ai.response.id, gen_ai.response.finish_reasons, gen_ai.conversation.compacted |
| Usage | gen_ai.usage.input_tokens, .output_tokens, .reasoning.output_tokens, .cache_read.input_tokens, .cache_creation.input_tokens, gen_ai.cost.usd |
| Latency | gen_ai.response.time_to_first_chunk |
An execute_tool span carries the same identity attributes plus gen_ai.tool.name, gen_ai.tool.type, gen_ai.tool.call.id, and the call's arguments and result. Results are kept for failed calls as well as successful ones, so a conversation reconstructs losslessly from the trace rather than only when everything worked.
Metrics
Pass a meter alongside the tracer for the standard histograms: gen_ai.client.operation.duration, gen_ai.client.token.usage, gen_ai.client.operation.time_to_first_chunk, gen_ai.client.operation.time_per_output_chunk, gen_ai.execute_tool.duration and gen_ai.invoke_agent.duration.
Cancellations are not errors
This distinction matters more than it sounds. A user pressing escape, a runtime stopping a run, and an interaction pausing for an answer are all aborts, and treating them as failures poisons every error rate and alert you build on the trace.
A requested abort ends its span with gen_ai.response.finish_reasons = ["aborted"] and introspection.termination_reason of cancelled or awaiting_user, with no error status and no synthetic exception. Tool calls cut short the same way are marked and not failed. An unclaimed abort, or a real provider or model failure, is still recorded as an error with a standard exception event.
So status = ERROR on a recipe trace means something actually went wrong, which is what makes the number worth alerting on.
Conversation content
GenAI spans carry the full conversation: prompts, replies, system instructions, tool arguments and results. That is what makes them useful for judging and debugging, and it is also why they are not the spans you want in a general-purpose infrastructure backend.
Export one stream to two places by wrapping the second exporter. It strips the content-bearing attributes and keeps the structural signal: operation, provider, model, usage, timing and tool names.
Scrubbing decides per attribute on every span rather than per scope, so there is no configuration that can fail open, and the original span is never mutated, so a second processor on the same stream still sees it whole. isGenAiContentAttribute(key) exposes the same predicate if you scrub in your own pipeline.