/** * Structured JSONL logger for the soak harness. * * One JSON line per call, written to stdout by default. Child loggers * inherit parent meta so scenarios can bind room/game context once and * every subsequent call carries it automatically. */ import type { Logger, LogLevel } from './types'; const LEVEL_ORDER: Record = { debug: 0, info: 1, warn: 2, error: 3, }; export interface LoggerOptions { runId: string; minLevel?: LogLevel; /** Defaults to process.stdout.write bound to stdout. Override for tests. */ write?: (line: string) => boolean; baseMeta?: Record; } export function createLogger(opts: LoggerOptions): Logger { const minLevel = opts.minLevel ?? 'info'; const write = opts.write ?? ((s: string) => process.stdout.write(s)); const baseMeta = opts.baseMeta ?? {}; function emit(level: LogLevel, msg: string, meta?: object): void { if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel]) return; const line = JSON.stringify({ timestamp: new Date().toISOString(), level, msg, runId: opts.runId, ...baseMeta, ...(meta ?? {}), }) + '\n'; write(line); } const logger: Logger = { debug: (msg, meta) => emit('debug', msg, meta), info: (msg, meta) => emit('info', msg, meta), warn: (msg, meta) => emit('warn', msg, meta), error: (msg, meta) => emit('error', msg, meta), child: (meta) => createLogger({ runId: opts.runId, minLevel, write, baseMeta: { ...baseMeta, ...meta }, }), }; return logger; }