> ## Documentation Index
> Fetch the complete documentation index at: https://raindrop.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Claude Platform OpenTelemetry

> Send Claude Platform's native OpenTelemetry spans to Raindrop. Covers the Messages API, the Claude SDKs, and Claude Managed Agents, including self-hosted sandboxes.

Claude Platform emits OpenTelemetry spans for your Claude usage on its own. You don't need a Raindrop
package. Point those spans at Raindrop and you get one Raindrop event per user turn, with the input,
the output, tool calls, token usage, and cost. The full span tree appears in the trace view.

The spans come from two sources. Set up one or both, depending on how you call Claude:

| Source | What it is | Set up in |
| - | - | - |
| **API destination** | Claude's API sends spans for every Messages API request and Managed Agents session in your organization, from any client. | The Claude Console, once per organization |
| **SDK tracing** | The Claude SDK (Python, TypeScript, Go, and others) emits spans in your application, including spans for tools that run in your code. | Your application's OpenTelemetry setup |

| You use | API destination | SDK tracing | What SDK tracing adds |
| - | - | - | - |
| [Messages API from any client](#messages-api) (raw HTTP, any language) | Required | n/a | |
| [Claude SDK](#claude-sdk) (`messages.create`, tool runner) | Recommended | Recommended | Tool spans with duration and status; your own spans in the same trace |
| [Claude Managed Agents](#claude-managed-agents) | Required | Recommended | Spans for custom tools your app runs |
| [Managed Agents with a self-hosted sandbox](#self-hosted-sandboxes) | Required | Required in the worker | Every tool run, since the tools run on your infrastructure |

If you send both, Raindrop deduplicates them: each model call is counted once.

> [!NOTE]
>
> Claude Platform's OpenTelemetry support is in beta, and Anthropic may change span names and attributes.
> For a wrapper-based integration with more control over users and properties, see
> [Claude Managed Agents](https://raindrop.ai/docs/integrations/claude-managed-agents.md) and
> [Claude Agent SDK](https://raindrop.ai/docs/integrations/claude-agent-sdk.md).

## Prerequisites

* A Raindrop **write key** from [app.raindrop.ai](https://app.raindrop.ai).
* The **admin** role in your Claude Console organization, to add a destination.

## 1. Add Raindrop as a destination

1. In the Claude Console, open **Settings → OpenTelemetry**
   ([platform.claude.com/settings/telemetry-destinations](https://platform.claude.com/settings/telemetry-destinations))
   and click **Add destination**.

   ![Claude Console OpenTelemetry settings page with the Add destination button](https://mintcdn.com/dawn-a6c57108/T4Z1JpzpxGz8makm/images/claude-otel/1-destinations.png?fit=max\&auto=format\&n=T4Z1JpzpxGz8makm\&q=85\&s=a202bd5a69aba6419d3e7b11845ff6d3)

2. Fill in the destination:

   | Field | Value |
   | - | - |
   | **Provider** | **Custom** |
   | **Name** | Any name you like, for example `Raindrop` |
   | **OTLP endpoint URL** | `https://api.raindrop.ai` (Claude adds `/v1/traces`) |
   | **Request headers** | `Authorization`: `Bearer YOUR_WRITE_KEY` |
   | | Optional: `X-Raindrop-Project-Id`: your [project](https://raindrop.ai/docs/platform/projects.md) slug. Without it, spans go to your default Production project. |
   | **Content mode** | **Full content**. See [Content mode](#content-mode). |
   | **Workspaces** | **All**, or the workspaces you want to monitor |
   | **Maximum content size in MB** (Advanced) | Leave empty, or use any value up to 2 |

   ![Add destination form filled in for Raindrop](https://mintcdn.com/dawn-a6c57108/T4Z1JpzpxGz8makm/images/claude-otel/2-destination-form.png?fit=max\&auto=format\&n=T4Z1JpzpxGz8makm\&q=85\&s=fe73b978ea7ee7484a2cecc5aee5ed83)

3. Click **Add destination**.

4. In the destination's **More actions** menu, click **Test**. The Console should report that the test span
   was delivered. A `401` means the write key or the `Bearer ` prefix is wrong.

   ![Test delivery result for the Raindrop destination](https://mintcdn.com/dawn-a6c57108/T4Z1JpzpxGz8makm/images/claude-otel/3b-test-delivered.png?fit=max\&auto=format\&n=T4Z1JpzpxGz8makm\&q=85\&s=e98cbd5ed44a8192549e13defba987fb)

The destination now forwards spans for all Claude API traffic in the selected workspaces. No code changes
are needed for the Messages API. Add only one Raindrop destination per workspace: a second one sends every
span twice.

> [accordion] Add the destination with the Admin API instead
>
> ```bash theme={null}
> curl --fail-with-body -sS https://api.anthropic.com/v1/organizations/telemetry_destinations \
>   -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \
>   -H "anthropic-version: 2023-06-01" \
>   -H "anthropic-beta: telemetry-destinations-2026-08-11" \
>   -H "content-type: application/json" \
>   -d '{
>     "endpoint_url": "https://api.raindrop.ai",
>     "headers": {"authorization": "Bearer '"$RAINDROP_WRITE_KEY"'"},
>     "traces": {"content_mode": "content"},
>     "workspaces": {"type": "all"}
>   }'
> ```

### Content mode

* **Full content**: spans carry prompts, responses, system prompts, and tool inputs and outputs.
  Raindrop needs this mode to create events.
* **Metadata only**: spans carry models, token counts, timing, and stop reasons, with no text. Raindrop
  stores the spans in the trace view, but doesn't create events from them, so they get no signals and
  aren't included in event usage or cost.

Raindrop accepts spans of any size Claude sends. Raindrop stores the full span and caps event inputs and
outputs for display.

***

## Messages API

With the destination in place, every Messages API request appears in Raindrop. This works from any
client, including raw HTTP. Each request produces:

* `anthropic.messages.create`: the request, with input and output messages, model, stop reason, and usage.
* `anthropic.model_request`: each model run inside the request.
* `anthropic.tool_use <name>`: each [server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) the API ran, such as web search.

**Tool loops.** When your code runs a tool and calls Claude again, Raindrop reads the tool call and its
result from the conversation history. They appear on the event and in the trace view. The API can't see
how long your tool took, so there's no duration or status. Use the [Claude SDK](#claude-sdk) for that.

**Keep a tool loop in one trace.** Send a W3C `traceparent` header with each request from a span you own,
so all the requests of one user turn share a trace. Without it, each request starts its own trace.

```typescript theme={null}
// Any HTTP client. The OpenTelemetry API writes the current span into the headers.
import { context, propagation } from "@opentelemetry/api";

const headers: Record<string, string> = {
  "x-api-key": process.env.ANTHROPIC_API_KEY!,
  "anthropic-version": "2023-06-01",
  "content-type": "application/json",
};
propagation.inject(context.active(), headers); // adds traceparent
await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers,
  body: JSON.stringify(body),
});
```

***

## Claude SDK

The Claude SDK emits its own spans: one per request, one per tool the
[tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) runs (with real
duration and status), and one for the whole tool runner loop. It also sends `traceparent`, so the API
destination's spans nest under the SDK's in the same trace.

Register a tracer provider that exports to Raindrop before you create the client:

```typescript theme={null}
// npm install @anthropic-ai/sdk @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-proto
import Anthropic from "@anthropic-ai/sdk";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const provider = new NodeTracerProvider({
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter({
        url: "https://api.raindrop.ai/v1/traces",
        headers: { Authorization: `Bearer ${process.env.RAINDROP_WRITE_KEY}` },
      }),
    ),
  ],
});
provider.register(); // installs the context manager, so SDK spans nest under yours

const client = new Anthropic({
  openTelemetry: { tracerProvider: provider, traces: { contentMode: "content" } },
});

// ... use client.messages.create / client.beta.messages.toolRunner as usual ...

await provider.shutdown(); // flush before a short-lived process exits
```

* **`contentMode`**: the SDK defaults to metadata only. Set `content` to send prompts and
  responses (the env var `ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content` does the same).
  Without the destination, metadata-only SDK spans create no events.
* **Conversations**: pass a conversation ID per call in the request options
  (`{ openTelemetry: { conversationId } }`). Raindrop uses it as the event's conversation.
* **Without the destination**, Raindrop builds events from the SDK spans alone. You lose the per-model-run
  spans and server tool spans, which only the API emits.

> [!WARNING]
>
> In TypeScript, if you use `@opentelemetry/auto-instrumentations-node` (which includes the undici
> instrumentation), set `openTelemetry: { propagation: false }` on the client. Otherwise each request
> carries two `traceparent` headers and the API's spans don't join your trace.

***

## Claude Managed Agents

The destination covers Managed Agents sessions with no code changes:

* `anthropic.session.turn`: each agent turn, with its input, output, system prompt, and stop reason.
* `anthropic.model_request`: each model run in the turn, with token usage.
* `anthropic.tool_use <name>` / `anthropic.mcp_tool_use <name>`: tools the agent ran in Anthropic's
  cloud sandbox (bash, file tools, web fetch, MCP).

A user message that needs a [custom tool](https://platform.claude.com/docs/en/managed-agents/tools) takes
several turns: the first ends `requires_action`, and your tool result starts the next. Raindrop records
the whole chain as **one event**, with the user's message as input, the final answer as output, the
custom tool calls, and the usage of every turn.

**Add SDK tracing** ([as above](#claude-sdk)) to also get a span for each custom tool your app runs
with the session tool runner (`client.beta.sessions.events.toolRunner`), with duration and status.

**Keep each user message in one trace.** Raindrop chains the turns of one user message within a single
trace. Send the user message and the tool results from inside one span of yours:

```typescript theme={null}
await tracer.startActiveSpan("handle_message", async (span) => {
  const runner = client.beta.sessions.events.toolRunner(session.id, { tools: [myTool] });
  await client.beta.sessions.events.send(session.id, {
    events: [{ type: "user.message", content: [{ type: "text", text: userText }] }],
  });
  // Answers custom tool calls until the session goes idle.
  for await (const call of runner) console.log(`ran ${call.name}`);
  span.end();
});
```

If the user message and the tool results are sent from separate requests (for example, from webhook
handlers), the turns land in different traces. The event then covers only the final turn. See
[Known limitations](#known-limitations).

### Self-hosted sandboxes

In a [self-hosted sandbox](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes),
Claude runs on Anthropic's side, but an **environment worker** on your infrastructure runs every tool.
This applies wherever the worker runs: AWS (for example Lambda MicroVMs), a VPS, Kubernetes, or a
provider such as Modal or E2B. The API destination never sees those tool runs, so:

1. **Set up SDK tracing in the worker process**, exporting to Raindrop as [above](#claude-sdk). The
   `EnvironmentWorker` in the Python, TypeScript, and Go SDKs (and `ant beta:worker poll`) then emits:
   * `anthropic.environment.work`: one span per work item, with `anthropic.environment.id`,
     `anthropic.environment.work.id`, and `anthropic.session.id`.
   * A tool span for each tool it runs (`bash`, `read`, your custom tools), with duration and status.
   * Spans for its own requests, including the tool results it sends back.
2. **Allow outbound HTTPS from the sandbox to `api.raindrop.ai`.**
3. **Keep the API destination on** for the turns and model requests.

```typescript theme={null}
const client = new Anthropic({
  authToken: process.env.ANTHROPIC_ENVIRONMENT_KEY,
  openTelemetry: { tracerProvider: provider, traces: { contentMode: "content" } },
});

await new EnvironmentWorker({
  client,
  environmentId: process.env.ANTHROPIC_ENVIRONMENT_ID,
  environmentKey: process.env.ANTHROPIC_ENVIRONMENT_KEY,
  workdir: "/workspace",
  signal: controller.signal,
}).run();
await provider.shutdown(); // flush spans when the worker stops
```

The worker starts a new trace per work item, and the turns after each tool result nest under the
worker's request. So for a message that used tools, the Raindrop event comes from the worker's trace. It
includes the tool calls with durations, the final answer, and the final turn's usage, but not the turn
that carried the user's prompt. See [Known limitations](#known-limitations).

***

## How spans become events

| Raindrop field | Comes from |
| - | - |
| Input / output | The user's text and the final assistant message of the turn |
| Model | `gen_ai.response.model` (or the request model for Managed Agents) |
| Tool calls | Tool spans (SDK, worker, server tools), plus calls read from the messages when no span exists |
| Token usage and cost | Each model call, counted once across both sources, priced with Claude cache rates |
| Conversation | `anthropic.session.id` (Managed Agents) or `gen_ai.conversation.id` |
| User | See [Identifying users](#identifying-users) |

## Identifying users

Claude's spans don't carry your application's user ID yet. Until they do, Raindrop uses a fallback:
the request ID for Messages API events and the session ID for Managed Agents events. These fallback
users are marked with the event property `user_id_source`, so you can filter them out of user analytics.

## Limits

* Raindrop accepts OTLP/HTTP protobuf exports up to 10 MB compressed and 32 MiB decompressed. Claude's
  per-span content limit is well below this.
* Raindrop counts each Claude span toward your [OTLP rate limit](https://raindrop.ai/docs/sdk/opentelemetry.md#limits-and-troubleshooting).
  An always-on self-hosted worker also emits a small span per poll; these never become events.

## Known limitations

* **Managed Agents turns in separate traces aren't grouped.** When one user message's turns land in
  different traces (self-hosted sandboxes, or tool results sent from separate requests), the event covers
  the final turn's trace only. The user's prompt and the earlier turns' usage are missing from that event.
* **Metadata-only mode creates no events.** Spans appear in the trace view only.
* **Late spans.** Spans that arrive after Raindrop finalizes an event (by default, about 30 seconds after
  the trace goes quiet) are stored in the trace view but not added to the event.

## Troubleshooting

**The Test button fails.** Check that the endpoint is `https://api.raindrop.ai` with no `/v1/traces`
suffix, and that the header value is `Bearer ` followed by your write key.

**No Messages API events.** Make sure the destination covers the workspace your API key belongs to. If
your key isn't scoped to a workspace, requests go to the workspace you name in the
`anthropic-workspace-id` header, or to the default workspace.

**SDK spans are missing.** Register the tracer provider before you create the client, and call
`provider.shutdown()` (or `forceFlush()`) before a short-lived process exits.

**A tool shows no duration.** Only spans from code that runs the tool (the SDK tool runner or a
self-hosted worker) carry timing. Calls read from messages have none.

**Spans appear twice in the trace view.** You have two destinations pointing at Raindrop for the same
workspace. Raindrop still creates one event, but remove the extra destination.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.