DEV Community

Cover image for Let's build a production-ready logger using Winston
Naineel Soyantar
Naineel Soyantar

Posted on

Let's build a production-ready logger using Winston

Hi guys, long time no see...

Just Woke Up

Time to learn about another awesome concept: logging

What does Wikipedia say about logging?

In computing, logging is the act of keeping a log of events that occur in a computer system, such as problems, errors, or just information on current operations. A message or log entry is recorded for each such event. These log messages can then be used to monitor and understand the operation of the system, to debug problems, or during an audit. (Logging (computing))

So going by the definition, logging simply means keeping a log of events that occur in a computer system. This can be used to monitor and understand the operation of the system, to debug problems, or during an audit. Many types of logs can be generated by a system, but we will be focusing on Server Logs in this article.

What Are Server Logs?

Server Logs are logs that are generated by a server. These logs are used to monitor and understand the server's operation. Typical examples of server logs are:

  • Page Requests Logs
  • Error Logs
  • Access Logs
  • Information Logs, etc.

Logs help in understanding the behavior of the server, how many requests are being made to the server, what kind of requests are being made, what kind of errors are being generated, etc. Understanding the patterns in the logs can help optimize the server's performance and debug issues.

Winston is one such tool that can be used to generate logs in a Node.js application. Its various modifications can be used to generate logs in different formats and at different levels.

Installing Winston

To install Winston, run the following command:

npm install winston
Enter fullscreen mode Exit fullscreen mode

Using Winston

Let's create the simplest logger using Winston:

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),

    transports: [
        new winston.transports.Console()
    ]
});

// now we can use the logger to log messages as we want
logger.info('Hello, Winston!');
logger.error('An error occurred!');
logger.warn('Warning: This is a warning!');
Enter fullscreen mode Exit fullscreen mode

Output:

simplest.js

Understanding the components of the Winston Logger

We have created a simple logger using Winston. Let's understand the components of the logger:

  • Levels - Levels are used to specify the severity of the log. Winston is pre-configured with the following levels:

    • error: Severity 0 - This is an error message.
    • warn: Severity 1 - This is a warning message.
    • info: Severity 2 - This is an informational message.
    • http: Severity 3 - This is an HTTP log.
    • verbose: Severity 4 - This is a verbose log.
    • debug: Severity 5 - This is a debug message.
    • silly: Severity 6 - This is a silly message.

    The above example of the logger is configured to log messages at the info, warn, and error levels. We can also create custom levels for the Winston logger.

    We can use the syslog levels as well. The syslog levels can be used from winston.config.syslog.levels. The default levels are from winston.config.npm.levels.

    import winston from 'winston';
    
    const customLevels = {
        levels: {
            error: 0,
            warn: 1,
            info: 2,
        },
        colors: {
            error: 'red',
            warn: 'yellow',
            info: 'green',
        }
    };
    
    // add colors to the custom levels
    winston.addColors(customLevels.colors);
    
    // define a logger with custom levels
    const logger = winston.createLogger({
        levels: customLevels.levels,
        format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
        ),
    
        transports: [
            new winston.transports.Console()
        ]
    });
    
    // now we can use the logger to log messages as we want
    logger.error('Hello, Winston!');
    logger.warn('An error occurred!');
    logger.info('Warning: This is a warning!');
    

    Output:

    custom-levels.js

    Here, we have defined custom levels for the logger and added colors to the custom levels. We have to inform Winston about the custom levels and colors using the addColors method.

  • Formats - Formats are used to specify the format of the log message. Winston is pre-configured with the following formats:

    • JSON - This format logs the message in JSON format.
    • simple - This format logs the message in a simple format.
    • colorize - This format logs the message in color.
    • printf - This format logs the message in a custom format.
    • timestamp - This format logs the message with a timestamp.
    • combine - This format combines multiple formats.

    We can also create custom formats for the Winston logger.

    import winston from 'winston';
    
    // define a logger with custom format
    const logger = winston.createLogger({
        level: 'info',
        format: winston.format.combine(
            winston.format.colorize(),
            winston.format.timestamp({
                format: 'YYYY-MM-DD HH:mm:ss',
            }),
            winston.format.printf(({ level, message, timestamp }) => {
                return `${timestamp} [${level}]: ${message}`;
            })
        ),
        transports: [
            new winston.transports.Console()
        ]
    });
    
    // now we can use the logger to log messages as we want
    logger.info('Hello, Winston!');
    logger.error('An error occurred!');
    logger.warn('Warning: This is a warning!');
    

    Output:

    custom-format.js

    Here, we have defined a custom format for the logger. The custom format logs the message in the format [timestamp] [level]: message

  • Transports - Transports are used to specify the destination of the log message. Winston is pre-configured with the following transports:

    • Console - This transport logs the message to the console. Mostly used for development purposes.
    • File - This transport logs the message to a file. We can specify the filename, the maximum size of the file, and the maximum number of files.
    • HTTP - This transport logs the message to another server using HTTP. Mostly used for remote logging services.
    • Stream - This transport logs the message to a NodeJS writable stream.
  • Daily Rotate File - Daily Rotate File is an important way to prevent the accumulation of unnecessary old log files that are of no use in a live server and can be replaced automatically, ensuring the latest log files are available always and the redundant old files are removed/rotated to save the space on the live server

Let's install the Daily Rotate File package:

  npm install winston-daily-rotate-file
