Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 338 lines 8.3 kB view raw
1"use client"; 2 3import type { MetricCard } from "@/components/metric/metric-card"; 4import { formatDateTime, formatMilliseconds } from "@/lib/formatter"; 5import type { RouterOutputs } from "@openstatus/api"; 6import { monitorRegions } from "@openstatus/db/src/schema/constants"; 7import { startOfDay, subDays } from "date-fns"; 8import type { RegionMetric } from "./region-metrics"; 9 10export const STATUS = ["success", "error", "degraded"] as const; 11export const PERIODS = ["1d", "7d", "14d"] as const; 12export const REGIONS = 13 monitorRegions as unknown as (typeof monitorRegions)[number][]; 14export const PERCENTILES = ["p50", "p75", "p90", "p95", "p99"] as const; 15export const INTERVALS = [5, 15, 30, 60, 120, 240, 480, 1440] as const; 16export const TRIGGER = ["api", "cron"] as const; 17 18const PERCENTILE_MAP = { 19 p50: "p50Latency", 20 p75: "p75Latency", 21 p90: "p90Latency", 22 p95: "p95Latency", 23 p99: "p99Latency", 24} as const; 25 26// FIXME: rename pipe return values 27 28export function mapMetrics(metrics: RouterOutputs["tinybird"]["metrics"]) { 29 return metrics.data?.map((metric) => { 30 return { 31 p50: metric.p50Latency, 32 p75: metric.p75Latency, 33 p90: metric.p90Latency, 34 p95: metric.p95Latency, 35 p99: metric.p99Latency, 36 total: metric.count, 37 uptime: (metric.success + metric.degraded) / metric.count, 38 degraded: metric.degraded, 39 error: metric.error, 40 lastTimestamp: metric.lastTimestamp, 41 }; 42 }); 43} 44 45export const metricsCards = { 46 uptime: { 47 label: "UPTIME", 48 variant: "success", 49 }, 50 degraded: { 51 label: "DEGRADED", 52 variant: "warning", 53 }, 54 error: { 55 label: "FAILING", 56 variant: "destructive", 57 }, 58 total: { 59 label: "REQUESTS", 60 variant: "default", 61 }, 62 lastTimestamp: { 63 label: "LAST CHECKED", 64 variant: "ghost", 65 }, 66 p50: { 67 label: "P50", 68 variant: "default", 69 }, 70 p75: { 71 label: "P75", 72 variant: "default", 73 }, 74 p90: { 75 label: "P90", 76 variant: "default", 77 }, 78 p95: { 79 label: "P95", 80 variant: "default", 81 }, 82 p99: { 83 label: "P99", 84 variant: "default", 85 }, 86} as const satisfies Record< 87 keyof ReturnType<typeof mapMetrics>[number], 88 { 89 label: string; 90 variant: React.ComponentProps<typeof MetricCard>["variant"]; 91 } 92>; 93 94export function mapUptime(status: RouterOutputs["tinybird"]["uptime"]) { 95 return status.data 96 .map((status) => { 97 return { 98 ...status, 99 ok: status.success, 100 interval: formatDateTime(status.interval), 101 total: status.success + status.error + status.degraded, 102 }; 103 }) 104 .reverse(); 105} 106 107/** 108 * Transform Tinybird `metricsRegions` response into RegionMetric[] for UI. 109 */ 110export function mapRegionMetrics( 111 timeline: RouterOutputs["tinybird"]["metricsRegions"] | undefined, 112 regions: string[], 113 percentile: (typeof PERCENTILES)[number], 114): RegionMetric[] { 115 if (!timeline) 116 return (regions 117 .sort((a, b) => a.localeCompare(b)) 118 .map((region) => ({ 119 region, 120 p50: 0, 121 p90: 0, 122 p99: 0, 123 trend: [] as { 124 latency: number; 125 timestamp: number; 126 [key: string]: number; 127 }[], 128 })) ?? []) satisfies RegionMetric[]; 129 130 type TimelineRow = (typeof timeline.data)[number]; 131 132 const map = new Map< 133 string, 134 { 135 region: string; 136 p50: number; 137 p90: number; 138 p99: number; 139 trend: { 140 latency: number; 141 timestamp: number; 142 [key: string]: number; 143 }[]; 144 } 145 >(); 146 147 (timeline.data as TimelineRow[]) 148 .filter((row) => regions.includes(row.region)) 149 .sort((a, b) => a.region.localeCompare(b.region)) 150 .forEach((row) => { 151 const region = row.region; 152 const entry = map.get(region) ?? { 153 region, 154 p50: 0, 155 p90: 0, 156 p99: 0, 157 trend: [], 158 }; 159 160 entry.trend.push({ 161 latency: row[PERCENTILE_MAP[percentile]] ?? 0, 162 timestamp: row.timestamp, 163 [region]: row[PERCENTILE_MAP[percentile]] ?? 0, 164 }); 165 166 entry.p50 += row.p50Latency ?? 0; 167 entry.p90 += row.p90Latency ?? 0; 168 entry.p99 += row.p99Latency ?? 0; 169 170 map.set(region, entry); 171 }); 172 173 map.forEach((entry) => { 174 const count = entry.trend.length || 1; 175 entry.trend.reverse(); 176 entry.p50 = Math.round(entry.p50 / count); 177 entry.p90 = Math.round(entry.p90 / count); 178 entry.p99 = Math.round(entry.p99 / count); 179 }); 180 181 return Array.from(map.values()) as RegionMetric[]; 182} 183 184export function mapGlobalMetrics( 185 metrics: RouterOutputs["tinybird"]["globalMetrics"], 186) { 187 return metrics.data?.map((metric) => { 188 return { 189 p50: metric.p50Latency, 190 p75: metric.p75Latency, 191 p90: metric.p90Latency, 192 p95: metric.p95Latency, 193 p99: metric.p99Latency, 194 total: metric.count, 195 monitorId: metric.monitorId, 196 }; 197 }); 198} 199 200export type MonitorListMetric = { 201 title: string; 202 key: "degraded" | "error" | "active" | "inactive" | "p95"; 203 value: number | string | undefined; 204 variant: React.ComponentProps<typeof MetricCard>["variant"]; 205}; 206 207export const globalCards = [ 208 "active", 209 "degraded", 210 "error", 211 "inactive", 212 "p95", 213] as const; 214 215export const metricsGlobalCards: Record< 216 (typeof globalCards)[number], 217 { 218 title: string; 219 key: (typeof globalCards)[number]; 220 } 221> = { 222 active: { 223 title: "Normal", 224 key: "active" as const, 225 }, 226 degraded: { 227 title: "Degraded", 228 key: "degraded" as const, 229 }, 230 error: { 231 title: "Failing", 232 key: "error" as const, 233 }, 234 inactive: { 235 title: "Inactive", 236 key: "inactive" as const, 237 }, 238 p95: { 239 title: "Slowest P95", 240 key: "p95" as const, 241 }, 242}; 243 244/** 245 * Build the metric cards data that is shown on the monitors list page. 246 */ 247export function getMonitorListMetrics( 248 monitors: RouterOutputs["monitor"]["list"] = [], 249 data: { 250 p95Latency: number; 251 monitorId: string; 252 }[] = [], 253): readonly MonitorListMetric[] { 254 const variantMap: Record< 255 (typeof globalCards)[number], 256 React.ComponentProps<typeof MetricCard>["variant"] 257 > = { 258 active: "success", 259 degraded: "warning", 260 error: "destructive", 261 inactive: "default", 262 p95: "ghost", 263 } as const; 264 265 return globalCards.map((key) => { 266 let value: number | string | undefined; 267 switch (key) { 268 case "active": 269 value = monitors.filter( 270 (m) => m.status === "active" && m.active, 271 ).length; 272 break; 273 case "degraded": 274 value = monitors.filter( 275 (m) => m.status === "degraded" && m.active, 276 ).length; 277 break; 278 case "error": 279 value = monitors.filter((m) => m.status === "error" && m.active).length; 280 break; 281 case "inactive": 282 value = monitors.filter((m) => m.active === false).length; 283 break; 284 case "p95": 285 const p95 = data.sort((a, b) => b.p95Latency - a.p95Latency)[0] 286 ?.p95Latency; 287 value = p95 ? formatMilliseconds(p95) : "N/A"; 288 break; 289 } 290 291 return { 292 title: metricsGlobalCards[key].title, 293 key, 294 value, 295 variant: variantMap[key], 296 } as const; 297 }) as readonly MonitorListMetric[]; 298} 299 300export function mapLatency( 301 latency: RouterOutputs["tinybird"]["metricsLatency"], 302 percentile: (typeof PERCENTILES)[number], 303) { 304 return latency.data?.map((metric) => { 305 return { 306 timestamp: formatDateTime(new Date(metric.timestamp)), 307 latency: metric[PERCENTILE_MAP[percentile]], 308 }; 309 }); 310} 311 312export function mapTimingPhases( 313 timingPhases: RouterOutputs["tinybird"]["metricsTimingPhases"], 314 percentile: (typeof PERCENTILES)[number], 315) { 316 return timingPhases.data?.map((metric) => { 317 return { 318 timestamp: formatDateTime(new Date(metric.timestamp)), 319 dns: metric[`${percentile}Dns`], 320 ttfb: metric[`${percentile}Ttfb`], 321 transfer: metric[`${percentile}Transfer`], 322 connect: metric[`${percentile}Connect`], 323 tls: metric[`${percentile}Tls`], 324 }; 325 }); 326} 327 328export const periodToInterval = { 329 "1d": 60, 330 "7d": 240, 331 "14d": 480, 332} satisfies Record<(typeof PERIODS)[number], number>; 333 334export const periodToFromDate = { 335 "1d": startOfDay(subDays(new Date(), 1)), 336 "7d": startOfDay(subDays(new Date(), 7)), 337 "14d": startOfDay(subDays(new Date(), 14)), 338} satisfies Record<(typeof PERIODS)[number], Date>;