DEV Community

Cover image for Better logs for ExpressJS using Winston and Morgan with Typescript
Andrea Vassallo
Andrea Vassallo

Posted on

Better logs for ExpressJS using Winston and Morgan with Typescript

A step-by-step guide on how to configure an ExpressJS application with Winston and Morgan loggers using Typescript

A great log system is one of the easiest ways to check your application behavior and it’s our first weapon to look into bugs

If you are starting or have already started an ExpressJS application, you may have wondered, how can I create a great and well-organized log system?

The problem is that a lot of applications don’t have a comprehensive log system, or even worse, they use simple console.log everywhere.

In this article, you’ll find out how to configure logs using Winston and Morgan.

TL;DR;

Here you can find the fully configured project (Use the branch called complete)

I didn’t add unit tests in this article but the code below is fully tested. You can find all the tests inside the repository above.

Do you need a great template to start your ExpressJS GraphQL APIs? Use mine: https://github.com/vassalloandrea/express-template

Let’s start

First of all, we need an ExpressJS application. You can clone this repository.

git clone https://github.com/vassalloandrea/medium-morgan-winston-example.git
Enter fullscreen mode Exit fullscreen mode

Start the server

The project was created using a from-scratch basic configuration. Start the server using this command:

cd medium-morgan-winston-example
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Install Winston

Winston is a useful library that is needed to configure and customize the application logs accessing a lot of helpful features.

To use the plainconsole.log without a third-party library, we need to write a lot of code and reinvent the wheel understanding all the edge cases caught by Winston in these years.

Here there are the main features that we should implement inside our project:

  • Differentiate log levels: error, warn, info, HTTP, debug

  • Differentiate colors adding one to each log level

  • Show or hide different log levels based on the application ENV; e.g., we won’t show all the logs when the application runs in production.

  • Adding a timestamp to each log line

  • Save logs inside files

npm install winston
Enter fullscreen mode Exit fullscreen mode

Configure Winston

In the lines below, there is a simple configuration of our logger. Copy and paste them inside your project. You can use this path: src/lib/logger.ts or something similar.

I’ll explain every single row later.

import winston from 'winston'

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
}

const level = () => {
  const env = process.env.NODE_ENV || 'development'
  const isDevelopment = env === 'development'
  return isDevelopment ? 'debug' : 'warn'
}

const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
}

winston.addColors(colors)

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.colorize({ all: true }),
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
  ),
)

const transports = [
  new winston.transports.Console(),
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error',
  }),
  new winston.transports.File({ filename: 'logs/all.log' }),
]

const Logger = winston.createLogger({
  level: level(),
  levels,
  format,
  transports,
})

export default Logger
Enter fullscreen mode Exit fullscreen mode

Now you have the ability to use the Logger function anywhere inside your application importing it.

Go to the index.ts file where the ExpressJS server is defined and replace all the console.log with the custom Logger methods.

import express from "express";

import Logger from "./lib/logger";

const app = express();
const PORT = 3000;

app.get("/logger", (_, res) => {
  Logger.error("This is an error log");
  Logger.warn("This is a warn log");
  Logger.info("This is a info log");
  Logger.http("This is a http log");
  Logger.debug("This is a debug log");

  res.send("Hello world");
});

