/** * Structured logger for API handlers. * * Why this exists: `console.log` everywhere in 159 route files is impossible * to grep through. With a request-scoped `turnId` (or any other correlation * id), prod incidents go from "search by guess" to "ripgrep one ID, get the * whole timeline." * * Output shape (single line, JSON, prod-friendly): * {"ts":"2026-05-17T20:00:00.000Z","level":"info","route":"api.chat", * "turnId":"…","projectId":"…","user":"mark@…","msg":"…","ctx":{…}} * * In dev, we prefix with a coloured tag and pretty-print `ctx` for eyeball- * ability. In prod, single-line JSON so a log shipper can parse it. */ export type LogLevel = "debug" | "info" | "warn" | "error"; export interface LogContext { route?: string; turnId?: string; projectId?: string; workspaceSlug?: string; user?: string; userId?: string; [k: string]: unknown; } const isDev = process.env.NODE_ENV !== "production"; function emit(level: LogLevel, msg: string, ctx: LogContext = {}) { const ts = new Date().toISOString(); const { route, turnId, projectId, workspaceSlug, user, userId, ...rest } = ctx; const base = { ts, level, msg, route, turnId, projectId, workspaceSlug, user, userId }; if (isDev) { const tag = level === "error" ? "✗" : level === "warn" ? "!" : level === "debug" ? "·" : "→"; const idStr = turnId ? ` [${turnId.slice(0, 8)}]` : ""; const routeStr = route ? ` ${route}` : ""; const extra = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : ""; // eslint-disable-next-line no-console console[level === "debug" ? "log" : level](`${tag}${routeStr}${idStr} ${msg}${extra}`); return; } const line = JSON.stringify({ ...base, ...rest }); // eslint-disable-next-line no-console (console[level === "debug" ? "log" : level] as (m: string) => void)(line); } /** * Create a logger pre-bound to a request context. Pass through `info()` / * `warn()` / `error()` / `debug()` and every line carries the same * `turnId` / `projectId` / `route`. */ export function makeLogger(base: LogContext) { return { debug: (msg: string, ctx: LogContext = {}) => emit("debug", msg, { ...base, ...ctx }), info: (msg: string, ctx: LogContext = {}) => emit("info", msg, { ...base, ...ctx }), warn: (msg: string, ctx: LogContext = {}) => emit("warn", msg, { ...base, ...ctx }), error: (msg: string, ctx: LogContext = {}) => emit("error", msg, { ...base, ...ctx }), child(extra: LogContext) { return makeLogger({ ...base, ...extra }); }, }; } export const log = { debug: (msg: string, ctx: LogContext = {}) => emit("debug", msg, ctx), info: (msg: string, ctx: LogContext = {}) => emit("info", msg, ctx), warn: (msg: string, ctx: LogContext = {}) => emit("warn", msg, ctx), error: (msg: string, ctx: LogContext = {}) => emit("error", msg, ctx), };