Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Chore/improve server file structure (#342)

* chore: update file structure and add public status route

* chore: add caching

* chore: bun lock

authored by

Maximilian Kaske and committed by
GitHub
290223d6 9ec01396

+150 -28
+1 -1
apps/docs/pages/rest-api/openapi.mdx
··· 3 3 import { useData } from "nextra/data"; 4 4 5 5 export const getStaticProps = async ({ params }) => { 6 - const res = await fetch("https://api.openstatus.dev/openapi"); 6 + const res = await fetch("https://api.openstatus.dev/v1/openapi"); 7 7 const spec = await res.json(); 8 8 return { 9 9 props: {
+4 -1
apps/server/.env.example
··· 1 1 DATABASE_URL=http://127.0.0.1:8080 2 2 DATABASE_AUTH_TOKEN= 3 3 NODE_ENV=production 4 - UNKEY_TOKEN= 4 + UNKEY_TOKEN= 5 + TINY_BIRD_API_KEY= 6 + UPSTASH_REDIS_REST_URL= 7 + UPSTASH_REDIS_REST_TOKEN=
+1
apps/server/package.json
··· 13 13 "zod": "3.21.4", 14 14 "@openstatus/db": "workspace:*", 15 15 "@openstatus/tinybird": "workspace:*", 16 + "@openstatus/upstash": "workspace:*", 16 17 "@unkey/api": "0.6.22", 17 18 "hono": "3.6.1" 18 19 },
+1 -1
apps/server/src/incident.ts apps/server/src/v1/incident.ts
··· 7 7 incidentUpdate, 8 8 } from "@openstatus/db/src/schema"; 9 9 10 - import type { Variables } from "."; 10 + import type { Variables } from "./index"; 11 11 import { ErrorSchema } from "./shared"; 12 12 13 13 const incidentApi = new OpenAPIHono<{ Variables: Variables }>();
+18 -23
apps/server/src/index.ts
··· 1 - import { OpenAPIHono } from "@hono/zod-openapi"; 1 + import { Hono } from "hono"; 2 2 3 - import { incidentApi } from "./incident"; 4 - import { middleware } from "./middleware"; 5 - import { monitorApi } from "./monitor"; 3 + import { publicRoute } from "./public"; 4 + import { api } from "./v1"; 6 5 import { VercelIngest } from "./vercel"; 7 6 8 - export type Variables = { 9 - workspaceId: string; 10 - }; 7 + const app = new Hono(); 8 + 9 + /** 10 + * Vercel Integration 11 + */ 12 + app.post("/integration/vercel", VercelIngest); 13 + 14 + /** 15 + * Public Routes 16 + */ 17 + app.route("/public", publicRoute); 11 18 12 19 /** 13 - * Base Path "/v1" for our api 20 + * Ping Pong 14 21 */ 15 - const app = new OpenAPIHono<{ Variables: Variables }>(); 16 - app.doc("/openapi", { 17 - openapi: "3.0.0", 18 - info: { 19 - version: "1.0.0", 20 - title: "OpenStatus API", 21 - }, 22 - }); 23 22 app.get("/ping", (c) => c.text("pong")); 24 23 25 - // Where we ingest data from Vercel 26 - app.post("/integration/vercel", VercelIngest); 27 24 /** 28 - * Authentification Middleware 25 + * API Routes v1 29 26 */ 30 - 31 - app.use("/v1/*", middleware); 32 - app.route("/v1/monitor", monitorApi); 33 - app.route("/v1/incident", incidentApi); 27 + app.route("/v1", api); 34 28 35 29 if (process.env.NODE_ENV === "development") { 36 30 app.showRoutes(); 37 31 } 32 + 38 33 console.log("Starting server on port 3000"); 39 34 40 35 export default app;
+2 -1
apps/server/src/middleware.ts apps/server/src/v1/middleware.ts
··· 8 8 workspaceId: string; 9 9 }; 10 10 }, 11 - "/v1/*", 11 + "/*", 12 12 {} 13 13 >, 14 14 next: Next, ··· 24 24 if (!result.valid) return c.text("Unauthorized", 401); 25 25 c.set("workspaceId", `${result.ownerId}`); 26 26 } else { 27 + // REMINDER: localhost only 27 28 c.set("workspaceId", "1"); 28 29 } 29 30 await next();
+2 -1
apps/server/src/monitor.ts apps/server/src/v1/monitor.ts
··· 8 8 periodicity, 9 9 } from "@openstatus/db/src/schema/monitor"; 10 10 11 - import type { Variables } from "."; 11 + import type { Variables } from "./index"; 12 12 import { ErrorSchema } from "./shared"; 13 13 14 14 const ParamsSchema = z.object({ ··· 393 393 }, 394 394 }, 395 395 }); 396 + 396 397 monitorApi.openapi(deleteRoute, async (c) => { 397 398 const workspaceId = Number(c.get("workspaceId")); 398 399 const { id } = c.req.valid("param");
+7
apps/server/src/public/index.ts
··· 1 + import { Hono } from "hono"; 2 + 3 + import { status } from "./status"; 4 + 5 + export const publicRoute = new Hono(); 6 + 7 + publicRoute.route("/status", status);
+84
apps/server/src/public/status.ts
··· 1 + import { Hono } from "hono"; 2 + 3 + import { db, eq } from "@openstatus/db"; 4 + import { monitor, monitorsToPages, page } from "@openstatus/db/src/schema"; 5 + import { getMonitorList, Tinybird } from "@openstatus/tinybird"; 6 + import { Redis } from "@openstatus/upstash"; 7 + 8 + // TODO: include ratelimiting 9 + 10 + const tb = new Tinybird({ token: process.env.TINY_BIRD_API_KEY || "" }); 11 + const redis = Redis.fromEnv(); 12 + 13 + enum Status { 14 + Operational = "operational", 15 + DegradedPerformance = "degraded_performance", 16 + PartialOutage = "partial_outage", 17 + MajorOutage = "major_outage", 18 + UnderMaintenance = "under_maintenance", 19 + Unknown = "unknown", 20 + } 21 + 22 + export const status = new Hono(); 23 + 24 + status.get("/:slug", async (c) => { 25 + const { slug } = c.req.param(); 26 + 27 + const cache = await redis.get(slug); 28 + if (cache) return c.json({ status: cache }); 29 + 30 + // { monitors, pages, monitors_to_pages } 31 + const monitorData = await db 32 + .select() 33 + .from(monitorsToPages) 34 + .leftJoin(monitor, eq(monitorsToPages.monitorId, monitor.id)) 35 + .leftJoin(page, eq(monitorsToPages.pageId, page.id)) 36 + .where(eq(page.slug, slug)) 37 + .all(); 38 + 39 + // { data: [{ ok, count }] } 40 + const lastMonitorPings = await Promise.allSettled( 41 + monitorData.map(async ({ monitors_to_pages }) => { 42 + return await getMonitorList(tb)({ 43 + monitorId: String(monitors_to_pages.monitorId), 44 + limit: 3, // limits the grouped cronTimestamps 45 + }); 46 + }), 47 + ); 48 + 49 + // { ok, count } 50 + const data = lastMonitorPings.reduce( 51 + (prev, curr) => { 52 + if (curr.status === "fulfilled") { 53 + const { ok, count } = curr.value.data.reduce( 54 + (p, c) => { 55 + p.ok += c.ok; 56 + p.count += c.count; 57 + return p; 58 + }, 59 + { ok: 0, count: 0 }, 60 + ); 61 + prev.ok += ok; 62 + prev.count += count; 63 + } 64 + return prev; 65 + }, 66 + { ok: 0, count: 0 }, 67 + ); 68 + 69 + const ratio = data.ok / data.count; 70 + 71 + const status = getStatus(ratio); 72 + await redis.set(slug, status, { ex: 30 }); 73 + 74 + return c.json({ status }); 75 + }); 76 + 77 + function getStatus(ratio: number) { 78 + if (isNaN(ratio)) return Status.Unknown; 79 + if (ratio >= 0.98) return Status.Operational; 80 + if (ratio >= 0.6) return Status.DegradedPerformance; 81 + if (ratio >= 0.3) return Status.PartialOutage; 82 + if (ratio >= 0) return Status.MajorOutage; 83 + return Status.Unknown; 84 + }
apps/server/src/shared.ts apps/server/src/v1/shared.ts
+27
apps/server/src/v1/index.ts
··· 1 + import { OpenAPIHono } from "@hono/zod-openapi"; 2 + 3 + import { incidentApi } from "./incident"; 4 + import { middleware } from "./middleware"; 5 + import { monitorApi } from "./monitor"; 6 + 7 + export type Variables = { 8 + workspaceId: string; 9 + }; 10 + 11 + export const api = new OpenAPIHono<{ Variables: Variables }>(); 12 + 13 + api.doc("/openapi", { 14 + openapi: "3.0.0", 15 + info: { 16 + version: "1.0.0", 17 + title: "OpenStatus API", 18 + }, 19 + }); 20 + 21 + /** 22 + * Authentification Middleware 23 + */ 24 + api.use("/*", middleware); 25 + 26 + api.route("/monitor", monitorApi); 27 + api.route("/incident", incidentApi);
bun.lockb

This is a binary file and will not be displayed.

+3
pnpm-lock.yaml
··· 81 81 '@openstatus/tinybird': 82 82 specifier: workspace:* 83 83 version: link:../../packages/tinybird 84 + '@openstatus/upstash': 85 + specifier: workspace:* 86 + version: link:../../packages/upstash 84 87 '@unkey/api': 85 88 specifier: 0.6.22 86 89 version: 0.6.22