Repository: pinojs/pino (js)

Issue:
Access to pino instance from prettifier function
Hi. Are there any method that will allow to get pino instance from formatter? Probably we can bind it to `this` somewhere when prettifier is constructed?

It could be utilized, for example, for getting levels (especially if there are custom levels).

Pull request:
Bind pino instance to prettifier
This PR passes pino instance to prettifier function.

It can be used to access configured levels, for example, here https://github.com/pinojs/pino-pretty/blob/master/lib/colors.js#L32 levels are hardcoded and if there are `customLevels` setup, then it will not work correctly.

Closes #720.

PS: I'm not a fan of `tap`, so tests might look not right.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ce34d1b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,38 @@
+name: CI
+on: [push, pull_request]
+jobs:
+  build:
+    name: ${{ matrix.node-version }} ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [macOS-latest, windows-latest, ubuntu-latest]
+        node-version: [6, 8, 10, 11, 12, 13]
+    steps:
+      - uses: actions/checkout@v1
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Install
+        run: npm i
+      - name: Tests
+        run: npm run test
+  coverage:
+    name: coverage
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - name: Use Node.js 12
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+      - name: Install
+        run: npm i
+      - name: run with coverage
+        run: npm run cov-ci
+      - name: Coveralls
+        uses: coverallsapp/github-action@master
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.travis.yml b/.travis.yml
index 0c45ee9..3aa2522 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,8 @@ node_js:
   - 8
   - 10
   - 11
+  - 12
+  - 13
 script:
   - npm run ci
 env:
