Set CLAUDE_CODE_ENABLE_TELEMETRY=1 plus the standard OTLP variables and Claude Code exports eight counters: sessions, tokens, estimated cost, lines of code, commits, pull requests, code-edit decisions, and active time. Metrics, logs, and traces are three separate exporters. Two defaults matter: session.id and user.account_uuid are attached to metrics unless you turn them off, and the temporality preference is delta, which Prometheus will not accept without conversion. Content logging is off by default.
- One variable enables it:
CLAUDE_CODE_ENABLE_TELEMETRY=1. - Metrics, logs, and traces are three separate exporters, configured independently.
session.idships in metrics by default. SetOTEL_METRICS_INCLUDE_SESSION_ID=false.- Temporality defaults to delta. Prometheus needs cumulative.
- Prompt and response content is off by default and stays off unless you opt in.
Turning it on
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# cardinality: opt out of the two attributes that are on by default
export OTEL_METRICS_INCLUDE_SESSION_ID=false
export OTEL_METRICS_INCLUDE_ACCOUNT_UUID=false
# shorter intervals while you are verifying it works
export OTEL_METRIC_EXPORT_INTERVAL=10000
The variables worth knowing, with the defaults as of August 2026.
| Variable | Default | Purpose |
|---|---|---|
CLAUDE_CODE_ENABLE_TELEMETRY | off | Master switch. Nothing is exported without it |
OTEL_METRICS_EXPORTER | none | otlp, prometheus, console, or none |
OTEL_LOGS_EXPORTER | none | Events. Separate from metrics |
OTEL_TRACES_EXPORTER | none | Spans. Needs the beta flag as well |
OTEL_EXPORTER_OTLP_PROTOCOL | none | grpc, http/protobuf, or http/json |
OTEL_EXPORTER_OTLP_ENDPOINT | none | Your collector. Per-signal overrides exist |
OTEL_EXPORTER_OTLP_HEADERS | none | Auth for a hosted backend |
OTEL_METRIC_EXPORT_INTERVAL | 60000 | Milliseconds |
OTEL_LOGS_EXPORT_INTERVAL | 5000 | Milliseconds |
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE | delta | delta or cumulative |
OTEL_RESOURCE_ATTRIBUTES | none | Your own labels, for example team=payments |
The eight metrics
Every exported metric. All are counters.
| Metric | Unit | Counts |
|---|---|---|
claude_code.session.count | none | CLI sessions started |
claude_code.token.usage | tokens | Tokens, split by the type attribute |
claude_code.cost.usage | USD | Estimated cost of the session |
claude_code.lines_of_code.count | none | Lines added or removed |
claude_code.commit.count | none | Commits created through Claude Code |
claude_code.pull_request.count | none | Pull requests created through Claude Code |
claude_code.code_edit_tool.decision | none | Edit permission decisions, accept or reject |
claude_code.active_time.total | s | Active engagement time |
The attributes that turn a total into an answer
The attribute set on the token and cost counters is far richer than most people realise, and it is where the interesting queries come from. It answers which subsystem spent the money, not just which model.
Attributes on the token and cost counters.
| Attribute | Values | Why you want it |
|---|---|---|
model | claude-sonnet-5 and similar | The single biggest cost lever |
type | input, output, cacheRead, cacheCreation | Gives you the cache hit ratio |
query_source | main, subagent, auxiliary | Separates the agent from its helpers |
effort | low to max | Thinking budget, billed as output |
speed | fast, normal | Fast mode is priced separately |
agent.name | Subagent type | Which subagent is expensive |
skill.name | Active skill | Which skill is expensive |
mcp_server.name | MCP server | Which integration is expensive |
plugin.name | Owning plugin | Third-party names redacted by default |
Global attributes, and which ones are on metrics by default.
| Attribute | On metrics by default | Cardinality |
|---|---|---|
session.id | Yes | Unbounded. This is the hazard |
user.account_uuid | Yes | Bounded by headcount |
organization.id | Yes | One value |
app.version | No | Low, and useful for regressions |
app.entrypoint | No | Low: cli, sdk-cli, claude-vscode |
terminal.type | Yes, on events | Low |
processors:
attributes/strip:
actions:
- key: session.id
action: delete
deltatocumulative: {}
service:
pipelines:
metrics:
receivers: [otlp]
processors: [attributes/strip, deltatocumulative, batch]
exporters: [prometheus]
Events and traces
The logs exporter carries fifteen structured events. They are the layer where per-request detail lives, and they are what you want when a metric spike needs explaining.
claude_code.user_prompt,claude_code.assistant_responseclaude_code.api_request,claude_code.api_error,claude_code.api_refusalclaude_code.api_request_body,claude_code.api_response_bodyclaude_code.tool_result,claude_code.tool_decisionclaude_code.permission_mode_changed,claude_code.authclaude_code.mcp_server_connection,claude_code.internal_errorclaude_code.plugin_installed,claude_code.plugin_loaded
The most useful of these is claude_code.api_request, which carries cost_usd, cost_usd_micros, duration_ms, ttft_ms on the span side, all four token counts, the request id, and the same attribution attributes as the counters. It is a per-request billing record.
Span tracing, in beta
Setting CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 alongside a traces exporter produces spans: claude_code.interaction wrapping a whole turn, claude_code.llm_request per model call with ttft_ms and retry attempt, claude_code.tool per tool call, and claude_code.tool.blocked_on_user measuring how long the agent waited on a permission prompt.
That last span is the one to instrument first if you are trying to work out where agent time actually goes. It separates "the model is slow" from "the human did not answer the prompt for four minutes", and those have completely different fixes.
Queries worth having
# spend per day
sum(increase(claude_code_cost_usage_total[1d]))
# spend by model
sum by (model) (increase(claude_code_cost_usage_total[1d]))
# spend by subsystem: main thread vs subagents vs auxiliary
sum by (query_source) (increase(claude_code_cost_usage_total[1d]))
# cache hit ratio: the health metric for agent efficiency
sum(rate(claude_code_token_usage_total{type="cacheRead"}[1h]))
/
sum(rate(claude_code_token_usage_total{type=~"input|cacheRead|cacheCreation"}[1h]))
# how often people reject the agent edits
sum(rate(claude_code_code_edit_tool_decision_total{decision="reject"}[1d]))
/
sum(rate(claude_code_code_edit_tool_decision_total[1d]))
# adoption: distinct engineers active this week
count(count by (user_account_uuid) (
increase(claude_code_session_count_total[7d]) > 0
))
The rejection rate is the most interesting series in the set and the least used. A rising rejection rate is an early signal that a model change, a prompt convention, or a project memory file has made the agent worse, and it moves before anyone files a complaint.
Rolling it out to a team
Asking every engineer to export eight variables produces partial coverage and silent gaps, and the gaps are invisible: a machine with telemetry off looks exactly like a machine that had a quiet week. Ship the configuration with the tool instead.
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.internal.example.com",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer <token>",
"OTEL_METRICS_INCLUDE_SESSION_ID": "false",
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative",
"OTEL_RESOURCE_ATTRIBUTES": "team=payments,env=prod"
}
}
For a backend that needs short-lived credentials, set otelHeadersHelper in settings to a script that prints a JSON object of headers. Claude Code re-runs it on an interval, so a token with a one-hour life works without anyone rotating anything by hand.
Questions people ask
Set CLAUDE_CODE_ENABLE_TELEMETRY=1 along with OTEL_METRICS_EXPORTER, OTEL_EXPORTER_OTLP_PROTOCOL, and OTEL_EXPORTER_OTLP_ENDPOINT. Metrics, logs, and traces are three separate exporters and each needs its own variable.
Eight counters as of August 2026: session.count, token.usage, cost.usage, lines_of_code.count, commit.count, pull_request.count, code_edit_tool.decision, and active_time.total, with attributes for model, token type, query source, effort, speed, subagent, skill, plugin, and MCP server.
No. It is computed locally from token counts at standard list rates, so it ignores promotional pricing and contracted discounts. On a subscription no money moved at all. Treat it as intensity, and use the Console usage page for billing.
Almost certainly session.id, which is attached to metrics by default and is unbounded. Set OTEL_METRICS_INCLUDE_SESSION_ID=false, and consider OTEL_METRICS_INCLUDE_ACCOUNT_UUID=false if you do not need per-person breakdowns.
Temporality. Claude Code exports delta by default and Prometheus is cumulative. Set OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative, or run the deltatocumulative processor in your collector.
Yes. Both accept OTLP. Set the endpoint and put your key in OTEL_EXPORTER_OTLP_HEADERS, or route through a collector. For rotating credentials, point otelHeadersHelper at a script that prints headers as JSON.
Not by default. Prompt text, assistant responses, tool parameters, and raw API bodies are all redacted unless you opt in with OTEL_LOG_USER_PROMPTS, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_DETAILS, or OTEL_LOG_RAW_API_BODIES. Whatever you enable goes only to the endpoint you configured.
Yes, and it is the main reason to use it there. The Claude Code Analytics API and the first-party dashboards do not cover cloud-provider deployments, so OpenTelemetry export from each machine is the supported route to per-user attribution.
Sources
Every figure above was read from these pages on August 2026. Vendors reprice without notice; if you find a stale number, tell us.