Repository: pinojs/hapi-pino
Pull request #42 (2018-01-02 15:38:56)

wrap custom serializers
This wraps custom serializers in the same way as implemented in https://github.com/pinojs/pino-http/pull/43.

This PR replaces https://github.com/pinojs/hapi-pino/pull/34.

Most of the code is copied from the implementation in https://github.com/pinojs/pino-http/pull/43 with small adjustments for hapi specific things.

Tests are updated to test for the existence of `req.raw` and `res.raw` in custom serializers.


Patch:
diff --git a/README.md b/README.md
index 50945dd..944ddf0 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,14 @@ events"](#hapievents) section.
   mappings. The default level tags are exposed via `hapi-pino.levelTags`.
 - `[allTags]` - the logging level to apply to all tags not matched by
   `tags`, defaults to `'info'`.
+- `[serializers]` - an object to overwrite the default serializers. You can but don't have to overwrite all of them. E.g. to redact the authorization header in the logs:
+  ```
+  {
+    req: require('pino-noir')(['req.headers.authorization'])
+    res: ...
+    err: ...
+  }
+  ```
 - `[instance]` - uses a previously created Pino instance as the logger.
   The instance's `stream` and `serializers` take precedence.
 - `[logEvents]` - Takes an array of strings with the events to log. Default is to
@@ -103,17 +111,16 @@ events"](#hapievents) section.
   into Pino's logged attributes at root level. If data is a string, it will be used as
   the value for the `msg` key. Default is `false`, in which case data will be logged under 
   a `data` key.
+    E.g.
+  ```js
+  server.log(['info'], {hello: 'world'})
 
-  E.g.
-```js
-server.log(['info'], {hello: 'world'})
-
-// with mergeHapiLogData: true
-{ level: 30, hello: 'world', ...}
+  // with mergeHapiLogData: true
+  { level: 30, hello: 'world', ...}
 
-// with mergeHapiLogData: false (Default)
-{ level: 30, data: { hello: 'world' }}
-```
+  // with mergeHapiLogData: false (Default)
+  { level: 30, data: { hello: 'world' }}
+  ```
 
 <a name="serverdecorations"></a>
 ### Server Decorations
