The technology stacks used in this article are typescript, Express, Winston, and Morgan.

A good logging system is one of the easiest ways to check your application’s behavior, and it’s also our first weapon in bug hunting.

If you have an ExpressJS application, you might be wondering, how do you create a good, well-organized logging system?

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

In this article, you’ll learn how to configure logging using Winston and Morgan.

TL; DR;

You can view the complete configuration on GitHub (the Complete branch).

I didn’t add unit tests in this article, but the code below has been thoroughly tested. You can find all the tests in the repository above.

Let’s start

First, you need an Express application. You can download this project:

git clone https://github.com/vassalloandrea/medium-morgan-winston-example.git node-logging
Copy the code

Start the service:

cd node-logging
npm install
npm run dev
Copy the code

Install and configure Winston

Winston is a great library that configures your application’s logging and has customizable features.

To use console.log without a third-party library would require a lot of code, and Winston has covered all edge cases over the years, just use it.

Here are the main features we want to implement in the project:

  • Log levels are error, WARN, INFO, HTTP, and DEBUG
  • Log Level Color
  • Show or hide different levels of logs depending on ENV: for example, in a production environment we don’t show all log information.
  • Add a log timestamp
  • Logs are saved to a file

Next, install Winston:

npm install winston
Copy the code

In the code below, there is a simple configuration for a Logger. Copy and paste it into the project. You can use this path: SRC /lib/logger.ts.

I’ll explain the code in a moment.

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
Copy the code

Now you can use the Logger function in your application.

Go to the index.ts file, which is the location defined by the Express service, and use the Logger function to replace all console. logs.

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}`);
});
Copy the code

Restart the service and request the Logger service to view the log content:

As you can see, logs are printed in different colors depending on their severity, and another important point is that these logs are printed to all.log and error.log in the logs folder.

Next, let’s look at the Winston configuration we just introduced. You can check out the comments:

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
Copy the code

Now we can add a feature log to the application code. And with Winston, you can also change the severity of the log at run time using the ENV variable.

Since ExpressJS is used to handle requests, we should add a request Logger that automatically logs each request. You should use a library that can easily integrate with Winston to achieve this goal. It’s Morgan!

Install and configure Morgan

Morgan is a Node.js middleware for customizing request logs.

Integrating with Winston is simple. Earlier we also configured HTTP-level logging for Winston, which is only available in Morgan middleware.

npm install morgan @types/morgan
Copy the code

The following code is a simple configuration for Morgan. Copy and paste it into your project. You can put in SRC/config/morganMiddleware ts directory.

Configuration annotations are already there.

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";
  returnenv ! = ="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;
Copy the code

Then, in the index.ts file, add middleware morganMiddleware:

import morganMiddleware from './config/morganMiddleware'. .const PORT = 3000;
app.use(morganMiddleware)
app.get("/logger".(_, res) = >{...Copy the code

Restart the service and request the Logger service to view the log content again:

GraphQL Morgan configuration

If you are using the GraphQL APIs, read on.

By default, GraphQL has only one route, so we need to change the Morgan configuration to make it more meaningful.

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";
  returnenv ! = ="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;
Copy the code

If you want to use a great ExpressJS GraphQL APIs template, check out this link: github.com/vassalloand…

Enjoy it

That’s all! Hopefully this configuration will help you debug your code and find hidden errors more easily. 🐛

reference

  • Better logs for ExpressJS using Winston and Morgan with Typescript