Winston Logger With Typescript Typescript

Dec 3rd, 2021 - written by Kimserey with .

Winston logger is one of the most popular logger for node application. In today’s post we will look at how we can configure and use Winston.

Install Winston

We start by installing Winston:

1
npm install winston

We can then import the createLogger function which is the main function used to create the logger and the transports and format namespaces containing the functions needed to configure the logger.

1
import { createLogger, transports, format } from "winston";

Simple Logger Configuration

The simplest logger we can start with would be a console logger with a colorized simple output. To configure that we have to specify:

  • the console transport,
  • the combined format; colorized plus a simple output.
1
2
3
4
5
6
7
8
9
10
11
12
const logger = createLogger({
  transports: [new transports.Console()],
  format: format.combine(
    format.colorize(),
    format.timestamp(),
    format.printf(({ timestamp, level, message }) => {
      return `[${timestamp}] ${level}: ${message}`;
    })
  ),
});

logger.info("Hello world!");

We used createLogger to create a logger and passed in the Console transport and a simple format taking the timestamp, level and message.

1
[2021-11-21T09:08:40.844Z] info: Hello world!

We can see that logging Hello world! then yield the expected log format.

Additionally we can specify extra metadata to be used in the format:

1
2
3
4
5
6
7
8
9
10
11
12
13
const logger = createLogger({
  transports: [new transports.Console()],
  format: format.combine(
    format.colorize(),
    format.timestamp(),
    format.printf(({ timestamp, level, message, service }) => {
      return `[${timestamp}] ${service} ${level}: ${message}`;
    })
  ),
  defaultMeta: {
    service: "WinstonExample",
  },
});

Here we added default metadata service which we used in the printf format and that will print our service in the logs:

1
[2021-11-21T09:13:14.545Z] WinstonExample info: Hello world!

Transports

There are several core transports build-in, we already seen Console, the other available are File, Http and Stream.

File can be used to save logs into a file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const logger = createLogger({
  transports: [
    new transports.File({
      dirname: "logs",
      filename: "winston_example.log",
    }),
  ],
  format: format.combine(
    format.timestamp(),
    format.printf(({ timestamp, level, message, service }) => {
      return `[${timestamp}] ${service} ${level}: ${message}`;
    })
  ),
  defaultMeta: {
    service: "WinstonExample",
  },
});

Here the logs will be written to a file under logs/winston_example.log. The logs will be appended, multiple options are available to specify the max size, rotate the file and zip the file.

Http is used to ship logs to a defined http endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
const logger = createLogger({
  transports: [new transports.Http()],
  format: format.combine(
    format.timestamp(),
    format.printf(({ timestamp, level, message, service }) => {
      return `[${timestamp}] ${service} ${level}: ${message}`;
    })
  ),
  defaultMeta: {
    service: "WinstonExample",
  },
});

Stream is the most flexible transport as it accepts a Node stream. Using it, we are able to implement any transport. For example here, we are putting the logs into a stream file just to demonstrate the usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createLogger, transports, format } from "winston";
import { createWriteStream } from "fs";

const logger = createLogger({
  transports: [
    new transports.Stream({
      stream: createWriteStream("hello.txt"),
    }),
  ],
  format: format.combine(
    format.timestamp(),
    format.printf(({ timestamp, level, message, service }) => {
      return `[${timestamp}] ${service} ${level}: ${message}`;
    })
  ),
  defaultMeta: {
    service: "WinstonExample",
  },
});

And there are many more community driven transports available.

Formats

We saw earlier that format namespace exposes the different format functions which can be used to generate a format for the logger. The main formats we used in this post were:

  • combine
  • colorize
  • timestamp
  • printf
  • json

combine is a format building block which allow us to combine multiple formats. For example if we want to have a colorized human readable formatted string with timestamp, we can combine the following:

1
2
3
4
5
6
7
format.combine(
  format.colorize(),
  format.timestamp(),
  format.printf(({ timestamp, level, message }) => {
    return `[${timestamp}] ${level}: ${message}`;
  })
);

We can see that colorize will apply color to the level so that error would appear red, and timestamp will make available the current timestamp in metadata. Then all the metadata can be extracted in printf to construct a nice log.

string formatted log are great for console debugging but when shipping logs to server application, json format is preferable as it enables querying from tools. To ship as json we can use the format json.

1
format.combine(format.timestamp(), format.json());

We removed colorize as we do not want the color unicode to be added to the level or the message.

Another interesting format is splat which enables string interpolation:

1
2
3
4
5
6
7
format.combine(
  format.colorize(),
  format.splat(),
  format.printf(({ timestamp, level, message }) => {
    return `[${timestamp}] ${level}: ${message}`;
  })
);

We can then use %d for digits and %s for strings:

1
2
const userId = "123";
logger.info("Hello world! userId: %s", userId);

And the message will be interpolated:

1
[2021-11-21T10:06:31.477Z] WinstonExample info: Hello world! userId: 123

Lastly we can provide extra metadata which are not part of the message on each logs. This is useful to provide request ID or correlation ID.

1
logger.info("Hello world!", { correlationId: "123", userId: "456 " });

By default those will be captured in the log information but we can specifically place them into a property of the log using the metadata format:

1
2
3
4
5
6
7
8
9
format.combine(
  format.colorize(),
  format.splat(),
  format.metadata(),
  format.timestamp(),
  format.printf(({ timestamp, level, message, metadata }) => {
    return `[${timestamp}] ${level}: ${message}. ${JSON.stringify(metadata)}`;
  })
);

All the metadata are being capture into { metadata: {...} }. Take note that timestamp is registered after metadata - the order matters otherwise it will be captured within metadata.

Specify Different Formats per Transports

So far we’ve seen how to define logs for console and how to define logs for files. We saw that the colorize format was only useful for the console transport and json was only useful for file or http transports. Therefore what we want to achieve is to have specific formats per transports.

Each transport have a format attribute which can be used to be combined with the format defined at the root option of the logger.

To achieve what we describe above we can then do the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const logger = createLogger({
  transports: [
    new transports.Console({
      format: format.combine(
        format.colorize(),
        format.printf(({ timestamp, level, message, metadata }) => {
          return `[${timestamp}] ${level}: ${message}. ${JSON.stringify(
            metadata
          )}`;
        })
      ),
    }),
    new transports.File({
      dirname: "logs",
      filename: "winston_example.log",
      format: format.combine(format.json()),
    }),
  ],
  format: format.combine(format.metadata(), format.timestamp()),
});

logger.info("Hello world!", { correlationId: "123", userId: "456 " });

We define a global format which gather metadata and add a timestamp. And we then define two transports one for console which colorize the log and print the log in a defined format. And we define another file transport which output the log in json.

We end up with the following log printed in console:

1
[2021-11-21T10:23:23.380Z] info: Hello world!. {"correlationId":"123","userId":"456 "}

while having the following in file:

1
2
3
4
5
6
{
  "level": "info",
  "message": "Hello world!",
  "metadata": { "correlationId": "123", "userId": "456 " },
  "timestamp": "2021-11-21T10:23:23.380Z"
}

And that concludes today’s post on Winston logger!

Conclusion

Today we looked at Winston logger. We started by looking at how we could install it then looked at a very simple example to get started quickly. We then moved on to look in more details at the transports available to ship the logs and we also took a look at the different format functions available to transform the logs into either human readable logs or json logs. We completed the post by looking at how we could define sepcific format for different transports. I hope you liked this post and I’ll see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.