Repository: pinojs/pino
Pull request #2105 (2024-12-11 00:03:05)

add new streamWrite hook
This PR adds a new `streamWrite` hook which is able to modify the final stringified log just before it written to a transport (stream).

The use case here is to allow a user (or plugin author like myself) to scrub _known sensitive secrets_ from logs before they are sent out to various transports, regardless of how these values ended up there (see #2102)

I played around with injecting a hook in a few different places but this seemed like the simplest and best place for it to go. Obviously could add more checks / tests / etc, but overall seems fairly simple.

Please let me know if you have any concerns or anything else you would like to see.

jsumners: @mcollina I don't have a dedicated system I can run the benchmarks on. Can you run them? The CI results are too variable to be of much use.
theoephraim: appears to be an unrelated intermittent test failure?

Patch:
diff --git a/docs/api.md b/docs/api.md
index b3d5a0b..c68ba5c 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -375,6 +375,23 @@ const hooks = {
 }
 ```
 
+
+<a id="streamWrite"></a>
+##### `streamWrite`
+
+Allows for manipulating the _stringified_ JSON log data just before writing to various transports.
+
+The method receives the stringified JSON and must return valid stringified JSON.
+
+For example:
+```js
+const hooks = {
+  streamWrite (s) {
+    return s.replaceAll('sensitive-api-key', 'XXX')
+  }
+}
+```
+
 <a id=opt-formatters></a>
 #### `formatters` (Object)
 
diff --git a/lib/proto.js b/lib/proto.js
index ef49a71..1ce47f6 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -27,7 +27,8 @@ const {
   stringifySym,
   formatOptsSym,
   stringifiersSym,
-  msgPrefixSym
+  msgPrefixSym,
+  hooksSym
 } = require('./symbols')
 const {
   getLevel,
@@ -185,6 +186,7 @@ function write (_obj, msg, num) {
   const messageKey = this[messageKeySym]
   const mixinMergeStrategy = this[mixinMergeStrategySym] || defaultMixinMergeStrategy
   let obj
+  const streamWriteHook = this[hooksSym].streamWrite
 
   if (_obj === undefined || _obj === null) {
     obj = {}
@@ -214,7 +216,7 @@ function write (_obj, msg, num) {
     stream.lastTime = t.slice(this[timeSliceIndexSym])
     stream.lastLogger = this // for child loggers
   }
-  stream.write(s)
+  stream.write(streamWriteHook ? streamWriteHook(s) : s)
 }
 
 function noop () {}
diff --git a/pino.d.ts b/pino.d.ts
index cdc5958..87eebe3 100644
--- a/pino.d.ts
+++ b/pino.d.ts
@@ -641,6 +641,12 @@ declare namespace pino {
              * using apply, like so: method.apply(this, newArgumentsArray).
              */
             logMethod?: (this: Logger, args: Parameters<LogFn>, method: LogFn, level: number) => void;
+
+            /**
+             * Allows for manipulating the stringified JSON log output just before writing to various transports.
+             * This function must return a string and must be valid JSON.
+             */
+            streamWrite?: (s: string) => string;
         };
 
         /**
diff --git a/pino.js b/pino.js
index 2398e5a..cabeaf6 100644
--- a/pino.js
+++ b/pino.js
@@ -70,7 +70,8 @@ const defaultOptions = {
     }
   }),
   hooks: {
-    logMethod: undefined
+    logMethod: undefined,
+    streamWrite: undefined
   },
   timestamp: epochTime,
   name: undefined,


Test patch:
diff --git a/test/hooks.test.js b/test/hooks.test.js
index c5a67b6..5ed8905 100644
--- a/test/hooks.test.js
+++ b/test/hooks.test.js
@@ -95,3 +95,24 @@ tap.test('log method hook', t => {
 
   t.end()
 })
+
+tap.test('streamWrite hook', t => {
+  t.test('gets invoked', async t => {
+    t.plan(1)
+
+    const stream = sink()
+    const logger = pino({
+      hooks: {
+        streamWrite (s) {
+          return s.replaceAll('redact-me', 'XXX')
+        }
+      }
+    }, stream)
+
+    const o = once(stream, 'data')
+    logger.info('hide redact-me in this string')
+    t.match(await o, { msg: 'hide XXX in this string' })
+  })
+
+  t.end()
+})
diff --git a/test/types/pino.test-d.ts b/test/types/pino.test-d.ts
index 02284cc..3c3d6e9 100644
--- a/test/types/pino.test-d.ts
+++ b/test/types/pino.test-d.ts
@@ -209,6 +209,10 @@ const withHooks = pino({
             expectType<pino.Logger>(this);
             return method.apply(this, args);
         },
+        streamWrite(s) {
+            expectType<string>(s);
+            return s.replaceAll('secret-key', 'xxx');
+        },
     },
 });