export hyperhive.agent.needs_login and alert per agent
This commit is contained in:
parent
5af927bfaa
commit
9043be4a61
6 changed files with 117 additions and 12 deletions
|
|
@ -354,6 +354,7 @@ filters those series out and the host dashboard's top-10 panels group by
|
|||
| `hyperhive.container.cpu.percent` | `%` | gauge | host-normalised percent (custom — no `rate()` needed) |
|
||||
| `hyperhive.agent.unrestricted_internet` | `1` | gauge | the agent's `unrestricted_internet` flag in the swarm permissions this hive last read: `1` on, `0` off; a flag never set reads `1`. Omitted while the stored permissions file is unreadable |
|
||||
| `hyperhive.agent.closure_pullable` | `1` | gauge | `1` when this hive can fetch the agent's latest closure from the swarm's binary cache and verify its signature, else `0` with a `reason` label (`no-record`, `not-cached`, `no-credentials`, `fetch-failed`, `bad-signature`; see [the builder docs](../swarm/builder.md#what-hives-do-with-the-cache)). Omitted before the first probe pass, every 5 minutes |
|
||||
| `hyperhive.agent.needs_login` | `1` | gauge | `1` when a running agent needs a claude login (the dashboard's needs-login flag), `0` when it is logged in. Omitted for a stopped agent and before the first container scan |
|
||||
|
||||
The `hyperhive.`-prefixed metrics have no semconv equivalent (memory
|
||||
limit + peak, on-disk footprint, and an instantaneous cpu percent kept
|
||||
|
|
@ -552,15 +553,16 @@ failing sweep such as `swarm_status_publish`, `pending_logins`), and the message
|
|||
|
||||
The swarm Grafana (`nix/host-modules/swarm-grafana.nix`) alerts on these:
|
||||
|
||||
| Alert | Fires when |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `hh-hive-degraded` | `hyperhive.hive.degraded` is `1` for 2 minutes |
|
||||
| `hh-hive-warning` | any `hyperhive.hive.warning` series other than `disk_pressure` exists for 2 minutes |
|
||||
| `hh-host-disk-high` | `hyperhive.hive.nix_store.utilization` stays above 0.85 for 10 minutes |
|
||||
| `hh-host-disk-critical` | `hyperhive.hive.nix_store.utilization` stays above 0.95 for 2 minutes |
|
||||
| Alert | Fires when |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `hh-hive-degraded` | `hyperhive.hive.degraded` is `1` for 2 minutes |
|
||||
| `hh-hive-warning` | any `hyperhive.hive.warning` series other than `disk_pressure` and `pending_logins` exists for 2 minutes |
|
||||
| `hh-agent-needs-login` | `hyperhive.agent.needs_login` is `1` for an agent for 5 minutes, one alert per agent |
|
||||
| `hh-host-disk-high` | `hyperhive.hive.nix_store.utilization` stays above 0.85 for 10 minutes |
|
||||
| `hh-host-disk-critical` | `hyperhive.hive.nix_store.utilization` stays above 0.95 for 2 minutes |
|
||||
|
||||
The disk alerts replace the old dashboard banner for the nix store, and
|
||||
`hh-hive-warning` replaces it for every other warning. Disk pressure has its
|
||||
`hh-hive-warning` replaces it for every other warning. `pending_logins` has its own per-agent alert, `hh-agent-needs-login`, so a login never pages twice. Disk pressure has its
|
||||
own pair so the thresholds sit on the metric rather than on the registry
|
||||
entry.
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -1720,18 +1720,29 @@ in
|
|||
# pressure is left out: it has its own thresholds below, on
|
||||
# the filesystem metric rather than the registry entry.
|
||||
# Sweeps raise their warning only after several misses, so
|
||||
# `for` is short. `pending_logins` pages too, on purpose:
|
||||
# an agent that waits more than 2m for its claude login
|
||||
# needs a human to act, which is what the banner it
|
||||
# replaces was for.
|
||||
# `for` is short. `pending_logins` is left out: it has no
|
||||
# `agent` label, and `hh-agent-needs-login` below pages
|
||||
# once per agent instead.
|
||||
(metricThresholdRule {
|
||||
uid = "hh-hive-warning";
|
||||
title = "hive warning raised";
|
||||
expr = ''max by (hive, kind, level) ({__name__="hyperhive.hive.warning", kind!="disk_pressure"})'';
|
||||
expr = ''max by (hive, kind, level) ({__name__="hyperhive.hive.warning", kind!~"disk_pressure|pending_logins"})'';
|
||||
threshold = 0;
|
||||
for = "2m";
|
||||
summary = "hive {{ $labels.hive }} has a {{ $labels.level }} warning raised: {{ $labels.kind }}; the message is the `hive warning raised` line in the hive-c0re log";
|
||||
})
|
||||
# `hyperhive.agent.needs_login` is `1` for a running agent
|
||||
# without a usable claude login; stopped agents report
|
||||
# nothing. One alert instance per `(hive, agent)`; `for`
|
||||
# lets a login in progress finish first.
|
||||
(metricThresholdRule {
|
||||
uid = "hh-agent-needs-login";
|
||||
title = "agent needs a claude login";
|
||||
expr = ''max by (hive, agent) ({__name__="hyperhive.agent.needs_login"}) == 1'';
|
||||
threshold = 0;
|
||||
for = "5m";
|
||||
summary = "agent {{ $labels.agent }} on hive {{ $labels.hive }} has needed a claude login for 5 minutes; open its page in the dashboard to complete it";
|
||||
})
|
||||
# `hyperhive.hive.nix_store.utilization` is the used fraction of the
|
||||
# filesystem holding the nix store, the one that failed CI
|
||||
# with ENOSPC before the GC guardrails.
|
||||
|
|
|
|||
|
|
@ -940,6 +940,34 @@ let
|
|||
&& lib.hasInfix "/ 18000" expr
|
||||
&& lib.hasInfix "max by (account, window)" expr;
|
||||
}
|
||||
{
|
||||
# The needs-login alert is one instance per (hive, agent), fires after
|
||||
# 5 minutes on the gauge hive-c0re exports, and `hh-hive-warning` leaves
|
||||
# `pending_logins` to it so a login never pages twice. The disk rule is
|
||||
# the presence control: it goes through the same lookup.
|
||||
# 🔬 Invert-proof: change `for`, `max by (hive, agent)` or the metric on
|
||||
# `hh-agent-needs-login`, drop `pending_logins` from `hh-hive-warning`,
|
||||
# or rename the gauge in `otel_metrics.rs`.
|
||||
name = "the needs-login alert is per agent, fires after 5 minutes and the hive warning rule skips pending_logins";
|
||||
ok =
|
||||
let
|
||||
groups =
|
||||
grafanaHere.containers.swarm-grafana.config.services.grafana.provision.alerting.rules.settings.groups;
|
||||
rules = lib.concatMap (g: g.rules) groups;
|
||||
source = builtins.readFile ../../hive-c0re/src/stats/otel_metrics.rs;
|
||||
rule = uid: lib.findFirst (r: r.uid == uid) null rules;
|
||||
login = rule "hh-agent-needs-login";
|
||||
expr = uid: (lib.head (rule uid).data).model.expr;
|
||||
in
|
||||
login != null
|
||||
&& login.for == "5m"
|
||||
&& rule "hh-host-disk-high" != null
|
||||
&& lib.hasInfix ''{__name__="hyperhive.agent.needs_login"}'' (expr "hh-agent-needs-login")
|
||||
&& lib.hasInfix ''"hyperhive.agent.needs_login"'' source
|
||||
&& lib.hasInfix "max by (hive, agent)" (expr "hh-agent-needs-login")
|
||||
&& lib.hasInfix "pending_logins" (expr "hh-hive-warning")
|
||||
&& lib.hasInfix "disk_pressure" (expr "hh-hive-warning");
|
||||
}
|
||||
{
|
||||
# The held-deploys alert fires on any agent held for 30 minutes and
|
||||
# selects the gauge name `held_deploys.rs` spells in Rust. The disk rule
|
||||
|
|
|
|||
Loading…
Reference in a new issue