Files
ridebus-status/server/monitor.mjs
2026-10-13 02:24:39 +03:00

61 lines
3.1 KiB
JavaScript

import { checkService, nextState } from './probe.mjs';
export function createMonitor(store, config, { check = checkService, runScenario = null, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
let active = null;
let stopped = true;
let timer;
let lastPruned = 0;
let scenarioActive = null, scenarioTimer;
function previousState(service, scope, current) {
// Expected failures during maintenance do not count toward a new outage.
if (current && store.inMaintenance(service, current.checked_at) && !store.inMaintenance(service, now()) && !store.hasOpenIncident(service, scope)) return null;
return current;
}
async function cycle() {
if (active) return active;
active = (async () => {
await Promise.all(config.services.map(async service => {
const result = await check(service, config);
store.record(service, result, nextState(previousState(service, 'availability', store.state(service)), result, config), now(), { notify: config.telegram?.enabled });
}));
if (now() - lastPruned > 86400000) { store.prune(now() - config.retentionDays * 86400000); lastPruned = now(); }
})();
try { await active; } finally { active = null; }
}
async function scenarioCycle() {
if (!runScenario) return;
if (scenarioActive) return scenarioActive;
scenarioActive = (async () => {
await Promise.all(config.services.filter(service => service.scenario).map(async service => {
const previous = store.scenario(service);
if (previous && now() - previous.checked_at < config.scenarioIntervalSeconds * 1000) return;
const result = await runScenario(service);
store.recordScenario(service, result, nextState(previousState(service, 'scenario', previous), result, { ...config, slowMs: config.scenarioSlowMs }), now(), { notify: config.telegram?.enabled });
}));
})();
try { await scenarioActive; } finally { scenarioActive = null; }
}
async function loop() {
try { await cycle(); } catch { console.error('Monitoring cycle failed; previous results will expire.'); }
if (!stopped) timer = setTimer(loop, config.intervalSeconds * 1000);
}
async function scenarioLoop() {
const interval = config.scenarioIntervalSeconds * 1000;
let delay = interval;
try {
await scenarioCycle();
// Resume each saved deadline after a restart, even when a check is not due yet.
for (const service of config.services.filter(service => service.scenario)) {
const saved = store.scenario(service);
if (saved) delay = Math.min(delay, Math.max(100, saved.checked_at + interval - now()));
}
} catch { console.error('User journey checks failed; previous results will expire.'); }
if (!stopped) scenarioTimer = setTimer(scenarioLoop, delay);
}
return {
cycle, scenarioCycle,
start() { if (!stopped) return; stopped = false; void loop(); if (runScenario) void scenarioLoop(); },
async stop() { stopped = true; clearTimer(timer); clearTimer(scenarioTimer); await Promise.all([active, scenarioActive].filter(Boolean)); },
};
}