Repository: pinojs/pino
Pull request #800 (2020-03-27 11:44:00)

v6 release merge


Patch:
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
new file mode 100644
index 0000000..ba11a5f
--- /dev/null
+++ b/.github/workflows/bench.yml
@@ -0,0 +1,47 @@
+name: Benchmarks
+on:
+  - push
+  - pull_request
+
+jobs:
+  benchmark_current:
+    name: benchmark current
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v2
+        with:
+          ref: ${{ github.base_ref }}
+      - name: Setup Node
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+      - name: Install Modules
+        run: npm i
+      - name: Run Benchmark
+        run: npm run bench | tee current.txt
+      - name: Upload Current Results
+        uses: actions/upload-artifact@v1
+        with:
+          name: current
+          path: current.txt
+
+  benchmark_branch:
+    name: benchmark branch
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v2
+      - name: Setup Node
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+      - name: Install Modules
+        run: npm i
+      - name: Run Benchmark
+        run: npm run bench | tee branch.txt
+      - name: Upload Branch Results
+        uses: actions/upload-artifact@v1
+        with:
+          name: branch
+          path: branch.txt
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ce34d1b..4e0a9c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,16 +1,16 @@
 name: CI
 on: [push, pull_request]
 jobs:
-  build:
+  test:
     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]
+        node-version: [10, 12, 13]
     steps:
-      - uses: actions/checkout@v1
+      - uses: actions/checkout@v2
       - name: Use Node.js ${{ matrix.node-version }}
         uses: actions/setup-node@v1
         with:
@@ -18,21 +18,4 @@ jobs:
       - 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 }}
+        run: npm test
diff --git a/.taprc b/.taprc
new file mode 100644
index 0000000..b293983
--- /dev/null
+++ b/.taprc
@@ -0,0 +1,5 @@
+esm: false
+ts: false
+jsx: false
+coverage: true
+timeout: 480
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 3aa2522..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-language: node_js
-sudo: false
-node_js:
-  - 6
-  - 8
-  - 10
-  - 11
-  - 12
-  - 13
-script:
-  - npm run ci
-env:
-  global:
-    - secure: WeX/nE4FbSZkldDQHyHQruDRbJGbCDmerFqVC1Lbx7RYz5pU5pLraFRUWPUcSguq7Pt7iem7iOADtEqoIuurb0tttc91ilVIOTvtICixATFpRsiuilMqEo5YKNKwmsTiJzOnaoMEnxWOZ8WK+mVBN5h1qTGzg1gEIXBU8wjy1eSQ2+GEGC/NvtKjMwn3ew5XXqr5w7sU7AvoVBBJkUBucG09DVxIW9iB+Pz+k1kCcaCWunZ3r1tSOaIZoKCIW6SZCy2XD0ns0GKdBw+AygxVqDN5LpSj9EJ27wQpkpMcdFnpOyUoijHuqa8d8mnofWrkd3UQyIi1bz/Dq2wmSfLHfxb9+U/iWSi9q3ed2YfeX4+Q3of80onucqD1gTzjwZU+KZi9XSlVEgKEv+WwmTPE+okURGsaONCB3QLzgTo27cv/ACe8GU4UhomkrmDES8y6P1Zj49Np9GOOkYMzerUyV++qaTF6ugi8Z1k93/Put7EaESVmZJxxrtuhm70Dq96Eftrag9ypByYEgRYVKRqL7840bBb+I8J5pC8+uS5xBcusI9amYv01qdqP43F3f3XvRI4KHEG/OO4TfTSmt1htiDKKnciC2/6K6766y3wCOzxEeD2XPie4VkGsSTMJegxkPH37imogXJ/p4Ket7pprZXDnkEsiMn1b+CgTSaNb8ms=
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index acb7f2b..eaa4b37 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,6 +13,7 @@ There are a few basic ground-rules for contributors:
 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors.
 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor.
 1. Contributors should attempt to adhere to the prevailing code-style.
+1. 100% code coverage
 
 ## Releases
 
diff --git a/README.md b/README.md
index 04a4070..dccb7a8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
 ![banner](pino-banner.png)
 
