How to Get Node.js Logging Right

RisingStack's services:

Sign up to our newsletter!

In this article:

The aim of this article is to provide help with logging in Node.js modules, applications and distributed systems (or microservice architectures). To do so, I’ve compiled the tools, methods, rules and best practices we use at RisingStack for developing enterprise Node projects.

This means that you can learn how to use the built-in console module of Node.js, the debug module, the winston library and Trace.

UPDATE: This article mentions Trace, RisingStack’s Node.js Monitoring platform several times. On 2017 October, Trace has been merged with Keymetrics’s APM solution. Click here to give it a try!

Doing Node.js Logging Right

Whether talking about Node.js modules or complete applications, it is crucial to get logging right.

Logging in Node.js enables you to:

  • have a better understanding of how your applications works,
  • discover what errors you have,
  • find out if your services are running properly.

Using the console

The most basic kind of logging one can think of is using the built in console logging. With the console module, you can put messages on both the stdout and stderr:

  • console.log('some msg') will put some msg to the standard output (stdout),
  • console.error('some error') will put some error to the standard error (stderr).

This approach has a lot of flaws like you cannot turn it off or add log levels to it. For logging, we usually have requirements, which the console module can’t do.

Node.js logging requirements

1. Timestamps

You want to know when a given event happened, right? You will need a timestamp for that.

2. Logging format

Writing readable log lines are equally important for both humans and computers: you’d like to quickly understand what is going on, while machines have to parse the file as well.

3. Log destinations

Sometimes multi-transport logging appears as a requirement – but it shouldn’t be. The target should always be the standard output/error. It is not the responsibility of the application to route logs.

4. Support for log levels

Log events can have different severity levels – in some cases, you just want to log events with at least a warning level, sometimes log lines have to be more verbose.

Before jumping into the more technical part, we have to distinguish between three areas of logging:

  • when you are building a node module,
  • when you are building an application,
  • when you are building a distributed system.

Let’s discover best practices for these situations one by one!

Logging in Node.js modules

It is not recommended to pollute the log files with your events when others use the module you build. However, in some cases, application developers may need it to understand better why a given issue arose.

In this situation, you can use the debug module. Adding it to your project is as easy as:

npm install debug --save

Then:

const debug = require('debug')('my-namespace')
const name = 'my-app'
debug('booting %s', name)

By default, it will not produce any output. To enable this logger, you have run your application with a special environment variable, called DEBUG.

DEBUG=my-namespace node app.js

Once you do that, the debug module will come to life and will start producing log events for stdout.

Luckily, this module is widespread in the Node.js community, so if you are using a popular project, like Express, chances are high you can use the debug module with that. Also, you can inspect multiple modules at once, by separating the names with a comma.

DEBUG=my-namespace,express* node app.js

The output from a sample express run looks something like this:

Node.js logging express output

Logging in your Applications

When it comes to your application, you can do more sophisticated application logging.

For that, you can use one of the most popular package called winston. Winston is a multi-transport async logging library for Node.js.

You can add winston to your project by installing it:

npm install winston --save

Once you have it, you can add winston to your project this way:

const winston = require('winston')

winston.log('info', 'Hello log files!', {
  someKey: 'some-value'
})

The snippet above will put the following line to stdout:

info: Hello log files! someKey=some-value

As you could see above, we are passing the info string to winston, which tells winston that the event it is going to log has a log level associated with it, which is info. By default, winston ships with the following log levels (custom levels can be added):

  • error,
  • warn,
  • info,
  • verbose,
  • debug,
  • and silly.

You can set what levels to log with the following line:

winston.level = 'debug'

At RisingStack, we usually set the configuration from an environment variable called LOG_LEVEL. This way we can dynamically change what should be logged:

winston.level = process.env.LOG_LEVEL

What Should Not Be Logged

When you are logging on a silly level, you should try to log almost everything. Still, there is a rule of thumb that you should follow: never, ever log credentials, passwords or any sensitive information.

Usually, more than one people has access to logs, so logging events containing credentials adds an extra security risk.

Logging in Distributed Systems

When you start using microservices, the biggest challenge from a logging perspective is doing distributed tracing.

In a distributed architecture, error detection gets frustrating because of the flood of alerts caused by flaws in the system. Distributed tracing aims to eliminate this problem by providing insights on transactions and errors in a way that log files are not capable of doing so.

Adding correlation IDs

For that, you have to use a so-called correlation identifier – and pass it to all the services the transaction goes through. Then, if every log line is labeled with this ID, you can search for these events.

To generate a unique ID, you can use the uuid module this way:

const uuid = require('uuid')
const id = uuid.v1()

This ID has to be passed around in function calls, and it has to be sent to downstream services too. As you can imagine, is not a rewarding task to do.

Using Trace

This is where Trace, our Node.js / Microservices monitoring solution comes into the picture. Trace attaches correlation IDs to requests out of the box and visualizes the data-flow on a simple tree-graph. You won’t need to search through logs since it will display your transactions in an easy-to-understand way:

Node.js distributed tracing with trace by risingstack

Thanks to this approach, you can see the dependencies between your microservices and localize ongoing issues.

Logging in Node.js shouldn’t be hard if you implement these methods and tools we just discussed.

If you have questions or recommendations regarding this topic, feel free to share them in the comments section.

Share this post

Twitter
Facebook
LinkedIn
Reddit