Repository: pinojs/pino
Pull request #759 (2020-01-07 00:52:11)

Support new nestedKey config to avoid property name collisions with pino standard properties when logging objects
Some people log objects a lot.  Those objects, in general, may have properties that conflict with pino's standard logging fields (`level`, `v`, `pid`, etc).  Also, when searching logs for logged objects, it's nice to always have them in a consistent location (that is, property of the root log record object).

This PR adds a new config option ~~`objectKey`~~ `nestedKey` that causes log records to include a key whose name is the value of the ~~`objectKey`~~ `nestedKey` config, and whose value is the logged object (if it is an object and not a string).  It allows folks using things like Kibana, StackDriver, MongoDB, etc, to consistently know the path in their log records to search for, and guarantees that no logged object's properties will conflict with pino's standard properties.


Patch:
diff --git a/docs/api.md b/docs/api.md
index 6e15ba3..f4cfd0b 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -201,6 +201,32 @@ Default: `'msg'`
 
 The string key for the 'message' in the JSON object.
 
+<a id=opt-nestedkey></a>
+#### `nestedKey` (String)
+
+Default: `null`
+
+If there's a chance that objects being logged have properties that conflict with those from pino itself (`level`, `timestamp`, `v`, `pid`, etc)
+and duplicate keys in your log records are undesirable, pino can be configured with a `nestedKey` option that causes any `object`s 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,
+```js
+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"},"v":1}
+```
+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.
+
 <a id=prettyPrint></a>
 #### `prettyPrint` (Boolean | Object)
 
diff --git a/lib/symbols.js b/lib/symbols.js
index 22446a8..9e27d7c 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -24,6 +24,7 @@ const stringifiersSym = Symbol('pino.stringifiers')
 const endSym = Symbol('pino.end')
 const formatOptsSym = Symbol('pino.formatOpts')
 const messageKeySym = Symbol('pino.messageKey')
+const nestedKeySym = Symbol('pino.nestedKey')
 
 const wildcardFirstSym = Symbol('pino.wildcardFirst')
 
@@ -54,6 +55,7 @@ module.exports = {
   endSym,
   formatOptsSym,
   messageKeySym,
+  nestedKeySym,
   wildcardFirstSym,
   changeLevelNameSym,
   wildcardGsym,
diff --git a/lib/tools.js b/lib/tools.js
index 586c84d..c5dd17f 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -20,7 +20,8 @@ const {
   needsMetadataGsym,
   wildcardGsym,
   redactFmtSym,
-  streamSym
+  streamSym,
+  nestedKeySym
 } = require('./symbols')
 
 function noop () {}
@@ -33,6 +34,7 @@ function genLog (z) {
       } else if (typeof o.setHeader === 'function') {
         o = mapHttpResponse(o)
       }
+      if (this[nestedKeySym]) o = { [this[nestedKeySym]]: o }
       this[writeSym](o, format(null, n, this[formatOptsSym]), z)
     } else this[writeSym](null, format(o, n, this[formatOptsSym]), z)
   }
diff --git a/pino.js b/pino.js
index 9a6aeb3..70c94bd 100644
--- a/pino.js
+++ b/pino.js
@@ -27,6 +27,7 @@ const {
   endSym,
   formatOptsSym,
   messageKeySym,
+  nestedKeySym,
   useLevelLabelsSym,
   changeLevelNameSym,
   mixinSym,
@@ -40,6 +41,7 @@ const defaultOptions = {
   level: 'info',
   useLevelLabels: false,
   messageKey: 'msg',
+  nestedKey: null,
   enabled: true,
   prettyPrint: false,
   base: { pid, hostname },
@@ -66,6 +68,7 @@ function pino (...args) {
     serializers,
     timestamp,
     messageKey,
+    nestedKey,
     base,
     name,
     level,
@@ -112,6 +115,7 @@ function pino (...args) {
     [endSym]: end,
     [formatOptsSym]: formatOpts,
     [messageKeySym]: messageKey,
+    [nestedKeySym]: nestedKey,
     [serializersSym]: serializers,
     [mixinSym]: mixin,
     [chindingsSym]: chindings


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index fbb1c96..eeb91b6 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -286,6 +286,26 @@ test('set the messageKey', async ({ is, same }) => {
   })
 })
 
+test('set the nestedKey', async ({ is, same }) => {
+  const stream = sink()
+  const object = { hello: 'world' }
+  const nestedKey = 'stuff'
+  const instance = pino({
+    nestedKey
+  }, stream)
+  instance.info(object)
+  const result = await once(stream, 'data')
+  is(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
+  delete result.time
+  same(result, {
+    pid: pid,
+    hostname: hostname,
+    level: 30,
+    stuff: object,
+    v: 1
+  })
+})
+
 test('set undefined properties', async ({ is, same }) => {
   const stream = sink()
   const instance = pino(stream)