feat(soak): structured JSONL logger with child contexts

Single file, no transport, writes one JSON line per call to stdout.
Child loggers inherit parent meta so scenarios can bind room/game
context once and forget about it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-11 17:12:27 -04:00
parent 02642840da
commit 066e482f06
2 changed files with 111 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createLogger } from '../core/logger';
describe('logger', () => {
let writes: string[];
let write: (s: string) => boolean;
beforeEach(() => {
writes = [];
write = (s: string) => {
writes.push(s);
return true;
};
});
it('emits a JSON line per call with level and msg', () => {
const log = createLogger({ runId: 'r1', write });
log.info('hello');
expect(writes).toHaveLength(1);
const parsed = JSON.parse(writes[0]);
expect(parsed.level).toBe('info');
expect(parsed.msg).toBe('hello');
expect(parsed.runId).toBe('r1');
expect(parsed.timestamp).toBeTypeOf('string');
});
it('merges meta into the log line', () => {
const log = createLogger({ runId: 'r1', write });
log.warn('slow', { turnMs: 3000 });
const parsed = JSON.parse(writes[0]);
expect(parsed.turnMs).toBe(3000);
expect(parsed.level).toBe('warn');
});
it('child logger inherits parent meta', () => {
const log = createLogger({ runId: 'r1', write });
const roomLog = log.child({ room: 'room-1' });
roomLog.info('game_start');
const parsed = JSON.parse(writes[0]);
expect(parsed.room).toBe('room-1');
expect(parsed.runId).toBe('r1');
});
it('respects minimum level', () => {
const log = createLogger({ runId: 'r1', write, minLevel: 'warn' });
log.debug('nope');
log.info('nope');
log.warn('yes');
log.error('yes');
expect(writes).toHaveLength(2);
});
});