Tobias Erdle's Blog

Development Engineer working with different technologies. Former Jakarta MVC & Eclipse Krazo Committer. All views are my own.

Github: erdlet | E-Mail: blog (at) erdlet (punkt) de

Configure winston logging in express app

In this short post I'll explain how I configured winston as logging solution in an Express application. I assume there is an existing application that has the structure generated by the express-generator.

Install required dependencies

To be able to use winston I need to install the following dependencies. Because I want to use winston as Express middleware, I also install the express-winston package, as well as the winston-daily-rotate-file package for rolling log files:

npm i winston express-winston winston-daily-rotate-file

With these both dependencies installed, I can start to implement the logging configuration.

Configure the logger

As 'single point of truth' for the logging configuration I create a file called logging.services.ts which is located at some place in the application. The exports of this module will be used later inside the application.

Before I start to dig into code, I'll just loose some words about winstons basic wording. A logger is the core class that is used by the client code. Each logger contains at least one transport, which is the target of the log. In case you come from the Java world, a transport is something like Log4Js appender. A transports target may be the console, a file or some HTTP endpoint.

In this example I'll now add two rolling file transports and a console transport in case the application runs in development mode.

As a first step, I add a logger variable and the console transport definition. This is the only transport that is exported since it is required for the express-winston lib later:

import * as winston from 'winston';
import { ConsoleTransportInstance } from 'winston/lib/winston/transports';

export const consoleTransport: ConsoleTransportInstance =
  new winston.transports.Console({
    level: 'debug',
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    ),
  });

export let logger: winston.Logger;

Here you can see, that the console transport logs everything that has at least log level debug. More on log levels you can find in the winston documentation about logging levels. Also I define the simple format that is written colored into the console. At the code's bottom I define a variable that contains the logger instance later.

Now I define an arrow function that initializes the file transports and configures the logger itself. The code of the first example is ommitted for brevity. The isDevEnv function is just a wrapper that checks if the NODE_ENV variable is not production.

... // other imports

import DailyRotateFile from 'winston-daily-rotate-file';
import { isDevEnv } from '../common/env'; // just a wrapper for NODE_ENV check

... // console transport and logger variable ommitted

export const initLogger = () => {
  // Enable environment specific configuration for log files
  const LOGFILE_LOCATION = process.env.LOGFILE_LOCATION || './logs';

  // Create daily rotating file appender for common logs
  const logFileTransport: DailyRotateFile = new DailyRotateFile({
    filename: 'app-%DATE%.log',
    datePattern: 'YYYY-MM-DD-HH',
    level: 'info',
    dirname: LOGFILE_LOCATION,
    zippedArchive: true,
    maxSize: '20m',
    maxFiles: '14d',
  });

  // Create daily rolling file appender for critical logs
  const criticalLogFileTransport: DailyRotateFile = new DailyRotateFile({
    filename: 'app-%DATE%.critical.log',
    datePattern: 'YYYY-MM-DD-HH',
    dirname: LOGFILE_LOCATION,
    zippedArchive: true,
    maxSize: '20m',
    maxFiles: '14d',
  });

  // Log 'debug' level too in case of dev environment
  const minLogLevel = isDevEnv() ? 'debug' : 'info';

  // Create the logger with minimum log level, format, ...
  logger = winston.createLogger({
    level: minLogLevel,
    format: winston.format.json(),
    defaultMeta: { service: 'mern-backend' },
    transports: [logFileTransport],
    exceptionHandlers: [criticalLogFileTransport],
  });

  // Add console transport for dev environments
  if (isDevEnv()) {
    logger.add(consoleTransport);
  }

  logger.info('Logger initialized');
};

The method above creates a daily rotating file transport that contains all logs that have at least the logging level info and saves them inside the directory defined by LOGFILE_LOCATION. The output file is formatted by the combination of filename and datePattern and is going to be zipped after one day. Each log file has 20 megabytes maximum size and the system stores log files for 14 days.

The second file transport is used as exception handler transport that catches critical application errors and makes them visible for easier debugging. It is the same config as above but uses error as logging level.

Afterwards the general minimum log level is defined, depending on the environment the app runs in. All these settings are then used for creating the logger instance that is set into the prepared variable.

As a last step here, the logger gets the console transport appended in case the application runs in development mode.

Initialize logger in application

To use the logger inside the application, it needs to be loaded on startup. I'll initialize it as first infrastructure component directly after the imports in the file /bin/www. In case your app layout is different, please check where the best location for this is.

/**
 * Module dependencies.
 */
... // other imports
const { initLogger } = require('../services/logging.service');

// Initialize the logger in module logging.service
initLogger();

// Import logger after initialization
const { logger } = require('../services/logging.service');

// Use logger
logger.info('Starting application');

As you can see here, it is important to initialize the logger first and THEN import it into the application. In case the import is done like const { logger, initLogger } = require('../services/logging.service'), the call to logger.info will fail because logger is undefined.

Use logger in application

After the initialization is done properly, the logger can be used without problems inside the application. It is just required to be imported.