Thermal & IO Checkers Design
Date: 2026-08-07 Status: Approved (design) Stage: diagnose (apps.checkers) Build order: disk_temp first (recurring real pain on a bare-metal node), then cpu_temp and io_strain.
Problem
One bare-metal node repeatedly runs its disks hot, and we have no visibility into hardware temperature or IO saturation. We want checkers for disk temperature, CPU temperature, and IO strain.
Scope decision
Three separate checkers, not one combined checker: they measure different things, in different units, with different thresholds and different data sources. This matches the one-concern-one-checker pattern (cpu, memory, disk are already separate) and the BaseChecker single warning/critical threshold model.
disk_temp— hottest disk temperature (°C)cpu_temp— hottest CPU package/core temperature (°C)io_strain— busiest disk utilization (% busy time)
OS gating (not a choice — forced by data source)
All three read Linux-only data, so all are Linux-gated with skip-as-OK on non-Linux, following the raid / disk_linux precedent:
- Temperatures:
psutil.sensors_temperatures()reads/sys/class/hwmon(Linux only; the function is absent on macOS/Windows). No sudo. - IO:
psutil.disk_io_counters(perdisk=True)busy_timeis a Linux field.
Shared architecture
- Pattern: numeric-threshold checkers (like
cpu/memory/disk) — read a value, map to OK/WARNING/CRITICAL viaBaseChecker._determine_status()with per-checker thresholds. Thresholds stay overridable via--warning-threshold/--critical-thresholdand the DB config path. - Worst-wins: with multiple disks/sensors, the worst reading drives the status; every reading is included in
metricsso the offending disk/sensor is identifiable. - Sensor-absent = skip-as-OK. On cloud VMs / many VPS hosts,
sensors_temperatures()returns{}and disks expose nodrivetemp. When the relevant sensor is simply not present, return OK with a “no … sensors available” message — not UNKNOWN (which would spam every VM). The bare-metal node with the heat problem has sensors and gets real readings. - Layout: flat files
cpu_temp.py,disk_temp.py,io_strain.py, plus a small shared_sensors.pyhelper for the two temperature checkers (both parsepsutil.sensors_temperatures()). Parsing is kept pure/separate from the check side effects for unit-testing.
Per-checker specifics
| Checker | Source | Status driver | Default thresholds | Key metrics |
|---|---|---|---|---|
disk_temp | sensors_temperatures() chips drivetemp, nvme | hottest disk | warn 55 °C, crit 60 °C | per-disk °C + label, hottest disk name, disk count |
cpu_temp | sensors_temperatures() chips coretemp, k10temp, zenpower, cpu_thermal | max package/core temp | warn 80 °C, crit 90 °C | per-sensor °C, max, chip label |
io_strain | disk_io_counters(perdisk=True), sample busy_time over an interval | busiest disk %util | warn 80 %, crit 95 % | per-disk %util, r/w throughput & IOPS over the sample |
Thresholds are standard-ops defaults (disk 55/60 °C is common HDD/SSD guidance; CPU 80/90 °C is conservative vs typical ~90–100 °C Tj limits). Kept as-is.
disk_temp (first build)
- Read
psutil.sensors_temperatures(); select entries whose chip key is in the disk allowlist (drivetemp,nvme). Each entry has a label (device) andcurrent°C; alsohigh/criticalif the sensor exposes them (recorded in metrics, not used to override thresholds). - Status:
_determine_status(hottest_current). - Message e.g.
Hottest disk 58.0°C (sda) [drivetemp]. - No-sudo caveat: relies on the
drivetempkernel module (Linux 5.6+) /nvmesensors being loaded so temps appear in hwmon without root. SMART (smartctl, needs root) is out. Ifdrivetempisn’t loaded → skip-OK (“no disk temperature sensors”). Documentmodprobe drivetemp(and persisting it) as the operational prerequisite.
io_strain
- Take two
disk_io_counters(perdisk=True)reads separated by a sample interval (default ~1 s, overridable likecpu’ssample_interval); per disk%util = busy_time_delta / (elapsed_ms) × 100, clamped to [0, 100]. - Status from the busiest disk’s %util.
cpu_temp
- Read
sensors_temperatures(); select CPU chips (allowlist above); status from the maxcurrent.
Error handling & edge cases
- Non-Linux → skip-OK.
sensors_temperatures()missing / returns{}/ no matching chips → skip-OK.- Sensor
currentofNoneor0.0filtered out; if nothing valid remains → skip-OK. disk_io_counters()returnsNone(rare/containers) → skip-OK.BaseChecker.run()already wraps unexpected exceptions into UNKNOWN.
Tests
Mock psutil.sensors_temperatures() / disk_io_counters() fixtures for each checker: healthy, warning, critical, multi-disk/-sensor worst-wins, sensor-absent skip, non-Linux skip, None/zero filtering, (io) two-sample %util math. 100% branch coverage; pure parsing in _sensors.py unit-tested directly.
Wiring
Register disk_temp (then cpu_temp, io_strain) in CHECKER_REGISTRY (auto-flows into check_health, run_check, pipeline). Docs rows in apps/checkers/README.md and the checker lists in AGENTS.md / apps/checkers/AGENTS.md.
Acceptance criteria
- Each checker registered and runnable via
run_check <name>. - Correct worst-wins OK/WARNING/CRITICAL mapping against fixtures.
- Non-Linux and sensor-absent paths skip cleanly as OK.
- 100% branch coverage on changed code;
black/ruff/bandit/pytestclean.
Build phases
disk_temp+ shared_sensors.pyhelper (this plan).cpu_temp(reuses_sensors.py).io_strain.