Back to Logging guides

Logging in Node.js: A Comparison of the Top 8 Libraries

Ayooluwa Isaiah
Updated on October 13, 2023

If you've recently delved into Node.js logging, you've likely encountered the limitations of the Console API. While it's useful for basic debugging in development, it lacks crucial features like log levels, timestamps, structured formats, and more. These limitations make production logging challenging.

To overcome these shortcomings, logging libraries come to the rescue. These libraries offer a wide range of features that enable you to effectively capture and record notable events in your application.

Choosing the right logging library from the myriad of options available can be overwhelming. However, this article is here to help. We'll dive into the top eight Node.js logging libraries, discussing their strengths and weaknesses. This information will empower you to make an informed decision that aligns with your logging needs.

Let's begin!

Library Weekly downloads GitHub stars
Pino 5.4m+ 12.1k
Winston 12.2m+ 21.1k
Log4js-node 3.6m+ 5.7k
Bunyan 1.4m+ 7.1k
Roarr 2m+ 963
Signale 1.2m+ 8.8k
Tracer 48k+ 1.1k
Morgan 4.1m+ 7.6k

1. Pino

pino.png

Pino is a structured logging framework for Node.js that produces JSON output by default, allowing for easy searching, monitoring and visualization of log data in log management tools.

Getting started with Pino is really straightforward as you can observe below:

 
npm install pino
 
const logger = require('pino')();

logger.info('Hello from Pino logger');
Output
{"level":30,"time":1687675301170,"pid":2376870,"hostname":"fedora","msg":"Hello from Pino logger"}

It's designed to be lightweight, fast, and memory efficient so that your application remains responsive even when it generates a substantial volume of logs. According to the benchmark results on their GitHub repo, it is amongst the fastest options for Node.js logging. This exceptional speed is one reason why it's integrated by default into the Fastify web framework, although it works seamlessly with Express and other frameworks as well.

 
const fastify = require('fastify')({
  logger: true, // enables the built-in Pino logger
});

// You can subsequently write logs like this
fastify.log.info('an info message');

// or log within request handlers like this:
fastify.get('/', options, function (request, reply) {
  request.log.info('info about the current request');
  reply.send({ msg: 'hello world!' });
})

Pino supports many of the features expected in a good logging framework such as contextual logging, multiple transport options for sending logs to various destinations, automatic error stack traces, and more. It also offers a useful log redaction feature that prevents sensitive data from leaking into your logs.

 
const pino = require('pino');

const logger = pino({
  level: process.env.PINO_LOG_LEVEL || 'debug',
  timestamp: pino.stdTimeFunctions.isoTime,
redact: {
paths: ['email'],
},
}); const user = { name: 'John doe', id: '283487', email: 'john@doe.com', }; // the `email` field in the user object will be redacted logger.info(user, 'user profile updated');
Output
{
  "level": 30,
  "time": "2023-09-26T11:52:16.060Z",
  "pid": 595644,
  "hostname": "fedora",
  "name": "John doe",
  "id": "283487",
"email": "[Redacted]",
"msg": "user profile updated" }

Notably, Pino supports asynchronous logging, where log messages are buffered and written in chunks to prevent blocking the event loop. This feature minimizes logging overhead but increases the risk of losing some log data if an unexpected system failure occurs.

 
const pino = require('pino');
const logger = pino(
  pino.destination({
    sync: false, // Enable asynchronous logging
    minLength: 4096, // size of buffer before writing the logs
  })
);

You can find more information on how to use Pino effectively in our comprehensive Pino guide.

Pino pros

  • Excellent performance and low overhead.
  • Good defaults, requires no configuration.
  • Highly customizable.
  • Defaults to structured logging in JSON.

Pino cons

  • Does not support including source file and line number.

2. Winston

winston.png

At the time of writing, Winston boasts over 12 million weekly downloads making it by far the most popular choice for application logging in the Node.js ecosystem. Its popularity attests to its prowess—a rich and adaptable API tailored to offer maximum flexibility in log formatting and transportation.

Before you can start logging with Winston, you must create a logger and configure a destination for the logs (such as the console). Winston defaults to a structured JSON output, but it supports many other formats accessible via winston.format.

 
npm install winston
 
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

logger.info('Hello from Winston logger!')
Output
{"level":"info","message":"Hello from Winston logger!"}

Likewise, transportation options are also plentiful with several built-in and third-party options available. The example below sends each log entry to the console, a file, and Better Stack.

 
const winston = require('winston');
const { combine, timestamp, json } = winston.format;

const { Logtail } = require('@logtail/node');

const { LogtailTransport } = require('@logtail/winston');

const logtail = new Logtail('<your_source_token>');

