mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-21 01:06:37 +00:00
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const statusHistoryQuerySchema = z
|
|
.object({
|
|
days: z
|
|
.string()
|
|
.optional()
|
|
.transform((v) => (v ? parseInt(v, 10) : 90)),
|
|
})
|
|
.pipe(
|
|
z.object({
|
|
days: z.number().int().min(1).max(365),
|
|
})
|
|
);
|
|
|
|
export interface StatusHistoryDayBucket {
|
|
date: string; // ISO date "YYYY-MM-DD"
|
|
uptimePercent: number; // 0-100
|
|
totalDowntimeSeconds: number;
|
|
downtimeWindows: { start: number; end: number | null; status: string }[];
|
|
status: "good" | "degraded" | "bad" | "no_data";
|
|
}
|
|
|
|
export interface StatusHistoryResponse {
|
|
entityType: string;
|
|
entityId: number;
|
|
days: StatusHistoryDayBucket[];
|
|
overallUptimePercent: number;
|
|
totalDowntimeSeconds: number;
|
|
}
|
|
|
|
export function computeBuckets(
|
|
events: { entityType: string; entityId: number; orgId: string; status: string; timestamp: number; id: number }[],
|
|
days: number
|
|
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
const buckets: StatusHistoryDayBucket[] = [];
|
|
let totalDowntime = 0;
|
|
|
|
for (let d = 0; d < days; d++) {
|
|
const dayStartSec = nowSec - (days - d) * 86400;
|
|
const dayEndSec = dayStartSec + 86400;
|
|
|
|
const dayEvents = events.filter(
|
|
(e) => e.timestamp >= dayStartSec && e.timestamp < dayEndSec
|
|
);
|
|
|
|
// Determine the status at the start of this day (last event before dayStart)
|
|
const lastBeforeDay = [...events]
|
|
.filter((e) => e.timestamp < dayStartSec)
|
|
.at(-1);
|
|
|
|
const currentStatus = lastBeforeDay?.status ?? null;
|
|
|
|
const windows: { start: number; end: number | null; status: string }[] = [];
|
|
let dayDowntime = 0;
|
|
|
|
let windowStart = dayStartSec;
|
|
let windowStatus = currentStatus;
|
|
|
|
for (const evt of dayEvents) {
|
|
if (windowStatus !== null && windowStatus !== evt.status) {
|
|
const windowEnd = evt.timestamp;
|
|
const isDown =
|
|
windowStatus === "offline" ||
|
|
windowStatus === "unhealthy" ||
|
|
windowStatus === "unknown";
|
|
if (isDown) {
|
|
dayDowntime += windowEnd - windowStart;
|
|
windows.push({
|
|
start: windowStart,
|
|
end: windowEnd,
|
|
status: windowStatus,
|
|
});
|
|
}
|
|
}
|
|
windowStart = evt.timestamp;
|
|
windowStatus = evt.status;
|
|
}
|
|
|
|
// Close the final window at the end of the day (or now if day hasn't ended)
|
|
if (windowStatus !== null) {
|
|
const finalEnd = Math.min(dayEndSec, nowSec);
|
|
const isDown =
|
|
windowStatus === "offline" ||
|
|
windowStatus === "unhealthy" ||
|
|
windowStatus === "unknown";
|
|
if (isDown && finalEnd > windowStart) {
|
|
dayDowntime += finalEnd - windowStart;
|
|
windows.push({
|
|
start: windowStart,
|
|
end: finalEnd,
|
|
status: windowStatus,
|
|
});
|
|
}
|
|
}
|
|
|
|
totalDowntime += dayDowntime;
|
|
|
|
const effectiveDayLength = Math.max(
|
|
0,
|
|
Math.min(dayEndSec, nowSec) - dayStartSec
|
|
);
|
|
const uptimePct =
|
|
effectiveDayLength > 0
|
|
? Math.max(
|
|
0,
|
|
((effectiveDayLength - dayDowntime) /
|
|
effectiveDayLength) *
|
|
100
|
|
)
|
|
: 100;
|
|
|
|
const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10);
|
|
|
|
let status: StatusHistoryDayBucket["status"] = "no_data";
|
|
if (currentStatus !== null || dayEvents.length > 0) {
|
|
if (uptimePct >= 99) status = "good";
|
|
else if (uptimePct >= 50) status = "degraded";
|
|
else status = "bad";
|
|
}
|
|
|
|
buckets.push({
|
|
date: dateStr,
|
|
uptimePercent: Math.round(uptimePct * 100) / 100,
|
|
totalDowntimeSeconds: dayDowntime,
|
|
downtimeWindows: windows,
|
|
status,
|
|
});
|
|
}
|
|
|
|
return { buckets, totalDowntime };
|
|
}
|