OpenTelemetry Tracing Plugin for Claude Code#
A Claude Code plugin that traces conversations, tool calls, subagent executions, and context compaction via OpenTelemetry OTLP to any compatible backend — Honeycomb, Jaeger, Grafana, Datadog, or anything else that speaks OTLP.
Prerequisites#
- Node.js v20+
Installation#
From source#
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:
{
"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:
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
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io"
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY"
Jaeger (local)
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
Grafana Cloud
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 attributes:
gen_ai.system:"anthropic"gen_ai.operation.name:"chat"gen_ai.request.model/gen_ai.response.modelgen_ai.usage.input_tokens/gen_ai.usage.output_tokensgen_ai.usage.cache_read_input_tokens/gen_ai.usage.cache_creation_input_tokensgen_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 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.
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#
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