DEV Community

Victor A. Barzana
Victor A. Barzana

Posted on

Enhancing Pino to Support (Message, Payload) Logging Without Breaking Pretty Logs

Introduction

Pino is a blazing-fast logging library for Node.js, built for structured logs and high performance. However, one common frustration developers face is that Pino does not natively support logging a message alongside an object the way console.log does.

Note: this is my approach and a solution that works well for me. If you have any suggestions, improvements, or use a different method in your projects, I’d love to hear your thoughts!

Migrating to Pino: Common Pitfalls and Issues

When migrating an existing codebase to Pino—whether from Winston, another logging framework, or even the raw console API—you may expect it to work similarly. However, Pino handles logging differently, requiring adjustments to your log statements.

1️⃣ See for example, logs that work in console or Winston but fail in Pino

console.log('User created', { userId: 123 });
logger.info('User created', { userId: 123 }); // Winston
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

Expected Output:

INFO  [12:30:45]  User created { userId: 123 }
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

Pino Output (Incorrect):

INFO  [12:30:45]  User created
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

🚨 The second argument is ignored because Pino expects an object as the first argument.


2️⃣ Using %j to Force JSON Logging

logger.debug('User created %j', { userId: 123 });
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

🚨 While this works, it flattens the JSON into a string:

DEBUG  [12:30:45]  User created {"userId": 123}
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

This prevents structured log processors from parsing the data properly.


3️⃣ Moving Messages Inside JSON

To properly log both the message and object, you might be forced to rewrite logs like this:

logger.info({ msg: 'User created', userId: 123 });
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

✅ This works with structured logging but requires changing every log statement in your codebase.


4️⃣ The %j Issue With Errors

If you log errors using %j:

logger.debug('hello %j', { error: new Error('some crazy error') });
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

🚨 The error loses its stack trace and serializes incorrectly:

DEBUG  [12:30:45]  hello {"error":{}}
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

With our fix:

logger.debug('hello', { error: new Error('some crazy error') });
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

✅ Logs correctly in dev mode with pino-pretty:

DEBUG  [12:30:45]  hello 
{
  "error": {
    "type": "Error",
    "message": "some crazy error",
    "stack": "Error: some crazy error
 at file.js:12:34"
  }
}
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

✅ Fixed Behavior: Error Serializer Works in Dev Mode

With our fix:

logger.debug('hello', new Error('some crazy error'));
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

🎉 Logs correctly in development environments with pino-pretty:

DEBUG  [12:30:45]  hello 
{
  "error": {
    "type": "Error",
    "message": "some crazy error",
    "stack": "Error: some crazy error
 at file.js:12:34"
  }
}
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน
  • ✅ Preserves the error stack trace.
  • ✅ Works seamlessly in pino-pretty and JSON logs.

The Right Fix: Supporting (message, payload) While Preserving Pino's Structure

To fix this behavior, we need to:

  • ✅ Detect when the first argument is a string and second is an object.
  • ✅ Ensure %s, %d, and %j placeholders still work correctly.
  • ✅ Preserve Pino’s structured JSON logging.
  • ✅ Ensure compatibility with pino-pretty, transports, and serializers.

Let's fix your logger.ts file with just a few lines

import pino, { LogFn, Logger, LoggerOptions } from 'pino';

const isDevelopment = process.env.NEXT_PUBLIC_NODE_ENV === 'development' || process.env.NODE_ENV === 'development';

const pinoConfig: LoggerOptions = {
  level: process.env.LOG_LEVEL || 'debug',
  // We recommend using pino-pretty in development environments only
  ...(isDevelopment
    ? {
        transport: {
          target: 'pino-pretty',
          options: { colorize: true, translateTime: 'HH:MM:ss Z', sync: true },
        },
      }
    : {}),
  hooks: { logMethod },
  timestamp: pino.stdTimeFunctions.isoTime,
  serializers: {
    err: pino.stdSerializers.err,
    error: pino.stdSerializers.err,
  },
};

