Repro
bun -e "import { createLogger } from '@tetratelabs/logging'; const l = createLogger({ level: 'debug' }); l.debug('debug-visible'); l.info('info-visible');"
Output on 0.3.1:
{"level":30,"time":...,"msg":"info-visible"}
Expected: both debug-visible and info-visible lines.
Root cause
src/sink-pino.ts constructs the pino instance with opts?.pino ?? {} and never threads in opts.level. Pino then defaults to its own info floor. The wrapper-level filter in LoggerImpl lets debug calls through, but the underlying pino instance drops them at the sink.
The GCP sink writes via console.log so it is unaffected. Only the default Node pino path is broken — i.e. dev and tests.
Secondary issue
LOG_LEVEL=trace (a valid pino level) is not in our Level union. The cast process.env.LOG_LEVEL as Level ends up as a value missing from levelRanks, and shouldLog evaluates undefined >= N as false for every call — silencing the logger entirely, including errors. Callers porting from pino hit this and assume the library is broken.
Fix plan
- Thread
opts.level into the pino sink so debug calls reach the default stream. Explicit opts.pino.level keeps winning.
- Export
parseLevel so consumers can validate process.env.LOG_LEVEL without an unsafe cast.
- Accept
trace and fatal as aliases in parseLevel (map to debug and error) so pino habits do not produce surprise silence.
Found while reviewing tetratelabs/fraser-auth#170.
Repro
bun -e "import { createLogger } from '@tetratelabs/logging'; const l = createLogger({ level: 'debug' }); l.debug('debug-visible'); l.info('info-visible');"Output on 0.3.1:
Expected: both
debug-visibleandinfo-visiblelines.Root cause
src/sink-pino.tsconstructs the pino instance withopts?.pino ?? {}and never threads inopts.level. Pino then defaults to its owninfofloor. The wrapper-level filter inLoggerImpllets debug calls through, but the underlying pino instance drops them at the sink.The GCP sink writes via
console.logso it is unaffected. Only the default Node pino path is broken — i.e. dev and tests.Secondary issue
LOG_LEVEL=trace(a valid pino level) is not in ourLevelunion. The castprocess.env.LOG_LEVEL as Levelends up as a value missing fromlevelRanks, andshouldLogevaluatesundefined >= Nas false for every call — silencing the logger entirely, including errors. Callers porting from pino hit this and assume the library is broken.Fix plan
opts.levelinto the pino sink so debug calls reach the default stream. Explicitopts.pino.levelkeeps winning.parseLevelso consumers can validateprocess.env.LOG_LEVELwithout an unsafe cast.traceandfatalas aliases inparseLevel(map todebuganderror) so pino habits do not produce surprise silence.Found while reviewing tetratelabs/fraser-auth#170.