diff options
author | Alan Pearce | 2023-09-17 17:31:18 +0200 |
---|---|---|
committer | Alan Pearce | 2023-09-17 17:31:18 +0200 |
commit | 602f249c2cfac0e7b6613fb63f5fb519aa1ca952 (patch) | |
tree | be522208e3172e62b4777a66af4bd931677f7fcb /src/index.ts | |
parent | 1a7abb3723d6b9db0d199c26d2a207e03636738a (diff) | |
download | website-602f249c2cfac0e7b6613fb63f5fb519aa1ca952.tar.lz website-602f249c2cfac0e7b6613fb63f5fb519aa1ca952.tar.zst website-602f249c2cfac0e7b6613fb63f5fb519aa1ca952.zip |
Move servers into app.ts and export for testing
Diffstat (limited to 'src/index.ts')
-rw-r--r-- | src/index.ts | 234 |
1 files changed, 5 insertions, 229 deletions
diff --git a/src/index.ts b/src/index.ts index 450fe8f..83ad03d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,231 +1,7 @@ -import path from "node:path"; -import fs, { Stats } from "node:fs"; -import type { BunFile, Serve } from "bun"; -import * as Sentry from "@sentry/node"; -import prom from "bun-prometheus-client"; +import { server, metricsServer } from "./app"; -import readConfig from "./config"; +const metricsServed = Bun.serve(metricsServer); +console.info(`Metrics server started on port ${metricsServed.port}`); -Sentry.init({ - release: `homestead@${Bun.env.FLY_MACHINE_VERSION}`, - tracesSampleRate: 1.0, -}); - -const base = "."; -const publicDir = path.resolve(base, "public") + path.sep; - -const config = readConfig(base); -const defaultHeaders = { - ...config.extra.headers, - vary: "Accept-Encoding", -}; - -type File = { - filename: string; - handle: BunFile; - relPath: string; - headers?: Record<string, string>; - type: string; - size: number; - mtime: Date; -}; - -const metrics = { - requests: new prom.Counter({ - name: "homestead_requests", - help: "Number of requests by path, status code, and method", - labelNames: ["path", "status_code", "method"], - }), - requestDuration: new prom.Histogram({ - name: "homestead_request_duration_seconds", - help: "Request duration in seconds", - labelNames: ["path"], - }), -}; - -let files = new Map<string, File>(); - -function registerFile( - path: string, - pathname: string, - filename: string, - stat: Stats, -): void { - pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); - - if (files.get(pathname) !== undefined) { - console.warn("File already registered:", pathname); - } - const handle = Bun.file(filename); - files.set(pathname, { - filename, - relPath: "/" + path, - handle: handle, - type: pathname.startsWith("/feed-styles.xsl") ? "text/xsl" : handle.type, - headers: - pathname === "/404.html" - ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) - : undefined, - size: stat.size, - mtime: stat.mtime, - }); -} - -function walkDirectory(root: string, dir: string) { - const absDir = path.join(root, dir); - for (let pathname of fs.readdirSync(absDir)) { - const relPath = path.join(dir, pathname); - const absPath = path.join(absDir, pathname); - const stat = fs.statSync(absPath); - if (stat.isDirectory()) { - walkDirectory(root, relPath + path.sep); - } else if (stat.isFile()) { - if (pathname.startsWith("index.html")) { - const dir = relPath.replace("index.html", ""); - registerFile(relPath, dir, absPath, stat); - if (dir !== "") { - registerFile(relPath, dir + path.sep, absPath, stat); - } - } - registerFile(relPath, relPath, absPath, stat); - } - } -} - -walkDirectory(publicDir, ""); - -async function serveFile( - file: File | undefined, - statusCode: number = 200, - extraHeaders: Record<string, string> = {}, -): Promise<Response> { - return new Response(await file.handle.arrayBuffer(), { - headers: { - "last-modified": file.mtime.toUTCString(), - ...extraHeaders, - ...(file.headers || defaultHeaders), - }, - status: statusCode, - }); -} - -function parseIfModifiedSinceHeader(header: string | null): number { - return header ? new Date(header).getTime() + 999 : 0; -} - -const metricsServer = Bun.serve({ - port: 9091, - fetch: async function (request) { - const pathname = new URL(request.url).pathname; - switch (pathname) { - case "/metrics": - return new Response(await prom.register.metrics()); - default: - return new Response("", { status: 404 }); - } - }, -}); - -console.info( - `Serving metrics on http://${metricsServer.hostname}:${metricsServer.port}/metrics`, -); - -const server = Bun.serve({ - fetch: async function (request) { - const pathname = new URL(request.url).pathname.replace(/\/\/+/g, "/"); - const endTimer = metrics.requestDuration.startTimer({ path: pathname }); - const transaction = Sentry.startTransaction({ - name: pathname, - op: "http.server", - description: `${request.method} ${pathname}`, - tags: { - url: request.url, - "http.method": request.method, - "http.user_agent": request.headers.get("user-agent"), - }, - }); - try { - const file = files.get(pathname); - metrics.requests.inc({ path: pathname }); - if (file && (await file.handle.exists())) { - if ( - parseIfModifiedSinceHeader( - request.headers.get("if-modified-since"), - ) >= file?.mtime.getTime() - ) { - metrics.requests.inc({ - method: request.method, - path: pathname, - status_code: 304, - }); - transaction.setHttpStatus(304); - return new Response("", { status: 304, headers: defaultHeaders }); - } - metrics.requests.inc({ - method: request.method, - path: pathname, - status_code: 200, - }); - const encodings = (request.headers.get("accept-encoding") || "") - .split(",") - .map((x) => x.trim().toLowerCase()); - if (encodings.includes("br") && files.has(file.relPath + ".br")) { - transaction.setHttpStatus(200); - transaction.setTag("http.content-encoding", "br"); - return serveFile(files.get(file.relPath + ".br"), 200, { - "content-encoding": "br", - "content-type": file.type, - }); - } else if ( - encodings.includes("zstd") && - files.has(file.relPath + ".zst") - ) { - transaction.setHttpStatus(200); - transaction.setTag("http.content-encoding", "zstd"); - return serveFile(files.get(file.relPath + ".zst"), 200, { - "content-encoding": "zstd", - "content-type": file.type, - }); - } else if ( - encodings.includes("gzip") && - files.has(file.relPath + ".gz") - ) { - transaction.setHttpStatus(200); - transaction.setTag("http.content-encoding", "gzip"); - return serveFile(files.get(file.relPath + ".gz"), 200, { - "content-encoding": "gzip", - "content-type": file.type, - }); - } - transaction.setHttpStatus(200); - transaction.setTag("http.content-encoding", "identity"); - return serveFile(file); - } else { - metrics.requests.inc({ - method: request.method, - path: pathname, - status_code: 404, - }); - transaction.setHttpStatus(404); - transaction.setTag("http.content-encoding", "identity"); - return serveFile(files.get("/404.html"), 404); - } - } catch (error) { - transaction.setTag("http.content-encoding", "identity"); - transaction.setHttpStatus(503); - metrics.requests.inc({ - method: request.method, - path: pathname, - status_code: 503, - }); - Sentry.captureException(error); - return new Response("Something went wrong", { status: 503 }); - } finally { - const seconds = endTimer(); - metrics.requestDuration.observe(seconds); - transaction.finish(); - } - }, -}); - -console.info(`Serving website on http://${server.hostname}:${server.port}/`); +const served = Bun.serve(server); +console.info(`Serving website on http://${served.hostname}:${served.port}/`); |