Sensors: per-service and container sensors, pattern-limited tokens, fleet query, steady sampler

- service.<unit>.active|state for services that are enabled, up or failed
- containers.* and container.<name>.running|state|health|mem|cpu (cgroup reads, no docker stats)
- sensors:read tokens can be limited to ID patterns, enforced on every endpoint and,
  through the hub's filter header, on managed machines; the header can only narrow
- /api/fleet/sensors/query: one shaped query, every machine, one request
- the sampler waits what's left of the interval, so 250 ms means 4 a second
- Monitors asks for the families it draws instead of *
- unpowered hwmon sentinels (-93 °C) are not temperatures

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 08:08:44 -07:00
co-authored by Claude Fable 5.1
parent 4ddf10be51
commit 4c2eb57ea6
11 changed files with 383 additions and 51 deletions
+10 -1
View File
@@ -178,7 +178,13 @@ func NewCollector() *Collector {
func (c *Collector) Run(ctx context.Context) {
c.sample() // prime deltas
for {
t := time.NewTimer(c.Interval())
// The wait is what's left of the interval after the work, so a machine whose
// sample takes 30 ms still delivers 4 a second at 250 ms, not 3.6.
wait := c.Interval() - time.Since(c.prevT)
if wait < 0 {
wait = 0
}
t := time.NewTimer(wait)
select {
case <-ctx.Done():
t.Stop()
@@ -420,6 +426,9 @@ func hwmonDir(d string) (chip string, temps []Temp, fans []Fan) {
label = base
}
if m[1] == "temp" {
if raw < -60000 { // an unpowered sensor's sentinel (an idle audio amp reads -93 °C), not weather
continue
}
temps = append(temps, Temp{Chip: chip, Label: label, C: float64(raw) / 1000,
Max: tempLimit(filepath.Join(d, base+"_max")), Crit: tempLimit(filepath.Join(d, base+"_crit"))})
} else if raw > 0 || fileExists(filepath.Join(d, base+"_enable")) {