Openstatus
www.openstatus.dev
1import { AsyncLocalStorage } from "node:async_hooks";
2
3import { sentry } from "@hono/sentry";
4import {
5 configure,
6 // configureSync,
7 getConsoleSink,
8 getLogger,
9 jsonLinesFormatter,
10 withContext,
11} from "@logtape/logtape";
12import { getOpenTelemetrySink } from "@logtape/otel";
13import { Hono } from "hono";
14import { showRoutes } from "hono/dev";
15
16import { resourceFromAttributes } from "@opentelemetry/resources";
17import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from "@opentelemetry/semantic-conventions/incubating";
18import { prettyJSON } from "hono/pretty-json";
19import { requestId } from "hono/request-id";
20import { env } from "./env";
21import { handleError } from "./libs/errors";
22import { publicRoute } from "./routes/public";
23import { api } from "./routes/v1";
24
25type Env = {
26 Variables: {
27 event: Record<string, unknown>;
28 };
29};
30
31/* biome-ignore lint/suspicious/noExplicitAny: <explanation> */
32function shouldSample(event: Record<string, any>): boolean {
33 // Always keep errors
34 if (event.status_code >= 500) return true;
35 if (event.error) return true;
36
37 // Always keep slow requests (above p99)
38 if (event.duration_ms > 2000) return true;
39
40 // Random sample the rest at 20%
41 return Math.random() < 0.2;
42}
43
44const defaultLogger = getOpenTelemetrySink({
45 serviceName: "openstatus-server",
46 otlpExporterConfig: {
47 url: "https://eu-central-1.aws.edge.axiom.co/v1/logs",
48 headers: {
49 Authorization: `Bearer ${env.AXIOM_TOKEN}`,
50 "X-Axiom-Dataset": env.AXIOM_DATASET,
51 },
52 },
53 additionalResource: resourceFromAttributes({
54 [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: env.NODE_ENV,
55 }),
56});
57
58await configure({
59 sinks: {
60 console: getConsoleSink({ formatter: jsonLinesFormatter }),
61
62 otel: defaultLogger,
63 },
64 loggers: [
65 {
66 category: "api-server",
67 lowestLevel: "error",
68 sinks: ["console"],
69 },
70 {
71 category: "api-server-otel",
72 lowestLevel: "info",
73 sinks: ["otel"],
74 },
75 ],
76 contextLocalStorage: new AsyncLocalStorage(),
77});
78
79const logger = getLogger("api-server");
80
81const otelLogger = getLogger("api-server-otel");
82
83export const app = new Hono<Env>({
84 strict: false,
85});
86
87/**
88 * Middleware
89 */
90app.use("*", sentry({ dsn: process.env.SENTRY_DSN }));
91app.use("*", requestId());
92app.use("*", prettyJSON());
93
94app.use("*", async (c, next) => {
95 const requestId = c.get("requestId");
96 const startTime = Date.now();
97
98 await withContext(
99 {
100 request_id: requestId,
101 method: c.req.method,
102 url: c.req.url,
103 user_agent: c.req.header("User-Agent"),
104 },
105 async () => {
106 // Initialize wide event - one canonical log line per request
107 const event: Record<string, unknown> = {
108 timestamp: new Date().toISOString(),
109 request_id: requestId,
110 // Request context
111 method: c.req.method,
112 path: c.req.path,
113 url: c.req.url,
114 // Client context
115 user_agent: c.req.header("User-Agent"),
116 // Request metadata
117 content_type: c.req.header("Content-Type"),
118 };
119 c.set("event", event);
120
121 await next();
122
123 // Performance
124 const duration = Date.now() - startTime;
125 event.duration_ms = duration;
126
127 // Response context
128 event.status_code = c.res.status;
129
130 // Outcome
131 if (c.error) {
132 event.outcome = "error";
133 event.error = {
134 type: c.error.name,
135 message: c.error.message,
136 stack: c.error.stack,
137 };
138 } else {
139 event.outcome = c.res.status < 400 ? "success" : "failure";
140 }
141
142 // Emit single canonical log line (sampled for otel, always for console in dev)
143 if (shouldSample(event)) {
144 otelLogger.info("request", { ...event });
145 }
146
147 // Console logging only for errors in production
148 if (env.NODE_ENV !== "production" || c.res.status >= 500) {
149 logger.info("request", {
150 request_id: requestId,
151 method: c.req.method,
152 path: c.req.path,
153 status_code: c.res.status,
154 duration_ms: duration,
155 outcome: event.outcome,
156 });
157 }
158 },
159 );
160});
161
162app.onError(handleError);
163
164/**
165 * Public Routes
166 */
167app.route("/public", publicRoute);
168
169/**
170 * Ping Pong
171 */
172app.get("/ping", (c) => {
173 return c.json(
174 { ping: "pong", region: env.FLY_REGION, requestId: c.get("requestId") },
175 200,
176 );
177});
178
179/**
180 * API Routes v1
181 */
182app.route("/v1", api);
183
184/**
185 * TODO: move to `workflows` app
186 * This route is used by our checker to update the status of the monitors,
187 * create incidents, and send notifications.
188 */
189
190const isDev = process.env.NODE_ENV === "development";
191const port = 3000;
192
193if (isDev) showRoutes(app, { verbose: true, colorize: true });
194
195console.log(`Starting server on port ${port}`);
196
197const server = { port, fetch: app.fetch };
198
199export default server;