app.listen(PORT, () => {
  Logger.debug(`Server is up and running @ http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

See the result starting the server and navigating through the logger endpoint:

server start

As you can see, the logger shows logs using different colors based on their severity, and another important feature is that all these logs are also printed out inside the all.log and error.log files under the logs directory.

Learn more about the configuration

The configuration file is straightforward. Check the comments inside the file below.

import winston from 'winston'

// Define your severity levels. 
// With them, You can create log files, 
// see or hide levels based on the running ENV.
const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
}

// This method set the current severity based on 
// the current NODE_ENV: show all the log levels 
// if the server was run in development mode; otherwise, 
// if it was run in production, show only warn and error messages.
const level = () => {
  const env = process.env.NODE_ENV || 'development'
  const isDevelopment = env === 'development'
  return isDevelopment ? 'debug' : 'warn'
}

// Define different colors for each level. 
// Colors make the log message more visible,
// adding the ability to focus or ignore messages.
const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
}

// Tell winston that you want to link the colors 
// defined above to the severity levels.
winston.addColors(colors)

// Chose the aspect of your log customizing the log format.
const format = winston.format.combine(
  // Add the message timestamp with the preferred format
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  // Tell Winston that the logs must be colored
  winston.format.colorize({ all: true }),
  // Define the format of the message showing the timestamp, the level and the message
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
  ),
)

// Define which transports the logger must use to print out messages. 
// In this example, we are using three different transports 
const transports = [
  // Allow the use the console to print the messages
  new winston.transports.Console(),
  // Allow to print all the error level messages inside the error.log file
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error',
  }),
  // Allow to print all the error message inside the all.log file
  // (also the error log that are also printed inside the error.log(
  new winston.transports.File({ filename: 'logs/all.log' }),
]

// Create the logger instance that has to be exported 
// and used to log messages.
const Logger = winston.createLogger({
  level: level(),
  levels,
  format,
  transports,
})

export default Logger
Enter fullscreen mode Exit fullscreen mode

Take stock of the situation

Now we have the ability to instrument the application code adding logs based on the complexity of the feature.

With Winston, you can also change the log severity at the runtime using an ENV variable.

Since ExpressJS is made to handle requests, we should add a request logger that automatically logs every request information. The goal should be reached using a library that can be easily integrated with the Winston configuration.

Install Morgan

Morgan is a NodeJS middleware that is needed to customize request logs.

The integration with Winston it’s very simple. Do you remember the HTTP severity level that we added to the Winston configuration? Well, it was made to be used only from the Morgan middleware.

npm install morgan @types/morgan
Enter fullscreen mode Exit fullscreen mode

Configure Morgan

In the lines below, there is a simple configuration of the Morgan middleware. Copy and paste them inside your project. You can use this path: src/config/morganMiddleware.ts or something similar.

Read the comments to understand or extend the configuration below.

import morgan, { StreamOptions } from "morgan";

import Logger from "../lib/logger";

// Override the stream method by telling
// Morgan to use our custom logger instead of the console.log.
const stream: StreamOptions = {
  // Use the http severity
  write: (message) => Logger.http(message),
};

// Skip all the Morgan http log if the 
// application is not running in development mode.
// This method is not really needed here since 
// we already told to the logger that it should print
// only warning and error messages in production.
const skip = () => {
  const env = process.env.NODE_ENV || "development";
  return env !== "development";
};

// Build the morgan middleware
const morganMiddleware = morgan(
  // Define message format string (this is the default one).
  // The message format is made from tokens, and each token is
  // defined inside the Morgan library.
  // You can create your custom token to show what do you want from a request.
  ":method :url :status :res[content-length] - :response-time ms",
  // Options: in this case, I overwrote the stream and the skip logic.
  // See the methods above.
  { stream, skip }
);

export default morganMiddleware;
Enter fullscreen mode Exit fullscreen mode

Add this middleware to the ExpressJS server inside the index.ts file:

import morganMiddleware from './config/morganMiddleware'

...
...

const PORT = 3000;

app.use(morganMiddleware)

app.get("/logger", (_, res) => {

...
Enter fullscreen mode Exit fullscreen mode

Start the server and make the request visiting the logger endpoint:

Alt Text

Here are other examples of request logs:

Alt Text

Enjoy the configuration

That’s all! I hope that this configuration will help you all to instrument your code, finding hidden bugs more easily. 🐛

Did you like this article? Let me know with a clap and a comment 🙏

Do you need help with your ExpressJS app? Hire me

Deepening with the GraphQL Morgan configuration

This section is just a deepening for projects that use GraphQL.

GraphQL has just one route by default, so we need to change the Morgan configuration to make sense.

import morgan, { StreamOptions } from "morgan";

import { IncomingMessage } from "http";

import Logger from "../lib/logger";

interface Request extends IncomingMessage {
  body: {
    query: String;
  };
}

const stream: StreamOptions = {
  write: (message) =>
    Logger.http(message.substring(0, message.lastIndexOf("\n"))),
};

const skip = () => {
  const env = process.env.NODE_ENV || "development";
  return env !== "development";
};

const registerGraphQLToken = () => {
  morgan.token("graphql-query", (req: Request) => `GraphQL ${req.body.query}`);
};

registerGraphQLToken();

const morganMiddleware = morgan(
  ":method :url :status :res[content-length] - :response-time ms\n:graphql-query",
  { stream, skip }
);

export default morganMiddleware;
Enter fullscreen mode Exit fullscreen mode

Latest comments (5)

Collapse
 
codewithrabeeh profile image
Rabeeh Ebrahim

Very informative blog! Thank you.

Collapse
 
larrynavarr profile image
Larry Navarrete

Thanks, it was very clear and easy to understand. It worked for me.

Collapse
 
jdriesen profile image
Johnny Driesen

Amazing & super detailed info.
Works like a charm.
Thanks for your time to write this article.
Regards,
Johnny - Belgium

Collapse
 
celeron profile image
Khushal Bhardwaj

Thank you it worked for me.
I was applying the same implementation of this logger setup that I had in a js based project.

Collapse
 
placideirandora profile image
Placide IRANDORA

Thank you for sharing this productive information. It has helped a lot!