diff --git a/README.md b/README.md
index 1452aad..04a4070 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ For using Pino with a web framework see:
 * [Pino with Restify](docs/web.md#restify)
 * [Pino with Koa](docs/web.md#koa)
 * [Pino with Node core `http`](docs/web.md#http)
+* [Pino with Nest](docs/web.md#nest)
 
 
 <a name="essentials"></a>
diff --git a/docs/api.md b/docs/api.md
index a6b389e..bbd57dd 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -96,6 +96,28 @@ logger.foo('hi')
 logger.info('hello') // Will throw an error saying info in not found in logger object
 ```
 
+#### `mixin` (Function):
+
+Default: `undefined`
+
+If provided, the `mixin` function is called each time one of the active
+logging methods is called. The function must synchronously return an
+object. The properties of the returned object will be added to the
+logged JSON.
+
+```js
+let n = 0
+const logger = pino({
+  mixin () {
+    return { line: ++n }
+  }
+})
+logger.info('hello')
+// {"level":30,"time":1573664685466,"pid":78742,"hostname":"x","line":1,"msg":"hello","v":1}
+logger.info('world')
+// {"level":30,"time":1573664685469,"pid":78742,"hostname":"x","line":2,"msg":"world","v":1}
+```
+
 #### `redact` (Array | Object):
 
 Default: `undefined`
@@ -179,6 +201,32 @@ Default: `'msg'`
 
 The string key for the 'message' in the JSON object.
 
+<a id=opt-nestedkey></a>
+#### `nestedKey` (String)
+
+Default: `null`
+
+If there's a chance that objects being logged have properties that conflict with those from pino itself (`level`, `timestamp`, `v`, `pid`, etc)
+and duplicate keys in your log records are undesirable, pino can be configured with a `nestedKey` option that causes any `object`s that are logged
+to be placed under a key whose name is the value of `nestedKey`.
+
+This way, when searching something like Kibana for values, one can consistently search under the configured `nestedKey` value instead of the root log record keys.
+
+For example,
+```js
+const logger = require('pino')({
+  nestedKey: 'payload'
+})
+
+const thing = { level: 'hi', time: 'never', foo: 'bar'} // has pino-conflicting properties!
+logger.info(thing)
+
+// logs the following:
+// {"level":30,"time":1578357790020,"pid":91736,"hostname":"x","payload":{"level":"hi","time":"never","foo":"bar"},"v":1}
+```
+In this way, logged objects' properties don't conflict with pino's standard logging properties,
+and searching for logged objects can start from a consistent path.
+
 <a id=prettyPrint></a>
 #### `prettyPrint` (Boolean | Object)
 
@@ -206,14 +254,18 @@ Enables printing of level labels instead of level values in the printed logs.
 Warning: this option may not be supported by downstream transports.
 
 <a id="changeLevelName"></a>
-#### `changeLevelName` (String)
+#### `changeLevelName` (String) - DEPRECATED
+Use `levelKey` instead. This will be removed in v7.
+
+<a id="levelKey"></a>
+#### `levelKey` (String)
 
 Default: `'level'`
 
 Changes the property `level` to any string value you pass in:
 ```js
 const logger = pino({
-  changeLevelName: 'priority'
+  levelKey: 'priority'
 })
 logger.info('hello world')
 // {"priority":30,"time":1531257112193,"msg":"hello world","pid":55956,"hostname":"x","v":1}
@@ -434,6 +486,13 @@ Write a `'error'` level log, if the configured `level` allows for it.
 
 Write a `'fatal'` level log, if the configured `level` allows for it.
 
+Since `'fatal'` level messages are intended to be logged just prior to the process exiting the `fatal`
+method will always sync flush the destination.
+Therefore it's important not to misuse `fatal` since
+it will cause performance overhead if used for any
+other purpose than writing final log messages before
+the process crashes or exits.
+
 * See [`mergingObject` log method parameter](#mergingobject)
 * See [`message` log method parameter](#message)
 * See [`...interpolationValues` log method parameter](#interpolationvalues)
@@ -549,7 +608,7 @@ The `silent` logging level is a specialized level which will disable all logging
 there is no `silent` log method.
 
 <a id="islevelenabled"></a>
-### `logger.islevelenabled(level)`
+### `logger.isLevelEnabled(level)`
 
 A utility method for determining if a given log level will write to the destination.
 
@@ -778,6 +837,7 @@ The `pino.stdTimeFunctions` object provides a very small set of common functions
 * `pino.stdTimeFunctions.epochTime`: Milliseconds since Unix epoch (Default)
 * `pino.stdTimeFunctions.unixTime`: Seconds since Unix epoch
 * `pino.stdTimeFunctions.nullTime`: Clears timestamp property (Used when `timestamp: false`)
+* `pino.stdTimeFunctions.isoTime`: ISO 8601-formatted time in UTC
 
 * See [`timestamp` option](#opt-timestamp)
 
diff --git a/docs/pretty.md b/docs/pretty.md
index c9eca19..bd90ebb 100644
--- a/docs/pretty.md
+++ b/docs/pretty.md
@@ -23,8 +23,12 @@ The API requires modules provide a factory function which returns a prettifier
 function. This prettifier function must accept either a string of NDJSON or
 a Pino log object. A psuedo-example of such a prettifier is:
 
+The uninitialized Pino instance is passed as `this` into prettifier factory function,
+so it can be accessed via closure by the returned prettifier function.
+
 ```js
 module.exports = function myPrettifier (options) {
+  // `this` is bound to the pino instance
   // Deal with whatever options are supplied.
   return function prettifier (inputData) {
     let logObject
diff --git a/docs/redaction.md b/docs/redaction.md
index 200f8c9..ea87531 100644
--- a/docs/redaction.md
+++ b/docs/redaction.md
@@ -93,7 +93,7 @@ This will output
 {"level":30,"time":1527782356751,"pid":5758,"hostname":"Davids-MacBook-Pro-3.local","path":{"to":{"another":"thing"}},"stuff":{"thats":[{"logme":"will be logged"},{"logme":"as will this"}]},"v":1}
 ```
 
-See [pino options in API](api.md#pino) for `redact` API details.
+See [pino options in API](/docs/api.md#redact-array-object) for `redact` API details.
 
 <a name="paths"></a>
 ## Path Syntax
diff --git a/docs/transports.md b/docs/transports.md
index 55aaae0..73cb439 100644
--- a/docs/transports.md
+++ b/docs/transports.md
@@ -75,6 +75,7 @@ PR's to this document are welcome for any new transports!
 + [pino-datadog](#pino-datadog)
 + [pino-elasticsearch](#pino-elasticsearch)
 + [pino-mq](#pino-mq)
++ [pino-mysql](#pino-mysql)
 + [pino-papertrail](#pino-papertrail)
 + [pino-redis](#pino-redis)
 + [pino-sentry](#pino-sentry)
@@ -82,6 +83,7 @@ PR's to this document are welcome for any new transports!
 + [pino-stackdriver](#pino-stackdriver)
 + [pino-syslog](#pino-syslog)
 + [pino-websocket](#pino-websocket)
++ [pino-http-send](#pino-http-send)
 
 <a id="pino-applicationinsights"></a>
 ### pino-applicationinsights
@@ -222,6 +224,25 @@ node yourapp.js | pino-papertrail --host bar.papertrailapp.com --port 12345 --ap
 
 for full documentation of command line switches read [readme](https://github.com/ovhemert/pino-papertrail#readme)
 
+<a id="pino-mysql"></a>
+### pino-mysql
+
+[pino-mysql][pino-mysql] loads pino logs into [MySQL][MySQL] and [MariaDB][MariaDB].
+
+```sh
+$ node app.js | pino-mysql -c db-configuration.json
+```
+
+`pino-mysql` can extract and save log fields into corresponding database field
+and/or save the entire log stream as a [JSON Data Type][JSONDT].
+
+For full documentation and command line switches read the [readme][pino-mysql].
+
+[pino-mysql]: https://www.npmjs.com/package/pino-mysql
+[MySQL]: https://www.mysql.com/
+[MariaDB]: https://mariadb.org/
+[JSONDT]: https://dev.mysql.com/doc/refman/8.0/en/json.html
+
 <a id="pino-redis"></a>
 ### pino-redis
 
@@ -354,3 +375,13 @@ $ node app.js | pino-websocket -a my-websocket-server.example.com -p 3004
 ```
 
 For full documentation of command line switches read [readme](https://github.com/abeai/pino-webscoket#README)
+
+<a id="pino-http-send"></a>
+### pino-http-send
+
+[pino-http-send](https://npmjs.com/package/pino-http-send) is a configurable and low overhead
+transport that will batch logs and send to a specified URL.
+
+```console
+$ node app.js | pino-http-send -u http://localhost:8080/logs
+```
diff --git a/docs/web.md b/docs/web.md
index f42fab5..b8f9890 100644
--- a/docs/web.md
+++ b/docs/web.md
@@ -15,7 +15,7 @@ web framework ecosystem.
 ## Pino with Fastify
 
 The Fastify web framework comes bundled with Pino by default, simply set Fastify's
-`logger` option to `true` and use `reply.log` for log messages that correspond
+`logger` option to `true` and use `request.log` or `reply.log` for log messages that correspond
 to each individual request:
 
 ```js
@@ -29,7 +29,7 @@ fastify.get('/', async (request, reply) => {
 ```
 
 The `logger` option can also be set to an object, which will be passed through directly
-as the [`pino` options object](api.md#options).
+as the [`pino` options object](/docs/api.md#options-object).
 
 See the [fastify documentation](https://www.fastify.io/docs/latest/Logging/) for more information.
 
@@ -201,7 +201,7 @@ npm install nestjs-pino
 ```ts
 import { NestFactory } from '@nestjs/core'
 import { Controller, Get, Module } from '@nestjs/common'
-import { createLoggerMiddlewares, Logger } from 'nestjs-pino'
+import { LoggerModule, Logger } from 'nestjs-pino'
 
 @Controller()
 export class AppController {
@@ -216,13 +216,12 @@ export class AppController {
 
 @Module({
   controllers: [AppController],
-  providers: [Logger]
+  imports: [LoggerModule.forRoot()]
 })
 class MyModule {}
 
 async function bootstrap() {
   const app = await NestFactory.create(MyModule)
-  app.use(...createLoggerMiddlewares())
   await app.listen(3000)
 }
 bootstrap()
diff --git a/index.html b/index.html
index e195a28..587ab17 100644
--- a/index.html
+++ b/index.html
@@ -20,6 +20,9 @@
       margin-bottom: -.75em;
       margin-top: .75em;
     }
+    .sidebar > h1 img {
+      height: 4em;
+    }
     .markdown-section a code {
       color: var(--link-color)!important;
     }
@@ -33,7 +36,8 @@
 </body>
 <script>
   window.$docsify = {
-    name: '<img src=pino-tree.png style="height:4em;"><div style="margin-top:-.65em">pino</div>',
+    name: 'pino',
+    logo: './pino-tree.png',
     loadSidebar: 'docsify/sidebar.md',
     repo: 'https://github.com/pinojs/pino',
     auto2top: true,
diff --git a/lib/levels.js b/lib/levels.js
index ea8c33b..0f60f7b 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -1,6 +1,13 @@
 'use strict'
 const flatstr = require('flatstr')
-const { lsCacheSym, levelValSym, useLevelLabelsSym, changeLevelNameSym, useOnlyCustomLevelsSym } = require('./symbols')
+const {
+  lsCacheSym,
+  levelValSym,
+  useLevelLabelsSym,
+  levelKeySym,
+  useOnlyCustomLevelsSym,
+  streamSym
+} = require('./symbols')
 const { noop, genLog } = require('./tools')
 
 const levels = {
@@ -11,9 +18,19 @@ const levels = {
   error: 50,
   fatal: 60
 }
-
+const logFatal = genLog(levels.fatal)
 const levelMethods = {
-  fatal: genLog(levels.fatal),
+  fatal (...args) {
+    const stream = this[streamSym]
+    logFatal.call(this, ...args)
+    if (typeof stream.flushSync === 'function') {
+      try {
+        stream.flushSync()
+      } catch (e) {
+        // https://github.com/pinojs/pino/pull/740#discussion_r346788313
+      }
+    }
+  },
   error: genLog(levels.error),
   warn: genLog(levels.warn),
   info: genLog(levels.info),
@@ -32,7 +49,7 @@ const initialLsCache = Object.keys(nums).reduce((o, k) => {
 }, {})
 
 function genLsCache (instance) {
-  const levelName = instance[changeLevelNameSym]
+  const levelName = instance[levelKeySym]
   instance[lsCacheSym] = Object.keys(instance.levels.labels).reduce((o, k) => {
     o[k] = instance[useLevelLabelsSym]
       ? `{"${levelName}":"${instance.levels.labels[k]}"`
diff --git a/lib/proto.js b/lib/proto.js
index 8d71d99..5099727 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -11,6 +11,7 @@ const {
   setLevelSym,
   getLevelSym,
   chindingsSym,
+  mixinSym,
   asJsonSym,
   messageKeySym,
   writeSym,
@@ -46,6 +47,7 @@ const prototype = {
   constructor,
   child,
   bindings,
+  setBindings,
   flush,
   isLevelEnabled,
   version,
@@ -112,17 +114,23 @@ function bindings () {
   return bindingsFromJson
 }
 
+function setBindings (newBindings) {
+  const chindings = asChindings(this, newBindings)
+  this[chindingsSym] = chindings
+}
+
 function write (_obj, msg, num) {
   const t = this[timeSym]()
   const messageKey = this[messageKeySym]
+  const mixin = this[mixinSym]
   const objError = _obj instanceof Error
   var obj
 
   if (_obj === undefined || _obj === null) {
-    obj = {}
+    obj = mixin ? mixin() : {}
     obj[messageKey] = msg
   } else {
-    obj = Object.assign({}, _obj)
+    obj = Object.assign(mixin ? mixin() : {}, _obj)
     if (msg) {
       obj[messageKey] = msg
     } else if (objError) {
@@ -137,7 +145,7 @@ function write (_obj, msg, num) {
     }
   }
 
-  const s = this[asJsonSym](obj, num, t, objError)
+  const s = this[asJsonSym](obj, num, t)
 
   const stream = this[streamSym]
   if (stream[needsMetadataGsym] === true) {
diff --git a/lib/redaction.js b/lib/redaction.js
index 771faed..c1dd721 100644
--- a/lib/redaction.js
+++ b/lib/redaction.js
@@ -19,29 +19,31 @@ function redaction (opts, serialize) {
     rx.lastIndex = 0
     const first = rx.exec(str)
     const next = rx.exec(str)
-    // top level key:
-    if (next === null) {
-      if (str === '*') {
-        str = wildcardFirstSym
-      }
-      o[str] = null
-      return o
-    }
-    const { index } = next
-    const nextPath = `${str.substr(index, str.length - 1)}`
-    var ns = first[1]
-      ? first[1].replace(/^(?:"|'|`)(.+)(?:"|'|`)$/, '$1')
+
+    // ns is the top-level path segment, brackets + quoting removed.
+    let ns = first[1] !== undefined
+      ? first[1].replace(/^(?:"|'|`)(.*)(?:"|'|`)$/, '$1')
       : first[0]
 
     if (ns === '*') {
       ns = wildcardFirstSym
     }
 
+    // top level key:
+    if (next === null) {
+      o[ns] = null
+      return o
+    }
+
+    // path with at least two segments:
     // if ns is already redacted at the top level, ignore lower level redactions
     if (o[ns] === null) {
       return o
     }
 
+    const { index } = next
+    const nextPath = `${str.substr(index, str.length - 1)}`
+
     o[ns] = o[ns] || []
 
     // shape is a mix of paths beginning with literal values and wildcard
diff --git a/lib/symbols.js b/lib/symbols.js
index 38c3609..13d3662 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -4,8 +4,9 @@ const setLevelSym = Symbol('pino.setLevel')
 const getLevelSym = Symbol('pino.getLevel')
 const levelValSym = Symbol('pino.levelVal')
 const useLevelLabelsSym = Symbol('pino.useLevelLabels')
-const changeLevelNameSym = Symbol('pino.changeLevelName')
+const levelKeySym = Symbol('pino.levelKey')
 const useOnlyCustomLevelsSym = Symbol('pino.useOnlyCustomLevels')
+const mixinSym = Symbol('pino.mixin')
 
 const lsCacheSym = Symbol('pino.lsCache')
 const chindingsSym = Symbol('pino.chindings')
@@ -23,6 +24,7 @@ const stringifiersSym = Symbol('pino.stringifiers')
 const endSym = Symbol('pino.end')
 const formatOptsSym = Symbol('pino.formatOpts')
 const messageKeySym = Symbol('pino.messageKey')
+const nestedKeySym = Symbol('pino.nestedKey')
 
 const wildcardFirstSym = Symbol('pino.wildcardFirst')
 
@@ -37,6 +39,7 @@ module.exports = {
   getLevelSym,
   levelValSym,
   useLevelLabelsSym,
+  mixinSym,
   lsCacheSym,
   chindingsSym,
   parsedChindingsSym,
@@ -52,8 +55,9 @@ module.exports = {
   endSym,
   formatOptsSym,
   messageKeySym,
+  nestedKeySym,
   wildcardFirstSym,
-  changeLevelNameSym,
+  levelKeySym,
   wildcardGsym,
   needsMetadataGsym,
   useOnlyCustomLevelsSym
diff --git a/lib/time.js b/lib/time.js
index 104c440..420a028 100644
--- a/lib/time.js
+++ b/lib/time.js
@@ -6,4 +6,6 @@ const epochTime = () => `,"time":${Date.now()}`
 
 const unixTime = () => `,"time":${Math.round(Date.now() / 1000.0)}`
 
-module.exports = { nullTime, epochTime, unixTime }
+const isoTime = () => `,"time":"${new Date(Date.now()).toISOString()}"` // using Date.now() for testability
+
+module.exports = { nullTime, epochTime, unixTime, isoTime }
diff --git a/lib/tools.js b/lib/tools.js
index 586c84d..22aa0d3 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -20,7 +20,8 @@ const {
   needsMetadataGsym,
   wildcardGsym,
   redactFmtSym,
-  streamSym
+  streamSym,
+  nestedKeySym
 } = require('./symbols')
 
 function noop () {}
@@ -33,6 +34,7 @@ function genLog (z) {
       } else if (typeof o.setHeader === 'function') {
         o = mapHttpResponse(o)
       }
+      if (this[nestedKeySym]) o = { [this[nestedKeySym]]: o }
       this[writeSym](o, format(null, n, this[formatOptsSym]), z)
     } else this[writeSym](null, format(o, n, this[formatOptsSym]), z)
   }
@@ -105,8 +107,7 @@ function asJson (obj, num, time) {
         // this case explicity falls through to the next one
         case 'boolean':
           if (stringifier) value = stringifier(value)
-          data += ',"' + key + '":' + value
-          continue
+          break
         case 'string':
           value = (stringifier || asString)(value)
           break
@@ -151,8 +152,9 @@ function asChindings (instance, bindings) {
   return data
 }
 
-function getPrettyStream (opts, prettifier, dest) {
+function getPrettyStream (opts, prettifier, dest, instance) {
   if (prettifier && typeof prettifier === 'function') {
+    prettifier = prettifier.bind(instance)
     return prettifierMetaWrapper(prettifier(opts), dest)
   }
   try {
@@ -177,6 +179,7 @@ function prettifierMetaWrapper (pretty, dest) {
         return
       }
       warned = true
+      setMetadataProps(dest, this)
       dest.write(pretty(Object.assign({
         level: 40, // warn
         msg: 'pino.final with prettyPrint does not support flushing',
@@ -236,6 +239,8 @@ function prettifierMetaWrapper (pretty, dest) {
 
       const formatted = pretty(typeof redact === 'function' ? redact(obj) : obj)
       if (formatted === undefined) return
+
+      setMetadataProps(dest, this)
       dest.write(formatted)
     }
   }
@@ -268,7 +273,7 @@ function buildSafeSonicBoom (dest, buffer = 0, sync = true) {
 }
 
 function createArgsNormalizer (defaultOptions) {
-  return function normalizeArgs (opts = {}, stream) {
+  return function normalizeArgs (instance, opts = {}, stream) {
     // support stream as a string
     if (typeof opts === 'string') {
       stream = buildSafeSonicBoom(opts)
@@ -286,6 +291,14 @@ function createArgsNormalizer (defaultOptions) {
     if ('onTerminated' in opts) {
       throw Error('The onTerminated option has been removed, use pino.final instead')
     }
+    if ('changeLevelName' in opts) {
+      process.emitWarning(
+        'The changeLevelName option is deprecated and will be removed in v7. Use levelKey instead.',
+        { code: 'changeLevelName_deprecation' }
+      )
+      opts.levelKey = opts.changeLevelName
+      delete opts.changeLevelName
+    }
     const { enabled, prettyPrint, prettifier, messageKey } = opts
     if (enabled === false) opts.level = 'silent'
     stream = stream || process.stdout
@@ -294,7 +307,7 @@ function createArgsNormalizer (defaultOptions) {
     }
     if (prettyPrint) {
       const prettyOpts = Object.assign({ messageKey }, prettyPrint)
-      stream = getPrettyStream(prettyOpts, prettifier, stream)
+      stream = getPrettyStream(prettyOpts, prettifier, stream, instance)
     }
     return { opts, stream }
   }
@@ -350,6 +363,16 @@ function stringify (obj) {
   }
 }
 
+function setMetadataProps (dest, that) {
+  if (dest[needsMetadataGsym] === true) {
+    dest.lastLevel = that.lastLevel
+    dest.lastMsg = that.lastMsg
+    dest.lastObj = that.lastObj
+    dest.lastTime = that.lastTime
+    dest.lastLogger = that.lastLogger
+  }
+}
+
 module.exports = {
   noop,
   buildSafeSonicBoom,
diff --git a/package.json b/package.json
index 4822cc1..af81666 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pino",
-  "version": "5.13.4",
+  "version": "5.16.0",
   "description": "super fast, all natural json logger",
   "main": "pino.js",
   "browser": "./browser.js",
@@ -20,6 +20,7 @@
     "browser-test": "airtap --local 8080 test/browser*test.js",
     "test": "standard | snazzy && cross-env NODE_OPTIONS=\"--no-warnings -r qodaa\" tap --no-esm -j 4 --no-cov test/*test.js",
     "ci": "standard | snazzy && cross-env TAP_TIMEOUT=480000 NODE_OPTIONS=\"--no-warnings -r qodaa\" tap --no-esm -j 4 --100 test/*test.js",
+    "cov-ci": "cross-env TAP_TIMEOUT=480000 NODE_OPTIONS=\"--no-warnings -r qodaa\" tap --no-esm -j 4 --100 --coverage-report=lcov test/*test.js",
     "cov-ui": "cross-env NODE_OPTIONS=\"--no-warnings -r qodaa\" tap --no-esm -j 4 --coverage-report=html test/*test.js",
     "bench": "node benchmarks/utils/runbench all",
     "bench-basic": "node benchmarks/utils/runbench basic",
@@ -70,25 +71,26 @@
     "import-fresh": "^3.0.0",
     "log": "^5.0.0",
     "loglevel": "^1.6.4",
-    "pino-pretty": "^2.4.0",
+    "pino-pretty": "^2.6.1",
     "pre-commit": "^1.2.2",
     "proxyquire": "^2.1.3",
     "pump": "^3.0.0",
     "qodaa": "^1.0.1",
+    "semver": "^6.3.0",
     "snazzy": "^8.0.0",
     "split2": "^3.1.1",
     "standard": "^14.2.0",
     "steed": "^1.1.3",
-    "tap": "^12.6.0",
-    "tape": "^4.10.1",
+    "tap": "^12.7.0",
+    "tape": "^4.11.0",
     "through2": "^3.0.1",
     "winston": "^3.2.1"
   },
   "dependencies": {
     "fast-redact": "^2.0.0",
     "fast-safe-stringify": "^2.0.7",
-    "flatstr": "^1.0.9",
-    "pino-std-serializers": "^2.3.0",
+    "flatstr": "^1.0.12",
+    "pino-std-serializers": "^2.4.2",
     "quick-format-unescaped": "^3.0.3",
     "sonic-boom": "^0.7.5"
   }
diff --git a/pino.js b/pino.js
index 14ad86c..4138069 100644
--- a/pino.js
+++ b/pino.js
@@ -27,8 +27,10 @@ const {
   endSym,
   formatOptsSym,
   messageKeySym,
+  nestedKeySym,
   useLevelLabelsSym,
-  changeLevelNameSym,
+  levelKeySym,
+  mixinSym,
   useOnlyCustomLevelsSym
 } = symbols
 const { epochTime, nullTime } = time
@@ -39,6 +41,7 @@ const defaultOptions = {
   level: 'info',
   useLevelLabels: false,
   messageKey: 'msg',
+  nestedKey: null,
   enabled: true,
   prettyPrint: false,
   base: { pid, hostname },
@@ -49,7 +52,7 @@ const defaultOptions = {
   name: undefined,
   redact: null,
   customLevels: null,
-  changeLevelName: 'level',
+  levelKey: 'level',
   useOnlyCustomLevels: false
 }
 
@@ -58,19 +61,22 @@ const normalize = createArgsNormalizer(defaultOptions)
 const serializers = Object.assign(Object.create(null), stdSerializers)
 
 function pino (...args) {
-  const { opts, stream } = normalize(...args)
+  const instance = {}
+  const { opts, stream } = normalize(instance, ...args)
   const {
     redact,
     crlf,
     serializers,
     timestamp,
     messageKey,
+    nestedKey,
     base,
     name,
     level,
     customLevels,
     useLevelLabels,
-    changeLevelName,
+    levelKey,
+    mixin,
     useOnlyCustomLevels
   } = opts
 
@@ -92,14 +98,15 @@ function pino (...args) {
   const timeSliceIndex = time().indexOf(':') + 1
 
   if (useOnlyCustomLevels && !customLevels) throw Error('customLevels is required if useOnlyCustomLevels is set true')
+  if (mixin && typeof mixin !== 'function') throw Error(`Unknown mixin type "${typeof mixin}" - expected "function"`)
 
   assertDefaultLevelFound(level, customLevels, useOnlyCustomLevels)
   const levels = mappings(customLevels, useOnlyCustomLevels)
 
-  const instance = {
+  Object.assign(instance, {
     levels,
     [useLevelLabelsSym]: useLevelLabels,
-    [changeLevelNameSym]: changeLevelName,
+    [levelKeySym]: levelKey,
     [useOnlyCustomLevelsSym]: useOnlyCustomLevels,
     [streamSym]: stream,
     [timeSym]: time,
@@ -109,12 +116,14 @@ function pino (...args) {
     [endSym]: end,
     [formatOptsSym]: formatOpts,
     [messageKeySym]: messageKey,
+    [nestedKeySym]: nestedKey,
     [serializersSym]: serializers,
+    [mixinSym]: mixin,
     [chindingsSym]: chindings
-  }
+  })
   Object.setPrototypeOf(instance, proto)
 
-  if (customLevels || useLevelLabels || changeLevelName !== defaultOptions.changeLevelName) genLsCache(instance)
+  if (customLevels || useLevelLabels || levelKey !== defaultOptions.levelKey) genLsCache(instance)
 
   instance[setLevelSym](level)
 


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index a83c6e5..eeb91b6 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -1,14 +1,31 @@
 'use strict'
 const os = require('os')
 const { join } = require('path')
-const { readFileSync } = require('fs')
+const { readFileSync, existsSync, statSync } = require('fs')
 const { test } = require('tap')
 const { sink, check, once } = require('./helper')
 const pino = require('../')
 const { version } = require('../package.json')
 const { pid } = process
 const hostname = os.hostname()
-const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+const watchFileCreated = (filename) => new Promise((resolve, reject) => {
+  const TIMEOUT = 800
+  const INTERVAL = 100
+  const threshold = TIMEOUT / INTERVAL
+  let counter = 0
+  const interval = setInterval(() => {
+    // On some CI runs file is created but not filled
+    if (existsSync(filename) && statSync(filename).size !== 0) {
+      clearInterval(interval)
+      resolve()
+    } else if (counter <= threshold) {
+      counter++
+    } else {
+      clearInterval(interval)
+      reject(new Error(`${filename} was not created.`))
+    }
+  }, INTERVAL)
+})
 
 test('pino version is exposed on export', async ({ is }) => {
   is(pino.version, version)
@@ -34,6 +51,39 @@ test('bindings contain the name and the child bindings', async ({ same }) => {
   same(instance.bindings(), { name: 'basicTest', foo: 'bar', a: 2 })
 })
 
+test('set bindings on instance', async ({ same }) => {
+  const instance = pino({ name: 'basicTest', level: 'info' })
+  instance.setBindings({ foo: 'bar' })
+  same(instance.bindings(), { name: 'basicTest', foo: 'bar' })
+})
+
+test('newly set bindings overwrite old bindings', async ({ same }) => {
+  const instance = pino({ name: 'basicTest', level: 'info', base: { foo: 'bar' } })
+  instance.setBindings({ foo: 'baz' })
+  same(instance.bindings(), { name: 'basicTest', foo: 'baz' })
+})
+
+test('set bindings on child instance', async ({ same }) => {
+  const child = pino({ name: 'basicTest', level: 'info' }).child({})
+  child.setBindings({ foo: 'bar' })
+  same(child.bindings(), { name: 'basicTest', foo: 'bar' })
+})
+
+test('child should have bindings set by parent', async ({ same }) => {
+  const instance = pino({ name: 'basicTest', level: 'info' })
+  instance.setBindings({ foo: 'bar' })
+  const child = instance.child({})
+  same(child.bindings(), { name: 'basicTest', foo: 'bar' })
+})
+
+test('child should not share bindings of parent set after child creation', async ({ same }) => {
+  const instance = pino({ name: 'basicTest', level: 'info' })
+  const child = instance.child({})
+  instance.setBindings({ foo: 'bar' })
+  same(instance.bindings(), { name: 'basicTest', foo: 'bar' })
+  same(child.bindings(), { name: 'basicTest' })
+})
+
 function levelTest (name, level) {
   test(`${name} logs as ${level}`, async ({ is }) => {
     const stream = sink()
@@ -236,6 +286,26 @@ test('set the messageKey', async ({ is, same }) => {
   })
 })
 
+test('set the nestedKey', async ({ is, same }) => {
+  const stream = sink()
+  const object = { hello: 'world' }
+  const nestedKey = 'stuff'
+  const instance = pino({
+    nestedKey
+  }, stream)
+  instance.info(object)
+  const result = await once(stream, 'data')
+  is(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
+  delete result.time
+  same(result, {
+    pid: pid,
+    hostname: hostname,
+    level: 30,
+    stuff: object,
+    v: 1
+  })
+})
+
 test('set undefined properties', async ({ is, same }) => {
   const stream = sink()
   const instance = pino(stream)
@@ -479,7 +549,7 @@ test('pino.destination', async ({ same }) => {
   )
   const instance = pino(pino.destination(tmp))
   instance.info('hello')
-  await sleep(250)
+  await watchFileCreated(tmp)
   const result = JSON.parse(readFileSync(tmp).toString())
   delete result.time
   same(result, {
@@ -498,7 +568,7 @@ test('auto pino.destination with a string', async ({ same }) => {
   )
   const instance = pino(tmp)
   instance.info('hello')
-  await sleep(250)
+  await watchFileCreated(tmp)
   const result = JSON.parse(readFileSync(tmp).toString())
   delete result.time
   same(result, {
@@ -517,7 +587,7 @@ test('auto pino.destination with a string as second argument', async ({ same })
   )
   const instance = pino(null, tmp)
   instance.info('hello')
-  await sleep(250)
+  await watchFileCreated(tmp)
   const result = JSON.parse(readFileSync(tmp).toString())
   delete result.time
   same(result, {
@@ -538,7 +608,7 @@ test('does not override opts with a string as second argument', async ({ same })
     timestamp: () => ',"time":"none"'
   }, tmp)
   instance.info('hello')
-  await sleep(250)
+  await watchFileCreated(tmp)
   const result = JSON.parse(readFileSync(tmp).toString())
   same(result, {
     pid: pid,
diff --git a/test/custom-levels.test.js b/test/custom-levels.test.js
index 283504d..b7d19c3 100644
--- a/test/custom-levels.test.js
+++ b/test/custom-levels.test.js
@@ -251,17 +251,58 @@ test('does not share custom level state across siblings', async ({ doesNotThrow
   })
 })
 
-test('custom level does not affect changeLevelName', async ({ is }) => {
+test('custom level does not affect levelKey', async ({ is }) => {
   const stream = sink()
   const logger = pino({
     customLevels: {
       foo: 35,
       bar: 45
     },
-    changeLevelName: 'priority'
+    levelKey: 'priority'
   }, stream)
 
   logger.foo('test')
   const { priority } = await once(stream, 'data')
   is(priority, 35)
 })
+
+test('custom levels accesible in prettifier function', async ({ plan, same }) => {
+  plan(1)
+  const logger = pino({
+    prettyPrint: true,
+    prettifier: function prettifierFactory () {
+      const instance = this
+      return function () {
+        same(instance.levels, {
+          labels: {
+            10: 'trace',
+            20: 'debug',
+            30: 'info',
+            35: 'foo',
+            40: 'warn',
+            45: 'bar',
+            50: 'error',
+            60: 'fatal'
+          },
+          values: {
+            trace: 10,
+            debug: 20,
+            info: 30,
+            warn: 40,
+            error: 50,
+            fatal: 60,
+            foo: 35,
+            bar: 45
+          }
+        })
+      }
+    },
+    customLevels: {
+      foo: 35,
+      bar: 45
+    },
+    changeLevelName: 'priority'
+  })
+
+  logger.foo('test')
+})
diff --git a/test/http.test.js b/test/http.test.js
index eddcc79..1c2023e 100644
--- a/test/http.test.js
+++ b/test/http.test.js
@@ -2,6 +2,7 @@
 
 const http = require('http')
 const os = require('os')
+const semver = require('semver')
 const { test } = require('tap')
 const { sink, once } = require('./helper')
 const pino = require('../')
@@ -93,7 +94,7 @@ test('http request support via serializer without request connection', async ({
   }, sink((chunk, enc) => {
     ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
     delete chunk.time
-    same(chunk, {
+    const expected = {
       pid: pid,
       hostname: hostname,
       level: 30,
@@ -104,7 +105,12 @@ test('http request support via serializer without request connection', async ({
         url: originalReq.url,
         headers: originalReq.headers
       }
-    })
+    }
+    if (semver.gte(process.version, '13.0.0')) {
+      expected.req.remoteAddress = originalReq.connection.remoteAddress
+      expected.req.remotePort = originalReq.connection.remotePort
+    }
+    same(chunk, expected)
   }))
 
   const server = http.createServer(function (req, res) {
diff --git a/test/levels.test.js b/test/levels.test.js
index 8397e74..db5f321 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -257,12 +257,21 @@ test('resets levels from labels to numbers', async ({ is }) => {
   instance.info('hello world')
 })
 
+test('aliases changeLevelName to levelKey', async ({ is }) => {
+  const instance = pino({ changeLevelName: 'priority' }, sink((result, enc, cb) => {
+    is(result.priority, 30)
+    cb()
+  }))
+
+  instance.info('hello world')
+})
+
 test('changes label naming when told to', async ({ is }) => {
   const expected = [{
     priority: 30,
     msg: 'hello world'
   }]
-  const instance = pino({ changeLevelName: 'priority' }, sink((result, enc, cb) => {
+  const instance = pino({ levelKey: 'priority' }, sink((result, enc, cb) => {
     const current = expected.shift()
     is(result.priority, current.priority)
     is(result.msg, current.msg)
@@ -323,11 +332,11 @@ test('produces labels for custom levels', async ({ is }) => {
   instance.foo('foobar')
 })
 
-test('setting changeLevelName does not affect labels when told to', async ({ is }) => {
+test('setting levelKey does not affect labels when told to', async ({ is }) => {
   const instance = pino(
     {
       useLevelLabels: true,
-      changeLevelName: 'priority'
+      levelKey: 'priority'
     },
     sink((result, enc, cb) => {
       is(result.priority, 'info')
@@ -402,3 +411,31 @@ test('passes when creating a default value that exists in logger levels', async
     level: 30
   })
 })
+
+test('fatal method sync-flushes the destination if sync flushing is available', async ({ pass, doesNotThrow, plan }) => {
+  plan(2)
+  const stream = sink()
+  stream.flushSync = () => {
+    pass('destination flushed')
+  }
+  const instance = pino(stream)
+  instance.fatal('this is fatal')
+  await once(stream, 'data')
+  doesNotThrow(() => {
+    stream.flushSync = undefined
+    instance.fatal('this is fatal')
+  })
+})
+
+test('fatal method should call async when sync-flushing fails', ({ equal, fail, doesNotThrow, plan }) => {
+  plan(2)
+  const messages = [
+    'this is fatal 1'
+  ]
+  const stream = sink((result) => equal(result.msg, messages.shift()))
+  stream.flushSync = () => { throw new Error('Error') }
+  stream.flush = () => fail('flush should be called')
+
+  const instance = pino(stream)
+  doesNotThrow(() => instance.fatal(messages[0]))
+})
diff --git a/test/mixin.test.js b/test/mixin.test.js
new file mode 100644
index 0000000..057d766
--- /dev/null
+++ b/test/mixin.test.js
@@ -0,0 +1,106 @@
+'use strict'
+
+const os = require('os')
+const { test } = require('tap')
+const { sink, once } = require('./helper')
+const pino = require('../')
+
+const { pid } = process
+const hostname = os.hostname()
+const level = 50
+const name = 'error'
+
+test('mixin object is included', async ({ ok, same }) => {
+  let n = 0
+  const stream = sink()
+  const instance = pino({
+    mixin () {
+      return { hello: ++n }
+    }
+  }, stream)
+  instance.level = name
+  instance[name]('test')
+  const result = await once(stream, 'data')
+  ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level,
+    msg: 'test',
+    hello: 1,
+    v: 1
+  })
+})
+
+test('mixin object is new every time', async ({ plan, ok, same }) => {
+  plan(6)
+
+  let n = 0
+  const stream = sink()
+  const instance = pino({
+    mixin () {
+      return { hello: n }
+    }
+  }, stream)
+  instance.level = name
+
+  while (++n < 4) {
+    const msg = `test #${n}`
+    stream.pause()
+    instance[name](msg)
+    stream.resume()
+    const result = await once(stream, 'data')
+    ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
+    delete result.time
+    same(result, {
+      pid,
+      hostname,
+      level,
+      msg,
+      hello: n,
+      v: 1
+    })
+  }
+})
+
+test('mixin object is not called if below log level', async ({ ok }) => {
+  const stream = sink()
+  const instance = pino({
+    mixin () {
+      ok(false, 'should not call mixin function')
+    }
+  }, stream)
+  instance.level = 'error'
+  instance.info('test')
+})
+
+test('mixin object + logged object', async ({ ok, same }) => {
+  const stream = sink()
+  const instance = pino({
+    mixin () {
+      return { foo: 1, bar: 2 }
+    }
+  }, stream)
+  instance.level = name
+  instance[name]({ bar: 3, baz: 4 })
+  const result = await once(stream, 'data')
+  ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level,
+    foo: 1,
+    bar: 3,
+    baz: 4,
+    v: 1
+  })
+})
+
+test('mixin not a function', async ({ throws }) => {
+  const stream = sink()
+  throws(function () {
+    pino({ mixin: 'not a function' }, stream)
+  })
+})
diff --git a/test/pretty.test.js b/test/pretty.test.js
index caf9f62..4017d0c 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -272,3 +272,41 @@ test('works as expected with an object with the msg prop', async ({ isNot }) =>
   await once(child, 'close')
   isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): hello/), null)
 })
+
+test('should not lose stream metadata for streams with `needsMetadataGsym` flag', async ({ isNot }) => {
+  const dest = new Writable({
+    objectMode: true,
+    write () {
+      isNot(typeof this.lastLevel === 'undefined', true)
+      isNot(typeof this.lastMsg === 'undefined', true)
+      isNot(typeof this.lastObj === 'undefined', true)
+      isNot(typeof this.lastTime === 'undefined', true)
+      isNot(typeof this.lastLogger === 'undefined', true)
+    }
+  })
+
+  dest[pino.symbols.needsMetadataGsym] = true
+
+  const log = pino({
+    prettyPrint: true
+  }, dest)
+  log.info('foo')
+})
+
+test('should not add stream metadata for streams without `needsMetadataGsym` flag', async ({ is }) => {
+  const dest = new Writable({
+    objectMode: true,
+    write () {
+      is(typeof this.lastLevel === 'undefined', true)
+      is(typeof this.lastMsg === 'undefined', true)
+      is(typeof this.lastObj === 'undefined', true)
+      is(typeof this.lastTime === 'undefined', true)
+      is(typeof this.lastLogger === 'undefined', true)
+    }
+  })
+
+  const log = pino({
+    prettyPrint: true
+  }, dest)
+  log.info('foo')
+})
diff --git a/test/redact.test.js b/test/redact.test.js
index 30cbfda..be27cc1 100644
--- a/test/redact.test.js
+++ b/test/redact.test.js
@@ -240,7 +240,7 @@ test('redact.remove option – removes both key and value', async ({ is }) => {
   is('cookie' in req.headers, false)
 })
 
-test('redact.remove – top level key', async ({ is }) => {
+test('redact.remove – top level key - object value', async ({ is }) => {
   const stream = sink()
   const instance = pino({ redact: { paths: ['key'], remove: true } }, stream)
   instance.info({
@@ -250,6 +250,26 @@ test('redact.remove – top level key', async ({ is }) => {
   is('key' in o, false)
 })
 
+test('redact.remove – top level key - number value', async ({ is }) => {
+  const stream = sink()
+  const instance = pino({ redact: { paths: ['key'], remove: true } }, stream)
+  instance.info({
+    key: 1
+  })
+  const o = await once(stream, 'data')
+  is('key' in o, false)
+})
+
+test('redact.remove – top level key - boolean value', async ({ is }) => {
+  const stream = sink()
+  const instance = pino({ redact: { paths: ['key'], remove: true } }, stream)
+  instance.info({
+    key: false
+  })
+  const o = await once(stream, 'data')
+  is('key' in o, false)
+})
+
 test('redact.remove – top level key in child logger', async ({ is }) => {
   const stream = sink()
   const opts = { redact: { paths: ['key'], remove: true } }
@@ -626,6 +646,17 @@ test('supports bracket notation with further nesting', async ({ is }) => {
   is(o.a['b.b'].c, '[Redacted]')
 })
 
+test('supports bracket notation with empty string as path segment', async ({ is }) => {
+  const stream = sink()
+  const instance = pino({ redact: ['a[""].c'] }, stream)
+  const obj = {
+    a: { '': { c: 'd' } }
+  }
+  instance.info(obj)
+  const o = await once(stream, 'data')
+  is(o.a[''].c, '[Redacted]')
+})
+
 test('supports leading bracket notation (single quote)', async ({ is }) => {
   const stream = sink()
   const instance = pino({ redact: ['[\'a.a\'].b'] }, stream)
@@ -658,3 +689,25 @@ test('supports leading bracket notation (backtick quote)', async ({ is }) => {
   const o = await once(stream, 'data')
   is(o['a.a'].b, '[Redacted]')
 })
+
+test('supports leading bracket notation (single-segment path)', async ({ is }) => {
+  const stream = sink()
+  const instance = pino({ redact: ['[`a.a`]'] }, stream)
+  const obj = {
+    'a.a': { b: 'c' }
+  }
+  instance.info(obj)
+  const o = await once(stream, 'data')
+  is(o['a.a'], '[Redacted]')
+})
+
+test('supports leading bracket notation (single-segment path, wilcard)', async ({ is }) => {
+  const stream = sink()
+  const instance = pino({ redact: ['[*]'] }, stream)
+  const obj = {
+    'a.a': { b: 'c' }
+  }
+  instance.info(obj)
+  const o = await once(stream, 'data')
+  is(o['a.a'], '[Redacted]')
+})
diff --git a/test/timestamp.test.js b/test/timestamp.test.js
index 89e7843..87bdc09 100644
--- a/test/timestamp.test.js
+++ b/test/timestamp.test.js
@@ -11,6 +11,7 @@ test('pino exposes standard time functions', async ({ ok }) => {
   ok(pino.stdTimeFunctions.epochTime)
   ok(pino.stdTimeFunctions.unixTime)
   ok(pino.stdTimeFunctions.nullTime)
+  ok(pino.stdTimeFunctions.isoTime)
 })
 
 test('pino accepts external time functions', async ({ is }) => {
@@ -101,3 +102,20 @@ test('pino.stdTimeFunctions.unixTime returns seconds based timestamps', async ({
   is(result.time, 1531069920)
   Date.now = now
 })
+
+test('pino.stdTimeFunctions.isoTime returns ISO 8601 timestamps', async ({ is }) => {
+  const opts = {
+    timestamp: pino.stdTimeFunctions.isoTime
+  }
+  const stream = sink()
+  const instance = pino(opts, stream)
+  const ms = 1531069919686
+  const now = Date.now
+  Date.now = () => ms
+  const iso = new Date(ms).toISOString()
+  instance.info('foobar')
+  const result = await once(stream, 'data')
+  is(result.hasOwnProperty('time'), true)
+  is(result.time, iso)
+  Date.now = now
+})