63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
/**
|
|
* Structured request logging middleware. Emits one record per request with a
|
|
* request id, method, path, status, and duration — as pretty text (dev) or JSON
|
|
* (production/log aggregation). The request id is stored on `ctx.locals` so
|
|
* downstream handlers can correlate their own logs.
|
|
*/
|
|
|
|
import type { Context, Middleware } from "./context.ts";
|
|
|
|
export interface RequestRecord {
|
|
time: string;
|
|
id: string;
|
|
method: string;
|
|
path: string;
|
|
status: number;
|
|
durationMs: number;
|
|
}
|
|
|
|
export interface RequestLoggerOptions {
|
|
/** "pretty" (default) for humans, "json" for machines. */
|
|
format?: "pretty" | "json";
|
|
/** Where each finished record goes. Default console.log. */
|
|
sink?: (line: string, record: RequestRecord) => void;
|
|
/** ctx.locals key for the request id. Default "requestId". */
|
|
requestIdKey?: string;
|
|
/** Clock injection for tests. Default Date.now. */
|
|
now?: () => number;
|
|
}
|
|
|
|
export function requestLogger(options: RequestLoggerOptions = {}): Middleware {
|
|
const format = options.format ?? "pretty";
|
|
const sink = options.sink ?? ((line) => console.log(line));
|
|
const idKey = options.requestIdKey ?? "requestId";
|
|
const now = options.now ?? Date.now;
|
|
|
|
return async (ctx: Context, next) => {
|
|
const start = now();
|
|
const id = (ctx.locals[idKey] as string | undefined) ?? crypto.randomUUID();
|
|
ctx.locals[idKey] = id;
|
|
|
|
let status = 500;
|
|
try {
|
|
const res = await next();
|
|
status = res.status;
|
|
return res;
|
|
} finally {
|
|
const record: RequestRecord = {
|
|
time: new Date(start).toISOString(),
|
|
id,
|
|
method: ctx.req.method,
|
|
path: ctx.url.pathname,
|
|
status,
|
|
durationMs: now() - start,
|
|
};
|
|
sink(format === "json" ? JSON.stringify(record) : formatPretty(record), record);
|
|
}
|
|
};
|
|
}
|
|
|
|
function formatPretty(r: RequestRecord): string {
|
|
return `${r.method} ${r.path} → ${r.status} ${r.durationMs}ms [${r.id.slice(0, 8)}]`;
|
|
}
|