const logger = winston.createLogger({
  level: 'info',
  format: combine(timestamp(), json()),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'app.log',
}),
new LogtailTransport(logtail),
],
});

Screenshot 2023-06-13 at 13-54-57 Live tail Better Stack.png

Winston also supports capturing stack traces when logging errors, automatically logging uncaught exceptions and uncaught promise rejections, and it can even be used for basic code profiling.

The main downside to Winston is that its defaults aren't well thought out. For example, it doesn't log a timestamp by default unless you explicitly include the winston.format.timestamp() format. It also fails to include a stack trace for when logging errors without additional configuration.

 
const winston = require('winston');
const { combine, timestamp, json, errors } = winston.format;

const logger = winston.createLogger({
  level: 'info',
format: combine(errors({ stack: true }), timestamp(), json()),
transports: [new winston.transports.Console()], }); logger.info('Hello from Winston logger!'); logger.error(new Error('An error'));

If you decide to go with Winston, you must set it up correctly to ensure that you're getting the best out of it. See our complete guide to Winston for more details.

Winston pros

  • Highly customizable.
  • Offers multiple transport and formatting options.
  • It can automatically track and log uncaught exceptions.

Winston cons

  • Poor defaults which necessitates configuration effort.

3. Log4js-node

log4js-node.png

Log4js-node is a port of the Log4js logging framework for browser-based applications, which was itself inspired by the Log4j logging framework from the Java ecosystem. You can get started with Log4js-node with only the most minimal configuration needed:

 
npm install log4js
 
const log4js = require('log4js');
const logger = log4js.getLogger();
logger.level = 'info';
logger.info('Hello from Log4js-node', { name: 'John', age: 21 });
Output
[2023-09-27T06:56:02.101] [INFO] default - Hello from Log4js-node { name: 'John', age: 21 }

The library uses a semi-structured layout by default when printing logs to the standard output. This layout outputs the timestamp, level, and category, followed by the formatted log event data. While other built-in layouts are provided, there's notably missing support for JSON or other structured formats in the library.

To ensure your logs are fully structured, you can either create a custom layout (see an example) or use a separate package such as log4js-json-layout.

 
npm install log4js-json-layout
 
const log4js = require('log4js');
const jsonLayout = require('log4js-json-layout');
log4js.addLayout('json', jsonLayout);
log4js.configure({
appenders: { out: { type: 'stdout', layout: { type: 'json' } } },
categories: { default: { appenders: ['out'], level: 'info' } },
});
const logger = log4js.getLogger(); logger.level = 'info'; logger.info('Hello from Log4js-node', { name: 'John', age: 21 });
Output
{"startTime":"2023-09-27T04:59:15.967Z","categoryName":"default","level":"INFO","data":"Hello from Log4js-node","name":"John","age":21}

Appenders in Log4js-node allow you to filter your logs and configure their destination. The notable ones bundled with the core library are as follows:

  • file: Writes log events to a file asynchronously.
  • fileSync: Same as file, but performs synchronous writes.
  • multiFile: Allows writing to multiple files.
  • tcp: Writes logs to a TCP server.
  • logLevelFilter: Allows the restriction of log events based on their log level.

Appenders for Slack, Redis, RabbitMQ, Logstash, SMTP, Graylog, etc are also officially supported but the corresponding packages have to be installed separately.

Log4js-node also allows the categorization of logs such that events with the same category are sent to the same appenders. When a category isn't specified, the default category is used.

 
const log4js = require('log4js');
const jsonLayout = require('log4js-json-layout');

log4js.addLayout('json', jsonLayout);
log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    app: { type: 'file', filename: 'app.log', layout: { type: 'json' } },
  },
  categories: {
    default: { appenders: ['out'], level: 'info' },
    app: { appenders: ['app'], level: 'debug' },
  },
});

const defaultLogger = log4js.getLogger();
defaultLogger.info('Hello from Log4js-node', { name: 'John', age: 21 });

const appLogger = log4js.getLogger('app');
appLogger.info('Hello from Log4js-node', { name: 'John', age: 21 });

The defaultLogger uses the default category which is configured to log to the standard output, while appLogger is configured to log in JSON format to an app.log file in the current working directory.

Screenshot from 2023-09-27 07-11-31.png

For a more comprehensive information about Log4js-node, consult its official documentation.

Log4js-node pros

Log4js-node cons

  • Does not support structured JSON logging by default.

4. Bunyan

node-bunyan.png

Bunyan is a specialized library designed for JSON logging in Node.js. Much like other logging libraries, it starts with creating a Logger instance and then using the appropriate level methods:

 
npm install bunyan
 