Enter fullscreen mode Exit fullscreen mode
  import winston from 'winston';
  import 'winston-daily-rotate-file';

  const fileFormat = winston.format.combine(
    winston.format.timestamp({
        format: 'YYYY-MM-DD HH:mm:ss',
    }),
    winston.format.printf(({ timestamp, level, message }) => {
        return `${timestamp} [${level}]: ${message}`;
    })
  );

  const combinedFileTransport = new winston.transports.DailyRotateFile({
    filename: '%DATE%_combined.log',
    datePattern: 'YYYY-MM-DD-HH',
    maxSize: '2m',
    dirname: './logs/combined',
    maxFiles: '14d',
    format: fileFormat
  });
Enter fullscreen mode Exit fullscreen mode

Here, we have defined a Daily Rotate File transport for the logger. The Daily Rotate File transport logs the message to a file with the filename combined.log and rotates the file daily.

The maximum size of the file is 2m i.e. 2MB and the maximum number of days the files will be kept is 14d, where d is days. The %DATE% is a placeholder that will be replaced with the current date in the format YYYY-MM-DD-HH.

Designing a production-ready logger using Winston, Morgan and Daily Rotate File

Let's design a production-ready logger using Winston, Morgan and Daily Rotate File:

// winston.config.js
import winston from 'winston';
import 'winston-daily-rotate-file';

const fileFormat = winston.format.combine(
    winston.format.colorize(),
    winston.format.uncolorize(),
    winston.format.timestamp({
        format: 'YYYY-MM-DD HH:mm:ss',
    }),
    winston.format.prettyPrint({
        depth: 5
    }),
    winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
);

const consoleFormat = winston.format.combine(
    winston.format.colorize(),
    winston.format.timestamp({
        format: 'YYYY-MM-DD HH:mm:ss',
    }),
    winston.format.prettyPrint({
        depth: 5
    }),
    winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
);

const consoleTransport = new winston.transports.Console({
    format: consoleFormat,
});

const combinedFileTransport = new winston.transports.DailyRotateFile({
    filename: '%DATE%_combined.log',
    format: fileFormat, //format means - how the log should be formatted
    datePattern: 'YYYY-MM-DD-HH',
    maxSize: '2m',
    dirname: './logs/combined',
    maxFiles: '14d',
});

const errorFileTransport = new winston.transports.DailyRotateFile({
    filename: '%DATE%_error.log',
    level: 'error',
    format: fileFormat, //format means - how the log should be formatted
    datePattern: 'YYYY-MM-DD-HH',
    maxSize: '2m',
    dirname: './logs/errors',
    maxFiles: '14d',
});

const httpTransport = new winston.transports.Http({
    format: winston.format.json(),
    host: 'localhost',
    port: 4000,
    path: '/logs',
    ssl: false,
    batch: true,
    batchCount: 10,
    batchInterval: 10000,
});


const logger = winston.createLogger({
    levels: winston.config.syslog.levels,
    transports: [
        errorFileTransport,
        combinedFileTransport,
        consoleTransport,
        httpTransport,
    ],
});

export default logger;
Enter fullscreen mode Exit fullscreen mode

Here, we have defined a logger using the Winston library with the following functionalities:

  • Console Transport - Logs the message to the console.
  • Daily Rotate File Transport - Logs the message to a file and rotates the file daily.
  • HTTP Transport - Logs the message to another server using HTTP.
  • Error File Transport - Logs the error message to a file and rotates the file daily.
  • Combined File Transport - Logs the message to a file and rotates the file daily.
  • Syslog Levels - We have used the syslog levels for the logger.
// index.js
import express from 'express';
import morgan from 'morgan';
import logger from './config/winston.config.js';

const app = express();

app.use(express.json());

app.use(morgan('dev', {
    stream: {
        write: message => {
            logger.info(message);
        }
    }
}));

app.get('/', (_req, res) => {
    return res.status(200).json({ message: 'Hello World' });
});

function handleFatalError(err) {
    logger.error(err);
    process.exit(1);
}

app.listen(process.env.PORT || 3000, () => {
    logger.info('Server is running on http://localhost:' + (process.env.PORT || 3000) + '/');

    process.on('unhandledRejection', handleFatalError);
    process.on('SIGTERM', handleFatalError);
});
Enter fullscreen mode Exit fullscreen mode

To further enhance the functionality of our logger, we have integrated it with the Morgan library. Morgan is a middleware that logs the HTTP requests to the server. We have used the dev format of Morgan to log the HTTP requests to the server.

The stream option of Morgan is used to specify the destination of the log message. We have used the Winston logger to log the message to the console.

Output:

  • Files created by the Daily Rotate File Transport:

    files-create-by-file-transport-and-daily-rotate-file

  • Logs generated in the console:

    logs-generated-in-the-console

  • Logs generated by the HTTP Transport:

    The following logs were generated by our logger's HTTP Transport. The logs were sent to another server running on localhost:4000/logs. The requests were made in batches as specified in the configuration.

    logs-generated-by-the-HTTP-transport

  • Logs generated by the Error File Transport:

    logs-generated-by-the-error-file-transport

  • Logs generated by the Combined File Transport:

    logs-generated-by-the-combined-file-transport

Conclusion

In this article, we have learned about the concept of logging and how to create a logger using Winston in a Node.js application. We have also learned about the various components of the Winston logger, such as levels, formats, and transports.

Try out the Winston logger setup in your Node.js applications and let me know your thoughts in the comments. If you have any questions or suggestions, feel free to ask.

So... that's it for today.

I would like to thank you 🫡🎉 for reading here and appreciate your patience. It would mean the world to me if you give feedback/suggestions/recommendations below.

See you in the next article, which will be published on......

sleepy-joe

PS:

I typically write these articles in the TIL form, sharing the things I learn during my daily work or afterward. I aim to post once or twice a week with all the things I have learned in the past week.

Top comments (1)

Collapse
 
ekansh005 profile image
Ekansh Saxena

Great article. Any further insights on how to make the most of your log files, any tool to read those files and setup alerts, etc.?