feat(alerts): the host's temperature and throughput can drive alert rules (#5787)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m23s
CI / unit (push) Successful in 1m30s
CI / publish (push) Successful in 1m23s

temp_c_max was recorded on every ingest, but host_agent didn't offer it
to rules, so "host running hot" couldn't be watched. The catalog guard
didn't see the row(...) helper ingest builds its rows with. Now offered:
temp_c_max, temp_c per sensor, and network and disk throughput per host,
interface and disk. A new guard holds every literal row(...) name to the
catalog, except NOT_ALERTABLE, where each exclusion is given a reason.
temp_c_max reads in °C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 22:20:11 -04:00
co-authored by Claude Opus 5.5
parent 9a11f5788f
commit 26a3ec3339
5 changed files with 57 additions and 3 deletions
+8 -2
View File
@@ -279,7 +279,7 @@ def metric_value(name: str, value: Any) -> str:
if n.endswith("_secs") or n.startswith("seconds_") or (
n.endswith("_seconds") and not n.endswith("_timestamp_seconds")):
return human_duration(value)
if n.endswith("_c"):
if n.endswith("_c") or n.startswith("temp_c"): # temp_c_max too
return f"{value:.0f}°C"
if n.endswith("_days"):
return f"{value:.0f} days"
@@ -346,6 +346,12 @@ METRICS: dict[str, tuple[str, str]] = {
"updates_security": ("Security updates waiting", "of the updates waiting, those from a security source"),
"reboot_required": ("Reboot required", "1 when the host needs a reboot to finish an update, 0 when not"),
"maintenance_failed": ("Maintenance failed", "1 when the host's latest scheduled or manual update failed, timed out or didn't come back into service"),
"temp_c_max": ("Hottest sensor", "the host's hottest real thermometer; inputs with nothing wired to them are left out"),
"temp_c": ("Sensor temperature", "one temperature sensor (resource host:temp:label)"),
"net_rx_bps": ("Network in", "bytes per second received, by the whole host or one interface"),
"net_tx_bps": ("Network out", "bytes per second sent, by the whole host or one interface"),
"disk_read_bps": ("Disk reads", "bytes per second read, from all disks or one"),
"disk_write_bps": ("Disk writes", "bytes per second written, to all disks or one"),
}
# Where a rule's metric comes from, as the picker and the rules list say it.
@@ -418,7 +424,7 @@ def metric_unit(name: str) -> str:
return "bytes/s"
if n.endswith("_secs") or n.startswith("seconds_"):
return "seconds"
if n.endswith("_c"):
if n.endswith("_c") or n.startswith("temp_c"): # temp_c_max too
return "°C"
if n.endswith("_days"):
return "days"