const bunyan = require('bunyan');
const logger = bunyan.createLogger({ name: 'myapp' });
logger.info('Hello from Bunyan logger');

This results in well-structured JSON logs:

Output
{"name":"myapp","hostname":"fedora","pid":2394009,"level":30,"msg":"Hello from Bunyan logger","time":"2023-06-25T06:58:59.196Z","v":0}

Bunyan excels in several areas:

  • Contextual Logging: It readily supports contextual logging through an optional context parameter.

  • Child Loggers: You can create child loggers with additional bound fields, making it easy to add a set of fields to all logs within a specific scope.

  • Error Handling: Bunyan automatically handles logged errors, including outputting an err field with exception details, including its stack trace.

 
logger.info(
  { width: 300, height: 400, file: 'needsmore.jpg' },
  'image uploaded successfully'
);
logger.error(new Error('request failed to complete'));
Output
{"name":"myapp","hostname":"fedora","pid":2454325,"level":30,"width":300,"height":400,"file":"needsmore.jpg","msg":"image uploaded successfully","time":"2023-06-25T08:04:12.110Z","v":0}
{"name":"myapp","hostname":"fedora","pid":2433470,"level":50,"err":{"message":"request failed to complete","name":"Error","stack":"Error: request failed to complete\n    at Object.<anonymous> (/home/ayo/dev/betterstack/demo/nodejs-logging/index.js:7:14)\n    at Module._compile (node:internal/modules/cjs/loader:1254:14)\n    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)\n    at Module.load (node:internal/modules/cjs/loader:1117:32)\n    at Module._load (node:internal/modules/cjs/loader:958:12)\n    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)\n    at node:internal/main/run_main_module:23:47"},"msg":"request failed to complete","time":"2023-06-25T07:30:16.369Z","v":0}

Bunyan also offers a helpful built-in CLI for development purposes. You can easily pretty-print logs and apply filters to view specific log records:

 
node index.js | npx bunyan

Screenshot from 2023-06-25 09-33-36.png

You can also use it to filter log content so that only records matching a specified criteria are shown. For example, you can display only logs with ERROR or greater severity like this:

 
node index.js | npx bunyan -l error

Bunyan pros

  • Dedicated to structured logging in JSON.
  • Provides a useful CLI tool for pretty-printing and filtering logs in development.
  • Can output logs to multiple destinations.

Bunyan cons

  • Doesn't appear to be actively maintained at the time of writing.

Side note: Get a Node.js logs dashboard

Save hours of sifting through Node.js logs. Centralize with Better Stack and start visualizing your log data in minutes.

See the Node.js demo dashboard live.

5. Roarr

roarr.png

Roarr stands out as a versatile logging framework suitable for both Node.js and browser environments. It differentiates itself from other loggers by aiming to be compatible with both application and library code, allowing for correlation between the two. One unique feature is that you don't need explicit initialization to start logging:

 
npm install roarr
 
const logger = require('roarr').Roarr;

logger.trace('Processing request...');
logger.debug({ data: { foo: 'bar' } }, 'Received data');
logger.info('Request processed successfully.');
logger.warn('Invalid input detected.');
logger.error('Database connection failed.');
logger.fatal('Critical error occurred. Shutting down.');

However, logs are not written unless the ROARR_LOG environmental variable is set to true.

 
ROARR_LOG=true node index.js
Output
{"context":{"logLevel":30},"message":"Hello from Roarr logger","sequence":"0","time":1687682375862,"version":"2.0.0"}

Similar to Bunyan, Roarr also comes with a CLI that can be used to pretty-print or filter logs:

 
ROARR_LOG=true node index.js | npx roarr pretty-print

Screenshot from 2023-06-27 15-08-54.png

Roarr excels in structured logging, offering support for logging contextual data through various methods, such as including it directly at the log point, using the child() method, or utilizing adopt() to propagate contextual properties through asynchronous callbacks and promise chains:

 
const Roarr = require('roarr').Roarr;

const logger = Roarr.child({
  name: 'my-service', // this will be included in all logs
});

logger.info({ userId: 1234 }, 'User login');

logger.debug(
  {
    query: 'SELECT * FROM users',
  },
  'Executing database query'
);

logger.info({ amount: 100 }, 'Processing payment');

The contextual data appears under the context property:

Output
{"context":{"logLevel":30,"name":"my-service","userId":1234},"message":"User login","sequence":"0","time":1687813925555,"version":"2.0.0"}
{"context":{"logLevel":20,"name":"my-service","query":"SELECT * FROM users"},"message":"Executing database query","sequence":"1","time":1687813925556,"version":"2.0.0"}
{"context":{"amount":100,"logLevel":30,"name":"my-service"},"message":"Processing payment","sequence":"2","time":1687813925556,"version":"2.0.0"}

