Files
Void-Homelab/lib/health/checker.js
2026-06-02 22:55:03 +10:00

42 lines
1.7 KiB
JavaScript

import net from 'node:net';
const SLOW_MS = 3000;
export function classify({ ok, reachable, latency, error }) {
if (ok) return { status: latency > SLOW_MS ? 'warn' : 'ok', latency_ms: latency, detail: `${latency}ms` };
if (reachable) return { status: 'warn', latency_ms: latency ?? null, detail: 'degraded' };
return { status: 'down', latency_ms: null, detail: error || 'unreachable' };
}
// Default probe: HTTP (status 2xx/3xx) or TCP connect. Only called with
// operator-configured URLs from the registry — never user input.
export async function probe(svc) {
const started = Date.now();
const type = svc.check?.type || 'http';
try {
if (type === 'tcp') {
const u = new URL(svc.url);
await new Promise((resolve, reject) => {
const sock = net.connect({ host: u.hostname, port: Number(u.port) }, () => { sock.end(); resolve(); });
sock.setTimeout(5000); sock.on('timeout', () => { sock.destroy(); reject(new Error('timeout')); });
sock.on('error', reject);
});
return { ok: true, latency: Date.now() - started };
}
const base = svc.url.replace(/\/$/, '');
const url = base + (svc.check?.path || '');
const res = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(6000) });
const reachable = true;
const ok = res.status >= 200 && res.status < 400;
return { ok, reachable, latency: Date.now() - started };
} catch (e) {
return { ok: false, reachable: false, latency: Date.now() - started, error: e.code || e.message };
}
}
export async function checkAll(services, probeFn = probe) {
return Promise.all(services.map(async svc => {
const c = classify(await probeFn(svc));
return { service_id: svc.id, ...c };
}));
}