Repository: pinojs/pino-pretty (js)

Issue:
Dissapering logs with fatal
In this example the fatal log never appear:

```js
const pino = require('pino');
const pinoPretty = require('pino-pretty');

const logger = pino(
  { level: 'info' },
  pinoPretty({
    levelFirst: true,
    colorize: true,
    translateTime: 'yyyy-mm-dd HH:MM:ss',
  })
);

logger.info('test 123');
logger.fatal(new Error('not visible'));
process.exit(1)
```

It's clearly `pino-pretty` issue, as it does not happen with raw pino.
`sync: true,` is workaround.

Pull request description:
fix: building sonic boom safe
Added the following [block](https://github.com/pinojs/pino/blob/f8696ab5fb0e5a819b6255af420b29fb075cbdee/lib/tools.js#L335-L373) to build safe sonic boom.

Closes #304 

Patch:
diff --git a/index.js b/index.js
index 87234f9..663bd72 100644
--- a/index.js
+++ b/index.js
@@ -4,9 +4,7 @@ const { isColorSupported } = require('colorette')
 const pump = require('pump')
 const { Transform } = require('readable-stream')
 const abstractTransport = require('pino-abstract-transport')
-const sonic = require('sonic-boom')
 const sjs = require('secure-json-parse')
-
 const colors = require('./lib/colors')
 const { ERROR_LIKE_KEYS, MESSAGE_KEY, TIMESTAMP_KEY, LEVEL_KEY, LEVEL_NAMES } = require('./lib/constants')
 const {
@@ -17,6 +15,7 @@ const {
   prettifyMetadata,
   prettifyObject,
   prettifyTime,
+  buildSafeSonicBoom,
   filterLog
 } = require('./lib/utils')
 
@@ -227,7 +226,7 @@ function build (opts = {}) {
     if (typeof opts.destination === 'object' && typeof opts.destination.write === 'function') {
       destination = opts.destination
     } else {
-      destination = sonic({
+      destination = buildSafeSonicBoom({
         dest: opts.destination || 1,
         append: opts.append,
         mkdir: opts.mkdir,
diff --git a/lib/utils.js b/lib/utils.js
index 706f5e8..6de61c4 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -2,7 +2,9 @@
 
 const clone = require('rfdc')({ circles: true })
 const dateformat = require('dateformat')
+const SonicBoom = require('sonic-boom')
 const stringifySafe = require('fast-safe-stringify')
+const { isMainThread } = require('worker_threads')
 const defaultColorizer = require('./colors')()
 const {
   DATE_FORMAT,
@@ -23,6 +25,7 @@ module.exports = {
   prettifyMetadata,
   prettifyObject,
   prettifyTime,
+  buildSafeSonicBoom,
   filterLog
 }
 
@@ -580,3 +583,67 @@ function filterLog (log, ignoreKeys) {
   })
   return logCopy
 }
+
+function noop () {}
+
+/**
+ * Creates a safe SonicBoom instance
+ *
+ * @param {object} opts Options for SonicBoom
+ *
+ * @returns {object} A new SonicBoom stream
+ */
+function buildSafeSonicBoom (opts) {
+  const stream = new SonicBoom(opts)
+  stream.on('error', filterBrokenPipe)
+  // if we are sync: false, we must flush on exit
+  if (!opts.sync && isMainThread) {
+    setupOnExit(stream)
+  }
+  return stream
+
+  function filterBrokenPipe (err) {
+    if (err.code === 'EPIPE') {
+      stream.write = noop
+      stream.end = noop
+      stream.flushSync = noop
+      stream.destroy = noop
+      return
+    }
+    stream.removeListener('error', filterBrokenPipe)
+  }
+}
+
+function setupOnExit (stream) {
+  /* istanbul ignore next */
+  if (global.WeakRef && global.WeakMap && global.FinalizationRegistry) {
+    // This is leak free, it does not leave event handlers
+    const onExit = require('on-exit-leak-free')
+
+    onExit.register(stream, autoEnd)
+
+    stream.on('close', function () {
+      onExit.unregister(stream)
+    })
+  }
+}
+
+/* istanbul ignore next */
+function autoEnd (stream, eventName) {
+  // This check is needed only on some platforms
+
+  if (stream.destroyed) {
+    return
+  }
+
+  if (eventName === 'beforeExit') {
+    // We still have an event loop, let's use it
+    stream.flush()
+    stream.on('drain', function () {
+      stream.end()
+    })
+  } else {
+    // We do not have an event loop, so flush synchronously
+    stream.flushSync()
+  }
+}
diff --git a/package.json b/package.json
index d374634..011e932 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
     "dateformat": "^4.6.3",
     "fast-safe-stringify": "^2.0.7",
     "joycon": "^3.1.1",
+    "on-exit-leak-free": "^0.2.0",
     "pino-abstract-transport": "^0.5.0",
     "pump": "^3.0.0",
     "readable-stream": "^3.6.0",


Test patch:
diff --git a/test/lib/utils.public.test.js b/test/lib/utils.public.test.js
index 0acbc2e..fb2b175 100644
--- a/test/lib/utils.public.test.js
+++ b/test/lib/utils.public.test.js
@@ -3,6 +3,9 @@
 const tap = require('tap')
 const getColorizer = require('../../lib/colors')
 const utils = require('../../lib/utils')
+const rimraf = require('rimraf')
+const { join } = require('path')
+const fs = require('fs')
 
 tap.test('prettifyErrorLog', t => {
   const { prettifyErrorLog } = utils
@@ -428,3 +431,48 @@ tap.test('#filterLog with circular references', t => {
 
   t.end()
 })
+
+tap.test('buildSafeSonicBoom', t => {
+  const { buildSafeSonicBoom } = utils
+
+  function noop () {}
+
+  const file = () => {
+    const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`)
+    const fd = fs.openSync(dest, 'w')
+    return { dest, fd }
+  }
+
+  t.test('should not write when error emitted and code is "EPIPE"', async t => {
+    t.plan(1)
+
+    const { fd, dest } = file()
+    const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
+    t.teardown(() => rimraf(dest, noop))
+
+    stream.emit('error', { code: 'EPIPE' })
+    stream.write('will not work')
+
+    const dataFile = fs.readFileSync(dest)
+    t.equal(dataFile.length, 0)
+  })
+
+  t.test('should stream.write works when error code is not "EPIPE"', async t => {
+    t.plan(3)
+    const { fd, dest } = file()
+    const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
+
+    t.teardown(() => rimraf(dest, noop))
+
+    stream.on('error', () => t.pass('error emitted'))
+
+    stream.emit('error', 'fake error description')
+
+    t.ok(stream.write('will work'))
+
+    const dataFile = fs.readFileSync(dest)
+    t.equal(dataFile.toString(), 'will work')
+  })
+
+  t.end()
+})