# Join Anthropic's OpenTelemetry export with gateway traces

URL: https://infro.io/blog/join-anthropic-s-opentelemetry-export-with-gateway-traces
Author: INFRO Editorial (https://infro.io/blog/editorial)
Publisher: INFRO
Published: 2026-10-29
Last modified: 2026-10-29
Last reviewed: 2026-10-29 (next review due 2026-11-28)
Topic: Architecture and migration
Format: Guide

> Treat the gateway span as the record of what your application asked for and what it was estimated to cost, and the Anthropic export as the provider's view of the same call. Join them on a request id, and let only one source feed cost.

Key takeaways:
- Anthropic launched OpenTelemetry export in beta on 14 October 2026, covering Messages API requests and Claude Managed Agents sessions, so treat attribute names as unstable until you have checked them.
- A single call can produce up to three spans: your SDK or app span, the gateway span, and the Anthropic-side span. Token counts and cost appear in more than one of them.
- Pick one source for cost and tag every span with its origin, so dashboards never add tokens or dollars across layers.
- Join on a request id, not on timestamps; expect one gateway request to map to zero, one or several provider calls once caching, failover and retries are involved.
- Span delivery is best effort, so spans can show you where to look, but a reconciliation against the provider's cost report is what proves the totals.

## 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 metadata[3] | Your collector |
| Provider | Anthropic, through a Console destination | The request as Anthropic processed it | The destination you configured |

Table: The three vantage points on one Claude call.

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](https://infro.io/blog/detect-gateway-bypass-provider-usage).

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:

1. **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]
2. **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.
3. **Tag every span with its origin** at the collector, so a query can exclude a layer by construction.
4. **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.

> **Example: Illustrative: how double counting looks**
> These numbers are made up to show the mechanism. One request uses 1,000 input and 200 output tokens. The gateway span records 1,200 tokens and the provider span records 1,200 tokens. A dashboard that groups by model across both sources reports 2,400 tokens and twice the cost, and it does so for every call, so the error is a constant factor that looks like growth when you first turn the export on. The fix is the origin tag plus a filter, not a change to either exporter.

## 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.

```yaml
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

```sql
-- 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](https://infro.io/blog/llm-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.[1][2] 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](https://infro.io/docs/observability/exports).

## 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.origin` tag 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](https://infro.io/blog/opentelemetry-llm-tracing), and for multi-call agent runs, [tracing an agent run](https://infro.io/blog/trace-ai-agent-runs).

## Questions

### Do I still need gateway spans if Anthropic exports its own?

Yes, if you route more than one provider or use failover. The Anthropic export describes calls that reached Anthropic. It cannot show a request that was answered from a gateway cache, refused by a budget before any provider call, or sent to another provider after a failure.

### Can I sum token counts from both exports to get a total?

No. Both layers describe the same call, so adding them counts it twice. Choose one source for tokens and cost, and use the other only for timing and for checking the first.

### Is the Anthropic export safe to rely on in production?

Anthropic released it as a beta, so attribute names and behavior can change. Pin your collector transformations, alert on missing attributes, and keep your reconciliation against the provider's cost report as the check on totals.

## Sources

1. Claude Platform release notes — Anthropic. https://platform.claude.com/docs/en/release-notes/overview (accessed 2026-10-29)
2. Generative AI semantic conventions (moved) — OpenTelemetry. https://opentelemetry.io/docs/specs/semconv/gen-ai/ (accessed 2026-10-29)
3. Exports & OpenTelemetry — INFRO. https://infro.io/docs/observability/exports (accessed 2026-10-29)
4. Request traces — INFRO. https://infro.io/docs/observability/traces (accessed 2026-10-29)
5. Manage costs and demonstrate ROI for Claude Code on Google Cloud — Anthropic. https://claude.com/resources/articles/how-to-manage-costs-and-demonstrate-roi-for-claude-code-on-google-cloud (accessed 2026-10-29)
