Logging in Express.js

02 Mins

Logging is an essential part of backend development. It helps developers:

  • Debug issues
  • Monitor application behavior
  • Track incoming requests
  • Detect failures
  • Analyze production systems

While console.log() works for small applications, production systems require structured and scalable logging solutions.

In Express.js applications, one of the most popular logging libraries is Pino.

Why Use a Logging Library?

Logging libraries provide features like:

  • Structured JSON logs
  • Log levels (info, warn, error, debug)
  • Request tracing
  • Better performance
  • Integration with monitoring tools

Structured logging becomes extremely valuable in production environments and distributed systems.


Installing Pino

npm install pino pino-http
  • pino → Core logger
  • pino-http → Express/HTTP request logging middleware

Creating a Logger

import pino from "pino";

const logger = pino({
    level:
        process.env.NODE_ENV === "production"
            ? "info"
            : "debug",
});

The log level controls which logs are displayed.

Common levels:

LevelPurpose
debugDetailed debugging information
infoGeneral application events
warnWarnings or unexpected behavior
errorApplication errors

Integrating Logging with Express.js

import express from "express";

import pino from "pino";
import pinoHttp from "pino-http";

const app = express();

const logger = pino({
    level:
        process.env.NODE_ENV === "production"
            ? "info"
            : "debug",
});

// HTTP logging middleware
app.use(
    pinoHttp({
        logger,
    })
);

pino-http automatically logs: Incoming requests, Response status codes, Response times, Request metadata


Logging Inside Routes

Once configured, every request gets a request-specific logger attached to req.log

Example:

router.get("/", (req, res) => {
    req.log.info("Fetching users");

    res.json(users);
});

This automatically includes useful request metadata such as request IDs and HTTP information.


Production Logging

  • Use Log Streams Instead of printing logs directly to the console, pipe them to a file or external service
node app.js | pino-pretty >> logs/app.log

Note - pino-pretty → formats logs for readability in development

  • Request IDs / Correlation IDs Attach a unique ID to each request so you can trace a single user’s journey across multiple logs.
import { v4 as uuid } from "uuid";

app.use((req, res, next) => {
  req.id = uuid(); // Generate a UUID per request
  req.log = logger.child({ reqId: req.id }); // Attach it to req.log so every log line carries the ID
  res.setHeader("X-Request-ID", req.id); // propagate to client
  next();
});
  • Centralized Log Management

Local log files don’t scale; you need aggregation, search, and alerts. Send logs to external systems like Elasticsearch + Kibana, Grafana Loki, Datadog, CloudWatch, or Splunk. Define retention policies (e.g., 30 days) and alerting rules for errors.

import pinoElastic from "pino-elasticsearch";

const stream = pinoElastic({
  index: "express-logs", // name of the log index
  node: "http://localhost:9200", // Elasticsearch endpoint
});

const logger = pino({ level: "info" }, stream);

This means every logger.info(…) or req.log.error(…) call will be shipped directly into Elasticsearch under the express-logs index. Use Kibana (Elastic’s dashboard tool) to visualize and search logs.