diff --git a/docs/api.md b/docs/api.md
index ac6c45123..3efe63782 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -107,6 +107,7 @@ Default: `100`
Option to limit stringification of properties/elements when logging a specific object/array with circular references.
+
#### `mixin` (Function):
Default: `undefined`
@@ -130,7 +131,8 @@ logger.info('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:
+In the following example, passing `mergingObject` argument to the first `info` call will mutate the global `mixin` object by default:
+(* See [`mixinMergeStrategy` option](#opt-mixin-merge-strategy)):
```js
const mixin = {
appName: 'My app'
@@ -154,6 +156,67 @@ logger.info('Message 2')
If the `mixin` feature is being used merely to add static metadata to each log message,
then a [child logger ⇗](/docs/child-loggers.md) should be used instead.
+
+#### `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](#opt-mixin) or an empty object.
+The function must synchronously return an object.
+
+```js
+// 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"}
+```
+
+```js
+// 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"}
+```
+
+```js
+// 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):
diff --git a/lib/proto.js b/lib/proto.js
index 1eca1232c..f51e5d7dc 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -13,6 +13,7 @@ const {
mixinSym,
asJsonSym,
writeSym,
+ mixinMergeStrategySym,
timeSym,
timeSliceIndexSym,
streamSym,
@@ -158,9 +159,22 @@ function setBindings (newBindings) {
delete this[parsedChindingsSym]
}
+/**
+ * Default strategy for creating `mergeObject` from arguments and the result from `mixin()`.
+ * Fields from `mergeObject` have higher priority in this strategy.
+ *
+ * @param {Object} mergeObject The object a user has supplied to the logging function.
+ * @param {Object} mixinObject The result of the `mixin` method.
+ * @return {Object}
+ */
+function defaultMixinMergeStrategy (mergeObject, mixinObject) {
+ return Object.assign(mixinObject, mergeObject)
+}
+
function write (_obj, msg, num) {
const t = this[timeSym]()
const mixin = this[mixinSym]
+ const mixinMergeStrategy = this[mixinMergeStrategySym] || defaultMixinMergeStrategy
let obj
if (_obj === undefined || _obj === null) {
@@ -178,7 +192,7 @@ function write (_obj, msg, num) {
}
if (mixin) {
- obj = Object.assign(mixin(obj), obj)
+ obj = mixinMergeStrategy(obj, mixin(obj))
}
const s = this[asJsonSym](obj, msg, num, t)
diff --git a/lib/symbols.js b/lib/symbols.js
index 2845cbac0..75195ff07 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -26,6 +26,7 @@ const formatOptsSym = Symbol('pino.formatOpts')
const messageKeySym = Symbol('pino.messageKey')
const nestedKeySym = Symbol('pino.nestedKey')
const nestedKeyStrSym = Symbol('pino.nestedKeyStr')
+const mixinMergeStrategySym = Symbol('pino.mixinMergeStrategy')
const wildcardFirstSym = Symbol('pino.wildcardFirst')
@@ -64,5 +65,6 @@ module.exports = {
useOnlyCustomLevelsSym,
formattersSym,
hooksSym,
- nestedKeyStrSym
+ nestedKeyStrSym,
+ mixinMergeStrategySym
}
diff --git a/pino.d.ts b/pino.d.ts
index 52d7bd480..31efc4f7b 100644
--- a/pino.d.ts
+++ b/pino.d.ts
@@ -31,7 +31,8 @@ import type { WorkerOptions } from "worker_threads";
type ThreadStream = any
type TimeFn = () => string;
-type MixinFn = () => object;
+type MixinFn = (mergeObject: object) => object;
+type MixinMergeStrategyFn = (mergeObject: object, mixinObject: object) => object;
type CustomLevelLogger = Options extends { customLevels: Record } ? Record : Record
@@ -112,7 +113,7 @@ interface LoggerExtras extends EventEmitter {
declare namespace pino {
//// Exported types and interfaces
-
+
interface BaseLogger {
/**
* Set this property to the desired logging level. In order of priority, available levels are:
@@ -130,7 +131,7 @@ declare namespace pino {
* You can pass `'silent'` to disable logging.
*/
level: pino.LevelWithSilent | string;
-
+
/**
* Log at `'fatal'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
@@ -204,14 +205,14 @@ declare namespace pino {
type SerializerFn = (value: any) => any;
type WriteFn = (o: object) => void;
-
+
type LevelChangeEventListener = (
lvl: LevelWithSilent | string,
val: number,
prevLvl: LevelWithSilent | string,
prevVal: number,
) => void;
-
+
type LogDescriptor = Record;
type Logger = BaseLogger & LoggerExtras & CustomLevelLogger;
@@ -336,6 +337,14 @@ declare namespace pino {
*/
mixin?: MixinFn;
+ /**
+ * 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()` or an empty object.
+ * The function must synchronously return an object.
+ */
+ mixinMergeStrategy?: MixinMergeStrategyFn
+
/**
* As an array, the redact option specifies paths that should have their values redacted from any log output.
*
@@ -671,12 +680,12 @@ declare namespace pino {
readonly formattersSym: unique symbol;
readonly hooksSym: unique symbol;
};
-
+
/**
* Exposes the Pino package version. Also available on the logger instance.
*/
export const version: string;
-
+
/**
* Provides functions for generating the timestamp property in the log output. You can set the `timestamp` option during
* initialization to one of these functions to adjust the output format. Alternatively, you can specify your own time function.
@@ -701,7 +710,7 @@ declare namespace pino {
*/
isoTime: TimeFn;
};
-
+
//// Exported functions
/**
@@ -755,7 +764,7 @@ declare function pino(options
* @returns a new logger instance.
*/
declare function pino(options: Options, stream: DestinationStream): Logger;
-
+
// Pass through all the top-level exports, allows `import {version} from "pino"`
// Constants and functions
diff --git a/pino.js b/pino.js
index 315e8d46f..2afe6d267 100644
--- a/pino.js
+++ b/pino.js
@@ -39,7 +39,8 @@ const {
useOnlyCustomLevelsSym,
formattersSym,
hooksSym,
- nestedKeyStrSym
+ nestedKeyStrSym,
+ mixinMergeStrategySym
} = symbols
const { epochTime, nullTime } = time
const { pid } = process
@@ -94,6 +95,7 @@ function pino (...args) {
level,
customLevels,
mixin,
+ mixinMergeStrategy,
useOnlyCustomLevels,
formatters,
hooks,
@@ -166,6 +168,7 @@ function pino (...args) {
[nestedKeyStrSym]: nestedKey ? `,${JSON.stringify(nestedKey)}:{` : '',
[serializersSym]: serializers,
[mixinSym]: mixin,
+ [mixinMergeStrategySym]: mixinMergeStrategy,
[chindingsSym]: chindings,
[formattersSym]: allFormatters,
[hooksSym]: hooks,
diff --git a/test/mixin-merge-strategy.test.js b/test/mixin-merge-strategy.test.js
new file mode 100644
index 000000000..3c6dfe45d
--- /dev/null
+++ b/test/mixin-merge-strategy.test.js
@@ -0,0 +1,55 @@
+'use strict'
+
+const { test } = require('tap')
+const { sink, once } = require('./helper')
+const pino = require('../')
+
+const level = 50
+const name = 'error'
+
+test('default merge strategy', async ({ ok, same }) => {
+ const stream = sink()
+ const instance = pino({
+ base: {},
+ mixin () {
+ return { tag: 'k8s' }
+ }
+ }, stream)
+ instance.level = name
+ instance[name]({
+ tag: 'local'
+ }, '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, {
+ level,
+ msg: 'test',
+ tag: 'local'
+ })
+})
+
+test('custom merge strategy with mixin priority', async ({ ok, same }) => {
+ const stream = sink()
+ const instance = pino({
+ base: {},
+ mixin () {
+ return { tag: 'k8s' }
+ },
+ mixinMergeStrategy (mergeObject, mixinObject) {
+ return Object.assign(mergeObject, mixinObject)
+ }
+ }, stream)
+ instance.level = name
+ instance[name]({
+ tag: 'local'
+ }, '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, {
+ level,
+ msg: 'test',
+ tag: 'k8s'
+ })
+})