Add consistent API request logging and a themed 404 page, bump to 0.4.0
CI / Build and push image (push) Successful in 1m10s
CI / Static checks (push) Successful in 1m39s

Every app/api route handler now goes through withRouteLogging, which logs
a correlated reqId/method/path/status/duration for each request and turns
any uncaught error into a logged stack trace plus a clean JSON 500 instead
of Next's default opaque failure. proxy.ts logs rejected auth attempts, and
instrumentation.ts's onRequestError catches anything that still escapes a
route handler. Also adds a minimal not-found page matching the app's card
styling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 08:45:15 +02:00
co-authored by Claude Sonnet 5
parent 5484d3cefe
commit 9e0380ec75
20 changed files with 267 additions and 109 deletions
+43
View File
@@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { randomUUID } from "node:crypto";
import { createLogger } from "@/lib/logger";
const log = createLogger("api");
type RouteHandler<Ctx> = (req: Request, ctx: Ctx) => Promise<Response> | Response;
/**
* Wraps an App Router route handler with consistent request/response logging:
* a reqId (also echoed as `x-request-id`), method/path, status, and duration on
* every call, plus a logged stack trace and JSON 500 for anything the handler
* doesn't catch itself. Every export in app/api/** goes through this so log
* shape and error handling stay uniform across routes instead of being
* reimplemented per file.
*/
export function withRouteLogging<Ctx = unknown>(routeName: string, handler: RouteHandler<Ctx>): RouteHandler<Ctx> {
return async (req, ctx) => {
const reqId = randomUUID();
const start = performance.now();
const { pathname, search } = new URL(req.url);
const reqLog = log.child({ reqId, route: routeName, method: req.method });
reqLog.debug("request start", { path: pathname + search });
try {
const res = await handler(req, ctx);
const durationMs = Math.round(performance.now() - start);
const level = res.status >= 500 ? "error" : res.status >= 400 ? "warn" : "info";
reqLog[level]("request end", { status: res.status, durationMs });
res.headers.set("x-request-id", reqId);
return res;
} catch (err) {
const durationMs = Math.round(performance.now() - start);
reqLog.error("request failed", {
durationMs,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
return NextResponse.json({ error: "internal error", reqId }, { status: 500 });
}
};
}
+12 -5
View File
@@ -34,14 +34,21 @@ export interface Logger {
info(message: string, fields?: LogFields): void;
warn(message: string, fields?: LogFields): void;
error(message: string, fields?: LogFields): void;
/** Returns a logger that merges `bindings` into every call's fields - for attaching
* per-request context (reqId, route) without threading it through every log call. */
child(bindings: LogFields): Logger;
}
/** Scoped logger. `scope` is the bracketed tag, e.g. createLogger("api") -> "[api]". */
export function createLogger(scope: string): Logger {
export function createLogger(scope: string, bindings?: LogFields): Logger {
const log = (level: LogLevel, message: string, fields?: LogFields) =>
write(level, scope, message, bindings || fields ? { ...bindings, ...fields } : undefined);
return {
debug: (message, fields) => write("debug", scope, message, fields),
info: (message, fields) => write("info", scope, message, fields),
warn: (message, fields) => write("warn", scope, message, fields),
error: (message, fields) => write("error", scope, message, fields),
debug: (message, fields) => log("debug", message, fields),
info: (message, fields) => log("info", message, fields),
warn: (message, fields) => log("warn", message, fields),
error: (message, fields) => log("error", message, fields),
child: (extra) => createLogger(scope, { ...bindings, ...extra }),
};
}