In this blog post I will show how to intercept messages logged by a Node process using a pre-loaded module. You can find the source code for these examples in bahmutov/capture-logs-example repository.
Capture console logs
Imagine you want to collect all strings logged from a Node program. You could proxy all console.log
calls and store them until needed. Take an example Node program like this one in index.js:
1 | console.log('this is %s log message ✅', 'console') |
When we run this program, all logs are written to their respective output streams. Note that values are substituted into placeholders like %s -> "console"
.
1 | $ node . |
Note that console.log
writes its messages to STDOUT
, while console.warn
and console.error
write to STDERR
. You can verify this by hiding STDERR
stream via redirect.
1 | # hide STDERR stream |
Great, now let's collect the messages by pre-loading a module that is going to proxy console.*
methods. Here is the log.js file:
1 | const util = require('util') |
Each method like console.log
is replaced with our function that keeps a copy of the formatted string and calls the original log
method. We can now run our original program, but require the ./log.js
first; all logged strings will be collected and printed again at the end.
1 | $ node -r ./log . |
Capture debug
logs
I love using debug
module to control my verbose logs. Just import the module and control if its messages are showing up in the terminal using an environment variable DEBUG=some:name:space
. Here is a copy of the program but with additional logging statement. See index-debug.js file:
1 | const debug = require('debug')('verbose') |
If we run the program "normally" the debug
calls are ignored. We need to enable "verbose" namespace to actually see them.
1 | $ node ./index-debug.js |
Perfect - can we capture the log calls to the debug
instances? Well, debug
module writes directly into process.stderr
stream bypassing console.*
methods, thus our proxy is not going to work. Luckily, the debug
module exposes its internals so we can easily proxy its methods. Bonus: we can log even the disabled debug
namespaces!
Look at the implementation of log-debug.js. When a new debug log instance is created using require('debug')(... namespace name ...)
, it is added to the list of debug instances. That list is an Array
instance, and a new instance is pushed to the array using "standard" debug.instances.push()
. Well, we can overwrite this method, just like we can overwrite pretty much everything in JavaScript. When we do this, we get access to every debug
instance instantiated by the process. That's where we can insert out logic to capture debug log messages - even if the namespace is disabled!
1 | const util = require('util') |
The above file log-debug.js
exports the logDebugCalls
and we can call it from our log.js
file passing the messages
list. Thus the debug messages get inserted into the same list as the intercepted console.log
messages, giving us the right order.
1 | // in log.js |
And there is the logging in action - first let's run with DEBUG=verbose
enabled. The debug
call shows up in the terminal AND is captured. Then we will run the process without DEBUG
environment variable. The debug
call is NOT in the normal terminal output, because the namespace is not enabled, yet it is still captured.
1 | $ DEBUG=verbose node -r ./log ./index-debug.js |
Beautiful - we have all the logs we might need, yet the "normal" terminal output only shows top level messages. We are now free to do anything with the verbose logs. For example we could show all the logs ONLY if something goes wrong, avoiding double test runs like locha does.
See also
- I have released the log interception in
@bahmutov/all-logs
NPM module. It can collect allconsole.{log, warn, error}
messages- all messages via
debug
module - all messages via
util.debuglog
(see the update below)
- Good logging
- OO vs FP console log example
debug
module- Source code for this blog post is in the bahmutov/capture-logs-example repository.
Update 1 - util.debuglog
In Node, there is built-in NODE_DEBUG=<namespace>
mechanism for verbose logs, see util.debuglog
.
1 | const util = require('util'); |
If you run the above piece of code with NODE_DEBUG=foo
it will show the namespace (uppercased) and process id
1 | FOO 3245: hello from foo [123] |
One can intercept these log messages by overwriting util.debuglog
method.