Tommz
Tommz

Reputation: 3453

How to log JavaScript objects and arrays in winston as console.log does?

I was looking at top Node logging systems: npmlog, log4js, bunyan and winston and decided to use winston for having the most npm monthly downloads.

What I want to set up is custom logger which I will be able to use on development environment with logger.debug(...) which won't log anything on production environment. This will help me so when I'm on development environment, I won't need to write anything since I'll see all the outputs.

This is what I have now:

var level = 'debug';
if (process.env.NODE_ENV !== 'development'){
  level = 'production'; // this will never be logged!
}

var logger = new winston.Logger({
  transports: [
    // some other loggings
    new winston.transports.Console({
      name: 'debug-console',
      level: level,
      prettyPrint: true,
      handleExceptions: true,
      json: false,
      colorize: true
    })

  ],
  exitOnError: false // don't crush no error
});

Problem occurs when I'm trying to log JavaScript Object or Javascript Array. With Object, I need to do toJSON(), and for Array I need first JSON.stringify() and then JSON.parse().

It's not nice to write all the time this methods, just to log something that I want. Furthermore, it's not even resource-friendly, because those formatting methods need to be executed before logger.debug() realises that it's on production and that it shouldn't log it in the first place (basically, it's evaluating arguments before function call). I just like how old-fashined console.log() logs JavaScript objects and arrays.

Now, as I'm writing this question, I found that there is a way of describing custom format for every winston transports object. Is that the way of doing it, or is there some other way?

Upvotes: 38

Views: 33853

Answers (9)

Thompson Filgueiras
Thompson Filgueiras

Reputation: 1

The below code worked well for me:

import { Request } from 'express';
import util from 'util';
import winston, { Logger } from 'winston';

const splatSymbol = Symbol.for('splat');
export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.colorize(),
    winston.format.timestamp(),
    winston.format.printf(({ timestamp, level, message, [splatSymbol]: splatArgs = [] }) => {
      const formattedMessage = [message, ...splatArgs].map((value) => {
        try {
          if (typeof value === 'object' || Array.isArray(value)) {
            return util.inspect(value, { depth: 5, showHidden: false, showProxy: false, maxArrayLength: null });
          }
          
          if (typeof value === 'function') {
            return util.inspect(value, { depth: 5, showHidden: false, showProxy: false, compact: true });
          }

          if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
            return value;
          }

          if (typeof value === 'symbol' || typeof value === 'bigint') {
            return value.toString();
          }

          return String(value);
        } catch (error) {
          console.error(`Error formatting value: ${error}`);
          return value;
        }
      }).join(' ');
      return `${level}: ${timestamp} - ${formattedMessage}`;
    })
  ),
  transports: [new winston.transports.Console()],
});

export interface RequestWithLogger extends Request {
  logger: Logger;
}

basically I used util to deal with objects and arrays, also I checked other edge cases. Here a log and an output example:

app.listen(process.env.PORT, () => {
    logger.info('Server Started', {
      server: `http://localhost:${process.env.PORT}`,
      swagger: `http://localhost:${process.env.PORT}/api-docs`,
      bullDashboard: `http://localhost:${process.env.PORT}/admin/queues`,
    }, null, undefined, new Error('Server Started'), { a: { b: { c: { d: { e: { f: 1 }}}}}},
    test,
    () => {});
  });


queuenode-app-1    | info: 2024-10-04T23:29:29.757Z - Server Started {
queuenode-app-1    |   server: 'http://localhost:3333',
queuenode-app-1    |   swagger: 'http://localhost:3333/api-docs',
queuenode-app-1    |   bullDashboard: 'http://localhost:3333/admin/queues'
queuenode-app-1    | } null undefined Error: Server Started
queuenode-app-1    |     at Server.<anonymous> (/app/src/server.ts:60:25)
queuenode-app-1    |     at Object.onceWrapper (node:events:633:28)
queuenode-app-1    |     at Server.emit (node:events:531:35)
queuenode-app-1    |     at Server.incomingRequest (/app/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/instrumentation-http/src/http.ts:446:25)
queuenode-app-1    |     at emitListeningNT (node:net:1947:10)
queuenode-app-1    |     at processTicksAndRejections (node:internal/process/task_queues:81:21) {
queuenode-app-1    |   a: {
queuenode-app-1    |     b: {
queuenode-app-1    |       c: { d: { e: { f: 1 } } }
queuenode-app-1    |     }
queuenode-app-1    |   }
queuenode-app-1    | } [Function: test] [Function (anonymous)]

Upvotes: 0

logeekal
logeekal

Reputation: 525

