Skip to content

ccusage: push each cycle's readings to an external dashboard - #587

Merged
ZacxDev merged 3 commits into
mainfrom
feat/cc-usage-push
Nov 11, 2026
Merged

ZacxDev merged 3 commits into
mainfrom
feat/cc-usage-push

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #577: the per-account Claude usage poller can now push its readings to an external dashboard after each cycle, instead of only being pulled.

Env contract

Variable Default Notes
MUSTER_CC_USAGE_PUSH_URL off Absolute https URL (plain http only to loopback); no userinfo. Refused without MUSTER_CC_USAGE_POLL.
MUSTER_CC_USAGE_PUSH_TOKEN — Required with the URL; sent as Authorization: Bearer. Never logged.
MUSTER_CC_USAGE_PUSH_INSTANCE MUSTER_PUBLIC_URL host, else muster ≤64 chars.

The token or instance set without the URL is refused too (a knob that does nothing).

Behaviour

  • After each poll cycle (Poller.AfterPass, once per pass, inline in the loop): PUT {"source_instance", "read_at", "poll_interval_seconds", "accounts": [...]} (poll_interval_seconds is the API's own field: the configured interval in whole seconds, 0 with the poller off); 10s timeout; expects 204.
  • accounts comes from the same ccUsageAccounts the GET /api/cc-accounts/usage handler uses, for the admin owner, so the entries are byte-for-byte the API's, used_by included.
  • Failure: logged as status/class only, counted in muster_cc_usage_push_failures_total{reason} (build|timeout|transport|http_4xx|http_5xx|http_other|canceled), and not retried until the next cycle. Redirects are not followed. On a 4xx the dashboard's answer (first 512 bytes) is logged as one line: control characters stripped, cut at 200 runes, the token redacted. A push cut short by shutdown counts as canceled.
  • Not forwarded by Compose (the poller isn't). The NOT_FORWARDED ledger, configuration.md, the jobs.md "Pushing the readings" section and the metric ledger are all updated.

Tests

  • Golden: poll_interval_seconds equals the API's value, and reads 420 / 0 for a 7m / off interval. The pushed accounts equals the API's accounts byte-for-byte, with a positive control showing the fixture has attribution and extra windows.
  • Boot refusal matrix: 8 refusal cases plus 4 accepts, and no refusal carries the token.
  • 204 → success. 401/422 → http_4xx, 500/503 → http_5xx, 200 and redirect → http_other, slow → timeout, closed port → transport, store error → build with no request. Each makes exactly one request and counts under exactly its own reason.
  • Logs never carry the token or body, even when the dashboard echoes the token. Positive control: the failures are logged.
  • Run calls AfterPass exactly once per pass, after the pass.
  • Mutation sweep: 35 mutants. Every one was killed, and each kill came from its own guard's assertion.

🤖 Generated with Claude Code

ZacxDev and others added 2 commits November 10, 2026 19:03
An optional pusher for the Claude account usage poller: with
MUSTER_CC_USAGE_PUSH_URL (https; http only to loopback) and
MUSTER_CC_USAGE_PUSH_TOKEN (Bearer), after each poll cycle muster PUTs
{source_instance, read_at, accounts} once, 10s timeout, expecting 204.
accounts is built by the same ccUsageAccounts the GET
/api/cc-accounts/usage handler uses, so the two are byte-for-byte equal.
A failure is logged by class only (never the token, body or answer),
counted in muster_cc_usage_push_failures_total{reason}, and not retried
until the next cycle; redirects are not followed.

Boot is refused when the URL is set with the poller off, without the
token, or not https, and when the token or instance is set without the
URL. MUSTER_CC_USAGE_PUSH_INSTANCE (<=64 chars) defaults to
MUSTER_PUBLIC_URL's host, else "muster".

Tests: golden (pushed accounts == API accounts, used_by included),
refusal matrix, 204 vs 4xx/5xx/200/redirect/timeout/transport/build,
no token or body in logs, one AfterPass per pass. 24 mutants, all killed
by the specific assertion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The push body gains the API's own top-level poll_interval_seconds (the
poller's configured interval in whole seconds), so the dashboard can mark
a source stale at a multiple of it. accounts stays byte-for-byte the
API's. The golden pins it equal to the API's and to 420 / 0 for a 7m /
off interval (three mutants: hardcoded, omitted, minutes; all killed).

Fixes CI's two ledger failures from the first push: the three
MUSTER_CC_USAGE_PUSH_* variables join the Claude Code agents feature's
settings and the banner exemptions (each reason read off the code).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner Author

Round 0 (requirements and deletion): proceed. D1 is kept: the instance label is cheap, and it names the source on the dashboard.

D1 — MUSTER_CC_USAGE_PUSH_INSTANCE: the default (MUSTER_PUBLIC_URL's host) already labels a single muster; the knob could go until a second sender exists

Round 1 (nine-axis checklist) at e58c882f: 0 🔴, 1 🟡, 1 🟢. That ends the ladder. The 🟡 is fixed without a re-audit.

  • 🟡 When the receiver rejects a push for hitting a cap, muster logs only the status code.
  • 🟢 A push interrupted by shutdown is counted as a timeout.
1. a 4xx push failure logs the receiver's answer sentence (truncated, never the request or token)

From the #587 audit: a dashboard refusing the push (a cap exceeded) said
only "HTTP 400" every cycle. On a 4xx the first 512 bytes of its answer
are now logged as one line: control characters made spaces, cut at 200
runes, and the token redacted should the dashboard echo it. The request
body is still never logged.

A push cut short by muster shutting down (the parent context ended) is
now counted as reason "canceled", not "timeout" or "transport".

Tests: the 400 answer appears as one sanitized line (with the token
redacted, cut on a rune boundary), and a shutdown counts as canceled.
Mutants: no redaction, no control-character strip, no cut, a cut by
bytes, the answer not logged, the token logged, canceled reported as
timeout, and no canceled branch. Every one was killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev marked this pull request as ready for review November 11, 2026 04:19
@ZacxDev
ZacxDev merged commit d24100f into main Nov 11, 2026
19 checks passed
@ZacxDev
ZacxDev deleted the feat/cc-usage-push branch November 11, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant