diff --git a/docs/api.md b/docs/api.md index 7e7afae1b..6e15ba350 100644 --- a/docs/api.md +++ b/docs/api.md @@ -96,6 +96,28 @@ logger.foo('hi') logger.info('hello') // Will throw an error saying info in not found in logger object ``` +#### `mixin` (Function): + +Default: `undefined` + +If provided, the `mixin` function is called each time one of the active +logging methods is called. The function must synchronously return an +object. The properties of the returned object will be added to the +logged JSON. + +```js +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","v":1} +logger.info('world') +// {"level":30,"time":1573664685469,"pid":78742,"hostname":"x","line":2,"msg":"world","v":1} +``` + #### `redact` (Array | Object): Default: `undefined` diff --git a/lib/proto.js b/lib/proto.js index 09cc0d9dd..a063b21d6 100644 --- a/lib/proto.js +++ b/lib/proto.js @@ -11,6 +11,7 @@ const { setLevelSym, getLevelSym, chindingsSym, + mixinSym, asJsonSym, messageKeySym, writeSym, @@ -115,14 +116,15 @@ function bindings () { function write (_obj, msg, num) { const t = this[timeSym]() const messageKey = this[messageKeySym] + const mixin = this[mixinSym] const objError = _obj instanceof Error var obj if (_obj === undefined || _obj === null) { - obj = {} + obj = mixin ? mixin() : {} obj[messageKey] = msg } else { - obj = Object.assign({}, _obj) + obj = Object.assign(mixin ? mixin() : {}, _obj) if (msg) { obj[messageKey] = msg } else if (objError) { diff --git a/lib/symbols.js b/lib/symbols.js index 38c3609e6..22446a80b 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -6,6 +6,7 @@ const levelValSym = Symbol('pino.levelVal') const useLevelLabelsSym = Symbol('pino.useLevelLabels') const changeLevelNameSym = Symbol('pino.changeLevelName') const useOnlyCustomLevelsSym = Symbol('pino.useOnlyCustomLevels') +const mixinSym = Symbol('pino.mixin') const lsCacheSym = Symbol('pino.lsCache') const chindingsSym = Symbol('pino.chindings') @@ -37,6 +38,7 @@ module.exports = { getLevelSym, levelValSym, useLevelLabelsSym, + mixinSym, lsCacheSym, chindingsSym, parsedChindingsSym, diff --git a/pino.js b/pino.js index 14ad86c8c..9a6aeb32f 100644 --- a/pino.js +++ b/pino.js @@ -29,6 +29,7 @@ const { messageKeySym, useLevelLabelsSym, changeLevelNameSym, + mixinSym, useOnlyCustomLevelsSym } = symbols const { epochTime, nullTime } = time @@ -71,6 +72,7 @@ function pino (...args) { customLevels, useLevelLabels, changeLevelName, + mixin, useOnlyCustomLevels } = opts @@ -92,6 +94,7 @@ function pino (...args) { const timeSliceIndex = time().indexOf(':') + 1 if (useOnlyCustomLevels && !customLevels) throw Error('customLevels is required if useOnlyCustomLevels is set true') + if (mixin && typeof mixin !== 'function') throw Error(`Unknown mixin type "${typeof mixin}" - expected "function"`) assertDefaultLevelFound(level, customLevels, useOnlyCustomLevels) const levels = mappings(customLevels, useOnlyCustomLevels) @@ -110,6 +113,7 @@ function pino (...args) { [formatOptsSym]: formatOpts, [messageKeySym]: messageKey, [serializersSym]: serializers, + [mixinSym]: mixin, [chindingsSym]: chindings } Object.setPrototypeOf(instance, proto) diff --git a/test/mixin.test.js b/test/mixin.test.js new file mode 100644 index 000000000..49bcfa94f --- /dev/null +++ b/test/mixin.test.js @@ -0,0 +1,106 @@ +'use strict' + +const os = require('os') +const { test } = require('tap') +const { sink, once } = require('./helper') +const pino = require('../') + +const { pid } = process +const hostname = os.hostname() +const level = 50 +const name = 'error' + +test('mixin object is included', async ({ ok, same }) => { + let n = 0 + const stream = sink() + const instance = pino({ + mixin () { + return { hello: ++n } + } + }, stream) + instance.level = name + instance[name]('test') + const result = await once(stream, 'data') + ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()') + delete result.time + same(result, { + pid, + hostname, + level, + msg: 'test', + hello: 1, + v: 1 + }) +}) + +test('mixin object is new every time', async ({ plan, ok, same }) => { + plan(6) + + let n = 0 + const stream = sink() + const instance = pino({ + mixin () { + return { hello: n } + } + }, stream) + instance.level = name + + while (++n < 4) { + const msg = `test #${n}` + stream.pause() + instance[name](msg) + stream.resume() + const result = await once(stream, 'data') + ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()') + delete result.time + same(result, { + pid, + hostname, + level, + msg, + hello: n, + v: 1 + }) + } +}) + +test('mixin object is not called if below log level', async ({ ok }) => { + const stream = sink() + const instance = pino({ + mixin () { + ok(false, 'should not call mixin function') + } + }, stream) + instance.level = 'error' + instance.info('test') +}) + +test('mixin object + logged object', async ({ ok, same }) => { + let n = 0 + const stream = sink() + const instance = pino({ + mixin () { + return { hello: ++n } + } + }, stream) + instance.level = name + instance[name]({ foo: 42 }) + const result = await once(stream, 'data') + ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()') + delete result.time + same(result, { + pid, + hostname, + level, + foo: 42, + hello: 1, + v: 1 + }) +}) + +test('mixin not a function', async ({ throws }) => { + const stream = sink() + throws(function () { + pino({ mixin: 'not a function' }, stream) + }) +})