diff --git a/benchmarks/multistream.js b/benchmarks/multistream.js
new file mode 100644
index 000000000..d67b7e235
--- /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 73b1bb4ad..e82492cc2 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)
+
+
+### `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')
+ ```
+
### `pino.stdSerializers` (Object)
diff --git a/docs/help.md b/docs/help.md
index 2957b5202..b713ad610 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
## 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).
## 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))
```
-
## How Pino handles duplicate keys
diff --git a/lib/levels.js b/lib/levels.js
index df65e5e67..1a707277b 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 000000000..3756523a6
--- /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 0d0e3e33f..05dddf226 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
diff --git a/test/multistream.test.js b/test/multistream.test.js
new file mode 100644
index 000000000..f5d49a0f5
--- /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()
+ })()
+ })
+})