Skip to main content

Claude Platform

Claude exports OpenTelemetry traces itself, in the GenAI semantic conventions, so no third-party instrumentation is needed. There are two sources of spans. Each works on its own, and with both on they form one trace.

API tracingPython SDK tracing
Sent byAnthropic's API, server sideThe Claude SDK, in your process
SetupAn organization admin adds Oodle as a destination, onceRegister a tracer provider before the first Claude call
CoversMessages and Sessions requests from any client: SDKs, curl, other toolsEvery SDK request, the tool runner and your custom tools
service.nameclaude-platformYour service
Span kindSERVERCLIENT
Model requests inside one callanthropic.model_requestnone
Server tools (web search, code execution)anthropic.tool_use <tool>none
Managed Agents turns and sandbox toolsanthropic.session.turn, anthropic.tool_use <tool>none
New to Oodle?

Oodle is a managed observability platform for metrics, logs, traces and agent traces. It ingests OpenTelemetry natively, so the snippet on this page is the complete setup.

Each trace shows the full transcript, the token counts and cost of every call, the agent structure (runs, steps and tool calls) and Signals: labels for loops, rate limits, refusals and tool failures, detected as the trace arrives.

Sign up for free to get an instance ID and an API key, or see the Agent Observability overview first.

You will need:

  • OODLE_INSTANCE: your Oodle instance ID (ap1, us1)
  • OODLE_API_KEY: an Oodle API key (ap1, us1)
  • OTLP_ENDPOINT: your Oodle OTLP endpoint, <OODLE_INSTANCE>-otlp.collector.oodle.ai

Open the Claude Platform tile on the ap1, us1 page to get these filled in for you, or let an agent do the setup with /oodle-onboarding set up the claude_platform_observability integration.

API tracing​

Anthropic sends a span for every request in the organization, in the workspaces the destination covers, straight to Oodle. Nothing in your application changes. Adding the destination needs an organization admin (or owner).

In the Claude Console​

Go to Settings > OpenTelemetry (platform.claude.com/settings/telemetry-destinations), click Add destination, select Custom, and enter:

FieldValue
Endpoint URLhttps://<OTLP_ENDPOINT> (the base URL: Anthropic appends /v1/traces)
HeadersX-OODLE-INSTANCE: <OODLE_INSTANCE> and X-API-KEY: <OODLE_API_KEY>
Maximum content size750000 bytes
Content modecontent or metadata_only

The Delivery column shows Healthy once spans arrive, about a minute after the first request.

With the ant CLI​

The same destination, created with the ant CLI (brew install anthropics/tap/ant) and jq:

# Once, as an organization admin. Approve the login in the browser.
ant auth login --profile admin --scope "org:admin"

export OODLE_API_KEY="<OODLE_API_KEY>"

# content: prompts and responses on the spans.
# metadata_only: ids, timing, status and token counts only.
jq -n '{headers: {
"X-OODLE-INSTANCE": "<OODLE_INSTANCE>",
"X-API-KEY": env.OODLE_API_KEY
}}' | env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL \
ant --profile admin \
beta:organization:telemetry-destinations create \
--display-name "Oodle (<OODLE_INSTANCE>)" \
--endpoint-url "https://<OTLP_ENDPOINT>" \
--traces '{"content_mode": "content", "max_content_bytes": 750000}' \
--workspaces '{"type": "all"}' \
--format json

The headers go in on stdin, so the Oodle key is not on a command line. env -u keeps an API key or base URL in your shell from taking the place of the admin login. Anthropic never returns header values: the reply lists only header_names, which must include X-API-KEY. To cover only some workspaces, pass --workspaces '{"type": "include", "workspace_ids": ["wrkspc_..."]}'.

Check delivery: the destination is healthy when state is enabled and last_success_at is set.

env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL \
ant --profile admin \
beta:organization:telemetry-destinations list --format jsonl |
jq '{display_name, state: .state.type,
last_success_at: .delivery_health.last_success_at,
last_error: .delivery_health.last_error}'

Run ant auth logout --profile admin when you are done.

Content mode​

content puts prompts and responses on the spans, which the Transcript view reads. Oodle then stores that text under its retention and access controls. Images and documents are sent as their type only. metadata_only sends ids, timing, status and token counts.

Joining your own trace​

Anthropic makes its span of a request a child of the span named in the W3C traceparent header of that request. The Claude SDKs send the header for you. From any other client, send it yourself, with the sampled flag on:

TRACE_ID=$(openssl rand -hex 16); SPAN_ID=$(openssl rand -hex 8)
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-H "traceparent: 00-$TRACE_ID-$SPAN_ID-01" \
-d '{"model": "claude-opus-5-5", "max_tokens": 256,
"messages": [{"role": "user", "content": "Hello"}]}'
echo "Search Oodle for trace $TRACE_ID"

In an application with OpenTelemetry, inject the current span context into the request headers instead, for example with TraceContextTextMapPropagator().inject(headers) in Python.

Python SDK tracing​

The anthropic Python SDK emits a span for every request through the tracer provider your application registers. No destination or admin is needed.

Install​

pip install "anthropic>=1.14.0" \
opentelemetry-sdk \
opentelemetry-exporter-otlp-proto-http

Instrument​

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# Register the tracer provider before the first Claude call.
provider = TracerProvider(
resource=Resource.create({"service.name": "my-llm-app"})
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)

