A close up of a typewriter

Announcing winston@3.0.0!

7 min read
Charlie Robbins

winston is the most popular logging solution for Node.js. In fact, when measured in public npm downloads winston is so popular that it has more usage than the top four comparable logging solutions combined. For nearly the last three years the winston project has undergone a complete rewrite for a few reasons:

  • Replace the winston internals with Node.js objectMode streams.
  • Empower users to format their logs without changes to winston itself.
  • Modularize winston into several smaller packages: winston-transport, logform, and triple-beam.
  • Modernize and performance optimize a now seven year old codebase to ES6 (which it turns out was necessary to meet the API goals).

Why don’t you take it for a spin?

npm i winston@3

# or if `yarn` is more your fancy
yarn add winston@3

winston@3 API

If you’re familiar with winston the default v3 API will look pretty familiar to you:

const { transports, format, createLogger } = require('winston');

const logger = createLogger({
  level: 'info',
  format: format.combine(
    format.printf(info => `[${info.timestamp}] ${info.level}: ${info.message}`)
  transports: [
    // - Write to all logs with level `info` and below to `combined.log`
    // - Write all logs error (and below) to `error.log`.
    new transports.File({ filename: 'error.log', level: 'error' }),
    new transports.File({ filename: 'combined.log' })

logger.info('Hello again distributed logs');

And that’s because the core logging semantics are the same. There are however, a few key differences:

  1. createLogger instead of new Logger this change allowed for a major performance increase due to how prototype functions are optimized.
  2. All log formatting is now handled by formats this overhaul to the API will streamline winston core itself and allow for userland (i.e. user-defined formats to be shared just like userland transports are already. We’ll discuss formats in-depth below.
  3. Logger and Transport instances are now Node.js objectMode streams.
  4. Logging methods no longer accept a callback use the core Node.js streams finish event instead to know when all logs have been written and the process is safe to exit.

There is an extensive upgrade guide on how to migrate to winston@3. Particular care was taken to ensure backwards compatibility with existing transports. There are also a long list of full featured examples.

Backwards compatibility for ecosystem Transports

You shouldn’t be worried about your favorite Transport not working with winston@3the winston-transport API was designed with this in mind. Any winston@2 transport should get seamlessly wrapped with a compatibility stream that will tell you to politely nudge the author:

SomeTransport is a legacy winston transport. Consider upgrading:
- Upgrade docs: https://github.com/winstonjs/winston/blob/master/UPGRADE-3.0.md

Are you a transport author? We’d love your input! There’s a great discussion going on about how to seamlessly support both winston@2 and winston@3 with low maintenance overhead.

Formats. Why? What? How?

“~Life~ Open Source is a series of natural and spontaneous changes.”

To understand why formats were the right decision for winston@3 we must look backwards. The essence of the winston@2.x API is summed up by common.log which will forever exist in winston-compat as a compatibility layer for transport authors. It began innocent enough – a shared utility function that accepts options and returns a formatted string representing the log message.

Then came a series of natural and spontaneous changes over the course of many years – and it grew. And grew. And grew. And created an unending list of feature requests for log formatting (see just a few).

As this pattern became more evident so did the cost. This made the design goals clear for formats:

  1. Enable userland log formatting: most importantly without any changes needed to winston itself.
  2. Strive to make users “pay” only for formatting features that they use: ensure that the performance impact of log formatting features is opt-in. In other words if you don’t use a feature you won’t pay any cost for it’s implementation.

This focus on log formatting features is not by accident. What winston has shown is that reading logs is an immensely personal experience. By putting the formatting features in userland we support that demand with less burden.

What is a format? Let’s start with an example that adds a timestamp, colorizes the level and message, aligns message content with t and prints using the template [timestamp] [level]: [message].

const { format } = require('winston');

const customFormat = format.combine(
  format.timestamp(),              // Adds info.timestamp
  format.colorize({ all: true }),  // Colorizes { level, message } on the info
  format.align(),                  // Prepends message with `t`
  format.printf(info => `[${info.timestamp}] ${info.level}: ${info.message}`)

Each one of these format methods resemble a Node.js TransformStream but are specifically designed to be synchronous since they do not have to handle backpressure (i.e. a fast reader and a slow writer – such as reading from file and writing to an HTTPS socket).

There is too much to go into about Formats in a single blog post, but there’s a lot more in the Format documentation and in logform where they are implemented.

Node.js LTS, ES6 Symbols, & maintaining your npm packages – oh my!

The key to the winston@3 API is ES6 Symbols – the API itself would have not been possible without them. Why? The property value for the LEVEL symbol is a copy of the original level value that should be considered immutable to formats.

Without an ES6 Symbol we’d still have the same need to accomplish API goals. That means instead of [LEVEL] in this example info object:

{ [LEVEL]: 'info', level: 'info', message: 'Look at my logs, my logs are amazing.' }

we would have to pick a plain property name, say _level:

{ _level: 'info', level: 'info', message: 'Look at my logs, my logs are amazing.' }

You might think that a _level property vs. a LEVEL symbol are more or less the same. They are except for one important difference: Symbols are excluded from JSON by definition. This is critical because so many log files are serialized to JSON and including these internal properties would be unacceptable.

This seemingly small nuance of the API turned out to be critically important to the winston@3 release timeline. To understand why one must understand what Node.js LTS is and what it can mean to authors of popular npm packages such as winston.

Since Node.js began the LTS program it’s been the goal of winston to support all LTS releases of Node (including maintenance LTS). Why support such old LTS releases of Node.js? The underlying motivation is to make winston as reliable for large teams & enterprises as Node.js itself.

This LTS goal is a core reason why this release took three years to ship. Let’s look at a recent release chart for Node.js LTS:

As you can see node@4 entered “end of life” in April 2018. Until then it was in the active support matrix for winston releases. Since ES6 features (including Symbols) were only available in node@6 this meant that even if API development and re-write progress was ahead of schedule (it wasn’t) that version wouldn’t be able to use it.

Why not polyfill the ES6 Symbol API? That’s definitely a possibility to consider, but given the size and scope of the rewrite itself it didn’t seem necessary to rush ourselves. Having seen how delicate these API decisions can impact such a large ecosystem of packages (almost 1000 results on npm).

Want to get involved?

Looking to get involved in an actively maintained open source project? Then look no further - we’d love to have you join us on winston!

You can find us on Gitter in winston. And with that – happy logging!

The author would like to extend a very special thanks to all of the contributors for the winston@3 release. In particular: David Hyde, Chris Alderson, and Jarrett Cruger.