No description
Find a file
anaeem 56f7afeb08 Initial: docs, client install, prom + grafana config
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>
2026-05-23 16:51:36 +01:00
client Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00
docs Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00
grafana Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00
prometheus Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00
.gitignore Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00
install-client.sh Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00
README.md Initial: docs, client install, prom + grafana config 2026-05-23 16:51:36 +01:00

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.sh to your ~/.bashrc (edit the endpoint)
  • Merge client/settings.json's env block into ~/.claude/settings.json
  • Open a new shell (or source ~/.bashrc) and run claude

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_ATTRIBUTES NOT in settings.json: Claude's settings.json env block 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=cumulative is mandatory.

What this does NOT do

  • No logs (Prom OTLP path is metrics-only). For logs, run Loki and set OTEL_LOGS_EXPORTER=otlp with 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.