Export Claude Code token usage (per agent/model) to Prometheus, OTLP, or stdout -- parsed from transcripts, works on Max/Pro subscriptions.
Project description
claude-usage-exporter
Export Claude Code token usage -- per agent, model, and token type -- to Prometheus, OTLP, or stdout, parsed straight from the transcripts Claude Code already writes. Works on Max/Pro OAuth subscriptions, where API-key telemetry isn't available.
claude_tokens_total{agent="engineer", model="claude-opus-4-8", token_type="output"} 1840
claude_tokens_total{agent="chief-of-staff", model="claude-opus-4-8", token_type="cache_read"} 39000
Why this exists
If you run Claude Code -- especially a multi-agent setup that shells out to
claude -p -- and you want token spend as time-series metrics in your own
Prometheus/Grafana stack, the existing options don't quite get you there:
- ccusage is excellent for CLI summaries, but it's CLI-only -- no metrics push.
- Claude Code's native OpenTelemetry exports metrics, but short
claude -pruns (often sub-second in an orchestrator) can exit before the metric export interval flushes, so those runs drop their telemetry. It also leans on API-key-style telemetry config rather than a Max/Pro subscription.
The transcripts Claude Code writes to ~/.claude/projects/**/*.jsonl carry the real
per-turn message.usage, are written regardless of how short the run was, and encode
the working directory (hence the agent) in the project-dir name. This tool parses
those into cumulative counters. Same message.usage source as native telemetry, no
flush race, and free per-agent attribution.
Scope, honestly: this is a focused utility for a specific niche -- subscription users who want Claude Code token metrics in their own observability stack. It is not a general "agent telemetry platform," and it stays small on purpose.
Install
# Core (stdout + Prometheus sinks, zero dependencies):
pip install claude-usage-exporter
# With the OTLP sink (pulls in the OpenTelemetry SDK):
pip install "claude-usage-exporter[otlp]"
# Or run without installing, via uv:
uvx claude-usage-exporter --dry-run
Requires Python 3.11+.
Quickstart
See what it would emit from your own transcripts, without writing state or pushing anywhere:
claude-usage-exporter --dry-run
Try it against the bundled sample (note the duplicated message id is de-duplicated):
claude-usage-exporter --dry-run \
--projects-dir examples/sample-transcript \
--labeler-config examples/labeler-openclaw.toml
Sinks
Pick one or more with --sink (repeatable). Default is stdout.
stdout -- debugging / piping (no dependencies)
claude-usage-exporter --sink stdout --format text # exposition-style lines
claude-usage-exporter --sink stdout --format json # structured
Prometheus -- textfile collector (no dependencies)
Writes the node_exporter textfile-collector
format atomically. Point a running node_exporter at the same directory
(--collector.textfile.directory) and it scrapes the file on its next cycle -- no
Prometheus server needed by this tool.
claude-usage-exporter --sink prometheus \
--prometheus-textfile /var/lib/node_exporter/textfile/claude.prom
OTLP -- push to an OTLP/HTTP endpoint (needs the [otlp] extra)
Emits an OTLP ObservableCounter with CUMULATIVE temporality, so rate() /
increase() work across runs. Auth is sent verbatim as the Authorization header
(e.g. Basic <token> for Grafana Cloud), from --otlp-auth or $OTLP_AUTH.
export OTLP_AUTH="Basic <base64-token>"
claude-usage-exporter --sink otlp \
--otlp-endpoint https://otlp-gateway.example.net/otlp
Two OTLP gotchas worth knowing:
- Stable instance id.
service.instance.idis pinned to the hostname by default (override with--service-instance-id/$OTLP_SERVICE_INSTANCE_ID). Left to the SDK it would be a random UUID per process, so a cron-style run would create a new series set every invocation. Keep it stable. _totalsuffix normalization. OTLP-to-Prometheus backends (e.g. Grafana Cloud) move a counter's_totalsuffix to the end of the name.claude_tokens_totalpasses through unchanged; a name likemy_total_metricwould arrive asmy_metric_total. Name your metric so_totalis already terminal.
Per-agent labeling (and "multi-framework" support)
By default there's no agent label -- usage is keyed by model + token_type. To
attribute usage to agents, pass a rules file. Rules are first-match-wins regexes over
the transcript's project-dir name:
claude-usage-exporter --labeler-config examples/labeler-openclaw.toml --dry-run
# examples/labeler-openclaw.toml
label = "agent"
default = "unknown"
match_on = "parent"
[[rules]]
pattern = "-agents-engineer$"
value = "engineer"
The key design point: orchestration frameworks (OpenClaw, Hermes, ...) don't have
their own transcript formats -- they all drive claude -p, and Claude Code writes
the transcripts. The only thing that differs per framework is how a launch directory
maps to an agent. So supporting a new framework is a config file, not code.
How it works
- Incremental -- a per-file byte offset is persisted in a state file; each run reads only newly appended bytes. Scans are sub-second.
- Exact -- messages are de-duplicated by
message.id(Claude Code repeats assistant lines within a turn), and only the top-levelusageis read. - Monotonic -- cumulative totals persist in state and are fully re-emitted every run, producing a proper counter. A state reset reads as a normal counter reset. The state snapshot is written before the push, so a failed push self-heals next run.
State lives at ~/.local/state/claude-usage-exporter/state.json by default. Its size
is dominated by a FIFO-capped (500k) set of seen message ids -- a few MB at most.
Querying tip (OTLP push gotcha)
When pushed via OTLP, samples can be sparse -- sparser than Prometheus's default 5-minute staleness window. A plain instant query for the metric can look empty even when everything is healthy. Probe with a long-window function instead:
# Does data exist / how fresh is it?
count(last_over_time(claude_tokens_total[60d]))
# Activity over the last 7 days, per agent:
sum by (agent) (increase(claude_tokens_total[7d]))
Running on a schedule
Generic systemd user units are in examples/systemd/ (a oneshot
service + a 10-minute timer). Put secrets in an EnvironmentFile rather than
source-ing them, so a value containing a space (like Basic <token>) survives intact.
Architecture
Three seams, each independently swappable:
Source Labeler Sink
(Claude Code jsonl reader) --> (path -> {agent, ...}) --------> (stdout / prometheus / otlp)
core.py labelers.py sinks.py
- Source (
core.py) -- the only code that knows the Claude Code transcript format. One implementation; deliberately not abstracted over other agent CLIs (Codex, Cursor, ...) until there's real demand -- that's the documented seam for a future contributor. - Labeler (
labelers.py) -- turns a transcript into labels. Config-driven, so new frameworks need no code. - Sink (
sinks.py) -- turns cumulative totals into a destination write. Adding one (Datadog, StatsD, remote-write) is a ~20-line class implementingemit(metric_name, samples).
Development
pip install -e ".[otlp,dev]"
pytest
The stdout and Prometheus sinks are fully verified offline; the Prometheus output is
additionally linted with promtool check metrics when promtool is on PATH. The OTLP
mapping is unit-tested via the OpenTelemetry SDK without needing a live endpoint.
Metric reference
Single metric: claude_tokens_total (configurable via --metric).
| Label | Values |
|---|---|
token_type |
input, output, cache_read, cache_creation |
model |
the model id from the transcript |
agent |
whatever your labeler assigns (absent if none) |
Related
- ccusage -- excellent CLI for Claude Code usage summaries. Complementary: ccusage is for interactive CLI reporting; this tool is for pushing continuous metrics to Prometheus/OTLP.
License
MIT -- see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_usage_exporter-0.1.2.tar.gz.
File metadata
- Download URL: claude_usage_exporter-0.1.2.tar.gz
- Upload date:
- Size: 18.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ebd2c8ea12fa2c57aaed69dcdea683cfc3d6a48571dc3b2a027d2919e80b0a78
|
|
| MD5 |
c27b749addd005fe064ea9373846c254
|
|
| BLAKE2b-256 |
5aff56d3f53c67ea95e8cc7b0984abc63d5d4ae22d44c9b03bbfcc2c99f46b7b
|
Provenance
The following attestation bundles were made for claude_usage_exporter-0.1.2.tar.gz:
Publisher:
release.yml on niks999/claude-usage-exporter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_usage_exporter-0.1.2.tar.gz -
Subject digest:
ebd2c8ea12fa2c57aaed69dcdea683cfc3d6a48571dc3b2a027d2919e80b0a78 - Sigstore transparency entry: 2095926192
- Sigstore integration time:
-
Permalink:
niks999/claude-usage-exporter@fd3ce70a2cba4eb4f1c6cc3bee8749dcae786cb4 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/niks999
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fd3ce70a2cba4eb4f1c6cc3bee8749dcae786cb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file claude_usage_exporter-0.1.2-py3-none-any.whl.
File metadata
- Download URL: claude_usage_exporter-0.1.2-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab8ef03d29f9280dbb798b3ead759a2b2b94f47f7401aa367e7fcf6f09737cc6
|
|
| MD5 |
9b76c9f6b3569dcaf107deb26ce53c41
|
|
| BLAKE2b-256 |
9941724a089f9abceaf7aaa1a790818ad1843480388b2445879dd937166e06f8
|
Provenance
The following attestation bundles were made for claude_usage_exporter-0.1.2-py3-none-any.whl:
Publisher:
release.yml on niks999/claude-usage-exporter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_usage_exporter-0.1.2-py3-none-any.whl -
Subject digest:
ab8ef03d29f9280dbb798b3ead759a2b2b94f47f7401aa367e7fcf6f09737cc6 - Sigstore transparency entry: 2095927183
- Sigstore integration time:
-
Permalink:
niks999/claude-usage-exporter@fd3ce70a2cba4eb4f1c6cc3bee8749dcae786cb4 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/niks999
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fd3ce70a2cba4eb4f1c6cc3bee8749dcae786cb4 -
Trigger Event:
push
-
Statement type: