Can we change the face of JavaScript logging, too?
Update: since this post was published, the project has become structured-log.
If you use Serilog in .NET and write code for the web, chances are you’ve wondered whether similar hybrid “structured/text” loggers exist for the browser and Node.js…
If you haven’t tried Serilog, here’s a one-minute primer (using JavaScript syntax) so you can follow the rest of this post. Using Serilog is similar to every logging framework, ever:
var name = "Nick";
var wait = 45;
log("Hi {name}, your flight leaves in {wait} minutes", name, wait);
Look carefully at the string formatting though - particularly the way we give names to each of the parameters. The “message” part isn’t destructively rendered into text, but preserved as an object with property values. It can be formatted to the console etc. as usual:
[inf] Hi Nick, your flight leaves in 45 minutes
But more interestingly, it can be serialized in a format like JSON for processing and querying:
{
"message": "Hi Nick, your flight leaves in 45 minutes",
"messageTemplate": "Hi {name}, your flight leaves in {wait} minutes",
"properties": {
"name": "Nick",
"wait": 45
}
}
Preserving the structure of log data, and making it trivially easy to enrich that data with more contextual information, makes it possible to do some really clever things with the resulting logs.
Try finding all messages where the user’s name starts with “N”, or where the wait time is greater than one hour in a traditional log file if you’re skeptical here. In some trivial cases a regular expression might handle this cleanly, but it quickly becomes complex and cumbersome when more data is involved. Using Serilog it can be as simple as startswith(name, "N")
or wait > 60
.
Preserving the message template itself ("Hi, {name}..."
) also has some interesting and very useful results: the message template becomes a kind of “message type” that can be used to find - or exclude - messages of a specific type. Again very hard, often impossible with traditional loggers.
What’s going on in JavaScript?
In JavaScript as elsewhere, there are already some sophisticated logging frameworks, with varied levels of structured data support. A fairly new project called node-bunyan interests me the most, with its clever DTRACE integration and support for serializing objects instead of messages, but even here the new hybrid-style API and DSL provided by Serilog is missing. With node-bunyan it’s possible to log an object and/or a message, but messages are still formatted out in a lossy manner like the earlier loggers on .NET did.
Whether it grows into a standalone project in its own right, or encourages more established frameworks to take a look at this new style of logging, bringing Serilog’s API to JavaScript seems like a fun and worthwhile thing to try.
serilog.js
Tinkering away over a month of so I’ve sketched out what I think serilog.js should look like.
Yes, serilog.js terminal output is type-aware, just like syntax highlighting in an IDE! It’s useful when dealing with structured events to see the difference between a number 5
(magenta for numbers) and a string "5"
(cyan for strings). In itself this feature is a nice trimming, but it’s not just a party-trick; below the surface, the event is carrying all of this information.
Getting started - installing from NPM
All the code is currently published to NPM. At the command line:
npm install serilog
Then in your node application:
var serilog = require('serilog');
var terminal = require('serilog/src/serilog-terminal-sink');
var file = require('serilog/src/serilog-file-sink');
Serilog “sinks” are destinations for log events. This example uses the terminal sink, which writes to the system terminal/console. At the moment, the sinks are all zipped up directly in the serilog
package, but they need to be split out.
Tour of the API
At present, with very little of the back end ‘sinks’ built, serilog.js is all about the API.
Configuring the pipeline
One of the first things to notice is that the logger is a pipeline. Thinking of log events as structured data leads naturally to an API more like what you’d find in event processing frameworks than the typical logging library.
var log = serilog.configuration()
.writeTo(terminal())
.minimumLevel('WARNING')
.writeTo(file({path: 'log.txt'}))
.createLogger();
Each element is applied in turn:
-
serilog.configuration()
kicks things off by creating a new configuration object -
.writeTo(terminal())
emits all events to the terminal sink -
.minimumLevel('WARNING')
filters out any that are below theWARNING
level (we’ll cover levels below) -
.writeTo(file({path: 'log.txt'})
emits the events that reach it to a log file, and -
.createLogger()
takes all of these pipeline stages and bundles them up into an efficient logger object
The result is log
, which we’ll use below. (As in Serilog for .NET, there’s no “global” logger - multiple loggers can be created and are completely independent, unless you link them together with something like .writeTo(log2)
).
You might be wondering - why not Node.js streams or RxJS etc.? Simple really - generic APIs are great for glueing together solutions, but for something that’s used extensively in a codebase, maintaining complete control over dependencies, performance and semantics is a good idea long-term, and that’s the path taken here.
Logging levels
serilog.js has four simple logging levels, listed here in order of importance:
log.trace('This is a lot of detail');
log.information('Something of note happened');
log.warning('Look out, wheels are coming off!');
log.error('Something\'s broken');
If log.information()
looks like a bit of a mouthful, don’t worry: the log
object itself is a function that can be called as a synonym for information
:
log('This is an information-level event');
The default level for a new logger is INFORMATION
; using .minimumLevel('TRACE')
at the start of the pipeline will bump this up.
Message templates
The syntax of message templates is carried over directly from Serilog. Event properties are named by including them in braces, and the values are provided as positional arguments:
log('{a} + {b} = {c}', 1, 2, 3);
// -> {properties: {a: 1, b: 2, c: 3}}
I’ve left the message/template/timestamp out of the example output (// -> ...
) to keep the focus on the structured part of the event, but these other parts of the event exist too.
If you pass an object to the log, e.g.:
var mail = {to: 'nblumhardt', subject: 'Is the post finished?'};
log('Sending {mail}', mail);
// -> {properties: {mail: '[Object] object'}}
You might be surprised that it’s stored using toString()
. We haven’t missed a great opportunity to serialize here - the problem is that it is very easy to serialize too much data by mistake this way.
If you know that what you’re passing into a log method is a nice little chunk of data to work with later, prefix the property name with @
to say “I know what I’m doing - serialize this!”:
var mail = {to: 'nblumhardt', subject: 'Is the post finished?'};
log('Sending {@mail}', mail);
// -> {properties: {mail: {to: 'nblumhardt', subject: 'Is the post finished?'}}}
serilog.js can handle structured storage of objects and arrays nicely, and these can support some really cool processing scenarios - just don’t forget the @
(if you watch the events roll by on the terminal, you’ll notice the difference too - objects are colored green, while strings render in cyan).
Context and enrichment
The strength of structured logs is in correlation; this is one of the great challenges when instrumenting distributed apps, and probably the #1 reason that interest in structured logs has picked up again in recent times.
When searching a large number of events produced across many machines, it’s vital to have properties to filter on so that a single location or thread of interaction can be followed.
In a simple case, you might want to instrument all events with the name of the machine producing them. This can be done when configuring the logger:
var log = serilog.configuration()
.enrich({machine: env.HOSTNAME})
...
Events created by the logger will all carry the specified properties:
log('Hello, {name}', 'Nick');
// -> {properties: {name: 'Nick', machine: 'NBLUMHARDT-RMBP'}}
More often though, it’s desirable to tag events produced within a part of the application, or in a transaction. For this the using()
method is provided on the logger itself:
var smtpLog = log.using({subsystem: 'SMTP'});
smtpLog('About to send mail to {name}', 'Nick');
// -> { properties: {subsystem: 'SMTP', name: 'Nick'}}
Filtering
A final part of the API to check out is filtering. While most filtering will happen server-side wherever you collect your logs, it can be useful to apply some filters in the logging pipeline if you have a component producing a lot of noise, or if you need to avoid passing events with sensitive data to logs.
This fits in as you’d expect:
var log = serilog.configuration()
.filter(function(evt){
return evt.messageTemplate.raw !== 'Making {noise}' &&
evt.properties.subsystem !== 'SMTP';
})
.writeTo(terminal())
Building serilog.js
If you want to tinker with the project, you can grab the source from GitHub. The build process uses gulp to drive some mocha tests. Just type:
npm install
gulp
…and you’re away!
The browser
serilog.js also runs in the browser (though version support will be patchy and is completely untested!). There are some notes on the GitHub site linked above on how to do this, and a test.html page in the repository that sets some of it up.
The catch
Serilog in .NET is out there in production use today. It’s being extended and refined by a core group of contributors and takes what time I have available to coordinate.
I’ll be using serilog.js in a few small JavaScript projects and extending it for my own needs, and I’ll be building a sink that writes to Seq at some point. But, I don’t have the capacity to really drive this project beyond the prototype that it is right now.
If you’re interested in JavaScript, event processing, application monitoring and/or API design, serilog.js needs you. Kick the tyres, check out the code, send a pull request! It’s a project “up for grabs” in the largest sense. Can you help change the face of JavaScript logging?