Try using util.inspect for objects. It handles circular references correctly as well. @radon-rosborough has already given this answer but I thought of adding an example. Please see below

const customTransports = [
    new winston.transports.Console({
        format: combine(
            timestamp({
                format: 'DD-MMM-YYYY HH:MM:SS'
            }),
            label({
                label: file
            }),
            prettyPrint(),
            format.splat(),
            simple(),
            printf( (msg)=> {
                let message = msg.message;

                return colorize().colorize(msg.level, `${ msg.level } :  ${msg.timestamp} :  ${ msg.label } : \n`) + `${ util.inspect(message,{
                    depth: 2,
                    compact:true,
                    colors: true,
                } )}`;
            })
        )
    })
] 

Upvotes: 1

smith64fx
smith64fx

Reputation: 337

In Winston > 3 you can use

logger.log('%o', { lol: 123 }')

Anyway... Couldn't accept that I have to use %o always and made this simple solution:

const prettyJson = format.printf(info => {
  if (info.message.constructor === Object) {
    info.message = JSON.stringify(info.message, null, 4)
  }
  return `${info.level}: ${info.message}`
})

const logger = createLogger({
  level: 'info',
  format: format.combine(
    format.colorize(),
    format.prettyPrint(),
    format.splat(),
    format.simple(),
    prettyJson,
  ),
  transports: [
    new transports.Console({})
  ],
})

So this logger....

  logger.info({ hi: 123 })

...transforms to this in the console

info: {
    "hi": 123
}

Upvotes: 9

Atishay Jain
Atishay Jain

Reputation: 1445

Instead of doing

prettyPrint: function ( object ){
    return JSON.stringify(object)
}

it's better go with utils-deep-clone package

// initialize package on the top
const { toJSON } = require('utils-deep-clone')


// and now in your `prettyPrint` parameter do this
prettyPrint: function ( object ){
    return toJSON(object)
}

if you'll go with JSON.stringify you won't be able to print error

console.log(JSON.stringify(new Error('some error')))
// output will '{}'

Upvotes: 0

K435
K435

Reputation: 53

As Leo already pointed out in his answer, Winston makes use of String Interpolation provided by util.format:

const winston = require("winston");                                                                                                                                                                                                    
const logger = new winston.Logger({                                                                                                                                                                                                    
  transports: [                                                                                                                                                                                                                        
    // some other loggings                                                                                                                                                                                                             
    new winston.transports.Console({                                                                                                                                                                                                   
      name: "debug-console",                                                                                                                                                                                                           
      level: process.env.LOGLEVEL || "info",                                                                                                                                                                                           
      prettyPrint: true,                                                                                                                                                                                                               
      handleExceptions: true,                                                                                                                                                                                                          
      json: false,                                                                                                                                                                                                                     
      colorize: true                                                                                                                                                                                                                   
    })                                                                                                                                                                                                                                 
  ],                                                                                                                                                                                                                                   
  exitOnError: false // don't crush no error                                                                                                                                                                                           
});                                                                                                                                                                                                                                    

const nestedObj = {                                                                                                                                                                                                                    
  foo: {                                                                                                                                                                                                                               
    bar: {                                                                                                                                                                                                                             
      baz: "example"                                                                                                                                                                                                                   
    }                                                                                                                                                                                                                                  
  }                                                                                                                                                                                                                                    
};                                                                                                                                                                                                                                     

const myString = "foo";                                                                                                                                                                                                                

logger.log("info", "my nested object: %j. My string: %s", nestedObj, myString);                                                                                                                                                                                                                                  

When calling logger.log, you can define placeholders which will be appropriately replaced. %j will be replaced by the equivalent of JSON.stringify(nestedObj)

Upvotes: 1

Leo
Leo

Reputation: 1120

logger.log("info", "Starting up with config %j", config);

Winstons uses the built-in utils.format library. https://nodejs.org/dist/latest/docs/api/util.html#util_util_format_format_args

Upvotes: 31

Resigned June 2023
Resigned June 2023

Reputation: 4957

Use the built-in Node.js function util.format to convert your objects to strings in the same way that console.log does.

Upvotes: 4

Bharat Kul Ratan
Bharat Kul Ratan

Reputation: 1003

try changing prettyPrint parameter to

prettyPrint: function ( object ){
    return JSON.stringify(object);
}

Upvotes: 16

ambrons
ambrons

Reputation: 136

My recommendation is to write your own abstraction on top of winston that has a convenience method for printing your objects for debugging.

You may also look at this response for a hint of how the method could be developed.

https://stackoverflow.com/a/12620543/2211743

Upvotes: 2

Related Questions