Skip to content
Maple Docs
Open app
Browse the docs
On this page

Trace Claude API and Claude SDK calls with OpenTelemetry

Turn on the Claude SDK's built-in OpenTelemetry tracing, or add Maple as a destination in the Claude Console, so each conversation shows up in Maple as one Agent Session.

The Claude SDK and the Claude API export OpenTelemetry spans (beta). SDK tracing runs in your process and nests Claude’s spans under your own. API tracing runs at Anthropic once you add a destination in the Claude Console. Start with the SDK: it carries the conversation id that Agent Sessions groups by.

Pass a conversationId with every request, or each request becomes a session of its own.

Tested with @anthropic-ai/sdk 0.135.0. For the Claude Agent SDK or Claude Code, use the Claude Agent SDK guide.

Set up SDK tracing

Install the Claude SDK and the OpenTelemetry Node SDK:

npm install @anthropic-ai/sdk@0.135.0 @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto
pnpm add @anthropic-ai/sdk@0.135.0 @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto
bun add @anthropic-ai/sdk@0.135.0 @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto

Pin the Claude SDK version. Span names and attributes can change between releases during the beta.

Register a tracer provider before the first Claude call:

// instrumentation.ts
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
import { NodeSDK } from "@opentelemetry/sdk-node"

// Reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS.
export const sdk = new NodeSDK({
	serviceName: "support-agent",
	traceExporter: new OTLPTraceExporter(),
})

sdk.start()

Point the exporter at Maple with a private ingest key (maple_sk_…) from Settings → Ingestion:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.maple.dev"   # https://ingest.eu.maple.dev for EU organizations
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer maple_sk_YOUR_INGEST_KEY"

Send a conversation id with every request

Maple groups spans into a session by gen_ai.conversation.id, which the SDK sets from the conversationId request option. The id is exported in every content mode, so use an opaque one, never an email address or a name.

// agent.ts
import { sdk } from "./instrumentation.ts"

import Anthropic from "@anthropic-ai/sdk"
import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema"
import { trace } from "@opentelemetry/api"

const tracer = trace.getTracer("support-agent")

// "content" puts prompts, replies and tool arguments on the spans.
const client = new Anthropic({ openTelemetry: { traces: { contentMode: "content" } } })

const lookupOrder = betaTool({
	name: "lookup_order",
	description: "Look up an order by its id. Returns status and total.",
	inputSchema: {
		type: "object",
		properties: { order_id: { type: "string" } },
		required: ["order_id"],
	},
	run: ({ order_id }) => JSON.stringify({ order_id, status: "shipped", total: 89.5 }),
})

async function answer(conversationId: string, question: string) {
	return tracer.startActiveSpan("support.answer", async (span) => {
		try {
			const message = await client.beta.messages
				.toolRunner(
					{
						model: "claude-sonnet-5-5",
						max_tokens: 1024,
						system: "You are the support agent for an online shop. Use the tools to answer.",
						tools: [lookupOrder],
						messages: [{ role: "user", content: question }],
					},
					{ openTelemetry: { conversationId } },
				)
				.runUntilDone()
			return message.content
		} finally {
			span.end()
		}
	})
}

console.log(await answer("conv_7f3a", "Where is my order A-1042?"))

// Flush buffered spans before the process exits.
await sdk.shutdown()

Use runUntilDone() or await the runner. runner.done() resolves only while something else iterates the runner, so a script that calls it hangs and exports nothing.

Choose what content to record

contentMode: "content" records the system prompt, messages, replies, and tool arguments and results, which the transcript needs. The default, metadata_only, records ids, timing, status and token counts, and the rest of the session view still works. Turn on content only if your Maple organization is allowed to store it.

The SDK keeps up to 0.75 MB of content per span and drops the oldest messages beyond that. Raise maxContentBytes for long contexts.

Add API tracing in the Claude Console

API tracing adds what only the API sees: each model run inside a request, server tools such as web search, MCP connector tools and Managed Agents turns. It needs the Console admin role and covers the Claude Platform, including Claude Platform on AWS. Calls through Amazon Bedrock, Google Cloud or Microsoft Foundry are traced by the SDK only.

In the Console, open Settings → OpenTelemetry, click Add destination and pick the Custom provider:

  • OTLP endpoint URL: https://ingest.maple.dev, or https://ingest.eu.maple.dev for EU organizations, without /v1/traces.
  • Request headers: Authorization with the value Bearer maple_sk_….
  • Content mode: Full content for transcripts.
  • Workspaces: the workspaces whose traffic you want in Maple.
The Claude Console's Add destination form filled in for Maple: provider Custom, endpoint https://ingest.maple.dev, an Authorization header and Full content selected.

Save, then click Test in the destination’s More actions menu. The API sends one anthropic.test span with the service claude-platform, which you can find in Explore → Traces.

With SDK tracing on as well, each request is one trace. The SDK sends a traceparent header and the API parents its spans under the SDK’s request span:

support.answer                              your span           service: support-agent
└─ anthropic.messages.tool_runner           SDK, INTERNAL       service: support-agent
   ├─ anthropic.messages.create             SDK, CLIENT         service: support-agent
   │  └─ anthropic.messages.create          API, SERVER         service: claude-platform
   │     ├─ anthropic.model_request         API                 service: claude-platform
   │     └─ anthropic.tool_use web_search   API                 service: claude-platform
   └─ anthropic.custom_tool_use lookup_order  SDK, INTERNAL     service: support-agent

Check that it works

Run a two-turn conversation with one tool call, then open Agent Sessions. You should see one session with one trace per turn, the model calls with their tokens, and the tool call by name. The framework shows as Unidentified.

The Traces tab of a Maple Agent Session: two turns, each with anthropic.messages.tool_runner, two anthropic.messages.create calls with token counts, and a lookup_order tool span.

Cost shows as unpriced, because Claude’s spans carry token counts without a price. Anthropic’s Usage and Cost API has spend per API key.

Troubleshooting

  • No spans at all. Register the tracer provider before the first Claude call, and check that ANTHROPIC_OPEN_TELEMETRY isn’t false.
  • Every request is its own session. Pass conversationId on each request.
  • The last spans of a script are missing. Call sdk.shutdown() before the process exits.
  • API spans don’t nest under your spans, or tokens are counted twice. Register the W3C trace context propagator (NodeSDK does) and keep the client’s propagation option on. With @opentelemetry/auto-instrumentations-node, set propagation: false on the Claude client instead: its undici instrumentation adds a second traceparent header.
  • The Console shows Failing with 401. The Authorization header is wrong, or the key belongs to the other region.
  • The transcript is empty. The client or the destination is in metadata-only mode.