Creating a Logger in Node Express using Morgan and Winston

In this blog, learn how to set up a robust logging system in a Node.js Express app using Morgan for HTTP request logging and Winston for general logging tasks like errors and debugging. Combine these two libraries for clearer, more structured logs, and boost application transparency.

Creating a Logger in Node Express using Morgan and Winston

Logging is one of the most crucial aspects of any backend application. Whether you're debugging, monitoring performance, or tracking user activities, a reliable logging system helps you maintain application transparency. In a Node.js Express application, you can use popular logging libraries like Morgan for HTTP request logging and Winston for general application logging.

In this blog post, we’ll walk through creating an efficient logging middleware using Morgan for HTTP logging and Winston for other logging tasks like errors, warnings, and debug messages.

Why Use Morgan and Winston?

  • Morgan is a great tool for logging HTTP requests, providing insights into incoming requests and outgoing responses.
  • Winston is a more comprehensive logger, allowing us to categorize logs based on severity, output them in a structured format, and transport them to different locations like files, the console, or external services.

By combining these two powerful libraries, we get a robust logging solution that covers both HTTP requests and general application logs.

Setting Up HTTP Logging with Morgan

First, let's start by setting up HTTP request logging with Morgan.

Morgan allows you to define custom formats and tokens. For example, you might want to log the response time, HTTP status code, the user associated with the request, and other metadata. Here’s how you can create a custom format using Morgan and integrate it with your Express app.

import morgan, { FormatFn } from "morgan";
import { Request, Response } from "express";
import { ExtendedRequest } from "../domains/user/types";  // A custom type for request
import moment from "moment";

// Custom token to get the userId (or username) from the request
morgan.token("userId", (req: Request) =>
  (req as ExtendedRequest).user
    ? (req as ExtendedRequest).user.username
    : "guest",
);

// Define a custom logging format
const logFormat: FormatFn<Request, Response> = (tokens, req, res) => {
  const status = tokens.status(req, res);
  const statusColor =
    Number(status) >= 500
      ? "\x1b[31m" // Red for 500+ errors
      : Number(status) >= 400
        ? "\x1b[33m" // Yellow for 400+ errors
        : Number(status) >= 300
          ? "\x1b[36m" // Cyan for 300+ redirects
          : "\x1b[32m"; // Green for 200+ success

  const responseTime = tokens["response-time"](req, res) as string;
  const responseTimeColor =
    parseInt(responseTime, 10) >= 1000
      ? "\x1b[31m" // Red for response time > 1000ms
      : parseInt(responseTime, 10) >= 500
        ? "\x1b[33m" // Yellow for response time > 500ms
        : "\x1b[32m"; // Green for fast responses

  const userId = tokens.userId(req, res);
  const userIdColor = userId === "guest" ? "\x1b[90m" : "\x1b[37m"; // Gray for guests, white for users
  const date = moment().format("YYYY-MM-DD HH:mm:ss");

  // The custom format for logging
  return `${date} http: ${tokens.method(req, res)} ${tokens.url(req, res)} ${statusColor}${status}\x1b[0m ${responseTimeColor}${responseTime}ms\x1b[0m - ${tokens.res(req, res, "content-length") || "0"} bytes - ${userIdColor}${userId}\x1b[0m`;
};

// Create a Morgan HTTP logger middleware using the custom format
const httpLogger = morgan(logFormat);

export default httpLogger;

Breakdown of the Morgan Logger

  1. Custom Token: We added a custom token userId that extracts the user's ID (or username) from the request object. If the user is not authenticated, it returns "guest".
  2. Custom Format: The logFormat function customizes the way HTTP requests are logged:
    • Status Color: The HTTP status is color-coded for better readability. Errors (500 and above) are logged in red, warnings (400+) in yellow, redirects (300+) in cyan, and success (200+) in green.
    • Response Time: The response time is color-coded based on how fast the server responds.
    • User Identification: It logs the userId to associate requests with users or guests.
    • Content-Length: Logs the size of the response.
  3. Moment.js for Date Formatting: We use moment.js to format the date and time, making sure each log entry has a readable timestamp.

