Logging in NodeJS: Why Pino Logger is Your New Best Friend

Ever felt lost in a sea of console.log statements?

Tired of slow, clunky logging slowing down your app?

Pino Logger might just be the lifesaver you need.

Here's the deal:

  • Pino is fast. Really fast.
  • It's lightweight, so your app stays snappy.
  • Structured logging? Pino's got you covered.

This guide will show you:

  • How to set up Pino in your NodeJS app
  • Cool tricks to make logging work for you
  • Ways to level up your debugging game

Whether you're just starting out or you're a seasoned pro, there's something here for you.

Ready to make your NodeJS logging actually useful?

Let's dive in.

What is Pino Logger?

Pino is a fast and low-overhead logging library designed specifically for NodeJS applications. Pino produces logs in a structured JSON format, which is easy to parse and analyze, making it ideal for production environments. Pino logger is a great option for developers searching for a dependable and adaptable logging solution. This is due to its adaptability and simplicity of interaction with other Node.js web frameworks.

Key Features of Pino

  • High Performance: Pino is designed to be one of the fastest logging libraries for NodeJS, ensuring that logging operations do not significantly impact application performance.
  • Low Overhead: Pino minimizes the overhead associated with logging, making it suitable for applications that require high throughput and low latency.
  • Structured Logs: Logs are output in JSON format, which is both machine-readable and easy to parse, facilitating efficient log analysis and processing.
  • Extensibility: Pino supports various plugins and integrations, allowing developers to extend its functionality to meet specific logging needs.
  • Custom Serializers: Pino allows the use of custom serializers to transform log data, enabling the redaction of sensitive information or formatting of complex data structures.
  • Multiple Log Levels: Pino supports various log levels (fatal, error, warn, info, debug, trace), providing granular control over log verbosity.
  • Asynchronous Logging: Pino can offload logging operations to a separate process, reducing the impact on the main application thread and improving performance.
  • Child Loggers: Child loggers inherit the configuration of the parent logger but can add additional context, useful for logging within specific modules or components.
  • Pretty Printing: Pino offers pretty printing of logs for development purposes, making them human-readable and easier to debug.

Installing and Setting Up Pino

Before moving forward with the implementation, let's start with installing Pino and setting it up in a Node.js application. First, ensure you have Node.js and npm (Node Package Manager) installed on your machine. You can download them from Node.js official website.

To install Pino, you can use npm

npm install pino --save
Installing Pino via npm
Installing Pino via npm

Installing Pino via npm

Basic Setup

Let us learn how to set up a basic Pino Logger, which involves creating and using a logger instance to log messages. Here's a simple example:

const pino = require('pino')
const logger = pino()

logger.info('This is an info message')
logger.error('This is an error message')

This code initializes Pino and logs an info and an error message. By default, the logs are output to the console in JSON format.

Output:

{"level":30,"time":1688510400000,"pid":12345,"hostname":"Anubhavs-MacBook-Air.local","msg":"This is an info message"}
{"level":50,"time":1688510401000,"pid":12345,"hostname":"Anubhavs-MacBook-Air.local","msg":"This is an error message"}

Configuring Pino Logger

Pino offers various configuration options to tailor the logging behaviour to your needs. Two key configurations include setting log levels and defining custom serializers.

Level Configuration

Pino Logger supports different log levels to control the verbosity of logs. Log levels are essential for filtering the logs based on their importance and severity. Pino’s log levels, in order of severity from highest to lowest, are:

  • fatal: Logs critical issues that cause the application to crash.
  • error: Logs errors that require attention but don't crash the application.
  • warn: Logs warnings about potential issues that might need investigation.
  • info: Logs general information about the application's operation.
  • debug: Logs detailed debugging information for troubleshooting.
  • trace: Logs highly detailed information, typically used for tracing code execution.

You can set the log level when creating the logger instance to filter logs accordingly:

const logger = pino({ level: 'debug' })