const logger = pino(pinoConfig);

function logMethod(this: Logger, args: Parameters<LogFn>, method: LogFn) {
  // If two arguments: (message, payload) -> Format correctly
  if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'object' && !args[0].includes('%')) {
    const payload = { msg: args[0], ...args[1] };

    // If the object is an Error, serialize it properly
    if (args[1] instanceof Error) {
      payload.error = payload.error ?? args[1];
    }

    method.call(this, payload);
  } else {
    // any other amount of parameters, or order of parameters will be considered here
    method.apply(this, args);
  }
}

export default logger;
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

How This Fix Works

✅ (Message, Payload) Now Works as Expected

logger.info('User created', { userId: 123 });
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

➡️ Logs correctly:

INFO  [12:30:45]  User created { userId: 123 }
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

Before: ❌ Second argument ignored

Now: ✅ Structured properly.


✅ Preserves String Interpolation

When a string interpolation is present in the message, we fallback to the default behavior of Pino to handle string interpolations. See pino docs for more details: https://github.com/pinojs/pino/blob/main/docs/api.md#logmethod

logger.info('User %s created with ID %d', 'John', 123);
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

➡️ Logs:

INFO  [12:30:45]  User John created with ID 123
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

✅ Ensures Error Serialization Works

logger.error('Something went wrong', new Error('Oops!'));
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

➡️ Logs:

ERROR [12:30:45]  Something went wrong { error: { msg: 'Oops!', stack: '...' } }
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

Before: ❌ Pino didn't include the error in the logs
Now: ✅ Uses Pino's error serializer.


Why I believe This Fix is The Right Approach

  • Respects Pino’s structured logging (doesn’t mutate logs).
  • Fully supports pino-pretty formatting.
  • Doesn’t break child loggers, transports, or serializers.
  • Makes Pino more intuitive without hacks.

Final Thoughts

With this fix:

  • Pino behaves the way you expect.
  • Messages and objects log together properly.
  • Pino remains fast, structured, and developer-friendly.
  • If you have a big codebase based on console logging API or Winston, your transition to Pino will be very smooth.

Happy coding!

Top comments (1)

✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน
 
kiranmarshall profile image
kiranmarshall •

Hey man I think you might need to have a think about why pino is advertised as a structured logger because I think you are misusing it. The whole point of it working the way it does is as below:

logger.info({ id: 'someUserId', privileges: 'admin' }, 'user logged in')
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

This way 'msg' is automatically populated by the second argument and you never ever worry about string interpolation. In fact using 'standard' log messages with a log object makes parsing logs a lot more manageable in a production setting.

When I am in dev and I want my logs to be readable in the browser I just do this:

const logger = pino({
  level,
  browser: {
    asObject: true,
    formatters: {
      level: (label) => ({ level: label })
    },

    write: DEV
      ? (logObj) => {
          const { level, msg, time, ...rest } = logObj as LogObj;
          const logMessage = formatLog({ level, msg, time });

          if (level === 'fatal') {
            console.error(logMessage, rest);
          } else {
            console[level](logMessage, rest);
          }
        }
      : undefined
  }
});

function formatLog(params: {
  level: Level;
  msg: string | undefined;
  time: string;
}) {
  const { level, msg, time } = params;

  const levelUppercased = level.toUpperCase();

  const formattedTime = new Date(time).toLocaleTimeString([], {
    hour12: false,
    hour: 'numeric',
    minute: 'numeric',
    second: 'numeric',
    fractionalSecondDigits: 3
  });

  return `[${formattedTime}] ${levelUppercased}: ${msg || ''}`;
}
✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน ✨ อัปเดตล่าสุด: vbarzana/enhancing pino to support message payload logging without breaking pretty logs hjb - ประเด็นร้อน

This way you get the human readability without sacrificing any of the core functionality of the way that pino is supposed to work. Your log messages are way easier to change as well because there's no interpolation, just keys and values. In prod, if you have logs enabled, everything is just serialised json. Nice and simple!