Make Your Logs Work for You

The days of logging in to servers and manually viewing log files are over. SolarWinds® Papertrail™ aggregates logs from applications, devices, and platforms to a central location.

View Technology Info

FEATURED TECHNOLOGY

Troubleshoot Fast and Enjoy It

SolarWinds® Papertrail™ provides cloud-based log management that seamlessly aggregates logs from applications, servers, network devices, services, platforms, and much more.

View Capabilities Info

FEATURED CAPABILITIES

Aggregate and Search Any Log

SolarWinds® Papertrail™ provides lightning-fast search, live tail, flexible system groups, team-wide access, and integration with popular communications platforms like PagerDuty and Slack to help you quickly track down customer problems, debug app requests, or troubleshoot slow database queries.

View Languages Info

FEATURED LANGUAGES

TBD - APM Integration Title

TBD - APM Integration Description

TBD Link

APM Integration Feature List

TBD - Built for Collaboration Title

TBD - Built for Collaboration Description

TBD Link

Built for Collaboration Feature List

Tips from the Team

Node.js Logging – How to Get Started

START FREE TRIAL

Fully Functional for 30 Days

Last updated: October 2025

Node.js may be the runtime environment that lets you use a single programming language (JavaScript) for both server-side and client-side applications, but it has no shortage of logging methods. Even though there are clearly delineated trade-offs with each of the logging techniques, if you’re a newcomer, it can be challenging to make sense of them and to know which one is right for your environment.

Fortunately, Node.js provides support for logging out of the box, so taking your first steps is easy. From there, you can progress to more sophisticated logging modules that allow you to turn log statements on and off, customize where your log data is sent, and assign a severity to log messages.

In this article, we’ll show you how to start writing logs with Node.js quickly. We’ll gradually introduce new modules with more flexibility, so you can make the best choice for logging Node.js in your environment.

What Should I Log?

You should log information that helps you understand the flow of your application. That way, you can easily identify, troubleshoot, and debug issues at runtime. Common information you can consider logging includes:

  • Errors and exceptions that happen in your application, making it easy to troubleshoot and diagnose issues.
  • Warnings that help you identify non-critical issues that need attention. For example, you may log warnings on deprecated packages or endpoints.
  • Application start and stop time. This is essential in troubleshooting application uptime.
  • User activities, such as logging and subsequent actions, in the application. Logging user activities is handy when you want to determine which user performed a specific action.

Built-in Node.js Libraries

The starter log choice for Node.js is its built-in console module. The console lets you focus solely on writing log messages to track your application’s behavior. After console, you can move up to the debug module, which provides more flexibility. Let’s take a closer look at these two modules, along with their pros and cons.

console.log and console.error

The built-in console module in Node.js lets you write log messages to standard output (STDOUT) and standard error (STDERR) using the log and error functions, respectively. This module is the simplest method when you’re just getting started, since it doesn’t require importing or configuring any third-party packages.

The following code snippet uses console.log to write a message to STDOUT:

console.log('A log message');

Though console.log offers a hassle-free way to write log messages, it leaves a lot to be desired. For one, there’s no way to disable console.log calls. If you add calls to your code, your messages will appear on the console whether you want them there or not. Secondly, console.log doesn’t accept logging levels, which means that apart from the two log and error functions, you cannot differentiate between log messages with different severities. And that makes for noisy applications, especially when you’re writing production code or libraries used by other developers.

Lastly, console.log is implemented as part of the JavaScript runtime, but its behavior isn’t standardized across all implementations. In the vernacular of programming language designers, the console.log behavior is undefined. That means it can behave however it wants, and its functionality can change from release to release.

Usually, this undefined behavior isn’t a problem, but it can have consequences when you write to a console before it’s active. After you’ve called console.log, the console updates asynchronously; instead of queuing the text exactly as you’d expect it to appear, a reference is queued. If you change the queued object, the old value will be rendered on the console when it updates, not the new one.

There are ways to work around this oddity by converting whatever you’re printing to an immutable string, but the most straightforward answer is not to use console.log at all, opting instead to use a logging module.

While the console module is always available for quickly writing messages, a proper logging package allows you to disable all logging calls and have at least some control over which log messages you see. The debug module provides just such a feature.

The debug module

A step up from using console.log is the debug module. This module is a tiny JavaScript debugging package that provides a simple mechanism for turning logging on and off inside your code.

The debug module provides a function that accepts your module’s name as its only parameter and returns a decorated console.error object that writes log messages STDERR.

Here’s an example of using debug in a module called foo:

var debug = require('debug')('foo') 

function alarm() {
  debug('This is a debug message');
}

alarm();

If you run this code with no changes to your environment, nothing is printed on the console. But if you set the DEBUG environment variable to foo, you’ll see the following output:

$ DEBUG=foo nodejs debug.js

foo This is a debug message +0ms

That’s because the debug module disables all log messages by default, so you must specifically enable the ones you want. To enable them, use the DEBUG environment variable, which takes a string of module names separated by commas.