import anthropic

# Unchanged: every request it sends is now traced.
client = anthropic.Anthropic()

message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)

# Short-lived processes must flush before they exit.
provider.shutdown()

Environment​

Straight to Oodle:

# OTLP endpoint (points straight at Oodle)
export OTEL_EXPORTER_OTLP_ENDPOINT=https://<OTLP_ENDPOINT>

export OTEL_EXPORTER_OTLP_HEADERS="X-API-KEY=<OODLE_API_KEY>,X-OODLE-INSTANCE=<OODLE_INSTANCE>"

# Compress the export: prompt payloads are large
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

# Prompts and responses on the spans. The SDK default,
# metadata_only, sends ids, timing, status and token counts.
export ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content

Or through an OpenTelemetry Collector, where redaction and sampling belong:

# OTLP endpoint (points to your OTel Collector)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

# Prompts and responses on the spans. The SDK default,
# metadata_only, sends ids, timing, status and token counts.
export ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content
receivers:
otlp:
protocols:
http:
endpoint: "0.0.0.0:4318"

processors:
batch:
timeout: 5s
send_batch_size: 512

exporters:
otlphttp/oodle:
endpoint: "https://<OTLP_ENDPOINT>"
# GenAI spans carry whole prompts, and a batch is bounded
# by span count rather than bytes, so an uncompressed
# export can exceed the ingress body limit and be refused.
compression: gzip
headers:
"X-OODLE-INSTANCE": "<OODLE_INSTANCE>"
"X-API-KEY": "<OODLE_API_KEY>"

service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/oodle]

SDK options​

SettingEffect
ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODEcontent or metadata_only (the default)
ANTHROPIC_OPEN_TELEMETRY_TRACES_MAX_CONTENT_BYTESThe content limit of one span (default 750000). Past it, the oldest messages are dropped and the span carries anthropic.content.truncated=true
ANTHROPIC_OPEN_TELEMETRY=falseNo SDK spans. Use it when another Anthropic instrumentation already traces the SDK
Anthropic(open_telemetry={...})The same options in code. When given, the client reads none of the environment variables

To group the requests of one conversation, set a conversation id: client.with_options(open_telemetry={**client.open_telemetry, "conversation_id": "..."}). Every span then carries gen_ai.conversation.id. with_options replaces the whole option, so spread client.open_telemetry into it to keep the content mode.

Using both sources​

With both on, the SDK's CLIENT span of a request is the parent of Anthropic's SERVER span of the same request, and both have the same name:

anthropic.messages.tool_runner              my-llm-app
├─ anthropic.messages.create [CLIENT] my-llm-app
│ └─ anthropic.messages.create [SERVER] claude-platform
│ └─ anthropic.model_request claude-platform
├─ anthropic.custom_tool_use get_weather my-llm-app
└─ anthropic.messages.create [CLIENT] my-llm-app
└─ anthropic.messages.create [SERVER] claude-platform
└─ anthropic.model_request claude-platform

Both spans of a request carry token counts. Read token and cost totals from one source: in ap1, us1, filter on service claude-platform.

Managed Agents​

A Claude Managed Agents session uses both sources. The SDK sends an anthropic.sessions.* span for each Sessions API request. Anthropic sends what the agent does: an anthropic.session.turn span per turn, its anthropic.model_request spans, and an anthropic.tool_use <tool> span for each sandbox tool (bash, write, ...).

anthropic.sessions.events.send  [CLIENT]   my-llm-app
└─ anthropic.sessions.events.send [SERVER] claude-platform
└─ anthropic.session.turn claude-platform
├─ anthropic.model_request
├─ anthropic.tool_use write
├─ anthropic.tool_use bash
└─ anthropic.model_request

Verify​

Send a request to Claude, then open ap1, us1:

  • Filter on service claude-platform for the spans Anthropic sends. Each one shares a trace id with the span whose traceparent it received.
  • Filter on your own service for the SDK spans: anthropic.messages.create, anthropic.messages.tool_runner, anthropic.custom_tool_use <tool> and anthropic.messages.count_tokens.

Click a trace for the Transcript, the waterfall and the cost breakdown.

Demo application​

claude-opentelemetry-demo runs all three cases against Oodle: the Messages API over plain HTTP traced by API tracing, the Python SDK (create, stream, tool runner, count tokens, a conversation), and a Managed Agents session that runs code in its sandbox.

Troubleshooting​

SymptomCheck
400 ... must include the anthropic-workspace-id headerThe Anthropic API key is not scoped to a workspace. Send the anthropic-workspace-id header, and make sure the destination covers that workspace
No claude-platform spansList the destinations: the destination must be enabled and cover the workspace of the API key. Allow about a minute after creating it
Destination failing with 401 or 403The X-API-KEY or X-OODLE-INSTANCE header is wrong
claude-platform spans in traces of their ownThe request had no traceparent header, or its sampled flag was off
No SDK spansThe tracer provider is registered before the first Claude call, and ANTHROPIC_OPEN_TELEMETRY is not false
Each request appears twiceThose are the CLIENT span and the SERVER span of one request. If a third-party Anthropic instrumentation is also installed, set ANTHROPIC_OPEN_TELEMETRY=false

OpenTelemetry export from Claude is in beta, so span names and attributes can change between SDK versions.


Support

If you need assistance or have any questions, please reach out to us through: