Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 229 lines 6.9 kB view raw
1import type { 2 Incident, 3 Maintenance, 4 StatusReport, 5 StatusReportUpdate, 6} from "@openstatus/db/src/schema"; 7 8import { isInBlacklist } from "./blacklist"; 9import { classNames, statusDetails } from "./config"; 10import type { StatusDetails, StatusVariant } from "./types"; 11import { Status } from "./types"; 12import { endOfDay, isSameDay, startOfDay } from "./utils"; 13 14type Monitor = { 15 count: number; 16 ok: number; 17 day: string; 18}; 19type StatusReports = (StatusReport & { 20 statusReportUpdates?: StatusReportUpdate[]; 21})[]; 22type Incidents = Incident[]; 23type Maintenances = Maintenance[]; 24 25/** 26 * Tracker Class is supposed to handle the data and calculate from a single monitor. 27 * But we use it to handle the StatusCheck as well (with no data for a single monitor). 28 * We can create Inheritence to handle the StatusCheck and Monitor separately and even 29 * StatusPage with multiple Monitors. 30 */ 31export class Tracker { 32 private data: Monitor[] = []; 33 private statusReports: StatusReports = []; 34 private incidents: Incidents = []; 35 private maintenances: Maintenances = []; 36 37 constructor(arg: { 38 data?: Monitor[]; 39 statusReports?: StatusReports; 40 incidents?: Incidents; 41 maintenances?: Maintenance[]; 42 }) { 43 this.data = arg.data || []; // TODO: use another Class to handle a single Day 44 this.statusReports = arg.statusReports || []; 45 this.incidents = arg.incidents || []; 46 this.maintenances = arg.maintenances || []; 47 } 48 49 private calculateUptime(data: { ok: number; count: number }[]) { 50 const { count, ok } = this.aggregatedData(data); 51 if (count === 0) return 100; // starting with 100% uptime 52 return Math.round((ok / count) * 10_000) / 100; // round to 2 decimal places 53 } 54 55 private aggregatedData(data: { ok: number; count: number }[]) { 56 return data.reduce( 57 (prev, curr) => { 58 prev.ok += curr.ok; 59 prev.count += curr.count; 60 return prev; 61 }, 62 { count: 0, ok: 0 }, 63 ); 64 } 65 66 get isDataMissing() { 67 const { count } = this.aggregatedData(this.data); 68 return count === 0; 69 } 70 71 private calculateUptimeStatus(data: { ok: number; count: number }[]): Status { 72 const uptime = this.calculateUptime(data); 73 if (uptime >= 98) return Status.Operational; 74 if (uptime >= 60) return Status.DegradedPerformance; 75 if (uptime > 30) return Status.PartialOutage; 76 return Status.MajorOutage; 77 } 78 79 private isOngoingIncident() { 80 return this.incidents.some((incident) => !incident.resolvedAt); 81 } 82 83 private isOngoingReport() { 84 const resolved: StatusReport["status"][] = ["monitoring", "resolved"]; 85 return this.statusReports.some( 86 (report) => !resolved.includes(report.status), 87 ); 88 } 89 90 private isOngoingMaintenance() { 91 return this.maintenances.some((maintenance) => { 92 const now = new Date(); 93 return ( 94 new Date(maintenance.from).getTime() <= now.getTime() && 95 new Date(maintenance.to).getTime() >= now.getTime() 96 ); 97 }); 98 } 99 100 get totalUptime(): number { 101 return this.calculateUptime(this.data); 102 } 103 104 get currentStatus(): Status { 105 if (this.isOngoingMaintenance()) return Status.UnderMaintenance; 106 if (this.isOngoingReport()) return Status.DegradedPerformance; 107 if (this.isOngoingIncident()) return Status.Incident; 108 return this.calculateUptimeStatus(this.data); 109 } 110 111 get currentVariant(): StatusVariant { 112 return statusDetails[this.currentStatus].variant; 113 } 114 115 get currentDetails(): StatusDetails { 116 return statusDetails[this.currentStatus]; 117 } 118 119 get currentClassName(): string { 120 return classNames[this.currentVariant]; 121 } 122 123 // HACK: this is a temporary solution to get the incidents 124 private getIncidentsByDay(day: Date): Incidents { 125 const incidents = this.incidents?.filter((incident) => { 126 const { startedAt, resolvedAt } = incident; 127 const eod = endOfDay(day); 128 const sod = startOfDay(day); 129 130 if (!startedAt) return false; // not started 131 132 const hasStartedAfterEndOfDay = startedAt.getTime() >= eod.getTime(); 133 134 if (hasStartedAfterEndOfDay) return false; 135 136 if (!resolvedAt) return true; // still ongoing 137 138 const hasResolvedBeforeStartOfDay = resolvedAt.getTime() <= sod.getTime(); 139 140 if (hasResolvedBeforeStartOfDay) return false; 141 142 const hasStartedBeforeEndOfDay = startedAt.getTime() <= eod.getTime(); 143 144 const hasResolvedBeforeEndOfDay = resolvedAt.getTime() <= eod.getTime(); 145 146 if (hasStartedBeforeEndOfDay || hasResolvedBeforeEndOfDay) return true; 147 148 return false; 149 }); 150 151 return incidents; 152 } 153 154 // HACK: this is a temporary solution to get the status reports 155 private getStatusReportsByDay(props: Monitor): StatusReports { 156 const statusReports = this.statusReports?.filter((report) => { 157 const firstStatusReportUpdate = report?.statusReportUpdates?.sort( 158 (a, b) => a.date.getTime() - b.date.getTime(), 159 )?.[0]; 160 161 if (!firstStatusReportUpdate) return false; 162 163 const day = new Date(props.day); 164 return isSameDay(firstStatusReportUpdate.date, day); 165 }); 166 return statusReports; 167 } 168 169 private getMaintenancesByDay(day: Date): Maintenances { 170 const maintenances = this.maintenances.filter((maintenance) => { 171 const eod = endOfDay(day); 172 const sod = startOfDay(day); 173 return ( 174 maintenance.from.getTime() <= eod.getTime() && 175 maintenance.to.getTime() >= sod.getTime() 176 ); 177 }); 178 return maintenances; 179 } 180 181 // TODO: it would be great to create a class to handle a single day 182 // FIXME: will be always generated on each tracker.days call - needs to be in the constructor? 183 get days() { 184 const data = this.data.map((props) => { 185 const day = new Date(props.day); 186 const blacklist = isInBlacklist(day); 187 const incidents = this.getIncidentsByDay(day); 188 const statusReports = this.getStatusReportsByDay(props); 189 const maintenances = this.getMaintenancesByDay(day); 190 191 const isMissingData = props.count === 0; 192 193 /** 194 * 1. Maintenance 195 * 2. Status Reports (Degraded Performance) 196 * 3. Incidents 197 * 4. Uptime Status (Operational, Degraded Performance, Partial Outage, Major Outage) 198 */ 199 const status = maintenances.length 200 ? Status.UnderMaintenance 201 : statusReports.length 202 ? Status.DegradedPerformance 203 : incidents.length 204 ? Status.Incident 205 : isMissingData 206 ? Status.Unknown 207 : this.calculateUptimeStatus([props]); 208 209 const variant = statusDetails[status].variant; 210 const label = statusDetails[status].short; 211 212 return { 213 ...props, 214 blacklist, 215 incidents, 216 statusReports, 217 maintenances, 218 status, 219 variant, 220 label: isMissingData ? "Missing" : label, 221 }; 222 }); 223 return data; 224 } 225 226 get toString() { 227 return statusDetails[this.currentStatus].short; 228 } 229}