hive-agent: warn on usage responses with no recognisable windows
addresses argus's review on #4842 (c88740): fetch's success path was silent when the response parsed but windows() found nothing to record. should_warn_empty tracks the same once-per-streak shape as the existing token-skip logging, reset the moment a response has windows again, and never logs the body. Also notes in the observability doc that a past resets_at means the paired percent gauge is stale.
This commit is contained in:
parent
8a735ddcb3
commit
5d7042e655
2 changed files with 40 additions and 2 deletions
|
|
@ -378,7 +378,8 @@ with the OAuth access token from the agent's own
|
|||
`~/.claude/.credentials.json`. The harness only reads that token and never
|
||||
refreshes it — claude owns refresh-token rotation. An agent with no OAuth
|
||||
session (API-key backends, not yet logged in) or an expired token skips the
|
||||
poll, so a gauge keeps its last value until the next successful poll.
|
||||
poll, so a gauge keeps its last value until the next successful poll — a
|
||||
`resets_at` in the past means the paired `percent` is stale.
|
||||
|
||||
Resource attributes (`service.name`, `agent`, `hive`, `swarm`) come from the
|
||||
same container-wide `OTEL_RESOURCE_ATTRIBUTES` as everything else in this
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ pub async fn run() {
|
|||
};
|
||||
let creds = crate::paths::claude_dir().join(CREDENTIALS_FILE);
|
||||
let mut skip_logged = false;
|
||||
// Separate streak from `skip_logged`: a token can be usable while the
|
||||
// response still carries no recognisable window (endpoint change,
|
||||
// unexpected account state), and that deserves its own once-per-streak
|
||||
// warning rather than silence.
|
||||
let mut empty_logged = false;
|
||||
let mut tick = tokio::time::interval(POLL_INTERVAL);
|
||||
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
loop {
|
||||
|
|
@ -104,7 +109,13 @@ pub async fn run() {
|
|||
skip_logged = false;
|
||||
match fetch(&client, &token).await {
|
||||
Ok(body) => {
|
||||
for w in windows(&body) {
|
||||
let windows = windows(&body);
|
||||
if should_warn_empty(&mut empty_logged, windows.len()) {
|
||||
tracing::warn!(
|
||||
"claude usage watch: usage response had no recognisable windows"
|
||||
);
|
||||
}
|
||||
for w in windows {
|
||||
crate::otel_turn_metrics::record_claude_usage(&w.name, w.percent, w.resets_at);
|
||||
}
|
||||
}
|
||||
|
|
@ -159,6 +170,21 @@ async fn fetch(client: &reqwest::Client, token: &AccessToken) -> Result<Value, S
|
|||
.map_err(|e| format!("unreadable response: {e}"))
|
||||
}
|
||||
|
||||
/// Streak-deduped decision for the empty-windows warning: updates
|
||||
/// `empty_logged` and returns whether this tick should actually log. Split
|
||||
/// from [`run`] so the once-per-streak logic is testable without a timer.
|
||||
fn should_warn_empty(empty_logged: &mut bool, window_count: usize) -> bool {
|
||||
if window_count > 0 {
|
||||
*empty_logged = false;
|
||||
return false;
|
||||
}
|
||||
if *empty_logged {
|
||||
return false;
|
||||
}
|
||||
*empty_logged = true;
|
||||
true
|
||||
}
|
||||
|
||||
/// Window entries are the top-level objects carrying both `utilization` and
|
||||
/// `resets_at`; other top-level keys (`extra_usage`, `limits`, …) are not
|
||||
/// windows. A window whose `utilization` is `null` is not reported.
|
||||
|
|
@ -242,6 +268,17 @@ mod tests {
|
|||
assert!(matches!(token_from(f, 1000), Token::Expired));
|
||||
}
|
||||
|
||||
/// Empty responses warn once per streak; a non-empty response in
|
||||
/// between resets it, so a second empty streak warns again.
|
||||
#[test]
|
||||
fn empty_windows_warn_once_per_streak() {
|
||||
let mut empty_logged = false;
|
||||
assert!(should_warn_empty(&mut empty_logged, 0));
|
||||
assert!(!should_warn_empty(&mut empty_logged, 0));
|
||||
assert!(!should_warn_empty(&mut empty_logged, 2));
|
||||
assert!(should_warn_empty(&mut empty_logged, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_malformed_credentials_are_absent() {
|
||||
assert!(matches!(token_from(b"{}", 0), Token::Absent));
|
||||
|
|
|
|||
Loading…
Reference in a new issue