Claude Code OpenTelemetry gen_ai Attributes: How to Read Them
What gen_ai.provider.name, gen_ai.conversation.id, gen_ai.tool.name and error.type mean on Claude Code spans, plus the split 5m/1h cache token metrics.

Claude Code v2.1.300, released on 14 October, added OpenTelemetry GenAI semantic-convention attributes to its trace spans. The release notes name four of them: gen_ai.provider.name, gen_ai.conversation.id, gen_ai.tool.name and error.type. The same release added cache_creation_5m_tokens and cache_creation_1h_tokens to the api_request event, splitting prompt-cache writes by how long the cached entry lives.
Both changes are listed as additions, not as a new switch to flip. If your pipeline already receives Claude Code spans and events, the first thing to check is the version on the machine emitting them. Below is what each attribute means, where it lives, how to query it from whatever backend you export to, and the related fixes that landed in the same release.
What v2.1.300 changed in Claude Code telemetry#
Three lines in the v2.1.300 release notes matter for anyone running an OpenTelemetry collector against Claude Code:
- GenAI attributes on spans. Trace spans now carry attributes from the OpenTelemetry GenAI semantic conventions. The notes say "such as", so the four named ones are examples rather than an exhaustive list.
- Cache writes split by lifetime. The
api_requestevent gainedcache_creation_5m_tokensandcache_creation_1h_tokens. - Two fixes to existing telemetry.
tool_resultevents now includegit_commit_idaftergit commit -qin reftable repositories, and theuser_promptevent and interaction span now record what a mod'sprompt.submithook actually submitted. More on both further down.
Enabling export itself isn't touched by these notes. If you're setting up from scratch, the Claude Code monitoring docs hold the current configuration options, and they're the place to look when a later release moves something.
What each gen_ai attribute tells you#
The GenAI semantic conventions are a shared vocabulary so that spans from different AI tools can be queried the same way. Claude Code adopting them means a dashboard built for one GenAI-instrumented app can mostly be reused for another. Here's how to read the four attributes the release notes call out.
| Attribute | Question it answers | Typical use |
|---|---|---|
gen_ai.provider.name |
Which provider or backend handled the model call | Split latency and error panels by backend; filter traces |
gen_ai.conversation.id |
Which conversation this span belongs to | Group every span from one session; follow a prompt through its tool calls |
gen_ai.tool.name |
Which tool the span is about | Per-tool duration, call counts and failure rates |
error.type |
What kind of failure ended the span | Error-rate panels and alerts; it's absent when nothing went wrong |
A few reading notes:
gen_ai.conversation.idis the join key. Once you can group by it, the interaction span, its model calls and its tool spans line up as one story instead of a flat list.gen_ai.tool.namelets you stop parsing span names to find out which tool ran. Group by the attribute instead.error.typeis a general OpenTelemetry attribute, not a GenAI-specific one. Backends that already alert onerror.typefor HTTP or database spans can pick up Claude Code failures with the same rule.- The exact string values depend on your setup. The release notes don't enumerate them, so pull one raw span from your exporter and look before you hard-code a filter.
Querying spans by conversation and tool#
Most backends let you group by any attribute, but it helps to see the logic in plain code first. The script below takes a handful of spans in roughly the shape an OTLP exporter hands you (only the attribute names are real; the values are invented) and groups them by conversation, counting tool calls and error types per conversation.
// Synthetic spans. Attribute names come from the v2.1.300 release notes;
// every value here is made up for the sake of the example.
const spans = [
{ attributes: { "gen_ai.provider.name": "example-provider", "gen_ai.conversation.id": "conv-a" } },
{ attributes: { "gen_ai.conversation.id": "conv-a", "gen_ai.tool.name": "Bash" } },
{ attributes: { "gen_ai.conversation.id": "conv-a", "gen_ai.tool.name": "Read" } },
{ attributes: { "gen_ai.conversation.id": "conv-b", "gen_ai.tool.name": "Bash", "error.type": "timeout" } },
{ attributes: { "gen_ai.conversation.id": "conv-b", "gen_ai.tool.name": "Edit" } },
{ attributes: { "gen_ai.tool.name": "Write" } }, // older client: no conversation id
];
const byConversation = {};
for (const { attributes: a } of spans) {
const id = a["gen_ai.conversation.id"] || "(none)";
const bucket = byConversation[id] || (byConversation[id] = { spans: 0, tools: {}, errors: {} });
bucket.spans += 1;
const tool = a["gen_ai.tool.name"];
if (tool) bucket.tools[tool] = (bucket.tools[tool] || 0) + 1;
const err = a["error.type"];
if (err) bucket.errors[err] = (bucket.errors[err] || 0) + 1;
}
console.log(JSON.stringify(byConversation, null, 2));
Translate that into your backend's query language and you have the three panels most people want from this release: spans per conversation, calls per tool, and errors by type. Add gen_ai.provider.name as a second group-by if you run more than one backend. The (none) bucket is deliberate: it's how you'll spot machines still on an older client.
Reading cache_creation_5m_tokens and cache_creation_1h_tokens#
Prompt caching lets a request write part of its prompt into a cache so later requests can reuse it. Cached entries have a lifetime, and the two new fields on the api_request event count the tokens written under each lifetime separately:
| Field | Where | Meaning |
|---|---|---|
cache_creation_5m_tokens |
api_request event |
Tokens written to the cache with the five-minute lifetime |
cache_creation_1h_tokens |
api_request event |
Tokens written to the cache with the one-hour lifetime |
The release notes describe this as "splitting cache writes by lifetime". Two practical consequences follow:
- Combined writes are the sum. If a dashboard previously charted one cache-write number, add both fields to get the same total. Charting only one of them under-reports.
- You can now see which lifetime your sessions actually use. Long-lived and short-lived writes show up as separate series, which is what you need when reconciling telemetry against a bill. The prompt caching docs cover how each lifetime behaves and is priced. Don't take those numbers from a blog post, including this one.
The script below shows the arithmetic on synthetic events. Field names are from the release notes; the numbers are made up.
# Synthetic api_request events. Field names come from the v2.1.300 release notes;
# the token counts are invented.
events = [
{"event": "api_request", "cache_creation_5m_tokens": 1200, "cache_creation_1h_tokens": 0},
{"event": "api_request", "cache_creation_5m_tokens": 0, "cache_creation_1h_tokens": 8400},
{"event": "api_request", "cache_creation_5m_tokens": 300, "cache_creation_1h_tokens": 300},
{"event": "api_request", "cache_creation_5m_tokens": 0, "cache_creation_1h_tokens": 0},
]
total_5m = sum(e.get("cache_creation_5m_tokens", 0) for e in events)
total_1h = sum(e.get("cache_creation_1h_tokens", 0) for e in events)
print(f"{'request':>8} {'5m writes':>10} {'1h writes':>10} note")
for i, e in enumerate(events, 1):
five = e.get("cache_creation_5m_tokens", 0)
hour = e.get("cache_creation_1h_tokens", 0)
if five == hour == 0:
note = "no cache writes"
elif hour > five:
note = "mostly 1h"
elif five > hour:
note = "mostly 5m"
else:
note = "even split"
print(f"{i:>8} {five:>10} {hour:>10} {note}")
print(f"{'total':>8} {total_5m:>10} {total_1h:>10} combined = {total_5m + total_1h}")
In a real pipeline, the same sums become two counters or two series on one chart. Keep them as separate series rather than collapsing them. The whole point of the split is that you can tell them apart.
Other telemetry fixes in the same release#
Two more items are easy to miss because they're filed as fixes rather than features, and both change what you'll see in exported data.
git_commit_id on tool_result events. Before 2.1.300, a git commit -q run inside a repository using git's reftable ref storage could leave tool_result events without git_commit_id. If you join telemetry to commits and some rows have a blank commit id from quiet commits, upgrading is the fix.
Rewritten prompts in user_prompt events. A mod's prompt.submit hook can rewrite or drop a prompt. Older versions still recorded the prompt as typed in the user_prompt event and the interaction span. From 2.1.300 the recorded prompt is the one that was actually submitted, so if your prompt logs disagree with what Claude acted on, that's why.
Neither fix needs configuration. They're worth knowing about because they change historical comparisons: data from before and after the upgrade won't line up for those two fields.
Common mistakes and gotchas#
- Looking for the attributes on the wrong signal. The
gen_ai.*attributes anderror.typeare on trace spans. The cache fields are on theapi_requestevent. If you export only traces or only events, you'll see only one half. - Treating the four named attributes as the full list. The notes say "such as". Inspect a raw span before deciding what your dashboard can and can't show.
- Filtering on
error.typeby value too early. Spans that succeeded don't carry it. Start by filtering on presence, then look at the distinct values your setup actually produces. - Assuming old data has the fields. Spans from a client older than 2.1.300 simply lack
gen_ai.conversation.id. If a group-by shows a large empty bucket, check the versions of the machines emitting telemetry before you debug the collector. - Hard-coding provider names. The value of
gen_ai.provider.namedepends on the backend you use. Read it from your data. - Charting only one cache field.
cache_creation_5m_tokenspluscache_creation_1h_tokensis the total. Pick one and you lose part of the writes. - Expecting pre-upgrade
git_commit_idvalues to backfill. The reftable fix applies to new events only.
Quick reference#
What to check, in order, when wiring up a dashboard on the new fields:
- Confirm the emitting machines run Claude Code v2.1.300 or later.
- Confirm telemetry export is configured per the monitoring docs.
- Pull one raw span and one raw
api_requestevent and list every attribute you get. - Group spans by
gen_ai.conversation.id. That's your session view. - Group spans by
gen_ai.tool.namefor per-tool counts and durations. - Filter spans where
error.typeexists for an error panel; break down by value second. - Add
gen_ai.provider.nameas a group-by if you use more than one backend. - Chart
cache_creation_5m_tokensandcache_creation_1h_tokensas separate series, and sum them for the old combined figure. - Note the upgrade date on charts that touch
git_commit_idor prompt text, since those changed meaning in the same release.
Names at a glance:
Spans: gen_ai.provider.name gen_ai.conversation.id gen_ai.tool.name error.type
api_request event: cache_creation_5m_tokens cache_creation_1h_tokens
Quick answers
Which Claude Code version adds the gen_ai span attributes?
Version 2.1.300, released on 14 October. Spans from older clients won't carry gen_ai.provider.name, gen_ai.conversation.id, gen_ai.tool.name or error.type.
Are cache_creation_5m_tokens and cache_creation_1h_tokens on spans or events?
They're fields on the api_request event, not span attributes. Together they split cache writes by lifetime, so their sum is the combined cache-write figure.
Do I need to change my OpenTelemetry config to get the new attributes?
The release notes list them as additions with no new setting. Upgrade the client and check the Claude Code monitoring docs for the current export configuration.
Why is error.type missing from most of my Claude Code spans?
It only appears on spans that failed. Filter on whether the attribute exists first, then break down by its value.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note