21 June 2022
Node.js logging guide – the best Node.js logger tools & practices
When done badly, logging in Node.js is little more than a nuisance that adds to the number of things a (bored) developer needs to care about. When done correctly, it can be an essential and highly useful element at every stage of development – from coding to debugging, to planning for new features. Let’s embrace the benefits of logging by learning about the best Node.js loggers and best practices for their use.
In this article, I’m going to give you a solid introduction to Node.js loggers. I’m going to go over:
- definitions and other important terminology that has to do with Node.js logging.
- the configuration required to set up a logging application.
- best practices of using Node.js loggers.
- the factors that (will) influence your decision about which logger tool to use.
Let’s get right to it!
Who uses Node.js loggers?
Node.js loggers have always seemed to be a somewhat underappreciated element of development to me.
In fact, many developers I know never even got to set up a Node.js logger on their own since, by the time they join a new project, the logger is already put in place by someone else.
But when you think about it, loggers really do matter for all actors in a software development project.
Developers and DevOps engineers
They use loggers to debug code. The logging entries will alert them of unusual occurrences in the app such as insufficient memory or failure to retrieve a specific piece of data.
Technical marketers
They can use the logging data to track the performance of their software. Particularly skilled ones can even create their own custom campaigns, log events, and send the data over to a third-party tool for further processing and analysis.
Data scientists
They can set up and optimize loggers to record just the data they need in a format suitable for them. They go on to interpret the data in order to make informed decisions about products, businesses, or even the whole world around them. For example, they can find patterns for joint purchasing of specific products in both physical stores (e.g. big chain supermarkets) and digital stores. Such products can then be stored together or offered in a bundle to increase sales. Such applications of loggers verge on data mining or machine learning. They are in great demand among the biggest players in industries such as commerce, manufacturing, geophysics, and many more.
But it all may sound a little bit abstract when you don’t have actual practical experience in using logging applications.
Let’s get some practice, then!
Node.js logging – basics
As I have already said, many developers, let alone other team members in a project, don’t think twice about logging.
What is logging all about?
Logging is the act of recording an event occurrence, data flow, or manipulation in computer software. It helps you understand how the software actually runs. It makes it much easier to debug and improve your code when it misbehaves.
There are many approaches to logging. The three most typical ones include:
- Simply printing out to a console or standard output (stdout).
- Employing libraries that make printing more uniform and structured.
- Using specialized software that captures, stores, and enables analysis of stored data.
In this article, I am referencing Node.js libraries and utilities. However, most of the ideas aren’t Node.js-specific and can be extended or applied to other programming languages or environments.
Application logging challenges
There are some challenges to application logging that you, as an aspiring data wizard, have to resolve:
- What format should I write my messages in (simple string, JSON, or any other structured format)?
- Where do I want to send and store my messages (stdout, filesystem, database, or an external tool such as ELK stack)?
- If I decide on storing in a filesystem, do I want to put logs in a single file or multiple ones?
- How do I name these log files? What criteria should I apply when deciding on which file I want to write to?
- How long do I want to store my logs?
- Which of the many log management tools or libraries do I choose for logging? Do I even need one in the first place?
- What information should I log and which I must not?
- How many logs is enough?
Unfortunately, I won’t give you a definite answer to any of them because answers are specific to each use case. But don’t go away – I’ll offer you general guidelines on how to approach these challenges.
Node.js logger – the case for logging libraries
While it is certainly possible to just print to a stdout using a built-in toll (such as console.log in JavaScript), making meaningful and useful log messages that way would be tedious and simply not feasible, as it would force you to spend too much time on repetitive tasks, having to type the commands manually. Logging libraries are there to help you with that (and much more).
Here are some of the most popular Node.js logging libraries by the number of GitHub stars.
Winston
One of the most popular logging library tools. This is my typical logger of choice. It is compact and offers everything you might need from a logger, including levels (sorting messages by importance), formatting (concerning structure and syntax of log entries), different transports (storage for logs), and so on. It is also highly configurable.
Pino
This one offers very similar functionality to Winston. It boasts very little overhead and provides transports as well as log processing. It also offers a pretty-pino module for formating logs during development using NDJSON (Newline Delimited JSON).
Bunyan
It is a simple and fast JSON logging library. It comes with a CLI tool for browsing and pretty-printing logs. It offers custom log rendering with serializers, and logger specialization with log.child. In addition, it streams for specifying log targets.
How to configure a Node.js logger?
Of course, as is typically the case in our field of work, configuration requirements for Node.js loggers depend on many factors, including what you actually want to log. I’m going to provide you with some options on how to configure your logger, but ultimately it is up to you what you choose to or not to include in your next project. However, this example configuration should give you an idea of how to go about it.
This is a configuration example based on our open-source module. Let’s break it down.
Below you can see the overview of a Winston logger configuration that The Software House team used in this project. The comments that you can see in the code describe how and why levels, formats, and transports for our log entries are set up.
How exactly will your log entries look? The code below shows the precise log format that takes into consideration data masking (i.e. obfuscation) for sensitive data.
This data masking functionality itself needs some configuration. Here’s how it’s achieved in this project.
And here is an example log that contains masked data. Notice the way certain elements are obfuscated in the result code.
This is a simple example, but it does cover all of the basics:
- Categorization of your logs.
- The way your logs look.
- The method for storing your logs.
- Rules of data masking that protect sensitive data (a must-have in a lot of commercial projects).
There is a lot more to be learned about logs before you can use them safely and efficiently. Enter best practices.
The Software House’s Node.js boilerplate helps you set up your project quickly. It supports advanced logging through the Winston library. Check it out!
Best practices of using Node.js loggers
Regardless of how much you know about setting up Node.js logger from a technical point of view, you won’t get much value from logging if you don’t stick to some best practices. For starters…
You’ve got to know what to log:
- Try to cover critical flows of your app and identify what information will be helpful in debugging an issue in production.
- Log errors when they are actually errors (e.g. not finding a resource for GET /resource/{id} probably isn’t an error, but a failure to find a user for GET /me when there is a user token might be one). Missing a resource under PATCH /resource/{id} clearly is not a correct situation
- Definitely log uncaught exceptions and unhandled rejections.
- Log data that is useful for app profiling (an in-depth analysis of app performance, down to all of its functions individually).
There are also some things that you should definitely skip when logging in a Node.js app.
What you definitely NOT want to log:
- Personally identifiable information (PII) and other sensitive types of data should be avoided. Examples of such data would be emails as well as phone, credit cards, CVC, bank accounts, and social security numbers.
Use log levels
Jokes aside, it really is helpful to categorize your logs. That way, you can sort through them far more easily. You will know which logs can be safely ignored or at least put aside temporarily, and which require you to act quickly.
Depending on the library of your choice, there are various methods of logging messages in a different manner based on their importance or severity. They are typically based on the Syslog Protocol. Such categorization systems make it easy to sort through all the logging messages in order to deal with them accordingly. A typical set of levels include:
- ERROR – indicates an error, which needs to be addressed. An issue like this typically doesn’t prevent the app from running.
- INFO – general data. It may include information on what kind of data was received by the app and when it was received.
- DEBUG – debug logs are used during development and debugging and should be switched off in production.
- WARN – it informs about errors that generally don’t really warrant any action on our side (e.g. incorrect data received from the user or third-party API).
- FATAL – a very serious issue has arisen, which requires your immediate attention. This issue may result in an application shutdown.
Make your logs meaningful and informative:
The text info passed in logs should be precise enough to actually help solve an issue. You might think that it is not such a big deal when the messages are few. But the bigger the system, the more of a difference good naming practices matter.
Examples of good logs:
- File xyz.pdf not found.
- User {id} not found.
- Failed transitioning of transaction abc: cannot transition abc from the pending to active state.
Example of poor logs:
- User not found.
- Task failed.
- File not found.
- Wrong state.
I would like to propose a straightforward rule of thumb for naming logs:
If there is no placeholder for meaningful data such as ID, enum value, or file name, the log is most likely sub-par.
Of course, some common-sense adjustments apply to this rule. If you are in some kind of logical branch in your code and there is no way to confuse or misinterpret your values, you could hardcode them. For example, if you configure a part of your system entirely dedicated to the process of account activation, you can get away with logging logger.info(“Can’t activate account”) instead of logger.info(“Can’t set status to %s”, status).
Extend functionality & usability using loggers
Some popular logger libraries come with functionalities that extend the functionality or usability of a typical logging regime. It’s worth it to give them a try. For example:
- logger.child(options) – creates a specialized logger object that adds a passed options object with each subsequent log, enabling higher contextualization.
- Log message interpolation splat such as logger.info(”Received message %o“, message):
- %o – allows stringifying passed object,
- %s – fills a string in,
- %d – passes in a number.
Here is the child logger initialization. It includes an example of log outputs for default and child loggers.
Standardize rules for logging in your application
Establish some rules for logging in your application. Each system is different so these directives will vary from codebase to codebase. When designing your own, combine project requirements with general recommendations. A good example of the latter would be OWASP guidelines.
Are you up-to-date with all the latest Node.js features? Check out our Node.js features overview to brush up
How to choose a Node.js logger?
In order to answer this question, I’ll recall the decision-making process that I used in a recent project of mine. My teammates and I pondered the exact same question – which logging library to choose. There is an abundance of them and many many feel very similar.
I started by reviewing the most popular ones, such as Winston, Pino, Bunyan, or npmlog. I was looking at their weekly downloads, stars on Github, the number of issues reported, and the last version published. None of those factors by themselves tell you if the library is good or bad. But together, they can present a picture of the library’s environment, especially when you understand the context of your findings.
To give you an example, the library with the most weekly downloads was npmlog, having got nearly three times the number of downloads of the runner-up. However, this value is inflated due to npm being the internal logging library of npm. As such, it is linked to many popular libraries. It also felt sub-par in terms of functionality.
On the other hand, I heard good things from my teammates about Bunyan. This is non-quantitative information, but real practical experience with a tool is valuable. Bunyan has good stats, but it felt a little bit stale with the last published version dating over a year back.
One might say: well, what can you add to an established logging library? While this might be true, there are some security issues surfacing every now and then and it is nice having them patched as soon as possible, especially in applications that rely on security, such as banking apps.
Ultimately, I was torn between Winston and Pino. Both offer everything one would need. They are also both popular and supported by a very active community. In the end, we decided to go with Pino.
What tipped the balance in favor of Pino was the fact that it integrated neatly with the NestJS framework that we used in the project. While both Winston and Pino had a library that adapts them with NestJS, Pino gave us an HTTP request logging out of the box. The configuration was fairly simple and pleasant, It also boasts very low overhead when used, which is always a welcome thing.
Node.js loggers – conclusions
And that’s it for the loggers’ guide! While it is not that lengthy, amazingly it still takes more time to read than some Node.js developers dedicate to configuring loggers in their projects!
I’m sure you’re not one of them – you are better than that! But just in case you need a quick refresher:
- Logging in a Node.js app helps you improve your code and quickly react to undesirable situations, including those that cause the app to crash.
- Before you get to actual logging, you need to analyze your system and decide on what to log and what not to log. Overlogging is just as bad as under logging – an excess of unnecessary information can make it more difficult to process the valuable part. Besides, there is also sensitive data that you should not log. In this case, data masking will help.
- Getting familiar with best practices of logging and developing a project-specific list of this kind for all your team members to follow is definitely the way to go.
- There are a lot of Node.js logger tools and quite a few of them are viable options for your project. Pick the one that you like working with best!
I highly encourage you to take a closer look at the code and organization of the open-source project I linked previously in the configuration section. I highlighted the most important parts, but the whole thing should give you a lot more practical insights on how to incorporate loggers in your project.
Want even more expert Node.js knowledge?
Check out our State of Microservices 2020 report and learn about microservices- and Node.js-related trends and practices as told by +650 tech leaders.