logger.debug('This is a debug message')
logger.info('This is an info message')

In this example, the logger is configured to log messages at the debug level and above, meaning it will log debug, info, warn, error, and fatal messages.

If you try to log a message below the level with which the logger is initialized, that message will not be logged. For instance, if the logger is set to info level:

const logger = pino({ level: 'info' })

logger.debug('This debug message will not be logged')
logger.info('This info message will be logged')

In this case, the debug message will not appear in the logs because the logger is set to only log messages at info level and above (i.e., info, warn, error, and fatal).

Pino also supports dynamic changes to the log level at runtime. This feature is particularly useful in production environments where you may need to increase or decrease logging verbosity on-the-fly to diagnose issues or reduce log noise without restarting the application.

You can use Pino's level method to change the log level dynamically. Here's an example:

// logger.js
import pino from 'pino'

const logger = pino({ level: 'info' })

export default logger
// app.js
import express from 'express'
import logger from './logger.js'

const app = express()

app.get('/', (req, res) => {
  logger.info('Info level log - Home route accessed')
  logger.debug('Debug level log - Home route accessed')
  res.send('Hello, World!')
})

// Route to change log level
app.post('/log-level', (req, res) => {
  const newLevel = req.query.level
  if (newLevel && ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes(newLevel)) {
    logger.level = newLevel
    res.send(`Log level changed to ${newLevel}`)
  } else {
    res.status(400).send('Invalid log level')
  }
})

// Start the server
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  logger.info(`Server is running on port ${PORT}`)
})

Custom Serializers

Custom serializers allow you to transform log data before it is written to the log output. This feature is handy for redacting sensitive information or formatting complex data structures for better readability and security.

To define a custom serializer, you specify it in the Pino configuration. The serializer is a function that takes an object as an input and returns a transformed version of that object. Here’s an example:

const pino = require('pino')

const logger = pino({
  serializers: {
    user: (user) => {
      return {
        id: user.id,
        name: user.name,
        // Redacting sensitive information
        email: user.email ? user.email.replace(/(.{2})(.*)(@.*)/, '$1***$3') : undefined,
      }
    },
  },
})

logger.info({ user: { id: 1, name: 'Alice', email: 'alice@example.com' } }, 'User info')

Output:

Custom Serializers in Pino - Output
Custom Serializers in Pino - Output

Custom Serializers in Pino - Output

In this example, the user serializer transforms the user object to redact the email address, ensuring sensitive information is not exposed in the logs. Custom serializers can be applied to any object you log, making them a powerful tool for maintaining security and readability in your logs.

Logging HTTP requests with Pino

Pino Logger can be integrated with HTTP servers to automatically log incoming requests and outgoing responses, providing valuable insights into your application's HTTP traffic. This is particularly useful for monitoring, debugging, and analyzing the performance and behaviour of web applications.

Using Pino HTTP Middleware

You can log HTTP requests and responses using the pino-http module, which integrates Pino with HTTP servers like Express. This middleware logs details about each request and response, including HTTP method, URL, status code, and response time.

Step 1: Install the pino-http module

npm install pino --save

Step 2: Integrate pino-http into your application

Here’s an example using Express:

const express = require('express')
const pino = require('pino')
const pinoHttp = require('pino-http')

const logger = pino()
const httpLogger = pinoHttp({ logger })

const app = express()

// Use Pino HTTP middleware
app.use(httpLogger)

app.get('/', (req, res) => {
  res.send('Hello, world!')
})

app.listen(3000, () => {
  logger.info('Server is running on port 3000')
})

Output -

{"level":30,"time":1720120558325,"pid":18553,"hostname":"Anubhavs-MacBook-Air.local","msg":"Server is running on port 3000"}

In this example, the Pino HTTP middleware is added to the Express application using app.use(httpLogger). This ensures that all incoming HTTP requests and outgoing responses are logged automatically.

Advanced Features of Pino

Pino Logger provides several advanced features that can enhance your logging setup and make it more powerful and flexible. Here are some of them-

Child Loggers

Child loggers are useful when you want to create a logger that inherits the configuration of a parent logger but adds additional context. This is particularly useful for logging within specific modules or components of your application.

For example, you might have a main logger for your application, but you want to add module-specific context for logs generated by different parts of your application:

const pino = require('pino')
const logger = pino({ level: 'info' })

const childLogger = logger.child({ module: 'user-service' })

logger.info('This is a log message from the main logger')
childLogger.info('This is a log message from the user service')

In this example, the childLogger inherits the configuration from the logger but adds the module: 'user-service' context to each log message it generates. This helps in categorizing and differentiating logs from various parts of your application. It is often helpful in large complex applications where each module might generate numerous log messages. By using child loggers, you can easily identify which module a particular log message came from, making it easier to troubleshoot issues and monitor specific parts of the application.

Asynchronous Logging

Asynchronous logging helps to offload logging operations to a separate process, reducing the impact on the main application thread and improving performance. This is particularly useful in high-performance applications where logging needs to be done without blocking the main execution flow.

To set up asynchronous logging with Pino using transports, you define where and how the logs are written, which helps offload I/O operations from the main application thread. Transports can write logs to files, send them to remote servers, or integrate with other logging services.

const pino = require('pino')
const transport = pino.transport({
  target: 'pino/file',
  options: { destination: './logfile' },
})

const logger = pino(transport)

logger.info('This message will be logged asynchronously')

In this example, logs are written asynchronously to a file using the pino/file transport. This offloads the I/O operations from the main application thread, enhancing overall performance.

Pretty Printing

While JSON logs are great for machines, they can be difficult to read for humans. For development and debugging purposes, you can use the pino-pretty module to format logs in a more readable way:

First, install the pino-pretty module:

npm install pino-pretty

Then, integrate pino-pretty with Pino:

const pino = require('pino')
const pretty = require('pino-pretty')

const logger = pino(pretty())

const complexLog = {
  level: 'info',
  time: new Date(),
  user: {
    id: '12345',
    name: 'John Doe',
    email: 'john.doe@example.com',
    roles: ['admin', 'user'],
  },
  request: {
    id: 'abcd-1234-efgh-5678',
    method: 'GET',
    url: '/api/resource',
    headers: {
      'user-agent': 'Mozilla/5.0',
      'accept-language': 'en-US',
    },
    params: {
      id: '7890',
    },
    query: {
      search: 'example',
      page: 1,
      limit: 10,
    },
  },
  response: {
    statusCode: 200,
    body: {
      data: [
        { id: 1, name: 'Resource 1' },
        { id: 2, name: 'Resource 2' },
      ],
      metadata: {
        count: 2,
        total: 2,
      },
    },
  },
}

logger.info(complexLog)

Output-

[02:21:50.283] INFO (26411):
    user: {
      "id": "12345",
      "name": "John Doe",
      "email": "john.doe@example.com",
      "roles": [
        "admin",
        "user"
      ]
    }
    request: {
      "id": "abcd-1234-efgh-5678",
      "method": "GET",
      "url": "/api/resource",
      "headers": {
        "user-agent": "Mozilla/5.0",
        "accept-language": "en-US"
      },
      "params": {
        "id": "7890"
      },
      "query": {
        "search": "example",
        "page": 1,
        "limit": 10
      }
    }
    response: {
      "statusCode": 200,
      "body": {
        "data": [
          {
            "id": 1,
            "name": "Resource 1"
          },
          {
            "id": 2,
            "name": "Resource 2"
          }
        ],
        "metadata": {
          "count": 2,
          "total": 2
        }
      }
    }

This setup will format the log output to be more human-readable, making it easier to debug and understand the logs during development. The pino-pretty module supports various formatting options, such as color coding and timestamp formatting, to enhance readability further.

Why Sending Logs to an Observability Platform is Helpful

Implementing logging in your NodeJS application with Pino Logger is just the first step. To truly leverage the power of logs, sending them to an observability platform like SigNoz can provide numerous benefits:

  1. Centralized Log Management: All your logs are in one place, making it easier to search and analyze.
  2. Real-time Monitoring: Spot issues as they happen, not after they've impacted users.
  3. Advanced Search and Filtering: Quickly find relevant logs across your entire system.
  4. Correlation with Metrics and Traces: Connect logs with other telemetry data for deeper insights.
  5. Anomaly Detection: Identify unusual patterns that might indicate problems.
  6. Historical Analysis: Understand trends and patterns over time to prevent future issues.
  7. Compliance and Auditing: Maintain records for regulatory requirements and security audits.
  8. Team Collaboration: Share log data easily across your organization for faster problem-solving.

Monitoring Logs with SigNoz

SigNoz is an open-source observability platform that offers a unified solution for logs, metrics, and traces. Here's how you can set it up to monitor your NodeJS application logs:

  1. SetUp SigNoz

    SigNoz cloud is the easiest way to run SigNoz. Sign up for a free account and get 30 days of unlimited access to all features. Try SigNoz Cloud
CTA You can also install and self-host SigNoz yourself since it is open-source. With 16,000+ GitHub stars, open-source SigNoz is loved by developers. Find the instructions to self-host SigNoz.

  2. Install Dependencies:

    npm install pino pino-opentelemetry-transport
    
  3. Configure Pino Logger: Set up Pino to send logs to OpenTelemetry:

    // logger.js
    import pino from 'pino';
    
        const logger = pino({
          transport: {
            targets: [
              {
                target: 'pino-opentelemetry-transport',
                options: {
                  resourceAttributes: {
                    'service.name': 'your-service-name',
                  },
                },
              },
              {
                target: 'pino-pretty',
                options: { colorize: true },
              },
            ],
          },
        });
    
        export default logger;
    
        ```
    
    
  4. Use Logger in Your Application: Import and use the logger in your app:

    import logger from './logger.js';
    
        // Log messages
        logger.info('Server started');
        logger.error({ err }, 'An error occurred');
    
        ```
    
    
  5. Configure OpenTelemetry: Ensure your OpenTelemetry setup includes log export to SigNoz.

  6. View Logs in SigNoz: Access the SigNoz dashboard to view, search, and analyze your logs alongside metrics and traces.

    Viewing Logs in Signoz
    Viewing Logs in Signoz

    Viewing Logs in Signoz

For a detailed guide on setting up logging with OpenTelemetry and SigNoz, check out our comprehensive tutorial: Logging in NodeJS with OpenTelemetry

By following these steps, you'll have a powerful logging setup that integrates seamlessly with SigNoz, giving you deep insights into your NodeJS application's behavior and performance.

Pino Logger in Node.Js Complete Example

Here's a real-world example demonstrating how to use Pino in a Node.js application to log HTTP requests and responses:

const express = require('express')
const pino = require('pino')
const pinoHttp = require('pino-http')
const pretty = require('pino-pretty')

const logger = pino(pretty())

// Initialize Pino HTTP middleware
const httpLogger = pinoHttp({ logger })

const app = express()

// Use Pino HTTP middleware
app.use(httpLogger)

// Sample route
app.get('/', (req, res) => {
  req.log.info('Home route accessed')
  res.send('Hello, World!')
})

app.get('/user/:id', (req, res) => {
  const userId = req.params.id
  req.log.info({ userId }, 'User route accessed')
  res.send(`User ID: ${userId}`)
})

// Error handling middleware
app.use((err, req, res, next) => {
  req.log.error({ err }, 'Unhandled error')
  res.status(500).send('Something went wrong')
})

// Start the server
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  logger.info(`Server is running on port ${PORT}`)
})

Now execute node server.js to start the Node.js server.

Output

Running The Server
Running The Server

Running The Server

Verifying Server in localhost
Verifying Server in localhost

Verifying Server in localhost

Best Practices for Using Pino Logger

  • Set Appropriate Log Levels: Use the appropriate log levels (info, warn, error, etc.) to ensure clarity and relevance of logs.
  • Avoid Over-Logging: Be mindful of logging too much information, which can impact performance and readability.
  • Use Child Loggers: Utilize child loggers for modular applications to maintain context and simplify log management.
  • Handle Errors Gracefully: Implement error handling mechanisms to capture and log errors effectively.

Comparing Pino Logger vs. Regular Logger

When choosing a logging solution for your Node.js application, it's essential to understand the differences between Pino and other regular loggers. Here's a comparison highlighting the key aspects:

FeaturePino LoggerRegular Logger
PerformanceHigh performance and low overhead. Uses binary serialization for minimal impact on application performance.May have higher overhead and slower performance due to more complex features and less optimized serialization methods.
Log FormatJSON formatted by default, easy to parse and integrate with various log management tools.Formats vary; some use JSON while others use plain text or custom formats, making them less standardized and harder to process automatically.
Ease of UseSimple to set up and use with minimal configuration.Offers more configuration options and features, which can be beneficial for complex logging needs but may require more setup and maintenance.
Use CasesIdeal for high-performance logging with minimal overhead, suitable for microservices, serverless functions, and real-time applications.Suitable for applications needing extensive logging features, customizations, and integrations, such as enterprise applications with complex logging requirements.
Troubleshooting and DebuggingProvides clear log output for easy identification of errors, warnings, and unexpected behaviors. Tools like pino-pretty help format and visualize logs for easier analysis.Depending on the logger, may provide extensive features for log management and analysis but can be more complex to navigate and configure for troubleshooting.

Conclusion

  • Pino is a fast, lightweight logging library for Node.js, ideal for applications where performance and speed are crucial.
  • Pino offers high performance, being one of the fastest logging libraries available.
  • It provides simple integration and configuration like logging levels, custom serializers, and child loggers making it easy to get started.
  • The pino-pretty module provides human-readable log formats. while the pino-http middleware seamlessly logs HTTP requests and responses.
  • Pino can be integrated with observability platforms like Signoz for centralized log management and analysis.
  • Leveraging Pino can lead to better log management, enhanced application monitoring, and improved overall performance.

FAQs

What is Pino package?

The Pino package is a Node.js logging library designed for speed and efficiency, offering robust features and flexible configuration options for developers.

What is the common log format in Pino?

Pino uses a default JSON-based log format, which is structured and easy to parse for automated processing and analysis.

What is Pino pretty?

Pino pretty (pino-pretty) is a tool that formats Pino's JSON logs into a more human-readable format, enhancing readability during development and debugging.

What are the benefits of Pino logger?

Pino provides benefits such as high performance, low CPU overhead, simple integration, and support for custom serializers and log levels, making it ideal for production environments.

Can Pino handle asynchronous logging?

Yes, Pino supports asynchronous logging, which helps maintain high performance and low overhead by not blocking the main event loop during log operations.

Is it possible to change the log level in Pino dynamically?

Yes, Pino allows dynamic changes to the log level at runtime. This can be useful for adjusting the verbosity of logs without restarting the application, especially in production environments.

What is a child logger?

A child logger in Pino inherits settings and behaviours from a parent logger, allowing developers to manage and customize logging behaviour in modular applications.

What is Pino transport?

Pino transport refers to modules that facilitate the transportation of log messages to external services or destinations, enhancing Pino's versatility in logging.

What is Pino HTTP?

Pino HTTP (pino-http) is middleware for Node.js applications that automatically logs HTTP requests and responses, providing detailed insight into application traffic.

What are the three types of logs?

The three primary log levels in Pino are info, warn, and error, each indicating different levels of severity and importance in logging and monitoring applications.