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 tracing | Python SDK tracing | |
|---|---|---|
| Sent by | Anthropic's API, server side | The Claude SDK, in your process |
| Setup | An organization admin adds Oodle as a destination, once | Register a tracer provider before the first Claude call |
| Covers | Messages and Sessions requests from any client: SDKs, curl, other tools | Every SDK request, the tool runner and your custom tools |
service.name | claude-platform | Your service |
| Span kind | SERVER | CLIENT |
| Model requests inside one call | anthropic.model_request | none |
| Server tools (web search, code execution) | anthropic.tool_use <tool> | none |
| Managed Agents turns and sandbox tools | anthropic.session.turn, anthropic.tool_use <tool> | none |
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:
| Field | Value |
|---|---|
| Endpoint URL | https://<OTLP_ENDPOINT> (the base URL: Anthropic appends /v1/traces) |
| Headers | X-OODLE-INSTANCE: <OODLE_INSTANCE> and X-API-KEY: <OODLE_API_KEY> |
| Maximum content size | 750000 bytes |
| Content mode | content 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
| Setting | Effect |
|---|---|
ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE | content or metadata_only (the default) |
ANTHROPIC_OPEN_TELEMETRY_TRACES_MAX_CONTENT_BYTES | The 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=false | No 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-platformfor the spans Anthropic sends. Each one shares a trace id with the span whosetraceparentit received. - Filter on your own service for the SDK spans:
anthropic.messages.create,anthropic.messages.tool_runner,anthropic.custom_tool_use <tool>andanthropic.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
| Symptom | Check |
|---|---|
400 ... must include the anthropic-workspace-id header | The 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 spans | List 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 403 | The X-API-KEY or X-OODLE-INSTANCE header is wrong |
claude-platform spans in traces of their own | The request had no traceparent header, or its sampled flag was off |
| No SDK spans | The tracer provider is registered before the first Claude call, and ANTHROPIC_OPEN_TELEMETRY is not false |
| Each request appears twice | Those 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:
- Email at [email protected]