-# pino&nbsp;&nbsp;[![Build Status](https://travis-ci.org/pinojs/pino.svg?branch=master)](https://travis-ci.org/pinojs/pino)&nbsp;[![Coverage Status](https://coveralls.io/repos/github/pinojs/pino/badge.svg?branch=master)](https://coveralls.io/github/pinojs/pino?branch=master) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![TypeScript definitions on DefinitelyTyped](https://definitelytyped.org/badges/standard.svg)](https://definitelytyped.org)
+# pino
+[![Build Status](https://img.shields.io/github/workflow/status/pinojs/pino/CI)](https://github.com/pinojs/pino/actions)
+&nbsp;[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
+&nbsp;[![TypeScript definitions on DefinitelyTyped](https://img.shields.io/badge/DefinitelyTyped-.d.ts-brightgreen.svg?style=flat)](https://definitelytyped.org)
 
 [Very low overhead](#low-overhead) Node.js logger, inspired by Bunyan.
 
@@ -18,6 +21,7 @@
 * [Ecosystem ⇗](/docs/ecosystem.md)
 * [Legacy](/docs/legacy.md)
 * [Help ⇗](/docs/help.md)
+* [Long Term Support Policy ⇗](/docs/lts.md)
 
 ## Install
 
@@ -39,8 +43,8 @@ child.info('hello child!')
 This produces:
 
 ```
-{"level":30,"time":1531171074631,"msg":"hello world","pid":657,"hostname":"Davids-MBP-3.fritz.box","v":1}
-{"level":30,"time":1531171082399,"msg":"hello child!","pid":657,"hostname":"Davids-MBP-3.fritz.box","a":"property","v":1}
+{"level":30,"time":1531171074631,"msg":"hello world","pid":657,"hostname":"Davids-MBP-3.fritz.box"}
+{"level":30,"time":1531171082399,"msg":"hello child!","pid":657,"hostname":"Davids-MBP-3.fritz.box","a":"property"}
 ```
 
 For using Pino with a web framework see:
diff --git a/benchmarks/formatters.bench.js b/benchmarks/formatters.bench.js
new file mode 100644
index 0000000..e6cc861
--- /dev/null
+++ b/benchmarks/formatters.bench.js
@@ -0,0 +1,50 @@
+'use strict'
+
+const formatters = {
+  level (label, number) {
+    return {
+      log: {
+        level: label
+      }
+    }
+  },
+  bindings (bindings) {
+    return {
+      process: {
+        pid: bindings.pid
+      },
+      host: {
+        name: bindings.hostname
+      }
+    }
+  },
+  log (obj) {
+    return { foo: 'bar', ...obj }
+  }
+}
+
+const bench = require('fastbench')
+const pino = require('../')
+delete require.cache[require.resolve('../')]
+const pinoNoFormatters = require('../')(pino.destination('/dev/null'))
+delete require.cache[require.resolve('../')]
+const pinoFormatters = require('../')({ formatters }, pino.destination('/dev/null'))
+
+const max = 10
+
+const run = bench([
+  function benchPinoNoFormatters (cb) {
+    for (var i = 0; i < max; i++) {
+      pinoNoFormatters.info({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchPinoFormatters (cb) {
+    for (var i = 0; i < max; i++) {
+      pinoFormatters.info({ hello: 'world' })
+    }
+    setImmediate(cb)
+  }
+], 10000)
+
+run(run)
diff --git a/benchmarks/utils/runbench.js b/benchmarks/utils/runbench.js
index d9660ac..4503ba8 100644
--- a/benchmarks/utils/runbench.js
+++ b/benchmarks/utils/runbench.js
@@ -22,6 +22,7 @@ function usage () {
     ・child      ⁃ child from a parent
     ・child-child ⁃ child from a child
     ・child-creation ⁃ child constructor
+    ・formatters ⁃ difference between with or without formatters
 
     Example:
 
@@ -46,7 +47,8 @@ const benchmarks = {
   'long-string': 'long-string.bench.js',
   child: 'child.bench.js',
   'child-child': 'child-child.bench.js',
-  'child-creation': 'child-creation.bench.js'
+  'child-creation': 'child-creation.bench.js',
+  formatters: 'formatters.bench.js'
 }
 
 function runBenchmark (name, done) {
diff --git a/browser.js b/browser.js
index da8d9cc..a9a1a30 100644
--- a/browser.js
+++ b/browser.js
@@ -140,8 +140,6 @@ function pino (opts) {
   return logger
 }
 
-pino.LOG_VERSION = 1
-
 pino.levels = {
   values: {
     fatal: 60,
diff --git a/docs/api.md b/docs/api.md
index bbd57dd..c246bd0 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -20,16 +20,13 @@
   * [logger\[Symbol.for('pino.serializers')\]](#serializers)
   * [Event: 'level-change'](#level-change)
   * [logger.version](#version)
-  * [logger.LOG_VERSION](#log_version)
 * [Statics](#statics)
   * [pino.destination()](#pino-destination)
-  * [pino.extreme()](#pino-extreme)
   * [pino.final()](#pino-final)
   * [pino.stdSerializers](#pino-stdserializers)
   * [pino.stdTimeFunctions](#pino-stdtimefunctions)
   * [pino.symbols](#pino-symbols)
   * [pino.version](#pino-version)
-  * [pino.LOG_VERSION](#pino-LOG_VERSION)
 
 <a id="export"></a>
 ## `pino([options], [destination]) => logger`
@@ -140,6 +137,58 @@ If an object is supplied, three options can be specified:
 * See the [redaction ⇗](/docs/redaction.md) documentation.
 * See [fast-redact#caveat ⇗](http://github.com/davidmarkclements/fast-redact#caveat)
 
+<a id=opt-formatters></a>
+#### `formatters` (Object)
+
+An object containing functions for formatting the shape of the log lines.
+These functions should return a JSONifiable object and
+should never throw. These functions allow for full customization of
+the resulting log lines. For example, they can be used to change
+the level key name or to enrich the default metadata.
+
+##### `level`
+
+Changes the shape of the log level. The default shape is `{ level: number }`.
+The function takes two arguments, the label of the level (e.g. `'info'`)
+and the numeric value (e.g. `30`).
+
+```js
+const formatters = {
+  level (label, number) {
+    return { level: number }
+  }
+}
+```
+
+##### `bindings`
+
+Changes the shape of the bindings. The default shape is `{ pid, hostname }`.
+The function takes a single argument, the bindings object. It will
+be called every time a child logger is created.
+
+```js
+const formatters = {
+  bindings (bindings) {
+    return { pid: bindings.pid, hostname: bindings.hostname }
+  }
+}
+```
+
+##### `log`
+
+Changes the shape of the log object. This function will be called every time
+one of the log methods (such as `.info`) is called. All arguments passed to the
+log method, except the message, will be pass to this function. By default it does
+not change the shape of the log object.
+
+```js
+const formatters = {
+  log (object) {
+    return object
+  }
+}
+```
+
 <a id=opt-serializers></a>
 #### `serializers` (Object)
 
@@ -152,12 +201,9 @@ matching the exact key of a serializer will be serialized using the defined seri
 
 * See [pino.stdSerializers](#pino-stdserializers)
 
-##### `serializers[Symbol.for('pino.*')]` (Function)
+##### `serializers[Symbol.for('pino.*')]` (Function) - DEPRECATED
 
-Default: `undefined`
-
-The `serializers` object may contain a key which is the global symbol: `Symbol.for('pino.*')`.
-This will act upon the complete log object rather than corresponding to a particular key.
+Use `formatters.log` instead.
 
 #### `base` (Object)
 
@@ -246,30 +292,18 @@ npm install pino-pretty
 ```
 
 <a id="useLevelLabels"></a>
-#### `useLevelLabels` (Boolean)
+#### `useLevelLabels` (Boolean) - DEPRECATED
 
-Default: `false`
-
-Enables printing of level labels instead of level values in the printed logs.
-Warning: this option may not be supported by downstream transports.
+Use `formatters.level` instead. This will be removed in v7.
 
 <a id="changeLevelName"></a>
 #### `changeLevelName` (String) - DEPRECATED
-Use `levelKey` instead. This will be removed in v7.
+Use `formatters.level` instead. This will be removed in v7.
 
 <a id="levelKey"></a>
-#### `levelKey` (String)
+#### `levelKey` (String) - DEPRECATED
 
-Default: `'level'`
-
-Changes the property `level` to any string value you pass in:
-```js
-const logger = pino({
-  levelKey: 'priority'
-})
-logger.info('hello world')
-// {"priority":30,"time":1531257112193,"msg":"hello world","pid":55956,"hostname":"x","v":1}
-```
+Use `formatters.level` instead. This will be removed in v7.
 
 #### `browser` (Object)
 
@@ -286,7 +320,7 @@ Default: `pino.destination(1)` (STDOUT)
 The `destination` parameter, at a minimum must be an object with a `write` method.
 An ordinary Node.js `stream` can be passed as the destination (such as the result
 of `fs.createWriteStream`) but for peak log writing performance it is strongly
-recommended to use `pino.destination` or `pino.extreme` to create the destination stream.
+recommended to use `pino.destination` to create the destination stream.
 
 ```js
 // pino.destination(1) by default
@@ -310,7 +344,6 @@ However, there are some special instances where `pino.destination` is not used a
 In these cases `process.stdout` is used instead.
 
 * See [`pino.destination`](#pino-destination)
-* See [`pino.extreme`](#pino-extreme)
 
 <a id="metadata"></a>
 #### `destination[Symbol.for('pino.metadata')]`
@@ -373,7 +406,7 @@ of the `mergingObject` is copied in to the JSON log line.
 
 ```js
 logger.info({MIX: {IN: true}})
-// {"level":30,"time":1531254555820,"pid":55956,"hostname":"x","MIX":{"IN":true},"v":1}
+// {"level":30,"time":1531254555820,"pid":55956,"hostname":"x","MIX":{"IN":true}}
 ```
 
 <a id=message></a>
@@ -387,7 +420,7 @@ JSON log line under the `msg` key:
 
 ```js
 logger.info('hello world')
-// {"level":30,"time":1531257112193,"msg":"hello world","pid":55956,"hostname":"x","v":1}
+// {"level":30,"time":1531257112193,"msg":"hello world","pid":55956,"hostname":"x"}
 ```
 
 The `message` parameter takes precedence over the `mergedObject`.
@@ -415,23 +448,22 @@ then be interpolated accordingly.
 #### `...interpolationValues` (Any)
 
 All arguments supplied after `message` are serialized and interpolated according
-to any supplied printf-style placeholders (`%s`, `%d`, `%o`|`%O`|`%j`)
-or else concatenated together with the `message` string to form the final
-output `msg` value for the JSON log line.
+to any supplied printf-style placeholders (`%s`, `%d`, `%o`|`%O`|`%j`) to form
+the final output `msg` value for the JSON log line.
 
 ```js
 logger.info('hello', 'world')
-// {"level":30,"time":1531257618044,"msg":"hello world","pid":55956,"hostname":"x","v":1}
+// {"level":30,"time":1531257618044,"msg":"hello world","pid":55956,"hostname":"x"}
 ```
 
 ```js
 logger.info('hello', {worldly: 1})
-// {"level":30,"time":1531257797727,"msg":"hello {\"worldly\":1}","pid":55956,"hostname":"x","v":1}
+// {"level":30,"time":1531257797727,"msg":"hello {\"worldly\":1}","pid":55956,"hostname":"x"}
 ```
 
 ```js
 logger.info('%o hello', {worldly: 1})
-// {"level":30,"time":1531257826880,"msg":"{\"worldly\":1} hello","pid":55956,"hostname":"x","v":1}
+// {"level":30,"time":1531257826880,"msg":"{\"worldly\":1} hello","pid":55956,"hostname":"x"}
 ```
 
 * See [`message` log method parameter](#message)
@@ -520,9 +552,9 @@ via the returned child logger.
 ```js
 const child = logger.child({ MIX: {IN: 'always'} })
 child.info('hello')
-// {"level":30,"time":1531258616689,"msg":"hello","pid":64849,"hostname":"x","MIX":{"IN":"always"},"v":1}
+// {"level":30,"time":1531258616689,"msg":"hello","pid":64849,"hostname":"x","MIX":{"IN":"always"}}
 child.info('child!')
-// {"level":30,"time":1531258617401,"msg":"child!","pid":64849,"hostname":"x","MIX":{"IN":"always"},"v":1}
+// {"level":30,"time":1531258617401,"msg":"child!","pid":64849,"hostname":"x","MIX":{"IN":"always"}}
 ```
 
 The `bindings` object may contain any key except for reserved configuration keys `level` and `serializers`.
@@ -549,10 +581,10 @@ any configured parent serializers.
 ```js
 const logger = require('pino')()
 logger.info({test: 'will appear'})
-// {"level":30,"time":1531259759482,"pid":67930,"hostname":"x","test":"will appear","v":1}
+// {"level":30,"time":1531259759482,"pid":67930,"hostname":"x","test":"will appear"}
 const child = logger.child({serializers: {test: () => `child-only serializer`}})
 child.info({test: 'will be overwritten'})
-// {"level":30,"time":1531259784008,"pid":67930,"hostname":"x","test":"child-only serializer","v":1}
+// {"level":30,"time":1531259784008,"pid":67930,"hostname":"x","test":"child-only serializer"}
 ```
 
 * See [`serializers` option](#opt-serializers)
@@ -574,19 +606,20 @@ console.log(anotherChild.bindings())
 <a id="flush"></a>
 ### `logger.flush()`
 
-Flushes the content of the buffer when using a `pino.extreme` destination.
+Flushes the content of the buffer when using `pino.destination({
+sync: false })`.
 
 This is an asynchronous, fire and forget, operation.
 
-The use case is primarily for Extreme mode logging, which may hold up to
-4KiB of logs. The `logger.flush` method can be used to flush the logs
+The use case is primarily for asynchronous logging, which may buffer
+log lines while others are being written. The `logger.flush` method can be
+used to flush the logs
 on an long interval, say ten seconds. Such a strategy can provide an
 optimium balance between extremely efficient logging at high demand periods
 and safer logging at low demand periods.
 
-* See [`pino.extreme`](#pino-extreme)
 * See [`destination` parameter](#destination)
-* See [Extreme mode ⇗](/docs/extreme.md)
+* See [Asynchronous Logging ⇗](/docs/asynchronous.md)
 
 <a id="level"></a>
 ### `logger.level` (String) [Getter/Setter]
@@ -706,18 +739,10 @@ Exposes the Pino package version. Also available on the exported `pino` function
 
 * See [`pino.version`](#pino-version)
 
-<a id="log_version"></a>
-### `logger.LOG_VERSION` (Number)
-
-Holds the current log format version as output in the `v` property of each log record.
-Also available on the exported `pino` function.
-
-* See [`pino.LOG_VERSION`](#pino-LOG_VERSION)
-
 ## Statics
 
 <a id="pino-destination"></a>
-### `pino.destination([target]) => SonicBoom`
+### `pino.destination([opts]) => SonicBoom`
 
 Create a Pino Destination instance: a stream-like object with
 significantly more throughput (over 30%) than a standard Node.js stream.
@@ -726,6 +751,11 @@ significantly more throughput (over 30%) than a standard Node.js stream.
 const pino = require('pino')
 const logger = pino(pino.destination('./my-file'))
 const logger2 = pino(pino.destination())
+const logger3 = pino(pino.destination({
+  dest: './my-file',
+  minLength: 4096, // Buffer before writing
+  sync: false // Asynchronous logging
+}))
 ```
 
 The `pino.destination` method may be passed a file path or a numerical file descriptor.
@@ -739,35 +769,7 @@ A `pino.destination` instance can also be used to reopen closed files
 * See [`destination` parameter](#destination)
 * See [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom)
 * See [Reopening log files](/docs/help.md#reopening)
-
-<a id="pino-extreme"></a>
-### `pino.extreme([target]) => SonicBoom`
-
-Create an extreme mode destination. This yields an additional 60% performance boost.
-There are trade-offs that should be understood before usage.
-
-```js
-const pino = require('pino')
-const logger = pino(pino.extreme('./my-file'))
-const logger2 = pino(pino.extreme())
-```
-
-The `pino.extreme` method may be passed a file path or a numerical file descriptor.
-By default, `pino.extreme` will use `process.stdout.fd` (1) as the file descriptor.
-
-`pino.extreme` is implemented with the [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom)
-module.
-
-A `pino.extreme` instance can also be used to reopen closed files
-(for example, for some log rotation scenarios), see [Reopening log files](/docs/help.md#reopening).
-
-On AWS Lambda we recommend to call `extreme.flushSync()` at the end
-of each function execution to avoid losing data.
-
-* See [`destination` parameter](#destination)
-* See [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom)
-* See [Extreme mode ⇗](/docs/extreme.md)
-* See [Reopening log files](/docs/help.md#reopening)
+* See [Asynchronous Logging ⇗](/docs/asynchronous.md)
 
 <a id="pino-final"></a>
 
@@ -859,10 +861,3 @@ for general use.
 Exposes the Pino package version. Also available on the logger instance.
 
 * See [`logger.version`](#version)
-
-<a id="pino-log_version"></a>
-### `pino.LOG_VERSION` (Number)
-
-Holds the current log format version as output in the `v` property of each log record. Also available on the logger instance.
-
-* See [`logger.LOG_VERSION`](#log_version)
diff --git a/docs/extreme.md b/docs/asynchronous.md
similarity index 81%
rename from docs/extreme.md
rename to docs/asynchronous.md
index 791be39..a2adc4e 100644
--- a/docs/extreme.md
+++ b/docs/asynchronous.md
@@ -1,9 +1,10 @@
-# Extreme Mode
+# Asynchronous Logging
 
-In essence, extreme mode enables even faster performance by Pino.
+In essence, asynchronous logging enables even faster performance by Pino.
 
 In Pino's standard mode of operation log messages are directly written to the
-output stream as the messages are generated. Extreme mode works by buffering
+output stream as the messages are generated with a _blocking_ operation.
+Asynchronous logging works by buffering
 log messages and writing them in larger chunks.
 
 ## Caveats
@@ -41,15 +42,20 @@ logging and it is acceptable to potentially lose the most recent logs.
   In the case of `SIGHUP`, we will look to see if any other handlers are
   registered for the event. If not, we will proceed as we do with all other
   signals. If there are more handlers registered than just our own, we will
-  simply flush the extreme mode buffer.
+  simply flush the asynchronous logging buffer.
+
+### AWS Lambda
+
+On AWS Lambda we recommend to call `dest.flushSync()` at the end
+of each function execution to avoid losing data.
 
 ## Usage
 
-The `pino.extreme()` method will provide an Extreme Mode destination.
+The `pino.destination({ sync: false })` method will provide an asynchronous destination.
 
 ```js
 const pino = require('pino')
-const dest = pino.extreme() // logs to stdout with no args
+const dest = pino.destination({ sync: false }) // logs to stdout with no args
 const logger = pino(dest)
 ```
 
@@ -60,7 +66,7 @@ The following strategy can be used to minimize log loss:
 
 ```js
 const pino = require('pino')
-const dest = pino.extreme() // no arguments
+const dest = pino.destination({ sync: false })
 const logger = pino(dest)
 
 // asynchronously flush every 10 seconds to keep the buffer empty
@@ -90,6 +96,6 @@ An extreme destination is an instance of
 buffering.
 
 
-* See [`pino.extreme` api](/docs/api.md#pino-extreme)
+* See [`pino.destination` api](/docs/api.md#pino-destination)
 * See [`pino.final` api](/docs/api.md#pino-final)
 * See [`destination` parameter](/docs/api.md#destination)
diff --git a/docs/child-loggers.md b/docs/child-loggers.md
index c664c85..de9163f 100644
--- a/docs/child-loggers.md
+++ b/docs/child-loggers.md
@@ -68,7 +68,7 @@ pino(pino.destination('./my-log'))
 
 ```sh
 $ cat my-log
-{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":1459534114473,"a":"property","a":"prop","v":1}
+{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":1459534114473,"a":"property","a":"prop"}
 ```
 
 Notice how there's two key's named `a` in the JSON output. The sub-childs properties
@@ -80,7 +80,7 @@ namespace holds the final value assigned to it:
 
 ```sh
 $ cat my-log | node -e "process.stdin.once('data', (line) => console.log(JSON.stringify(JSON.parse(line))))"
-{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":"2016-04-01T18:08:34.473Z","a":"prop","v":1}
+{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":"2016-04-01T18:08:34.473Z","a":"prop"}
 ```
 
 Ultimately the conflict is resolved by taking the last value, which aligns with Bunyans child logging
diff --git a/docs/lts.md b/docs/lts.md
new file mode 100644
index 0000000..2032f3a
--- /dev/null
+++ b/docs/lts.md
@@ -0,0 +1,12 @@
+## Long Term Support
+<a id="lts"></a>
+
+Pino's Long Term Support (LTS) is tied to the
+[Node.js LTS policy](https://github.com/nodejs/Release). Major versions of
+Pino, "X" releases in X.Y.Z of [semantic versioning][semver] nomenclature, will
+be issued as close as possible to the release of a new major LTS release of
+Node.js (subject to the Pino maintainers's capcity). Pino makes no guarantees
+as to the operability of the library on unsupported releases of Node.js.
+
+Pino will not remove support for a deprecated Node.js release via a minor,
+or patch, release of Pino.
diff --git a/docs/redaction.md b/docs/redaction.md
index ea87531..f0c79f9 100644
--- a/docs/redaction.md
+++ b/docs/redaction.md
@@ -27,7 +27,7 @@ logger.info({
 This will output:
 
 ```JSON
-{"level":30,"time":1527777350011,"pid":3186,"hostname":"Davids-MacBook-Pro-3.local","key":"[Redacted]","path":{"to":{"key":"[Redacted]","another":"thing"}},"stuff":{"thats":[{"secret":"[Redacted]","logme":"will be logged"},{"secret":"[Redacted]","logme":"as will this"}]},"v":1}
+{"level":30,"time":1527777350011,"pid":3186,"hostname":"Davids-MacBook-Pro-3.local","key":"[Redacted]","path":{"to":{"key":"[Redacted]","another":"thing"}},"stuff":{"thats":[{"secret":"[Redacted]","logme":"will be logged"},{"secret":"[Redacted]","logme":"as will this"}]}}
 ```
 
 The `redact` option can take an array (as shown in the above example) or
@@ -60,7 +60,7 @@ logger.info({
 This will output:
 
 ```JSON
-{"level":30,"time":1527778563934,"pid":3847,"hostname":"Davids-MacBook-Pro-3.local","key":"**GDPR COMPLIANT**","path":{"to":{"key":"**GDPR COMPLIANT**","another":"thing"}},"stuff":{"thats":[{"secret":"**GDPR COMPLIANT**","logme":"will be logged"},{"secret":"**GDPR COMPLIANT**","logme":"as will this"}]},"v":1}
+{"level":30,"time":1527778563934,"pid":3847,"hostname":"Davids-MacBook-Pro-3.local","key":"**GDPR COMPLIANT**","path":{"to":{"key":"**GDPR COMPLIANT**","another":"thing"}},"stuff":{"thats":[{"secret":"**GDPR COMPLIANT**","logme":"will be logged"},{"secret":"**GDPR COMPLIANT**","logme":"as will this"}]}}
 ```
 
 The `redact.remove` option also allows for the key and value to be removed from output:
@@ -90,7 +90,7 @@ logger.info({
 This will output
 
 ```JSON
-{"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}
+{"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"}]}}
 ```
 
 See [pino options in API](/docs/api.md#redact-array-object) for `redact` API details.
diff --git a/docs/transports.md b/docs/transports.md
index 4e5c64f..cd4f964 100644
--- a/docs/transports.md
+++ b/docs/transports.md
@@ -385,7 +385,7 @@ $ node app.js | pino-syslog | pino-socket -a syslog.example.com
 Example output for the "hello world" log:
 
 ```
-<134>Apr  1 16:44:58 MacBook-Pro-3 none[94473]: {"pid":94473,"hostname":"MacBook-Pro-3","level":30,"msg":"hello world","time":1459529098958,"v":1}
+<134>Apr  1 16:44:58 MacBook-Pro-3 none[94473]: {"pid":94473,"hostname":"MacBook-Pro-3","level":30,"msg":"hello world","time":1459529098958}
 ```
 
 [pino-syslog]: https://www.npmjs.com/package/pino-syslog
diff --git a/example.js b/example.js
index 5e39692..866338e 100644
--- a/example.js
+++ b/example.js
@@ -31,5 +31,6 @@ pino.trace('this is a trace statement')
 pino.debug('this is a "debug" statement with "')
 
 pino.info(new Error('kaboom'))
+pino.info(null)
 
 pino.info(new Error('kaboom'), 'with', 'a', 'message')
diff --git a/lib/levels.js b/lib/levels.js
index 0f60f7b..81e6631 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -1,12 +1,12 @@
 'use strict'
+/* eslint no-prototype-builtins: 0 */
 const flatstr = require('flatstr')
 const {
   lsCacheSym,
   levelValSym,
-  useLevelLabelsSym,
-  levelKeySym,
   useOnlyCustomLevelsSym,
-  streamSym
+  streamSym,
+  formattersSym
 } = require('./symbols')
 const { noop, genLog } = require('./tools')
 
@@ -49,13 +49,14 @@ const initialLsCache = Object.keys(nums).reduce((o, k) => {
 }, {})
 
 function genLsCache (instance) {
-  const levelName = instance[levelKeySym]
-  instance[lsCacheSym] = Object.keys(instance.levels.labels).reduce((o, k) => {
-    o[k] = instance[useLevelLabelsSym]
-      ? `{"${levelName}":"${instance.levels.labels[k]}"`
-      : flatstr(`{"${levelName}":` + Number(k))
-    return o
-  }, Object.assign({}, instance[lsCacheSym]))
+  const formatter = instance[formattersSym].level
+  const { labels } = instance.levels
+  const cache = {}
+  for (const label in labels) {
+    const level = formatter(labels[label], Number(label))
+    cache[label] = JSON.stringify(level).slice(0, -1)
+  }
+  instance[lsCacheSym] = cache
   return instance
 }
 
diff --git a/lib/meta.js b/lib/meta.js
index 21a43e9..6195daa 100644
--- a/lib/meta.js
+++ b/lib/meta.js
@@ -2,6 +2,4 @@
 
 const { version } = require('../package.json')
 
-const LOG_VERSION = 1
-
-module.exports = { version, LOG_VERSION }
+module.exports = { version }
diff --git a/lib/proto.js b/lib/proto.js
index 5099727..8e888a6 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -13,12 +13,12 @@ const {
   chindingsSym,
   mixinSym,
   asJsonSym,
-  messageKeySym,
   writeSym,
   timeSym,
   timeSliceIndexSym,
   streamSym,
   serializersSym,
+  formattersSym,
   useOnlyCustomLevelsSym,
   needsMetadataGsym
 } = require('./symbols')
@@ -33,11 +33,11 @@ const {
 } = require('./levels')
 const {
   asChindings,
-  asJson
+  asJson,
+  buildFormatters
 } = require('./tools')
 const {
-  version,
-  LOG_VERSION
+  version
 } = require('./meta')
 
 // note: use of class is satirical
@@ -59,18 +59,20 @@ const prototype = {
   [writeSym]: write,
   [asJsonSym]: asJson,
   [getLevelSym]: getLevel,
-  [setLevelSym]: setLevel,
-  LOG_VERSION
+  [setLevelSym]: setLevel
 }
 
 Object.setPrototypeOf(prototype, EventEmitter.prototype)
 
 module.exports = prototype
 
+const resetChildingsFormatter = bindings => bindings
 function child (bindings) {
-  const { level } = this
+  if (!bindings) {
+    throw Error('missing bindings for child Pino')
+  }
   const serializers = this[serializersSym]
-  const chindings = asChindings(this, bindings)
+  const formatters = this[formattersSym]
   const instance = Object.create(this)
   if (bindings.hasOwnProperty('serializers') === true) {
     instance[serializersSym] = Object.create(null)
@@ -93,13 +95,27 @@ function child (bindings) {
       instance[serializersSym][bks] = bindings.serializers[bks]
     }
   } else instance[serializersSym] = serializers
+  if (bindings.hasOwnProperty('formatters')) {
+    const { level, bindings: chindings, log } = bindings.formatters
+    instance[formattersSym] = buildFormatters(
+      level || formatters.level,
+      chindings || resetChildingsFormatter,
+      log || formatters.log
+    )
+  } else {
+    instance[formattersSym] = buildFormatters(
+      formatters.level,
+      resetChildingsFormatter,
+      formatters.log
+    )
+  }
   if (bindings.hasOwnProperty('customLevels') === true) {
     assertNoLevelCollisions(this.levels, bindings.customLevels)
     instance.levels = mappings(bindings.customLevels, instance[useOnlyCustomLevelsSym])
     genLsCache(instance)
   }
-  instance[chindingsSym] = chindings
-  const childLevel = bindings.level || level
+  instance[chindingsSym] = asChindings(instance, bindings)
+  const childLevel = bindings.level || this.level
   instance[setLevelSym](childLevel)
 
   return instance
@@ -121,20 +137,16 @@ function setBindings (newBindings) {
 
 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 = mixin ? mixin() : {}
-    obj[messageKey] = msg
   } else {
     obj = Object.assign(mixin ? mixin() : {}, _obj)
-    if (msg) {
-      obj[messageKey] = msg
-    } else if (objError) {
-      obj[messageKey] = _obj.message
+    if (!msg && objError) {
+      msg = _obj.message
     }
 
     if (objError) {
@@ -145,14 +157,11 @@ function write (_obj, msg, num) {
     }
   }
 
-  const s = this[asJsonSym](obj, num, t)
+  const s = this[asJsonSym](obj, msg, num, t)
 
   const stream = this[streamSym]
   if (stream[needsMetadataGsym] === true) {
     stream.lastLevel = num
-    // TODO remove in the next major release,
-    // it is not needed anymore
-    stream.lastMsg = msg
     stream.lastObj = obj
     stream.lastTime = t.slice(this[timeSliceIndexSym])
     stream.lastLogger = this // for child loggers
diff --git a/lib/symbols.js b/lib/symbols.js
index 13d3662..1480ce7 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -4,7 +4,6 @@ const setLevelSym = Symbol('pino.setLevel')
 const getLevelSym = Symbol('pino.getLevel')
 const levelValSym = Symbol('pino.levelVal')
 const useLevelLabelsSym = Symbol('pino.useLevelLabels')
-const levelKeySym = Symbol('pino.levelKey')
 const useOnlyCustomLevelsSym = Symbol('pino.useOnlyCustomLevels')
 const mixinSym = Symbol('pino.mixin')
 
@@ -31,7 +30,7 @@ const wildcardFirstSym = Symbol('pino.wildcardFirst')
 // public symbols, no need to use the same pino
 // version for these
 const serializersSym = Symbol.for('pino.serializers')
-const wildcardGsym = Symbol.for('pino.*')
+const formattersSym = Symbol.for('pino.formatters')
 const needsMetadataGsym = Symbol.for('pino.metadata')
 
 module.exports = {
@@ -57,8 +56,7 @@ module.exports = {
   messageKeySym,
   nestedKeySym,
   wildcardFirstSym,
-  levelKeySym,
-  wildcardGsym,
   needsMetadataGsym,
-  useOnlyCustomLevelsSym
+  useOnlyCustomLevelsSym,
+  formattersSym
 }
diff --git a/lib/tools.js b/lib/tools.js
index 22aa0d3..abb1dbc 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -18,25 +18,38 @@ const {
   stringifySym,
   wildcardFirstSym,
   needsMetadataGsym,
-  wildcardGsym,
   redactFmtSym,
   streamSym,
-  nestedKeySym
+  nestedKeySym,
+  formattersSym,
+  messageKeySym
 } = require('./symbols')
 
 function noop () {}
 
 function genLog (z) {
   return function LOG (o, ...n) {
-    if (typeof o === 'object' && o !== null) {
-      if (o.method && o.headers && o.socket) {
-        o = mapHttpRequest(o)
-      } else if (typeof o.setHeader === 'function') {
-        o = mapHttpResponse(o)
+    if (typeof o === 'object') {
+      var msg = o
+      if (o !== null) {
+        if (o.method && o.headers && o.socket) {
+          o = mapHttpRequest(o)
+        } 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)
+      var formatParams
+      if (msg === null && n.length === 0) {
+        formatParams = [null]
+      } else {
+        msg = n.shift()
+        formatParams = n
+      }
+      this[writeSym](o, format(msg, formatParams, this[formatOptsSym]), z)
+    } else {
+      this[writeSym](null, format(o, n, this[formatOptsSym]), z)
+    }
   }
 }
 
@@ -70,12 +83,14 @@ function asString (str) {
   return point < 32 ? JSON.stringify(str) : '"' + result + '"'
 }
 
-function asJson (obj, num, time) {
+function asJson (obj, msg, num, time) {
   const stringify = this[stringifySym]
   const stringifiers = this[stringifiersSym]
   const end = this[endSym]
   const chindings = this[chindingsSym]
   const serializers = this[serializersSym]
+  const formatters = this[formattersSym]
+  const messageKey = this[messageKeySym]
   var data = this[lsCacheSym][num] + time
 
   // we need the child bindings added to the output first so instance logged
@@ -84,8 +99,11 @@ function asJson (obj, num, time) {
 
   var value
   var notHasOwnProperty = obj.hasOwnProperty === undefined
-  if (serializers[wildcardGsym]) {
-    obj = serializers[wildcardGsym](obj)
+  if (formatters.log) {
+    obj = formatters.log(obj)
+  }
+  if (msg !== undefined) {
+    obj[messageKey] = msg
   }
   const wildcardStringifier = stringifiers[wildcardFirstSym]
   for (var key in obj) {
@@ -123,22 +141,20 @@ function asJson (obj, num, time) {
 }
 
 function asChindings (instance, bindings) {
-  if (!bindings) {
-    throw Error('missing bindings for child Pino')
-  }
   var key
   var value
   var data = instance[chindingsSym]
   const stringify = instance[stringifySym]
   const stringifiers = instance[stringifiersSym]
   const serializers = instance[serializersSym]
-  if (serializers[wildcardGsym]) {
-    bindings = serializers[wildcardGsym](bindings)
-  }
+  const formatter = instance[formattersSym].bindings
+  bindings = formatter(bindings)
+
   for (key in bindings) {
     value = bindings[key]
     const valid = key !== 'level' &&
       key !== 'serializers' &&
+      key !== 'formatters' &&
       key !== 'customLevels' &&
       bindings.hasOwnProperty(key) &&
       value !== undefined
@@ -199,7 +215,7 @@ function prettifierMetaWrapper (pretty, dest) {
       if (lastLogger.hasOwnProperty(parsedChindingsSym)) {
         chindings = lastLogger[parsedChindingsSym]
       } else {
-        chindings = JSON.parse('{"v":1' + lastLogger[chindingsSym] + '}')
+        chindings = JSON.parse('{' + lastLogger[chindingsSym].substr(1) + '}')
         lastLogger[parsedChindingsSym] = chindings
       }
 
@@ -250,8 +266,8 @@ function hasBeenTampered (stream) {
   return stream.write !== stream.constructor.prototype.write
 }
 
-function buildSafeSonicBoom (dest, buffer = 0, sync = true) {
-  const stream = new SonicBoom(dest, buffer, sync)
+function buildSafeSonicBoom (opts) {
+  const stream = new SonicBoom(opts)
   stream.on('error', filterBrokenPipe)
   return stream
 
@@ -276,10 +292,10 @@ function createArgsNormalizer (defaultOptions) {
   return function normalizeArgs (instance, opts = {}, stream) {
     // support stream as a string
     if (typeof opts === 'string') {
-      stream = buildSafeSonicBoom(opts)
+      stream = buildSafeSonicBoom({ dest: opts })
       opts = {}
     } else if (typeof stream === 'string') {
-      stream = buildSafeSonicBoom(stream)
+      stream = buildSafeSonicBoom({ dest: stream })
     } else if (opts instanceof SonicBoom || opts.writable || opts._writableState) {
       stream = opts
       opts = null
@@ -303,7 +319,7 @@ function createArgsNormalizer (defaultOptions) {
     if (enabled === false) opts.level = 'silent'
     stream = stream || process.stdout
     if (stream === process.stdout && stream.fd >= 0 && !hasBeenTampered(stream)) {
-      stream = buildSafeSonicBoom(stream.fd)
+      stream = buildSafeSonicBoom({ fd: stream.fd })
     }
     if (prettyPrint) {
       const prettyOpts = Object.assign({ messageKey }, prettyPrint)
@@ -363,6 +379,14 @@ function stringify (obj) {
   }
 }
 
+function buildFormatters (level, bindings, log) {
+  return {
+    level,
+    bindings,
+    log
+  }
+}
+
 function setMetadataProps (dest, that) {
   if (dest[needsMetadataGsym] === true) {
     dest.lastLevel = that.lastLevel
@@ -382,5 +406,6 @@ module.exports = {
   genLog,
   createArgsNormalizer,
   final,
-  stringify
+  stringify,
+  buildFormatters
 }
diff --git a/package.json b/package.json
index 534827c..0a39ad8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pino",
-  "version": "5.17.0",
+  "version": "6.0.0",
   "description": "super fast, all natural json logger",
   "main": "pino.js",
   "browser": "./browser.js",
@@ -18,10 +18,8 @@
   "scripts": {
     "docs": "docsify serve",
     "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",
+    "test": "standard | snazzy && tap --100 test/*test.js",
+    "cov-ui": "tap --coverage-report=html test/*test.js",
     "bench": "node benchmarks/utils/runbench all",
     "bench-basic": "node benchmarks/utils/runbench basic",
     "bench-object": "node benchmarks/utils/runbench object",
@@ -31,6 +29,7 @@
     "bench-child": "node benchmarks/utils/runbench child",
     "bench-child-child": "node benchmarks/utils/runbench child-child",
     "bench-child-creation": "node benchmarks/utils/runbench child-creation",
+    "bench-formatters": "node benchmarks/utils/runbench formatters",
     "update-bench-doc": "node benchmarks/utils/generate-benchmark-doc > docs/benchmarks.md"
   },
   "bin": {
@@ -63,26 +62,25 @@
     "benchmark": "^2.1.4",
     "bole": "^3.0.2",
     "bunyan": "^1.8.12",
-    "cross-env": "^5.2.1",
-    "docsify-cli": "^4.2.1",
+    "docsify-cli": "^4.4.0",
     "execa": "^1.0.0",
     "fastbench": "^1.0.1",
     "flush-write-stream": "^2.0.0",
-    "import-fresh": "^3.0.0",
+    "import-fresh": "^3.2.1",
     "log": "^5.0.0",
-    "loglevel": "^1.6.4",
-    "pino-pretty": "^2.6.1",
+    "loglevel": "^1.6.7",
+    "pino-pretty": "^3.0.0",
     "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",
+    "standard": "^14.3.3",
     "steed": "^1.1.3",
-    "tap": "^12.7.0",
-    "tape": "^4.11.0",
+    "strip-ansi": "^6.0.0",
+    "tap": "^14.0.0",
+    "tape": "^4.13.2",
     "through2": "^3.0.1",
     "winston": "^3.2.1"
   },
@@ -91,7 +89,7 @@
     "fast-safe-stringify": "^2.0.7",
     "flatstr": "^1.0.12",
     "pino-std-serializers": "^2.4.2",
-    "quick-format-unescaped": "^3.0.3",
-    "sonic-boom": "^0.7.5"
+    "quick-format-unescaped": "^4.0.1",
+    "sonic-boom": "^1.0.0"
   }
 }
diff --git a/pino.js b/pino.js
index 4138069..1f03864 100644
--- a/pino.js
+++ b/pino.js
@@ -1,4 +1,5 @@
 'use strict'
+/* eslint no-prototype-builtins: 0 */
 const os = require('os')
 const stdSerializers = require('pino-std-serializers')
 const redaction = require('./lib/redaction')
@@ -11,9 +12,10 @@ const {
   asChindings,
   final,
   stringify,
-  buildSafeSonicBoom
+  buildSafeSonicBoom,
+  buildFormatters
 } = require('./lib/tools')
-const { version, LOG_VERSION } = require('./lib/meta')
+const { version } = require('./lib/meta')
 const {
   chindingsSym,
   redactFmtSym,
@@ -28,10 +30,9 @@ const {
   formatOptsSym,
   messageKeySym,
   nestedKeySym,
-  useLevelLabelsSym,
-  levelKeySym,
   mixinSym,
-  useOnlyCustomLevelsSym
+  useOnlyCustomLevelsSym,
+  formattersSym
 } = symbols
 const { epochTime, nullTime } = time
 const { pid } = process
@@ -39,7 +40,6 @@ const hostname = os.hostname()
 const defaultErrorSerializer = stdSerializers.err
 const defaultOptions = {
   level: 'info',
-  useLevelLabels: false,
   messageKey: 'msg',
   nestedKey: null,
   enabled: true,
@@ -48,11 +48,19 @@ const defaultOptions = {
   serializers: Object.assign(Object.create(null), {
     err: defaultErrorSerializer
   }),
+  formatters: Object.assign(Object.create(null), {
+    bindings (bindings) {
+      return bindings
+    },
+    level (label, number) {
+      return { level: number }
+    }
+  }),
   timestamp: epochTime,
   name: undefined,
   redact: null,
   customLevels: null,
-  levelKey: 'level',
+  levelKey: undefined,
   useOnlyCustomLevels: false
 }
 
@@ -75,21 +83,54 @@ function pino (...args) {
     level,
     customLevels,
     useLevelLabels,
+    changeLevelName,
     levelKey,
     mixin,
-    useOnlyCustomLevels
+    useOnlyCustomLevels,
+    formatters
   } = opts
 
+  const allFormatters = buildFormatters(
+    formatters.level,
+    formatters.bindings,
+    formatters.log
+  )
+
+  if (useLevelLabels && !(changeLevelName || levelKey)) {
+    process.emitWarning('useLevelLabels is deprecated, use the formatters.level option instead', 'Warning', 'PINODEP001')
+    allFormatters.level = labelsFormatter
+  } else if ((changeLevelName || levelKey) && !useLevelLabels) {
+    process.emitWarning('changeLevelName and levelKey are deprecated, use the formatters.level option instead', 'Warning', 'PINODEP002')
+    allFormatters.level = levelNameFormatter(changeLevelName || levelKey)
+  } else if ((changeLevelName || levelKey) && useLevelLabels) {
+    process.emitWarning('useLevelLabels is deprecated, use the formatters.level option instead', 'Warning', 'PINODEP001')
+    process.emitWarning('changeLevelName and levelKey are deprecated, use the formatters.level option instead', 'Warning', 'PINODEP002')
+    allFormatters.level = levelNameLabelFormatter(changeLevelName || levelKey)
+  }
+
+  if (serializers[Symbol.for('pino.*')]) {
+    process.emitWarning('The pino.* serializer is deprecated, use the formatters.log options instead', 'Warning', 'PINODEP003')
+    allFormatters.log = serializers[Symbol.for('pino.*')]
+  }
+
+  if (!allFormatters.bindings) {
+    allFormatters.bindings = defaultOptions.formatters.bindings
+  }
+  if (!allFormatters.level) {
+    allFormatters.level = defaultOptions.formatters.level
+  }
+
   const stringifiers = redact ? redaction(redact, stringify) : {}
   const formatOpts = redact
     ? { stringify: stringifiers[redactFmtSym] }
     : { stringify }
-  const end = ',"v":' + LOG_VERSION + '}' + (crlf ? '\r\n' : '\n')
+  const end = '}' + (crlf ? '\r\n' : '\n')
   const coreChindings = asChindings.bind(null, {
     [chindingsSym]: '',
     [serializersSym]: serializers,
     [stringifiersSym]: stringifiers,
-    [stringifySym]: stringify
+    [stringifySym]: stringify,
+    [formattersSym]: allFormatters
   })
   const chindings = base === null ? '' : (name === undefined)
     ? coreChindings(base) : coreChindings(Object.assign({}, base, { name }))
@@ -105,8 +146,6 @@ function pino (...args) {
 
   Object.assign(instance, {
     levels,
-    [useLevelLabelsSym]: useLevelLabels,
-    [levelKeySym]: levelKey,
     [useOnlyCustomLevelsSym]: useOnlyCustomLevels,
     [streamSym]: stream,
     [timeSym]: time,
@@ -119,19 +158,49 @@ function pino (...args) {
     [nestedKeySym]: nestedKey,
     [serializersSym]: serializers,
     [mixinSym]: mixin,
-    [chindingsSym]: chindings
+    [chindingsSym]: chindings,
+    [formattersSym]: allFormatters
   })
   Object.setPrototypeOf(instance, proto)
 
-  if (customLevels || useLevelLabels || levelKey !== defaultOptions.levelKey) genLsCache(instance)
+  genLsCache(instance)
 
   instance[setLevelSym](level)
 
   return instance
 }
 
-pino.extreme = (dest = process.stdout.fd) => buildSafeSonicBoom(dest, 4096, false)
-pino.destination = (dest = process.stdout.fd) => buildSafeSonicBoom(dest, 0, true)
+function labelsFormatter (label, number) {
+  return { level: label }
+}
+
+function levelNameFormatter (name) {
+  return function (label, number) {
+    return { [name]: number }
+  }
+}
+
+function levelNameLabelFormatter (name) {
+  return function (label, number) {
+    return { [name]: label }
+  }
+}
+
+pino.extreme = (dest = process.stdout.fd) => {
+  process.emitWarning(
+    'The pino.extreme() option is deprecated and will be removed in v7. Use pino.destination({ sync: false }) instead.',
+    { code: 'extreme_deprecation' }
+  )
+  return buildSafeSonicBoom({ dest, minLength: 4096, sync: false })
+}
+pino.destination = (dest = process.stdout.fd) => {
+  if (typeof dest === 'object') {
+    dest.dest = dest.dest || process.stdout.fd
+    return buildSafeSonicBoom(dest)
+  } else {
+    return buildSafeSonicBoom({ dest, minLength: 0, sync: true })
+  }
+}
 
 pino.final = final
 pino.levels = mappings()
@@ -139,6 +208,5 @@ pino.stdSerializers = serializers
 pino.stdTimeFunctions = Object.assign({}, time)
 pino.symbols = symbols
 pino.version = version
-pino.LOG_VERSION = LOG_VERSION
 
 module.exports = pino


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index eeb91b6..eef67fd 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -106,7 +106,6 @@ function levelTest (name, level) {
     is(result.hostname, hostname)
     is(result.level, level)
     is(result.hello, 'world')
-    is(result.v, 1)
     same(Object.keys(obj), ['hello'])
   })
 
@@ -124,8 +123,7 @@ function levelTest (name, level) {
       hostname: hostname,
       level: level,
       msg: 'a string',
-      hello: 'world',
-      v: 1
+      hello: 'world'
     })
     same(Object.keys(obj), ['hello'])
   })
@@ -143,8 +141,7 @@ function levelTest (name, level) {
       hostname: hostname,
       level: level,
       msg: 'string',
-      hello: 'world',
-      v: 1
+      hello: 'world'
     })
   })
 
@@ -163,7 +160,7 @@ function levelTest (name, level) {
     instance.level = name
 
     const sym = Symbol('foo')
-    instance[name]('hello', sym)
+    instance[name]('hello %s', sym)
 
     const result = await once(stream, 'data')
 
@@ -191,8 +188,7 @@ function levelTest (name, level) {
         type: 'Error',
         message: err.message,
         stack: err.stack
-      },
-      v: 1
+      }
     })
   })
 
@@ -210,8 +206,7 @@ function levelTest (name, level) {
       hostname: hostname,
       level: level,
       msg: 'hello world',
-      hello: 'world',
-      v: 1
+      hello: 'world'
     })
   })
 }
@@ -261,8 +256,7 @@ test('set the name', async ({ is, same }) => {
     hostname: hostname,
     level: 60,
     name: 'hello',
-    msg: 'this is fatal',
-    v: 1
+    msg: 'this is fatal'
   })
 })
 
@@ -281,8 +275,7 @@ test('set the messageKey', async ({ is, same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    fooMessage: message,
-    v: 1
+    fooMessage: message
   })
 })
 
@@ -301,8 +294,7 @@ test('set the nestedKey', async ({ is, same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    stuff: object,
-    v: 1
+    stuff: object
   })
 })
 
@@ -317,8 +309,7 @@ test('set undefined properties', async ({ is, same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    hello: 'world',
-    v: 1
+    hello: 'world'
   })
 })
 
@@ -345,8 +336,7 @@ test('set the base', async ({ is, same }) => {
   same(result, {
     a: 'b',
     level: 60,
-    msg: 'this is fatal',
-    v: 1
+    msg: 'this is fatal'
   })
 })
 
@@ -361,17 +351,16 @@ test('set the base to null', async ({ is, same }) => {
   delete result.time
   same(result, {
     level: 60,
-    msg: 'this is fatal',
-    v: 1
+    msg: 'this is fatal'
   })
 })
 
-test('set the base to null and use a serializer', async ({ is, same }) => {
+test('set the base to null and use a formatter', async ({ is, same }) => {
   const stream = sink()
   const instance = pino({
     base: null,
-    serializers: {
-      [Symbol.for('pino.*')]: (input) => {
+    formatters: {
+      log (input) {
         return Object.assign({}, input, { additionalMessage: 'using pino' })
       }
     }
@@ -383,15 +372,19 @@ test('set the base to null and use a serializer', async ({ is, same }) => {
   same(result, {
     level: 60,
     msg: 'this is fatal too',
-    additionalMessage: 'using pino',
-    v: 1
+    additionalMessage: 'using pino'
   })
 })
 
-test('throw if creating child without bindings', async ({ throws }) => {
+test('throw if creating child without bindings', async ({ is, fail }) => {
   const stream = sink()
   const instance = pino(stream)
-  throws(() => instance.child())
+  try {
+    instance.child()
+    fail('it should throw')
+  } catch (err) {
+    is(err.message, 'missing bindings for child Pino')
+  }
 })
 
 test('correctly escapes msg strings with stray double quote at end', async ({ same }) => {
@@ -408,8 +401,7 @@ test('correctly escapes msg strings with stray double quote at end', async ({ sa
     hostname: hostname,
     level: 60,
     name: 'hello',
-    msg: 'this contains "',
-    v: 1
+    msg: 'this contains "'
   })
 })
 
@@ -426,8 +418,7 @@ test('correctly escape msg strings with unclosed double quote', async ({ same })
     hostname: hostname,
     level: 60,
     name: 'hello',
-    msg: '" this contains',
-    v: 1
+    msg: '" this contains'
   })
 })
 
@@ -443,8 +434,7 @@ test('object and format string', async ({ same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    msg: 'foo bar',
-    v: 1
+    msg: 'foo bar'
   })
 })
 
@@ -459,8 +449,7 @@ test('object and format string property', async ({ same }) => {
     hostname: hostname,
     level: 30,
     msg: 'foo bar',
-    answer: 42,
-    v: 1
+    answer: 42
   })
 })
 
@@ -485,8 +474,7 @@ test('correctly supports stderr', async ({ same }) => {
         pid: pid,
         hostname: hostname,
         level: 60,
-        msg: 'a message',
-        v: 1
+        msg: 'a message'
       })
     }
   }
@@ -504,8 +492,7 @@ test('normalize number to string', async ({ same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    msg: '1',
-    v: 1
+    msg: '1'
   })
 })
 
@@ -520,8 +507,7 @@ test('normalize number to string with an object', async ({ same }) => {
     hostname: hostname,
     level: 30,
     msg: '1',
-    answer: 42,
-    v: 1
+    answer: 42
   })
 })
 
@@ -537,8 +523,7 @@ test('handles objects with null prototype', async ({ same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    test: 'test',
-    v: 1
+    test: 'test'
   })
 })
 
@@ -556,8 +541,7 @@ test('pino.destination', async ({ same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    msg: 'hello',
-    v: 1
+    msg: 'hello'
   })
 })
 
@@ -575,8 +559,7 @@ test('auto pino.destination with a string', async ({ same }) => {
     pid: pid,
     hostname: hostname,
     level: 30,
-    msg: 'hello',
-    v: 1
+    msg: 'hello'
   })
 })
 
@@ -594,8 +577,7 @@ test('auto pino.destination with a string as second argument', async ({ same })
     pid: pid,
     hostname: hostname,
     level: 30,
-    msg: 'hello',
-    v: 1
+    msg: 'hello'
   })
 })
 
@@ -615,8 +597,7 @@ test('does not override opts with a string as second argument', async ({ same })
     hostname: hostname,
     level: 30,
     time: 'none',
-    msg: 'hello',
-    v: 1
+    msg: 'hello'
   })
 })
 
@@ -647,7 +628,7 @@ test('fast-safe-stringify must be used when interpolating', async (t) => {
 
   const o = { a: { b: {} } }
   o.a.b.c = o.a.b
-  instance.info('test', o)
+  instance.info('test %j', o)
 
   const { msg } = await once(stream, 'data')
   t.is(msg, 'test {"a":{"b":{"c":"[Circular]"}}}')
diff --git a/test/broken-pipe.test.js b/test/broken-pipe.test.js
index 0454cee..5bc56c2 100644
--- a/test/broken-pipe.test.js
+++ b/test/broken-pipe.test.js
@@ -23,7 +23,7 @@ t.jobs = 42
 
 test('basic.js')
 test('destination.js')
-test('extreme.js')
+test('syncfalse.js')
 
 t.test('let error pass through', ({ is, plan }) => {
   plan(3)
diff --git a/test/browser.test.js b/test/browser.test.js
index 76caba3..8aea14a 100644
--- a/test/browser.test.js
+++ b/test/browser.test.js
@@ -94,12 +94,6 @@ test('exposes levels object', ({ end, same }) => {
   end()
 })
 
-test('exposes LOG_VERSION', ({ end, is }) => {
-  is(pino.LOG_VERSION, 1)
-
-  end()
-})
-
 test('exposes faux stdSerializers', ({ end, ok, same }) => {
   ok(pino.stdSerializers)
   // make sure faux stdSerializers match pino-std-serializers
@@ -197,7 +191,7 @@ test('opts.browser.write func string joining', ({ end, ok, is }) => {
       }
     }
   })
-  instance.info('test', 'test2', 'test3')
+  instance.info('test %s %s', 'test2', 'test3')
 
   end()
 })
@@ -213,7 +207,7 @@ test('opts.browser.write func string joining when asObject is true', ({ end, ok,
       }
     }
   })
-  instance.info('test', 'test2', 'test3')
+  instance.info('test %s %s', 'test2', 'test3')
 
   end()
 })
@@ -229,7 +223,7 @@ test('opts.browser.write func string joining when asObject is true', ({ end, ok,
       }
     }
   })
-  instance.info('test', 'test2', 'test3')
+  instance.info('test %s %s', 'test2', 'test3')
 
   end()
 })
@@ -244,7 +238,7 @@ test('opts.browser.write func string object joining', ({ end, ok, is }) => {
       }
     }
   })
-  instance.info('test', { test: 'test2' }, { test: 'test3' })
+  instance.info('test %j %j', { test: 'test2' }, { test: 'test3' })
 
   end()
 })
@@ -260,7 +254,7 @@ test('opts.browser.write func string object joining when asObject is true', ({ e
       }
     }
   })
-  instance.info('test', { test: 'test2' }, { test: 'test3' })
+  instance.info('test %j %j', { test: 'test2' }, { test: 'test3' })
 
   end()
 })
diff --git a/test/custom-levels.test.js b/test/custom-levels.test.js
index b7d19c3..f7b024c 100644
--- a/test/custom-levels.test.js
+++ b/test/custom-levels.test.js
@@ -6,6 +6,10 @@ const { test } = require('tap')
 const { sink, once } = require('./helper')
 const pino = require('../')
 
+// Silence all warnings for this test
+process.removeAllListeners('warning')
+process.on('warning', () => {})
+
 test('adds additional levels', async ({ is }) => {
   const stream = sink()
   const logger = pino({
@@ -251,14 +255,18 @@ test('does not share custom level state across siblings', async ({ doesNotThrow
   })
 })
 
-test('custom level does not affect levelKey', async ({ is }) => {
+test('custom level does not affect the levels serializer', async ({ is }) => {
   const stream = sink()
   const logger = pino({
     customLevels: {
       foo: 35,
       bar: 45
     },
-    levelKey: 'priority'
+    formatters: {
+      level (label, number) {
+        return { priority: number }
+      }
+    }
   }, stream)
 
   logger.foo('test')
@@ -266,6 +274,28 @@ test('custom level does not affect levelKey', async ({ is }) => {
   is(priority, 35)
 })
 
+test('When useOnlyCustomLevels is set to true, the level formatter should only get custom levels', async ({ is }) => {
+  const stream = sink()
+  const logger = pino({
+    customLevels: {
+      answer: 42
+    },
+    useOnlyCustomLevels: true,
+    level: 42,
+    formatters: {
+      level (label, number) {
+        is(label, 'answer')
+        is(number, 42)
+        return { level: number }
+      }
+    }
+  }, stream)
+
+  logger.answer('test')
+  const { level } = await once(stream, 'data')
+  is(level, 42)
+})
+
 test('custom levels accesible in prettifier function', async ({ plan, same }) => {
   plan(1)
   const logger = pino({
diff --git a/test/deprecations.test.js b/test/deprecations.test.js
new file mode 100644
index 0000000..59c51c2
--- /dev/null
+++ b/test/deprecations.test.js
@@ -0,0 +1,101 @@
+'use strict'
+/* eslint no-prototype-builtins: 0 */
+
+const { test } = require('tap')
+const { sink, once } = require('./helper')
+const pino = require('../')
+
+process.removeAllListeners('warning')
+
+test('useLevelLabels', async ({ match, is }) => {
+  process.on('warning', onWarning)
+  function onWarning (warn) {
+    is(warn.code, 'PINODEP001')
+  }
+
+  const stream = sink()
+  const logger = pino({
+    useLevelLabels: true
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, { level: 'info' })
+  process.removeListener('warning', onWarning)
+})
+
+test('changeLevelName', async ({ match, is }) => {
+  process.on('warning', onWarning)
+  function onWarning (warn) {
+    is(warn.code, 'PINODEP002')
+  }
+
+  const stream = sink()
+  const logger = pino({
+    changeLevelName: 'priority'
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, { priority: 30 })
+  process.removeListener('warning', onWarning)
+})
+
+test('levelKey', async ({ match, is }) => {
+  process.on('warning', onWarning)
+  function onWarning (warn) {
+    is(warn.code, 'PINODEP002')
+  }
+
+  const stream = sink()
+  const logger = pino({
+    levelKey: 'priority'
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, { priority: 30 })
+  process.removeListener('warning', onWarning)
+})
+
+test('useLevelLabels and changeLevelName', async ({ match, is }) => {
+  let count = 0
+  process.on('warning', onWarning)
+  function onWarning (warn) {
+    is(warn.code, count === 0 ? 'PINODEP001' : 'PINODEP002')
+    count += 1
+  }
+
+  const stream = sink()
+  const logger = pino({
+    changeLevelName: 'priority',
+    useLevelLabels: true
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, { priority: 'info' })
+  process.removeListener('warning', onWarning)
+})
+
+test('pino.* serializer', async ({ match, is, pass }) => {
+  process.on('warning', onWarning)
+  function onWarning (warn) {
+    is(warn.code, 'PINODEP003')
+  }
+
+  const stream = sink()
+  const logger = pino({
+    serializers: {
+      [Symbol.for('pino.*')] (log) {
+        pass('called')
+        return log
+      }
+    }
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, { level: 30 })
+  process.removeListener('warning', onWarning)
+})
diff --git a/test/error.test.js b/test/error.test.js
index ea7c04c..ec19001 100644
--- a/test/error.test.js
+++ b/test/error.test.js
@@ -28,8 +28,7 @@ test('err is serialized with additional properties set on the Error object', asy
     type: 'Error',
     msg: err.message,
     stack: err.stack,
-    foo: err.foo,
-    v: 1
+    foo: err.foo
   })
 })
 
@@ -48,8 +47,7 @@ test('type should be retained, even if type is a property', async ({ ok, same })
     level: level,
     type: 'bar',
     msg: err.message,
-    stack: err.stack,
-    v: 1
+    stack: err.stack
   })
 })
 
@@ -70,8 +68,7 @@ test('type, message and stack should be first level properties', async ({ ok, sa
     type: 'Error',
     msg: err.message,
     stack: err.stack,
-    foo: err.foo,
-    v: 1
+    foo: err.foo
   })
 })
 
@@ -98,8 +95,7 @@ test('err serializer', async ({ ok, same }) => {
       message: err.message,
       stack: err.stack,
       foo: err.foo
-    },
-    v: 1
+    }
   })
 })
 
@@ -121,8 +117,7 @@ test('an error with statusCode property is not confused for a http response', as
     type: 'Error',
     msg: err.message,
     stack: err.stack,
-    statusCode: err.statusCode,
-    v: 1
+    statusCode: err.statusCode
   })
 })
 
@@ -173,7 +168,6 @@ test('correctly ignores toString on errors', async ({ same }) => {
     level: 60,
     type: 'Error',
     msg: err.message,
-    stack: err.stack,
-    v: 1
+    stack: err.stack
   })
 })
diff --git a/test/escaping.test.js b/test/escaping.test.js
index 3edf683..f9aea2b 100644
--- a/test/escaping.test.js
+++ b/test/escaping.test.js
@@ -22,8 +22,7 @@ function testEscape (ch, key) {
       hostname: hostname,
       level: 60,
       name: 'hello',
-      msg: 'this contains ' + key,
-      v: 1
+      msg: 'this contains ' + key
     })
   })
 }
@@ -87,7 +86,6 @@ test('correctly escape `hello \\u001F world \\n \\u0022`', async ({ same }) => {
     hostname: hostname,
     level: 60,
     name: 'hello',
-    msg: 'hello \u001F world \n \u0022',
-    v: 1
+    msg: 'hello \u001F world \n \u0022'
   })
 })
diff --git a/test/exit.test.js b/test/exit.test.js
index fe7542a..b4279f2 100644
--- a/test/exit.test.js
+++ b/test/exit.test.js
@@ -22,9 +22,9 @@ test('pino.destination log everything when calling process.exit(0)', async ({ is
   isNot(actual.match(/world/), null)
 })
 
-test('pino.extreme does not log everything when calling process.exit(0)', async ({ is }) => {
+test('sync false does not log everything when calling process.exit(0)', async ({ is }) => {
   var actual = ''
-  const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'extreme-exit.js')])
+  const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'syncfalse-exit.js')])
 
   child.stdout.pipe(writer((s, enc, cb) => {
     actual += s
@@ -37,9 +37,9 @@ test('pino.extreme does not log everything when calling process.exit(0)', async
   is(actual.match(/world/), null)
 })
 
-test('pino.extreme logs everything when calling flushSync', async ({ isNot }) => {
+test('sync false logs everything when calling flushSync', async ({ isNot }) => {
   var actual = ''
-  const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'extreme-flush-exit.js')])
+  const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'syncfalse-flush-exit.js')])
 
   child.stdout.pipe(writer((s, enc, cb) => {
     actual += s
diff --git a/test/final.test.js b/test/final.test.js
index b5d6d0d..8915dbd 100644
--- a/test/final.test.js
+++ b/test/final.test.js
@@ -24,26 +24,26 @@ test('throws if the supplied handler is not a function', async ({ throws }) => {
   }, Error('if supplied, the handler parameter should be a function'))
 })
 
-test('throws if not supplied logger with pino.extreme instance', async ({ throws, doesNotThrow }) => {
+test('throws if not supplied logger with pino.destination instance with sync false', async ({ throws, doesNotThrow }) => {
   throws(() => {
     pino.final(pino(fs.createWriteStream(getPathToNull())), () => {})
-  }, Error('final requires a stream that has a flushSync method, such as pino.destination and pino.extreme'))
+  }, Error('final requires a stream that has a flushSync method, such as pino.destination'))
 
   doesNotThrow(() => {
-    pino.final(pino(pino.extreme()), () => {})
+    pino.final(pino(pino.destination({ sync: false })), () => {})
   })
 
   doesNotThrow(() => {
-    pino.final(pino(pino.extreme()), () => {})
+    pino.final(pino(pino.destination({ sync: false })), () => {})
   })
 })
 
 test('returns an exit listener function', async ({ is }) => {
-  is(typeof pino.final(pino(pino.extreme()), () => {}), 'function')
+  is(typeof pino.final(pino(pino.destination({ sync: false })), () => {}), 'function')
 })
 
-test('listener function immediately sync flushes when fired', async ({ pass, fail }) => {
-  const dest = pino.extreme(getPathToNull())
+test('listener function immediately sync flushes when fired (sync false)', async ({ pass, fail }) => {
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   var passed = false
   dest.flushSync = () => {
     passed = true
@@ -54,8 +54,8 @@ test('listener function immediately sync flushes when fired', async ({ pass, fai
   if (passed === false) fail('flushSync not called')
 })
 
-test('listener function immediately sync flushes when fired (pino.destination)', async ({ pass, fail }) => {
-  const dest = pino.destination(getPathToNull())
+test('listener function immediately sync flushes when fired (sync true)', async ({ pass, fail }) => {
+  const dest = pino.destination({ dest: getPathToNull(), sync: true })
   var passed = false
   dest.flushSync = () => {
     passed = true
@@ -67,14 +67,14 @@ test('listener function immediately sync flushes when fired (pino.destination)',
 })
 
 test('swallows the non-ready error', async ({ doesNotThrow }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   doesNotThrow(() => {
     pino.final(pino(dest), () => {})()
   })
 })
 
 test('listener function triggers handler function parameter', async ({ pass, fail }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   var passed = false
   pino.final(pino(dest), () => {
     passed = true
@@ -85,14 +85,14 @@ test('listener function triggers handler function parameter', async ({ pass, fai
 })
 
 test('passes any error to the handler', async ({ is }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   pino.final(pino(dest), (err) => {
     is(err.message, 'test')
   })(Error('test'))
 })
 
 test('passes a specialized final logger instance', async ({ is, isNot, error }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   const logger = pino(dest)
   pino.final(logger, (err, finalLogger) => {
     error(err)
@@ -116,7 +116,7 @@ test('passes a specialized final logger instance', async ({ is, isNot, error })
 })
 
 test('returns a specialized final logger instance if no handler is passed', async ({ is, isNot }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   const logger = pino(dest)
   const finalLogger = pino.final(logger)
   is(typeof finalLogger.trace, 'function')
@@ -138,7 +138,7 @@ test('returns a specialized final logger instance if no handler is passed', asyn
 })
 
 test('final logger instances synchronously flush after a log method call', async ({ pass, fail, error }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   const logger = pino(dest)
   var passed = false
   var count = 0
@@ -158,7 +158,7 @@ test('final logger instances synchronously flush after a log method call', async
 })
 
 test('also instruments custom log methods', async ({ pass, fail, error }) => {
-  const dest = pino.extreme(getPathToNull())
+  const dest = pino.destination({ dest: getPathToNull(), sync: false })
   const logger = pino({
     customLevels: {
       foo: 35
diff --git a/test/fixtures/broken-pipe/extreme.js b/test/fixtures/broken-pipe/syncfalse.js
similarity index 83%
rename from test/fixtures/broken-pipe/extreme.js
rename to test/fixtures/broken-pipe/syncfalse.js
index 27a624b..75cbff4 100644
--- a/test/fixtures/broken-pipe/extreme.js
+++ b/test/fixtures/broken-pipe/syncfalse.js
@@ -5,7 +5,7 @@ Date.now = function () { return 1459875739796 }
 require('os').hostname = function () { return 'abcdefghijklmnopqr' }
 
 const pino = require('../../..')
-const logger = pino(pino.extreme())
+const logger = pino(pino.destination({ sync: false }))
 
 for (let i = 0; i < 1000; i++) {
   logger.info('hello world')
diff --git a/test/fixtures/pretty/error.js b/test/fixtures/pretty/error.js
index 309976d..4786e79 100644
--- a/test/fixtures/pretty/error.js
+++ b/test/fixtures/pretty/error.js
@@ -4,4 +4,4 @@ require('os').hostname = function () { return 'abcdefghijklmnopqr' }
 var pino = require(require.resolve('./../../../'))
 var log = pino({ prettyPrint: true })
 log.error(new Error('kaboom'))
-log.error(new Error('kaboom'), 'with', 'a', 'message')
+log.error(new Error('kaboom'), 'with a message')
diff --git a/test/fixtures/extreme.js b/test/fixtures/syncfalse-child.js
similarity index 77%
rename from test/fixtures/extreme.js
rename to test/fixtures/syncfalse-child.js
index 20b2a6c..8d9b8ce 100644
--- a/test/fixtures/extreme.js
+++ b/test/fixtures/syncfalse-child.js
@@ -2,5 +2,5 @@ global.process = { __proto__: process, pid: 123456 }
 Date.now = function () { return 1459875739796 }
 require('os').hostname = function () { return 'abcdefghijklmnopqr' }
 var pino = require(require.resolve('./../../'))
-var extreme = pino(pino.extreme())
+var extreme = pino(pino.destination({ sync: false })).child({ hello: 'world' })
 pino.final(extreme, (_, logger) => logger.info('h'))()
diff --git a/test/fixtures/extreme-exit.js b/test/fixtures/syncfalse-exit.js
similarity index 81%
rename from test/fixtures/extreme-exit.js
rename to test/fixtures/syncfalse-exit.js
index a90cc67..7b7867b 100644
--- a/test/fixtures/extreme-exit.js
+++ b/test/fixtures/syncfalse-exit.js
@@ -2,7 +2,7 @@ global.process = { __proto__: process, pid: 123456 }
 Date.now = function () { return 1459875739796 }
 require('os').hostname = function () { return 'abcdefghijklmnopqr' }
 var pino = require(require.resolve('./../../'))
-var dest = pino.extreme(1)
+var dest = pino.destination({ dest: 1, minLength: 4096, sync: false })
 var logger = pino({}, dest)
 logger.info('hello')
 logger.info('world')
diff --git a/test/fixtures/extreme-flush-exit.js b/test/fixtures/syncfalse-flush-exit.js
similarity index 81%
rename from test/fixtures/extreme-flush-exit.js
rename to test/fixtures/syncfalse-flush-exit.js
index 3abf7e0..e0d931d 100644
--- a/test/fixtures/extreme-flush-exit.js
+++ b/test/fixtures/syncfalse-flush-exit.js
@@ -2,7 +2,7 @@ global.process = { __proto__: process, pid: 123456 }
 Date.now = function () { return 1459875739796 }
 require('os').hostname = function () { return 'abcdefghijklmnopqr' }
 var pino = require(require.resolve('./../../'))
-var dest = pino.extreme(1)
+var dest = pino.destination({ dest: 1, minLength: 4096, sync: false })
 var logger = pino({}, dest)
 logger.info('hello')
 logger.info('world')
diff --git a/test/fixtures/extreme-child.js b/test/fixtures/syncfalse.js
similarity index 79%
rename from test/fixtures/extreme-child.js
rename to test/fixtures/syncfalse.js
index 81528ab..119ca6f 100644
--- a/test/fixtures/extreme-child.js
+++ b/test/fixtures/syncfalse.js
@@ -2,5 +2,5 @@ global.process = { __proto__: process, pid: 123456 }
 Date.now = function () { return 1459875739796 }
 require('os').hostname = function () { return 'abcdefghijklmnopqr' }
 var pino = require(require.resolve('./../../'))
-var extreme = pino(pino.extreme()).child({ hello: 'world' })
+var extreme = pino(pino.destination({ minLength: 4096, sync: false }))
 pino.final(extreme, (_, logger) => logger.info('h'))()
diff --git a/test/formatters.test.js b/test/formatters.test.js
new file mode 100644
index 0000000..7e16f0e
--- /dev/null
+++ b/test/formatters.test.js
@@ -0,0 +1,299 @@
+'use strict'
+/* eslint no-prototype-builtins: 0 */
+
+const { hostname } = require('os')
+const { test } = require('tap')
+const { sink, once } = require('./helper')
+const pino = require('../')
+
+test('level formatter', async ({ match }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      level (label, number) {
+        return {
+          log: {
+            level: label
+          }
+        }
+      }
+    }
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, {
+    log: {
+      level: 'info'
+    }
+  })
+})
+
+test('bindings formatter', async ({ match }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      bindings (bindings) {
+        return {
+          process: {
+            pid: bindings.pid
+          },
+          host: {
+            name: bindings.hostname
+          }
+        }
+      }
+    }
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  match(await o, {
+    process: {
+      pid: process.pid
+    },
+    host: {
+      name: hostname()
+    }
+  })
+})
+
+test('no bindings formatter', async ({ match, notOk }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      bindings (bindings) {
+        return null
+      }
+    }
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info('hello world')
+  const log = await o
+  notOk(log.hasOwnProperty('pid'))
+  notOk(log.hasOwnProperty('hostname'))
+  match(log, { msg: 'hello world' })
+})
+
+test('log formatter', async ({ match, is }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      log (obj) {
+        is(obj.hasOwnProperty('msg'), false)
+        return { hello: 'world', ...obj }
+      }
+    }
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info({ foo: 'bar', nested: { object: true } }, 'hello world')
+  match(await o, {
+    hello: 'world',
+    foo: 'bar',
+    nested: { object: true }
+  })
+})
+
+test('Formatters combined', async ({ match }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      level (label, number) {
+        return {
+          log: {
+            level: label
+          }
+        }
+      },
+      bindings (bindings) {
+        return {
+          process: {
+            pid: bindings.pid
+          },
+          host: {
+            name: bindings.hostname
+          }
+        }
+      },
+      log (obj) {
+        return { hello: 'world', ...obj }
+      }
+    }
+  }, stream)
+
+  const o = once(stream, 'data')
+  logger.info({ foo: 'bar', nested: { object: true } }, 'hello world')
+  match(await o, {
+    log: {
+      level: 'info'
+    },
+    process: {
+      pid: process.pid
+    },
+    host: {
+      name: hostname()
+    },
+    hello: 'world',
+    foo: 'bar',
+    nested: { object: true }
+  })
+})
+
+test('Formatters in child logger', async ({ match }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      level (label, number) {
+        return {
+          log: {
+            level: label
+          }
+        }
+      },
+      bindings (bindings) {
+        return {
+          process: {
+            pid: bindings.pid
+          },
+          host: {
+            name: bindings.hostname
+          }
+        }
+      },
+      log (obj) {
+        return { hello: 'world', ...obj }
+      }
+    }
+  }, stream)
+
+  const child = logger.child({
+    foo: 'bar',
+    nested: { object: true },
+    formatters: {
+      bindings (bindings) {
+        return { ...bindings, faz: 'baz' }
+      }
+    }
+  })
+
+  const o = once(stream, 'data')
+  child.info('hello world')
+  match(await o, {
+    log: {
+      level: 'info'
+    },
+    process: {
+      pid: process.pid
+    },
+    host: {
+      name: hostname()
+    },
+    hello: 'world',
+    foo: 'bar',
+    nested: { object: true },
+    faz: 'baz'
+  })
+})
+
+test('Formatters without bindings in child logger', async ({ match }) => {
+  const stream = sink()
+  const logger = pino({
+    formatters: {
+      level (label, number) {
+        return {
+          log: {
+            level: label
+          }
+        }
+      },
+      bindings (bindings) {
+        return {
+          process: {
+            pid: bindings.pid
+          },
+          host: {
+            name: bindings.hostname
+          }
+        }
+      },
+      log (obj) {
+        return { hello: 'world', ...obj }
+      }
+    }
+  }, stream)
+
+  const child = logger.child({
+    foo: 'bar',
+    nested: { object: true },
+    formatters: {
+      log (obj) {
+        return { other: 'stuff', ...obj }
+      }
+    }
+  })
+
+  const o = once(stream, 'data')
+  child.info('hello world')
+  match(await o, {
+    log: {
+      level: 'info'
+    },
+    process: {
+      pid: process.pid
+    },
+    host: {
+      name: hostname()
+    },
+    foo: 'bar',
+    other: 'stuff',
+    nested: { object: true }
+  })
+})
+
+test('elastic common schema format', async ({ match, type }) => {
+  const stream = sink()
+  const ecs = {
+    formatters: {
+      level (label, number) {
+        return {
+          log: {
+            level: label,
+            logger: 'pino'
+          }
+        }
+      },
+      bindings (bindings) {
+        return {
+          process: {
+            pid: bindings.pid
+          },
+          host: {
+            name: bindings.hostname
+          }
+        }
+      },
+      log (obj) {
+        return { ecs: { version: '1.4.0' }, ...obj }
+      }
+    },
+    messageKey: 'message',
+    timestamp: () => `,"@timestamp":"${new Date(Date.now()).toISOString()}"`
+  }
+
+  const logger = pino({ ...ecs }, stream)
+
+  const o = once(stream, 'data')
+  logger.info({ foo: 'bar' }, 'hello world')
+  const log = await o
+  type(log['@timestamp'], 'string')
+  match(log, {
+    log: { level: 'info', logger: 'pino' },
+    process: { pid: process.pid },
+    host: { name: hostname() },
+    ecs: { version: '1.4.0' },
+    foo: 'bar',
+    message: 'hello world'
+  })
+})
diff --git a/test/helper.js b/test/helper.js
index 1074099..94fa6bb 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -5,7 +5,6 @@ const writer = require('flush-write-stream')
 const split = require('split2')
 const pid = process.pid
 const hostname = os.hostname()
-const v = 1
 
 const isWin = process.platform === 'win32'
 
@@ -43,7 +42,6 @@ function check (is, chunk, level, msg) {
   is(chunk.hostname, hostname)
   is(chunk.level, level)
   is(chunk.msg, msg)
-  is(chunk.v, v)
 }
 
 function sleep (ms) {
diff --git a/test/http.test.js b/test/http.test.js
index 1c2023e..868bce1 100644
--- a/test/http.test.js
+++ b/test/http.test.js
@@ -20,7 +20,6 @@ test('http request support', async ({ ok, same, error, teardown }) => {
       hostname: hostname,
       level: 30,
       msg: 'my request',
-      v: 1,
       req: {
         method: originalReq.method,
         url: originalReq.url,
@@ -59,7 +58,6 @@ test('http request support via serializer', async ({ ok, same, error, teardown }
       hostname: hostname,
       level: 30,
       msg: 'my request',
-      v: 1,
       req: {
         method: originalReq.method,
         url: originalReq.url,
@@ -99,7 +97,6 @@ test('http request support via serializer without request connection', async ({
       hostname: hostname,
       level: 30,
       msg: 'my request',
-      v: 1,
       req: {
         method: originalReq.method,
         url: originalReq.url,
@@ -139,10 +136,9 @@ test('http response support', async ({ ok, same, error, teardown }) => {
       hostname: hostname,
       level: 30,
       msg: 'my response',
-      v: 1,
       res: {
         statusCode: originalRes.statusCode,
-        headers: originalRes._headers
+        headers: originalRes.getHeaders()
       }
     })
   }))
@@ -176,7 +172,6 @@ test('http response support via a serializer', async ({ ok, same, error, teardow
       hostname: hostname,
       level: 30,
       msg: 'my response',
-      v: 1,
       res: {
         statusCode: 200,
         headers: {
@@ -218,7 +213,6 @@ test('http request support via serializer in a child', async ({ ok, same, error,
       hostname: hostname,
       level: 30,
       msg: 'my request',
-      v: 1,
       req: {
         method: originalReq.method,
         url: originalReq.url,
diff --git a/test/levels.test.js b/test/levels.test.js
index db5f321..dd79970 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -4,6 +4,10 @@ const { test } = require('tap')
 const { sink, once, check } = require('./helper')
 const pino = require('../')
 
+// Silence all warnings for this test
+process.removeAllListeners('warning')
+process.on('warning', () => {})
+
 test('set the level by string', async ({ is }) => {
   const expected = [{
     level: 50,
@@ -233,7 +237,13 @@ test('produces labels when told to', async ({ is }) => {
     level: 'info',
     msg: 'hello world'
   }]
-  const instance = pino({ useLevelLabels: true }, sink((result, enc, cb) => {
+  const instance = pino({
+    formatters: {
+      level (label, number) {
+        return { level: label }
+      }
+    }
+  }, sink((result, enc, cb) => {
     const current = expected.shift()
     check(is, result, current.level, current.msg)
     cb()
@@ -271,7 +281,13 @@ test('changes label naming when told to', async ({ is }) => {
     priority: 30,
     msg: 'hello world'
   }]
-  const instance = pino({ levelKey: 'priority' }, sink((result, enc, cb) => {
+  const instance = pino({
+    formatters: {
+      level (label, number) {
+        return { priority: number }
+      }
+    }
+  }, sink((result, enc, cb) => {
     const current = expected.shift()
     is(result.priority, current.priority)
     is(result.msg, current.msg)
@@ -292,7 +308,13 @@ test('children produce labels when told to', async ({ is }) => {
       msg: 'child 2'
     }
   ]
-  const instance = pino({ useLevelLabels: true }, sink((result, enc, cb) => {
+  const instance = pino({
+    formatters: {
+      level (label, number) {
+        return { level: label }
+      }
+    }
+  }, sink((result, enc, cb) => {
     const current = expected.shift()
     check(is, result, current.level, current.msg)
     cb()
@@ -317,7 +339,11 @@ test('produces labels for custom levels', async ({ is }) => {
     }
   ]
   const opts = {
-    useLevelLabels: true,
+    formatters: {
+      level (label, number) {
+        return { level: label }
+      }
+    },
     customLevels: {
       foo: 35
     }
@@ -335,8 +361,11 @@ test('produces labels for custom levels', async ({ is }) => {
 test('setting levelKey does not affect labels when told to', async ({ is }) => {
   const instance = pino(
     {
-      useLevelLabels: true,
-      levelKey: 'priority'
+      formatters: {
+        level (label, number) {
+          return { priority: label }
+        }
+      }
     },
     sink((result, enc, cb) => {
       is(result.priority, 'info')
@@ -412,6 +441,36 @@ test('passes when creating a default value that exists in logger levels', async
   })
 })
 
+test('log null value when message is null', async ({ is }) => {
+  const expected = {
+    msg: null,
+    level: 30
+  }
+
+  const stream = sink()
+  const instance = pino(stream)
+  instance.level = 'info'
+  instance.info(null)
+
+  const result = await once(stream, 'data')
+  check(is, result, expected.level, expected.msg)
+})
+
+test('formats when base param is null', async ({ is }) => {
+  const expected = {
+    msg: 'a string',
+    level: 30
+  }
+
+  const stream = sink()
+  const instance = pino(stream)
+  instance.level = 'info'
+  instance.info(null, 'a %s', 'string')
+
+  const result = await once(stream, 'data')
+  check(is, result, expected.level, expected.msg)
+})
+
 test('fatal method sync-flushes the destination if sync flushing is available', async ({ pass, doesNotThrow, plan }) => {
   plan(2)
   const stream = sink()
diff --git a/test/metadata.test.js b/test/metadata.test.js
index d902d3c..7fb1c9a 100644
--- a/test/metadata.test.js
+++ b/test/metadata.test.js
@@ -14,7 +14,6 @@ test('metadata works', async ({ ok, same, is }) => {
     write (chunk) {
       is(instance, this.lastLogger)
       is(30, this.lastLevel)
-      is('a msg', this.lastMsg)
       ok(Number(this.lastTime) >= now)
       same(this.lastObj, { hello: 'world', msg: 'a msg' })
       const result = JSON.parse(chunk)
@@ -25,8 +24,7 @@ test('metadata works', async ({ ok, same, is }) => {
         hostname: hostname,
         level: 30,
         hello: 'world',
-        msg: 'a msg',
-        v: 1
+        msg: 'a msg'
       })
     }
   })
@@ -40,7 +38,6 @@ test('child loggers works', async ({ ok, same, is }) => {
     write (chunk) {
       is(child, this.lastLogger)
       is(30, this.lastLevel)
-      is('a msg', this.lastMsg)
       same(this.lastObj, { from: 'child', msg: 'a msg' })
       const result = JSON.parse(chunk)
       ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
@@ -51,8 +48,7 @@ test('child loggers works', async ({ ok, same, is }) => {
         level: 30,
         hello: 'world',
         from: 'child',
-        msg: 'a msg',
-        v: 1
+        msg: 'a msg'
       })
     }
   })
@@ -67,7 +63,6 @@ test('without object', async ({ ok, same, is }) => {
     write (chunk) {
       is(instance, this.lastLogger)
       is(30, this.lastLevel)
-      is('a msg', this.lastMsg)
       same({ msg: 'a msg' }, this.lastObj)
       const result = JSON.parse(chunk)
       ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
@@ -76,8 +71,7 @@ test('without object', async ({ ok, same, is }) => {
         pid: pid,
         hostname: hostname,
         level: 30,
-        msg: 'a msg',
-        v: 1
+        msg: 'a msg'
       })
     }
   })
@@ -100,8 +94,7 @@ test('without msg', async ({ ok, same, is }) => {
         pid: pid,
         hostname: hostname,
         level: 30,
-        hello: 'world',
-        v: 1
+        hello: 'world'
       })
     }
   })
diff --git a/test/mixin.test.js b/test/mixin.test.js
index 057d766..8ec94ac 100644
--- a/test/mixin.test.js
+++ b/test/mixin.test.js
@@ -28,8 +28,7 @@ test('mixin object is included', async ({ ok, same }) => {
     hostname,
     level,
     msg: 'test',
-    hello: 1,
-    v: 1
+    hello: 1
   })
 })
 
@@ -58,8 +57,7 @@ test('mixin object is new every time', async ({ plan, ok, same }) => {
       hostname,
       level,
       msg,
-      hello: n,
-      v: 1
+      hello: n
     })
   }
 })
@@ -93,8 +91,7 @@ test('mixin object + logged object', async ({ ok, same }) => {
     level,
     foo: 1,
     bar: 3,
-    baz: 4,
-    v: 1
+    baz: 4
   })
 })
 
diff --git a/test/pretty.test.js b/test/pretty.test.js
index 4017d0c..b9f2223 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -7,13 +7,7 @@ const execa = require('execa')
 const writer = require('flush-write-stream')
 const { once } = require('./helper')
 const pino = require('../')
-const tap = require('tap')
-
-const isWin = process.platform === 'win32'
-if (isWin) {
-  tap.comment('Skipping pretty printing tests on Windows as colour codes are different and tests fail')
-  process.exit(0)
-}
+const strip = require('strip-ansi')
 
 test('can be enabled via exported pino function', async ({ isNot }) => {
   var actual = ''
@@ -24,7 +18,7 @@ test('can be enabled via exported pino function', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): h/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): h/), null)
 })
 
 test('can be enabled via exported pino function with pretty configuration', async ({ isNot }) => {
@@ -36,7 +30,7 @@ test('can be enabled via exported pino function with pretty configuration', asyn
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/^INFO.*h/), null)
+  isNot(strip(actual).match(/^INFO.*h/), null)
 })
 
 test('can be enabled via exported pino function with prettifier', async ({ isNot }) => {
@@ -49,7 +43,7 @@ test('can be enabled via exported pino function with prettifier', async ({ isNot
   }))
 
   await once(child, 'close')
-  isNot(actual.match(/^INFO.*h/), null)
+  isNot(strip(actual).match(/^INFO.*h/), null)
 })
 
 test('does not throw error when enabled with stream specified', async ({ doesNotThrow }) => {
@@ -111,11 +105,11 @@ test('parses and outputs chindings', async ({ is, isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): h/), null)
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): h2/), null)
-  isNot(actual.match(/a: 1/), null)
-  isNot(actual.match(/b: 2/), null)
-  is(actual.match(/a: 1/g).length, 3)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): h/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): h2/), null)
+  isNot(strip(actual).match(/a: 1/), null)
+  isNot(strip(actual).match(/b: 2/), null)
+  is(strip(actual).match(/a: 1/g).length, 3)
 })
 
 test('applies serializers', async ({ is, isNot }) => {
@@ -127,8 +121,8 @@ test('applies serializers', async ({ is, isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): h/), null)
-  isNot(actual.match(/foo: "bar"/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): h/), null)
+  isNot(strip(actual).match(/foo: "bar"/), null)
 })
 
 test('applies redaction rules', async ({ is, isNot }) => {
@@ -140,9 +134,9 @@ test('applies redaction rules', async ({ is, isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): h/), null)
-  isNot(actual.match(/\[Redacted\]/), null)
-  is(actual.match(/object/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): h/), null)
+  isNot(strip(actual).match(/\[Redacted\]/), null)
+  is(strip(actual).match(/object/), null)
 })
 
 test('dateformat', async ({ isNot }) => {
@@ -154,7 +148,7 @@ test('dateformat', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): h/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): h/), null)
 })
 
 test('without timestamp', async ({ isNot }) => {
@@ -166,7 +160,7 @@ test('without timestamp', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.slice(2), '[]')
+  isNot(strip(actual).slice(2), '[]')
 })
 
 test('with custom timestamp', async ({ is }) => {
@@ -178,7 +172,7 @@ test('with custom timestamp', async ({ is }) => {
     cb()
   }))
   await once(child, 'close')
-  is(actual.slice(0, 8), '["test"]')
+  is(strip(actual).slice(0, 8), '["test"]')
 })
 
 test('with custom timestamp label', async ({ is }) => {
@@ -190,7 +184,7 @@ test('with custom timestamp label', async ({ is }) => {
     cb()
   }))
   await once(child, 'close')
-  is(actual.slice(0, 8), '["test"]')
+  is(strip(actual).slice(0, 8), '["test"]')
 })
 
 test('errors', async ({ isNot }) => {
@@ -202,9 +196,9 @@ test('errors', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): kaboom/), null)
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): with a message/), null)
-  isNot(actual.match(/.*error\.js.*/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): kaboom/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): with a message/), null)
+  isNot(strip(actual).match(/.*error\.js.*/), null)
 })
 
 test('errors with props', async ({ isNot }) => {
@@ -216,10 +210,10 @@ test('errors with props', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): kaboom/), null)
-  isNot(actual.match(/code: ENOENT/), null)
-  isNot(actual.match(/errno: 1/), null)
-  isNot(actual.match(/.*error-props\.js.*/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): kaboom/), null)
+  isNot(strip(actual).match(/code: ENOENT/), null)
+  isNot(strip(actual).match(/errno: 1/), null)
+  isNot(strip(actual).match(/.*error-props\.js.*/), null)
 })
 
 test('final works with pretty', async ({ isNot }) => {
@@ -231,8 +225,8 @@ test('final works with pretty', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/WARN\s+\(123456 on abcdefghijklmnopqr\): pino.final with prettyPrint does not support flushing/), null)
-  isNot(actual.match(/INFO\s+\(123456 on abcdefghijklmnopqr\): beforeExit/), null)
+  isNot(strip(actual).match(/WARN\s+\(123456 on abcdefghijklmnopqr\): pino.final with prettyPrint does not support flushing/), null)
+  isNot(strip(actual).match(/INFO\s+\(123456 on abcdefghijklmnopqr\): beforeExit/), null)
 })
 
 test('final works when returning a logger', async ({ isNot }) => {
@@ -244,8 +238,8 @@ test('final works when returning a logger', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/WARN\s+\(123456 on abcdefghijklmnopqr\): pino.final with prettyPrint does not support flushing/), null)
-  isNot(actual.match(/INFO\s+\(123456 on abcdefghijklmnopqr\): after/), null)
+  isNot(strip(actual).match(/WARN\s+\(123456 on abcdefghijklmnopqr\): pino.final with prettyPrint does not support flushing/), null)
+  isNot(strip(actual).match(/INFO\s+\(123456 on abcdefghijklmnopqr\): after/), null)
 })
 
 test('final works without prior logging', async ({ isNot }) => {
@@ -257,8 +251,8 @@ test('final works without prior logging', async ({ isNot }) => {
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/WARN\s+: pino.final with prettyPrint does not support flushing/), null)
-  isNot(actual.match(/INFO\s+\(123456 on abcdefghijklmnopqr\): beforeExit/), null)
+  isNot(strip(actual).match(/WARN\s+: pino.final with prettyPrint does not support flushing/), null)
+  isNot(strip(actual).match(/INFO\s+\(123456 on abcdefghijklmnopqr\): beforeExit/), null)
 })
 
 test('works as expected with an object with the msg prop', async ({ isNot }) => {
@@ -270,7 +264,7 @@ test('works as expected with an object with the msg prop', async ({ isNot }) =>
     cb()
   }))
   await once(child, 'close')
-  isNot(actual.match(/\(123456 on abcdefghijklmnopqr\): hello/), null)
+  isNot(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): hello/), null)
 })
 
 test('should not lose stream metadata for streams with `needsMetadataGsym` flag', async ({ isNot }) => {
diff --git a/test/redact.test.js b/test/redact.test.js
index be27cc1..34a58de 100644
--- a/test/redact.test.js
+++ b/test/redact.test.js
@@ -116,7 +116,7 @@ test('redact option – interpolated object', async ({ is }) => {
   const stream = sink()
   const instance = pino({ redact: ['req.headers.cookie'] }, stream)
 
-  instance.info('test', {
+  instance.info('test %j', {
     req: {
       id: 7915,
       method: 'GET',
@@ -180,7 +180,7 @@ test('redact.paths option – interpolated object', async ({ is }) => {
   const stream = sink()
   const instance = pino({ redact: { paths: ['req.headers.cookie'] } }, stream)
 
-  instance.info('test', {
+  instance.info('test %j', {
     req: {
       id: 7915,
       method: 'GET',
diff --git a/test/serializers.test.js b/test/serializers.test.js
index 70e8a6a..d2d871c 100644
--- a/test/serializers.test.js
+++ b/test/serializers.test.js
@@ -206,40 +206,3 @@ test('non-overridden serializers are available in the children', async ({ is })
   parent.fatal({ onlyChild: 'test' })
   is((await o4).onlyChild, 'test')
 })
-
-test('Symbol.for(\'pino.*\') serializer', async ({ notSame, is, isNot }) => {
-  const stream = sink()
-  const globalSerializer = {
-    [Symbol.for('pino.*')]: function (obj) {
-      if (obj.lionel === 'richie') {
-        return { hello: 'is', it: 'me', you: 'are', looking: 'for' }
-      }
-      return { lionel: 'richie' }
-    }
-  }
-
-  const logger = pino({ serializers: globalSerializer }, stream)
-
-  const o = once(stream, 'data')
-  logger.info({ hello: 'is', it: 'me', you: 'are', looking: 'for' })
-  is((await o).lionel, 'richie')
-  isNot((await o).hello, 'is')
-  isNot((await o).it, 'me')
-  isNot((await o).you, 'are')
-  isNot((await o).looking, 'for')
-
-  const o2 = once(stream, 'data')
-  logger.info({ lionel: 'richie' })
-  is((await o2).lionel, 'richie')
-  is((await o2).hello, 'is')
-  is((await o2).it, 'me')
-  is((await o2).you, 'are')
-  is((await o2).looking, 'for')
-
-  const o3 = once(stream, 'data')
-  logger.info('message')
-  is((await o3).lionel, 'richie')
-  is('pid' in (await o3), false)
-  is('hostname' in (await o3), false)
-  notSame(await o3, ['pid', 'hostname'])
-})
diff --git a/test/extreme.test.js b/test/syncfalse.test.js
similarity index 73%
rename from test/extreme.test.js
rename to test/syncfalse.test.js
index 7145d45..f56560b 100644
--- a/test/extreme.test.js
+++ b/test/syncfalse.test.js
@@ -42,7 +42,7 @@ test('extreme mode', async ({ is, teardown }) => {
   var expected2 = expected.split('\n')[0]
   var actual2 = ''
 
-  const child = fork(join(__dirname, '/fixtures/extreme.js'), { silent: true })
+  const child = fork(join(__dirname, '/fixtures/syncfalse.js'), { silent: true })
   child.stdout.pipe(writer((s, enc, cb) => {
     actual2 += s
     cb()
@@ -58,7 +58,7 @@ test('extreme mode', async ({ is, teardown }) => {
   })
 })
 
-test('extreme mode with child', async ({ is, teardown }) => {
+test('sync false with child', async ({ is, teardown }) => {
   const now = Date.now
   const hostname = os.hostname
   const proc = process
@@ -96,7 +96,7 @@ test('extreme mode with child', async ({ is, teardown }) => {
   var expected2 = expected.split('\n')[0]
   var actual2 = ''
 
-  const child = fork(join(__dirname, '/fixtures/extreme-child.js'), { silent: true })
+  const child = fork(join(__dirname, '/fixtures/syncfalse-child.js'), { silent: true })
   child.stdout.pipe(writer((s, enc, cb) => {
     actual2 += s
     cb()
@@ -119,7 +119,24 @@ test('throw an error if extreme is passed', async ({ throws }) => {
   })
 })
 
-test('flush does nothing without extreme mode', async () => {
+test('flush does nothing with sync true (default)', async () => {
   var instance = require('..')()
   instance.flush()
 })
+
+test('pino.extreme() emits a warning', async ({ is }) => {
+  const pino = require('..')
+  process.removeAllListeners('warning')
+  process.nextTick(() => pino.extreme(0))
+  const warning = await once(process, 'warning')
+  const expected = 'The pino.extreme() option is deprecated and will be removed in v7. Use pino.destination({ sync: false }) instead.'
+  is(expected, warning.message)
+  is('extreme_deprecation', warning.code)
+})
+
+test('pino.extreme() defaults to stdout', async ({ is }) => {
+  const pino = require('..')
+  process.removeAllListeners('warning')
+  const dest = pino.extreme()
+  is(dest.fd, process.stdout.fd)
+})