Repository: pinojs/pino (js)

Issue:
Support multiple output streams
Hi there,

Does Pino support multiple output streams? I took a look at the API and source, and it looks like currently Pino only supports one output stream. We have use cases where we'd like to output the logs both to stdout and HTTP, and multiple output streams, similar to what [bunyan has](https://github.com/trentm/node-bunyan#streams) would be required for this use case.

Additionally, if we can change the API to support adding an additional stream after construction, it would also be helpful -- as some streams have to be created asynchronously.

Pull request description:
Added metadata protocol
This can be used to implement #145, but done separately so we can keep multi-stream support out of the core `pino` module.

This was partially done in https://github.com/pinojs/pino/pull/255.

cc @yunong

Fixes #249 

Patch:
diff --git a/docs/API.md b/docs/API.md
index bd4c6b9..41a371b 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -27,6 +27,7 @@
     + [.epochTime](#epochTimeFunction)
     + [.slowTime](#slowTimeFunction)
     + [.nullTime](#nullTimeFunction)
++ [Metadata Support](#metadata)
 
 # module.exports
 
@@ -76,7 +77,8 @@
   * `enabled` (boolean): enables logging. Default: `true`
   * `browser` (Object): browser only, may have `asObject` and `write` keys, see [Pino in the Browser](../readme.md#browser)
 + `stream` (Writable): a writable stream where the logs will be written.
-  Default: `process.stdout`
+  It can also receive some log-line [metadata](#metadata), if the
+  relative protocol is enabled. Default: `process.stdout`
 
 ### Example:
 ```js
@@ -576,3 +578,31 @@ performance penalty.
 
 Returns an empty string. This function is used when the `timestamp` option
 is set to `false`.
+
+<a id="metadata"></a>
+# Metadata
+
+A destination stream can have a property `stream[Symbol.for('needsMetadata')] = true`
+to indicate that for every log line written, the following properties of the stream
+should be set:
+
+* the last logging level as `stream.lastLevel`
+* the last logging message as `stream.lastMsg`
+* the last logging object as `stream.lastObj`
+* the last logger instance as `stream.lastLogger` (to support child
+  loggers)
+
+## Example
+
+```js
+var instance = pino({}, {
+  [Symbol.for('needsMetadata')]: true,
+  write: function (chunk) {
+    console.log('lastLevel', this.lastLevel)
+    console.log('lastMsg', this.lastMsg)
+    console.log('lastObj', this.lastObj)
+    console.log('lastLogger', this.lastLogger)
+    console.log('line', chunk)
+  }
+})
+```
diff --git a/pino.js b/pino.js
index 87491a5..2621869 100644
--- a/pino.js
+++ b/pino.js
@@ -11,6 +11,7 @@ var levels = require('./lib/levels')
 var tools = require('./lib/tools')
 var serializers = require('./lib/serializers')
 var time = require('./lib/time')
+var needsMetadata = Symbol.for('needsMetadata')
 
 var LOG_VERSION = 1
 
@@ -199,14 +200,21 @@ Object.defineProperty(pinoPrototype, 'child', {
 
 function pinoWrite (obj, msg, num) {
   var s = this.asJson(obj, msg, num)
+  var stream = this.stream
   if (!this.cache) {
-    this.stream.write(flatstr(s))
+    if (stream[needsMetadata]) {
+      stream.lastLevel = num
+      stream.lastMsg = msg
+      stream.lastObj = obj
+      stream.lastLogger = this // for child loggers
+    }
+    stream.write(flatstr(s))
     return
   }
 
   this.cache.buf += s
   if (this.cache.buf.length > this.cache.size) {
-    this.stream.write(flatstr(this.cache.buf))
+    stream.write(flatstr(this.cache.buf))
     this.cache.buf = ''
   }
 }


Test patch:
diff --git a/test/metadata.test.js b/test/metadata.test.js
new file mode 100644
index 0000000..fe46333
--- /dev/null
+++ b/test/metadata.test.js
@@ -0,0 +1,121 @@
+'use strict'
+
+var test = require('tap').test
+var os = require('os')
+var pino = require('../')
+var sink = require('./helper').sink
+
+var pid = process.pid
+var hostname = os.hostname()
+
+test('metadata works', function (t) {
+  t.plan(6)
+  var dest = sink(function (chunk, enc, cb) {
+    t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
+    delete chunk.time
+    t.deepEqual(chunk, {
+      pid: pid,
+      hostname: hostname,
+      level: 30,
+      hello: 'world',
+      msg: 'a msg',
+      v: 1
+    })
+  })
+  var instance = pino({}, {
+    [Symbol.for('needsMetadata')]: true,
+    write: function (chunk) {
+      t.equal(instance, this.lastLogger)
+      t.equal(30, this.lastLevel)
+      t.equal('a msg', this.lastMsg)
+      t.deepEqual({ hello: 'world' }, this.lastObj)
+      dest.write(chunk)
+    }
+  })
+
+  instance.info({ hello: 'world' }, 'a msg')
+})
+
+test('child loggers works', function (t) {
+  t.plan(6)
+  var dest = sink(function (chunk, enc, cb) {
+    t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
+    delete chunk.time
+    t.deepEqual(chunk, {
+      pid: pid,
+      hostname: hostname,
+      level: 30,
+      hello: 'world',
+      from: 'child',
+      msg: 'a msg',
+      v: 1
+    })
+  })
+  var instance = pino({}, {
+    [Symbol.for('needsMetadata')]: true,
+    write: function (chunk) {
+      t.equal(child, this.lastLogger)
+      t.equal(30, this.lastLevel)
+      t.equal('a msg', this.lastMsg)
+      t.deepEqual({ from: 'child' }, this.lastObj)
+      dest.write(chunk)
+    }
+  })
+
+  var child = instance.child({ hello: 'world' })
+  child.info({ from: 'child' }, 'a msg')
+})
+
+test('without object', function (t) {
+  t.plan(6)
+  var dest = sink(function (chunk, enc, cb) {
+    t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
+    delete chunk.time
+    t.deepEqual(chunk, {
+      pid: pid,
+      hostname: hostname,
+      level: 30,
+      msg: 'a msg',
+      v: 1
+    })
+  })
+  var instance = pino({}, {
+    [Symbol.for('needsMetadata')]: true,
+    write: function (chunk) {
+      t.equal(instance, this.lastLogger)
+      t.equal(30, this.lastLevel)
+      t.equal('a msg', this.lastMsg)
+      t.equal(null, this.lastObj)
+      dest.write(chunk)
+    }
+  })
+
+  instance.info('a msg')
+})
+
+test('without msg', function (t) {
+  t.plan(6)
+  var dest = sink(function (chunk, enc, cb) {
+    t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
+    delete chunk.time
+    t.deepEqual(chunk, {
+      pid: pid,
+      hostname: hostname,
+      level: 30,
+      hello: 'world',
+      v: 1
+    })
+  })
+  var instance = pino({}, {
+    [Symbol.for('needsMetadata')]: true,
+    write: function (chunk) {
+      t.equal(instance, this.lastLogger)
+      t.equal(30, this.lastLevel)
+      t.equal(undefined, this.lastMsg)
+      t.deepEqual({ hello: 'world' }, this.lastObj)
+      dest.write(chunk)
+    }
+  })
+
+  instance.info({ hello: 'world' })
+})