Of course, the debug module is still just a wrapper around the rudimentary console.log, and it doesn’t provide a granular way to turn log messages on and off. To take advantage of a more feature-rich logging solution, you need to consider other libraries.

Best Node.js Logging Libraries

Below is a list of some of the best Node.js logging libraries.

Winston

Winston is a flexible logging library for Node.js. From its inception, Winston was designed to be simple yet configurable, so it includes support for custom log formats, sending log messages to multiple types of destinations, and colorizing logs. Critically, Winston also comes with a feature we’ve been missing in our discussion until now: logging levels.

No one wants to pick through log messages in the middle of an urgent troubleshooting session to figure out whether, at that moment, a particular message is a vitally important or inconsequential noise.

The best logging frameworks offer logging levels so you can highlight the messages you care about. Use them correctly when writing code, and they’ll save you a lot of time when you need to view only a select category of messages.

The logging levels available in Winston correspond to those defined for npm. Ordered from most important to least important, Winston has corresponding functions: error, warn, info, verbose, debug, and silly.

A transport is the component that sends log messages to their destination, and a Winston logger can use multiple transports at the same time. This comes in handy if you want to use Winston’s core transports to send debug or info logs to the console and use one of the community transports to send critical messages to a remote log management tool.

Here is an example of creating a simple logger object with Winston that writes a debug and error string:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.simple(),
  transports: [
    //
    // - Write to all logs with level `info` and below to `combined.log` 
    // - Write all logs error (and below) to `error.log`
    //
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

logger.info('Info string');
logger.error('Error string');

Writing a message with the highest priority level (error) writes to both combined.log and error.log:

$ cat combined.log 
info: Info string
error: Error string

Pino

Pino is a fast, low-overhead, and highly configurable Node.js JSON logging library. The low overhead and high performance make it suitable for use in a production environment. Some of its features include:

  • Child loggers: Create a child logger with inherited properties from the parent logger. Child loggers are good for organization and adding context-specific information to logs.
  • Log transport: Use transports to send logs to various destinations. This could be for files or logging management tools.
  • Log redaction: Redact sensitive data from log messages.
  • Log levels: Set log levels for different environments. For example: trace, debug, info, warn, and error log levels.

Here’s an example of creating a simple parent and child logger:

const logger = require("pino")();
logger.info("this is parent info log");
const child = logger.child({ a: "property" });
child.info("this is child info log");

Here’s the result

{"level":30,"time":1716577065425,"pid":86439,"hostname":"MBP.local","msg":"this is parent info log"}
{"level":30,"time":1716577065426,"pid":86439,"hostname":"MBP.local","a":"property","msg":"this is child info log"}

Unlike most Node.js logging libraries, Pino logs the level using numbers that increase in tens instead of strings.

Morgan

Morgan is a logging middleware for Node.js. It logs HTTP requests and responses and works with Node.js web frameworks like Express. Morgan logs various information for each request, including the IP address, the time the request was received, the URL requested and the HTTP method, the HTTP status code, and more.

Morgan follows the standard Apache common log output and provides different predefined formats, such as combined, common, dev, short, and tiny.

Here’s an example of creating a simple logger using the combined output:

const express = require("express"); 
const morgan = require("morgan"); 
const app = express();
// Set up Morgan to log requests using the combined format
app.use(morgan("combined"));
// Define your routes and other middleware...
app.listen(3000, () => {
   console.log("Server started on port 3000"); 
});

Here’s the result:

Server started on port 3000 ::1 - - [20/May/2024:19:25:41 +0000] "GET / HTTP/1.1" 404 139 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"

Best Practices for Node.js Logging

Ultimately, the Node.js logging module you pick for your code will come down to both personal taste and business requirements. But there’s one thing that everyone can agree on; it’s not just how you log that matters, it’s also what you log.

Here are a few best practices that everyone should follow for creating proper logs no matter which Node.js module you use.

Use correct log levels

Log levels allow you to categorize logs based on severity. By assigning levels, you can filter and manage your logs appropriately. Here’s a list of common levels:

  • Info: Gives information on the regular operation of your application.
  • Debug: Although verbose, it gives detailed diagnostic information.
  • Warn: Used for non-critical issues that require attention.
  • Error: Gives information on errors and exceptions preventing the application from running successfully.
  • Fatal: Used to capture information on severe errors that lead the app to terminate abruptly or crash.

When you use the correct logging levels, you ensure that your logs are meaningful. As a result, the logs provide the right level of detail based on your environment’s specific needs.

Add context

Providing sufficient context in log messages is relevant for effective debugging and troubleshooting. Without adequate context, it can be challenging to understand the circumstances surrounding a logged event. In addition to the logged message, add metadata that gives context to the application’s state. For example, you can include user information like ID or email, request details (URL, methods, headers), application version, request IDs, transaction IDs, and more.

Here’s an example of providing context to your logs using Pino:

const logger = require("pino")();
logger.info(
  { userID: "0809bd4e-58fa-46c6-a9bd-b1f8d171affa" },
  "User updated successfully"
);

The result looks like this:

{"level":30,"time":1716582173671,"pid":13771,"hostname":"MBP.local","userID":"0809bd4e-58fa-46c6-a9bd-b1f8d171affa","msg":"User creation successful"}

The userID adds context to the log output, allowing you to identify the updated user easily.

Avoid Logging Sensitive Information

Logging sensitive information—such as passwords, API keys, or personal data—can pose a significant security risk if an unauthorized individual gains access to the logs. One way to prevent this is to configure your logging library to redact specific fields based on a pattern.

Here’s an example of how you can configure Pino to redact passwords:

const pino = require("pino");
 const logger = pino({
   redact: ["password"],
 });
 logger.info(
   { password: "myPassword", other: "data" },
   "Sensitive log message" 
);

The resulting log looks like this:

{"level":30,"time":1716580274830,"pid":3748,"hostname":"MBP.local","password":"[Redacted]","other":"data","msg":"Sensitive log message"}

Add Timestamps

Knowing exactly when a log message was created allows you to retrace your app’s events and find an issue’s root cause. In other words, timestamps help to ensure you’re looking at a list of events in the correct order. And timestamps become even more important when you’re correlating log data across multiple apps and services.

Winston allows you to add them to your log messages through its format option. These formats are implemented in logform, a separate module to Winston. Let’s expand our example to customize the format of your log messages using the printf function.

const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format; 

const myFormat = printf(({ level, message, timestamp }) => { 
     return `${timestamp} ${level}: ${message}`;
 }); 
const logger = createLogger({ 
     level: 'info', 
     format: combine(
       timestamp(),
       myFormat
     ), 
     transports: [
       // 
       // - Write to all logs with level `info` and below to `combined.log` 
       // - Write all logs error (and below) to `error.log`.
       // 
       new transports.File({ filename: 'error.log', level: 'error' }), 
       new transports.File({ filename: 'combined.log' }) 
   ] 
}); 
logger.info('Info string'); 
logger.error('Error string');

In this example, the resulting output in the combined.log file looks like this:

2025-04-24T11:58:55.786Z info: Info string 
2025-04-24T11:58:55.787Z error: Error string

Include Tags

When working with complex apps that have many components, including tags in your logs lets you filter out messages you don’t want, so you can focus on the ones that matter. Beyond the benefit tags provide when searching through log files manually with grep, cloud-based log aggregation tools run searches and filter log data through tags as well.

For Winston, the format option comes to the rescue once again. Let’s expand our example further to include our script’s filename in the log messages, creating a kind of namespace:

const { createLogger, format, transports } = require('winston'); 
const { combine, timestamp, label, printf } = format; 

const myFormat = printf(({ level, message, label, timestamp }) => {
 return `${timestamp} [${label}] ${level}: ${message}`;
 }); 

var filename = module.filename.split('/').slice(-1);

const logger = createLogger({
     level: 'info',
     format: combine(
     label({ label: filename }),
     timestamp(),
     myFormat, 
     format.json()
     ),
transports: [ 
     // 
     // - Write to all logs with level `info` and below to `combined.log`
     // - Write all logs error (and below) to `error.log`. 
     // 
     new transports.File({ filename: 'error.log', level: 'error' }), 
     new transports.File({ filename: 'combined.log' }) 
     ]
}); 
logger.info('Info string'); 
logger.error('Error string');

Using SolarWinds Papertrail Logging

Cloud-based log aggregation and analysis tools such as SolarWinds Papertrail make uncovering insights in your log data easy. Using SolarWinds Papertrail, you can consolidate your logs into a single location and use advanced searching and filtering to spot trends and troubleshoot issues using your logs. You can view and search events in real time, and run filters against incoming messages by time period, origin, or message content.

If you want to take monitoring up a step, SolarWinds Papertrail works with SolarWinds Observability SaaS Application Performance Monitoring (APM), to provide continuous application performance monitoring. With the unified SolarWinds Observability SaaS platform, you can automatically add tracing and performance metrics to your monitoring program and uncover performance issues, such as a memory leak or long-running processes blocking the Node.js event loop before users are impacted.

Adding trace context to application logs provides the ability to correlate the log messages from a traced transaction, and if sampled, the log messages to the transaction trace detail. Check the supported components page for the complete list of supported logging packages and versions.

Application logs for your service entity are not included in the data sent by APM libraries. Configure your server or application to send application logs to SolarWinds Observability SaaS. See this page for details.

If trace context is added to logs and those logs are sent to SolarWinds Papertrail, use the Traces Explorer to see the logs associated with a traced transaction. Automatic insertion adds the trace context fields to the log record. See this page for details.

Aggregate, organize, and manage your logs

  • Collect real-time log data from your applications, servers, cloud services, and more
  • Search log messages to analyze and troubleshoot incidents, identify trends, and set alerts
  • Create automated backups, and archives of up to a year of historical data
Start Free Trial

Fully Functional for 30 Days

Let's talk it over

Contact our team, anytime.