By adding this middleware to your Express app, every HTTP request will now be logged in a meaningful and visually distinct manner.

General Logging with Winston

While Morgan is great for HTTP request logging, you need a more flexible solution for other application logs, such as errors, warnings, and debugging information. This is where Winston comes in.

Setting Up Winston Logger

Winston allows you to create loggers with various transports (i.e., where the logs go: console, files, or external logging services). You can also assign log levels such as error, warn, info, http, and debug, each representing a different severity.

import winston from "winston";

// Define custom log levels
const levels = {
  error: 0,
  warn: 1,
  info: 2,
  debug: 3,
};

// Assign colors to each log level for better visibility
const colors = {
  error: "red",
  warn: "yellow",
  info: "blue",
  debug: "white",
};

winston.addColors(colors);

// Configure Winston's logging format
const format = winston.format.combine(
  winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
  winston.format.colorize(), // Colorize the output
  winston.format.printf(({ timestamp, level, message }) => {
    return `${timestamp} ${level}: ${message}`;
  }),
);

// Define where the logs should go (console in this case)
const transports = [new winston.transports.Console()];

// Create the Winston logger instance
const logger = winston.createLogger({
  level: "info", // Default logging level
  levels,        // Use the custom levels defined above
  format,        // Use the custom format defined above
  transports,    // Send logs to the console
});

export default logger;

Breakdown of the Winston Logger

  1. Custom Log Levels: We defined custom log levels (error, warn, info, and debug), each mapped to a severity. For instance, error is the most severe (0), while debug is the least severe (4).
  2. Colorized Log Levels: We also define custom colors for each log level. Errors are logged in red, warnings in yellow, informational logs in blue, and debug logs in white.
  3. Log Format: Using winston.format.combine(), we define the log output. Each log entry has a timestamp and the log level, with the message that was logged. The colorize() method makes the logs more readable.
  4. Transports: In this example, we’re only logging to the console using winston.transports.Console(). However, Winston supports multiple transports, so you could easily add file-based logging or send logs to an external service like AWS CloudWatch or Loggly.

This is what the logs will look like in the console. The format is shown below. From the logs, you can identify the type of message, such as 'info', along with the color you configured in the Winston setup.

Using the Loggers

First, add the Morgan logger as middleware in your server.ts file.

import express from "express";

import mainRouter from "./routes";
import httpLogger from "./libs/morgan";

const app = express();

app.use(httpLogger);
app.use(mainRouter);

app.get("/", (req, res) => {
  res.send("The Server is Running Fine 🚀");
});

export default app;

You can use the Winston logger anywhere in your application by importing it and calling the appropriate logging methods based on the severity.

import logger from "./middlewares/logger";

// Log an info message
logger.info("Server started successfully.");

// Log an error message
logger.error("Unable to connect to the database.");

// Log a debug message
logger.debug("Fetching user details from the database.");

With Morgan handling HTTP request logs and Winston taking care of general logging, you now have a robust and scalable logging system for your Node.js Express application.

  • Morgan logs incoming HTTP requests and responses with custom formats that include details like request time, status, response time, and user IDs.
  • Winston is used for more granular, application-wide logging, with the ability to create custom log levels, colors, and formats.

This dual-logger setup is both simple and effective. It provides clear, readable logs that can help track down issues, monitor performance, and analyze application behavior over time.

💡
One more thing you can do is pass the morgan logs through winston. This can be helpful if you want to store all the logs for future reference.

Next Steps

  • Consider adding file-based logging or cloud-based logging using Winston for production environments.
  • Explore log rotation for file logs, which is especially useful in production to manage log size.

By using these tools effectively, you'll make your application much easier to debug, monitor, and maintain.

That's it for today. See ya 👋


Your Name

Smruti Ranjan Badatya

Full Stack Developer @eLitmus

LinkedIn LinkedIn