One thing to note about Roarr is that it doesn't support in-process transports due to the limitations of the single threaded nature of Node.js processes. Instead, it encourages the use of log shippers such as Fluentd, Logstash, or Vector for transforming, filtering, and shipping logs to their final destination.

Roarr pros

  • Supports Node.js and the browser.
  • Can be used for debug logging in libraries.
  • Has a companion CLI program.
  • Does not require initialization.
  • Produces structured output.

Roarr cons

  • Relies on external log shippers alone.
  • Offers limited customization possibilities.

6. Signale

signale.png

Signale is a robust logging utility library that enhances the logging experience in Node.js Command-Line Interfaces (CLIs) by providing a sleek and adaptable way to log messages. It offers a wide array of features and options to cater to various logging needs. Here are some notable aspects of Signale:

  • Colorized and prettified output: Signale generates colorized and aesthetically pleasing log output by default, improving the readability of logs.

  • Interactive mode: It supports an interactive mode where subsequent logged messages can override earlier ones. This can be particularly useful for dynamic CLI programs.

Screenshot from 2023-06-27 15-10-51.png

  • Customizable output streams: While Signale prints to standard output by default, it offers extensive customization options. You can specify one or more writable streams to route log messages to specific destinations, tailoring the logging behavior to your needs.

  • Timed logging: Signale provides time measurement capabilities through its time() and timeEnd() methods. You can define a timer by giving it a unique name and later conclude the timing using timeEnd(). This feature is handy for measuring the duration of specific operations in interactive CLI programs.

Signale pros

  • Highly customizable.
  • Chiefly aimed at interactive CLI programs.
  • Offers redaction and filtering of program secrets.

Signale cons

  • Not a general-purpose logging framework.
  • Does not support structured logging.

7. Tracer

tracer.png

Tracer is a logging and debugging utility for Node.js that offers a much better alternative to the Console API with support for log levels, colourized output, logging to files, and the ability to log to multiple destinations simultaneously.

 
const logger = require('tracer').colorConsole({});

logger.log('hello');
logger.trace('hello', 'world');
logger.debug('hello %s', 'world', 123);
logger.info('hello %s %d', 'world', 123, { foo: 'bar' });
logger.warn('hello %s %d %j', 'world', 123, { foo: 'bar' });
logger.error(
  'hello %s %d %j',
  'world',
  123,
  { foo: 'bar' },
  [1, 2, 3, 4],
  Object
);

Screenshot from 2023-09-27 09-15-58.png

In terms of log formatting options, Tracer relies on formatting tags to customize the log output format, and you can have a different format for both regular logs and errors:

 
const logger = require('tracer').colorConsole({
  format: [
    '{{timestamp}} <{{title}}> {{message}} (in {{file}}:{{line}})', //default format
    {
      error:
        '{{timestamp}} <{{title}}> {{message}} (in {{file}}:{{line}})\nCall Stack:\n{{stack}}' // error format
    }
  ],
  dateformat: 'HH:MM:ss.L',
})

Regarding log destinations, Tracer supports logging to the console, a file, any writable stream, MongoDB, and more. Do check out its documentation and example folder for more details.

Tracer pros

  • Supports colorized output.
  • Supports logging to multiple destinations.
  • Allows user-defined log levels.

Tracer cons

  • Limited support for structured and contextual logging.

8. Morgan

morgan.png

Morgan is a logging middleware that provides support for HTTP request logging in Node.js. It can automatically record details of incoming requests to a server including its method, IP address, user agent, response time, response size, and more. The main advantage of using Morgan is that it saves you the effort of creating your own logging middleware.

Morgan provides a variety of formats for logging HTTP requests. These formats are simply an arrangement of a predefined set of tokens, most notably the combined and common formats that follow standard Apache Web Server conventions. It also offers the ability to create custom tokens and formats as you please so that you can ensure that the logging output contains the desired details.

Morgan pros

  • Lightweight.
  • Integrates directly with Express.
  • Does not offer structured logging by default.

Morgan cons

  • Does not appear to be actively maintained.

Final thoughts

Throughout this article, we have explored eight of the best Node.js logging libraries available. Each library offers unique features, customization options, and integrations that cater to diverse logging requirements. Whether you prioritize performance, structured logging, extensibility, or you simply want something to enhance the Console API, there should be an library for you amongst these options.

If you'd like to learn more about logging in Node.js, check out our best practices article. Thanks for reading, and happy logging!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is the Head of Content at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he’s not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
A Comprehensive Guide to Logging in Python
Python provides a built-in logging module in its standard library that provides comprehensive logging capabilities for Python programs
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github