Pipeline: Claude Code OTel metrics → Prometheus OTLP receiver → Grafana dashboard, with per-tmux-pane identification via a claude() shell wrapper that injects service.instance.id at launch time. Captures the two non-obvious requirements (cumulative temporality, OTEL_RESOURCE_ATTRIBUTES only in bashrc) in docs/gotchas.md so future setups don't waste an hour rediscovering them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| client | ||
| docs | ||
| grafana | ||
| prometheus | ||
| .gitignore | ||
| install-client.sh | ||
| README.md | ||
claude-grafana-tracker
Push Claude Code OpenTelemetry metrics from every developer tmux pane into a
shared Prometheus + Grafana, with each Claude run identified by its tmux
session/window.pane so you can tell which terminal is burning tokens.
What this does
tmux pane (e.g. "apme/0.0")
│
▼ bash sources ~/.bashrc → claude() wrapper sets service.instance.id
$ claude
│
▼ OTLP/HTTP, cumulative temporality
Prometheus :9090/api/v1/otlp/v1/metrics
│
▼
Grafana :3000 → dashboard "Claude Code"
Metrics that show up (one series per tmux pane):
| Metric | What it tells you |
|---|---|
claude_code_session_count_total |
sessions started (split by start_type fresh/resumed) |
claude_code_token_usage_tokens_total |
tokens used, split by type (input/output/cacheRead/cacheCreation), model, effort |
claude_code_cost_usage_USD_total |
accumulated $ |
claude_code_active_time_seconds_total |
wall time, split by type (cli/user) |
The instance label on every series is <tmux_session>/<window>.<pane>, e.g.
app-modern/1.0. target_info adds tmux_session, tmux_window, tmux_pane
as separate labels for richer joins.
Quick start
1. Prometheus server (one-time)
Prometheus must run with --web.enable-otlp-receiver. If you're using the
docker-compose stack in prometheus/compose-snippet.yaml, that flag is already
in the command: list. Otherwise:
# add to the prometheus container's command list
--web.enable-otlp-receiver
# then
docker compose up -d prometheus
Optionally tune otlp.translation_strategy in prometheus.yml — see
docs/gotchas.md.
2. Client (every developer box)
Pick one of:
A. Run the installer:
./install-client.sh http://YOUR.PROM.HOST:9090
B. Manual:
- Append
client/bashrc.shto your~/.bashrc(edit the endpoint) - Merge
client/settings.json'senvblock into~/.claude/settings.json - Open a new shell (or
source ~/.bashrc) and runclaude
The wrapper auto-injects service.instance.id=<tmux_session>/<window>.<pane>
when launched inside tmux. Non-tmux launches still report (just without the
instance label).
3. Grafana dashboard
curl -u admin:PASSWORD -H "Content-Type: application/json" \
-X POST http://YOUR.GRAFANA.HOST:3000/api/dashboards/db \
-d @grafana/dashboard.json
Update the datasource.uid in grafana/dashboard.json to match your Prom
datasource UID first (find it via /api/datasources).
Repo layout
prometheus/compose-snippet.yaml compose stanza that enables OTLP receiver
client/bashrc.sh env exports + tmux-aware claude() wrapper
client/settings.json env block to merge into ~/.claude/settings.json
grafana/dashboard.json 7-panel dashboard, pivots on tmux pane
install-client.sh one-shot installer for the client side
docs/architecture.md how the pipeline is wired and why
docs/gotchas.md the two non-obvious traps (cumulative + env override)
docs/dashboard.md panel-by-panel guide and useful PromQL
Why each piece exists
- Prom OTLP receiver: avoids running a separate OTel collector. Single hop, no buffering layer.
- bashrc wrapper instead of static env: tmux pane identity is only knowable at launch time, so the env must be computed per-
claude-invocation. OTEL_RESOURCE_ATTRIBUTESNOT in settings.json: Claude's settings.jsonenvblock clobbers shell env for any key it lists, which would defeat the wrapper. Keep dynamic attrs in bashrc, static ones in settings.json. See docs/gotchas.md.- Cumulative temporality: Prom's OTLP receiver rejects delta with 500. The OTel SDK defaults to delta for counters, so the explicit
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulativeis mandatory.
What this does NOT do
- No logs (Prom OTLP path is metrics-only). For logs, run Loki and set
OTEL_LOGS_EXPORTER=otlpwith a different endpoint. - No traces (same reason).
- No per-tool-call breakdown (Claude emits aggregate counters, not span events to OTLP).
- No alerting rules — add your own in Prometheus or Grafana once you know what's normal.
Existing-session caveat
Env vars only apply to new Claude processes. Claude sessions started before
the wrapper was installed will not have an instance label and will appear as
(none) in the dashboard until they're /restart-ed.