Layers of an LLM observability design
Top: is it useful? → bottom: is it up?
- Business outcomeresolution rate, escalations, user feedback
- Answer qualitysampled grading, refusals, stop reasons
- Cost and tokensper tenant, feature, model; cache hit ratio
- Agent traceseach model call and tool call in a run
- Service healthlatency percentiles, 429 / 529 / 5xx rates
Why Claude systems are hard to observe
Anthropic’s multi-agent research post is candid about the problem: agents behave differently across runs with identical prompts, small failures compound over long runs, and debugging without full production tracing is guesswork. At scale, six challenges recur, and each one points to a strategy.
| Challenge | What it looks like | Strategy it calls for |
|---|---|---|
| Non-determinism | A customer’s bad answer cannot be reproduced by re-running it | Capture the run as it happened: trace with model, config and prompt versions |
| Silent quality failure | HTTP 200, low latency, wrong answer | Quality signals: sampled grading, stop-reason and refusal rates, user feedback |
| Long multi-step runs | Failure appears at step 20, cause was step 3 | Hierarchical traces linking every model and tool call, including subagents |
| Volatile cost | Token use per task varies widely; multi-agent runs use far more | Per-request usage, attributed by tenant, feature and model |
| Sensitive content | Prompts and tool results contain customer data | Structural telemetry by default; content only in approved, access-controlled stores |
| Volume and cardinality | Millions of calls; session IDs explode metric storage | Low-cardinality metrics; IDs on sampled traces and events |
Two of these deserve emphasis. Silent quality failure is what makes LLM monitoring different in kind: the transport layer succeeds, so every classic health check stays green. And cost volatility is real — the same post reports that multi-agent research used about fifteen times the tokens of a chat interaction, and that token usage alone explained most of the variance in quality. A cost spike may be a bug, or it may be the system doing more work because the task demanded it; only per-run data can tell you which.
What classic monitoring sees versus what you need
Classic APM alone
- Uptime, error codes and mean latency
- One request equals one unit of work
- Full request logging is cheap and harmless
- A green dashboard means users are fine
LLM-aware monitoring
- Plus
stop_reason, refusals, tool-call patterns, graded quality - One run spans many model and tool calls
- Content is sensitive: structure by default, content by approval
- Outcome metrics and sampled review confirm usefulness
Signal by signal: what to capture
Metrics answer “is something wrong?” cheaply, for every request. Track latency percentiles (time to first token and total), error rates by type — 429 rate limiting, 529 overloaded, 5xx, and errors that arrive mid-stream after a 200 — and tokens split into uncached input, cache reads, cache writes and output. Track the distribution of stop_reason: a rise in max_tokens or model_context_window_exceeded means truncated answers, and a rise in refusal means something changed in inputs or prompts. Rate-limit headers such as anthropic-ratelimit-input-tokens-remaining show headroom before you hit a wall. Keep metric labels low-cardinality — model, feature, tenant tier — so storage stays affordable.
Traces answer “what happened in this run?”. For agents built on the Claude Agent SDK, the CLI it runs can export OpenTelemetry spans: claude_code.interaction for a turn, claude_code.llm_request for each API call with model, latency and tokens, and claude_code.tool for each tool call, with subagent spans nested under the parent’s tool span so a whole delegation chain is one trace. The SDK propagates W3C trace context (TRACEPARENT), so the agent’s spans appear inside your application’s own trace rather than as a disconnected root. Tracing is enabled with CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and is in beta.
Events and logs answer “exactly which request?”. Every API response carries a request-id header (the Python SDK exposes it as _request_id), which support needs and which joins your logs to Anthropic’s. Log it with the model, prompt and module versions, effort, stop_reason and usage. In Claude Code and the Agent SDK, events such as tool_result, api_request, api_error and tool_decision share a prompt.id that links everything one prompt caused.
from opentelemetry import trace
tracer = trace.get_tracer("claims-assistant")
def ask(messages, tenant, feature):
with tracer.start_as_current_span("claude.request") as span:
span.set_attribute("app.tenant_tier", tenant.tier) # low cardinality
span.set_attribute("app.feature", feature)
span.set_attribute("app.prompt_version", PROMPT_VERSION)
resp = client.messages.create(model=MODEL, max_tokens=2000,
system=SYSTEM, messages=messages)
u = resp.usage
span.set_attribute("claude.request_id", resp._request_id)
span.set_attribute("claude.stop_reason", resp.stop_reason)
for key in ("cache_read_input_tokens", "cache_creation_input_tokens",
"input_tokens", "output_tokens"):
span.set_attribute(f"claude.usage.{key}", getattr(u, key) or 0)
# No prompt or answer text here: content goes only to an
# approved, access-controlled store, if anywhere.
return respOne trace across the application and the agent
query() with active spanTRACEPARENTllm_request spantool span, subagents nestedScale: sample, attribute, and keep content under control
At millions of requests, keeping every trace in full is expensive and rarely useful. A common pattern is to keep metrics for everything, keep full traces for every error, truncation, refusal, unusually slow or unusually expensive run, and sample the healthy remainder. Quality needs its own sample: route a slice of production traffic to an automated grader — code checks where possible, a model-based rubric where necessary — and a smaller slice to human reviewers, as the evals post describes for offline testing. Read transcripts of failures; the evals post notes that reading them is what shows what the agent got wrong and why.
Attribution turns cost from a surprise into a signal. Tag every request with the tenant, feature and prompt version that caused it. In the Agent SDK, end-user and tenant identity can be added as OTEL_RESOURCE_ATTRIBUTES, because by default identity attributes describe your service’s credential, not the person the agent acted for — which also turns tool and permission events into a per-user audit trail for a SIEM. For the organisation’s authoritative numbers, the Usage and Cost Admin API reports usage grouped by model, workspace, API key and more, typically within about five minutes, and daily cost in USD. Use near-real-time telemetry for alerts and the Admin API to reconcile against the bill.
Content is the hardest call. The Agent SDK’s telemetry is structural by default: durations, model names, tool names and token counts, but not prompts, tool arguments, tool output or API bodies — each of those needs its own opt-in variable, and the docs advise leaving them off unless your pipeline is approved to store that data. Anthropic’s own research system monitors agent decision patterns and interaction structures without monitoring conversation contents. Follow that default: prove you need content for a specific diagnosis, capture it for a sample, redact it, restrict access and set retention (governance is covered in domain 5).
Which signal answers this question?
- Is it up and fast?Metricsp95, error types, headroom
- Why did this run fail?Tracemodel and tool spans, versions
- Are answers still good?Sampled gradingcode, model and human review
- Who is spending what?Attributed usagetelemetry plus Admin API
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Treating green uptime and error dashboards as proof the system works | Add quality signals: stop reasons, tool-call patterns, sampled grading and outcomes. |
| Logging every prompt and response in full by default | Capture structure everywhere; capture content only for approved, sampled, access-controlled use. |
| Debugging agent failures by re-running the input | Keep traces of the original run, tagged with model, config and prompt versions. |
| Putting session or user IDs on every metric | Keep metric labels low-cardinality; put IDs on traces and events. |
| One shared key and no request tags in a multi-tenant system | Attribute every request by tenant, feature and version; split workloads by workspace. |
You should now be able to
- Explain why non-determinism, silent failures and multi-step runs make LLM systems hard to observe.
- Select metrics, traces, events and sampled grading to match each observability question.
- Propagate trace context so agent model and tool calls appear inside application traces.
- Design cost attribution with request tags, workspaces and the Usage and Cost Admin API.
- Control sensitive content in telemetry with structural defaults and opt-in, approved capture.
- Keep observability affordable at scale with sampling and low-cardinality metrics.