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

  1. Make formatters.level include the numeric level: (label, number) => ({ level: number })
  2. Return both fields, e.g. { level: number, levelLabel: label }
  3. 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

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


AI-assisted analysis of pinojs/pino@5aa62305c5 (2026-09-02). Data as JSON: /api/errors/66c95e9b577120da. Report an issue: GitHub.