Tracing and observability for LLM calls
Trace every LLM call in production, from what to record to linking traces with users and outcomes, reviewing traces, and monitoring cost and latency.
On this page
Why this matters
When a user reports that “the AI gave me a weird answer”, you need to see exactly what happened: what went in, what came out, which prompt and model produced it, how it ended, how long it took and what it cost. Without that, the report becomes a shrug and a guess. With it, it becomes a bug fix and a new eval case.
Traces are also how you watch the feature as a system: latency creeping up, refusals spiking after a prompt change, cost per request doubling because someone added a long document to the context. This article covers what a trace should contain, a small wrapper that records one per call, a habit for reviewing them, and the alerts worth setting.
What a trace should contain
A trace is the record of one AI request, from your code’s point of view. For each model call, record:
Show text descriptionHide text description
4 parts, in order:
- Context: Feature, prompt version, model, flag variant, pseudonymous user Example: reply-drafts · reply-v4 · Opus · variant B
- Outcome: Stop reason or error type, and the provider request ID Example: end_turn · req_011C…
- Performance: Latency, input, output and cached tokens, cost Example: 3.1 s · 2,900 in · 410 out
- Content (optional): Input and output text, only where policy allows Example: Stored separately, short retention
Two of these deserve emphasis:
- The prompt version. Without it, you can’t tell whether a spike in bad outputs started with a prompt change.
- The provider’s request ID. Every API response carries one. If you ever need to ask the provider about a specific request, it’s the first thing they’ll want.
Content is the sensitive part. Inputs and outputs are what make traces useful for debugging, and they’re user data. Decide deliberately: store content for a sample or for internal users only, keep it in a system with restricted access and short retention, redact what you can, and follow your privacy rules. Many teams keep metadata for every request and content for a small, consented sample.
Instrumenting calls
A thin wrapper around the SDK call is enough to record a trace for every request. This one records context, outcome, tokens, latency and the request ID, and stores content only when you say so:
import Anthropic from '@anthropic-ai/sdk';
import { randomUUID } from 'node:crypto';
const client = new Anthropic();
export interface Trace {
traceId: string;
feature: string;
promptVersion: string;
model: string;
startedAt: string;
latencyMs: number;
outcome: string;
requestId?: string | null | undefined;
inputTokens?: number;
outputTokens?: number;
cacheReadTokens?: number | null;
/** Only when allowed by policy, and stored with restricted access and short retention. */
content?: { input: Anthropic.MessageParam[]; output: string };
}
type Sink = (trace: Trace) => void;
// Replace with your tracing backend. A JSON line per request is already searchable in most log tools.
const logSink: Sink = (trace) => console.log(JSON.stringify(trace));
interface TraceOptions {
feature: string;
promptVersion: string;
storeContent?: boolean;
sink?: Sink;
}
export async function tracedCreate(
params: Anthropic.MessageCreateParamsNonStreaming,
{ feature, promptVersion, storeContent = false, sink = logSink }: TraceOptions,
): Promise<Anthropic.Message> {
const startedAt = new Date();
const base = { traceId: randomUUID(), feature, promptVersion, model: params.model, startedAt: startedAt.toISOString() };
try {
const message = await client.messages.create(params);
const output = message.content.flatMap((b) => (b.type === 'text' ? [b.text] : [])).join('');
sink({
...base,
latencyMs: Date.now() - startedAt.getTime(),
outcome: String(message.stop_reason),
requestId: message._request_id,
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
cacheReadTokens: message.usage.cache_read_input_tokens,
...(storeContent && { content: { input: params.messages, output } }),
});
return message;
} catch (error) {
// Record failures too: error spikes are one of the most useful things a trace shows.
sink({
...base,
latencyMs: Date.now() - startedAt.getTime(),
outcome: error instanceof Anthropic.APIError ? `error ${error.status ?? 'network'}` : 'error unknown',
requestId: error instanceof Anthropic.APIError ? error.requestID : undefined,
});
throw error;
}
}
// Usage: same parameters as client.messages.create, plus trace context.
const message = await tracedCreate(
{
model: 'claude-opus-5-5',
max_tokens: 4096,
output_config: { effort: 'low' },
messages: [{ role: 'user', content: 'Summarize: the launch moved to the 19th because legal needs a week.' }],
},
{ feature: 'thread-summary', promptVersion: 'summary-v2' },
);
// The caller still handles every stop reason, as in every other example on this site.
console.log(message.stop_reason); Things to notice:
- Errors are traced too, with the HTTP status and request ID, then re-thrown so the caller handles them as usual.
- Content is opt-in. The default records metadata only.
- The wrapper doesn’t change behavior. It returns the same message the SDK would; stop-reason handling stays in your feature code.
- The same idea works for streaming: record the trace after
finalMessage(), as the day-one demo route does in its log line.
If you already run an observability backend, you may not need to write this yourself. The Claude SDKs and API can emit OpenTelemetry (opens in a new tab) spans for Messages API requests, following the OpenTelemetry GenAI conventions, so they appear alongside your application’s own traces. It’s in beta at the time of writing, so check the docs for current setup. You’ll still want to attach your own context, such as feature and prompt version.
Reviewing traces
Traces are most valuable when someone reads them regularly, not only during incidents. A weekly habit that takes 30 minutes:
- Sample broadly: 20 random traces from the week, plus every trace with an error, a refusal, a truncation, or a thumbs-down, edit or retry linked to it through the request ID in your analytics events.
- Read inputs and outputs, where you’re allowed to. Note anything surprising: requests outside the intended scope, outputs that are technically fine but unhelpful, unusually long inputs.
- Tag and count, as in capturing feedback.
- Act: new eval cases for real failures, bug tickets for integration problems, and questions for the next product review.
Reading real traces is also the fastest way to keep your intuition calibrated. Teams that never read traces gradually lose touch with what users actually ask.
Monitoring and alerts
From the same traces, build a small dashboard and a few alerts:
| Signal | Why it matters | Example alert |
|---|---|---|
| Error rate | API or integration problems | Above 2% for 10 minutes |
| Refusal rate | Prompt or input changes triggering declines | Twice the 7-day average |
| Truncation rate | Output limits too tight, or verbose prompts | Above 5% |
| Latency (slow tail) | User experience degrading | 95th percentile above budget for 15 minutes |
| Cost per request | Prompt or context growth, wrong model | 50% above the 7-day average |
| Volume | Abuse, loops, or a broken client | Three times normal for the hour |
Break every chart down by prompt version and model. Most regressions line up with a change, and seeing that immediately saves hours. Cost views get their own article: cost dashboards and budgets.
Common mistakes
- No prompt version in traces. Regressions after a prompt change become impossible to pin down.
- Tracing only successes. Errors, refusals and truncations are where traces are most useful.
- Logging all content by default. Store metadata for every request and content only where policy allows, with restricted access and short retention.
- Traces nobody reads. Review a sample weekly; dashboards alone miss the surprising cases.
- Alerts without a breakdown by version. Split monitoring by prompt version and model so you can see which change caused a spike.
Try it
Exercise
About 60 min
Add tracing to a feature and review twenty traces
- Wrap your feature’s model calls with a tracing wrapper like the one above, including prompt version and request ID.
- Generate traffic by using the feature yourself, including error cases (for example, an invalid model name in a test).
- Review twenty traces: tag anything surprising and note one improvement.
- Write down the three alerts you’d set first, with thresholds based on what you saw.
Show a hint
Link traces to your analytics events with a shared request ID. Then “show me the traces behind last week’s thumbs-down” becomes a simple lookup.
Show a worked solution
Example findings from twenty traces of a summary feature (illustrative):
- Two requests included entire email threads with signatures and disclaimers, tripling input tokens. Improvement: strip quoted replies and signatures before sending.
- One truncation on a very long thread. Improvement: raise the output limit slightly and test long threads in evals.
- First alerts: error rate above 2% for 10 minutes; 95th percentile latency above 8 seconds; cost per request 50% above the weekly average.
Check your understanding
Key takeaways
- A trace records context, outcome, performance and, where allowed, content for every model call.
- Always include the prompt version and the provider request ID; trace errors as well as successes.
- Review a sample of traces weekly and turn findings into eval cases and fixes.
- Alert on errors, refusals, truncation, slow-tail latency, cost and volume, broken down by prompt version and model.
Progress is saved in this browser and lights up the roadmap.
Related articles
- Cost dashboards and budgets Track and control AI spend with cost dashboards by feature, model and customer, budgets and alerts, and spend reviewed as a product metric. Level: Expert
- Product analytics events for AI features Instrument AI features with product analytics, with an event schema for requests, outcomes, edits and costs, and the questions it lets you answer. Level: Advanced
- Feature flags and staged rollouts for AI Ship AI features gradually with feature flags, covering staged exposure, kill switches, flagging prompts and models, and rollback plans that work in minutes. Level: Advanced