Openstatus www.openstatus.dev

fix: public status api cache (#554)

authored by

Maximilian Kaske and committed by
GitHub
63ab0a78 42b4bf3b

+113 -37
+58 -37
apps/server/src/public/status.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { endTime, setMetric, startTime } from "hono/timing"; 3 3 4 - import { and, db, eq } from "@openstatus/db"; 4 + import { and, db, eq, inArray } from "@openstatus/db"; 5 5 import { 6 6 monitor, 7 7 monitorsToPages, ··· 10 10 pagesToStatusReports, 11 11 statusReport, 12 12 } from "@openstatus/db/src/schema"; 13 - import { getMonitorList, Tinybird } from "@openstatus/tinybird"; 13 + import { getPublicStatus, Tinybird } from "@openstatus/tinybird"; 14 14 import { Redis } from "@openstatus/upstash"; 15 15 16 16 import { env } from "../env"; 17 + import { notEmpty } from "../utils/not-empty"; 17 18 18 19 // TODO: include ratelimiting 19 20 ··· 43 44 } 44 45 45 46 startTime(c, "database"); 46 - 47 - const monitorData = await db 48 - .select() 49 - .from(monitorsToPages) 50 - .leftJoin(monitor, and(eq(monitorsToPages.monitorId, monitor.id))) 51 - .leftJoin( 52 - monitorsToStatusReport, 53 - eq(monitor.id, monitorsToStatusReport.monitorId), 54 - ) 55 - .leftJoin( 56 - statusReport, 57 - eq(monitorsToStatusReport.statusReportId, statusReport.id), 58 - ) 59 - .leftJoin(page, eq(monitorsToPages.pageId, page.id)) 60 - .where(eq(page.slug, slug)) // TODO: query only active monitors 61 - .all(); 62 - 63 - const pageStatusReportData = await db 64 - .select() 65 - .from(pagesToStatusReports) 66 - .leftJoin( 67 - statusReport, 68 - eq(pagesToStatusReports.statusReportId, statusReport.id), 69 - ) 70 - .leftJoin(page, eq(pagesToStatusReports.pageId, page.id)) 71 - .where(eq(page.slug, slug)) 72 - .all(); 47 + const { monitorData, pageStatusReportData, monitorStatusReportData } = 48 + await getStatusPageData(slug); 49 + endTime(c, "database"); 73 50 74 - const isIncident = [...pageStatusReportData, ...monitorData].some((data) => { 75 - if (!data.status_report) return false; 76 - return !["monitoring", "resolved"].includes(data.status_report.status); 77 - }); 51 + const isIncident = [...pageStatusReportData, ...monitorStatusReportData].some( 52 + (data) => { 53 + if (!data.status_report) return false; 54 + return !["monitoring", "resolved"].includes(data.status_report.status); 55 + }, 56 + ); 78 57 79 58 startTime(c, "clickhouse"); 80 - // { data: [{ ok, count }] } 81 59 const lastMonitorPings = await Promise.allSettled( 82 60 monitorData.map(async ({ monitors_to_pages }) => { 83 - return await getMonitorList(tb)({ 61 + return await getPublicStatus(tb)({ 84 62 monitorId: String(monitors_to_pages.monitorId), 85 - limit: 5, // limits the grouped cronTimestamps 86 63 }); 87 64 }), 88 65 ); ··· 111 88 112 89 const status: Status = isIncident ? Status.Incident : getStatus(ratio); 113 90 114 - await redis.set(slug, status, { ex: 30 }); 91 + await redis.set(slug, status, { ex: 60 }); // 1m cache 115 92 116 93 return c.json({ status }); 117 94 }); ··· 124 101 if (ratio >= 0) return Status.MajorOutage; 125 102 return Status.Unknown; 126 103 } 104 + 105 + async function getStatusPageData(slug: string) { 106 + const monitorData = await db 107 + .select() 108 + .from(monitorsToPages) 109 + .leftJoin( 110 + monitor, 111 + // REMINDER: query only active monitors as they are the ones that are displayed on the status page 112 + and(eq(monitorsToPages.monitorId, monitor.id), eq(monitor.active, true)), 113 + ) 114 + .leftJoin(page, eq(monitorsToPages.pageId, page.id)) 115 + .where(eq(page.slug, slug)) 116 + .all(); 117 + 118 + const monitorIds = monitorData.map((i) => i.monitor?.id).filter(notEmpty); 119 + 120 + const monitorStatusReportData = await db 121 + .select() 122 + .from(monitorsToStatusReport) 123 + .leftJoin( 124 + statusReport, 125 + eq(monitorsToStatusReport.statusReportId, statusReport.id), 126 + ) 127 + .where(inArray(monitorsToStatusReport.monitorId, monitorIds)) 128 + .all(); 129 + 130 + // REMINDER: the query can overlap with the previous one 131 + const pageStatusReportData = await db 132 + .select() 133 + .from(pagesToStatusReports) 134 + .leftJoin( 135 + statusReport, 136 + eq(pagesToStatusReports.statusReportId, statusReport.id), 137 + ) 138 + .leftJoin(page, eq(pagesToStatusReports.pageId, page.id)) 139 + .where(eq(page.slug, slug)) 140 + .all(); 141 + 142 + return { 143 + monitorData, 144 + pageStatusReportData, 145 + monitorStatusReportData, 146 + }; 147 + }
+5
apps/server/src/utils/not-empty.ts
··· 1 + export function notEmpty<TValue>( 2 + value: TValue | null | undefined, 3 + ): value is TValue { 4 + return value !== null && value !== undefined; 5 + }
+23
packages/tinybird/pipes/public_status.pipe
··· 1 + VERSION 0 2 + 3 + DESCRIPTION > 4 + last 5 cron timestamps within last 3 hours 5 + 6 + NODE group_by_cronTimestamp 7 + SQL > 8 + 9 + % 10 + SELECT 11 + cronTimestamp, 12 + count() AS count, 13 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 14 + FROM ping_response_v5 15 + WHERE 16 + monitorId = {{ String(monitorId, '1') }} 17 + AND cronTimestamp 18 + >= toUnixTimestamp64Milli(toDateTime64(now() - INTERVAL 3 HOUR, 3)) 19 + GROUP BY cronTimestamp, monitorId 20 + ORDER BY cronTimestamp DESC 21 + LIMIT {{ Int16(limit, 5)}} 22 + 23 +
+10
packages/tinybird/src/client.ts
··· 3 3 import { 4 4 tbBuildHomeStats, 5 5 tbBuildMonitorList, 6 + tbBuildPublicStatus, 6 7 tbBuildResponseList, 7 8 tbIngestPingResponse, 8 9 tbParameterHomeStats, 9 10 tbParameterMonitorList, 11 + tbParameterPublicStatus, 10 12 tbParameterResponseList, 11 13 } from "./validation"; 12 14 ··· 71 73 }, 72 74 }); 73 75 } 76 + 77 + export function getPublicStatus(tb: Tinybird) { 78 + return tb.buildPipe({ 79 + pipe: "public_status__v0", 80 + parameters: tbParameterPublicStatus, 81 + data: tbBuildPublicStatus, 82 + }); 83 + }
+17
packages/tinybird/src/validation.ts
··· 81 81 count: z.number().int(), 82 82 }); 83 83 84 + /** 85 + * Params for pipe public_status (used for our API /public/status/[slug]) 86 + */ 87 + export const tbParameterPublicStatus = z.object({ 88 + monitorId: z.string(), 89 + limit: z.number().int().default(5).optional(), // 5 last cronTimestamps 90 + }); 91 + 92 + /** 93 + * Values from the pipe public_status (used for our API /public/status/[slug]) 94 + */ 95 + export const tbBuildPublicStatus = z.object({ 96 + ok: z.number().int(), 97 + count: z.number().int(), 98 + cronTimestamp: z.number().int(), 99 + }); 100 + 84 101 export type Ping = z.infer<typeof tbBuildResponseList>; 85 102 export type Region = (typeof availableRegions)[number]; // TODO: rename type AvailabeRegion 86 103 export type Monitor = z.infer<typeof tbBuildMonitorList>;