pinojs/pino · warning
PINO_TRANSPORT_MULTI_TARGETS_LEVEL_FORMATTER
PINO_TRANSPORT_MULTI_TARGETS_LEVEL_FORMATTER
Error message
custom formatters.level detected with multiple transport targets/pipelines; ensure logs include a numeric "level" field or they may be dropped
What it means
This is a process warning (not a throw) emitted by pino() (pino.js:188, code PINO_TRANSPORT_MULTI_TARGETS_LEVEL_FORMATTER). When multiple transport targets are used and a custom formatters.level does not output a numeric level field, worker-thread transports may drop logs because they cannot compare non-numeric levels against their configured thresholds.
Source
Thrown at pino.js:188
if (stream && stream[transportUsesMultistreamSym] === true) {
let sampleLabel = typeof level === 'string' ? level : undefined
if (!sampleLabel || levels[sampleLabel] === undefined) {
sampleLabel = Object.keys(levels)[0]
}
const sampleNumber = levels[sampleLabel]
let ok = false
try {
const formatted = formatters.level(sampleLabel, sampleNumber)
ok = formatted && typeof formatted === 'object' && formatted.level === sampleNumber
} catch {
ok = false
}
if (!ok) {
process.emitWarning(
'custom formatters.level detected with multiple transport targets/pipelines; ensure logs include a numeric "level" field or they may be dropped',
{ code: 'PINO_TRANSPORT_MULTI_TARGETS_LEVEL_FORMATTER' }
)
}
}
if (typeof stream.emit === 'function') {
stream.emit('message', { code: 'PINO_CONFIG', config: { levels, messageKey, errorKey } })
}
assertLevelComparison(levelComparison)
const levelCompFunc = genLevelComparison(levelComparison)
Object.assign(instance, {
levels,
[levelCompSym]: levelCompFunc,
[useOnlyCustomLevelsSym]: useOnlyCustomLevels,
[streamSym]: stream,
[timeSym]: time,
[timeSliceIndexSym]: timeSliceIndex,View on GitHub (pinned to 5aa62305c5)
Solutions
- Make formatters.level include the numeric level: (label, number) => ({ level: number })
- Return both fields, e.g. { level: number, levelLabel: label }
- Restructure to a single target with a pipeline that adds the label at the presentation layer (e.g. pino-pretty)
Example fix
// before
formatters: { level: (label) => ({ level: label }) }
// after
formatters: { level: (label, number) => ({ level: number, levelLabel: label }) } Defensive patterns
Strategy: validation
Validate before calling
const hasMultiTargets = !!(opts.transport && (Array.isArray(opts.transport.targets) || opts.transport.target));
if (hasMultiTargets && opts.formatters && typeof opts.formatters.level === 'function') {
// ensure the formatter emits a numeric level
} Type guard
function emitsNumericLevel(formatter) {
const out = formatter('info', 30);
return typeof out.level === 'number';
} Try / catch
process.on('warning', (w) => { if (w.code === 'PINO_TRANSPORT_MULTI_TARGETS_LEVEL_FORMATTER') { /* fix formatters.level to emit numeric level */ } }); Prevention
- Keep formatters.level returning { level: number } and add labels at the transport/pretty layer
- Test logger output shape whenever combining formatters with transports
- Listen for process warnings in CI to catch this during builds
When it happens
Trigger: pino({ transport: { targets: [...] }, formatters: { level: (label, number) => ({ level: label }) } }) — custom level formatter returning a string label while multiple targets/pipelines are configured.
Common situations: Customizing level output to human-readable labels while also adding multiple destinations (file + pretty console); upgrading to multi-target transports without revisiting legacy formatters.
Related errors
- only one of target or targets can be specified
- unable to determine transport target for "${origin}"
- option.transport.targets do not allow custom level formatter
- missing bindings for child Pino
- Levels comparison should be one of "ASC", "DESC" or "functio
AI-assisted analysis of pinojs/pino@5aa62305c5 (2026-09-02).
Data as JSON: /api/errors/66c95e9b577120da.
Report an issue: GitHub.