- Exit logging
- Log rotation
- Reopening log files
- Saving to multiple files
- Log filtering
- Transports and systemd
- Duplicate keys
- Log levels as labels instead of numbers
- Pino with
debug
When a Node process crashes from uncaught exception, exits due to a signal, or exits of it's own accord we may want to write some final logs – particularly in cases of error.
Writing to a Node.js stream on exit is not necessarily guaranteed, and naively writing to an Extreme Mode logger on exit will definitely lead to lost logs.
To write logs in an exit handler, create the handler with pino.final
:
process.on('uncaughtException', pino.final(logger, (err, finalLogger) => {
finalLogger.error(err, 'uncaughtException')
process.exit(1)
}))
process.on('unhandledRejection', pino.final(logger, (err, finalLogger) => {
finalLogger.error(err, 'unhandledRejection')
process.exit(1)
}))
The finalLogger
is a special logger instance that will synchronously and reliably
flush every log line. This is important in exit handlers, since no more asynchronous
activity may be scheduled.
Use a separate tool for log rotation:
We recommend logrotate.
Consider we output our logs to /var/log/myapp.log
like so:
$ node server.js > /var/log/myapp.log
We would rotate our log files with logrotate, by adding the following to /etc/logrotate.d/myapp
:
/var/log/myapp.log {
su root
daily
rotate 7
delaycompress
compress
notifempty
missingok
copytruncate
}
The copytruncate
configuration has a very slight possibility of lost log lines due
to a gap between copying and truncating - the truncate may occur after additional lines
have been written. To perform log rotation without copytruncate
, see the Reopening log files
help.
In cases where a log rotation tool doesn't offer a copy-truncate capabilities,
or where using them is deemed inappropriate pino.destination
and pino.extreme
destinations are able to reopen file paths after a file has been moved away.
One way to use this is to set up a SIGUSR2
or SIGHUP
signal handler that
reopens the log file destination, making sure to write the process PID out
somewhere so the log rotation tool knows where to send the signal.
// write the process pid to a well known location for later
const fs = require('fs')
fs.writeFileSync('/var/run/myapp.pid', process.pid)
const dest = pino.destination('/log/file') // pino.extreme will also work
const logger = require('pino')(dest)
process.on('SIGHUP', () => dest.reopen())
The log rotation tool can then be configured to send this signal to the process after a log rotation event has occurred.
Given a similar scenario as in the Log rotation section a basic
logrotate
config that aligns with this strategy would look similar to the following:
/var/log/myapp.log {
su root
daily
rotate 7
delaycompress
compress
notifempty
missingok
postrotate
kill -HUP `cat /var/run/myapp.pid`
endscript
}
Let's assume we want to store all error messages to a separate log file.
Install pino-tee with:
npm i pino-tee -g
The following writes the log output of app.js
to ./all-logs
, while
writing only warnings and errors to `./warn-log:
node app.js | pino-tee warn ./warn-logs > ./all-logs
The Pino philosophy advocates common, pre-existing, system utilities.
Some recommendations in line with this philosophy are:
- Use
grep
:$ # View all "INFO" level logs $ node app.js | grep '"level":30'
- Use
jq
:$ # View all "ERROR" level logs $ node app.js | jq 'select(.level == 50)'
systemd
makes it complicated to use pipes in services. One method for overcoming
this challenge is to use a subshell:
ExecStart=/bin/sh -c '/path/to/node app.js | pino-transport'
Duplicate keys are possibly when a child logger logs an object with a key that collides with a key in the child loggers bindings.
See the child logger duplicate keys caveat for information on this is handled.
Pino log lines are meant to be parseable. Thus, Pino's default mode of operation
is to print the level value instead of the string name. However, while it is
possible to set the useLevelLabels
option, we recommend using one of these
options instead if you are able:
- If the only change desired is the name then a transport can be used. One such
transport is
pino-text-level-transport
. - Use a prettifier like
pino-pretty
to make the logs human friendly.
The popular debug
is used in many modules across the ecosystem.
The pino-debug
module
can capture calls to debug
loggers and run them
through pino
instead. This results in a 10x (20x in extreme mode)
performance improvement - event though pino-debug
is logging additional
data and wrapping it in JSON.
To quickly enable this install pino-debug
and preload it with the -r
flag, enabling any debug
logs with the
DEBUG
environment variable:
$ npm i pino-debug
$ DEBUG=* node -r pino-debug app.js
pino-debug
also offers fine grain control to map specific debug
namespaces to pino
log levels. See pino-debug
for more.