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.jsobjectMode
streams. - Empower users to format their logs without changes to
winston
itself. - Modularize
winston
into several smaller packages:winston-transport
,logform
, andtriple-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.timestamp(),
format.colorize(),
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:
createLogger
instead ofnew Logger
this change allowed for a major performance increase due to how prototype functions are optimized.- 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. Logger
andTransport
instances are now Node.jsobjectMode
streams.- 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@3
– the 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:
- Enable userland log formatting: most importantly without any changes needed to
winston
itself. - 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
!
- Learn more about contributing.
- Checkout the Roadmap (spoiler alert: first-class browser support, performance analysis using
0x
and even more test coverage). - Find your first issue.
- Help us triage old issues and close duplicates.
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.