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