97 lines
4.2 KiB
TypeScript
97 lines
4.2 KiB
TypeScript
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;
|
|
}
|