diff --git a/migrations/0005_add_indexes.sql b/migrations/0005_add_indexes.sql new file mode 100644 index 0000000..77b184e --- /dev/null +++ b/migrations/0005_add_indexes.sql @@ -0,0 +1,2 @@ +CREATE INDEX idx_pings_timestamp ON pings (timestamp); +CREATE INDEX idx_incidents_github ON incidents (github_repo, github_issue_number); diff --git a/src/db.ts b/src/db.ts index 37e5bac..c279606 100644 --- a/src/db.ts +++ b/src/db.ts @@ -31,21 +31,62 @@ export async function getUptime7d( service_id: string, ): Promise { const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60; - const total = await db + const row = await db .prepare( - "SELECT COUNT(*) as count FROM pings WHERE service_id = ? AND timestamp >= ?", + "SELECT COUNT(*) as total, SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as up_count FROM pings WHERE service_id = ? AND timestamp >= ?", ) .bind(service_id, since) - .first<{ count: number }>(); - const up = await db + .first<{ total: number; up_count: number }>(); + + if (!row || row.total === 0) return 100; + return Math.round((row.up_count / row.total) * 10000) / 100; +} + +export async function getAllLatestPings( + db: D1Database, +): Promise> { + const rows = await db .prepare( - "SELECT COUNT(*) as count FROM pings WHERE service_id = ? AND timestamp >= ? AND status = 'up'", + `SELECT p.service_id, p.status, p.latency_ms + FROM pings p + INNER JOIN (SELECT service_id, MAX(timestamp) as max_ts FROM pings GROUP BY service_id) latest + ON p.service_id = latest.service_id AND p.timestamp = latest.max_ts`, ) - .bind(service_id, since) - .first<{ count: number }>(); + .all(); - if (!total || total.count === 0) return 100; - return Math.round(((up?.count ?? 0) / total.count) * 10000) / 100; + const map = new Map(); + for (const row of rows.results) { + map.set(row.service_id as string, { + status: row.status as string, + latency_ms: row.latency_ms as number | null, + }); + } + return map; +} + +export async function getAllUptime7d( + db: D1Database, +): Promise> { + const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60; + const rows = await db + .prepare( + `SELECT service_id, COUNT(*) as total, SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as up_count + FROM pings WHERE timestamp >= ? + GROUP BY service_id`, + ) + .bind(since) + .all(); + + const map = new Map(); + for (const row of rows.results) { + const total = row.total as number; + const up = row.up_count as number; + map.set( + row.service_id as string, + total === 0 ? 100 : Math.round((up / total) * 10000) / 100, + ); + } + return map; } export async function getUptimeBuckets( @@ -220,16 +261,47 @@ export async function getActiveIncidents(db: D1Database): Promise { } export async function getActiveIncidentsWithUpdates(db: D1Database): Promise { - const incidents = await getActiveIncidents(db); - return Promise.all( - incidents.map(async (incident) => { - const updates = await db - .prepare("SELECT * FROM incident_updates WHERE incident_id = ? ORDER BY created_at ASC") - .bind(incident.id) - .all(); - return { ...incident, updates: updates.results as unknown as IncidentUpdate[] }; - }), - ); + const rows = await db + .prepare( + `SELECT i.*, u.id as update_id, u.status as update_status, u.message as update_message, u.created_at as update_created_at + FROM incidents i + LEFT JOIN incident_updates u ON u.incident_id = i.id + WHERE i.status != 'resolved' + ORDER BY i.created_at DESC, u.created_at ASC`, + ) + .all(); + + const incidentMap = new Map(); + for (const row of rows.results) { + const id = row.id as number; + if (!incidentMap.has(id)) { + incidentMap.set(id, { + id, + service_id: row.service_id as string, + title: row.title as string, + status: row.status as string, + severity: row.severity as string, + triage_report: row.triage_report as string | null, + github_repo: row.github_repo as string | null, + github_issue_number: row.github_issue_number as number | null, + started_at: row.started_at as number, + resolved_at: row.resolved_at as number | null, + created_at: row.created_at as number, + updated_at: row.updated_at as number, + updates: [], + }); + } + if (row.update_id) { + incidentMap.get(id)!.updates.push({ + id: row.update_id as number, + incident_id: id, + status: row.update_status as string, + message: row.update_message as string, + created_at: row.update_created_at as number, + }); + } + } + return Array.from(incidentMap.values()); } export async function getActiveIncidentForService( diff --git a/src/overall.ts b/src/overall.ts index 19d7cb0..81a3a3e 100644 --- a/src/overall.ts +++ b/src/overall.ts @@ -1,6 +1,6 @@ -import type { Env } from "./types"; +import type { Env, Incident, ServicesManifest } from "./types"; import { getManifest } from "./manifest"; -import { getLatestPing, getActiveIncidents } from "./db"; +import { getAllLatestPings, getActiveIncidents } from "./db"; import { getDeviceStatus } from "./tailscale"; export type OverallGrade = "up" | "degraded" | "down"; @@ -10,8 +10,19 @@ export interface OverallStatus { label: string; } -export async function getOverallStatus(env: Env): Promise { - const manifest = await getManifest(env); +export async function getOverallStatus( + env: Env, + prefetched?: { + manifest?: ServicesManifest; + latestPings?: Map; + activeIncidents?: Incident[]; + machineOnline?: Map; + }, +): Promise { + const manifest = prefetched?.manifest ?? await getManifest(env); + const latestPings = prefetched?.latestPings ?? await getAllLatestPings(env.DB); + const activeIncidents = prefetched?.activeIncidents ?? await getActiveIncidents(env.DB); + const servers = Object.entries(manifest).filter( ([, m]) => m.type === "server" && m.services.length > 0, ); @@ -19,13 +30,13 @@ export async function getOverallStatus(env: Env): Promise { const statuses: string[] = []; let anyServerOffline = false; - for (const [, machine] of servers) { - const online = await getDeviceStatus(env, machine.tailscale_host); + for (const [name, machine] of servers) { + const online = prefetched?.machineOnline?.get(name) ?? await getDeviceStatus(env, machine.tailscale_host); if (!online) anyServerOffline = true; for (const svc of machine.services.filter((s) => s.health_url)) { - const ping = await getLatestPing(env.DB, svc.name); - statuses.push((ping?.status as string) ?? "unknown"); + const ping = latestPings.get(svc.name); + statuses.push(ping?.status ?? "unknown"); } } @@ -42,7 +53,6 @@ export async function getOverallStatus(env: Env): Promise { s === "misconfigured", ); - const activeIncidents = await getActiveIncidents(env.DB); const hasCritical = activeIncidents.some((i) => i.severity === "critical"); const hasMajor = activeIncidents.some((i) => i.severity === "major"); diff --git a/src/routes/index.ts b/src/routes/index.ts index c0f5f75..7c41c60 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,36 +1,40 @@ import type { Env } from "../types"; import { getManifest } from "../manifest"; -import { getLatestPing, getUptime7d, getOverallUptimeDays, getLastCheckTime, getActiveIncidentsWithUpdates, getRecentIncidents } from "../db"; +import { getAllLatestPings, getAllUptime7d, getOverallUptimeDays, getLastCheckTime, getActiveIncidentsWithUpdates, getActiveIncidents, getRecentIncidents } from "../db"; import { getDeviceStatus } from "../tailscale"; import { getOverallStatus } from "../overall"; import { COMMIT_SHA } from "../version"; export async function handleIndex(env: Env): Promise { - const manifest = await getManifest(env); + const [manifest, latestPings, uptimes, lastCheck, uptimeDays, activeIncidentsWithUpdates, activeIncidentsList, recentIncidents] = await Promise.all([ + getManifest(env), + getAllLatestPings(env.DB), + getAllUptime7d(env.DB), + getLastCheckTime(env.DB), + getOverallUptimeDays(env.DB, 90), + getActiveIncidentsWithUpdates(env.DB), + getActiveIncidents(env.DB), + getRecentIncidents(env.DB, 7), + ]); + const machineOnline = new Map(); const machines = await Promise.all( Object.entries(manifest).map(async ([name, machine]) => { const online = await getDeviceStatus(env, machine.tailscale_host); - const services = await Promise.all( - machine.services.map(async (svc) => { - const ping = await getLatestPing(env.DB, svc.name); - const uptime = await getUptime7d(env.DB, svc.name); - return { - name: svc.name, - description: svc.description, - url: `https://${svc.domain}`, - status: ping?.status ?? "unknown", - latency_ms: ping?.latency_ms ?? null, - uptime_7d: uptime, - has_health: svc.health_url !== null, - }; - }), - ); + machineOnline.set(name, online); + const services = machine.services.map((svc) => ({ + name: svc.name, + description: svc.description, + url: `https://${svc.domain}`, + status: latestPings.get(svc.name)?.status ?? "unknown", + latency_ms: latestPings.get(svc.name)?.latency_ms ?? null, + uptime_7d: uptimes.get(svc.name) ?? 100, + has_health: svc.health_url !== null, + })); return { name, type: machine.type, online, services }; }), ); - const lastCheck = await getLastCheckTime(env.DB); const lastCheckISO = lastCheck ? new Date(lastCheck * 1000).toISOString() : null; @@ -38,10 +42,10 @@ export async function handleIndex(env: Env): Promise { const servers = machines.filter((m) => m.type === "server"); const clients = machines.filter((m) => m.type === "client"); - const { grade: overallClass, label: overallText } = await getOverallStatus(env); - const uptimeDays = await getOverallUptimeDays(env.DB, 90); - const activeIncidents = await getActiveIncidentsWithUpdates(env.DB); - const recentIncidents = await getRecentIncidents(env.DB, 7); + const { grade: overallClass, label: overallText } = await getOverallStatus(env, { + manifest, latestPings, activeIncidents: activeIncidentsList, machineOnline, + }); + const activeIncidents = activeIncidentsWithUpdates; const resolvedIncidents = recentIncidents.filter((i) => i.status === "resolved"); const html = ` diff --git a/src/routes/status.ts b/src/routes/status.ts index 93510a4..5340c15 100644 --- a/src/routes/status.ts +++ b/src/routes/status.ts @@ -1,6 +1,6 @@ import type { Env } from "../types"; import { getManifest } from "../manifest"; -import { getLatestPing, getUptime7d, getLastCheckTime } from "../db"; +import { getLatestPing, getUptime7d, getAllLatestPings, getAllUptime7d, getLastCheckTime } from "../db"; import { getDeviceStatus } from "../tailscale"; import { getOverallStatus } from "../overall"; @@ -16,17 +16,20 @@ function worstStatus(statuses: string[]): string { // GET /api/status/overall async function overallStatus(env: Env): Promise { - const manifest = await getManifest(env); + const [manifest, uptimes, latestPings] = await Promise.all([ + getManifest(env), + getAllUptime7d(env.DB), + getAllLatestPings(env.DB), + ]); const allServices = Object.values(manifest).flatMap((m) => m.services); const monitored = allServices.filter((s) => s.health_url !== null); let totalUptime = 0; for (const svc of monitored) { - const uptime = await getUptime7d(env.DB, svc.name); - totalUptime += uptime; + totalUptime += uptimes.get(svc.name) ?? 100; } - const { grade } = await getOverallStatus(env); + const { grade } = await getOverallStatus(env, { manifest, latestPings }); const avgUptime = monitored.length > 0 ? Math.round((totalUptime / monitored.length) * 100) / 100 @@ -45,23 +48,25 @@ async function overallStatus(env: Env): Promise { // GET /api/status async function fullStatus(env: Env): Promise { - const manifest = await getManifest(env); + const [manifest, latestPings, uptimes, lastCheck] = await Promise.all([ + getManifest(env), + getAllLatestPings(env.DB), + getAllUptime7d(env.DB), + getLastCheckTime(env.DB), + ]); const machines = await Promise.all( Object.entries(manifest).map(async ([name, machine]) => { const online = await getDeviceStatus(env, machine.tailscale_host); - const services = await Promise.all( - machine.services.map(async (svc) => { - const ping = await getLatestPing(env.DB, svc.name); - const uptime = await getUptime7d(env.DB, svc.name); - return { - id: svc.name, - status: (ping?.status ?? "unknown") as string, - latency_ms: ping?.latency_ms ?? null, - uptime_7d: uptime, - }; - }), - ); + const services = machine.services.map((svc) => { + const ping = latestPings.get(svc.name); + return { + id: svc.name, + status: (ping?.status ?? "unknown") as string, + latency_ms: ping?.latency_ms ?? null, + uptime_7d: uptimes.get(svc.name) ?? 100, + }; + }); const svcStatuses = services.map((s) => s.status); return { name, @@ -76,8 +81,7 @@ async function fullStatus(env: Env): Promise { }), ); - const { grade } = await getOverallStatus(env); - const lastCheck = await getLastCheckTime(env.DB); + const { grade } = await getOverallStatus(env, { manifest, latestPings }); return Response.json( {