What you will be able to do
- Explain why agent systems need tracing rather than bug reports alone
- Choose between metrics, log events and traces for a monitoring question
- Read the Claude span hierarchy and use agent attributes to follow multi-agent runs
- Configure telemetry per process or per agent and tag it by tenant
Key concept
Agent run tracing — Agents are non-deterministic and run many tool calls in sequence, so a final answer tells you little about why a run failed. A trace records each turn, model request and tool call as nested spans, which lets you see where in the run things went wrong.
1.Why agent runs resist ordinary debugging
In ordinary software, a bug usually reproduces: run the same input again and you get the same failure. Agents don't work that way. Anthropic's account of building its multi-agent research system says that agents "make dynamic decisions and are non-deterministic between runs, even with identical prompts." They also run for a long time and keep state across many tool calls, so a small early mistake carries through to the end. Users reported that agents were "not finding obvious information", and the team could not tell which of several causes was to blame: "Were the agents using bad search queries? Choosing poor sources? Hitting tool failures?"
What fixed this was instrumentation, not more careful prompting: "Adding full production tracing let us diagnose why agents failed and fix issues systematically." The team also monitored "agent decision patterns and interaction structures" while deliberately not monitoring "the contents of individual conversations, to maintain user privacy." That tension between enough detail to diagnose and little enough to protect users runs through every strategy decision in this subdomain.
For Claude-based agents, the supported route is OpenTelemetry. The Agent SDK exports traces, metrics and events to your own backend, so you can see which tools a run called, how long each model request took, how many tokens it spent, and where it failed.
Checkpoint 1 of 5· Check yourself
Users report that a research agent sometimes misses obvious information, and rerunning the same prompt doesn't reliably reproduce it. What does the source say let the team find the root cause?
Tracing showed what happened inside each run. The team explicitly avoided monitoring conversation contents, and identical prompts still give non-deterministic runs.
“Adding full production tracing let us diagnose why agents failed and fix issues systematically”Source: www.anthropic.com
Running this across a fleet adds volume and privacy concerns, and Claude Code gives you levers for each. Three are worth knowing before you look at specific signals.
First, which attributes ride on every metric datapoint. OTEL_METRICS_INCLUDE_SESSION_ID, OTEL_METRICS_INCLUDE_ACCOUNT_UUID and OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES all default to true. OTEL_METRICS_INCLUDE_VERSION, OTEL_METRICS_INCLUDE_ENTRYPOINT and OTEL_METRICS_INCLUDE_REPOSITORY default to false. Setting a default-true flag to false removes that attribute from metrics.
Second, content volume. Content-bearing attributes such as model responses and tool content are truncated at 60 KB by default, and CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH can be lowered to cut telemetry volume. Prompt text and tool details are off unless you enable them.
Third, who controls the configuration. When managed settings set OTEL_EXPORTER_OTLP_ENDPOINT, Claude Code removes every developer-set per-signal endpoint, so developers can't point one signal at a different collector. Exporter selectors still follow normal per-key precedence, so set them in managed settings too if you need them locked.
Checkpoint 2 of 5· Exam question
An enterprise is rolling out centrally managed OpenTelemetry export for Claude Code across 400 engineers on Kubernetes, and the observability team wants configuration that scales without per-developer setup and keeps backend storage costs predictable. Which combination of practices should they adopt? (Select all that apply)(Select 3)
Correct answers: A, B, C — Set the telemetry environment variables in the administrator-managed settings.json rather than relying on each developer's shell profile; Add custom OTEL_RESOURCE_ATTRIBUTES such as department and cost_center so spend and usage can be sliced by team in the backend; Turn off OTEL_METRICS_INCLUDE_SESSION_ID and similar cardinality flags that are not needed for the team's dashboards to limit unique time-series growth
- A. Correct. Centralizing telemetry variables in administrator-managed settings.json applies the same collector endpoint and exporter choices to every developer without relying on individual shell configuration, which is the documented enterprise pattern.
- B. Correct. Custom resource attributes like department, team.id, and cost_center are the documented mechanism for multi-team attribution, letting the backend break down token and cost metrics per group.
- C. Correct. High-cardinality attributes such as session.id or account UUIDs multiply the number of unique time series a metrics backend must store; disabling the ones a dashboard does not need is the documented way to control that cost as headcount grows.
- D. The console exporter writes telemetry to the same standard output channel the SDK/CLI uses for other output and is documented as unsuitable for anything beyond local debugging; it is not a scalable production export target.
- E. Requiring per-developer manual shell exports is exactly the fragile, non-scalable setup that centrally managed settings.json is meant to replace, and it invites drift across 400 machines.
2.Metrics, log events and traces answer different questions
Claude Code and the Agent SDK emit three OpenTelemetry signals. Each one has its own exporter selector, and you turn on only the ones you need. Choose by the question you're trying to answer. Metrics are aggregate counters, good for spotting cost spikes or unusual token consumption across a fleet. Log events are one structured record per occurrence, which supports tool success rates, average tool execution times and error patterns by tool type. Traces connect those occurrences into the shape of a single run.
| Signal | What it contains | Enable with |
|---|---|---|
| Metrics | Counters for tokens, cost, sessions, lines of code, and tool decisions | OTEL_METRICS_EXPORTER |
| Log events | Structured records for each prompt, API request, API error, and tool result | OTEL_LOGS_EXPORTER |
| Traces | Spans for each interaction, model request, tool call, and hook (beta) | OTEL_TRACES_EXPORTER plus CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 |
One caveat on the table: the hook span is the exception to its "plus CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1" rule. The interaction, model-request and tool spans need only the enhanced beta flag, but claude_code.hook requires detailed beta tracing, which means ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT. That pair also changes where your logs and traces go.
Checkpoint 3 of 5· Fill the gap
This SDK configuration is meant to export all three signals. Which variable fills the blank?
OTEL_ENV = {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
# Required for traces, which are in beta. Metrics and log events do not need this.
" ? ": "1",
# Choose an exporter per signal. Use otlp for the SDK; see the Note below.
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
# Standard OTLP transport configuration.
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector.example.com:4318",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer your-token",
}CLAUDE_CODE_ENHANCED_TELEMETRY_BETA turns on span tracing. ENABLE_BETA_TRACING_DETAILED is a different, detailed mode that also changes where logs and traces are sent.
Source: code.claude.com3.Following one run, and its subagents, through spans
A trace is a tree of spans. The root, claude_code.interaction, covers a single turn of the agent loop. Under it, each Claude API call and each tool call get their own spans. Tool spans are split into time spent waiting for permission and time spent executing, so a slow tool and a slow human approval look different. When the Agent tool starts a subagent, the subagent's own model-request and tool spans sit under that tool span.
| Span | What it wraps |
|---|---|
| claude_code.interaction | A single turn of the agent loop, from receiving a prompt to producing a response |
| claude_code.llm_request | Each call to the Claude API, with model name, latency, and token counts |
| claude_code.tool | Each tool invocation |
| claude_code.tool.blocked_on_user | The permission wait inside a tool call |
| claude_code.tool.execution | The tool execution itself |
| claude_code.hook | Each hook execution; requires detailed beta tracing |
In a multi-agent system, the attributes on the llm_request span are what make a run readable. agent_id identifies the subagent that issued a request and is absent on the main session. parent_agent_id identifies the agent that spawned it. query_source_safe gives a bounded subsystem label, such as repl_main_thread or agent.builtin.general-purpose, and is emitted even without detailed tracing. workflow.run_id ties agents back to the Workflow tool run that created them. For latency and failure analysis, the same span carries ttft_ms, attempt, error_class and stop_reason.
Checkpoint 4 of 5· Match them up
Match each llm_request span attribute to what it tells you
Tap a term, then the definition that fits it.
Together, agent_id and parent_agent_id rebuild the spawn tree. ttft_ms and stop_reason describe how each individual request behaved.
“Identifier of the agent that spawned this one. Absent for the main session and for agents spawned directly from it”Source: code.claude.com
4.Where telemetry config lives, and tagging it per tenant
There are two places to put the variables. In production, set them in the process environment: your shell, container or orchestrator. Every query() call then picks them up without any code change. Use per-call options (ClaudeAgentOptions.env in Python, options.env in TypeScript) when different agents in the same process need different telemetry settings. The two languages handle this differently. Python merges env on top of the inherited environment. TypeScript replaces the inherited environment entirely, so you have to spread process.env into the object, or you lose PATH and ANTHROPIC_API_KEY.
At scale, you also need to know whose run you're looking at. OTEL_SERVICE_NAME names the agent, and OTEL_RESOURCE_ATTRIBUTES can carry deployment tags or per-request identity. By default these keys are also copied onto metric datapoints, controlled by OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES. Separately, token metrics already carry skill.name, plugin.name and agent.name, which lets you attribute spend to specific subagent types.
That default has a cost. If you put identifiers such as enduser.id or tenant.id in OTEL_RESOURCE_ATTRIBUTES, they land on every metric datapoint, so your metrics backend stores them and metrics grow with the number of distinct users or tenants. Set OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES to false to keep them off metrics, and note that OTEL_METRICS_INCLUDE_SESSION_ID and OTEL_METRICS_INCLUDE_ACCOUNT_UUID are also true by default. The same identity tags still appear as resource attributes on the other signals. Decide per tenant and per signal how much identity you want in aggregate metrics, versus in logs and traces you query for a single run.
from urllib.parse import quote
options = ClaudeAgentOptions(
env={
# ... exporter configuration from the Enable telemetry export example ...
# request is the incoming request object from your web framework.
"OTEL_RESOURCE_ATTRIBUTES": f"enduser.id={quote(request.user_id)},tenant.id={quote(request.tenant_id)}",
},
)Checkpoint 5 of 5· Check yourself
A TypeScript service passes options: { env: otelEnv } to query(), where otelEnv holds only the OTEL variables. What goes wrong?
In TypeScript, env replaces the inherited environment, so you must spread ...process.env first. Python merges instead.
“In TypeScript, env replaces the inherited environment entirely, so include ...process.env in the object you pass.”Source: code.claude.com
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Setting CLAUDE_CODE_ENABLE_TELEMETRY=1 and OTEL_TRACES_EXPORTER=otlp is enough to get traces.Why is that wrong?
Traces are in beta and also need CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1. Metrics and log events don't.
Covered in Metrics, log events and traces answer different questions
2.Per-call env behaves the same in the Python and TypeScript SDKs.Why is that wrong?
Python merges env on top of the inherited environment. TypeScript replaces it entirely, so you must spread process.env.
Covered in Where telemetry config lives, and tagging it per tenant
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Agents make dynamic decisions and are non-deterministic between runs, even with identical prompts”
↩︎ Why agent runs resist ordinary debugging“all without monitoring the contents of individual conversations, to maintain user privacy”
↩︎ Why agent runs resist ordinary debugging“Adding full production tracing let us diagnose why agents failed and fix issues systematically”
↩︎ Checkpoint - 2.
“Export traces, metrics, and events from the Agent SDK to your observability backend using OpenTelemetry.”
↩︎ Why agent runs resist ordinary debugging“Structured records for each prompt, API request, API error, and tool result”
↩︎ Metrics, log events and traces answer different questions“Requires detailed beta tracing (ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT)”
↩︎ Metrics, log events and traces answer different questions“claude_code.llm_request: wraps each call to the Claude API, with model name, latency, and token counts as attributes.”
↩︎ Following one run, and its subagents, through spans“Every query() call picks them up automatically with no code change. This is the recommended approach for production deployments.”
↩︎ Where telemetry config lives, and tagging it per tenant“Spans for each interaction, model request, tool call, and hook (beta)”
↩︎ Key concept“Required for traces, which are in beta. Metrics and log events do not need this.”
↩︎ Exam trap 1“In TypeScript, env replaces the inherited environment entirely, so include ...process.env in the object you pass.”
↩︎ Exam trap 2“Required for traces, which are in beta. Metrics and log events do not need this.”
↩︎ Prediction“In TypeScript, env replaces the inherited environment entirely, so include ...process.env in the object you pass.”
↩︎ Checkpoint - 3.https://code.claude.com/docs/en/monitoring-usageOfficial docs
“Include keys from OTEL_RESOURCE_ATTRIBUTES as attributes on metric datapoints”
↩︎ Why agent runs resist ordinary debugging“or lower it to cut telemetry volume”
↩︎ Why agent runs resist ordinary debugging“Claude Code removes every developer-set per-signal endpoint”
↩︎ Why agent runs resist ordinary debugging“Error patterns by tool type”
↩︎ Metrics, log events and traces answer different questions“Identifier of the subagent or teammate that issued the request. Absent on the main session”
↩︎ Following one run, and its subagents, through spans“Attributing spend to specific skills, plugins, or subagent types via the skill.name, plugin.name, and agent.name attributes”
↩︎ Where telemetry config lives, and tagging it per tenant“Include keys from OTEL_RESOURCE_ATTRIBUTES as attributes on metric datapoints”
↩︎ Where telemetry config lives, and tagging it per tenant“Identifier of the agent that spawned this one. Absent for the main session and for agents spawned directly from it”
↩︎ Checkpoint