What Anthropic shipped, and what is still unconfirmed
On 14 October 2026 Anthropic's release notes announced OpenTelemetry export in beta, so you can trace Messages API requests and Claude Managed Agents sessions in your own observability platform. The Claude SDK emits spans in Python, TypeScript, C#, Go, Java, PHP and Ruby. To get spans from the API itself, you add an OpenTelemetry destination in the Claude Console.1
That is the whole confirmed surface for this article. I did not read the span reference, so the rest of this guide does not state which attributes the spans carry, whether they include a request id, how they are sampled or how fast they arrive. Those are things to check in a short test before you build dashboards on them, and the sections below say exactly what to check. The join method works whatever the answers are, as long as one field on the Anthropic side identifies the request.
The beta label matters for a second reason. OpenTelemetry's own GenAI conventions have moved to a separate repository, and the pages inside the main semantic-conventions site are marked as no longer maintained.2 If your dashboards rely on attribute names, pin the version of the conventions you built against and expect to revisit them.
Three layers can describe one call
If your application calls Claude through a gateway, one request can leave up to three kinds of span:
| Layer | Emitted by | What it knows | Where it goes |
|---|---|---|---|
| Client | Your app or the Claude SDK | The call as the application made it, including time spent waiting | Your collector |
| Gateway | The gateway | Request id, model, route, timing, token counts, estimated cost and configured metadata3 | Your collector |
| Provider | Anthropic, through a Console destination | The request as Anthropic processed it | The destination you configured |
Whether the SDK's spans still work when its base URL points at a gateway is something to test rather than assume. The same goes for whether the Console destination is scoped to an organization or narrower. Both change how many layers you really have.
The risk is not missing data. It is the same call showing up in three places with token counts on at least two of them, and a dashboard that adds them.
Which signals duplicate, which add
Duplicated across gateway and provider spans, in all likelihood: model, outcome, token counts and total latency. INFRO's span carries request id, model, route, timing, token counts and estimated cost, and it deliberately leaves out prompt and response captures.3 Anything the Anthropic export also carries for those fields is a duplicate, and you should pick one.
What the provider export can add, to be verified against real spans:
- Time inside Anthropic versus time outside it. The gateway sees total latency to the provider. A provider-side span, if it records its own duration, lets you subtract it and see network and queueing on your side.
- Managed Agents sessions. The release note says sessions are traced.1 A gateway sees only the model calls that pass through it, so work that happens inside a session on Anthropic's side is visible only here, if at all.
- Calls that skipped the gateway. If the destination covers a whole account, it will include traffic that never touched your gateway. That is useful for finding bypass, which is a separate problem covered in detecting provider usage that bypassed your gateway.
What only the gateway span can add: the route that was chosen, response-cache status, guardrail actions and anything decided before a provider call. INFRO's spans carry infro.cache.status on chat completions with the values hit, miss, bypass, uncacheable or disabled, and infro.guardrail.action when guardrails acted.3 A cache hit has a gateway span and no provider span at all, and that is correct, because the provider never billed it.
Rule one: a single owner for cost
Cost is where double counting does damage, because tokens turn into dollars and the dollars feed budgets. Decide up front:
- Tokens and cost come from one layer. For a gateway deployment that is normally the gateway span, because it is the only layer that knows which negotiated rate applies and which route served the call. INFRO's request log is explicit that its figure is an estimate and operational evidence, not an invoice.4
- The provider export is evidence, not a second ledger. Use it for timing, for sampling a handful of calls against the gateway's counts, and for the traffic the gateway never saw.
- Tag every span with its origin at the collector, so a query can exclude a layer by construction.
- Check totals against the provider's own cost report, not against a sum of spans. Span delivery is lossy by design, as the next section shows.
If you also run Claude Code, note that it has its own OpenTelemetry metrics and events that report usage and estimated cost.5 That is a fourth possible cost source. Give it the same treatment: a separate origin tag and no summing.
Rule two: join by request id, not by time
Timestamps do not join spans reliably: clocks differ, one call can be retried, and concurrent requests have near-identical start times. Use ids.
Step 1: put the gateway request id on your own span
The gateway returns or records an id for every request; INFRO's request records have ids such as req_8fJq3kT2 and are readable at GET /v1/requests/{id}.4 Capture it in your application span as an attribute, so the client layer already carries the gateway key.
Step 2: find the provider's identifier
Look at a few real spans from the Anthropic destination and find the field that identifies the request on Anthropic's side. Then check whether the gateway also stores that provider-side id. If it does not, you have a gap: the gateway id and the provider id live in different layers with nothing linking them. Resolve that before building anything. Options include storing the provider's response id on the client span, or relying on any trace context that the export honors. Test the second with a call carrying a known trace id, and do not assume it.
Step 3: build the join table at the collector
Tag origin at ingestion so every later query can separate the layers. This OpenTelemetry Collector fragment is a sketch; attribute names are yours to choose.
processors:
transform/gateway:
trace_statements:
- context: span
statements:
- set(attributes["telemetry.origin"], "gateway")
transform/provider:
trace_statements:
- context: span
statements:
- set(attributes["telemetry.origin"], "anthropic")Route each receiver through its own processor in separate pipelines, then export both to the same backend. The tag is the only thing that keeps the layers apart afterwards.
Step 4: query one layer for cost, the other for timing
-- illustrative schema: one row per span
SELECT g.gateway_request_id,
g.cost_estimate_usd, -- cost: gateway only
g.duration_ms - p.duration_ms AS outside_provider_ms
FROM spans g
LEFT JOIN spans p
ON p.provider_request_id = g.provider_request_id
AND p.origin = 'anthropic'
WHERE g.origin = 'gateway';The LEFT JOIN is deliberate. Gateway rows with no provider match are not errors; they are the cache hits, the requests refused before a call, and the calls sent to other providers.
Failure modes to design for
Cardinality is not one to one. A gateway request maps to zero provider calls on a cache hit or a refusal, to one in the normal case, and to more than one when a failed attempt is retried elsewhere. INFRO's failover retries a request that times out or returns a 5xx before the response starts, without the client seeing it. A failed attempt may leave a provider span and little or no token usage. If you want attempt-level detail, INFRO's request log records the provider connection and fallback attempts.4 Aggregate provider spans per gateway id before comparing.
Delivery is best effort. INFRO sends spans outside the response path, retries once on a server or network error, does not retry a 4xx from your collector, and drops spans on persistent failure rather than block requests. Its docs say not to use the channel as the sole accounting record.3 Assume the Anthropic beta has similar limits until you read its documentation, and alert on the ratio of joined to unjoined rows.
Payload content. The gateway's spans exclude prompt and response captures.3 Check whether the provider export includes content, and decide in your prompt logging policy before you turn it on in production, since a destination you forgot about is a second copy of the data.
Attribute drift. It is a beta and the conventions it follows are moving.12 Put a test in CI that fails when an attribute you depend on disappears from sample spans.
Getting the gateway layer without extra instrumentation
If you run INFRO, its OpenTelemetry export pushes spans from the request path to your own collector, with the endpoint and headers set in the console under Settings → Gateway → Trace export, as OTLP/HTTP protobuf, and it is included on every plan.3 That is the gateway layer described above. For totals, INFRO's daily reconciliation compares estimated spend against the cost reported by Anthropic's own cost API, which is the check that spans alone cannot give you. Details are in the exports and OpenTelemetry docs (opens in a new tab).
Checklist before you rely on the join
- Send 20 test calls with known ids and confirm each one appears in the gateway layer and in the Anthropic destination.
- Identify the provider-side request identifier field and confirm it is stable across the two layers.
- Add a
telemetry.origintag at the collector for every source. - Choose the cost source and exclude the others from every cost panel.
- Confirm cache hits and refused requests appear as unmatched gateway rows, not as errors.
- Alert on the share of gateway rows with no provider match once cache and failover traffic is excluded.
- Reconcile monthly totals against the provider's cost report.
- Review what the provider export carries about prompt content before enabling it broadly.
For the generic setup of GenAI spans and privacy, see OpenTelemetry for LLM calls, and for multi-call agent runs, tracing an agent run.
