Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
6.9 kB ยท 229 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230import type { Incident, Maintenance, StatusReport, StatusReportUpdate,} from "@openstatus/db/src/schema";
import { isInBlacklist } from "./blacklist";import { classNames, statusDetails } from "./config";import type { StatusDetails, StatusVariant } from "./types";import { Status } from "./types";import { endOfDay, isSameDay, startOfDay } from "./utils";
export type Monitor = { count: number; ok: number; day: string;};type StatusReports = (StatusReport & { statusReportUpdates?: StatusReportUpdate[];})[];type Incidents = Incident[];type Maintenances = Maintenance[];
/** * Tracker Class is supposed to handle the data and calculate from a single monitor. * But we use it to handle the StatusCheck as well (with no data for a single monitor). * We can create Inheritence to handle the StatusCheck and Monitor separately and even * StatusPage with multiple Monitors. */export class Tracker { private data: Monitor[] = []; private statusReports: StatusReports = []; private incidents: Incidents = []; private maintenances: Maintenances = [];
constructor(arg: { data?: Monitor[]; statusReports?: StatusReports; incidents?: Incidents; maintenances?: Maintenance[]; }) { this.data = arg.data || []; // TODO: use another Class to handle a single Day this.statusReports = arg.statusReports || []; this.incidents = arg.incidents || []; this.maintenances = arg.maintenances || []; }
private calculateUptime(data: { ok: number; count: number }[]) { const { count, ok } = this.aggregatedData(data); if (count === 0) return 100; // starting with 100% uptime return Math.round((ok / count) * 100_000) / 1_000; // round to 3 decimal places }
private aggregatedData(data: { ok: number; count: number }[]) { return data.reduce( (prev, curr) => { prev.ok += curr.ok; prev.count += curr.count; return prev; }, { count: 0, ok: 0 }, ); }
get isDataMissing() { const { count } = this.aggregatedData(this.data); return count === 0; }
private calculateUptimeStatus(data: { ok: number; count: number }[]): Status { const uptime = this.calculateUptime(data); if (uptime >= 98) return Status.Operational; if (uptime >= 60) return Status.DegradedPerformance; if (uptime > 30) return Status.PartialOutage; return Status.MajorOutage; }
private isOngoingIncident() { return this.incidents.some((incident) => !incident.resolvedAt); }
private isOngoingReport() { const resolved: StatusReport["status"][] = ["monitoring", "resolved"]; return this.statusReports.some( (report) => !resolved.includes(report.status), ); }
private isOngoingMaintenance() { return this.maintenances.some((maintenance) => { const now = new Date(); return ( new Date(maintenance.from).getTime() <= now.getTime() && new Date(maintenance.to).getTime() >= now.getTime() ); }); }
get totalUptime(): number { return this.calculateUptime(this.data); }
get currentStatus(): Status { if (this.isOngoingMaintenance()) return Status.UnderMaintenance; if (this.isOngoingReport()) return Status.DegradedPerformance; if (this.isOngoingIncident()) return Status.Incident; return this.calculateUptimeStatus(this.data); }
get currentVariant(): StatusVariant { return statusDetails[this.currentStatus].variant; }
get currentDetails(): StatusDetails { return statusDetails[this.currentStatus]; }
get currentClassName(): string { return classNames[this.currentVariant]; }
// HACK: this is a temporary solution to get the incidents private getIncidentsByDay(day: Date): Incidents { const incidents = this.incidents?.filter((incident) => { const { startedAt, resolvedAt } = incident; const eod = endOfDay(day); const sod = startOfDay(day);
if (!startedAt) return false; // not started
const hasStartedAfterEndOfDay = startedAt.getTime() >= eod.getTime();
if (hasStartedAfterEndOfDay) return false;
if (!resolvedAt) return true; // still ongoing
const hasResolvedBeforeStartOfDay = resolvedAt.getTime() <= sod.getTime();
if (hasResolvedBeforeStartOfDay) return false;
const hasStartedBeforeEndOfDay = startedAt.getTime() <= eod.getTime();
const hasResolvedBeforeEndOfDay = resolvedAt.getTime() <= eod.getTime();
if (hasStartedBeforeEndOfDay || hasResolvedBeforeEndOfDay) return true;
return false; });
return incidents; }
// HACK: this is a temporary solution to get the status reports private getStatusReportsByDay(props: Monitor): StatusReports { const statusReports = this.statusReports?.filter((report) => { const firstStatusReportUpdate = report?.statusReportUpdates?.sort( (a, b) => a.date.getTime() - b.date.getTime(), )?.[0];
if (!firstStatusReportUpdate) return false;
const day = new Date(props.day); return isSameDay(firstStatusReportUpdate.date, day); }); return statusReports; }
private getMaintenancesByDay(day: Date): Maintenances { const maintenances = this.maintenances.filter((maintenance) => { const eod = endOfDay(day); const sod = startOfDay(day); return ( maintenance.from.getTime() <= eod.getTime() && maintenance.to.getTime() >= sod.getTime() ); }); return maintenances; }
// TODO: it would be great to create a class to handle a single day // FIXME: will be always generated on each tracker.days call - needs to be in the constructor? get days() { const data = this.data.map((props) => { const day = new Date(props.day); const blacklist = isInBlacklist(day); const incidents = this.getIncidentsByDay(day); const statusReports = this.getStatusReportsByDay(props); const maintenances = this.getMaintenancesByDay(day);
const isMissingData = props.count === 0;
/** * 1. Maintenance * 2. Status Reports (Degraded Performance) * 3. Incidents * 4. Uptime Status (Operational, Degraded Performance, Partial Outage, Major Outage) */ const status = maintenances.length ? Status.UnderMaintenance : statusReports.length ? Status.DegradedPerformance : incidents.length ? Status.Incident : isMissingData ? Status.Unknown : this.calculateUptimeStatus([props]);
const variant = statusDetails[status].variant; const label = statusDetails[status].short;
return { ...props, blacklist, incidents, statusReports, maintenances, status, variant, label: isMissingData ? "Missing" : label, }; }); return data; }
get toString() { return statusDetails[this.currentStatus].short; }}