My first post talked about logging on an abstract level. In this post, I discuss a concrete implementation of those ideas.
I’d like to share with you Bragi, a Javascript logging library I wrote that has made my development workflow more efficient and enjoyable. In this first part, I’ll discuss the NodeJS version of Bragi. Next week, I’ll release and discuss the web browser version.
*tl;dr: Just want the library? Check out Bragi, a javascript logging library that allows expressive logging, uses colors and symbols, and allows piping logs to numerous outputs (console, file, remote hosts). Note that this is a public, early version which is subject to API changes.
Some of the core concepts driving Bragi are:
Before I dive into examples of how to use Bragi, I want to discuss how it differs from many traditional logging approaches.
Traditional Logging Breakdown
Normally, these two tasks are bundled together. For example, console.log() is a call to log which immediately prints something. When a log message directly prints something to your console, leaving log messages in a codebase pollutes the application’s output and can make it more difficult to develop or gain insights.
The calls to log() and the output of those calls should be decoupled, and it should be possible for the output to be filtered.
The paradigm I’ve found most helpful is to leave log messages in code and have a way to filter what is output and how it is outputted (e.g., via console or written to a file). Most libraries I’ve used just have a linear log level setting - e.g., “debug”, “info”, “warn”, “error.” This is a step in the right direction, but it’s not expressive enough. With linear log levels alone, your ability to filter messages is greatly limitted and you have no fine grain control over what is shown.
Bragi uses “log groups.” Instead of a linear log level, each log message belongs to some arbitrary group you create. This allows logging to be more expressive and tailored to your specific application. This allows the traditional “debug”, “info”, “error” log levels; but it also allows having log messages grouped by file name, method name, user id, etc.
I’ve found it helpful to have group names separated by a colon (namespaces, in a way). For instance, this allows such fine grain groups such as: “userController:updateUser:name:userId123”. The group name is just a string, so it can be built dynamically - userId could be populated by whatever user is being updated.
With groups, filtering becomes easy. We could show all messages from the userController namespace by enabling “userController” logs, or see only calls to updateUser by enabling “userController:updateUser”. If the logging library supports regular expressions (Bragi does), then we could also see all logs across the entire system for a specific user by adding /.*:userId123/
to the enabled groups. Expressive log groups can provide a complete and fully controllable view into exactly what your code is doing at any time by any metric or facet you desire.
Bragi uses the notion of “transports” (inspired by [winston]), which determine what is done with a log message. By default, Bragi will print colored log messages to the console. Currently, it can also write to a file or output JSON.
When log() is called, an object is created based on the group and passed in message. This object is augmented with some meta information, such as the time the message was logged and the calling function’s name (if it has one). Additionally, any extra arguments passed into log() are added onto a properties
key.
In Bragi, Console
is the only transport enabled by default. To remove all transports, you can call transports.empty()
, and to add a transport transports.add( new Tranport...() )
can be called.
Like naming variables, writing a good log message is not trivial. Some of the problems around writing good messages are tied to how code is structured. One thing I’ve found to be very helpful is to name all functions, so when looking at a stack trace I never see “anonymous function” and have to wonder where it is.
Another thing that has helped me is to use colors and symbols. Color is a powerful way to visualize information (e.g., all error messages have a red background with white text. Whenever you see it, you know immediately that it is an error).
Bragi provides a function, logger.util.print(message, color)
, which takes in a message {String} and a color {String} (like ‘red’ or ‘green’), and returns the passed in message colored by the passed in color. This is useful for printing messages that contain color. For instance:
Symbols are also incredibly useful way to encode data in an easy to consume form.
Symbols provide powerful visual cues. Looking at a sea of text is daunting, but if the text has various ✔︎ or ✗ or ☞, your eyes are drawn towards them. In fact, I can guess what happened when you saw this paragraph: the symbols initially jumped out at you and you directed your focus towards them, maybe even reading the word “various” before jumping back to the top of the paragraph to read it.
These cues allow you to quickly filter out certain messages. When you setup your logging, if you give all asynchronous calls an ✗ symbol, they will become much easier to spot. Or maybe you give all warnings a ☞ symbol. Or maybe before an async request is made you print a ◯ symbol, then a ◉ symbol when the async process is finished.
Bragi provides some UTF-8 symbols, accessible via
var logger = require(‘brag’);
// symbols can be accessed via the `util` property.
// It is a dictionary of symbols, such as logger.util.symbols.success
console.log( logger.util.symbols );
The core of Bragi involves a call to log(). It takes in two required parameters and any number of additional objects. For instance:
var logger = require(‘bragi’);
logger.log(‘group1’, ‘Hello world’);
logger.log(‘group1:subgroup1’, ‘I can also log some objects’, { key: 42 });
Filtering is a key feature of what makes this kind of logging powerful. With Bragi, you can specify what logs to show by providing a groupsEnabled
array which can contain strings or regular expressions. If group1:subgroup1
is a value in the array, all messages that match group1:subgroup1
, including nested subgroups, would get logged. If groupsEnabled
is a boolean with a value of true
, everything will be logged.
Similarly, you can specify log messages to ignore via the groupsDisabled
key (also an array). It works the same as groupsEnabled
, but in reverse. It will take priority over groups specified in groupsEnabled
. This is useful, for instance, if you want to log everything except some certain messages. As with groupsEnabled
, it is an array which can take in strings or regular expressions.
More configuration information can be found in Bragi's source code.
Bragi is the result of me trying to formalize and publicly release a logging library I’ve been using for the past couple of years. It’s worked great for my team and me, so I thought others might find it useful. Bragi is in the incipient stage so the API will likely change. Criticisms and improvements are welcome, and I’ll be updating it and adding more transports throughout the next few weeks. Bragi is not just for NodeJS - next week, I’ll release and discuss the web browser version.
You can find Bragi, a Javascript logger on Github