Claude Code OpenTelemetry: Metrics, Events and Cost

Set CLAUDE_CODE_ENABLE_TELEMETRY to 1, then choose exporters and an endpoint — typically OTEL_METRICS_EXPORTER and OTEL_LOGS_EXPORTER set to otlp, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS. There is no agent to install. Administrators put the same block in the env section of managed settings to lock the destination.
The cost and token usage metrics, because of what they are attributed by. Cost carries model, query source, speed, effort, agent name, skill name, plugin name, marketplace name, MCP server name and MCP tool name — so you can attribute spend to a specific connector or skill rather than just to a user or a month.
Not by default. Content logging is opt-in through separate variables for user prompts, assistant responses, tool details, tool content and raw API bodies. Enabling prompt and response logging means your collector holds the conversation, so its retention policy becomes part of your AI data-retention posture.
Session id and account UUID are included in metrics by default, while version and entrypoint are not. Session id is unbounded, so every session creates a new time series. Set OTEL_METRICS_INCLUDE_SESSION_ID to false if per-user totals are enough; keep it and size the collector accordingly if you need per-session attribution.
Yes. The API request event carries input, output, cache-read and cache-creation tokens as separate attributes, so you can watch the read-to-creation ratio per user and per session. Cache creation staying high turn after turn means something is invalidating the prefix, which shows up in a session rather than a billing cycle.

