64 lines
3.0 KiB
JavaScript
64 lines
3.0 KiB
JavaScript
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,
|
|
};
|
|
}
|