initial commit
This commit is contained in:
63
server/reliability.mjs
Normal file
63
server/reliability.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
export function unionIntervals(intervals) {
|
||||
const sorted = intervals.filter(([start, end]) => end > start).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||||
const result = [];
|
||||
for (const [start, end] of sorted) {
|
||||
const last = result.at(-1);
|
||||
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
|
||||
else result.push([start, end]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function intersectIntervals(left, right) {
|
||||
const a = unionIntervals(left), b = unionIntervals(right), result = [];
|
||||
let i = 0, j = 0;
|
||||
while (i < a.length && j < b.length) {
|
||||
const start = Math.max(a[i][0], b[j][0]), end = Math.min(a[i][1], b[j][1]);
|
||||
if (end > start) result.push([start, end]);
|
||||
if (a[i][1] <= b[j][1]) i++; else j++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function subtractIntervals(intervals, excluded) {
|
||||
const result = [], exclusions = unionIntervals(excluded);
|
||||
for (const [start, end] of unionIntervals(intervals)) {
|
||||
let cursor = start;
|
||||
for (const [a, b] of exclusions) {
|
||||
if (b <= cursor) continue;
|
||||
if (a >= end) break;
|
||||
if (a > cursor) result.push([cursor, Math.min(a, end)]);
|
||||
cursor = Math.max(cursor, b);
|
||||
if (cursor >= end) break;
|
||||
}
|
||||
if (cursor < end) result.push([cursor, end]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export const intervalDuration = intervals => unionIntervals(intervals).reduce((sum, [start, end]) => sum + end - start, 0);
|
||||
|
||||
export function reliabilitySummary({ availability, scenario, incidents, maintenance, since, now, lastRecovery }) {
|
||||
const excluded = unionIntervals(maintenance);
|
||||
const observed = subtractIntervals(scenario ? intersectIntervals(availability.known, scenario.known) : availability.known, excluded);
|
||||
const outages = [];
|
||||
let incomplete = false;
|
||||
for (const incident of incidents) {
|
||||
const expected = subtractIntervals([[Math.max(since, incident.startedAt), Math.min(now, incident.resolvedAt ?? now)]], excluded);
|
||||
if (!expected.length) continue;
|
||||
const data = incident.scope === 'scenario' ? scenario : availability;
|
||||
const measured = intersectIntervals(expected, data?.down || []);
|
||||
if (intervalDuration(intersectIntervals(expected, data?.known || [])) < intervalDuration(expected)) incomplete = true;
|
||||
outages.push({ intervals: measured, duration: intervalDuration(measured) });
|
||||
}
|
||||
const measured = unionIntervals(outages.flatMap(item => item.intervals));
|
||||
const duration = intervalDuration(measured);
|
||||
const hasData = observed.length > 0 || duration > 0;
|
||||
const usable = hasData && !(incomplete && duration === 0);
|
||||
return {
|
||||
downtimeMs: usable ? duration : null,
|
||||
longestDowntimeMs: usable ? outages.reduce((longest, item) => Math.max(longest, item.duration), 0) : null,
|
||||
incidentCount: outages.length, downtimeIncomplete: incomplete,
|
||||
lastRecoveryAt: lastRecovery?.time ?? null, lastRecoveryScope: lastRecovery?.scope ?? null,
|
||||
observedMs: intervalDuration(observed), coverage: now > since ? Number((intervalDuration(observed) / (now - since) * 100).toFixed(4)) : 0,
|
||||
since, asOf: now,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user