Key Takeaway
Claude Code exports OpenTelemetry metrics and events once you set CLAUDE_CODE_ENABLE_TELEMETRY to 1 along with an exporter and endpoint. Eight metrics cover sessions, code, cost, tokens, tool decisions and active time, and the cost metric is attributed by model, skill, plugin, agent and MCP tool — not just by user.
The question that made me set this up was not what are we spending. It was which of these things is spending it. A monthly figure tells you a team is using Claude Code; it does not tell you that one MCP server accounts for a third of the tokens, and it certainly does not tell you which skill was running when it happened.
The telemetry answers that, and the attribute list on the cost metric is why. This post covers the minimal setup, the eight metrics and which two actually matter, the event stream and its correlation identifiers, the content-logging options that are a policy decision rather than a configuration one, and the cardinality controls that decide whether your collector survives a large team.
There is no agent to install and nothing to run alongside Claude Code. It speaks OTLP directly, so the entire configuration is environment variables pointed at a collector you already have. Administrators put the same block in the environment section of managed settings, which overrides developer settings and removes conflicting per-signal variables so the destination is locked.
# The whole setup. Put it in the env block of managed settings
# to lock the destination for a fleet, or export it locally.
CLAUDE_CODE_ENABLE_TELEMETRY=1 # required, nothing exports without it
OTEL_METRICS_EXPORTER=otlp # otlp | prometheus | console | none
OTEL_LOGS_EXPORTER=otlp # otlp | console | none
OTEL_EXPORTER_OTLP_PROTOCOL=grpc # grpc | http/json | http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.example.com:4317
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-token"
# Export intervals, if the defaults do not suit your collector:
OTEL_METRIC_EXPORT_INTERVAL=60000 # ms, default 60s
OTEL_LOGS_EXPORT_INTERVAL=5000 # ms, default 5s
# Verify: look for claude_code.session.count in your backend.
# Logs only? Submit a prompt and look for the user_prompt event.
# Nothing arriving? claude --debug shows OTel export errors.Every metric carries a standard attribute set including session id, application version and entrypoint, organisation id, several user identifiers including email, and terminal type. That means user attribution is free and automatic — and also that you are shipping developer emails to your collector by default. Decide whether that is acceptable under your own policies before you roll it out, not after someone notices.
The full set covers activity well, but most of it is context rather than signal. Session counts and lines-of-code totals make good dashboard furniture and poor alerts. The two worth building on are cost and token usage, because of what they are attributed by.
# Eight metrics. The two most people actually need are the
# last two on this list, not the first.
claude_code.session.count start_type
claude_code.lines_of_code.count type, model
claude_code.pull_request.count
claude_code.commit.count
claude_code.code_edit_tool.decision tool_name, decision, source, language
claude_code.active_time.total seconds, by type
claude_code.cost.usage USD. Attributes go far beyond model:
query_source, speed, effort,
agent.name, skill.name, plugin.name,
marketplace.name, mcp_server.name,
mcp_tool.name
claude_code.token.usage tokens. type, model, query_source,
speed, effortThat attribute list is the whole reason to do this properly. Cost broken down by MCP server and MCP tool tells you whether a connector is worth its keep. Broken down by skill and plugin, it tells you which of your team's customisations are expensive. Broken down by speed and effort, it tells you whether someone left a session at a higher effort level for a week. None of that is visible in a billing total.
Events carry what metrics cannot: individual requests, tool results, errors and refusals. Three correlation attributes make the stream joinable, and knowing them turns a pile of events into a trace you can actually follow:
Five separate variables control whether the actual content of a session leaves the machine, and they are deliberately separate because they carry different risk. Logging tool details is a reasonable audit control. Logging user prompts and assistant responses means your collector now holds the conversation, and your collector's retention policy is now your AI data-retention policy.
# Content logging is opt-in, per kind, and each of these is a
# policy decision rather than a configuration one.
OTEL_LOG_USER_PROMPTS=1 # the actual prompt text
OTEL_LOG_ASSISTANT_RESPONSES=1 # the actual model responses
OTEL_LOG_TOOL_DETAILS=1 # tool parameters, commands, skill names
OTEL_LOG_TOOL_CONTENT=1 # tool input/output — requires tracing
OTEL_LOG_RAW_API_BODIES=1 # or file:<dir> to write bodies to disk
CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH=61440 # default, UTF-16 code units
# Cardinality controls, with their DEFAULTS shown. Session id
# is on by default, which is the one to think about at scale.
OTEL_METRICS_INCLUDE_SESSION_ID=true
OTEL_METRICS_INCLUDE_ACCOUNT_UUID=true
OTEL_METRICS_INCLUDE_VERSION=false
OTEL_METRICS_INCLUDE_ENTRYPOINT=falseRaw API bodies can be written inline or, with the file form, to a directory on disk. That is genuinely useful for debugging a gateway and genuinely dangerous as a standing configuration, because it puts full request bodies on developer machines with no lifecycle attached. Turn it on to diagnose something specific, and turn it off in the same change that closes the ticket.
Dashboards are easy and alerts are the hard part, because most of these metrics are activity rather than health. These four have a defensible threshold behind them.
Four alerts worth having:
| Signal | What it catches | Why it beats the obvious alternative |
|---|---|---|
| Cache-creation tokens staying high | Something invalidating the prefix every turn | Visible in a session; a cost alert takes a billing cycle |
| Cost by MCP server | A connector nobody uses that everybody pays for | A total tells you the number, not the cause |
| API error and refusal events | Provider trouble, or a repo tripping a classifier | Users report these as the tool being slow |
| Tool decision events with a denied outcome | Permission rules that are fighting the work | Nobody files a ticket about a prompt they clicked through |
Session id and account identifier are included in metrics by default; version and entrypoint are not. That default is right for a small team and wrong for a large one — session id is unbounded, so every session creates a new time series, and a few hundred developers running several sessions a day will make that felt.
Four toggles control it, and the honest approach is to decide what question you are answering. If you need per-session attribution, keep it and size the collector for it. If you only need per-user totals, turn session id off and keep the account identifier; you lose the ability to ask about one session and you stop paying for a series per session forever.
There is a distributed-tracing beta behind a separate variable that produces a real span hierarchy — an interaction span with model requests, hooks and tool spans beneath it, including a span for the time a tool spent blocked on the user. That last one is the most interesting number in the whole system, because it measures how much of a session is waiting for a human rather than waiting for a model.
Three checks, in this order, because each rules out a different layer:
Set this up before you need it, because the questions it answers are all retrospective. The configuration is six variables and an afternoon; the value shows up the first time someone asks why last month cost what it did, and the answer is a breakdown by MCP tool rather than a shrug. Just make the content-logging decision deliberately, and write down what your collector's retention policy now means — because once prompts are flowing into it, that policy is part of your AI data handling whether anyone wrote it down or not.
Sources & further reading