84 lines
6.4 KiB
JavaScript
84 lines
6.4 KiB
JavaScript
const DAY = 86400000;
|
|
import { affectsService, combinedStatus, maintenancePhase } from './maintenance.mjs';
|
|
import { incidentImpact, serviceImpact } from '../shared/impact.mjs';
|
|
export function summarize(checks) {
|
|
const known = checks.filter(check => check.reason !== 'unconfigured');
|
|
const successful = known.filter(check => check.ok);
|
|
return {
|
|
checks: known.length,
|
|
uptime: known.length ? Number((successful.length / known.length * 100).toFixed(2)) : null,
|
|
latencyMs: successful.length ? Math.round(successful.reduce((sum, check) => sum + check.latencyMs, 0) / successful.length) : null,
|
|
};
|
|
}
|
|
|
|
export function overallStatus(services) {
|
|
const states = services.map(service => service.status);
|
|
if (states.every(status => status === 'unknown')) return 'unknown';
|
|
if (states.every(status => status === 'down')) return 'down';
|
|
if (states.some(status => status === 'down' || status === 'degraded')) return 'degraded';
|
|
if (states.some(status => status === 'unknown')) return 'unknown';
|
|
if (states.some(status => status === 'maintenance')) return 'maintenance';
|
|
return 'operational';
|
|
}
|
|
|
|
export function makeSnapshot(store, config, range = 30, now = Date.now()) {
|
|
// UTC day boundaries are stable across the monitor and visitor timezones.
|
|
const today = Math.floor(now / DAY) * DAY;
|
|
const since = today - (range - 1) * DAY;
|
|
const maintenance = store.maintenances().map(item => ({ ...item, phase: maintenancePhase(item, now) }));
|
|
const services = config.services.map(service => {
|
|
const state = store.state(service);
|
|
const statistics = store.statistics(service, since, now);
|
|
const latest = statistics.recent[0];
|
|
const days = new Map(statistics.days.map(day => [day.start, day]));
|
|
const hours = new Map(statistics.hours.map(hour => [hour.bucket, hour]));
|
|
const summaryOf = summary => ({ checks: summary?.checks || 0, uptime: summary?.checks ? Number((summary.successes / summary.checks * 100).toFixed(2)) : null, latencyMs: summary?.latencyMs == null ? null : Math.round(summary.latencyMs) });
|
|
const summary = summaryOf(statistics.summary);
|
|
const stale = !state || now - state.checked_at > config.staleAfterSeconds * 1000;
|
|
const scenarioState = store.scenario(service);
|
|
const scenarioStale = !scenarioState || now - scenarioState.checked_at > config.scenarioIntervalSeconds * 3000;
|
|
const scenario = service.scenario ? {
|
|
type: service.scenario.type, status: scenarioStale ? 'unknown' : scenarioState.status,
|
|
lastCheckedAt: scenarioState?.checked_at || null, staleAfterSeconds: config.scenarioIntervalSeconds * 3,
|
|
intervalSeconds: config.scenarioIntervalSeconds, stale: Boolean(scenarioState && scenarioStale),
|
|
reason: scenarioStale ? 'not_run' : scenarioState.reason, mode: scenarioState?.mode || 'http',
|
|
pending: Boolean(scenarioState && (scenarioState.failures > 0 && scenarioState.failures < config.failureThreshold || scenarioState.status === 'down' && scenarioState.successes > 0)),
|
|
steps: scenarioState?.steps || [],
|
|
} : null;
|
|
const activeMaintenance = maintenance.filter(item => affectsService(item, service.id, now)).map(item => item.id);
|
|
const availabilityStatus = stale ? 'unknown' : state.status;
|
|
const history = Array.from({ length: range }, (_, index) => {
|
|
const start = since + index * DAY;
|
|
const samples = days.get(start);
|
|
const summary = summaryOf(samples);
|
|
const status = samples ? samples.severity === 2 ? 'down' : samples.severity === 1 ? 'degraded' : 'operational' : 'unknown';
|
|
// A sparse or interrupted day must not appear to have complete coverage.
|
|
const observableDuration = Math.max(0, Math.min(now, start + DAY) - start);
|
|
const expected = Math.max(1, Math.floor(observableDuration / (config.intervalSeconds * 1000)));
|
|
const coverage = Math.min(100, Number((summary.checks / expected * 100).toFixed(1)));
|
|
return { date: new Date(start).toISOString().slice(0, 10), status, ...summary, coverage };
|
|
});
|
|
const responseHistory = Array.from({ length: 24 }, (_, index) => {
|
|
const start = now - (24 - index) * 3600000;
|
|
return { time: start, ...summaryOf(hours.get(index)) };
|
|
});
|
|
return {
|
|
id: service.id, icon: service.icon, name: service.name, description: service.description,
|
|
publicUrl: service.publicUrl, kind: service.kind, functional: service.functional, impactPolicy: service.impactPolicy,
|
|
configured: service.probes.length > 0, status: combinedStatus(availabilityStatus, scenario, activeMaintenance.length > 0), availabilityStatus, scenario, activeMaintenance,
|
|
stale: Boolean(state && stale), pending: Boolean(state && (state.failures > 0 && state.failures < config.failureThreshold || state.status === 'down' && state.successes > 0)),
|
|
lastCheckedAt: state?.checked_at || null, latencyMs: latest?.latencyMs ?? null,
|
|
httpCode: latest?.httpCode ?? null, reason: latest?.reason ?? null,
|
|
uptime: summary.uptime, checks: summary.checks,
|
|
avgLatencyMs: summary.latencyMs, history, responseHistory,
|
|
reliability: store.reliability(service, since, now, config), latency: store.latencyDetails(service, since, now, config.intervalSeconds), latency24h: store.latencyDetails(service, now - DAY, now, config.intervalSeconds),
|
|
latencyHistory: history.map(day => ({ time: new Date(day.date).getTime(), checks: day.checks, uptime: day.uptime, latencyMs: day.latencyMs })),
|
|
recentChecks: statistics.recent.map(({ time, ok, latencyMs, httpCode, reason }) => ({ time, ok: Boolean(ok), latencyMs, httpCode, reason })),
|
|
};
|
|
});
|
|
services.forEach(service => { service.impact = serviceImpact(service, services); });
|
|
const times = services.map(service => service.lastCheckedAt).filter(Boolean);
|
|
const incidents = store.incidents().filter(incident => incident.source === 'manual' || config.services.some(service => service.id === incident.serviceId && (incident.scope === 'scenario' ? service.scenario?.signature : service.signature) === incident.signature)).map(({ signature: _signature, ...incident }) => ({ ...incident, impact: incidentImpact(incident, services) }));
|
|
return { generatedAt: now, lastCheckAt: times.length ? Math.min(...times) : null, intervalSeconds: config.intervalSeconds, scenarioIntervalSeconds: config.scenarioIntervalSeconds, staleAfterSeconds: config.staleAfterSeconds, failureThreshold: config.failureThreshold, recoveryThreshold: config.recoveryThreshold, slowMs: config.slowMs, range, demo: false, overall: overallStatus(services), services, incidents, maintenance };
|
|
}
|