Put the caller, site and route on every request's span and log line
CI / full-gate (push) Successful in 3m23s

This commit is contained in:
2026-11-08 14:59:55 +01:00
parent 996eef2dc5
commit a69593eb72
14 changed files with 275 additions and 68 deletions
+42
View File
@@ -0,0 +1,42 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { type Log, requestLogger, runWithLog } from "../logger.ts";
import { requestFields, type TrustedProxies } from "./request-fields.ts";
// Gives one request its own logger and span, runs `handle` under it, and writes the `request` line
// when the response closes. The handler adds `http.route` to the logger's context once it knows it.
export function trackRequest(
log: Log,
trusted: TrustedProxies,
req: IncomingMessage,
res: ServerResponse,
handle: (req: IncomingMessage, res: ServerResponse, reqLog: Log) => Promise<void>,
): void {
// "close" (not "finish") fires on both a completed response and a premature disconnect, so an
// aborted request is still logged and its span flushed.
const reqLog = requestLogger(log, {
attributes: requestFields(req, trusted),
requestId: randomUUID(),
...(typeof req.headers.traceparent === "string" ? { traceparent: req.headers.traceparent } : {}),
});
// end() must run exactly once, after BOTH the handler has unwound AND the response has closed.
// Earlier would throw "already ended" from a still-running handler's ctx.log on a client abort,
// or drop the access line on the happy path (the handler settles before close).
let settled = false;
let closed = false;
const finalize = (): void => { if (settled && closed) void reqLog.end().catch(() => {}); };
res.on("close", () => {
closed = true;
try {
// The context is what the span exports, so the status goes there; the span's duration is the latency.
reqLog.context["http.response.status_code"] = res.statusCode;
reqLog.info("request");
} catch { /* never let logging crash a served request */ }
finalize();
});
// Make reqLog ambient for the whole handler so all outbound fetch is traced. The .catch logs a
// pathological escape via the app logger — not reqLog, which may be the thing that broke.
void runWithLog(reqLog, () => handle(req, res, reqLog))
.catch((err) => log.error("request handler escaped its try/catch", { error: err instanceof Error ? (err.stack ?? err.message) : String(err) }))
.finally(() => { settled = true; finalize(); });
}