diff --git a/index.js b/index.js
index b67e829..7245753 100644
--- a/index.js
+++ b/index.js
@@ -14,8 +14,8 @@ const levelTags = {
 
 async function register (server, options) {
   options.serializers = options.serializers || {}
-  options.serializers.req = options.serializers.req || asReqValue
-  options.serializers.res = options.serializers.res || pino.stdSerializers.res
+  options.serializers.req = wrapReqSerializer(options.serializers.req || asReqValue)
+  options.serializers.res = wrapResSerializer(options.serializers.res || asResValue)
   options.serializers.err = options.serializers.err || pino.stdSerializers.err
 
   if (options.logEvents === undefined) {
@@ -171,15 +171,111 @@ async function register (server, options) {
   }
 }
 
+var rawSymbol = Symbol.for('hapi-pino-raw-ref')
+var pinoReqProto = Object.create({}, {
+  id: {
+    enumerable: true,
+    writable: true,
+    value: ''
+  },
+  method: {
+    enumerable: true,
+    writable: true,
+    value: ''
+  },
+  url: {
+    enumerable: true,
+    writable: true,
+    value: ''
+  },
+  headers: {
+    enumerable: true,
+    writable: true,
+    value: {}
+  },
+  remoteAddress: {
+    enumerable: true,
+    writable: true,
+    value: ''
+  },
+  remotePort: {
+    enumerable: true,
+    writable: true,
+    value: ''
+  },
+  raw: {
+    enumerable: false,
+    get: function () {
+      return this[rawSymbol]
+    },
+    set: function (val) {
+      this[rawSymbol] = val
+    }
+  }
+})
+Object.defineProperty(pinoReqProto, rawSymbol, {
+  writable: true,
+  value: {}
+})
+
+function wrapReqSerializer (serializer) {
+  if (serializer === asReqValue) return asReqValue
+  return function wrappedReqSerializer (req) {
+    return serializer(asReqValue(req))
+  }
+}
+
 function asReqValue (req) {
   const raw = req.raw.req
-  return {
-    id: req.info.id,
-    method: raw.method,
-    url: raw.url,
-    headers: raw.headers,
-    remoteAddress: raw.connection.remoteAddress,
-    remotePort: raw.connection.remotePort
+  const _req = Object.create(pinoReqProto)
+  _req.id = req.info.id
+  _req.method = raw.method
+  _req.url = raw.url
+  _req.headers = raw.headers
+  _req.remoteAddress = raw.connection && raw.connection.remoteAddress
+  _req.remotePort = raw.connection && raw.connection.remotePort
+  _req.raw = req.raw
+  return _req
+}
+
+var pinoResProto = Object.create({}, {
+  statusCode: {
+    enumerable: true,
+    writable: true,
+    value: 0
+  },
+  header: {
+    enumerable: true,
+    writable: true,
+    value: ''
+  },
+  raw: {
+    enumerable: false,
+    get: function () {
+      return this[rawSymbol]
+    },
+    set: function (val) {
+      this[rawSymbol] = val
+    }
+  }
+})
+Object.defineProperty(pinoResProto, rawSymbol, {
+  writable: true,
+  value: {}
+})
+
+function asResValue (res) {
+  const _res = Object.create(pinoResProto)
+  _res.statusCode = res.statusCode
+  _res.header = res._header
+  _res.raw = res
+  return _res
+}
+
+function wrapResSerializer (serializer) {
+  if (serializer === asResValue) return asResValue
+  return function wrappedResSerializer (res) {
+    return serializer(asResValue(res))
   }
 }
 


Test patch:
diff --git a/test.js b/test.js
index ad99fc5..0f32fff 100644
--- a/test.js
+++ b/test.js
@@ -748,8 +748,8 @@ experiment('logging with mergeHapiLogData option enabled', () => {
   })
 })
 
-experiment('logging with overridden serializer', () => {
-  test('with pre-defined req serializer', async () => {
+experiment('custom serializers', () => {
+  test('logging with configured req serializer', async () => {
     const server = getServer()
     let done
     const finish = new Promise(function (resolve, reject) {
@@ -778,7 +778,7 @@ experiment('logging with overridden serializer', () => {
     await finish
   })
 
-  test('with pre-defined res serializer', async () => {
+  test('logging with configured res serializer', async () => {
     const server = getServer()
     let done
     const finish = new Promise(function (resolve, reject) {
@@ -786,6 +786,7 @@ experiment('logging with overridden serializer', () => {
     })
     const stream = sink((data) => {
       expect(data.res.code).to.equal(404)
+      expect(data.res.raw).to.be.an.object()
       done()
     })
     const logger = require('pino')(stream)
@@ -794,7 +795,7 @@ experiment('logging with overridden serializer', () => {
       options: {
         instance: logger,
         serializers: {
-          res: (res) => ({ code: res.statusCode })
+          res: (res) => ({ code: res.statusCode, raw: res.raw })
         }
       }
     }
@@ -807,7 +808,7 @@ experiment('logging with overridden serializer', () => {
     await finish
   })
 
-  test('with pre-defined err serializer', async () => {
+  test('logging with pre-defined err serializer', async () => {
     const server = getServer()
     let done
     const finish = new Promise(function (resolve, reject) {
@@ -835,6 +836,72 @@ experiment('logging with overridden serializer', () => {
     })
     await finish
   })
+
+  test('req.raw is not enumerable', async () => {
+    const server = getServer()
+    let done
+    const finish = new Promise(function (resolve, reject) {
+      done = resolve
+    })
+    const stream = sink((data) => {
+      expect(data.req).to.be.an.object()
+      done()
+    })
+    const logger = require('pino')(stream)
+    const plugin = {
+      plugin: Pino,
+      options: {
+        instance: logger,
+        serializers: {
+          req: (req) => {
+            expect(req.raw).to.be.an.object()
+            expect(req.propertyIsEnumerable('raw')).to.be.false()
+            return req
+          }
+        }
+      }
+    }
+
+    await server.register(plugin)
+    await server.inject({
+      method: 'GET',
+      url: '/'
+    })
+    await finish
+  })
+
+  test('res.raw is not enumerable', async () => {
+    const server = getServer()
+    let done
+    const finish = new Promise(function (resolve, reject) {
+      done = resolve
+    })
+    const stream = sink((data) => {
+      expect(data.res).to.be.an.object()
+      done()
+    })
+    const logger = require('pino')(stream)
+    const plugin = {
+      plugin: Pino,
+      options: {
+        instance: logger,
+        serializers: {
+          res: (res) => {
+            expect(res.raw).to.be.an.object()
+            expect(res.propertyIsEnumerable('raw')).to.be.false()
+            return res
+          }
+        }
+      }
+    }
+
+    await server.register(plugin)
+    await server.inject({
+      method: 'GET',
+      url: '/'
+    })
+    await finish
+  })
 })
 
 experiment('logging with request payload', () => {