/** Structured logger: one JSON line per record on stderr, human line on stdout when SRC_LOG_PRETTY=1. */ export type LogLevel = "debug" | "info" | "warn" | "error"; const LEVELS: Record = { debug: 10, info: 20, warn: 30, error: 40 }; const minLevel = LEVELS[(process.env.SRC_LOG_LEVEL as LogLevel) ?? "info"] ?? 20; const pretty = process.env.SRC_LOG_PRETTY !== "0"; export interface Logger { debug(msg: string, data?: Record): void; info(msg: string, data?: Record): void; warn(msg: string, data?: Record): void; error(msg: string, data?: Record): void; child(bindings: Record): Logger; } function emit(level: LogLevel, scope: string, bindings: Record, msg: string, data?: Record) { if (LEVELS[level] < minLevel) return; const rec = { t: new Date().toISOString(), level, scope, msg, ...bindings, ...(data ?? {}) }; if (pretty) { const extra = { ...bindings, ...(data ?? {}) }; const tail = Object.keys(extra).length ? " " + JSON.stringify(extra) : ""; const line = `${rec.t.slice(11, 19)} ${level.toUpperCase().padEnd(5)} [${scope}] ${msg}${tail}`; (level === "error" || level === "warn" ? process.stderr : process.stdout).write(line + "\n"); } else { process.stderr.write(JSON.stringify(rec) + "\n"); } } export function createLogger(scope: string, bindings: Record = {}): Logger { return { debug: (m, d) => emit("debug", scope, bindings, m, d), info: (m, d) => emit("info", scope, bindings, m, d), warn: (m, d) => emit("warn", scope, bindings, m, d), error: (m, d) => emit("error", scope, bindings, m, d), child: (b) => createLogger(scope, { ...bindings, ...b }), }; }