Sensors: one standard read interface — catalog, single value, history, shaped query
Every number the collector samples becomes a sensor with a stable dotted ID (cpu.core.3.usage, fan.nct6775.fan2, fs.root.used_pct). Clients discover them from the catalog and ask for exactly what they want, including as a shaped query: send the object you want, get it back with the values filled in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
01aeb226ea
commit
43bf787696
+84
@@ -0,0 +1,84 @@
|
||||
# Sensors — the standard read interface
|
||||
|
||||
Everything Ham Lab measures is a **sensor**: a stable dotted ID, a little metadata, a
|
||||
current value. Any consumer — Ham Lab's own dashboard, Glance, Home Assistant, a shell
|
||||
script — discovers them from the catalog and asks for exactly the ones it wants.
|
||||
|
||||
Code: `internal/sensors` (types, registry, selectors, shaped query) and
|
||||
`internal/metrics/sensors.go` (the collector as a provider). The registry measures
|
||||
nothing; providers do. A new source of sensors implements `sensors.Provider` and is
|
||||
added to the registry in `api.New`.
|
||||
|
||||
## IDs
|
||||
|
||||
```
|
||||
cpu.usage cpu.core.<n>.usage cpu.temp load.1|5|15 uptime
|
||||
mem.total|used|avail|free|cached|buffers|used_pct swap.total|used|avail|used_pct
|
||||
temp.<chip>.<label> fan.<chip>.<label>
|
||||
net.<iface>.rx|tx|rx_total|tx_total|up
|
||||
disk.<dev>.read|write|util|size|temp
|
||||
fs.<mount>.size|used|used_pct
|
||||
ups.battery|runtime|load|load_pct|in_v|out_v|on_battery|state|source
|
||||
```
|
||||
|
||||
- Segments are lower case `[a-z0-9_-]`: `Package id 0` → `package_id_0`, `/mnt/tank` →
|
||||
`mnt_tank`, `/` → `root`. The real names are in the sensor's `labels`.
|
||||
- Built from names the hardware reports, never enumeration order (`hwmon3` moves between
|
||||
boots). Two identical chips collide; the second gets `_2`.
|
||||
- Which sensors exist differs per machine. That is the point of the catalog.
|
||||
|
||||
## A sensor
|
||||
|
||||
```json
|
||||
{ "id": "temp.coretemp.package_id_0", "name": "coretemp Package id 0",
|
||||
"kind": "temperature", "unit": "°C", "value": 51, "t": 1789826600878,
|
||||
"warn": 105, "crit": 105, "labels": { "chip": "coretemp", "label": "Package id 0" } }
|
||||
```
|
||||
|
||||
`value` is a number, bool or string. `t` is when it was really read, in unix ms — slow
|
||||
sections (filesystems 30 s, UPS 10 s) are older than the tick. `min`/`max` are the scale
|
||||
(0–100 for percentages, 0–total for used bytes); `warn`/`crit` are the hardware's own
|
||||
thresholds where it states them.
|
||||
|
||||
## Endpoints
|
||||
|
||||
All behind the same auth as the rest of `/api` (session cookie or `Authorization: Bearer`).
|
||||
On a hub, every one is also at `/api/nodes/<machine>/sensors…` for each managed machine.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `GET /api/sensors` | Catalog, with current values. `?ids=fan.*,cpu.usage` narrows it. |
|
||||
| `GET /api/sensors/{id}` | One sensor. `?format=text` → the bare value as text/plain. |
|
||||
| `GET /api/sensors/{id}/history` | `{id, unit, t[], v[]}` — what the ring holds (300 samples at the sampling interval). Numbers only. |
|
||||
| `POST /api/sensors/query` | Shaped query, below. |
|
||||
| `GET /api/sensors/query?k=selector&…` | The same, flat, for consumers that can only fetch a URL. |
|
||||
|
||||
A selector is an exact ID or a glob. `*` crosses dots: `fan.*` is every fan,
|
||||
`cpu.core.*.usage` every core.
|
||||
|
||||
## Shaped query
|
||||
|
||||
Send the object you want, with selectors where the values go. Get it back filled in.
|
||||
|
||||
```
|
||||
POST /api/sensors/query
|
||||
{ "cpu": "cpu.usage", "fans": "fan.*", "box": { "temp": "cpu.temp", "up": "uptime" } }
|
||||
|
||||
→ { "cpu": 12.4,
|
||||
"fans": { "fan.nct6775.fan1": 820, "fan.nct6775.fan2": 1140 },
|
||||
"box": { "temp": 41, "up": 80520 } }
|
||||
```
|
||||
|
||||
- Exact ID → its value; `null` if this machine has no such sensor (so one query works
|
||||
across machines that differ).
|
||||
- Glob → an object keyed by ID; `{}` if nothing matches.
|
||||
- Objects and arrays are walked; anything else in a query is a 400.
|
||||
- `?full=1` → every value is the whole sensor object instead of the bare value. (In the
|
||||
GET form `full` is therefore not usable as a key.)
|
||||
|
||||
## Not yet
|
||||
|
||||
Read-only scoped tokens, Prometheus `/metrics`, a push stream, widgets built on this,
|
||||
an embeddable widget page, and non-metric sensors (services, SMART, ZFS, updates). Until
|
||||
scoped tokens exist, **do not hand a Ham Lab token to a dashboard** — today's tokens are
|
||||
full control of the machine.
|
||||
Reference in New Issue
Block a user