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:
parent
b0d92ccbd8
commit
8a735ddcb3
4 changed files with 303 additions and 9 deletions
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue