initial commit
This commit is contained in:
60
server/monitor.mjs
Normal file
60
server/monitor.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
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)); },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user