Claude Code Already Emits OpenTelemetry: Who Spends What, on Which Model, in Grafana A developer demonstrated a local Docker Compose pipeline that turns Claude Code's built-in OpenTelemetry exporter into per-model, per-user spend dashboards in Grafana. Claude Code emits cost, token, session and code-edit metrics plus API request events once CLAUDE_CODE_ENABLE_TELEMETRY=1 is set, with prompt text and tool details excluded by default. The setup routes OTLP data through an OpenTelemetry Collector, scraped by Prometheus and visualized in Grafana. Your team's Claude Code spend lands as one number on an invoice. Then someone asks the obvious follow-ups. Which team drove last month's jump? Was it the expensive model or the cheap one? Are subagents quietly burning half the budget? Nobody can answer, because the data that would answer it was never switched on. Claude Code https://code.claude.com/docs/en/overview ships an OpenTelemetry https://opentelemetry.io/ exporter. Export is off until you set one environment variable, CLAUDE CODE ENABLE TELEMETRY=1 , per the monitoring docs https://code.claude.com/docs/en/monitoring-usage ; a few more say where the data goes. Once it is on, every API request bumps a cost counter and a token counter, each tagged with the model, the user, the session and the part of Claude Code that made the call. This walkthrough runs the whole pipeline on your laptop: Claude Code into an OpenTelemetry Collector https://opentelemetry.io/docs/collector/ , scraped by Prometheus https://prometheus.io/docs/introduction/overview/ , drawn by Grafana https://grafana.com/docs/grafana/latest/ , all in one Docker Compose https://docs.docker.com/compose/ file. Then you build one panel per question from the opening. Two signals matter for a spend dashboard. Metrics go out through the metrics exporter; events go out through the logs exporter. Per the docs https://code.claude.com/docs/en/monitoring-usage , the metrics are: claude code.cost.usage USD , incremented after each API request, with model , query source main , subagent or auxiliary , effort , and attribution for the active skill, plugin or subagent. claude code.token.usage tokens , split by type : input , output , cacheRead , cacheCreation . claude code.session.count , claude code.lines of code.count , claude code.commit.count , claude code.pull request.count , claude code.active time.total . claude code.code edit tool.decision , which counts accept and reject decisions on Edit, Write and NotebookEdit, with the file's language . The events include user prompt , api request with cost usd , duration ms and token counts per call , api error , tool result and tool decision . Every metric and event carries session.id and user.id , a random installation identifier. When you sign in with a Claude account, you also get organization.id , user.account uuid and user.email ; the docs say the email goes only to the endpoint you configure, never to Anthropic. The defaults lean private. Prompt text is not collected, only its length, unless you set OTEL LOG USER PROMPTS=1 . Bash commands and MCP tool arguments stay out unless you set OTEL LOG TOOL DETAILS=1 . The docs also state that raw file contents and code snippets are not part of metrics or events. Leave all of those flags off for a cost dashboard. You don't need them. Make a directory and put four files in it. The Collector receives OTLP from Claude Code and re-exposes metrics in Prometheus format on port 8889. Prometheus scrapes that. Grafana reads Prometheus. Claude Code pushes OTLP to the Collector; Prometheus scrapes it; Grafana draws it. docker-compose.yml , with image tags pinned to the current releases of the Collector https://github.com/open-telemetry/opentelemetry-collector-releases/releases , Prometheus https://github.com/prometheus/prometheus/releases and Grafana https://github.com/grafana/grafana/releases : services: otel-collector: image: otel/opentelemetry-collector-contrib:0.162.0 command: "--config=/etc/otelcol/config.yaml" volumes: - ./otel-collector.yaml:/etc/otelcol/config.yaml:ro ports: - "4317:4317" OTLP gRPC - "4318:4318" OTLP HTTP - "8889:8889" Prometheus scrape endpoint prometheus: image: prom/prometheus:v3.15.0 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: - "9090:9090" grafana: image: grafana/grafana:13.2.4 environment: GF SECURITY ADMIN PASSWORD: change-me volumes: - ./grafana:/etc/grafana/provisioning/datasources:ro ports: - "3000:3000" otel-collector.yaml . Metrics go to the Prometheus exporter https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/prometheusexporter/README.md . Events go to the debug exporter so you can watch them in the container logs without running a log store yet: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: {} exporters: prometheus: endpoint: 0.0.0.0:8889 debug: verbosity: detailed service: pipelines: metrics: receivers: otlp processors: batch exporters: prometheus logs: receivers: otlp processors: batch exporters: debug prometheus.yml : global: scrape interval: 15s scrape configs: - job name: otel-collector static configs: - targets: "otel-collector:8889" grafana/prometheus.yaml , a provisioned data source https://grafana.com/docs/grafana/latest/administration/provisioning/ so you skip the click-through: apiVersion: 1 datasources: - name: Prometheus type: prometheus access: proxy url: http://prometheus:9090 isDefault: true Then docker compose up -d . Grafana is on port 3000 user admin , the password from the Compose file . In the shell where you start Claude Code: 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 export OTEL EXPORTER OTLP METRICS TEMPORALITY PREFERENCE=cumulative export OTEL METRIC EXPORT INTERVAL=10000 export OTEL RESOURCE ATTRIBUTES="team.id=platform" claude Three of those lines deserve a word. The temporality line. Claude Code's default is delta , and the docs say to switch to cumulative if your backend expects it. Prometheus counters are cumulative, and the Collector's Prometheus exporter README doesn't document what it does with delta sums, so asking for cumulative at the source takes the question off the table. The export interval. The default is 60,000 ms. The docs suggest a shorter interval while you debug; 10 seconds means your first numbers show up fast. Put it back to the default once things work. The resource attribute. OTEL RESOURCE ATTRIBUTES keys land on every metric data point as labels by default, so team.id=platform gives you a team dimension for free. Values can't contain spaces. Run a prompt or two, then check what the Collector exposes: curl -s localhost:8889/metrics | grep '^claude code' Read the metric names off that output rather than guessing. The exporter swaps dots for underscores and appends the unit and total to counters, so the cost counter shows up as claude code cost usage USD total and tokens as claude code token usage tokens total . Those are the exact names the ROI measurement guide https://github.com/anthropics/claude-code-monitoring-guide in Anthropic's GitHub organization queries. Labels follow the same rule: user.email becomes user email , team.id becomes team id . If nothing shows up, the docs recommend claude --debug-file