Logging is hard. Here is my advice:
Use debug
module.
Module debug
is extremely useful because it allows to dynamically turn on messages from various modules. All my NPM modules use it with convention DEBUG='module name'
to turn on messages for a particular module.
1 | const debug = require('debug')('foo') |
By default the above program prints nothing. But if we turn on debug
logging for module foo
we see one log line
1 | DEBUG=foo node . |
By default, if the terminal supports it, debug
uses terminal colors to highlight log messages coming from different files, making visual scanning the logs a breeze.
Format statements
Instead of letting debug
or console.log
pick the "best" representation of the arguments, explicitly set the format for each. For example:
1 | const debug = require('debug')('foo') |
1 | DEBUG=foo node . |
Main format placeholders to remember (see Node documentation):
%s
- String.%d
- Number (integer or floating point value).%i
- Integer.%f
- Floating point value.%j
- JSON. Replaced with the string '[Circular]' if the argument contains circular references.%o
- Object. A string representation of an object with generic JavaScript object formatting.
If in doubt - use %j
to print value serialized as JSON.
1 | const debug = require('debug')('foo') |
1 | DEBUG=foo node . |
Another format I like is %o
that outputs argument as a JavaScript object, which to me is easier to quickly scan.
1 | const debug = require('debug')('foo') |
1 | DEBUG=foo node . |
Control the nesting depth
If an object has properties nested deeper than default threshold, %o
serialization can cut it off
1 | const debug = require('debug')('foo') |
1 | DEBUG=foo node . |
Luckily, debug
module can read environment variable DEBUG_DEPTH
to control the depth threshold
1 | DEBUG=foo DEBUG_DEPTH=10 node . |
Do not use console.log
So console.log
has a problem with %o
- it does not override depth threshold using an environment variable, and also it does NOT print the entire message on a single line. I will make the property names artificially long to show the problem
1 | const debug = require('debug')('foo') |
Compare the first message from debug
to the second message from console.log
1 | DEBUG=foo DEBUG_DEPTH=10 node . |
Notice how console.log
has split the output into 2 lines. This is bad for external logging tools - because they will treat separate lines as separate messages, breaking context and search.
Replace console.log
with debug
There are two differences between console.log
and debug
. First, debug
by default writes to STDERR stream, while console.log
writes to STDOUT stream. This usually is not very crucial.
Second, console.log
messages are ON by default, while debug
messages are only enabled via an environment variable (or programmatically). Thus we need a way to always print some messages using debug
API. Luckily this is easy to do.
1 | const debug = require('debug')('foo') |
Running with DEBUG=foo
environment variable prints everything
1 | DEBUG=foo node . |
Running without DEBUG
environment variable prints the second message only
1 | node . |
Log details under namespace
In addition to module messages, you can log details useful to debugging using namespaces. For example
1 | const debug = require('debug')('foo') |
By default we log high level debug messages only
1 | DEBUG=foo node . |
But if there is some problem, we can debug it printing the value x
1 | DEBUG=foo:x node . |
Finally, we can print all messages with foo
prefix using a wildcard
1 | DEBUG=foo* node . |
Expose debug function as a module method
To simplify debugging, if a file exports an object, you can attach the debug
function to that object. This will significantly simplify unit testing. Make sure to call debug
via method reference and not directly.
1 | const debug = require('debug')('foo') |
Now you can easily spy / stub api.debug
from your unit tests using sinon.js for example.
1 | const api = require('./foo') |
Log promise from promise chains
To better log intermediate values in promise chains, I usually use R.tap
with an extra function to ensure logging the entire object in a single line.
1 | const R = require('ramda') |
If you use Bluebird promise library, it already has .tap
and .tapCatch
methods on its promises
1 | Promise = require('bluebird') |
Log values from tests
To simplify debugging failed tests I use namespace test
inside unit and end-to-end tests.
1 | const debug = require('debug')('test') |
Whenever the test fails, I can run it with DEBUG=test npm t
to see the result
variable. I do not care much for single line output because these logs are only used for debugging tests and not for production.
I will also consider switching from human-readable to JSON-by-default format using super fast pino logger, especially because it has debug
compatible module pino-debug