Files
WRNexusJS/packages/core/src/logging.ts
T
2026-07-12 15:55:18 +05:30

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)}]`;
}