Journal How-to
Claude Code OpenTelemetry setup: a step-by-step guide
Turn on Claude Code's OpenTelemetry metrics the safe way: metrics only, logs and traces off, repository attribution, managed settings for the whole team, and a local collector to verify it works.
9 min read
Claude Code can report what it costs, how many tokens it uses and which repository the work happened in — through standard OpenTelemetry. It’s off by default, and the first time you switch it on it’s easy to send either too little (no repository, no session) or far too much (prompt text in your log pipeline).
This guide walks through a setup that is metrics only, attributes usage to repositories, rolls out to a whole team at once, and can be verified on your own machine before you point it at anything real.
What you’ll end up with
- Usage metrics (cost, tokens, sessions, lines changed, commits, pull requests) exported over OTLP every minute
- A session ID and repository on every data point
- Logs and traces explicitly disabled, so no prompt or tool content leaves the machine
- The same configuration on every developer’s machine, which they can’t redirect
Step 1: check the version
Repository attributes need Claude Code v2.1.269 or newer:
claude --version
Older versions still export metrics, but without the vcs.* attributes described in step 4. You’d then have to recover the repository some other way, such as a session hook. Upgrade first if you can.
Step 2: the environment block
All configuration is environment variables. Put them in the env block of a settings file rather than in shell profiles, so they apply however the agent is started:
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "none",
"OTEL_TRACES_EXPORTER": "none",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.example.com",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer YOUR_TOKEN",
"OTEL_METRICS_INCLUDE_SESSION_ID": "true",
"OTEL_METRICS_INCLUDE_REPOSITORY": "true",
"OTEL_METRICS_INCLUDE_VERSION": "true",
"OTEL_METRIC_EXPORT_INTERVAL": "60000"
}
}
What each line does:
| Variable | Why |
|---|---|
CLAUDE_CODE_ENABLE_TELEMETRY=1 |
Master switch. Nothing is exported without it. |
OTEL_METRICS_EXPORTER=otlp |
Send metrics over OTLP. |
OTEL_LOGS_EXPORTER=none |
Logs are event-level and can carry content. Off. |
OTEL_TRACES_EXPORTER=none |
Not needed for usage reporting. Off. |
OTEL_EXPORTER_OTLP_PROTOCOL=http/json |
Easy to debug; most collectors and vendors accept it. |
OTEL_EXPORTER_OTLP_ENDPOINT |
Base URL. Metrics go to <endpoint>/v1/metrics. |
OTEL_EXPORTER_OTLP_HEADERS |
Auth for your collector, as key=value pairs. |
OTEL_METRICS_INCLUDE_SESSION_ID=true |
Adds session.id, so you can group by session. |
OTEL_METRICS_INCLUDE_REPOSITORY=true |
Adds repository attributes (v2.1.269+). |
OTEL_METRICS_INCLUDE_VERSION=true |
Adds app.version. Handy for spotting machines on old versions. |
OTEL_METRIC_EXPORT_INTERVAL=60000 |
Export every 60 seconds. |
Metrics only, on purpose
Set OTEL_LOGS_EXPORTER and OTEL_TRACES_EXPORTER to none explicitly rather than leaving them out. Being explicit documents the intent, and in a managed rollout it stops a developer’s local setting from switching them on.
Leave the content flags — OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_DETAILS and OTEL_LOG_RAW_API_BODIES — untouched. They’re off by default, and none of them is needed to measure cost. If someone asks to turn them on “just for debugging”, that’s a conversation for your security team, not a config change.
Step 3: where the settings go
There are three places the env block can live, and only two of them work for telemetry.
- User settings —
~/.claude/settings.jsonon each machine. Good for trying it out or for a single developer. - Managed settings — a file deployed by IT or MDM, or server-managed settings pushed from your organization’s admin console on Team and Enterprise plans. The right choice for a team: one change, everyone covered.
- Project settings —
.claude/settings.jsoninside a repository. This doesn’t work for the exporter. Repository-level settings can’t configure OpenTelemetry exporters — which also means a cloned repository can’t redirect your telemetry. Don’t commit telemetry config there and wonder why nothing arrives.
Managed settings have one more property that matters: when the endpoint and headers come from managed settings, Claude Code ignores any endpoint a developer sets locally. Data can’t quietly be sent somewhere else.
Cloud sessions are the exception to “it’s on everyone’s machine”: they don’t read your local ~/.claude/settings.json. Cover them with server-managed settings in the admin console.
Step 4: repository attribution
With OTEL_METRICS_INCLUDE_REPOSITORY=true on v2.1.269+, each data point carries:
vcs.repository.url.full— the remote URLvcs.repository.name— e.g.checkout-apivcs.owner.name— e.g.acme
These are resolved locally from the repository the session is working in, and are set per data point, so a session that moves between repositories is attributed correctly.
The same repository can show up under several URLs (git@github.com:acme/checkout-api.git, https://github.com/acme/checkout-api, a URL with credentials embedded). Normalise to host/owner/name before using it as a key. Our cost per project guide covers that, plus how to add branch and ticket with a hook.
If you want a team label on everything without maintaining a mapping, add a static resource attribute per group in managed settings:
"OTEL_RESOURCE_ATTRIBUTES": "team.id=mobile"
The value is static. Shell expressions like $(git config user.team) aren’t expanded.
Step 5: verify with a local collector
Before pointing a whole team at a backend, see the data with your own eyes. Save this as otel-collector.yaml — an OpenTelemetry Collector config with a debug exporter that prints everything it receives:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
metrics:
receivers: [otlp]
exporters: [debug]
docker run --rm -p 4318:4318 \
-v "$PWD/otel-collector.yaml:/etc/otelcol-contrib/config.yaml" \
otel/opentelemetry-collector-contrib:latest
Point your user settings at it temporarily, with a short interval so you don’t wait a minute per export:
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318",
"OTEL_METRIC_EXPORT_INTERVAL": "10000"
Then start a new session inside a git repository and ask for something small. Settings are read at startup, so a session that was already running won’t pick up the change. Within a few seconds the collector should print metrics such as:
Metric #0
-> Name: claude_code.cost.usage
-> Unit: USD
-> AggregationTemporality: Delta
NumberDataPoints #0
Data point attributes:
-> session.id: Str(6f1c2a9e-…)
-> model: Str(…)
-> vcs.repository.name: Str(checkout-api)
-> vcs.owner.name: Str(acme)
-> vcs.repository.url.full: Str(git@github.com:acme/checkout-api.git)
Your checklist:
claude_code.cost.usageandclaude_code.token.usagearrive- Temporality is
Delta, so you can sum data points directly session.idis presentvcs.*attributes are present (if not, check the version)- Nothing arrives on the logs or traces pipelines
Once that’s clean, restore the real endpoint and interval, and move the block into managed settings.
Step 6: what you can measure now
| Metric | Useful for |
|---|---|
claude_code.cost.usage |
Spend by repository, team, developer, model |
claude_code.token.usage |
Why something is expensive (split by type: input, output, cacheRead, cacheCreation) |
claude_code.session.count |
Adoption |
claude_code.lines_of_code.count |
Volume of accepted changes |
claude_code.commit.count, claude_code.pull_request.count |
Output signals, with caveats — see cost per pull request |
claude_code.active_time.total |
Time spent with the agent |
Remember that cost.usage is an estimate from token counts and list prices. On seat-based plans, read it as the API-equivalent value of the usage, not cash paid.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Nothing arrives | CLAUDE_CODE_ENABLE_TELEMETRY missing, settings in a project file, or the session started before the change |
Metrics arrive without vcs.* |
Claude Code older than v2.1.269, OTEL_METRICS_INCLUDE_REPOSITORY not set, or the session isn’t in a git repository |
| 404 from your endpoint | The endpoint should be the base URL; the exporter adds /v1/metrics itself |
| 401 / 403 | Header format: Authorization=Bearer token (the = is between name and value) |
| Exporter errors about logs | Logs exporter not set to none, so it’s trying to send somewhere |
Or send it to us
If you’d rather not run a collector and build the dashboards, SpendByTeam is an OTLP endpoint that turns these metrics into spend by team, project and client. It accepts metrics only, drops logs and traces unread, and the installer writes exactly the configuration above. See pricing — it’s free for up to 3 developers.