Repository: pinojs/pino
Pull request #1004 (2021-04-07 17:14:14)

Add pino.multistream
Source code imported from https://github.com/pinojs/pino-multi-stream.

Following up from the introduction of #1003 and #937, I think we can include this in pino as well.

A user might like to send the data to a remote transport and also write the logs to disk.

I plan to make `pino.multistream()` to be usable _within_ a custom transport, so that we do not spawn up more than one thread.

mcollina: @jsumners @davidmarkclements could you take a final look?

Patch:
diff --git a/benchmarks/multistream.js b/benchmarks/multistream.js
new file mode 100644
index 0000000..d67b7e2
--- /dev/null
+++ b/benchmarks/multistream.js
@@ -0,0 +1,98 @@
+'use strict'
+
+const bench = require('fastbench')
+const bunyan = require('bunyan')
+const pino = require('../')
+const fs = require('fs')
+const dest = fs.createWriteStream('/dev/null')
+
+const tenStreams = [
+  { stream: dest },
+  { stream: dest },
+  { stream: dest },
+  { stream: dest },
+  { stream: dest },
+  { level: 'debug', stream: dest },
+  { level: 'debug', stream: dest },
+  { level: 'trace', stream: dest },
+  { level: 'warn', stream: dest },
+  { level: 'fatal', stream: dest }
+]
+const pinomsTen = pino({ level: 'debug' }, pino.multistream(tenStreams))
+
+const fourStreams = [
+  { stream: dest },
+  { stream: dest },
+  { level: 'debug', stream: dest },
+  { level: 'trace', stream: dest }
+]
+const pinomsFour = pino({ level: 'debug' }, pino.multistream(fourStreams))
+
+const pinomsOne = pino({ level: 'info' }, pino.multistream(dest))
+const blogOne = bunyan.createLogger({
+  name: 'myapp',
+  streams: [{ stream: dest }]
+})
+
+const blogTen = bunyan.createLogger({
+  name: 'myapp',
+  streams: tenStreams
+})
+const blogFour = bunyan.createLogger({
+  name: 'myapp',
+  streams: fourStreams
+})
+
+const max = 10
+const run = bench([
+  function benchBunyanTen (cb) {
+    for (let i = 0; i < max; i++) {
+      blogTen.info('hello world')
+      blogTen.debug('hello world')
+      blogTen.trace('hello world')
+      blogTen.warn('hello world')
+      blogTen.fatal('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchPinoMSTen (cb) {
+    for (let i = 0; i < max; i++) {
+      pinomsTen.info('hello world')
+      pinomsTen.debug('hello world')
+      pinomsTen.trace('hello world')
+      pinomsTen.warn('hello world')
+      pinomsTen.fatal('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchBunyanFour (cb) {
+    for (let i = 0; i < max; i++) {
+      blogFour.info('hello world')
+      blogFour.debug('hello world')
+      blogFour.trace('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchPinoMSFour (cb) {
+    for (let i = 0; i < max; i++) {
+      pinomsFour.info('hello world')
+      pinomsFour.debug('hello world')
+      pinomsFour.trace('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchBunyanOne (cb) {
+    for (let i = 0; i < max; i++) {
+      blogOne.info('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchPinoMSOne (cb) {
+    for (let i = 0; i < max; i++) {
+      pinomsOne.info('hello world')
+    }
+    setImmediate(cb)
+  }
+], 10000)
+
+run()
diff --git a/docs/api.md b/docs/api.md
index 73b1bb4..e82492c 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -24,6 +24,7 @@
 * [Statics](#statics)
   * [pino.destination()](#pino-destination)
   * [pino.final()](#pino-final)
+  * [pino.multistream()](#pino-multistream)
   * [pino.stdSerializers](#pino-stdserializers)
   * [pino.stdTimeFunctions](#pino-stdtimefunctions)
   * [pino.symbols](#pino-symbols)
@@ -938,6 +939,73 @@ finalLogger.info('exiting...')
 * See [Asynchronous logging ⇗](/docs/asynchronous.md)
 * See [Log loss prevention ⇗](/docs/asynchronous.md#log-loss-prevention)
 
+<a id="pino-multistream"></a>
+
+### `pino.multistream(options) => Stream`
+
+Create a stream composed by multiple destination streams:
+
+```js
+var fs = require('fs')
+var pino = require('pino')
+var streams = [
+  {stream: fs.createWriteStream('/tmp/info.stream.out')},
+  {level: 'debug', stream: fs.createWriteStream('/tmp/debug.stream.out')},
+  {level: 'fatal', stream: fs.createWriteStream('/tmp/fatal.stream.out')}
+]
+
+var log = pino({
+  level: 'debug' // this MUST be set at the lowest level of the
+                 // destinations
+}, pino.multistream(streams))
+
+log.debug('this will be written to /tmp/debug.stream.out')
+log.info('this will be written to /tmp/debug.stream.out and /tmp/info.stream.out')
+log.fatal('this will be written to /tmp/debug.stream.out, /tmp/info.stream.out and /tmp/fatal.stream.out')
+```
+
+In order for `multistream` to work, the log level __must__ be set to the lowest level used in the streams array.
+
+#### Options
+
+* `levels`:  Pass custom log level definitions to the instance as an object.
+
++ `dedupe`: Set this to `true` to send logs only to the stream with the higher level. Default: `false`
+
+    `dedupe` flag can be useful for example when using pino-multi-stream to redirect `error` logs to `process.stderr` and others to `process.stdout`:
+
+    ```js
+    var pino = require('pino')
+    var multistream = pino.multistream
+    var streams = [
+      {stream: process.stdout},
+      {level: 'error', stream: process.stderr},
+    ]
+
+    var opts = {
+        levels: {
+            silent: Infinity,
+            fatal: 60,
+            error: 50,
+            warn: 50,
+            info: 30,
+            debug: 20,
+            trace: 10
+        },
+        dedupe: true,
+    }
+
+    var log = pino({
+      level: 'debug' // this MUST be set at the lowest level of the
+                    // destinations
+    }, multistream(streams, opts))
+
+    log.debug('this will be written ONLY to process.stdout')
+    log.info('this will be written ONLY to process.stdout')
+    log.error('this will be written ONLY to process.stderr')
+    log.fatal('this will be written ONLY to process.stderr')
+    ```
+
 <a id="pino-stdserializers"></a>
 ### `pino.stdSerializers` (Object)
 
diff --git a/docs/help.md b/docs/help.md
index 2957b52..b713ad6 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -118,20 +118,7 @@ Given a similar scenario as in the [Log rotation](#rotate) section a basic
 <a id="multiple"></a>
 ## Saving to multiple files
 
-Let's assume we want to store all error messages to a separate log file.
-
-Install [pino-tee](https://npm.im/pino-tee) with:
-
-```bash
-npm i pino-tee -g
-```
-
-The following writes the log output of `app.js` to `./all-logs`, while
-writing only warnings and errors to `./warn-log:
-
-```bash
-node app.js | pino-tee warn ./warn-logs > ./all-logs
-```
+See [`pino.multistream`](/doc/api.md#pino-multistream).
 
 <a id="filter-logs"></a>
 ## Log Filtering
@@ -164,14 +151,13 @@ ExecStart=/bin/sh -c '/path/to/node app.js | pino-transport'
 
 Pino's default log destination is the singular destination of `stdout`. While
 not recommended for performance reasons, multiple destinations can be targeted
-by using [`pino-multi-stream`](https://github.com/pinojs/pino-multi-stream).
+by using [`pino.multistream`](/doc/api.md#pino-multistream).
 
 In this example we use `stderr` for `error` level logs and `stdout` as default
 for all other levels (e.g. `debug`, `info`, and `warn`).
 
 ```js
 const pino = require('pino')
-const { multistream } = require('pino-multi-stream')
 var streams = [
   {level: 'debug', stream: process.stdout},
   {level: 'error', stream: process.stderr},
@@ -180,11 +166,10 @@ var streams = [
 
 const logger = pino({
   name: 'my-app',
-  level: 'info',
-}, multistream(streams))
+  level: 'debug', // must be the lowest level of all streams
+}, pino.multistream(streams))
 ```
 
-
 <a id="dupe-keys"></a>
 ## How Pino handles duplicate keys
 
diff --git a/lib/levels.js b/lib/levels.js
index df65e5e..1a70727 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -188,6 +188,7 @@ module.exports = {
   setLevel,
   isLevelEnabled,
   mappings,
+  levels,
   assertNoLevelCollisions,
   assertDefaultLevelFound
 }
diff --git a/lib/multistream.js b/lib/multistream.js
new file mode 100644
index 0000000..3756523
--- /dev/null
+++ b/lib/multistream.js
@@ -0,0 +1,132 @@
+'use strict'
+
+const metadata = Symbol.for('pino.metadata')
+const { levels } = require('./levels')
+
+const defaultLevels = Object.create(levels)
+defaultLevels.silent = Infinity
+
+function multistream (streamsArray, opts) {
+  let counter = 0
+
+  streamsArray = streamsArray || []
+  opts = opts || { dedupe: false }
+
+  let levels = defaultLevels
+  if (opts.levels && typeof opts.levels === 'object') {
+    levels = opts.levels
+  }
+
+  const res = {
+    write,
+    add,
+    flushSync,
+    minLevel: 0,
+    streams: [],
+    clone,
+    [metadata]: true
+  }
+
+  if (Array.isArray(streamsArray)) {
+    streamsArray.forEach(add, res)
+  } else {
+    add.call(res, streamsArray)
+  }
+
+  // clean this object up
+  // or it will stay allocated forever
+  // as it is closed on the following closures
+  streamsArray = null
+
+  return res
+
+  // we can exit early because the streams are ordered by level
+  function write (data) {
+    let dest
+    const level = this.lastLevel
+    const { streams } = this
+    let stream
+    for (let i = 0; i < streams.length; i++) {
+      dest = streams[i]
+      if (dest.level <= level) {
+        stream = dest.stream
+        if (stream[metadata]) {
+          const { lastTime, lastMsg, lastObj, lastLogger } = this
+          stream.lastLevel = level
+          stream.lastTime = lastTime
+          stream.lastMsg = lastMsg
+          stream.lastObj = lastObj
+          stream.lastLogger = lastLogger
+        }
+        if (!opts.dedupe) {
+          stream.write(data)
+        }
+      } else {
+        break
+      }
+    }
+
+    if (opts.dedupe && stream) {
+      stream.write(data)
+    }
+  }
+
+  function flushSync () {
+    for (const { stream } of this.streams) {
+      if (typeof stream.flushSync === 'function') {
+        stream.flushSync()
+      }
+    }
+  }
+
+  function add (dest) {
+    const { streams } = this
+    if (typeof dest.write === 'function') {
+      return add.call(this, { stream: dest })
+    } else if (typeof dest.levelVal === 'number') {
+      return add.call(this, Object.assign({}, dest, { level: dest.levelVal, levelVal: undefined }))
+    } else if (typeof dest.level === 'string') {
+      return add.call(this, Object.assign({}, dest, { level: levels[dest.level] }))
+    } else if (typeof dest.level !== 'number') {
+      // we default level to 'info'
+      dest = Object.assign({}, dest, { level: 30 })
+    } else {
+      dest = Object.assign({}, dest)
+    }
+    dest.id = counter++
+
+    streams.unshift(dest)
+    streams.sort(compareByLevel)
+
+    this.minLevel = streams[0].level
+
+    return res
+  }
+
+  function clone (level) {
+    const streams = new Array(this.streams.length)
+
+    for (let i = 0; i < streams.length; i++) {
+      streams[i] = {
+        level: level,
+        stream: this.streams[i].stream
+      }
+    }
+
+    return {
+      write,
+      add,
+      minLevel: level,
+      streams,
+      clone,
+      flushSync,
+      [metadata]: true
+    }
+  }
+}
+
+function compareByLevel (a, b) {
+  return a.level - b.level
+}
+
+module.exports = multistream
diff --git a/pino.js b/pino.js
index 0d0e3e3..05dddf2 100644
--- a/pino.js
+++ b/pino.js
@@ -223,6 +223,8 @@ module.exports.destination = (dest = process.stdout.fd) => {
   }
 }
 
+module.exports.multistream = require('./lib/multistream')
+
 module.exports.final = final
 module.exports.levels = mappings()
 module.exports.stdSerializers = serializers


Test patch:
diff --git a/test/multistream.test.js b/test/multistream.test.js
new file mode 100644
index 0000000..f5d49a0
--- /dev/null
+++ b/test/multistream.test.js
@@ -0,0 +1,476 @@
+'use strict'
+
+const writeStream = require('flush-write-stream')
+const { join } = require('path')
+const { readFileSync } = require('fs')
+const os = require('os')
+const test = require('tap').test
+const pino = require('../')
+const multistream = pino.multistream
+
+test('sends to multiple streams using string levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const log = pino({
+    level: 'trace'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 9)
+  t.end()
+})
+
+test('sends to multiple streams using custom levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const log = pino({
+    level: 'trace'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 9)
+  t.end()
+})
+
+test('sends to multiple streams using optionally predefined levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const opts = {
+    levels: {
+      silent: Infinity,
+      fatal: 60,
+      error: 50,
+      warn: 50,
+      info: 30,
+      debug: 20,
+      trace: 10
+    }
+  }
+  const streams = [
+    { stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'info', stream: stream },
+    { level: 'warn', stream: stream },
+    { level: 'error', stream: stream },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const mstream = multistream(streams, opts)
+  const log = pino({
+    level: 'trace'
+  }, mstream)
+  log.trace('trace stream')
+  log.debug('debug stream')
+  log.info('info stream')
+  log.warn('warn stream')
+  log.error('error stream')
+  log.fatal('fatal stream')
+  log.silent('silent stream')
+  t.equal(messageCount, 24)
+  t.end()
+})
+
+test('sends to multiple streams using number levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 20, stream: stream },
+    { level: 60, stream: stream }
+  ]
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 6)
+  t.end()
+})
+
+test('level include higher levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const log = pino({}, multistream([{ level: 'info', stream: stream }]))
+  log.fatal('message')
+  t.equal(messageCount, 1)
+  t.end()
+})
+
+test('supports multiple arguments', function (t) {
+  const messages = []
+  const stream = writeStream(function (data, enc, cb) {
+    messages.push(JSON.parse(data))
+    if (messages.length === 2) {
+      const msg1 = messages[0]
+      t.equal(msg1.msg, 'foo bar baz foobar')
+
+      const msg2 = messages[1]
+      t.equal(msg2.msg, 'foo bar baz foobar barfoo foofoo')
+
+      t.end()
+    }
+    cb()
+  })
+  const log = pino({}, multistream({ stream }))
+  log.info('%s %s %s %s', 'foo', 'bar', 'baz', 'foobar') // apply not invoked
+  log.info('%s %s %s %s %s %s', 'foo', 'bar', 'baz', 'foobar', 'barfoo', 'foofoo') // apply invoked
+})
+
+test('supports children', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    const input = JSON.parse(data)
+    t.equal(input.msg, 'child stream')
+    t.equal(input.child, 'one')
+    t.end()
+    cb()
+  })
+  const streams = [
+    { stream: stream }
+  ]
+  const log = pino({}, multistream(streams)).child({ child: 'one' })
+  log.info('child stream')
+})
+
+test('supports grandchildren', function (t) {
+  const messages = []
+  const stream = writeStream(function (data, enc, cb) {
+    messages.push(JSON.parse(data))
+    if (messages.length === 3) {
+      const msg1 = messages[0]
+      t.equal(msg1.msg, 'grandchild stream')
+      t.equal(msg1.child, 'one')
+      t.equal(msg1.grandchild, 'two')
+
+      const msg2 = messages[1]
+      t.equal(msg2.msg, 'grandchild stream')
+      t.equal(msg2.child, 'one')
+      t.equal(msg2.grandchild, 'two')
+
+      const msg3 = messages[2]
+      t.equal(msg3.msg, 'debug grandchild')
+      t.equal(msg3.child, 'one')
+      t.equal(msg3.grandchild, 'two')
+
+      t.end()
+    }
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream }
+  ]
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams)).child({ child: 'one' }).child({ grandchild: 'two' })
+  log.info('grandchild stream')
+  log.debug('debug grandchild')
+})
+
+test('supports custom levels', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    t.equal(JSON.parse(data).msg, 'bar')
+    t.end()
+  })
+  const log = pino({
+    customLevels: {
+      foo: 35
+    }
+  }, multistream([{ level: 35, stream: stream }]))
+  log.foo('bar')
+})
+
+test('supports pretty print', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    t.not(data.toString().match(/INFO.*: pretty print/), null)
+    t.end()
+    cb()
+  })
+  const outStream = pino({
+    prettyPrint: {
+      levelFirst: true,
+      colorize: false
+    }
+  }, stream)
+
+  const log = pino({
+    level: 'debug',
+    name: 'helloName'
+  }, multistream([
+    { stream: outStream[pino.symbols.streamSym] }
+  ]))
+
+  log.info('pretty print')
+})
+
+test('children support custom levels', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    t.equal(JSON.parse(data).msg, 'bar')
+    t.end()
+  })
+  const parent = pino({
+    customLevels: {
+      foo: 35
+    }
+  }, multistream([{ level: 35, stream: stream }]))
+  const child = parent.child({ child: 'yes' })
+  child.foo('bar')
+})
+
+test('levelVal ovverides level', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'blabla', levelVal: 15, stream: stream },
+    { level: 60, stream: stream }
+  ]
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 6)
+  t.end()
+})
+
+test('forwards metadata', function (t) {
+  t.plan(3)
+  const streams = [
+    {
+      stream: {
+        [Symbol.for('pino.metadata')]: true,
+        write (chunk) {
+          t.equal(log, this.lastLogger)
+          t.equal(30, this.lastLevel)
+          t.same({ hello: 'world', msg: 'a msg' }, this.lastObj)
+        }
+      }
+    }
+  ]
+
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams))
+
+  log.info({ hello: 'world' }, 'a msg')
+  t.end()
+})
+
+test('forward name', function (t) {
+  t.plan(2)
+  const streams = [
+    {
+      stream: {
+        [Symbol.for('pino.metadata')]: true,
+        write (chunk) {
+          const line = JSON.parse(chunk)
+          t.equal(line.name, 'helloName')
+          t.equal(line.hello, 'world')
+        }
+      }
+    }
+  ]
+
+  const log = pino({
+    level: 'debug',
+    name: 'helloName'
+  }, multistream(streams))
+
+  log.info({ hello: 'world' }, 'a msg')
+  t.end()
+})
+
+test('forward name with child', function (t) {
+  t.plan(3)
+  const streams = [
+    {
+      stream: {
+        write (chunk) {
+          const line = JSON.parse(chunk)
+          t.equal(line.name, 'helloName')
+          t.equal(line.hello, 'world')
+          t.equal(line.component, 'aComponent')
+        }
+      }
+    }
+  ]
+
+  const log = pino({
+    level: 'debug',
+    name: 'helloName'
+  }, multistream(streams)).child({ component: 'aComponent' })
+
+  log.info({ hello: 'world' }, 'a msg')
+  t.end()
+})
+
+test('clone generates a new multistream with all stream at the same level', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'fatal', stream: stream }
+  ]
+  const ms = multistream(streams)
+  const clone = ms.clone(30)
+
+  t.not(clone, ms)
+
+  clone.streams.forEach((s, i) => {
+    t.not(s, streams[i])
+    t.equal(s.stream, streams[i].stream)
+    t.equal(s.level, 30)
+  })
+
+  const log = pino({
+    level: 'trace'
+  }, clone)
+
+  log.info('info stream')
+  log.debug('debug message not counted')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 8)
+
+  t.end()
+})
+
+test('one stream', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const log = pino({
+    level: 'trace'
+  }, multistream({ stream, level: 'fatal' }))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 1)
+  t.end()
+})
+
+test('dedupe', function (t) {
+  let messageCount = 0
+  const stream1 = writeStream(function (data, enc, cb) {
+    messageCount -= 1
+    cb()
+  })
+
+  const stream2 = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+
+  const streams = [
+    {
+      stream: stream1,
+      level: 'info'
+    },
+    {
+      stream: stream2,
+      level: 'fatal'
+    }
+  ]
+
+  const log = pino({
+    level: 'trace'
+  }, multistream(streams, { dedupe: true }))
+  log.info('info stream')
+  log.fatal('fatal stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 1)
+  t.end()
+})
+
+test('no stream', function (t) {
+  const log = pino({
+    level: 'trace'
+  }, multistream())
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.end()
+})
+
+test('add a stream', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const log = pino({
+    level: 'trace'
+  }, multistream(stream))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 2)
+  t.end()
+})
+
+test('flushSync', function (t) {
+  const tmp = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const destination = pino.destination({ dest: tmp, sync: false, minLength: 4096 })
+  const log = pino({ level: 'info' }, multistream([{ level: 'info', stream: destination }]))
+  destination.on('ready', () => {
+    log.info('foo')
+    log.info('bar')
+    t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 0)
+    pino.final(log, (err, finalLogger) => {
+      if (err) {
+        t.fail()
+        return t.end()
+      }
+      t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 2)
+      finalLogger.info('biz')
+      t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 3)
+      t.end()
+    })()
+  })
+})