Add host temperature telemetry and history

This commit is contained in:
2026-10-13 23:39:04 -04:00
parent 09396ab584
commit 171f657876
28 changed files with 1466 additions and 32 deletions
+96
View File
@@ -0,0 +1,96 @@
import type Database from 'better-sqlite3';
import { db } from '../db/index.js';
import { historyTemperatures } from '../../host-temperatures.js';
import type { HostInventory } from '../../hosts.js';
import type { CapacityQuery, TemperatureHistorySeries } from '../../capacity.js';
export function migrateTemperatures(handle: Database.Database): void {
handle.exec(`CREATE TABLE IF NOT EXISTS ec_host_temperature_samples (
host_id TEXT NOT NULL REFERENCES ec_hosts(id), bucket TEXT NOT NULL,
sensor_id TEXT NOT NULL, kind TEXT NOT NULL, label TEXT NOT NULL,
celsius REAL NOT NULL CHECK(celsius BETWEEN -100 AND 200),
PRIMARY KEY(host_id,bucket,sensor_id)
);
CREATE INDEX IF NOT EXISTS ec_host_temperatures_retention ON ec_host_temperature_samples(bucket);`);
}
export function recordTemperatures(
hostId: string,
inventory: HostInventory,
timestamp: string
): void {
const sensors = historyTemperatures(inventory.temperatures);
if (!sensors.length) return;
const bucket = new Date(Math.floor(Date.parse(timestamp) / 60_000) * 60_000).toISOString();
const insert = db().prepare(`INSERT INTO ec_host_temperature_samples
(host_id,bucket,sensor_id,kind,label,celsius) VALUES(?,?,?,?,?,?)
ON CONFLICT(host_id,bucket,sensor_id) DO UPDATE SET
celsius=MAX(celsius,excluded.celsius),kind=excluded.kind,label=excluded.label`);
for (const sensor of sensors)
insert.run(hostId, bucket, sensor.id, sensor.kind, sensor.label, sensor.celsius);
}
/** Runs on the capacity report worker, returning at most 168 points per series. */
export function readTemperatureHistory(
query: CapacityQuery,
hosts: { id: string; name: string; created_at: string }[],
startAt: number,
endAt: number,
bucketMs: number
): TemperatureHistorySeries[] {
const count = Math.round((endAt - startAt) / bucketMs);
const statement = db().prepare(`SELECT sensor_id,kind,label,
MIN(?,MAX(0,CAST((CAST(strftime('%s',bucket) AS INTEGER)*1000-?) / ? AS INTEGER))) AS period,
MAX(celsius) AS celsius
FROM ec_host_temperature_samples WHERE host_id=? AND bucket>=? AND bucket<=?
${query.hostId ? '' : "AND sensor_id='cpu'"}
GROUP BY sensor_id,period ORDER BY sensor_id,period`);
const result: TemperatureHistorySeries[] = [];
for (const host of hosts) {
const bySensor = new Map<string, TemperatureHistorySeries>();
const add = (id: string, kind: TemperatureHistorySeries['kind'], label: string) => {
const series = {
id: `${host.id}:${id}`,
hostId: host.id,
kind,
label,
points: Array.from({ length: count }, (_, i) => ({
at: startAt + i * bucketMs,
celsius: null as number | null
}))
};
bySensor.set(id, series);
result.push(series);
return series;
};
add('cpu', 'cpu', query.hostId ? 'CPU temperature' : host.name);
const rows = statement.all(
count - 1,
startAt,
bucketMs,
host.id,
new Date(
Math.max(
Math.floor(Date.parse(host.created_at) / 60_000) * 60_000,
Math.floor(startAt / 60_000) * 60_000
)
).toISOString(),
new Date(endAt).toISOString()
) as { sensor_id: string; kind: string; label: string; period: number; celsius: number }[];
for (const row of rows) {
let series = bySensor.get(row.sensor_id);
if (!series) {
if (!['nvme', 'chipset'].includes(row.kind) || bySensor.size >= 17) continue;
const kind = row.kind as 'nvme' | 'chipset';
const number = [...bySensor.values()].filter((s) => s.kind === kind).length + 1;
series = add(
row.sensor_id,
kind,
`${kind === 'nvme' ? 'NVMe drive' : 'Chipset'} ${number}`
);
}
if (series.points[row.period]) series.points[row.period].celsius = row.celsius;
}
}
return result;
}