Watch
0
0
Fork
You've already forked hyperhive
0

export hyperhive.agent.needs_login and alert per agent

This commit is contained in:
atlas 2026-11-09 10:35:16 +01:00
commit 9043be4a61
3 changed files with 117 additions and 12 deletions

View file

@ -496,6 +496,7 @@ impl Coordinator {
}
};
crate::host_stats::refresh_pending_logins(&fresh);
crate::host_stats::record_login_states(&fresh);
let mut last = self.last_containers.lock().await;
let mut changed_or_new = Vec::new();
let mut removed = Vec::new();

View file

@ -113,6 +113,40 @@ pub fn refresh_pending_logins(containers: &[ContainerView]) {
}
}
/// Per-agent login state of the last container snapshot; see
/// [`record_login_states`].
static LOGIN_STATES: std::sync::Mutex<Vec<(String, u64)>> = std::sync::Mutex::new(Vec::new());
/// `(agent, 1 if it needs a login else 0)` for every running agent. A stopped
/// agent has no entry: `needs_login` is already running-gated in
/// `container_view::build_all`, so its `false` would read as "logged in".
#[must_use]
pub fn login_readings(containers: &[ContainerView]) -> Vec<(String, u64)> {
containers
.iter()
.filter(|c| c.running)
.map(|c| (c.name.clone(), u64::from(c.needs_login)))
.collect()
}
/// Hold the login state of the current container snapshot for the
/// `hyperhive.agent.needs_login` gauge. Driven by the container rescan, next
/// to [`refresh_pending_logins`].
pub fn record_login_states(containers: &[ContainerView]) {
*LOGIN_STATES
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = login_readings(containers);
}
/// The held readings of [`record_login_states`]; empty before the first rescan.
#[must_use]
pub fn held_login_states() -> Vec<(String, u64)> {
LOGIN_STATES
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
/// Agent-state warnings derived from the live container snapshot: agents
/// that need a claude login. Takes the container list from its caller
/// because that is where the snapshot lives.
@ -307,6 +341,20 @@ mod tests {
assert!(current().is_none(), "no agent needing a login clears it");
}
#[test]
fn login_readings_are_per_running_agent_by_name() {
let mut stopped = cv("zed", false);
stopped.running = false;
let mut machine = cv("amy", true);
machine.container = "h-amy".to_owned();
let readings = login_readings(&[machine, cv("bob", false), stopped]);
assert_eq!(
readings,
vec![("amy".to_owned(), 1), ("bob".to_owned(), 0)],
"agent names, 1 for needs-login, 0 for logged in, nothing for stopped"
);
}
/// The gauge value is a fraction, so a percent leaking through would put
/// every alert threshold 100x off.
#[test]

View file

@ -259,6 +259,7 @@ fn register_instruments(provider: &SdkMeterProvider, snapshot: Arc<Mutex<Vec<Con
.build();
register_closure_pullable(&meter);
register_needs_login(&meter);
register_hive_instruments(&meter);
}
@ -281,6 +282,20 @@ fn register_closure_pullable(meter: &opentelemetry::metrics::Meter) {
.build();
}
/// 1 when a running agent needs a claude login, else 0, from the container
/// snapshot the dashboard shows. A stopped agent reports nothing.
fn register_needs_login(meter: &opentelemetry::metrics::Meter) {
meter
.u64_observable_gauge("hyperhive.agent.needs_login")
.with_unit("1")
.with_callback(|obs| {
for (agent, value) in crate::host_stats::held_login_states() {
obs.observe(value, &[KeyValue::new("agent", agent)]);
}
})
.build();
}
/// The gauge value for a sampled container: `None` for one that belongs to no
/// agent (the CI runner), else [`unrestricted_value`] for its agent.
fn unrestricted_reading(stored: &permissions::Stored, c: &ContainerResource) -> Option<u64> {