Repository: pinojs/pino
Pull request #1328 (2022-02-10 09:12:24)

Add option `mixinMergeStrategy`
Added new option `mixinMergeStrategy` for define custom strategy merge `mergeObject` from log method argument and `mixin`


Example:
```js
// Custom muttable 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"}
```

mcollina: Why is this needed?
alexpts: Everything is fine?
alexpts: @jsumners I changed docs and add jsdoc

Patch:
diff --git a/docs/api.md b/docs/api.md
index 3f30471..40b2887 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -94,6 +94,7 @@ logger.foo('hi')
 logger.info('hello') // Will throw an error saying info in not found in logger object
 ```
 
+<a id="opt-mixin"></a>
 #### `mixin` (Function):
 
 Default: `undefined`
@@ -117,7 +118,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'
@@ -141,6 +143,69 @@ 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.
 
+
+<a id="opt-mixin-merge-strategy"></a>
+#### `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"}
+```
+
+
 <a id="opt-redact"></a>
 #### `redact` (Array | Object):
 
diff --git a/lib/proto.js b/lib/proto.js
index 7a854e3..695fbab 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -16,6 +16,7 @@ const {
   mixinSym,
   asJsonSym,
   writeSym,
+  mixinMergeStrategySym,
   timeSym,
   timeSliceIndexSym,
   streamSym,
@@ -177,16 +178,29 @@ 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
   const objError = _obj instanceof Error
   let obj
 
   if (_obj === undefined || _obj === null) {
     obj = mixin ? mixin({}) : {}
   } else {
-    obj = Object.assign(mixin ? mixin(_obj) : {}, _obj)
+    obj = mixinMergeStrategy(_obj, mixin ? mixin(_obj) : {})
     if (!msg && objError) {
       msg = _obj.message
     }
diff --git a/lib/symbols.js b/lib/symbols.js
index 880cee2..037111a 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -24,6 +24,7 @@ const endSym = Symbol('pino.end')
 const formatOptsSym = Symbol('pino.formatOpts')
 const messageKeySym = Symbol('pino.messageKey')
 const nestedKeySym = Symbol('pino.nestedKey')
+const mixinMergeStrategySym = Symbol('pino.mixinMergeStrategy')
 
 const wildcardFirstSym = Symbol('pino.wildcardFirst')
 
@@ -60,5 +61,6 @@ module.exports = {
   needsMetadataGsym,
   useOnlyCustomLevelsSym,
   formattersSym,
-  hooksSym
+  hooksSym,
+  mixinMergeStrategySym
 }
diff --git a/pino.js b/pino.js
index 0d0e3e3..76930a9 100644
--- a/pino.js
+++ b/pino.js
@@ -17,6 +17,7 @@ const {
   noop
 } = require('./lib/tools')
 const { version } = require('./lib/meta')
+const { mixinMergeStrategySym } = require('./lib/symbols')
 const {
   chindingsSym,
   redactFmtSym,
@@ -91,6 +92,7 @@ function pino (...args) {
     changeLevelName,
     levelKey,
     mixin,
+    mixinMergeStrategy,
     useOnlyCustomLevels,
     formatters,
     hooks
@@ -173,6 +175,7 @@ function pino (...args) {
     [nestedKeySym]: nestedKey,
     [serializersSym]: serializers,
     [mixinSym]: mixin,
+    [mixinMergeStrategySym]: mixinMergeStrategy,
     [chindingsSym]: chindings,
     [formattersSym]: allFormatters,
     [hooksSym]: hooks,


Test patch:
diff --git a/test/mixin-merge-strategy.test.js b/test/mixin-merge-strategy.test.js
new file mode 100644
index 0000000..3c6dfe4
--- /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'
+  })
+})