Watch
0
0
Fork
You've already forked hyperhive
0

hive-agent: publish Claude subscription usage (5h/7d %) as metrics

A detached task polls GET https://api.anthropic.com/api/oauth/usage every
5 minutes with the OAuth access token from ~/.claude/.credentials.json and
records, per window the response names (five_hour, seven_day,
seven_day_sonnet, ...):

- hyperhive.agent.claude_usage.percent   (%, 0-100)
- hyperhive.agent.claude_usage.resets_at (s, unix seconds)

both labelled window=<name>. Endpoint, the anthropic-beta:
oauth-2025-04-20 header and the {utilization, resets_at} window shape are
taken from the claude-code 2.1.283 bundle's own /usage fetch.

The token is only read, never refreshed: claude owns refresh-token
rotation and a second refresher can log the agent out. An expired token
is skipped until claude's next turn refreshes it. API-key agents
(HIVE_USE_API_KEY, the ACP default) and agents with no credentials file
skip quietly, and the task does nothing when OTEL is not configured.
Request failures and non-2xx statuses warn with the status or transport
error only, never the body or the token.
This commit is contained in:
atlas 2026-09-30 20:26:09 +02:00 • committed by mara
commit 8a735ddcb3
4 changed files with 303 additions and 9 deletions

View file

@ -22,6 +22,8 @@
//! exporter setup rather than [`record`]'s per-turn call site — `CLAUDE.md`
//! size *is* continuously live (like hive-c0re's cgroup values), so
//! [`crate::claude_md_watch`] records it from its own periodic tick instead.
//! [`record_claude_usage`] is the same kind of exception, fed from
//! [`crate::claude_usage_watch`]'s poll of the subscription usage endpoint.
//!
//! Resource attributes (`service.name`, `agent`, `hive`, `swarm`) are picked
//! up automatically by the SDK from the container-wide `OTEL_RESOURCE_ATTRIBUTES`
@ -56,6 +58,10 @@ struct Instruments {
/// hive-c0re's container gauges), not a once-per-turn one, so it has
/// its own entry point rather than riding `record`'s per-turn call.
claude_md_lines: Gauge<u64>,
/// Recorded from [`crate::claude_usage_watch`]'s own tick, keyed by
/// `window`.
claude_usage_percent: Gauge<f64>,
claude_usage_resets_at: Gauge<u64>,
}
/// Lazily built on the first call to [`record`]. `None` when OTEL isn't
@ -109,6 +115,20 @@ pub fn record_claude_md_lines(lines: u64) {
inst.claude_md_lines.record(lines, &[]);
}
/// Record one subscription usage window (`five_hour`, `seven_day`, …):
/// `percent` is 0-100, `resets_at` unix seconds. No-op when OTEL isn't
/// configured, same as [`record`].
pub fn record_claude_usage(window: &str, percent: f64, resets_at: Option<u64>) {
let Some(inst) = INSTRUMENTS.get_or_init(build).as_ref() else {
return;
};
let attrs = [KeyValue::new("window", window.to_owned())];
inst.claude_usage_percent.record(percent, &attrs);
if let Some(resets_at) = resets_at {
inst.claude_usage_resets_at.record(resets_at, &attrs);
}
}
fn build() -> Option<Instruments> {
if !enabled() {
tracing::debug!("otel turn-metrics: no endpoint configured, exporter disabled");
@ -133,6 +153,14 @@ fn build() -> Option<Instruments> {
.u64_gauge("hyperhive.agent.loose_ends.reminders")
.build(),
claude_md_lines: meter.u64_gauge("hyperhive.agent.claude_md.lines").build(),
claude_usage_percent: meter
.f64_gauge("hyperhive.agent.claude_usage.percent")
.with_unit("%")
.build(),
claude_usage_resets_at: meter
.u64_gauge("hyperhive.agent.claude_usage.resets_at")
.with_unit("s")
.build(),
_provider: provider,
})
}
@ -168,7 +196,7 @@ fn build_provider(interval: Duration) -> anyhow::Result<SdkMeterProvider> {
/// hyperhive-specific one: it's also the variable the SDK itself reads to
/// build the exporter's URL, so "configured" and "where it goes" cannot
/// disagree).
fn enabled() -> bool {
pub fn enabled() -> bool {
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok_and(|s| !s.trim().is_empty())
}