File: README.md
pino
Very low overhead JavaScript logger.
Documentation
- Benchmarks โ
- API โ
- Browser API โ
- Redaction โ
- Child Loggers โ
- Transports โ
- Diagnostics โ
- Web Frameworks โ
- Pretty Printing โ
- Asynchronous Logging โ
- Ecosystem โ
- Help โ
- Long Term Support Policy โ
Runtimes
Node.js
Pino is built to run on Node.js.
Bare
Pino works on Bare with the pino-bare compatibility module.
Pear
Pino works on Pear, which is built on Bare, with the pino-bare compatibility module.
Install
Using NPM:
$ npm install pinoUsing YARN:
$ yarn add pinoIf you would like to install pino v6, refer to https://github.com/pinojs/pino/tree/v6.x.
Usage
const logger = require('pino')()
logger.info('hello world')
const child = logger.child({ a: 'property' })
child.info('hello child!')This produces:
{"level":30,"time":1531171074631,"msg":"hello world","pid":657,"hostname":"Davids-MBP-3.fritz.box"}
{"level":30,"time":1531171082399,"msg":"hello child!","pid":657,"hostname":"Davids-MBP-3.fritz.box","a":"property"}For using Pino with a web framework see:
- Pino with Fastify
- Pino with Express
- Pino with Hapi
- Pino with Restify
- Pino with Koa
- Pino with Node core
http - Pino with Nest
- Pino with Hono
Essentials
Development Formatting
The pino-pretty module can be used to
format logs during development:
Transports & Log Processing
Due to Node's single-threaded event-loop, it's highly recommended that sending,
alert triggering, reformatting, and all forms of log processing
are conducted in a separate process or thread.
In Pino terminology, we call all log processors "transports" and recommend that the
transports be run in a worker thread using our pino.transport API.
For more details see our Transportsโ document.
Low overhead
Using minimum resources for logging is very important. Log messages
tend to get added over time and this can lead to a throttling effect
on applications โย such as reduced requests per second.
In many cases, Pino is over 5x faster than alternatives.
See the Benchmarks document for comparisons.
Bundling support
Pino supports being bundled using tools like webpack or esbuild.
See Bundling document for more information.
The Team
Matteo Collina
https://www.npmjs.com/~matteo.collina
https://twitter.com/matteocollina
David Mark Clements
https://github.com/davidmarkclements
https://www.npmjs.com/~davidmarkclements
https://twitter.com/davidmarkclem
James Sumners
https://www.npmjs.com/~jsumners
https://twitter.com/jsumners79
Thomas Watson Steen
Contributing
Pino is an OPEN Open Source Project. This means that:
Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
See the CONTRIBUTING.md file for more details.
Acknowledgments
This project was kindly sponsored by nearForm.
This project is kindly sponsored by Platformatic.
Logo and identity designed by Cosmic Fox Design: https://www.behance.net/cosmicfox.
License
Licensed under MIT.
File: docs/api.md
API
- pino() => logger
- Logger Instance
- logger.trace()
- logger.debug()
- logger.info()
- logger.warn()
- logger.error()
- logger.fatal()
- logger.silent()
- logger.child()
- logger.bindings()
- logger.setBindings()
- logger.flush()
- logger.level
- logger.isLevelEnabled()
- logger.levels
- logger[Symbol.for('pino.serializers')]
- Event: 'level-change'
- logger.version
- logger.msgPrefix
- Statics
- Interfaces
- Types
- TypeScript
`pino([options], [destination]) => logger`
The exported pino function takes two optional arguments,options and destination, and
returns a logger instance.
`options` (Object)
`name` (String)
Default: undefined
The name of the logger. When set adds a name field to every JSON line logged.
`level` (String)
Default: 'info'
The minimum level to log: Pino will not log messages with a lower level. Setting this option reduces the load, as typically, debug and trace logs are only valid for development, and not needed in production.
One of 'fatal', 'error', 'warn', 'info', 'debug', 'trace' or 'silent'.
Additional levels can be added to the instance via the customLevels option.
`levelComparison` ("ASC", "DESC", Function)
Default: ASC
Use this option to customize levels order.
In order to be able to define custom levels ordering pass a function which will accept current and expected values and return boolean which shows should current level to be shown or not.
const logger = pino({
levelComparison: 'DESC',
customLevels: {
foo: 20, // `foo` is more valuable than `bar`
bar: 10
},
})
// OR
const logger = pino({
levelComparison: function(current, expected) {
return current >= expected;
}
})`customLevels` (Object)
Default: undefined
Use this option to define additional logging levels.
The keys of the object correspond to the namespace of the log level,
and the values should be the numerical value of the level.
const logger = pino({
customLevels: {
foo: 35
}
})
logger.foo('hi')`useOnlyCustomLevels` (Boolean)
Default: false
Use this option to only use defined customLevels and omit Pino's levels.
Logger's default level must be changed to a value in customLevels to use useOnlyCustomLevels
Warning: this option may not be supported by downstream transports.
const logger = pino({
customLevels: {
foo: 35
},
useOnlyCustomLevels: true,
level: 'foo'
})
logger.foo('hi')
logger.info('hello') // Will throw an error saying info is not found in logger object`depthLimit` (Number)
Default: 5
Maximum nesting depth used by Pino's fallback serializer.
`edgeLimit` (Number)
Default: 100
Maximum number of properties/elements per object or array used by Pino's fallback serializer.
Pino uses the fallback serializer, and therefore applies depthLimit and edgeLimit, only afterJSON.stringify throws. These options are not general limits on serialization work or output size.
For example, JSON.stringify successfully serializes a non-circular object graph that contains
repeated references to the same object. In that case, every reference is traversed and serialized
in full, without applying either limit.
`mixin` (Function):
Default: undefined
If provided, the mixin function is called each time one of the active
logging methods is called. The first parameter is the value mergeObject or an empty object. The second parameter is the log level number.
The third parameter is the logger or child logger itself, which can be used to
retrieve logger-specific context from within the mixin function.
The function must synchronously return an object. The properties of the returned object will be added to the
logged JSON.
let n = 0
const logger = pino({
mixin () {
return { line: ++n }
}
})
logger.info('hello')
// {"level":30,"time":1573664685466,"pid":78742,"hostname":"x","line":1,"msg":"hello"}
logger.info('world')
// {"level":30,"time":1573664685469,"pid":78742,"hostname":"x","line":2,"msg":"world"}The result of mixin() is supposed to be a new object. For performance reason, the object returned by mixin() will be mutated by pino.
In the following example, passing mergingObject argument to the first info call will mutate the global mixin object by default:
(* See mixinMergeStrategy option):
const mixin = {
appName: 'My app'
}
const logger = pino({
mixin() {
return mixin;
}
})
logger.info({
}, 'Message 1')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","appName":"My app","description":"Ok","msg":"Message 1"}
logger.info('Message 2')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","appName":"My app","description":"Ok","msg":"Message 2"}
// Note: the second log contains "description":"Ok" text, even if it was not provided.The mixin method can be used to add the level label to each log message such as in the following example:
const logger = pino({
mixin(_context, level) {
return { 'level-label': logger.levels.labels[level] }
}
})
logger.info({
}, 'Message 1')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","description":"Ok","level-label":"info","msg":"Message 1"}
logger.error('Message 2')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","level-label":"error","msg":"Message 2"}If the mixin feature is being used merely to add static metadata to each log message,
then a child logger โ should be used instead. Unless your application
needs to concatenate values for a specific key multiple times, in which case mixin can be
used to avoid the duplicate keys caveat:
const logger = pino({
mixin (obj, num, logger) {
return {
tags: logger.tags
}
}
})
logger.tags = {}
logger.addTag = function (key, value) {
logger.tags[key] = value
}
function createChild (parent, ...context) {
const newChild = logger.child(...context)
newChild.tags = { ...logger.tags }
newChild.addTag = function (key, value) {
newChild.tags[key] = value
}
return newChild
}
logger.addTag('foo', 1)
const child = createChild(logger, {})
child.addTag('bar', 2)
logger.info('this will only have `foo: 1`')
child.info('this will have both `foo: 1` and `bar: 2`')
logger.info('this will still only have `foo: 1`')As of pino 7.x, when the mixin is used with the nestedKey option,
the object returned from the mixin method will also be nested. Prior versions would mix
this object into the root.
const logger = pino({
nestedKey: 'payload',
mixin() {
return { requestId: requestId.currentId() }
}
})
logger.info({
}, 'Message 1')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","payload":{"requestId":"dfe9a9014b","description":"Ok"},"msg":"Message 1"}`mixinMergeStrategy` (Function):
Default: undefined
If provided, the mixinMergeStrategy function is called each time one of the active
logging methods is called. The first parameter is the value mergeObject or an empty object,
the second parameter is the value resulting from mixin() (* See mixin option or an empty object.
The function must synchronously return an object.
// Default strategy, `mergeObject` has priority
const logger = pino({
mixin() {
return { tag: 'docker' }
},
// mixinMergeStrategy(mergeObject, mixinObject) {
// return Object.assign(mixinMeta, mergeObject)
// }
})
logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"local","msg":"Message"}// Custom mutable strategy, `mixin` has priority
const logger = pino({
mixin() {
return { tag: 'k8s' }
},
mixinMergeStrategy(mergeObject, mixinObject) {
return Object.assign(mergeObject, mixinObject)
}
})
logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"k8s","msg":"Message"}// Custom immutable strategy, `mixin` has priority
const logger = pino({
mixin() {
return { tag: 'k8s' }
},
mixinMergeStrategy(mergeObject, mixinObject) {
return Object.assign({}, mergeObject, mixinObject)
}
})
logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"k8s","msg":"Message"}`redact` (Array | Object):
Default: undefined
As an array, the redact option specifies paths that should
have their values redacted from any log output.
Each path must be a string using a syntax that corresponds to JavaScript dot and bracket notation.
If an object is supplied, three options can be specified:
paths(array): Required. An array of paths. See redaction - Path Syntax โ for specifics.censor(String|Function|Undefined): Optional. When supplied as a String thecensoroption will overwrite keys that are to be redacted. When set toundefinedthe key will be removed entirely from the object.
Thecensoroption may also be a mapping function. The (synchronous) mapping function has the signature(value, path) => redactedValueand is called with the unredactedvalueandpathto the key being redacted, as an array. For example given a redaction path ofa.b.cthepathargument would be['a', 'b', 'c']. The value returned from the mapping function becomes the applied censor value. Default:'[Redacted]'
value synchronously.
Default:'[Redacted]'remove(Boolean): Optional. Instead of censoring the value, remove both the key and the value. Default:false
WARNING: Never allow user input to define redacted paths.
- See the redaction โ documentation.
- See fast-redact#caveat โ
`hooks` (Object)
An object mapping to hook functions. Hook functions allow for customizing
internal logger operations. Hook functions must be synchronous functions.
`logMethod`
Allows for manipulating the parameters passed to logger methods. The signature
for this hook is logMethod (args, method, level) {}, where args is an array
of the arguments that were passed to the log method and method is the log
method itself, level is the log level itself. This hook must invoke themethod function by using apply, like so: method.apply(this, newArgumentsArray).
For example, Pino expects a binding object to be the first parameter with an
optional string message as the second parameter. Using this hook the parameters
can be flipped:
const hooks = {
logMethod (inputArgs, method, level) {
if (inputArgs.length >= 2) {
const arg1 = inputArgs.shift()
const arg2 = inputArgs.shift()
return method.apply(this, [arg2, arg1, ...inputArgs])
}
return method.apply(this, inputArgs)
}
}`streamWrite`
Allows for manipulating the stringified JSON log data just before writing to various transports.
The method receives the stringified JSON and must return valid stringified JSON.
For example:
const hooks = {
streamWrite (s) {
return s.replaceAll('sensitive-api-key', 'XXX')
}
}`formatters` (Object)
An object containing functions for formatting the shape of the log lines.
These functions should return a JSONifiable object and
should never throw. These functions allow for full customization of
the resulting log lines. For example, they can be used to change
the level key name or to enrich the default metadata.
`level`
Changes the shape of the log level. The default shape is { level: number }.
The function takes two arguments, the label of the level (e.g. 'info')
and the numeric value (e.g. 30).
When using multiple transport targets, the log record must keep a numeric level field for per-target routing. Because of that, formatters.level cannot be used to replace level with a string or rename it in multi-target mode. timestamp customization is still supported.
const formatters = {
level (label, number) {
return { level: number }
}
}`bindings`
Changes the shape of the bindings. The default shape is { pid, hostname }.
The function takes a single argument, the bindings object, which can be configured
using the base option. Called once when creating logger.
const formatters = {
bindings (bindings) {
return { pid: bindings.pid, hostname: bindings.hostname }
}
}`log`
Changes the shape of the log object. This function will be called every time
one of the log methods (such as .info) is called. All arguments passed to the
log method, except the message, will be passed to this function. By default, it does
not change the shape of the log object.
const formatters = {
log (object) {
return object
}
}`serializers` (Object)
Default: {err: pino.stdSerializers.err}
An object containing functions for custom serialization of objects.
These functions should return an JSONifiable object and they
should never throw. When logging an object, each top-level property
matching the exact key of a serializer will be serialized using the defined serializer.
The serializers are applied when a property in the logged object matches a property
in the serializers. The only exception is the err serializer as it is also applied in case
the object is an instance of Error, e.g. logger.info(new Error('kaboom')).
See errorKey option to change err namespace.
`msgPrefix` (String)
Default: undefined
The msgPrefix property allows you to specify a prefix for every message of the logger and its children.
const logger = pino({
msgPrefix: '[HTTP] '
})
logger.info('got new request!')
// > [HTTP] got new request!
const child = logger.child({})
child.info('User authenticated!')
// > [HTTP] User authenticated!`base` (Object)
Default: {pid: process.pid, hostname: os.hostname()}
Key-value object added as child logger to each log line.
Set to undefined to avoid adding pid, hostname properties to each log.
`enabled` (Boolean)
Default: true
Set to false to disable logging.
`crlf` (Boolean)
Default: false
Set to true to logs newline delimited JSON with \r\n instead of \n.
`timestamp` (Boolean | Function)
Default: true
Enables or disables the inclusion of a timestamp in the
log message. If a function is supplied, it must synchronously return a partial JSON string
representation of the time, e.g. ,"time":1493426328206 (which is the default).
If set to false, no timestamp will be included in the output.
See stdTimeFunctions for a set of available functions
for passing in as a value for this option.
Example:
timestamp: () => `,"time":"${new Date(Date.now()).toISOString()}"`
// which is equivalent to:
// timestamp: stdTimeFunctions.isoTimeCaution: attempting to format time in-process will significantly impact logging performance.
`messageKey` (String)
Default: 'msg'
The string key for the 'message' in the JSON object.
`errorKey` (String)
Default: 'err'
The string key for the 'error' in the JSON object.
`nestedKey` (String)
Default: null
If there's a chance that objects being logged have properties that conflict with those from pino itself (level, timestamp, pid, etc)
and duplicate keys in your log records are undesirable, pino can be configured with a nestedKey option that causes any objects that are logged
to be placed under a key whose name is the value of nestedKey.
This way, when searching something like Kibana for values, one can consistently search under the configured nestedKey value instead of the root log record keys.
For example,
const logger = require('pino')({
nestedKey: 'payload'
})
const thing = { level: 'hi', time: 'never', foo: 'bar'} // has pino-conflicting properties!
logger.info(thing)
// logs the following:
// {"level":30,"time":1578357790020,"pid":91736,"hostname":"x","payload":{"level":"hi","time":"never","foo":"bar"}}In this way, logged objects' properties don't conflict with pino's standard logging properties,
and searching for logged objects can start from a consistent path.
`browser` (Object)
Browser only, may have asObject and write keys. This option is separately
documented in the Browser API โ documentation.
- See Browser API โ
`transport` (Object)
The transport option is a shorthand for the pino.transport() function.
It supports the same input options:
require('pino')({
transport: {
target: '/absolute/path/to/my-transport.mjs'
}
})
// or multiple transports
require('pino')({
transport: {
targets: [
{ target: '/absolute/path/to/my-transport.mjs', level: 'error' },
{ target: 'some-file-transport', options: { destination: '/dev/null' }
]
}
})If the transport option is supplied to pino, a destination parameter may not also be passed as a separate argument to pino:
pino({ transport: {}}, '/path/to/somewhere') // THIS WILL NOT WORK, DO NOT DO THIS
pino({ transport: {}}, process.stderr) // THIS WILL NOT WORK, DO NOT DO THISwhen using the transport option. In this case, an Error will be thrown.
- See pino.transport()
`onChild` (Function)
The onChild function is a synchronous callback that will be called on each creation of a new child, passing the child instance as its first argument.
Any error thrown inside the callback will be uncaught and should be handled inside the callback.
const parent = require('pino')({ onChild: (instance) => {
// Execute call back code for each newly created child.
}})
// `onChild` will now be executed with the new child.
parent.child(bindings)`destination` (Number | String | Object | DestinationStream | SonicBoomOpts | WritableStream)
Default: pino.destination(1) (STDOUT)
The destination parameter can be a file descriptor, a file path, or an
object with dest property pointing to a fd or path.
An ordinary Node.js stream file descriptor can be passed as the
destination (such as the result
of fs.createWriteStream) but for peak log writing performance, it is strongly
recommended to use pino.destination to create the destination stream.
Note that the destination parameter can be the result of pino.transport().
// pino.destination(1) by default
const stdoutLogger = require('pino')()
// destination param may be in first position when no options:
const fileLogger = require('pino')( pino.destination('/log/path'))
// use the stderr file handle to log to stderr:
const opts = {name: 'my-logger'}
const stderrLogger = require('pino')(opts, pino.destination(2))
// automatic wrapping in pino.destination
const fileLogger = require('pino')('/log/path')
// Asynchronous logging
const fileLogger = pino(pino.destination({ dest: '/log/path', sync: false }))However, there are some special instances where pino.destination is not used as the default:
- When something, e.g a process manager, has monkey-patched
process.stdout.write.
In these cases process.stdout is used instead.
Note: If the parameter is a string integer, e.g. '1', it will be coerced to
a number and used as a file descriptor. If this is not desired, provide a full
path, e.g. /tmp/1.
- See
pino.destination
`destination[Symbol.for('pino.metadata')]`
Default: false
Using the global symbol Symbol.for('pino.metadata') as a key on the destination parameter and
setting the key to true, indicates that the following properties should be
set on the destination object after each log line is written:
- the last logging level as
destination.lastLevel - the last logging message as
destination.lastMsg - the last logging object as
destination.lastObj - the last time as
destination.lastTime, which will be the partial string returned
by the time function. - the last logger instance as
destination.lastLogger(to support child
loggers)
The following is a succinct usage example:
const dest = pino.destination('/dev/null')
dest[Symbol.for('pino.metadata')] = true
const logger = pino(dest)
logger.info({a: 1}, 'hi')
const { lastMsg, lastLevel, lastObj, lastTime} = dest
console.log(
'Logged message "%s" at level %d with object %o at time %s',
lastMsg, lastLevel, lastObj, lastTime
) // Logged message "hi" at level 30 with object { a: 1 } at time 1531590545089Logger Instance
The logger instance is the object returned by the main exportedpino function.
The primary purpose of the logger instance is to provide logging methods.
The default logging methods are trace, debug, info, warn, error, and fatal.
Each logging method has the following signature:([mergingObject], [message], [...interpolationValues]).
The parameters are explained below using the logger.info method but the same applies to all logging methods.
Logging Method Parameters
`mergingObject` (Object)
An object can optionally be supplied as the first parameter. Each enumerable key and value
of the mergingObject is copied into the JSON log line.
logger.info({MIX: {IN: true}})
// {"level":30,"time":1531254555820,"pid":55956,"hostname":"x","MIX":{"IN":true}}Do not pass an externally supplied object directly as the mergingObject.
Top-level keys are written at the root of the log line and can conflict with
Pino fields such as level, time, or msg, as well as application or
security fields. If untrusted data must be logged, place it under an
application-controlled key:
// Avoid: user-controlled keys become top-level log fields
logger.info(untrustedObject)
// Prefer: user-controlled keys are contained under a trusted namespace
logger.info({ untrusted: untrustedObject })As a matter of good security hygiene, prefer not to log untrusted data at all
unless it is necessary. When it is necessary, sanitize and redact it first.
The same top-level key guidance applies to logger.child() bindings
and logger.setBindings().
If the object is of type Error, it is wrapped in an object containing a property err ({ err: mergingObject }).
This allows for a unified error handling flow.
Options serializers and errorKey could be used at instantiation time to change the namespace
from err to another string as preferred.
`message` (String)
A message string can optionally be supplied as the first parameter, or
as the second parameter after supplying a mergingObject.
By default, the contents of the message parameter will be merged into the
JSON log line under the msg key:
logger.info('hello world')
// {"level":30,"time":1531257112193,"msg":"hello world","pid":55956,"hostname":"x"}The message parameter takes precedence over the mergingObject.
That is, if a mergingObject contains a msg property, and a message parameter
is supplied in addition, the msg property in the output log will be the value of
the message parameter not the value of the msg property on the mergingObject.
See Avoid Message Conflict for information
on how to overcome this limitation.
If no message parameter is provided, and the mergingObject is of type Error or it has a property named err, themessage parameter is set to the message value of the error. See option errorKey if you want to change the namespace.
The messageKey option can be used at instantiation time to change the namespace
from msg to another string as preferred.
The message string may contain a printf style string with support for
the following placeholders:
%sโ string placeholder, every non-string value passed in will have.toString()called.%dโ digit placeholder%O,%o, and%jโ object placeholder
Values supplied as additional arguments to the logger method will
then be interpolated accordingly.
`...interpolationValues` (Any)
All arguments supplied after message are serialized and interpolated according
to any supplied printf-style placeholders (%s, %d, %o|%O|%j) to form
the final output msg value for the JSON log line.
logger.info('%o hello %s', {worldly: 1}, 'world')
// {"level":30,"time":1531257826880,"msg":"{\"worldly\":1} hello world","pid":55956,"hostname":"x"}Since pino v6, we do not automatically concatenate and cast to string
consecutive parameters:
logger.info('hello', 'world')
// {"level":30,"time":1531257618044,"msg":"hello","pid":55956,"hostname":"x"}
// world is missingHowever, it's possible to inject a hook to modify this behavior:
const pinoOptions = {
hooks: { logMethod }
}
function logMethod (args, method) {
if (args.length === 2) {
args[0] = `${args[0]} %j`
}
method.apply(this, args)
}
const logger = pino(pinoOptions)Errors
Errors can be supplied as either the first parameter or if already using mergingObject then as the err property on the mergingObject.
Options serializers and errorKey could be used at instantiation time to change the namespace
from err to another string as preferred.
Note
This section describes the default configuration. The error serializer can be
mapped to a different key using theserializersoption.
logger.info(new Error("test"))
// {"level":30,"time":1531257618044,"msg":"test","stack":"...","type":"Error","pid":55956,"hostname":"x"}
logger.info({ err: new Error("test"), otherkey: 123 }, "some text")
// {"level":30,"time":1531257618044,"err":{"msg": "test", "stack":"...","type":"Error"},"msg":"some text","pid":55956,"hostname":"x","otherkey":123}`logger.trace([mergingObject], [message], [...interpolationValues])`
Write a 'trace' level log, if the configured level allows for it.
- See
mergingObjectlog method parameter - See
messagelog method parameter - See
...interpolationValueslog method parameter
`logger.debug([mergingObject], [message], [...interpolationValues])`
Write a 'debug' level log, if the configured level allows for it.
- See
mergingObjectlog method parameter - See
messagelog method parameter - See
...interpolationValueslog method parameter
`logger.info([mergingObject], [message], [...interpolationValues])`
Write an 'info' level log, if the configured level allows for it.
- See
mergingObjectlog method parameter - See
messagelog method parameter - See
...interpolationValueslog method parameter
`logger.warn([mergingObject], [message], [...interpolationValues])`
Write a 'warn' level log, if the configured level allows for it.
- See
mergingObjectlog method parameter - See
messagelog method parameter - See
...interpolationValueslog method parameter
`logger.error([mergingObject], [message], [...interpolationValues])`
Write a 'error' level log, if the configured level allows for it.
- See
mergingObjectlog method parameter - See
messagelog method parameter - See
...interpolationValueslog method parameter
`logger.fatal([mergingObject], [message], [...interpolationValues])`
Write a 'fatal' level log, if the configured level allows for it.
Since 'fatal' level messages are intended to be logged just before the process exiting the fatal
method will always sync flush the destination.
Therefore it's important not to misuse fatal since
it will cause performance overhead if used for any
other purpose than writing final log messages before
the process crashes or exits.
- See
mergingObjectlog method parameter - See
messagelog method parameter - See
...interpolationValueslog method parameter
`logger.silent()`
Noop function.
`logger.child(bindings, [options]) => logger`
The logger.child method allows for the creation of stateful loggers,
where key-value pairs can be pinned to a logger causing them to be output
on every log line.
Child loggers use the same output stream as the parent and inherit
the current log level of the parent at the time they are spawned.
The log level of a child is mutable. It can be set independently
of the parent either by setting the level accessor after creating
the child logger or using the options.level key.
`bindings` (Object)
An object of key-value pairs to include in every log line output
via the returned child logger.
const child = logger.child({ MIX: {IN: 'always'} })
child.info('hello')
// {"level":30,"time":1531258616689,"msg":"hello","pid":64849,"hostname":"x","MIX":{"IN":"always"}}
child.info('child!')
// {"level":30,"time":1531258617401,"msg":"child!","pid":64849,"hostname":"x","MIX":{"IN":"always"}}The bindings object may contain any key except for reserved configuration keys level and serializers.
Do not pass externally supplied objects directly as child bindings. Binding keys
are top-level log fields emitted on every log line from the child logger, so
user-controlled keys can conflict with Pino fields such as level, time, ormsg, as well as application or security fields. If untrusted data must be
included in child bindings, place it under an application-controlled key:
// Avoid: user-controlled keys become top-level fields on every child log line
const child = logger.child(untrustedObject)
// Prefer: user-controlled keys are contained under a trusted namespace
const child = logger.child({ untrusted: untrustedObject })As a matter of good security hygiene, prefer not to log untrusted data at all
unless it is necessary. When it is necessary, sanitize and redact it first.
See also mergingObject and logger.setBindings().
`bindings.serializers` (Object) - DEPRECATED
Use options.serializers instead.
`options` (Object)
Options for child logger. These options will override the parent logger options.
`options.level` (String)
The level property overrides the log level of the child logger.
By default, the parent log level is inherited.
After the creation of the child logger, it is also accessible using the logger.level key.
const logger = pino()
logger.debug('nope') // will not log, since default level is info
const child = logger.child({foo: 'bar'}, {level: 'debug'})
child.debug('debug!') // will log as the `level` property set the level to debug`options.msgPrefix` (String)
Default: undefined
The msgPrefix property allows you to specify a prefix for every message of the child logger.
By default, the parent prefix is inherited.
If the parent already has a prefix, the prefix of the parent and then the child will be displayed.
const logger = pino({
msgPrefix: '[HTTP] '
})
logger.info('got new request!')
// > [HTTP] got new request!
const child = logger.child({avengers: 'assemble'}, {msgPrefix: '[Proxy] '})
child.info('message proxied!')
// > [HTTP] [Proxy] message proxied!`options.redact` (Array | Object)
Setting options.redact to an array or object will override the parent redact options. To remove redact options inherited from the parent logger set this value as an empty array ([]).
const logger = require('pino')({ redact: ['hello'] })
logger.info({ hello: 'world' })
// {"level":30,"time":1625794363403,"pid":67930,"hostname":"x","hello":"[Redacted]"}
const child = logger.child({ foo: 'bar' }, { redact: ['foo'] })
logger.info({ hello: 'world' })
// {"level":30,"time":1625794553558,"pid":67930,"hostname":"x","hello":"world", "foo": "[Redacted]" }- See
redactoption
`options.serializers` (Object)
Child loggers inherit the serializers from the parent logger.
Setting the serializers key of the options object will override
any configured parent serializers.
const logger = require('pino')()
logger.info({test: 'will appear'})
// {"level":30,"time":1531259759482,"pid":67930,"hostname":"x","test":"will appear"}
const child = logger.child({}, {serializers: {test: () => `child-only serializer`}})
child.info({test: 'will be overwritten'})
// {"level":30,"time":1531259784008,"pid":67930,"hostname":"x","test":"child-only serializer"}`logger.bindings()`
Returns an object containing all the current bindings, cloned from the ones passed in via logger.child().
const child = logger.child({ foo: 'bar' })
console.log(child.bindings())
// { foo: 'bar' }
const anotherChild = child.child({ MIX: { IN: 'always' } })
console.log(anotherChild.bindings())
// { foo: 'bar', MIX: { IN: 'always' } }`logger.setBindings(bindings)`
Adds to the bindings of this logger instance.
Note: Does not overwrite bindings. Can potentially result in duplicate keys in
log lines.
Do not pass externally supplied objects directly as bindings. Binding keys are
top-level log fields and can conflict with Pino fields or application fields.
If untrusted data must be added, place it under an application-controlled key,
for example logger.setBindings({ untrusted }), and sanitize or redact it first.
`logger.flush([cb])`
Flushes the content of the buffer when using pino.destination({ sync: false }).
This is an asynchronous, best used as fire and forget, operation.
The use case is primarily for asynchronous logging, which may buffer
log lines while others are being written. The logger.flush method can be
used to flush the logs
on a long interval, say ten seconds. Such a strategy can provide an
optimum balance between extremely efficient logging at high demand periods
and safer logging at low demand periods.
If there is a need to wait for the logs to be flushed, a callback should be used.
Note: flush() does not work when using pino-pretty. See
Flush Limitations with pino-pretty for more details.
`logger.level` (String) [Getter/Setter]
Set this property to the desired logging level.
The core levels and their values are as follows:
| Level: | trace | debug | info | warn | error | fatal | silent |
| Value: | 10 | 20 | 30 | 40 | 50 | 60 | Infinity |
The logging level is a minimum level based on the associated value of that level.
For instance if logger.level is info (30) then info (30), warn (40), error (50), and fatal (60) log methods will be enabled but the trace (10) and debug (20) methods, being less than 30, will not.
The silent logging level is a specialized level that will disable all logging,
the silent log method is a noop function.
`logger.isLevelEnabled(level)`
A utility method for determining if a given log level will write to the destination.
`level` (String)
The given level to check against:
if (logger.isLevelEnabled('debug')) logger.debug('conditional log')`levelLabel` (String)
Defines the method name of the new level.
- See
logger.level
`levelValue` (Number)
Defines the associated minimum threshold value for the level, and
therefore where it sits in order of priority among other levels.
- See
logger.level
`logger.levelVal` (Number)
Supplies the integer value for the current logging level.
if (logger.levelVal === 30) {
console.log('logger level is `info`')
}`logger.levels` (Object)
Levels are mapped to values to determine the minimum threshold that a
logging method should be enabled at (see logger.level).
The logger.levels property holds the mappings between levels and values,
and vice versa.
$ node -p "require('pino')().levels"{ labels:
{ '10': 'trace',
'20': 'debug',
'30': 'info',
'40': 'warn',
'50': 'error',
'60': 'fatal' },
values:
{ fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 } }- See
logger.level
logger\[Symbol.for('pino.serializers')\]
Returns the serializers as applied to the current logger instance. If a child logger did not
register its own serializer upon instantiation the serializers of the parent will be returned.
Event: 'level-change'
The logger instance is also an EventEmitter โ
A listener function can be attached to a logger via the level-change event
The listener is passed five arguments:
levelLabelโ the new level string, e.gtracelevelValueโย the new level number, e.g10previousLevelLabelโ the prior level string, e.ginfopreviousLevelValueโ the prior level number, e.g30loggerโ the logger instance from which the event originated
const logger = require('pino')()
logger.on('level-change', (lvl, val, prevLvl, prevVal) => {
console.log('%s (%d) was changed to %s (%d)', prevLvl, prevVal, lvl, val)
})
logger.level = 'trace' // trigger eventPlease note that due to a known bug, every logger.child() call will
fire a level-change event. These events can be ignored by writing an event handler like:
const logger = require('pino')()
logger.on('level-change', function (lvl, val, prevLvl, prevVal, instance) {
if (logger !== instance) {
return
}
console.log('%s (%d) was changed to %s (%d)', prevLvl, prevVal, lvl, val)
})
logger.child({}); // trigger an event by creating a child instance, notice no console.log
logger.level = 'trace' // trigger event using actual value change, notice console.log`logger.version` (String)
Exposes the Pino package version. Also available on the exported pino function.
- See
pino.version
`logger.msgPrefix` (String|Undefined)
Exposes the cumulative msgPrefix of the logger.
Statics
`pino.destination([opts]) => SonicBoom`
Create a Pino Destination instance: a stream-like object with
significantly more throughput than a standard Node.js stream.
const pino = require('pino')
const logger = pino(pino.destination('./my-file'))
const logger2 = pino(pino.destination())
const logger3 = pino(pino.destination({
dest: './my-file',
minLength: 4096, // Buffer before writing
sync: false // Asynchronous logging, the default
}))
const logger4 = pino(pino.destination({
dest: './my-file2',
sync: true // Synchronous logging
}))The pino.destination method may be passed a file path or a numerical file descriptor.
By default, pino.destination will use process.stdout.fd (1) as the file descriptor.
pino.destination is implemented on sonic-boom โ.
A pino.destination instance can also be used to reopen closed files
(for example, for some log rotation scenarios), see Reopening log files.
`pino.transport(options) => ThreadStream`
Create a stream that routes logs to a worker thread that
wraps around a Pino Transport.
const pino = require('pino')
const transport = pino.transport({
target: 'some-transport',
options: { some: 'options for', the: 'transport' }
})
pino(transport)Multiple transports may also be defined, and specific levels can be logged to each transport:
const pino = require('pino')
const transport = pino.transport({
targets: [{
level: 'info',
target: 'pino-pretty' // must be installed separately
}, {
level: 'trace',
target: 'pino/file',
options: { destination: '/path/to/store/logs' }
}]
})
pino(transport)A pipeline could also be created to transform log lines before sending them:
const pino = require('pino')
const transport = pino.transport({
pipeline: [{
target: 'pino-syslog' // must be installed separately
}, {
target: 'pino-socket' // must be installed separately
}]
})
pino(transport)Multiple transports can now be defined to include pipelines:
const pino = require('pino')
const transport = pino.transport({
targets: [{
level: 'info',
target: 'pino-pretty' // must be installed separately
}, {
level: 'trace',
target: 'pino/file',
options: { destination: '/path/to/store/logs' }
}, {
pipeline: [{
target: 'pino-syslog' // must be installed separately
}, {
target: 'pino-socket' // must be installed separately
}]
}
]
})
pino(transport)If WeakRef, WeakMap, and FinalizationRegistry are available in the current runtime (v14.5.0+), then the thread
will be automatically terminated in case the stream or logger goes out of scope.
The transport() function adds a listener to process.on('beforeExit') and process.on('exit') to ensure the worker
is flushed and all data synced before the process exits.
Note that calling process.exit() on the main thread will stop the event loop on the main thread from turning. As a result,
using console.log and process.stdout after the main thread called process.exit() will not produce any output.
If you are embedding/integrating pino within your framework, you will need to make pino aware of the script that is calling it,
like so:
const pino = require('pino')
const getCaller = require('get-caller-file')
module.exports = function build () {
const logger = pino({
transport: {
caller: getCaller(),
target: 'transport',
options: { destination: './destination' }
}
})
return logger
}Note that any 'error' event emitted by the transport must be considered a fatal error and the process must be terminated.
Error events are not recoverable.
For more on transports, how they work, and how to create them see the Transports documentation.
- See
Transports - See
thread-streamโ
Options
target: The transport to pass logs through. This may be an installed module name or an absolute path.options: An options object which is serialized (see Structured Clone Algorithm), passed to the worker thread, parsed and then passed to the exported transport function.worker: Worker thread configuration options. Additionally, theworkeroption supportsworker.autoEnd. If this is set tofalselogs will not be flushed on process exit. It is then up to the developer to calltransport.end()to flush logs.targets: May be specified instead oftarget. Must be an array of transport configurations and/or pipelines. Transport configurations include the aforementionedoptionsandtargetoptions plus aleveloption which will send only logs above a specified level to that transport. Iflevelis omitted, it defaults toinfo.pipeline: May be specified instead oftarget. Must be an array of transport configurations. Transport configurations include the aforementionedoptionsandtargetoptions. All intermediate steps in the pipeline must beTransformstreams and notWritable.dedupe: See pino.multistream options
Notes:
- The top-level
levelinpino.transport({ ... })is not used for filtering. - With a single
target(or a singlepipeline), filtering is controlled bylogger.level. - Per-transport level filtering is applied when using
targets(multi-destination mode).
`pino.multistream(streamsArray, opts) => MultiStreamRes`
Create a stream composed by multiple destination streams and returns an
object implementing the MultiStreamRes interface.
var fs = require('node:fs')
var pino = require('pino')
var pretty = require('pino-pretty')
var streams = [
{stream: fs.createWriteStream('/tmp/info.stream.out')},
{stream: pretty() },
{level: 'debug', stream: fs.createWriteStream('/tmp/debug.stream.out')},
{level: 'fatal', stream: fs.createWriteStream('/tmp/fatal.stream.out')}
]
var log = pino({
level: 'debug' // this MUST be set at the lowest level of the
// destinations
}, pino.multistream(streams))
log.debug('this will be written to /tmp/debug.stream.out')
log.info('this will be written to /tmp/debug.stream.out and /tmp/info.stream.out')
log.fatal('this will be written to /tmp/debug.stream.out, /tmp/info.stream.out and /tmp/fatal.stream.out')In order for multistream to work, the log level must be set to the lowest level used in the streams array. Default is info.
Options
levels: Pass custom log level definitions to the instance as an object.
dedupe: Set this totrueto send logs only to the stream with the higher level. Default:falsededupeflag can be useful for example when usingpino.multistreamto redirecterrorlogs toprocess.stderrand others toprocess.stdout:jsvar pino = require('pino') var multistream = pino.multistream var streams = [ {level: 'debug', stream: process.stdout}, {level: 'error', stream: process.stderr}, ] var opts = { levels: { silent: Infinity, fatal: 60, error: 50, warn: 50, info: 30, debug: 20, trace: 10 }, dedupe: true, } var log = pino({ level: 'debug' // this MUST be set at the lowest level of the // destinations }, multistream(streams, opts)) log.debug('this will be written ONLY to process.stdout') log.info('this will be written ONLY to process.stdout') log.error('this will be written ONLY to process.stderr') log.fatal('this will be written ONLY to process.stderr')
`pino.stdSerializers` (Object)
The pino.stdSerializers object provides functions for serializing objects common to many projects. The standard serializers are directly imported from pino-std-serializers.
`pino.stdTimeFunctions` (Object)
The timestamp option can accept a function that determines thetimestamp value in a log line.
The pino.stdTimeFunctions object provides a very small set of common functions for generating thetimestamp property. These consist of the following
pino.stdTimeFunctions.epochTime: Milliseconds since Unix epoch (Default)pino.stdTimeFunctions.unixTime: Seconds since Unix epochpino.stdTimeFunctions.nullTime: Clears timestamp property (Used whentimestamp: false)pino.stdTimeFunctions.isoTime: ISO 8601-formatted time in UTCpino.stdTimeFunctions.isoTimeNano: RFC 3339-formatted time in UTC with nanosecond precisionSee
timestampoption
`pino.symbols` (Object)
For integration purposes with ecosystem and third-party libraries pino.symbols
exposes the symbols used to hold non-public state and methods on the logger instance.
Access to the symbols allows logger state to be adjusted, and methods to be overridden or
proxied for performant integration where necessary.
The pino.symbols object is intended for library implementers and shouldn't be utilized
for general use.
`pino.version` (String)
Exposes the Pino package version. Also available on the logger instance.
- See
logger.version
Interfaces
`MultiStreamRes`
Properties:
write(data)dataObject | string- Returns: void
Write data onto the streams held by the current instance.
add(dest)destStreamEntry | DestinationStream- Returns: MultiStreamRes
Add dest stream to the array of streams of the current instance.
flushSync()- Returns:
undefined
- Returns:
Call flushSync on each stream held by the current instance.
lastId- number
The ID assigned to the last stream assigned to the current instance.
minLevel- number
The minimum level amongst all the streams held by the current instance.
remove(id)id[number]
Removes a stream from the array of streams of the current instance using its assigned ID.
streams- Returns: StreamEntry[]
The array of streams currently held by the current instance.
clone(level)levelLevel- Returns: MultiStreamRes
Returns a cloned object of the current instance but with the provided level.
`StreamEntry`
Properties:
stream- DestinationStream
level- Optional: Level
`DestinationStream`
Properties:
write(msg)msgstring
Types
`Level`
- Values:
"fatal"|"error"|"warn"|"info"|"debug"|"trace"
TypeScript
Module Augmentation
Pino supports TypeScript module augmentation to extend its type definitions. This allows you to customize the logging behavior to fit your application's specific requirements.
`LogFnFields` Interface
The LogFnFields interface can be augmented to control what fields are allowed in logging method objects. This is particularly useful for:
- Preventing certain fields from being logged (for security or compliance reasons)
- Enforcing specific field types across your application
- Enforcing consistent structured logging
Banning Fields
You can ban specific fields from being passed to logging methods by setting them to never. This helps prevent users from unintentionally overriding fields that are already set in the logger's base option, or clarifies that these fields are predefined.
declare module "pino" {
interface LogFnFields {
service?: never;
version?: never;
}
}
// These will now cause TypeScript errors
logger.info({ service: 'other-api', message: 'success' }) // โ
logger.info({ message: 'success' }) // โ
Enforcing Field Types
You can also enforce specific types for certain fields:
declare module "pino" {
interface LogFnFields {
userId?: string;
requestId?: string;
}
}
// These will cause TypeScript errors
logger.info({ userId: 123 }) // โ Error: userId must be string
logger.info({ requestId: null }) // โ Error: requestId must be string
// This works fine
logger.info({ userId: '123' }) // โ
Works fineEnforcing Structured Logging
Required fields (non-optional) enforce consistent structured logging by requiring specific fields in all log objects:
declare module "pino" {
interface LogFnFields {
userId: string
}
}
logger.info({ userId: '123' }) // โ
Works fine
logger.info({}) // โ Property 'userId' is missing in type '{}'Note: Required fields will cause TypeScript errors when logging certain types like Error objects that don't contain the required properties:
logger.error(new Error('test')) // โ Property 'userId' is missing in type 'Error'This ensures that all log entries include required context fields, promoting consistent logging practices.
File: docs/asynchronous.md
Asynchronous Logging
Asynchronous logging enables the minimum overhead of Pino.
Asynchronous logging works by buffering log messages and writing them in larger chunks.
const pino = require('pino')
const logger = pino(pino.destination({
dest: './my-file', // omit for stdout
minLength: 4096, // Buffer before writing
sync: false // Asynchronous logging
}))It's always possible to turn on synchronous logging by passing sync: true.
In this mode of operation, log messages are directly written to the
output stream as the messages are generated with a blocking operation.
- See
pino.destination pino.destinationis implemented onsonic-boomโ.
AWS Lambda
Asynchronous logging in AWS Lambda functions will tend to cause delayed
or lost log messages, as logs may not be written to the destination
before the runtime is frozen.
For Lambda deployments, it's recommended to always use a destination
with sync: true.
Caveats
Asynchronous logging has a couple of important caveats:
- As opposed to the synchronous mode, there is not a one-to-one relationship between
calls to logging methods (e.g.logger.info) and writes to a log file - There is a possibility of the most recently buffered log messages being lost
in case of a system failure, e.g. a power cut.
Flush Limitations with `pino-pretty`
The logger.flush() method does not work when using pino-pretty because:
Transport Architecture:
pino-prettyruns in a separate worker thread via the transport mechanism.Buffer Flow: When you call
logger.flush(), it flushes the SonicBoom destination in the main thread, but the logs remain queued in the thread-stream worker waiting to be processed bypino-pretty.No Cross-Thread Flush: The flush operation never propagates through to the worker thread where the pretty printer is processing the output.
This means that even with logger.flush(), your formatted logs may not appear immediately, and the flush will only ensure the main thread buffer is written, not the formatted output.
See also:
File: docs/benchmarks.md
Benchmarks
pino.info('hello world'):
BASIC benchmark averages
Bunyan average: 377.434ms
Winston average: 270.249ms
Bole average: 172.690ms
Debug average: 220.527ms
LogLevel average: 222.802ms
Pino average: 114.801ms
PinoMinLength average: 70.968ms
PinoNodeStream average: 159.192mspino.info({'hello': 'world'}):
OBJECT benchmark averages
BunyanObj average: 410.379ms
WinstonObj average: 273.120ms
BoleObj average: 185.069ms
LogLevelObject average: 433.425ms
PinoObj average: 119.315ms
PinoMinLengthObj average: 76.968ms
PinoNodeStreamObj average: 164.268mspino.info(aBigDeeplyNestedObject):
DEEP-OBJECT benchmark averages
BunyanDeepObj average: 1.839ms
WinstonDeepObj average: 5.604ms
BoleDeepObj average: 3.422ms
LogLevelDeepObj average: 11.716ms
PinoDeepObj average: 2.256ms
PinoMinLengthDeepObj average: 2.240ms
PinoNodeStreamDeepObj average: 2.595mspino.info('hello %s %j %d', 'world', {obj: true}, 4, {another: 'obj'}):
For a fair comparison, LogLevel was extended
to include a timestamp and bole hadfastTime mode switched on.
File: docs/browser.md
Browser API
Pino is compatible with browserify for browser-side usage:
This can be useful with isomorphic/universal JavaScript code.
By default, in the browser,pino uses corresponding Log4j console methods (console.error, console.warn, console.info, console.debug, console.trace) and uses console.error for any fatal level logs.
Options
Pino can be passed a browser object in the options object,
which can have the following properties:
`asObject` (Boolean)
const pino = require('pino')({browser: {asObject: true}})The asObject option will create a pino-like log object instead of
passing all arguments to a console method, for instance:
pino.info('hi') // creates and logs {msg: 'hi', level: 30, time: <ts>}When write is set, asObject will always be true.
`asObjectBindingsOnly` (Boolean)
const pino = require('pino')({browser: {asObjectBindingsOnly: true}})The asObjectBindingsOnly option is similar to asObject but will keep the message
and arguments unformatted. This allows to defer formatting the message to the
actual call to console methods, where browsers then have richer formatting in
their devtools than when pino will format the message to a string first.
pino.info('hello %s', 'world') // creates and logs {level: 30, time: <ts>}, 'hello %s', 'world'`formatters` (Object)
An object containing functions for formatting the shape of the log lines. When provided, it enables the logger to produce a pino-like log object with customized formatting. Currently, it supports formatting for the level object only.
`level`
Changes the shape of the log level. The default shape is { level: number }.
The function takes two arguments, the label of the level (e.g. 'info')
and the numeric value (e.g. 30).
const formatters = {
level (label, number) {
return { level: number }
}
}`reportCaller` (Boolean)
Attempts to capture and include the originating callsite (file:line:column) for each log call in the browser logger.
- When used together with
asObject(or whenformattersare provided), the callsite is added as acallerstring property on the emitted log object. - In the default mode (nonโobject), the callsite string is appended as the last argument passed to the corresponding
consolemethod. This makes the location visible in the console output even though the consoleโs clickable header still points to Pino internals.
// Object mode: adds `caller` to the log object
const pino = require('pino')({
browser: {
asObject: true,
reportCaller: true
}
})
pino.info('hello')
// -> { level: 30, msg: 'hello', time: <ts>, caller: '/path/to/file.js:10:15' }
// Default mode: appends the caller string as the last console argument
const pino2 = require('pino')({
browser: {
reportCaller: true
}
})
pino2.info('hello')
// -> console receives: 'hello', '/path/to/file.js:10:15'Notes:
- This is a bestโeffort feature that parses the JavaScript Error stack. Stack formats vary across engines.
- The clickable link shown by devtools for a console message is determined by where
console.*is invoked and cannot be changed by libraries;reportCallersurfaces the user callsite alongside the log message.
`write` (Function | Object)
Instead of passing log messages to console.log they can be passed to
a supplied function.
If write is set to a single function, all logging objects are passed
to this function.
const pino = require('pino')({
browser: {
write: (o) => {
// do something with o
}
}
})If write is an object, it can have methods that correspond to the
levels. When a message is logged at a given level, the corresponding
method is called. If a method isn't present, the logging falls back
to using the console.
const pino = require('pino')({
browser: {
write: {
info: function (o) {
//process info log object
},
error: function (o) {
//process error log object
}
}
}
})`serialize`: (Boolean | Array)
The serializers provided to pino are ignored by default in the browser, including
the standard serializers provided with Pino. Since the default destination for log
messages is the console, values such as Error objects are enhanced for inspection,
which they otherwise wouldn't be if the Error serializer was enabled.
We can turn all serializers on,
const pino = require('pino')({
browser: {
serialize: true
}
})Or we can selectively enable them via an array:
const pino = require('pino')({
serializers: {
custom: myCustomSerializer,
another: anotherSerializer
},
browser: {
serialize: ['custom']
}
})
// following will apply myCustomSerializer to the custom property,
// but will not apply anotherSerializer to another key
pino.info({custom: 'a', another: 'b'})When serialize is true the standard error serializer is also enabled (see https://github.com/pinojs/pino/blob/master/docs/api.md#stdSerializers).
This is a global serializer, which will apply to any Error objects passed to the logger methods.
If serialize is an array the standard error serializer is also automatically enabled, it can
be explicitly disabled by including a string in the serialize array: !stdSerializers.err, like so:
const pino = require('pino')({
serializers: {
custom: myCustomSerializer,
another: anotherSerializer
},
browser: {
serialize: ['!stdSerializers.err', 'custom'] //will not serialize Errors, will serialize `custom` keys
}
})The serialize array also applies to any child logger serializers (see https://github.com/pinojs/pino/blob/master/docs/api.md#discussion-2
for how to set child-bound serializers).
Unlike server pino the serializers apply to every object passed to the logger method,
if the asObject option is true, this results in the serializers applying to the
first object (as in server pino).
For more info on serializers see https://github.com/pinojs/pino/blob/master/docs/api.md#mergingobject.
`transmit` (Object)
An object with send and level properties.
The transmit.level property specifies the minimum level (inclusive) of when the send function
should be called, if not supplied the send function be called based on the main logging level
(set via options.level, defaulting to info).
The transmit object must have a send function which will be called after
writing the log message. The send function is passed the level of the log
message and a logEvent object.
The logEvent object is a data structure representing a log message, it represents
the arguments passed to a logger statement, the level
at which they were logged, and the hierarchy of child bindings.
The logEvent format is structured like so:
{
ts = Number,
messages = Array,
bindings = Array,
level: { label = String, value = Number}
}The ts property is a Unix epoch timestamp in milliseconds, the time is taken from the moment the
logger method is called.
The messages array is all arguments passed to logger method, (for instance logger.info('a', 'b', 'c')
would result in messages array ['a', 'b', 'c']).
The bindings array represents each child logger (if any), and the relevant bindings.
For instance, given logger.child({a: 1}).child({b: 2}).info({c: 3}), the bindings array
would hold [{a: 1}, {b: 2}] and the messages array would be [{c: 3}]. The bindings
are ordered according to their position in the child logger hierarchy, with the lowest index
being the top of the hierarchy.
By default, serializers are not applied to log output in the browser, but they will always be
applied to messages and bindings in the logEvent object. This allows us to ensure a consistent
format for all values between server and client.
The level holds the label (for instance info), and the corresponding numerical value
(for instance 30). This could be important in cases where client-side level values and
labels differ from server-side.
The point of the send function is to remotely record log messages:
const pino = require('pino')({
browser: {
transmit: {
level: 'warn',
send: function (level, logEvent) {
if (level === 'warn') {
// maybe send the logEvent to a separate endpoint
// or maybe analyze the messages further before sending
}
// we could also use the `logEvent.level.value` property to determine
// numerical value
if (logEvent.level.value >= 50) { // covers error and fatal
// send the logEvent somewhere
}
}
}
}
})`disabled` (Boolean)
const pino = require('pino')({browser: {disabled: true}})The disabled option will disable logging in browser if set
to true, by default it is set to false.
File: docs/bundling.md
Bundling
Due to its internal architecture based on Worker Threads, it is not possible to bundle Pino without generating additional files.
In particular, a bundler must ensure that the following files are also bundled separately:
lib/worker.jsfrom thethread-streamdependencyfile.jslib/worker.js- Any transport used by the user (like
pino-pretty)
Once the files above have been generated, the bundler must also add information about the files above by injecting a code that sets __bundlerPathsOverrides in the globalThis object.
The variable is an object whose keys are an identifier for the files and the values are the paths of files relative to the currently bundle files.
Example:
// Inject this using your bundle plugin
globalThis.__bundlerPathsOverrides = {
'thread-stream-worker': pinoWebpackAbsolutePath('./thread-stream-worker.js')
'pino/file': pinoWebpackAbsolutePath('./pino-file.js'),
'pino-worker': pinoWebpackAbsolutePath('./pino-worker.js'),
'pino-pretty': pinoWebpackAbsolutePath('./pino-pretty.js'),
};Note that pino/file, pino-worker and thread-stream-worker are required identifiers. Other identifiers are possible based on the user configuration.
Webpack Plugin
If you are a Webpack user, you can achieve this with pino-webpack-plugin without manual configuration of __bundlerPathsOverrides; however, you still need to configure it manually if you are using other bundlers.
Esbuild Plugin
esbuild-plugin-pino is the esbuild plugin to generate extra pino files for bundling.
Bun Plugin
bun-plugin-pino is the Bun plugin to generate extra pino files for bundling.
File: docs/child-loggers.md
Child loggers
Let's assume we want to have "module":"foo" added to every log within a
module foo.js.
To accomplish this, simply use a child logger:
'use strict'
// imports a pino logger instance of `require('pino')()`
const parentLogger = require('./lib/logger')
const log = parentLogger.child({module: 'foo'})
function doSomething () {
log.info('doSomething invoked')
}
module.exports = {
doSomething
}Child logger bindings are top-level fields on every log line emitted by the
child logger. Do not create child loggers from externally supplied objects or
user-controlled binding names, because those keys can conflict with Pino fields
such as level, time, or msg, as well as application or security fields.
If untrusted data must be associated with logs, place it under an
application-controlled key:
// Avoid: user-controlled keys become top-level fields on every child log line
const child = logger.child(untrustedObject)
// Prefer: user-controlled keys are contained under a trusted namespace
const child = logger.child({ untrusted: untrustedObject })As a matter of good security hygiene, prefer not to log untrusted data at all
unless it is necessary. When it is necessary, sanitize and redact it first.
See also logger.child() in the API documentation andmergingObject.
Cost of child logging
Child logger creation is fast:
benchBunyanCreation*10000: 564.514ms
benchBoleCreation*10000: 283.276ms
benchPinoCreation*10000: 258.745ms
benchPinoExtremeCreation*10000: 150.506msLogging through a child logger has little performance penalty:
benchBunyanChild*10000: 556.275ms
benchBoleChild*10000: 288.124ms
benchPinoChild*10000: 231.695ms
benchPinoExtremeChild*10000: 122.117msLogging via the child logger of a child logger also has negligible overhead:
benchBunyanChildChild*10000: 559.082ms
benchPinoChildChild*10000: 229.264ms
benchPinoExtremeChildChild*10000: 127.753msDuplicate keys caveat
Naming conflicts can arise between child loggers and
children of child loggers.
This isn't as bad as it sounds, even if the same keys between
parent and child loggers are used, Pino resolves the conflict in the sanest way.
For example, consider the following:
const pino = require('pino')
pino(pino.destination('./my-log'))
.child({a: 'property'})
.child({a: 'prop'})
.info('howdy')$ cat my-log
{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":1459534114473,"a":"property","a":"prop"}Notice how there are two keys named a in the JSON output. The sub-child's properties
appear after the parent child properties.
At some point, the logs will most likely be processed (for instance with a transport),
and this generally involves parsing. JSON.parse will return an object where the conflicting
namespace holds the final value assigned to it:
$ cat my-log | node -e "process.stdin.once('data', (line) => console.log(JSON.stringify(JSON.parse(line))))"
{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":"2016-04-01T18:08:34.473Z","a":"prop"}Ultimately the conflict is resolved by taking the last value, which aligns with Bunyan's child logging
behavior.
There may be cases where this edge case becomes problematic if a JSON parser with alternative behavior
is used to process the logs. It's recommended to be conscious of namespace conflicts with child loggers,
in light of an expected log processing approach. This is especially important for untrusted data: do not
allow externally supplied objects or user-controlled key names to become child logger bindings.
One of Pino's performance tricks is to avoid building objects and stringifying
them, so we're building strings instead. This is why duplicate keys between
parents and children will end up in the log output.
File: docs/ecosystem.md
Pino Ecosystem
This is a list of ecosystem modules that integrate with pino.
Modules listed under Core are maintained by the Pino team. Modules
listed under Community are maintained by independent community
members.
Please send a PR to add new modules!
Core
Frameworks
express-pino-logger: use
Pino to log requests within express.koa-pino-logger: use Pino to
log requests within Koa.restify-pino-logger: use
Pino to log requests within restify.rill-pino-logger: use Pino as
the logger for the Rill framework.
Utilities
pino-arborsculpture: change
log levels at runtime.pino-caller: add callsite to the log line.pino-clf: reformat Pino logs into
Common Log Format.pino-console: adapter for the WHATWG Console spec.pino-debug: use Pino to interpretdebuglogs.pino-elasticsearch: send
Pino logs to an Elasticsearch instance.pino-eventhub: send Pino logs
to an Event Hub.pino-filter: filter Pino logs in
the same fashion as thedebugmodule.pino-gelf: reformat Pino logs into
GELF format for Graylog.pino-hapi: use Pino as the logger
for Hapi.pino-http: easily use Pino to log
requests with the corehttpmodule.pino-http-print: reformat Pino
logs into traditional HTTPD style request logs.pino-mongodb: store Pino logs
in a MongoDB database.pino-multi-stream: send
logs to multiple destination streams (slow!).pino-noir: redact sensitive information
in logs.pino-pretty: basic prettifier to
make log lines human-readable.pino-socket: send logs to TCP or UDP
destinations.pino-std-serializers: the
core object serializers used within Pino.pino-syslog: reformat Pino logs
to standard syslog format.pino-tee: pipe Pino logs into files
based upon log levels.pino-test: a set of utilities for
verifying logs generated by the Pino logger.pino-toke: reformat Pino logs
according to a given format string.
Community
@google-cloud/pino-logging-gcp-config: Config helper and formatter to output Google Cloud Platform Structured Logging@newrelic/pino-enricher: a log customization to add New Relic context to use Logs In Context@sentry/node: Sentry SDK with native pinoIntegration for direct Pino instrumentation and error tracking.cloud-pine: transport that provides abstraction and compatibility with@google-cloud/logging.cls-proxify: integration of pino and CLS. Useful for creating dynamically configured child loggers (e.g. with added trace ID) for each request.crawlee-pino: use Pino to log within Crawleeeslint-plugin-pino: linting rules for pino usage, primarly for preventing missing context in logs due to incorrect argument order.pino-colada: cute ndjson formatter for pino.pino-dev: simple prettifier for pino with built-in support for common ecosystem packages.pino-fluentd: send Pino logs to Elasticsearch,
MongoDB, and many others via Fluentd.pino-lambda: log transport for cloudwatch support inside aws-lambdapino-pretty-min: a minimal
prettifier inspired by the logrus logger.pino-rotating-file: a hapi-pino log transport for splitting logs into separate, automatically rotating files.pino-tiny: a tiny (and extensible?) little log formatter for pino.
File: docs/help.md
Help
- Log rotation
- Reopening log files
- Saving to multiple files
- Log filtering
- Transports and systemd
- Log to different streams
- Duplicate keys
- Log levels as labels instead of numbers
- Pino with
debug - Unicode and Windows terminal
- Mapping Pino Log Levels to Google Cloud Logging (Stackdriver) Severity Levels
- Using Grafana Loki to evaluate pino logs in a kubernetes cluster
- Avoid Message Conflict
- Best performance for logging to
stdout - Testing
Log rotation
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.logWe 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.
Reopening log files
In cases where a log rotation tool doesn't offer copy-truncate capabilities,
or where using them is deemed inappropriate, pino.destination
can 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('node:fs')
fs.writeFileSync('/var/run/myapp.pid', process.pid)
const dest = pino.destination('/log/file')
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 basiclogrotate 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
}Saving to multiple files
See pino.multistream.
Log Filtering
The Pino philosophy advocates common, preexisting, system utilities.
Some recommendations in line with this philosophy are:
- Use
grep:sh$ # View all "INFO" level logs $ node app.js | grep '"level":30' - Use
jq:sh$ # View all "ERROR" level logs $ node app.js | jq 'select(.level == 50)'
Transports and systemd
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'Log to different streams
Pino's default log destination is the singular destination of stdout. While
not recommended for performance reasons, multiple destinations can be targeted
by using pino.multistream.
In this example, we use stderr for error level logs and stdout as default
for all other levels (e.g. debug, info, and warn).
const pino = require('pino')
var streams = [
{level: 'debug', stream: process.stdout},
{level: 'error', stream: process.stderr},
{level: 'fatal', stream: process.stderr}
]
const logger = pino({
name: 'my-app',
level: 'debug', // must be the lowest level of all streams
}, pino.multistream(streams))How Pino handles duplicate keys
Duplicate keys are possible when a logged object contains a top-level key that
collides with a Pino field or a key in the child logger's bindings, or when child
logger bindings collide with parent bindings.
For untrusted data, avoid passing externally supplied objects directly to logging
methods or child logger bindings. User-controlled top-level keys can conflict
with Pino fields such as level, time, or msg, as well as application or
security fields. If untrusted data must be logged, place it under an
application-controlled key, such as logger.info({ untrusted }), and prefer not
to log untrusted data at all unless necessary.
See the child logger duplicate keys caveat
and logger.child() bindings for more information.
Log levels as labels instead of numbers
Pino log lines are meant to be parsable. Thus, Pino's default mode of operation
is to print the level value instead of the string name.
However, you can use the formatters option
with a level function to print the string name instead of the level value :
const pino = require('pino')
const log = pino({
formatters: {
level: (label) => {
return {
level: label
}
}
}
})
log.info('message')
// {"level":"info","time":1661632832200,"pid":18188,"hostname":"foo","msg":"message"}Although it works, 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 ispino-text-level-transport. - Use a prettifier like
pino-prettyto make
the logs human friendly.
Pino with `debug`
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 asynchronous mode)
performance improvement - even 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 theDEBUG environment variable:
$ npm i pino-debug
$ DEBUG=* node -r pino-debug app.jspino-debug also offers fine-grain control to map specific debug
namespaces to pino log levels. See pino-debug
for more.
Unicode and Windows terminal
Pino uses sonic-boom to speed
up logging. Internally, it uses fs.write to write log lines directly to a file
descriptor. On Windows, Unicode output is not handled properly in the
terminal (both cmd.exe and PowerShell), and as such the output could
be visualized incorrectly if the log lines include utf8 characters. It
is possible to configure the terminal to visualize those characters
correctly with the use of chcp by
executing in the terminal chcp 65001. This is a known limitation of
Node.js.
Mapping Pino Log Levels to Google Cloud Logging (Stackdriver) Severity Levels
Google Cloud Logging uses severity levels instead of log levels. As a result, all logs may show as INFO
level logs while completely ignoring the level set in the pino log. Google Cloud Logging also prefers that
log data is present inside a message key instead of the default msg key that Pino uses. Use a technique
similar to the one below to retain log levels in Google Cloud Logging
const pino = require('pino')
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
const PinoLevelToSeverityLookup = {
trace: 'DEBUG',
debug: 'DEBUG',
info: 'INFO',
warn: 'WARNING',
error: 'ERROR',
fatal: 'CRITICAL',
};
const defaultPinoConf = {
messageKey: 'message',
formatters: {
level(label, number) {
return {
severity: PinoLevelToSeverityLookup[label] || PinoLevelToSeverityLookup['info'],
level: number,
}
}
},
}
module.exports = function createLogger(options) {
return pino(Object.assign({}, options, defaultPinoConf))
}A library that configures Pino for
Google Cloud Structured Logging
is available at:
@google-cloud/pino-logging-gcp-config
This library has the following features:
- Converts Pino log levels to Google Cloud Logging log levels, as above
- Uses
messageinstead ofmsgfor the message key, as above - Adds a millisecond-granularity timestamp in the
structure
recognised by Google Cloud Logging eg:"timestamp":{"seconds":1445470140,"nanos":123000000} - Adds a sequential
insertId
to ensure log messages with identical timestamps are ordered correctly. - Logs including an
Errorobject have thestack_trace
property set so that the error is forwarded to Google Cloud Error Reporting. - Includes a
ServiceContext
object in the logs for Google Cloud Error Reporting, auto detected from the
environment if not specified - Maps the OpenTelemetry properties
span_id,trace_id, andtrace_flags
to the equivalent Google Cloud Logging fields.
Using Grafana Loki to evaluate pino logs in a kubernetes cluster
To get pino logs into Grafana Loki there are two options:
- Push: Use pino-loki to send logs directly to Loki.
- Pull: Configure Grafana Promtail to read and properly parse the logs before sending them to Loki.
Similar to Google Cloud logging, this involves remapping the log levels. See this article for details.
Avoid Message Conflict
As described in the message documentation, when a log
is written like log.info({ msg: 'a message' }, 'another message') then the
final output JSON will have "msg":"another message" and the 'a message'
string will be lost. To overcome this, the logMethod hook
can be used:
'use strict'
const log = require('pino')({
level: 'debug',
hooks: {
logMethod (inputArgs, method) {
if (inputArgs.length === 2 && inputArgs[0].msg) {
inputArgs[0].originalMsg = inputArgs[0].msg
}
return method.apply(this, inputArgs)
}
}
})
log.info('no original message')
log.info({ msg: 'mapped to originalMsg' }, 'a message')
// {"level":30,"time":1596313323106,"pid":63739,"hostname":"foo","msg":"no original message"}
// {"level":30,"time":1596313323107,"pid":63739,"hostname":"foo","msg":"a message","originalMsg":"mapped to originalMsg"}Best performance for logging to `stdout`
The best performance for logging directly to stdout is usually achieved by using the
default configuration:
const log = require('pino')();You should only have to configure custom transports or other settings
if you have broader logging requirements.
Testing
See pino-test.
File: docs/lts.md
Long Term Support
Pino's Long Term Support (LTS) is provided according to the schedule laid
out in this document:
Major releases, "X" release of semantic versioning X.Y.Z release
versions, are supported for a minimum period of six months from their release
date. The release date of any specific version can be found at
https://github.com/pinojs/pino/releases.Major releases will receive security updates for an additional six months
from the release of the next major release. After this period
we will still review and release security fixes as long as they are
provided by the community and they do not violate other constraints,
e.g. minimum supported Node.js version.Major releases will be tested and verified against all Node.js
release lines that are supported by the
Node.js LTS policy within the
LTS period of that given Pino release line. This implies that only
the latest Node.js release of a given line is supported.
A "month" is defined as 30 consecutive days.
Security Releases and Semver
As a consequence of providing long-term support for major releases, there
are occasions where we need to release breaking changes as a minor
version release. Such changes will always be noted in the
release notes.To avoid automatically receiving breaking security updates it is possible to use
the tilde (~) range qualifier. For example, to get patches for the 6.1
release, and avoid automatically updating to the 6.1 release, specify
the dependency as"pino": "~6.1.x". This will leave your application vulnerable,
so please use with caution.
Supported Node.js versions
Pino follows the Node.js release schedule. A Pino release line
is tested against, and supports, every Node.js release line that is in its
current, active LTS, or maintenance LTS phase when the Pino line is
released, as described above.
Support for a Node.js version is only dropped in a new Pino major release. A
published Pino major will never drop a Node.js release line that it originally
shipped with, so upgrading within a major line (for example, from 9.1.0 to9.4.0) will never remove support for a Node.js version you already run on.
Once a Node.js release line reaches its end-of-life, Pino supports it on a
best-effort basis, with the following caveats:
- End-of-life Node.js versions are not exercised in continuous integration.
- Some fixes and features cannot be backported to the Pino lines that still
support them. - Dependencies cannot be kept up to date.
To keep running on an end-of-life Node.js version, pin your dependency to the
last Pino major line that supported it. The Schedule below maps
each Pino release line to the Node.js versions it supports; for example, to stay
on Node.js 14, install the latest 8.x release with "pino": "8.x".
Schedule
| Version | Release Date | End Of LTS Date | Node.js |
|---|---|---|---|
| 9.x | 2024-04-26 | TBD | 18, 20, 22 |
| 8.x | 2022-06-01 | 2024-10-26 | 14, 16, 18, 20 |
| 7.x | 2021-10-14 | 2023-06-01 | 12, 14, 16 |
| 6.x | 2020-03-07 | 2022-04-14 | 10, 12, 14, 16 |
CI tested operating systems
Pino uses GitHub Actions for CI testing, please refer to
GitHub's documentation regarding workflow runners
for further details on what the latest virtual environment is in relation to
the YAML workflow labels below:
| OS | YAML Workflow Label | Node.js |
|---|---|---|
| Linux | ubuntu-latest |
18, 20, 22 |
| Windows | windows-latest |
18, 20, 22 |
| MacOS | macos-latest |
18, 20, 22 |