Configure winston logging in express app
01 Aug 2023 - Tobias ErdleIn 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 winston
s 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.