You Built Your Node App, but Are You Logging?

Logging is a crucial part of developing an application. When the app is on production, logs are necessary to identify the problem if something goes wrong with it. So, if you are a developer, you should ask the question, “Am I doing logging the right way?”.
In this post, we are going to provide an answer to that question. We will discuss what are the best practices developers, especially Node developers, should follow when logging your application events.
Why Are Logs Important?
No matter how careful we are when developing an application, it’s a difficult task to make it 100% secure and bugs-free. We try to find and solve most of the problems during development by testing and debugging. Still, we won’t be able to catch all of them.
Because of these remaining errors, applications running in production might behave in unexpected ways in certain situations. Sometimes, they can be really critical, even crash the application entirely. In such a case, can we run a debugger to figure out what went wrong with our application? No, it’s not the most practical idea.
Instead, we use application logs to understand how and why the application is behaving differently. For this, we have to set up our application to record information about its events and errors. And this is what we call logging. Logging helps us identify problems with an application running in production.
Logging Best Practices
Since logs are quite important, we need to follow logging practices that will help us easily identify problems and their causes.
1. Don’t use console.log
Developers tend to rely on Node’s console.log
function to log application events since it’s easily accessible, needs no additional set up, and simple to use. But if you want to take logging seriously, and you should, this is not the way to achieve it.
console.log
prints its output to stdout
. Other console functions, like console.err
and console.warn
prints outputs to stderr
. You can’t configure console.log
to transport the logs to a file or a database. You can’t turn logging on and off or switch between different logging levels (which we will talk about later) when the app is on production.
Simply, console.log
doesn’t provide enough features or configuration options to become an adequate logging tool. You should instead use a dedicated logging library to get the job done properly.
2. Use a dedicated logging library
A dedicated logging library, unlike console.log
, provides a set of features to create logs that let us identify problems easily and enough configurations to make the best use of our logs.
Most logging libraries support several logging levels like info, debug, warning, and error. These levels help filter logs according to our needs.
The biggest advantage of using a logging library is being able to switch between logging levels even when the app is in production.
They also support formatting logs with different colors for different levels of logs. Some libraries also support the formatting of different data types like JSON.
Winston and Bunyan are two of the most popular logging libraries available to Node developers.
In this post, we are going to use Winston in the code examples.
3. Source, timestamp, context—the most important parts of a log
Every log recorded by your application should consist of these three parts.
Source
If we are debugging our application using the logs, it’s important to know where each event occurred. The source could be the name of the host, method, zone, or in microservices architecture, the name of the service.
Timestamp
Recording the timestamp of the events occurred is also important to logging. We might need to filter the logs recorded within a certain timeframe or sort the logs by the time they occurred. Hence, the timestamp is a must-include part of an application log.
Context and level
The context of a particular log is important when we are debugging the application.
For example, if the application is registering a new user, there are several ways this operation could fail. The user might provide invalid data or he/she could already be registered in the system. These failures are not occurring because our application is behaving faultily.
But if this operation fails because the application couldn’t connect to the database, then signifies that something have gone wrong with it. Therefore, providing the context of the event with every log is crucial to make the best of logging.
In addition, recording the level is also important to filter and identify different issues of the application based on how critical they are.
4. Use log levels properly
We use logging levels to sort them by urgency so that we can filter them accordingly.
Syslog standard provides specified levels, declared according to their severity, we could use when logging.
- Emergency: the system is unusable
- Alert: action must be taken immediately
- Critical: critical conditions
- Error: error conditions
- Warning: warning conditions
- Notice: normal but significant conditions
- Informational: informational messages
- Debug: debug-level messages
You can alter standard levels to create a list of levels that suit your application better. However, each log must be given a level to be able to filter them as required.
5. What not to do when logging
Logging should not generate any errors of its own
We are trying to find errors in our application with the logs. We don’t need logs to add their own errors on top of that. So, make sure that logging operations are written in a way that does not generate errors of its own.
For example, the following code could throw an error when logging. You should avoid instances like this.
const logger = require("../logger")
exports.findUserByUsername = async (req, res) => {
logger.info(`Invoking findUserById with the id ${req.params.username}`)
//implementation
logger.debug(`Finding user data from the database ${userModel.find({username: req.params.username})}`) //could throw an error.
}
Logging operations should be stateless
Logging operations should not generate any state changes in the application like changing the database. You should avoid scenarios like this.
exports.saveUser = async (req, res) => {
logger.info("invoking saveUser()")
//implementation
logger.debug(`saving user to the database ${userModel.save(req.body)}`) //changes application state
}
6. Use the appropriate logging level in production
Being able to log records of every level would be ideal when our app is in production. But it’s not always practical. If your application has heavy user traffic, logging every level of code would result in a huge performance dip.
We need to take a proactive approach to avoid this and log optimally. In a production-level application, the majority of the logs belong to debug and info levels. So, during the normal runtime, if we turn off debug and info logs, and log only the lower levels, we can avoid the performance issues that come with frequent logging.
In an application in production, we turn on warning, error, and other lower levels of logs to identify if it is in critical status. We can turn on debug and info level logs only when an error is detected.
One of the benefits of using a logging framework is being able to change the logging level easily while in production.
7. Store the current logging level as an environment variable
To ensure that we can easily change between level when needed, you should store the current logging level of the application as an environment variable. It gives us the ability to change the level when the application is still in production by simply changing the variable value.
//.en.
LOG_LEVEL = "warn"
//logger.js
const transports = {
console: new winston.transports.Console({ level: process.env.LOG_LEVEL}),
};
const logger = winston.createLogger({
transports: [
Transports.console,
]
});
Summary
If you are building an application intended to go to production, logging is a crucial feature it should have. In this post, we discussed what are the best practices you should use when creating a logging system. Using this knowledge, you can start building a great logging system for your application today.
Thanks for reading!
If you liked what you saw, please support my work!

Juan Cruz Martinez
Juan has made it his mission to help aspiring developers unlock their full potential. With over two decades of hands-on programming experience, he understands the challenges and rewards of learning to code. By providing accessible and engaging educational content, Juan has cultivated a community of learners who share their passion for coding. Leveraging his expertise and empathetic teaching approach, Juan has successfully guided countless students on their journey to becoming skilled developers, transforming lives through the power of technology.