# OpenTelemetry Tracing Plugin for Claude Code A Claude Code plugin that traces conversations, tool calls, subagent executions, and context compaction via [OpenTelemetry](https://opentelemetry.io/) OTLP to any compatible backend — Honeycomb, Jaeger, Grafana, Datadog, or anything else that speaks OTLP. ## Prerequisites - [Node.js](https://nodejs.org/) v20+ ## Installation ### From source ```bash pnpm install pnpm build claude --plugin-dir /path/to/otel-claude-code-hooks ``` ### Setting environment variables **Option 1: Claude Code settings file (recommended)** Add to `~/.claude/settings.json`: ```json { "env": { "CC_OTEL_ENABLED": "true", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.honeycomb.io", "OTEL_EXPORTER_OTLP_HEADERS": "x-honeycomb-team=YOUR_API_KEY", "OTEL_SERVICE_NAME": "claude-code" } } ``` **Option 2: Export to shell** Add to your `~/.zshrc`, `~/.bashrc`, or `~/.bash_profile`: ```bash export CC_OTEL_ENABLED="true" export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io" export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY" export OTEL_SERVICE_NAME="claude-code" ``` ### Backend examples **Honeycomb** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io" export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY" ``` **Jaeger (local)** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" ``` **Grafana Cloud** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-gateway-prod-us-central-0.grafana.net/otlp" export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic BASE64_ENCODED_CREDENTIALS" ``` ## What gets traced The plugin creates a span hierarchy for each conversation turn: ``` Claude Code Turn (SERVER) ├── chat claude-sonnet-4-5 (CLIENT) — LLM call with gen_ai.* attributes ├── Read (INTERNAL) — tool execution ├── Edit (INTERNAL) — tool execution ├── chat claude-sonnet-4-5 (CLIENT) — next LLM call └── Bash (INTERNAL) — tool execution ``` **LLM spans** include [Gen AI semantic convention](https://opentelemetry.io/docs/specs/semconv/gen-ai/) attributes: - `gen_ai.system`: `"anthropic"` - `gen_ai.operation.name`: `"chat"` - `gen_ai.request.model` / `gen_ai.response.model` - `gen_ai.usage.input_tokens` / `gen_ai.usage.output_tokens` - `gen_ai.usage.cache_read_input_tokens` / `gen_ai.usage.cache_creation_input_tokens` - `gen_ai.response.finish_reasons` **Tool spans** include `gen_ai.tool.name`. **Subagent runs** are nested under their parent Agent tool span as a chain of sub-turns. **Context compaction** events are traced with trigger type and summary. **Interrupted turns** are marked with `SpanStatusCode.ERROR` and message `"Interrupted"`. ## Environment variables | Variable | Required | Default | Description | |---|---|---|---| | `CC_OTEL_ENABLED` | Yes | — | Set to `"true"` to enable tracing | | `OTEL_EXPORTER_OTLP_ENDPOINT` | No | `http://localhost:4318` | OTLP endpoint URL | | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | No | — | Overrides the generic endpoint for traces only | | `OTEL_EXPORTER_OTLP_HEADERS` | No | — | Comma-separated `key=value` pairs (e.g., `x-honeycomb-team=KEY`) | | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | No | — | Overrides the generic headers for traces only | | `OTEL_SERVICE_NAME` | No | `"claude-code"` | `service.name` resource attribute | | `CC_OTEL_DEBUG` | No | `"false"` | Enable debug logging to `~/.claude/state/hook.log` | | `CC_OTEL_TRACEPARENT` | No | — | W3C traceparent to nest traces under an external parent | ## Nesting traces under an existing span Set `CC_OTEL_TRACEPARENT` to a [W3C traceparent](https://www.w3.org/TR/trace-context/) string to nest all Claude Code traces as children of an existing span. This is useful when Claude Code is invoked programmatically as part of a larger traced workflow. ```python import subprocess from opentelemetry import trace tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("my-workflow") as span: ctx = span.get_span_context() traceparent = f"00-{format(ctx.trace_id, '032x')}-{format(ctx.span_id, '016x')}-01" subprocess.run( ["claude", "-p", prompt], env={ **os.environ, "CC_OTEL_ENABLED": "true", "CC_OTEL_TRACEPARENT": traceparent, }, ) ``` The resulting trace hierarchy: ``` my-workflow (your app) └── Claude Code Turn 1 (SERVER) ├── chat claude-sonnet-4-5 (CLIENT) ├── Read (INTERNAL) └── chat claude-sonnet-4-5 (CLIENT) ``` ## Architecture The plugin uses 9 Claude Code hooks, each running as a separate Node.js process: | Hook | Purpose | |---|---| | `UserPromptSubmit` | Generates trace context (traceId + turnSpanId), stores in state | | `PreToolUse` | Records tool start timestamp | | `PostToolUse` | Stores agent_id mapping for subagent linking | | `Stop` | **Main hook** — reads transcript, creates all spans, flushes via OTLP | | `StopFailure` | Creates Turn span with error status on API failures | | `SubagentStop` | Queues subagent info for Stop to process | | `PreCompact` / `PostCompact` | Traces context compaction events | | `SessionEnd` | Closes interrupted turns on exit | **Cross-process span IDs** are handled by a `ControlledIdGenerator` that implements the OTel SDK's `IdGenerator` interface. Hooks pre-generate trace/span IDs and store them in a shared state file (`~/.claude/state/otel_tracing_state.json`) with file locking for concurrency safety. The plugin uses the real OTel SDK stack: - `@opentelemetry/sdk-trace-base` — `BasicTracerProvider`, `BatchSpanProcessor` - `@opentelemetry/exporter-trace-otlp-http` — `OTLPTraceExporter` - `@opentelemetry/semantic-conventions` — Gen AI and service attribute constants ## Known limitations - Subagents are only traced upon completion. If you interrupt during a subagent run, that subagent's traces will be lost. - Sessions older than 24 hours are pruned from state. Resuming after that starts fresh. ## Development ```bash pnpm install pnpm dev # Watch mode — recompiles on changes pnpm test # Run tests pnpm build # Production build (tsc + esbuild bundle) ``` After making changes, run `pnpm build` and send a new message in Claude Code to pick up the updated hooks. ## License MIT