Repository: pinojs/pino
Pull request #1070 (2021-07-20 15:05:05)

build(deps): bump actions/setup-node from 2.2.0 to 2.3.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.2.0 to 2.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/setup-node/releases">actions/setup-node's releases</a>.</em></p>
<blockquote>
<h2>Support caching pnpm dependencies</h2>
<p>This release introduces dependency caching support for the <code>pnpm</code> package manager (<a href="https://github-redirect.dependabot.com/actions/setup-node/issues/278">#278</a>).</p>
<p><strong>Caching pnpm dependencies:</strong></p>
<pre lang="yaml"><code># This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
<p>steps:</p>
<ul>
<li>uses: actions/checkout@v2</li>
<li>uses: pnpm/action-setup@646cdf48217256a3d0b80361c5a50727664284f2
with:
version: 6.10.0</li>
<li>uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'pnpm'</li>
<li>run: pnpm install</li>
<li>run: pnpm test
</code></pre></li>
</ul>
<p><strong>NOTE</strong>: pnpm caching support requires pnpm version &gt;= 6.10.0</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/setup-node/commit/aa759c6c94d3800c55b8601f21ba4b2371704cb7"><code>aa759c6</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/actions/setup-node/issues/278">#278</a> from jacobwgillespie/cache-pnpm</li>
<li><a href="https://github.com/actions/setup-node/commit/0ae03de2b799ae88b378c51b87476d242b3d8ef0"><code>0ae03de</code></a> Reorder to npm, yarn, pnpm</li>
<li><a href="https://github.com/actions/setup-node/commit/4bc87b8e10a7f0f76173ad410d874a0b1f984af0"><code>4bc87b8</code></a> Bump e2e tests to 6.10.0</li>
<li><a href="https://github.com/actions/setup-node/commit/b96348a4e97e0510304e475e5cec7a7a05518c5d"><code>b96348a</code></a> Remove unused imports</li>
<li><a href="https://github.com/actions/setup-node/commit/3af302a4f220de9ff48cd21cc1ab8e9c54382600"><code>3af302a</code></a> Switch to pnpm store path command</li>
<li><a href="https://github.com/actions/setup-node/commit/f452812b440ff2d1b0ee9d300a037464407004e5"><code>f452812</code></a> Unmock fs.existsSync after tests</li>
<li><a href="https://github.com/actions/setup-node/commit/e93556ca6645851a427f77742aeb1b22a95b4700"><code>e93556c</code></a> Mock fs.existsSync in tests</li>
<li><a href="https://github.com/actions/setup-node/commit/0453e516eb2a16fb262fa8f9db51405c7089b869"><code>0453e51</code></a> Regenerate compiled files</li>
<li><a href="https://github.com/actions/setup-node/commit/399982b36805ac9d772a419dc9f95fb5a8eb5c55"><code>399982b</code></a> Move existence check to cache-save</li>
<li><a href="https://github.com/actions/setup-node/commit/d278e78bddf957771f319214444100b4032fb7aa"><code>d278e78</code></a> Add logic to check that cache folder exists</li>
<li>Additional commits viewable in <a href="https://github.com/actions/setup-node/compare/v2.2.0...v2.3.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2.2.0&new-version=2.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..d058c83
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+pino.d.ts
+test/types/pino.test-d.ts
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 929f886..7ee9d26 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -19,7 +19,7 @@ jobs:
         with:
           ref: ${{ github.base_ref }}
       - name: Setup Node
-        uses: actions/setup-node@v2.2.0
+        uses: actions/setup-node@v2.3.0
         with:
           node-version: 12
       - name: Install Modules
@@ -39,7 +39,7 @@ jobs:
       - name: Checkout Code
         uses: actions/checkout@v2.3.4
       - name: Setup Node
-        uses: actions/setup-node@v2.2.0
+        uses: actions/setup-node@v2.3.0
         with:
           node-version: 12
       - name: Install Modules
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eca8e6d..de2e13c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,11 +16,11 @@ jobs:
       fail-fast: false
       matrix:
         os: [macOS-latest, windows-latest, ubuntu-latest]
-        node-version: [10, 12, 13, 14, 16]
+        node-version: [12, 14, 16]
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v2.2.0
+        uses: actions/setup-node@v2.3.0
         with:
           node-version: ${{ matrix.node-version }}
       - name: Install
diff --git a/README.md b/README.md
index 95e0852..499bdf3 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,11 @@
 ## Install
 
 ```
-$ npm install pino
+$ npm install pino@next
 ```
 
+If you would like to install pino v6, refer to https://github.com/pinojs/pino/tree/v6.x.
+
 ## Usage
 
 ```js
@@ -75,9 +77,10 @@ format logs during development:
 
 Due to Node's single-threaded event-loop, it's highly recommended that sending,
 alert triggering, reformatting and all forms of log processing
-is conducted in a separate process. In Pino parlance we call all log processors
-"transports", and recommend that the transports be run as separate
-processes, piping the stdout of the application to the stdin of the transport.
+be conducted in a separate process or thread. 
+
+In Pino terminology we call all log processors "transports", and recommend that the
+transports be run in a worker thread using our `pino.transport` API.
 
 For more details see our [Transports⇗](docs/transports.md) document.
 
diff --git a/benchmarks/multistream.js b/benchmarks/multistream.js
new file mode 100644
index 0000000..d67b7e2
--- /dev/null
+++ b/benchmarks/multistream.js
@@ -0,0 +1,98 @@
+'use strict'
+
+const bench = require('fastbench')
+const bunyan = require('bunyan')
+const pino = require('../')
+const fs = require('fs')
+const dest = fs.createWriteStream('/dev/null')
+
+const tenStreams = [
+  { stream: dest },
+  { stream: dest },
+  { stream: dest },
+  { stream: dest },
+  { stream: dest },
+  { level: 'debug', stream: dest },
+  { level: 'debug', stream: dest },
+  { level: 'trace', stream: dest },
+  { level: 'warn', stream: dest },
+  { level: 'fatal', stream: dest }
+]
+const pinomsTen = pino({ level: 'debug' }, pino.multistream(tenStreams))
+
+const fourStreams = [
+  { stream: dest },
+  { stream: dest },
+  { level: 'debug', stream: dest },
+  { level: 'trace', stream: dest }
+]
+const pinomsFour = pino({ level: 'debug' }, pino.multistream(fourStreams))
+
+const pinomsOne = pino({ level: 'info' }, pino.multistream(dest))
+const blogOne = bunyan.createLogger({
+  name: 'myapp',
+  streams: [{ stream: dest }]
+})
+
+const blogTen = bunyan.createLogger({
+  name: 'myapp',
+  streams: tenStreams
+})
+const blogFour = bunyan.createLogger({
+  name: 'myapp',
+  streams: fourStreams
+})
+
+const max = 10
+const run = bench([
+  function benchBunyanTen (cb) {
+    for (let i = 0; i < max; i++) {
+      blogTen.info('hello world')
+      blogTen.debug('hello world')
+      blogTen.trace('hello world')
+      blogTen.warn('hello world')
+      blogTen.fatal('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchPinoMSTen (cb) {
+    for (let i = 0; i < max; i++) {
+      pinomsTen.info('hello world')
+      pinomsTen.debug('hello world')
+      pinomsTen.trace('hello world')
+      pinomsTen.warn('hello world')
+      pinomsTen.fatal('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchBunyanFour (cb) {
+    for (let i = 0; i < max; i++) {
+      blogFour.info('hello world')
+      blogFour.debug('hello world')
+      blogFour.trace('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchPinoMSFour (cb) {
+    for (let i = 0; i < max; i++) {
+      pinomsFour.info('hello world')
+      pinomsFour.debug('hello world')
+      pinomsFour.trace('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchBunyanOne (cb) {
+    for (let i = 0; i < max; i++) {
+      blogOne.info('hello world')
+    }
+    setImmediate(cb)
+  },
+  function benchPinoMSOne (cb) {
+    for (let i = 0; i < max; i++) {
+      pinomsOne.info('hello world')
+    }
+    setImmediate(cb)
+  }
+], 10000)
+
+run()
diff --git a/docs/api.md b/docs/api.md
index 3f30471..1e9c58b 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -23,7 +23,9 @@
   * [logger.version](#version)
 * [Statics](#statics)
   * [pino.destination()](#pino-destination)
+  * [pino.transport()](#pino-transport)
   * [pino.final()](#pino-final)
+  * [pino.multistream()](#pino-multistream)
   * [pino.stdSerializers](#pino-stdserializers)
   * [pino.stdTimeFunctions](#pino-stdtimefunctions)
   * [pino.symbols](#pino-symbols)
@@ -258,11 +260,11 @@ These functions should return an JSONifiable object and they
 should never throw. When logging an object, each top-level property
 matching the exact key of a serializer will be serialized using the defined serializer.
 
-* See [pino.stdSerializers](#pino-stdserializers)
-
-##### `serializers[Symbol.for('pino.*')]` (Function) - DEPRECATED
+The serializers are applied when a property in the logged object matches a property
+in the serializers. The only exception is the `err` serializer as it is also applied in case
+the object is an instance of `Error`, e.g. `logger.info(new Error('kaboom'))`.
 
-Use `formatters.log` instead.
+* See [pino.stdSerializers](#pino-stdserializers)
 
 #### `base` (Object)
 
@@ -358,20 +360,6 @@ must be installed as a separate dependency:
 npm install pino-pretty
 ```
 
-<a id="useLevelLabels"></a>
-#### `useLevelLabels` (Boolean) - DEPRECATED
-
-Use `formatters.level` instead. This will be removed in v7.
-
-<a id="changeLevelName"></a>
-#### `changeLevelName` (String) - DEPRECATED
-Use `formatters.level` instead. This will be removed in v7.
-
-<a id="levelKey"></a>
-#### `levelKey` (String) - DEPRECATED
-
-Use `formatters.level` instead. This will be removed in v7.
-
 #### `browser` (Object)
 
 Browser only, may have `asObject` and `write` keys. This option is separately
@@ -479,6 +467,9 @@ logger.info({MIX: {IN: true}})
 // {"level":30,"time":1531254555820,"pid":55956,"hostname":"x","MIX":{"IN":true}}
 ```
 
+If the object is of type Error, it is wrapped in an object containing a property err (`{ err: mergingObject }`).
+This allows for a unified error handling flow.
+
 <a id="message"></a>
 #### `message` (String)
 
@@ -500,6 +491,9 @@ the `message` parameter not the value of the `msg` property on the `mergedObject
 See [Avoid Message Conflict](/docs/help.md#avoid-message-conflict) for information
 on how to overcome this limitation.
 
+If no `message` parameter is provided, and the `mergedObject` is of type `Error` or it has a property named `err`, the 
+`message` parameter is set to the `message` value of the error.
+
 The `messageKey` option can be used at instantiation time to change the namespace
 from `msg` to another string as preferred.
 
@@ -918,6 +912,104 @@ A `pino.destination` instance can also be used to reopen closed files
 * See [Reopening log files](/docs/help.md#reopening)
 * See [Asynchronous Logging ⇗](/docs/asynchronous.md)
 
+<a id="pino-transport"></a>
+### `pino.transport(options) => ThreadStream`
+
+Create a a stream that routes logs to a worker thread that 
+wraps around a [Pino Transport](/docs/transports.md).
+
+```js
+const pino = require('pino')
+const transport = pino.transport({
+  target: 'some-transport',
+  options: { some: 'options for', the: 'transport' }
+})
+pino(transport)
+```
+
+Multiple transports may also be defined, and specific levels can be logged to each transport:
+
+```js
+const pino = require('pino')
+const transports = pino.transport({
+  targets: [{
+    level: 'info',
+    target: 'some-transport',
+    options: { some: 'options for', the: 'transport' }
+  }, {
+    level: 'trace',
+    target: '#pino/file',
+    options: { destination: '/path/to/store/logs' }
+  }]
+})
+pino(transports)
+```
+
+If `WeakRef`, `WeakMap` and `FinalizationRegistry` are available in the current runtime (v14.5.0+), then the thread
+will be automatically terminated in case the stream or logger goes out of scope.
+The `transport()` function adds a listener to `process.on('beforeExit')` and `process.on('exit')` to ensure the worker
+is flushed and all data synced before the process exits.
+
+Note that calling `process.exit()` on the main thread will stop the event loop on the main thread from turning. As a result,
+using `console.log` and `process.stdout` after the main thread called `process.exit()` will not produce any output.
+
+For more on transports, how they work, and how to create them see the [`Transports documentation`](/docs/transports.md).
+
+* See [`Transports`](/docs/transports.md)
+* See [`thread-stream` ⇗](https://github.com/mcollina/thread-stream)
+
+#### Options
+
+* `target`:  The transport to pass logs through. This may be an installed module name, an absolute path or a built-in transport (see [Transport Builtins](#transport-builtins))
+* `options`:  An options object which is serialized (see [Structured Clone Algorithm][https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm]), passed to the worker thread, parsed and then passed to the exported transport function.
+* `worker`: [Worker thread](https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options) configuration options. Additionally, the `worker` option supports `worker.autoEnd`. If this is set to `false` logs will not be flushed on process exit. It is then up to the developer to call `transport.end()` to flush logs.
+* `targets`: May be specified instead of `target`. Must be an array of transport configurations. Transport configurations include the aforementioned `options` and `target` options plus a `level` option which will send only logs above a specified level to a transport.
+
+#### Transport Builtins
+
+The `target` option may be an absolute path, a module name or builtin. 
+
+A transport builtin takes the form `#pino/<name>`.
+
+The following transport builtins are supported:
+
+#####  `#pino/file`
+
+The `#pino/file` builtin routes logs to a file (or file descriptor).
+
+The `options.destination` property may be set to specify the desired file destination.
+
+```js
+const pino = require('pino')
+const transport = pino.transport({
+  target: '#pino/file',
+  options: { destination: '/path/to/file' }
+})
+pino(transport)
+```
+
+The `options.destination` property may also be a number to represent a file descriptor. Typically this would be `1` to write to STDOUT or `2` to write to STDERR. If `options.destination` is not set, it defaults to `1` which means logs will be written to STDOUT.
+
+The difference between using the `#pino/file` transport builtin and using `pino.destination` is that `pino.destination` runs in the main thread, whereas `#pino/file` sets up `pino.destination` in a worker thread.
+
+##### `#pino/pretty`
+
+The `#pino/pretty` builtin prettifies logs.
+
+
+By default the `#pino/pretty` builtin logs to STDOUT.
+
+The `options.destination` property may be set to log pretty logs to a file descriptor or file. The following would send the prettified logs to STDERR:
+
+```js
+const pino = require('pino')
+const transport = pino.transport({
+  target: '#pino/pretty',
+  options: { destination: 2 }
+})
+pino(transport)
+```
+
 <a id="pino-final"></a>
 
 ### `pino.final(logger, [handler]) => Function | FinalLogger`
@@ -967,6 +1059,73 @@ finalLogger.info('exiting...')
 * See [Asynchronous logging ⇗](/docs/asynchronous.md)
 * See [Log loss prevention ⇗](/docs/asynchronous.md#log-loss-prevention)
 
+<a id="pino-multistream"></a>
+
+### `pino.multistream(options) => Stream`
+
+Create a stream composed by multiple destination streams:
+
+```js
+var fs = require('fs')
+var pino = require('pino')
+var streams = [
+  {stream: fs.createWriteStream('/tmp/info.stream.out')},
+  {level: 'debug', stream: fs.createWriteStream('/tmp/debug.stream.out')},
+  {level: 'fatal', stream: fs.createWriteStream('/tmp/fatal.stream.out')}
+]
+
+var log = pino({
+  level: 'debug' // this MUST be set at the lowest level of the
+                 // destinations
+}, pino.multistream(streams))
+
+log.debug('this will be written to /tmp/debug.stream.out')
+log.info('this will be written to /tmp/debug.stream.out and /tmp/info.stream.out')
+log.fatal('this will be written to /tmp/debug.stream.out, /tmp/info.stream.out and /tmp/fatal.stream.out')
+```
+
+In order for `multistream` to work, the log level __must__ be set to the lowest level used in the streams array.
+
+#### Options
+
+* `levels`:  Pass custom log level definitions to the instance as an object.
+
++ `dedupe`: Set this to `true` to send logs only to the stream with the higher level. Default: `false`
+
+    `dedupe` flag can be useful for example when using pino-multi-stream to redirect `error` logs to `process.stderr` and others to `process.stdout`:
+
+    ```js
+    var pino = require('pino')
+    var multistream = pino.multistream
+    var streams = [
+      {stream: process.stdout},
+      {level: 'error', stream: process.stderr},
+    ]
+
+    var opts = {
+        levels: {
+            silent: Infinity,
+            fatal: 60,
+            error: 50,
+            warn: 50,
+            info: 30,
+            debug: 20,
+            trace: 10
+        },
+        dedupe: true,
+    }
+
+    var log = pino({
+      level: 'debug' // this MUST be set at the lowest level of the
+                    // destinations
+    }, multistream(streams, opts))
+
+    log.debug('this will be written ONLY to process.stdout')
+    log.info('this will be written ONLY to process.stdout')
+    log.error('this will be written ONLY to process.stderr')
+    log.fatal('this will be written ONLY to process.stderr')
+    ```
+
 <a id="pino-stdserializers"></a>
 ### `pino.stdSerializers` (Object)
 
diff --git a/docs/help.md b/docs/help.md
index 2957b52..b713ad6 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -118,20 +118,7 @@ Given a similar scenario as in the [Log rotation](#rotate) section a basic
 <a id="multiple"></a>
 ## Saving to multiple files
 
-Let's assume we want to store all error messages to a separate log file.
-
-Install [pino-tee](https://npm.im/pino-tee) with:
-
-```bash
-npm i pino-tee -g
-```
-
-The following writes the log output of `app.js` to `./all-logs`, while
-writing only warnings and errors to `./warn-log:
-
-```bash
-node app.js | pino-tee warn ./warn-logs > ./all-logs
-```
+See [`pino.multistream`](/doc/api.md#pino-multistream).
 
 <a id="filter-logs"></a>
 ## Log Filtering
@@ -164,14 +151,13 @@ ExecStart=/bin/sh -c '/path/to/node app.js | pino-transport'
 
 Pino's default log destination is the singular destination of `stdout`. While
 not recommended for performance reasons, multiple destinations can be targeted
-by using [`pino-multi-stream`](https://github.com/pinojs/pino-multi-stream).
+by using [`pino.multistream`](/doc/api.md#pino-multistream).
 
 In this example we use `stderr` for `error` level logs and `stdout` as default
 for all other levels (e.g. `debug`, `info`, and `warn`).
 
 ```js
 const pino = require('pino')
-const { multistream } = require('pino-multi-stream')
 var streams = [
   {level: 'debug', stream: process.stdout},
   {level: 'error', stream: process.stderr},
@@ -180,11 +166,10 @@ var streams = [
 
 const logger = pino({
   name: 'my-app',
-  level: 'info',
-}, multistream(streams))
+  level: 'debug', // must be the lowest level of all streams
+}, pino.multistream(streams))
 ```
 
-
 <a id="dupe-keys"></a>
 ## How Pino handles duplicate keys
 
diff --git a/docs/transports.md b/docs/transports.md
index 13ca52c..3ea824f 100644
--- a/docs/transports.md
+++ b/docs/transports.md
@@ -1,79 +1,163 @@
 # Transports
 
-A "transport" for Pino is a supplementary tool which consumes Pino logs.
+Pino transports can be used for both transmitting and transforming log output.
 
-Consider the following example:
+The way Pino generates logs:
 
-```js
-const split = require('split2')
-const pump = require('pump')
-const through = require('through2')
+1. Reduces the impact of logging on an application to the absolute minimum.
+2. Gives greater flexibility in how logs are processed and stored.
+
+It is recommended that any log transformation or transmission is performed either
+in a seperate thread or a seperate process. 
+
+Prior to Pino v7 transports would ideally operate in a seperate process - these are
+now referred to as [Legacy Transports](#legacy-transports).
+
+From Pino v7 and upwards transports can also operate inside a [Worker Thread][worker-thread],
+and can be used or configured via the options object passed to `pino` on initialization.
 
-const myTransport = through.obj(function (chunk, enc, cb) {
-  // do the necessary
-  console.log(chunk)
-  cb()
-})
 
-pump(process.stdin, split(JSON.parse), myTransport)
+[worker-thread]: https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html
+
+## v7+ Transports
+
+A transport is a module that exports a default function which returns a writable stream:
+
+```js
+import { Writable } from 'stream'
+export default (options) => {
+  const myTransportStream = new Writable({
+    write (chunk, enc, cb) {
+    // apply a transform and send to stdout
+    console.log(chunk.toString().toUpperCase())
+    cb()
+    }
+  })
+  return myTransportStream
+}
 ```
 
-The above defines our "transport" as the file `my-transport-process.js`.
+Let's imagine the above defines our "transport" as the file `my-transport.mjs` 
+(ESM files are supported even if the project is written in CJS).
 
-Logs can now be consumed using shell piping:
+We would set up our transport by creating a transport stream with `pino.transport`
+and passing it to the `pino` function:
 
-```sh
-node my-app-which-logs-stuff-to-stdout.js | node my-transport-process.js
+```js
+const pino = require('pino')
+const transport = pino.transport({
+  target: '/absolute/path/to/my-transport.mjs'
+})
+pino(transport)
 ```
 
-Ideally, a transport should consume logs in a separate process to the application,
-Using transports in the same process causes unnecessary load and slows down
-Node's single threaded event loop.
+The transport code will be executed in a separate worker thread. The main thread
+will write logs to the worker thread, which will write them to the stream returned
+from the function exported from the transport file/module.
 
-## In-process transports
+The exported function can also be async. Imagine the following transport:
+
+```js
+import fs from 'fs'
+import { once } from 'events'
+export default async (options) => {
+  const stream = fs.createWriteStream(options.destination)
+  await once(stream, 'open')
+  return stream
+}
+```
 
-> **Pino *does not* natively support in-process transports.**
+While initializing the stream we're able to use `await` to perform asynchronous operations. In this 
+case waiting for the write streams `open` event. 
 
-Pino does not support in-process transports because Node processes are
-single threaded processes (ignoring some technical details). Given this
-restriction, one of the methods Pino employs to achieve its speed is to
-purposefully offload the handling of logs, and their ultimate destination, to
-external processes so that the threading capabilities of the OS can be
-used (or other CPUs).
+Let's imagine the above was published to npm with the module name `some-file-transport`.
 
-One consequence of this methodology is that "error" logs do not get written to
-`stderr`. However, since Pino logs are in a parsable format, it is possible to
-use tools like [pino-tee][pino-tee] or [jq][jq] to work with the logs. For
-example, to view only logs marked as "error" logs:
+The `options.destination` value can be set when the creating the transport stream with `pino.transport` like so:
 
+```js
+const pino = require('pino')
+const transport = pino.transport({
+  target: 'some-file-transport',
+  options: { destination: '/dev/null' }
+})
+pino(transport)
 ```
-$ node an-app.js | jq 'select(.level == 50)'
+
+Note here we've specified a module by package rather than by relative path. The options object we provide
+is serialized and injected into the transport worker thread, then passed to the module's exported function.
+This means that the options object can only contain types that are supported by the 
+[Structured Clone Algorithm][sca] which is used to (de)serializing objects between threads.
+
+What if we wanted to use both transports, but send only error logs to `some-file-transport` while
+sending all logs to `my-transport.mjs`? We can use the `pino.transport` function's `destinations` option:
+
+```js
+const pino = require('pino')
+const transport = pino.transport({
+  targets: [
+    { target: '/absolute/path/to/my-transport.mjs', level: 'error' },
+    { target: 'some-file-transport', options: { destination: '/dev/null' }
+  ]
+})
+pino(transport)
 ```
 
-In short, the way Pino generates logs:
+For more details on `pino.transport` see the [API docs for `pino.transport`][pino-transport].
 
-1. Reduces the impact of logging on an application to the absolute minimum.
-2. Gives greater flexibility in how logs are processed and stored.
 
-Given all of the above, Pino recommends out-of-process log processing.
+[pino-transport]: /docs/api.md#pino-transport
+[sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
+
+
+
+## Legacy Transports
+
+A legacy Pino "transport" is a supplementary tool which consumes Pino logs.
+
+Consider the following example for creating a transport:
+
+```js
+const { pipeline, Writable } = require('stream')
+const split = require('split2')
+
+const myTransportStream = new Writable({
+  write (chunk, enc, cb) {
+  // apply a transform and send to stdout
+  console.log(chunk.toString().toUpperCase())
+  cb()
+  }
+})
+
+pipeline(process.stdin, split(JSON.parse), myTransportStream)
+```
+
+The above defines our "transport" as the file `my-transport-process.js`.
+
+Logs can now be consumed using shell piping:
 
-However, it is possible to wrap Pino and perform processing in-process.
-For an example of this, see [pino-multi-stream][pinoms].
+```sh
+node my-app-which-logs-stuff-to-stdout.js | node my-transport-process.js
+```
 
-[pino-tee]: https://npm.im/pino-tee
-[jq]: https://stedolan.github.io/jq/
-[pinoms]: https://npm.im/pino-multi-stream
+Ideally, a transport should consume logs in a separate process to the application,
+Using transports in the same process causes unnecessary load and slows down
+Node's single threaded event loop.
 
 ## Known Transports
 
 PR's to this document are welcome for any new transports!
 
+### Pino v7+ Compatible
+
++ [pino-elasticsearch](#pino-elasticsearch)
+
+### Legacy
+
 + [pino-applicationinsights](#pino-applicationinsights)
 + [pino-azuretable](#pino-azuretable)
 + [pino-cloudwatch](#pino-cloudwatch)
 + [pino-couch](#pino-couch)
 + [pino-datadog](#pino-datadog)
-+ [pino-elasticsearch](#pino-elasticsearch)
 + [pino-gelf](#pino-gelf)
 + [pino-http-send](#pino-http-send)
 + [pino-kafka](#pino-kafka)
diff --git a/example.js b/examples/basic.js
similarity index 96%
rename from example.js
rename to examples/basic.js
index 866338e..246ce7f 100644
--- a/example.js
+++ b/examples/basic.js
@@ -1,6 +1,6 @@
 'use strict'
 
-const pino = require('./')()
+const pino = require('..')()
 
 pino.info('hello world')
 pino.error('this is at error level')
diff --git a/examples/transport.js b/examples/transport.js
new file mode 100644
index 0000000..25717e4
--- /dev/null
+++ b/examples/transport.js
@@ -0,0 +1,66 @@
+'use strict'
+
+const pino = require('..')
+const { tmpdir } = require('os')
+const { join } = require('path')
+
+const file = join(tmpdir(), `pino-${process.pid}-example`)
+
+const transport = pino.transport({
+  targets: [{
+    level: 'warn',
+    target: '#pino/file',
+    options: {
+      destination: file
+    }
+  }, {
+    level: 'info',
+    target: 'pino-elasticsearch',
+    options: {
+      node: 'http://localhost:9200'
+    }
+  }, {
+    level: 'info',
+    target: '#pino/pretty'
+  }]
+})
+
+const logger = pino(transport)
+
+logger.info({
+  file
+}, 'logging destination')
+
+logger.info('hello world')
+logger.error('this is at error level')
+logger.info('the answer is %d', 42)
+logger.info({ obj: 42 }, 'hello world')
+logger.info({ obj: 42, b: 2 }, 'hello world')
+logger.info({ nested: { obj: 42 } }, 'nested')
+logger.warn('WARNING!')
+setImmediate(() => {
+  logger.info('after setImmediate')
+})
+logger.error(new Error('an error'))
+
+const child = logger.child({ a: 'property' })
+child.info('hello child!')
+
+const childsChild = child.child({ another: 'property' })
+childsChild.info('hello baby..')
+
+logger.debug('this should be mute')
+
+logger.level = 'trace'
+
+logger.debug('this is a debug statement')
+
+logger.child({ another: 'property' }).debug('this is a debug statement via child')
+logger.trace('this is a trace statement')
+
+logger.debug('this is a "debug" statement with "')
+
+logger.info(new Error('kaboom'))
+logger.info(null)
+
+logger.info(new Error('kaboom'), 'with', 'a', 'message')
diff --git a/lib/file-target.js b/lib/file-target.js
new file mode 100644
index 0000000..dde0fae
--- /dev/null
+++ b/lib/file-target.js
@@ -0,0 +1,20 @@
+'use strict'
+
+const { Transform, pipeline } = require('stream')
+const pino = require('../pino')
+const { once } = require('events')
+
+module.exports = async function (opts = {}) {
+  const stream = new Transform({
+    objectMode: true,
+    autoDestroy: true,
+    transform (chunk, enc, cb) {
+      cb(null, chunk.toString())
+    }
+  })
+
+  const destination = pino.destination({ dest: opts.destination || 1, sync: false })
+  await once(destination, 'ready')
+  pipeline(stream, destination, () => {})
+  return stream
+}
diff --git a/lib/levels.js b/lib/levels.js
index df65e5e..e38be0e 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -1,6 +1,5 @@
 'use strict'
 /* eslint no-prototype-builtins: 0 */
-const flatstr = require('flatstr')
 const {
   lsCacheSym,
   levelValSym,
@@ -47,7 +46,7 @@ const nums = Object.keys(levels).reduce((o, k) => {
 }, {})
 
 const initialLsCache = Object.keys(nums).reduce((o, k) => {
-  o[k] = flatstr('{"level":' + Number(k))
+  o[k] = '{"level":' + Number(k)
   return o
 }, {})
 
@@ -188,6 +187,7 @@ module.exports = {
   setLevel,
   isLevelEnabled,
   mappings,
+  levels,
   assertNoLevelCollisions,
   assertDefaultLevelFound
 }
diff --git a/lib/multistream.js b/lib/multistream.js
new file mode 100644
index 0000000..69819b0
--- /dev/null
+++ b/lib/multistream.js
@@ -0,0 +1,142 @@
+'use strict'
+
+const metadata = Symbol.for('pino.metadata')
+const { levels } = require('./levels')
+
+const defaultLevels = Object.create(levels)
+defaultLevels.silent = Infinity
+
+function multistream (streamsArray, opts) {
+  let counter = 0
+
+  streamsArray = streamsArray || []
+  opts = opts || { dedupe: false }
+
+  let levels = defaultLevels
+  if (opts.levels && typeof opts.levels === 'object') {
+    levels = opts.levels
+  }
+
+  const res = {
+    write,
+    add,
+    flushSync,
+    end,
+    minLevel: 0,
+    streams: [],
+    clone,
+    [metadata]: true
+  }
+
+  if (Array.isArray(streamsArray)) {
+    streamsArray.forEach(add, res)
+  } else {
+    add.call(res, streamsArray)
+  }
+
+  // clean this object up
+  // or it will stay allocated forever
+  // as it is closed on the following closures
+  streamsArray = null
+
+  return res
+
+  // we can exit early because the streams are ordered by level
+  function write (data) {
+    let dest
+    const level = this.lastLevel
+    const { streams } = this
+    let stream
+    for (let i = 0; i < streams.length; i++) {
+      dest = streams[i]
+      if (dest.level <= level) {
+        stream = dest.stream
+        if (stream[metadata]) {
+          const { lastTime, lastMsg, lastObj, lastLogger } = this
+          stream.lastLevel = level
+          stream.lastTime = lastTime
+          stream.lastMsg = lastMsg
+          stream.lastObj = lastObj
+          stream.lastLogger = lastLogger
+        }
+        if (!opts.dedupe) {
+          stream.write(data)
+        }
+      } else {
+        break
+      }
+    }
+
+    if (opts.dedupe && stream) {
+      stream.write(data)
+    }
+  }
+
+  function flushSync () {
+    for (const { stream } of this.streams) {
+      if (typeof stream.flushSync === 'function') {
+        stream.flushSync()
+      }
+    }
+  }
+
+  function add (dest) {
+    const { streams } = this
+    if (typeof dest.write === 'function') {
+      return add.call(this, { stream: dest })
+    } else if (typeof dest.levelVal === 'number') {
+      return add.call(this, Object.assign({}, dest, { level: dest.levelVal, levelVal: undefined }))
+    } else if (typeof dest.level === 'string') {
+      return add.call(this, Object.assign({}, dest, { level: levels[dest.level] }))
+    } else if (typeof dest.level !== 'number') {
+      // we default level to 'info'
+      dest = Object.assign({}, dest, { level: 30 })
+    } else {
+      dest = Object.assign({}, dest)
+    }
+    dest.id = counter++
+
+    streams.unshift(dest)
+    streams.sort(compareByLevel)
+
+    this.minLevel = streams[0].level
+
+    return res
+  }
+
+  function end () {
+    for (const { stream } of this.streams) {
+      if (typeof stream.flushSync === 'function') {
+        stream.flushSync()
+      }
+      stream.end()
+    }
+  }
+
+  function clone (level) {
+    const streams = new Array(this.streams.length)
+
+    for (let i = 0; i < streams.length; i++) {
+      streams[i] = {
+        level: level,
+        stream: this.streams[i].stream
+      }
+    }
+
+    return {
+      write,
+      add,
+      minLevel: level,
+      streams,
+      clone,
+      flushSync,
+      [metadata]: true
+    }
+  }
+}
+
+function compareByLevel (a, b) {
+  return a.level - b.level
+}
+
+module.exports = multistream
diff --git a/lib/pretty-target.js b/lib/pretty-target.js
new file mode 100644
index 0000000..5a8d082
--- /dev/null
+++ b/lib/pretty-target.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const pinoPretty = require('pino-pretty')
+const { Transform, pipeline } = require('stream')
+const pino = require('../pino')
+const { once } = require('events')
+
+module.exports = async function (opts = {}) {
+  const pretty = pinoPretty(opts)
+  const stream = new Transform({
+    objectMode: true,
+    autoDestroy: true,
+    transform (chunk, enc, cb) {
+      const line = pretty(chunk.toString())
+      cb(null, line)
+    }
+  })
+
+  const destination = pino.destination({ dest: opts.destination || 1, sync: false })
+  if (destination.fd === 1) {
+    // We cannot close the output
+    destination.end = function () {
+      this.emit('close')
+    }
+  }
+  await once(destination, 'ready')
+  pipeline(stream, destination, () => {})
+  return stream
+}
diff --git a/lib/proto.js b/lib/proto.js
index a38e659..6420632 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -3,8 +3,6 @@
 /* eslint no-prototype-builtins: 0 */
 
 const { EventEmitter } = require('events')
-const SonicBoom = require('sonic-boom')
-const flatstr = require('flatstr')
 const {
   lsCacheSym,
   levelValSym,
@@ -179,22 +177,19 @@ function setBindings (newBindings) {
 function write (_obj, msg, num) {
   const t = this[timeSym]()
   const mixin = this[mixinSym]
-  const objError = _obj instanceof Error
   let obj
 
   if (_obj === undefined || _obj === null) {
     obj = mixin ? mixin({}) : {}
-  } else {
-    obj = Object.assign(mixin ? mixin(_obj) : {}, _obj)
-    if (!msg && objError) {
+  } else if (_obj instanceof Error) {
+    obj = Object.assign(mixin ? mixin({}) : {}, { err: _obj })
+    if (msg === undefined) {
       msg = _obj.message
     }
-
-    if (objError) {
-      obj.stack = _obj.stack
-      if (!obj.type) {
-        obj.type = 'Error'
-      }
+  } else {
+    obj = Object.assign(mixin ? mixin({}) : {}, _obj)
+    if (msg === undefined && _obj.err) {
+      msg = _obj.err.message
     }
   }
 
@@ -208,8 +203,7 @@ function write (_obj, msg, num) {
     stream.lastTime = t.slice(this[timeSliceIndexSym])
     stream.lastLogger = this // for child loggers
   }
-  if (stream instanceof SonicBoom) stream.write(s)
-  else stream.write(flatstr(s))
+  stream.write(s)
 }
 
 function flush () {
diff --git a/lib/symbols.js b/lib/symbols.js
index 880cee2..226d791 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -24,6 +24,7 @@ const endSym = Symbol('pino.end')
 const formatOptsSym = Symbol('pino.formatOpts')
 const messageKeySym = Symbol('pino.messageKey')
 const nestedKeySym = Symbol('pino.nestedKey')
+const nestedKeyStrSym = Symbol('pino.nestedKeyStr')
 
 const wildcardFirstSym = Symbol('pino.wildcardFirst')
 
@@ -60,5 +61,6 @@ module.exports = {
   needsMetadataGsym,
   useOnlyCustomLevelsSym,
   formattersSym,
-  hooksSym
+  hooksSym,
+  nestedKeyStrSym
 }
diff --git a/lib/tools.js b/lib/tools.js
index 57398a7..296e135 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -5,7 +5,7 @@
 const format = require('quick-format-unescaped')
 const { mapHttpRequest, mapHttpResponse } = require('pino-std-serializers')
 const SonicBoom = require('sonic-boom')
-const stringifySafe = require('fast-safe-stringify')
+const stringifySafe = require('json-stringify-safe')
 const {
   lsCacheSym,
   chindingsSym,
@@ -22,7 +22,8 @@ const {
   streamSym,
   nestedKeySym,
   formattersSym,
-  messageKeySym
+  messageKeySym,
+  nestedKeyStrSym
 } = require('./symbols')
 
 function noop () {}
@@ -44,7 +45,6 @@ function genLog (level, hook) {
           o = mapHttpResponse(o)
         }
       }
-      if (this[nestedKeySym]) o = { [this[nestedKeySym]]: o }
       let formatParams
       if (msg === null && n.length === 0) {
         formatParams = [null]
@@ -108,10 +108,8 @@ function asJson (obj, msg, num, time) {
   if (formatters.log) {
     obj = formatters.log(obj)
   }
-  if (msg !== undefined) {
-    obj[messageKey] = msg
-  }
   const wildcardStringifier = stringifiers[wildcardFirstSym]
+  let propStr = ''
   for (const key in obj) {
     value = obj[key]
     if ((notHasOwnProperty || obj.hasOwnProperty(key)) && value !== undefined) {
@@ -139,11 +137,45 @@ function asJson (obj, msg, num, time) {
           value = (stringifier || stringify)(value)
       }
       if (value === undefined) continue
-      data += ',"' + key + '":' + value
+      propStr += ',"' + key + '":' + value
+    }
+  }
+
+  let msgStr = ''
+  if (msg !== undefined) {
+    value = serializers[messageKey] ? serializers[messageKey](msg) : msg
+    const stringifier = stringifiers[messageKey] || wildcardStringifier
+
+    switch (typeof value) {
+      case 'function':
+        break
+      case 'number':
+        /* eslint no-fallthrough: "off" */
+        if (Number.isFinite(value) === false) {
+          value = null
+        }
+      // this case explicitly falls through to the next one
+      case 'boolean':
+        if (stringifier) value = stringifier(value)
+        msgStr = ',"' + messageKey + '":' + value
+        break
+      case 'string':
+        value = (stringifier || asString)(value)
+        msgStr = ',"' + messageKey + '":' + value
+        break
+      default:
+        value = (stringifier || stringify)(value)
+        msgStr = ',"' + messageKey + '":' + value
     }
   }
 
-  return data + end
+  if (this[nestedKeySym]) {
+    // place all the obj properties under the specified key
+    // the nested key is already formatted from the constructor
+    return data + this[nestedKeyStrSym] + propStr.slice(1) + '}' + msgStr + end
+  } else {
+    return data + propStr + msgStr + end
+  }
 }
 
 function asChindings (instance, bindings) {
@@ -237,7 +269,10 @@ function prettifierMetaWrapper (pretty, dest, opts) {
 
       let time = this.lastTime
 
-      if (time.match(/^\d+/)) {
+      /* istanbul ignore next */
+      if (typeof time === 'number') {
+        // do nothing!
+      } else if (time.match(/^\d+/)) {
         time = parseInt(time)
       } else {
         time = time.slice(1, -1)
@@ -327,6 +362,7 @@ function createArgsNormalizer (defaultOptions) {
       opts = null
     }
     opts = Object.assign({}, defaultOptions, opts)
+    opts.serializers = Object.assign({}, defaultOptions.serializers, opts.serializers)
     if ('extreme' in opts) {
       throw Error('The extreme option has been removed, use pino.destination({ sync: false }) instead')
     }
@@ -401,7 +437,11 @@ function stringify (obj) {
   try {
     return JSON.stringify(obj)
   } catch (_) {
-    return stringifySafe(obj)
+    try {
+      return stringifySafe(obj)
+    } catch (_) {
+      return '"[unable to serialize, circular reference is too complex to analyze]"'
+    }
   }
 }
 
diff --git a/lib/transport.js b/lib/transport.js
new file mode 100644
index 0000000..00ff532
--- /dev/null
+++ b/lib/transport.js
@@ -0,0 +1,101 @@
+'use strict'
+
+const { createRequire } = require('module')
+const caller = require('get-caller-file')
+const { join, isAbsolute } = require('path')
+
+const ThreadStream = require('thread-stream')
+
+function setupOnExit (stream) {
+  /* istanbul ignore next */
+  if (global.WeakRef && global.WeakMap && global.FinalizationRegistry) {
+    // This is leak free, it does not leave event handlers
+    const onExit = require('on-exit-leak-free')
+
+    onExit.register(stream, autoEnd)
+
+    stream.on('close', function () {
+      onExit.unregister(stream)
+    })
+  } else {
+    const fn = autoEnd.bind(null, stream)
+    process.once('beforeExit', fn)
+    process.once('exit', fn)
+
+    stream.on('close', function () {
+      process.removeListener('beforeExit', fn)
+      process.removeListener('exit', fn)
+    })
+  }
+}
+
+function buildStream (filename, workerData, workerOpts) {
+  const stream = new ThreadStream({
+    filename,
+    workerData,
+    workerOpts,
+    sync: true // TODO should this be configurable?
+  })
+
+  stream.on('ready', function () {
+    stream.unref()
+
+    if (workerOpts.autoEnd !== false) {
+      setupOnExit(stream)
+    }
+  })
+
+  return stream
+}
+
+function autoEnd (stream) {
+  stream.ref()
+  stream.end()
+  stream.once('close', function () {
+    stream.unref()
+  })
+}
+
+function transport (fullOptions) {
+  const { targets, options = {}, worker = {} } = fullOptions
+  // This function call MUST stay in the top-level function of this module
+  const callerFile = caller()
+  const callerRequire = createRequire(callerFile)
+
+  let target = fullOptions.target
+
+  if (target && targets) {
+    throw new Error('Only one of target or targets can be specified')
+  }
+
+  if (targets) {
+    target = join(__dirname, 'worker.js')
+    options.targets = targets.map((dest) => {
+      return {
+        ...dest,
+        target: fixTarget(dest.target)
+      }
+    })
+  }
+
+  return buildStream(fixTarget(target), options, worker)
+
+  function fixTarget (origin) {
+    if (isAbsolute(origin) || origin.indexOf('file://') === 0) {
+      return origin
+    }
+
+    switch (origin) {
+      case '#pino/pretty':
+        return join(__dirname, 'pretty-target.js')
+      case '#pino/file':
+        return join(__dirname, 'file-target.js')
+      /* istanbul ignore next */
+      default:
+        // TODO we cannot test this on Windows.. might not work.
+        return callerRequire.resolve(origin)
+    }
+  }
+}
+
+module.exports = transport
diff --git a/lib/worker.js b/lib/worker.js
new file mode 100644
index 0000000..0b6c498
--- /dev/null
+++ b/lib/worker.js
@@ -0,0 +1,53 @@
+'use strict'
+
+const pino = require('../pino.js')
+const build = require('pino-abstract-transport')
+
+// This file is not checked by the code coverage tool,
+// as it is not reliable.
+
+/* istanbul ignore file */
+
+module.exports = async function ({ targets }) {
+  targets = await Promise.all(targets.map(async (t) => {
+    const toLoad = 'file://' + t.target
+    const stream = await (await import(toLoad)).default(t.options)
+    return {
+      level: t.level,
+      stream
+    }
+  }))
+  return build(process, {
+    parse: 'lines',
+    metadata: true,
+    close (err, cb) {
+      let expected = 0
+      for (const transport of targets) {
+        expected++
+        transport.stream.on('close', closeCb)
+        transport.stream.end()
+      }
+
+      function closeCb () {
+        if (--expected === 0) {
+          cb(err)
+        }
+      }
+    }
+  })
+
+  function process (stream) {
+    const multi = pino.multistream(targets)
+    // TODO manage backpressure
+    stream.on('data', function (chunk) {
+      const { lastTime, lastMsg, lastObj, lastLevel } = this
+      multi.lastLevel = lastLevel
+      multi.lastTime = lastTime
+      multi.lastMsg = lastMsg
+      multi.lastObj = lastObj
+
+      // TODO handle backpressure
+      multi.write(chunk + '\n')
+    })
+  }
+}
diff --git a/package.json b/package.json
index d1799cd..af6550a 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,14 @@
 {
   "name": "pino",
-  "version": "6.12.0",
+  "version": "7.0.0-rc.2",
   "description": "super fast, all natural json logger",
   "main": "pino.js",
+  "type": "commonjs",
+  "types": "pino.d.ts",
   "browser": "./browser.js",
   "files": [
     "pino.js",
+    "pino.d.ts",
     "bin.js",
     "browser.js",
     "pretty.js",
@@ -19,8 +22,9 @@
     "docs": "docsify serve",
     "browser-test": "airtap --local 8080 test/browser*test.js",
     "lint": "eslint .",
-    "test": "npm run lint && tap --100 test/*test.js test/*/*test.js",
-    "test-ci": "npm run lint && tap test/*test.js test/*/*test.js --coverage-report=lcovonly",
+    "test": "npm run lint && tap test/*test.js test/*/*test.js && npm run test-types",
+    "test-ci": "npm run lint && tap --no-check-coverage test/*test.js test/*/*test.js --coverage-report=lcovonly && npm run test-types",
+    "test-types": "tsc && tsd && ts-node test/types/pino.ts",
     "cov-ui": "tap --coverage-report=html test/*test.js test/*/*test.js",
     "bench": "node benchmarks/utils/runbench all",
     "bench-basic": "node benchmarks/utils/runbench basic",
@@ -60,6 +64,7 @@
   },
   "homepage": "http://getpino.io",
   "devDependencies": {
+    "@types/node": "^15.14.1",
     "airtap": "4.0.3",
     "benchmark": "^2.1.4",
     "bole": "^4.0.0",
@@ -76,7 +81,7 @@
     "import-fresh": "^3.2.1",
     "log": "^6.0.0",
     "loglevel": "^1.6.7",
-    "pino-pretty": "^4.1.0",
+    "pino-pretty": "^5.1.0",
     "pre-commit": "^1.2.2",
     "proxyquire": "^2.1.3",
     "pump": "^3.0.0",
@@ -87,14 +92,24 @@
     "tap": "^15.0.1",
     "tape": "^5.0.0",
     "through2": "^4.0.0",
+    "ts-node": "^10.0.0",
+    "tsd": "^0.17.0",
+    "typescript": "^4.3.5",
     "winston": "^3.3.3"
   },
   "dependencies": {
     "fast-redact": "^3.0.0",
     "fast-safe-stringify": "^2.0.8",
-    "flatstr": "^1.0.12",
-    "pino-std-serializers": "^3.1.0",
+    "get-caller-file": "^2.0.5",
+    "on-exit-leak-free": "^0.2.0",
+    "pino-abstract-transport": "^0.2.0",
+    "pino-std-serializers": "^4.0.0",
+    "json-stringify-safe": "^5.0.1",
     "quick-format-unescaped": "^4.0.3",
-    "sonic-boom": "^1.0.2"
+    "sonic-boom": "^2.1.0",
+    "thread-stream": "^0.11.0"
+  },
+  "tsd": {
+    "directory": "test/types"
   }
 }
diff --git a/pino.d.ts b/pino.d.ts
new file mode 100644
index 0000000..0e8e1ff
--- /dev/null
+++ b/pino.d.ts
@@ -0,0 +1,903 @@
+// Type definitions for pino 6.3
+// Project: https://github.com/pinojs/pino.git, http://getpino.io
+// Definitions by: Peter Snider <https://github.com/psnider>
+//                 BendingBender <https://github.com/BendingBender>
+//                 Christian Rackerseder <https://github.com/screendriver>
+//                 GP <https://github.com/paambaati>
+//                 Alex Ferrando <https://github.com/alferpal>
+//                 Oleksandr Sidko <https://github.com/mortiy>
+//                 Harris Lummis <https://github.com/lummish>
+//                 Raoul Jaeckel <https://github.com/raoulus>
+//                 Cory Donkin <https://github.com/Cooryd>
+//                 Adam Vigneaux <https://github.com/AdamVig>
+//                 Austin Beer <https://github.com/austin-beer>
+//                 Michel Nemnom <https://github.com/Pegase745>
+//                 Igor Savin <https://github.com/kibertoad>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+// TypeScript Version: 3.0
+
+/// <reference types="node"/>
+
+import { EventEmitter } from "events";
+import { SonicBoom } from "sonic-boom";
+import * as pinoStdSerializers from "pino-std-serializers";
+import { WriteStream } from "fs";
+import { WorkerOptions } from "worker_threads";
+
+export default P;
+export { P as pino }
+export type { P }
+
+type LogDescriptor = Record<string, any>;
+type MessageFormatFunc = (log: LogDescriptor, messageKey: string, levelLabel: string) => string;
+
+/**
+ * @param [optionsOrStream]: an options object or a writable stream where the logs will be written. It can also receive some log-line metadata, if the
+ * relative protocol is enabled. Default: process.stdout
+ * @returns a new logger instance.
+ */
+declare function P(optionsOrStream?: P.LoggerOptions | P.DestinationStream): P.Logger;
+
+/**
+ * @param [options]: an options object
+ * @param [stream]: a writable stream where the logs will be written. It can also receive some log-line metadata, if the
+ * relative protocol is enabled. Default: process.stdout
+ * @returns a new logger instance.
+ */
+declare function P(options: P.LoggerOptions, stream: P.DestinationStream): P.Logger;
+
+declare namespace P {
+    /**
+     * Holds the current log format version (as output in the v property of each log record).
+     */
+    const LOG_VERSION: number;
+    const levels: LevelMapping;
+    const symbols: {
+        readonly setLevelSym: unique symbol;
+        readonly getLevelSym: unique symbol;
+        readonly levelValSym: unique symbol;
+        readonly useLevelLabelsSym: unique symbol;
+        readonly mixinSym: unique symbol;
+        readonly lsCacheSym: unique symbol;
+        readonly chindingsSym: unique symbol;
+        readonly parsedChindingsSym: unique symbol;
+        readonly asJsonSym: unique symbol;
+        readonly writeSym: unique symbol;
+        readonly serializersSym: unique symbol;
+        readonly redactFmtSym: unique symbol;
+        readonly timeSym: unique symbol;
+        readonly timeSliceIndexSym: unique symbol;
+        readonly streamSym: unique symbol;
+        readonly stringifySym: unique symbol;
+        readonly stringifiersSym: unique symbol;
+        readonly endSym: unique symbol;
+        readonly formatOptsSym: unique symbol;
+        readonly messageKeySym: unique symbol;
+        readonly nestedKeySym: unique symbol;
+        readonly wildcardFirstSym: unique symbol;
+        readonly needsMetadataGsym: unique symbol;
+        readonly useOnlyCustomLevelsSym: unique symbol;
+        readonly formattersSym: unique symbol;
+        readonly hooksSym: unique symbol;
+    };
+    /**
+     * Exposes the Pino package version. Also available on the logger instance.
+     */
+    const version: string;
+
+    type SerializedError = pinoStdSerializers.SerializedError;
+    type SerializedResponse = pinoStdSerializers.SerializedResponse;
+    type SerializedRequest = pinoStdSerializers.SerializedRequest;
+
+    /**
+     * Provides functions for serializing objects common to many projects.
+     */
+    const stdSerializers: {
+        /**
+         * Generates a JSONifiable object from the HTTP `request` object passed to the `createServer` callback of Node's HTTP server.
+         */
+        req: typeof pinoStdSerializers.req;
+        /**
+         * Generates a JSONifiable object from the HTTP `response` object passed to the `createServer` callback of Node's HTTP server.
+         */
+        res: typeof pinoStdSerializers.res;
+        /**
+         * Serializes an Error object.
+         */
+        err: typeof pinoStdSerializers.err;
+        /**
+         * Returns an object:
+         * ```
+         * {
+         *   req: {}
+         * }
+         * ```
+         * where req is the request as serialized by the standard request serializer.
+         * @param req The request to serialize
+         * @return An object
+         */
+        mapHttpRequest: typeof pinoStdSerializers.mapHttpRequest;
+        /**
+         * Returns an object:
+         * ```
+         * {
+         *   res: {}
+         * }
+         * ```
+         * where res is the response as serialized by the standard response serializer.
+         * @param res The response to serialize.
+         * @return An object.
+         */
+        mapHttpResponse: typeof pinoStdSerializers.mapHttpResponse;
+        /**
+         * A utility method for wrapping the default error serializer. Allows custom serializers to work with the
+         * already serialized object.
+         * @param customSerializer The custom error serializer. Accepts a single parameter: the newly serialized
+         * error object. Returns the new (or updated) error object.
+         * @return A new error serializer.
+         */
+        wrapErrorSerializer: typeof pinoStdSerializers.wrapErrorSerializer;
+        /**
+         * A utility method for wrapping the default request serializer. Allows custom serializers to work with the
+         * already serialized object.
+         * @param customSerializer The custom request serializer. Accepts a single parameter: the newly serialized
+         * request object. Returns the new (or updated) request object.
+         * @return A new error serializer.
+         */
+        wrapRequestSerializer: typeof pinoStdSerializers.wrapRequestSerializer;
+        /**
+         * A utility method for wrapping the default response serializer. Allows custom serializers to work with the
+         * already serialized object.
+         * @param customSerializer The custom response serializer. Accepts a single parameter: the newly serialized
+         * response object. Returns the new (or updated) response object.
+         * @return A new error serializer.
+         */
+        wrapResponseSerializer: typeof pinoStdSerializers.wrapResponseSerializer;
+    };
+    /**
+     * Provides functions for generating the timestamp property in the log output. You can set the `timestamp` option during
+     * initialization to one of these functions to adjust the output format. Alternatively, you can specify your own time function.
+     * A time function must synchronously return a string that would be a valid component of a JSON string. For example,
+     * the default function returns a string like `,"time":1493426328206`.
+     */
+    const stdTimeFunctions: {
+        /**
+         * The default time function for Pino. Returns a string like `,"time":1493426328206`.
+         */
+        epochTime: TimeFn;
+        /*
+         * Returns the seconds since Unix epoch
+         */
+        unixTime: TimeFn;
+        /**
+         * Returns an empty string. This function is used when the `timestamp` option is set to `false`.
+         */
+        nullTime: TimeFn;
+        /*
+         * Returns ISO 8601-formatted time in UTC
+         */
+        isoTime: TimeFn;
+    };
+
+    /**
+     * Equivalent of SonicBoom constructor options object
+     */
+        // TODO: use SonicBoom constructor options interface when available
+    interface DestinationObjectOptions {
+        fd?: string | number;
+        dest?: string;
+        minLength?: number;
+        sync?: boolean;
+    }
+
+    /**
+     * Create a Pino Destination instance: a stream-like object with significantly more throughput (over 30%) than a standard Node.js stream.
+     * @param [dest]: 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` to create the destination stream.
+     * @returns A Sonic-Boom  stream to be used as destination for the pino function
+     */
+    function destination(
+        dest?: string | number | DestinationObjectOptions | DestinationStream | NodeJS.WritableStream,
+    ): SonicBoom;
+
+    interface TransportTargetOptions<TransportOptions = Record<string, any>> {
+        target: string
+        options: TransportOptions
+        level: LevelWithSilent
+    }
+
+    interface TransportBaseOptions<TransportOptions = Record<string, any>> {
+        options?: TransportOptions
+        worker?: WorkerOptions & { autoEnd?: boolean}
+    }
+
+    interface TransportSingleOptions<TransportOptions = Record<string, any>> extends TransportBaseOptions<TransportOptions>{
+        target: string
+    }
+
+    interface TransportMultiOptions<TransportOptions = Record<string, any>> extends TransportBaseOptions<TransportOptions>{
+        targets: readonly TransportTargetOptions<TransportOptions>[]
+    }
+
+    // ToDo https://github.com/pinojs/thread-stream/issues/24
+    type ThreadStream = any
+
+    function transport<TransportOptions = Record<string, any>>(
+        options: TransportSingleOptions<TransportOptions> | TransportMultiOptions<TransportOptions>
+    ): ThreadStream
+
+    interface MultiStreamOptions {
+        levels?: Record<string, number>
+        dedupe?: boolean
+    }
+
+    interface StreamEntry {
+        stream: WriteStream
+        level: Level
+    }
+
+    interface MultiStreamRes {
+        write: (data: any) => void,
+        add: (dest: Record<string, any>) => MultiStreamRes,
+        flushSync: () => void,
+        minLevel: number,
+        streams: WriteStream[],
+        clone(level: Level): MultiStreamRes,
+    }
+
+    function multistream(
+        streamsArray: StreamEntry[], opts: P.MultiStreamOptions
+    ): MultiStreamRes
+
+    /**
+     * The pino.final method can be used to create an exit listener function.
+     * This listener function can be supplied to process exit events.
+     * The exit listener function will call the handler with
+     * @param [logger]: pino logger that serves as reference for the final logger
+     * @param [handler]: Function that will be called by the handler returned from this function
+     * @returns Exit listener function that can be supplied to process exit events and will call the supplied handler function
+     */
+    function final(
+        logger: Logger,
+        handler: (error: Error, finalLogger: Logger, ...args: any[]) => void,
+    ): (error: Error | null, ...args: any[]) => void;
+
+    /**
+     * The pino.final method can be used to acquire a final logger instance that synchronously flushes on every write.
+     * @param [logger]: pino logger that serves as reference for the final logger
+     * @returns Final, synchronous logger
+     */
+    function final(logger: Logger): Logger;
+
+    interface LevelMapping {
+        /**
+         * Returns the mappings of level names to their respective internal number representation.
+         */
+        values: { [level: string]: number };
+        /**
+         * Returns the mappings of level internal level numbers to their string representations.
+         */
+        labels: { [level: number]: string };
+    }
+    type TimeFn = () => string;
+    type MixinFn = () => object;
+
+    interface DestinationStream {
+        write(msg: string): void;
+    }
+
+    interface LoggerOptions {
+        /**
+         * Avoid error causes by circular references in the object tree. Default: `true`.
+         */
+        safe?: boolean;
+        /**
+         * The name of the logger. Default: `undefined`.
+         */
+        name?: string;
+        /**
+         * an object containing functions for custom serialization of objects.
+         * These functions should return an JSONifiable object and they should never throw. When logging an object,
+         * each top-level property matching the exact key of a serializer will be serialized using the defined serializer.
+         */
+        serializers?: { [key: string]: SerializerFn };
+        /**
+         * Enables or disables the inclusion of a timestamp in the log message. If a function is supplied, it must
+         * synchronously return a JSON string representation of the time. If set to `false`, no timestamp will be included in the output.
+         * See stdTimeFunctions for a set of available functions for passing in as a value for this option.
+         * Caution: any sort of formatted time will significantly slow down Pino's performance.
+         */
+        timestamp?: TimeFn | boolean;
+        /**
+         * One of the supported levels or `silent` to disable logging. Any other value defines a custom level and
+         * requires supplying a level value via `levelVal`. Default: 'info'.
+         */
+        level?: LevelWithSilent | string;
+
+        /**
+         * Use this option to define additional logging levels.
+         * The keys of the object correspond the namespace of the log level, and the values should be the numerical value of the level.
+         */
+        customLevels?: { [key: string]: number };
+        /**
+         * Use this option to only use defined `customLevels` and omit Pino's levels.
+         * Logger's default `level` must be changed to a value in `customLevels` in order to use `useOnlyCustomLevels`
+         * Warning: this option may not be supported by downstream transports.
+         */
+        useOnlyCustomLevels?: boolean;
+
+        /**
+         * If provided, the `mixin` function is called each time one of the active logging methods
+         * is called. The function must synchronously return an object. The properties of the
+         * returned object will be added to the logged JSON.
+         */
+        mixin?: MixinFn;
+
+        /**
+         * As an array, the redact option specifies paths that should have their values redacted from any log output.
+         *
+         * Each path must be a string using a syntax which corresponds to JavaScript dot and bracket notation.
+         *
+         * If an object is supplied, three options can be specified:
+         *
+         *      paths (String[]): Required. An array of paths
+         *      censor (String): Optional. A value to overwrite key which are to be redacted. Default: '[Redacted]'
+         *      remove (Boolean): Optional. Instead of censoring the value, remove both the key and the value. Default: false
+         */
+        redact?: string[] | redactOptions;
+
+        /**
+         * When defining a custom log level via level, set to an integer value to define the new level. Default: `undefined`.
+         */
+        levelVal?: number;
+        /**
+         * The string key for the 'message' in the JSON object. Default: "msg".
+         */
+        messageKey?: string;
+        /**
+         * The string key to place any logged object under.
+         */
+        nestedKey?: string;
+        /**
+         * Enables pino.pretty. This is intended for non-production configurations. This may be set to a configuration
+         * object as outlined in http://getpino.io/#/docs/API?id=pretty. Default: `false`.
+         */
+        prettyPrint?: boolean | PrettyOptions;
+        /**
+         * Allows to optionally define which prettifier module to use.
+         */
+        // TODO: use type definitions from 'pino-pretty' when available.
+        prettifier?: any;
+        /**
+         * Enables logging. Default: `true`.
+         */
+        enabled?: boolean;
+        /**
+         * Browser only, see http://getpino.io/#/docs/browser.
+         */
+        browser?: {
+            /**
+             * The `asObject` option will create a pino-like log object instead of passing all arguments to a console
+             * method. When `write` is set, `asObject` will always be true.
+             *
+             * @example
+             * pino.info('hi') // creates and logs {msg: 'hi', level: 30, time: <ts>}
+             */
+            asObject?: boolean;
+            /**
+             * Instead of passing log messages to `console.log` they can be passed to a supplied function. If `write` is
+             * set to a single function, all logging objects are passed to this function. If `write` is an object, it
+             * can have methods that correspond to the levels. When a message is logged at a given level, the
+             * corresponding method is called. If a method isn't present, the logging falls back to using the `console`.
+             *
+             * @example
+             * const pino = require('pino')({
+             *   browser: {
+             *     write: (o) => {
+             *       // do something with o
+             *     }
+             *   }
+             * })
+             *
+             * @example
+             * const pino = require('pino')({
+             *   browser: {
+             *     write: {
+             *       info: function (o) {
+             *         //process info log object
+             *       },
+             *       error: function (o) {
+             *         //process error log object
+             *       }
+             *     }
+             *   }
+             * })
+             */
+            write?:
+                | WriteFn
+                | ({
+                fatal?: WriteFn;
+                error?: WriteFn;
+                warn?: WriteFn;
+                info?: WriteFn;
+                debug?: WriteFn;
+                trace?: WriteFn;
+            } & { [logLevel: string]: WriteFn });
+
+            /**
+             * The serializers provided to `pino` are ignored by default in the browser, including the standard
+             * serializers provided with Pino. Since the default destination for log messages is the console, values
+             * such as `Error` objects are enhanced for inspection, which they otherwise wouldn't be if the Error
+             * serializer was enabled. We can turn all serializers on or we can selectively enable them via an array.
+             *
+             * When `serialize` is `true` the standard error serializer is also enabled (see
+             * {@link https://github.com/pinojs/pino/blob/master/docs/api.md#pino-stdserializers}). This is a global
+             * serializer which will apply to any `Error` objects passed to the logger methods.
+             *
+             * If `serialize` is an array the standard error serializer is also automatically enabled, it can be
+             * explicitly disabled by including a string in the serialize array: `!stdSerializers.err` (see example).
+             *
+             * The `serialize` array also applies to any child logger serializers (see
+             * {@link https://github.com/pinojs/pino/blob/master/docs/api.md#bindingsserializers-object} for how to
+             * set child-bound serializers).
+             *
+             * Unlike server pino the serializers apply to every object passed to the logger method, if the `asObject`
+             * option is `true`, this results in the serializers applying to the first object (as in server pino).
+             *
+             * For more info on serializers see
+             * {@link https://github.com/pinojs/pino/blob/master/docs/api.md#serializers-object}.
+             *
+             * @example
+             * const pino = require('pino')({
+             *   browser: {
+             *     serialize: true
+             *   }
+             * })
+             *
+             * @example
+             * const pino = require('pino')({
+             *   serializers: {
+             *     custom: myCustomSerializer,
+             *     another: anotherSerializer
+             *   },
+             *   browser: {
+             *     serialize: ['custom']
+             *   }
+             * })
+             * // following will apply myCustomSerializer to the custom property,
+             * // but will not apply anotherSerializer to another key
+             * pino.info({custom: 'a', another: 'b'})
+             *
+             * @example
+             * const pino = require('pino')({
+             *   serializers: {
+             *     custom: myCustomSerializer,
+             *     another: anotherSerializer
+             *   },
+             *   browser: {
+             *     serialize: ['!stdSerializers.err', 'custom'] //will not serialize Errors, will serialize `custom` keys
+             *   }
+             * })
+             */
+            serialize?: boolean | string[];
+
+            /**
+             * Options for transmission of logs.
+             *
+             * @example
+             * const pino = require('pino')({
+             *   browser: {
+             *     transmit: {
+             *       level: 'warn',
+             *       send: function (level, logEvent) {
+             *         if (level === 'warn') {
+             *           // maybe send the logEvent to a separate endpoint
+             *           // or maybe analyse the messages further before sending
+             *         }
+             *         // we could also use the `logEvent.level.value` property to determine
+             *         // numerical value
+             *         if (logEvent.level.value >= 50) { // covers error and fatal
+             *
+             *           // send the logEvent somewhere
+             *         }
+             *       }
+             *     }
+             *   }
+             * })
+             */
+            transmit?: {
+                /**
+                 * Specifies the minimum level (inclusive) of when the `send` function should be called, if not supplied
+                 * the `send` function will be called based on the main logging `level` (set via `options.level`,
+                 * defaulting to `info`).
+                 */
+                level?: Level | string;
+                /**
+                 * Remotely record log messages.
+                 *
+                 * @description Called after writing the log message.
+                 */
+                send: (level: Level, logEvent: LogEvent) => void;
+            };
+        };
+        /**
+         * key-value object added as child logger to each log line. If set to null the base child logger is not added
+         */
+        base?: { [key: string]: any } | null;
+
+        /**
+         * 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.
+         */
+        formatters?: {
+            /**
+             * 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).
+             */
+            level?: (label: string, number: number) => object;
+            /**
+             * 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.
+             */
+            bindings?: (bindings: Bindings) => object;
+            /**
+             * 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.
+             */
+            log?: (object: object) => object;
+        };
+
+        /**
+         * An object mapping to hook functions. Hook functions allow for customizing internal logger operations.
+         * Hook functions must be synchronous functions.
+         */
+        hooks?: {
+            /**
+             * Allows for manipulating the parameters passed to logger methods. The signature for this hook is
+             * logMethod (args, method, level) {}, where args is an array of the arguments that were passed to the
+             * log method and method is the log method itself, and level is the log level. This hook must invoke the method function by
+             * using apply, like so: method.apply(this, newArgumentsArray).
+             */
+            logMethod?: (args: any[], method: LogFn, level: number) => void;
+        };
+    }
+
+    // Copied from "pino-pretty" types
+    type PrettyOptions = {
+        /**
+         * Hide objects from output (but not error object).
+         * @default false
+         */
+        hideObject?: boolean;
+        /**
+         * Translate the epoch time value into a human readable date and time string. This flag also can set the format
+         * string to apply when translating the date to human readable format. For a list of available pattern letters
+         * see the {@link https://www.npmjs.com/package/dateformat|dateformat documentation}.
+         * - The default format is `yyyy-mm-dd HH:MM:ss.l o` in UTC.
+         * - Requires a `SYS:` prefix to translate time to the local system's timezone. Use the shortcut `SYS:standard`
+         *   to translate time to `yyyy-mm-dd HH:MM:ss.l o` in system timezone.
+         * @default false
+         */
+        translateTime?: boolean | string;
+        /**
+         * If set to true, it will print the name of the log level as the first field in the log line.
+         * @default false
+         */
+        levelFirst?: boolean;
+        /**
+         * Define the key that contains the level of the log.
+         * @default "level"
+         */
+        levelKey?: string;
+        /**
+         * Output the log level using the specified label.
+         * @default "levelLabel"
+         */
+        levelLabel?: string;
+        /**
+         * The key in the JSON object to use as the highlighted message.
+         * @default "msg"
+         */
+        messageKey?: string;
+        /**
+         * Print each log message on a single line (errors will still be multi-line).
+         * @default false
+         */
+        singleLine?: boolean;
+        /**
+         * The key in the JSON object to use for timestamp display.
+         * @default "time"
+         */
+        timestampKey?: string;
+        /**
+         * Format output of message, e.g. {level} - {pid} will output message: INFO - 1123
+         * @default false
+         *
+         * @example
+         * ```typescript
+         * {
+         *   messageFormat: (log, messageKey) => {
+         *     const message = log[messageKey];
+         *     if (log.requestId) return `[${log.requestId}] ${message}`;
+         *     return message;
+         *   }
+         * }
+         * ```
+         */
+        messageFormat?: false | string | MessageFormatFunc;
+        /**
+         * If set to true, will add color information to the formatted output message.
+         * @default false
+         */
+        colorize?: boolean;
+        /**
+         * Appends carriage return and line feed, instead of just a line feed, to the formatted log line.
+         * @default false
+         */
+        crlf?: boolean;
+        /**
+         * Define the log keys that are associated with error like objects.
+         * @default ["err", "error"]
+         */
+        errorLikeObjectKeys?: string[];
+        /**
+         *  When formatting an error object, display this list of properties.
+         *  The list should be a comma separated list of properties.
+         * @default ""
+         */
+        errorProps?: string;
+        /**
+         * Specify a search pattern according to {@link http://jmespath.org|jmespath}
+         */
+        search?: string;
+        /**
+         * Ignore one or several keys.
+         * @example "time,hostname"
+         */
+        ignore?: string;
+    }
+
+    type Level = "fatal" | "error" | "warn" | "info" | "debug" | "trace";
+    type LevelWithSilent = Level | "silent";
+
+    type SerializerFn = (value: any) => any;
+    type WriteFn = (o: object) => void;
+
+    /**
+     * Describes a log line.
+     */
+    type LogDescriptor = Record<string, any>; // TODO replace `any` with `unknown` when TypeScript version >= 3.0
+
+    interface Bindings {
+        [key: string]: any;
+    }
+
+    interface ChildLoggerOptions {
+        level?: Level | string;
+        serializers?: { [key: string]: SerializerFn };
+        customLevels?: { [key: string]: number };
+        formatters?: {
+            level?: (label: string, number: number) => object;
+            bindings?: (bindings: Bindings) => object;
+            log?: (object: object) => object;
+        };
+        redact?: string[] | redactOptions;
+    }
+
+    /**
+     * A data structure representing a log message, it represents the arguments passed to a logger statement, the level
+     * at which they were logged and the hierarchy of child bindings.
+     *
+     * @description By default serializers are not applied to log output in the browser, but they will always be applied
+     * to `messages` and `bindings` in the `logEvent` object. This allows  us to ensure a consistent format for all
+     * values between server and client.
+     */
+    interface LogEvent {
+        /**
+         * Unix epoch timestamp in milliseconds, the time is taken from the moment the logger method is called.
+         */
+        ts: number;
+        /**
+         * All arguments passed to logger method, (for instance `logger.info('a', 'b', 'c')` would result in `messages`
+         * array `['a', 'b', 'c']`).
+         */
+        messages: any[];
+        /**
+         * Represents each child logger (if any), and the relevant bindings.
+         *
+         * @description For instance, given `logger.child({a: 1}).child({b: 2}).info({c: 3})`, the bindings array would
+         * hold `[{a: 1}, {b: 2}]` and the `messages` array would be `[{c: 3}]`. The `bindings` are ordered according to
+         * their position in the child logger hierarchy, with the lowest index being the top of the hierarchy.
+         */
+        bindings: Bindings[];
+        /**
+         * Holds the `label` (for instance `info`), and the corresponding numerical `value` (for instance `30`).
+         * This could be important in cases where client side level values and labels differ from server side.
+         */
+        level: {
+            label: string;
+            value: number;
+        };
+    }
+
+    type Logger = BaseLogger & { [key: string]: LogFn };
+
+    interface BaseLogger extends EventEmitter {
+        /**
+         * Exposes the current version of Pino.
+         */
+        readonly pino: string;
+        /**
+         * Holds the current log format version (as output in the v property of each log record).
+         */
+        readonly LOG_VERSION: number;
+        /**
+         * Exposes the Pino package version. Also available on the exported pino function.
+         */
+        readonly version: string;
+
+        levels: LevelMapping;
+
+        /**
+         * Set this property to the desired logging level. In order of priority, available levels are:
+         *
+         * - 'fatal'
+         * - 'error'
+         * - 'warn'
+         * - 'info'
+         * - 'debug'
+         * - 'trace'
+         *
+         * The logging level is a __minimum__ level. For instance if `logger.level` is `'info'` then all `'fatal'`, `'error'`, `'warn'`,
+         * and `'info'` logs will be enabled.
+         *
+         * You can pass `'silent'` to disable logging.
+         */
+        level: LevelWithSilent | string;
+        /**
+         * Outputs the level as a string instead of integer.
+         */
+        useLevelLabels: boolean;
+        /**
+         * Define additional logging levels.
+         */
+        customLevels: { [key: string]: number };
+        /**
+         * Use only defined `customLevels` and omit Pino's levels.
+         */
+        useOnlyCustomLevels: boolean;
+        /**
+         * Returns the integer value for the logger instance's logging level.
+         */
+        levelVal: number;
+
+        /**
+         * Registers a listener function that is triggered when the level is changed.
+         * Note: When browserified, this functionality will only be available if the `events` module has been required elsewhere
+         * (e.g. if you're using streams in the browser). This allows for a trade-off between bundle size and functionality.
+         *
+         * @param event: only ever fires the `'level-change'` event
+         * @param listener: The listener is passed four arguments: `levelLabel`, `levelValue`, `previousLevelLabel`, `previousLevelValue`.
+         */
+        on(event: "level-change", listener: LevelChangeEventListener): this;
+        addListener(event: "level-change", listener: LevelChangeEventListener): this;
+        once(event: "level-change", listener: LevelChangeEventListener): this;
+        prependListener(event: "level-change", listener: LevelChangeEventListener): this;
+        prependOnceListener(event: "level-change", listener: LevelChangeEventListener): this;
+        removeListener(event: "level-change", listener: LevelChangeEventListener): this;
+
+        /**
+         * Creates a child logger, setting all key-value pairs in `bindings` as properties in the log lines. All serializers will be applied to the given pair.
+         * Child loggers use the same output stream as the parent and inherit the current log level of the parent at the time they are spawned.
+         * From v2.x.x the log level of a child is mutable (whereas in v1.x.x it was immutable), and can be set independently of the parent.
+         * If a `level` property is present in the object passed to `child` it will override the child logger level.
+         *
+         * @param bindings: an object of key-value pairs to include in log lines as properties.
+         * @param options: an options object that will override child logger inherited options.
+         * @returns a child logger instance.
+         */
+        child(bindings: Bindings, options?: ChildLoggerOptions): Logger;
+
+        /**
+         * Log at `'fatal'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
+         * If more args follows `msg`, these will be used to format `msg` using `util.format`.
+         *
+         * @typeParam T: the interface of the object being serialized. Default is object.
+         * @param obj: object to be serialized
+         * @param msg: the log message to write
+         * @param ...args: format string values when `msg` is a format string
+         */
+        fatal: LogFn;
+        /**
+         * Log at `'error'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
+         * If more args follows `msg`, these will be used to format `msg` using `util.format`.
+         *
+         * @typeParam T: the interface of the object being serialized. Default is object.
+         * @param obj: object to be serialized
+         * @param msg: the log message to write
+         * @param ...args: format string values when `msg` is a format string
+         */
+        error: LogFn;
+        /**
+         * Log at `'warn'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
+         * If more args follows `msg`, these will be used to format `msg` using `util.format`.
+         *
+         * @typeParam T: the interface of the object being serialized. Default is object.
+         * @param obj: object to be serialized
+         * @param msg: the log message to write
+         * @param ...args: format string values when `msg` is a format string
+         */
+        warn: LogFn;
+        /**
+         * Log at `'info'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
+         * If more args follows `msg`, these will be used to format `msg` using `util.format`.
+         *
+         * @typeParam T: the interface of the object being serialized. Default is object.
+         * @param obj: object to be serialized
+         * @param msg: the log message to write
+         * @param ...args: format string values when `msg` is a format string
+         */
+        info: LogFn;
+        /**
+         * Log at `'debug'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
+         * If more args follows `msg`, these will be used to format `msg` using `util.format`.
+         *
+         * @typeParam T: the interface of the object being serialized. Default is object.
+         * @param obj: object to be serialized
+         * @param msg: the log message to write
+         * @param ...args: format string values when `msg` is a format string
+         */
+        debug: LogFn;
+        /**
+         * Log at `'trace'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
+         * If more args follows `msg`, these will be used to format `msg` using `util.format`.
+         *
+         * @typeParam T: the interface of the object being serialized. Default is object.
+         * @param obj: object to be serialized
+         * @param msg: the log message to write
+         * @param ...args: format string values when `msg` is a format string
+         */
+        trace: LogFn;
+        /**
+         * Noop function.
+         */
+        silent: LogFn;
+
+        /**
+         * A utility method for determining if a given log level will write to the destination.
+         */
+        isLevelEnabled(level: LevelWithSilent | string): boolean;
+
+        /**
+         * Returns an object containing all the current bindings, cloned from the ones passed in via logger.child().
+         */
+        bindings(): Bindings;
+    }
+
+    type LevelChangeEventListener = (
+        lvl: LevelWithSilent | string,
+        val: number,
+        prevLvl: LevelWithSilent | string,
+        prevVal: number,
+    ) => void;
+
+    interface LogFn {
+        /* tslint:disable:no-unnecessary-generics */
+        <T extends object>(obj: T, msg?: string, ...args: any[]): void;
+        (msg: string, ...args: any[]): void;
+    }
+
+    interface redactOptions {
+        paths: string[];
+        censor?: string | ((v: any) => any);
+        remove?: boolean;
+    }
+}
diff --git a/pino.js b/pino.js
index 0d0e3e3..1e46add 100644
--- a/pino.js
+++ b/pino.js
@@ -34,7 +34,8 @@ const {
   mixinSym,
   useOnlyCustomLevelsSym,
   formattersSym,
-  hooksSym
+  hooksSym,
+  nestedKeyStrSym
 } = symbols
 const { epochTime, nullTime } = time
 const { pid } = process
@@ -65,7 +66,6 @@ const defaultOptions = {
   name: undefined,
   redact: null,
   customLevels: null,
-  levelKey: undefined,
   useOnlyCustomLevels: false
 }
 
@@ -87,9 +87,6 @@ function pino (...args) {
     name,
     level,
     customLevels,
-    useLevelLabels,
-    changeLevelName,
-    levelKey,
     mixin,
     useOnlyCustomLevels,
     formatters,
@@ -102,23 +99,6 @@ function pino (...args) {
     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
   }
@@ -171,6 +151,8 @@ function pino (...args) {
     [formatOptsSym]: formatOpts,
     [messageKeySym]: messageKey,
     [nestedKeySym]: nestedKey,
+    // protect against injection
+    [nestedKeyStrSym]: nestedKey ? `,${JSON.stringify(nestedKey)}:{` : '',
     [serializersSym]: serializers,
     [mixinSym]: mixin,
     [chindingsSym]: chindings,
@@ -188,22 +170,6 @@ function pino (...args) {
   return instance
 }
 
-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 }
-  }
-}
-
 module.exports = pino
 
 module.exports.extreme = (dest = process.stdout.fd) => {
@@ -223,6 +189,9 @@ module.exports.destination = (dest = process.stdout.fd) => {
   }
 }
 
+module.exports.transport = require('./lib/transport')
+module.exports.multistream = require('./lib/multistream')
+
 module.exports.final = final
 module.exports.levels = mappings()
 module.exports.stdSerializers = serializers
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..be75b10
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+  "compilerOptions": {
+    "target": "es6",
+    "lib": [ "es2015" ],
+    "module": "commonjs",
+    "noEmit": true,
+    "strict": true,
+  },
+  "include": [
+    "./test/types/*.test-d.ts",
+    "./*.d.ts"
+  ]
+}


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index 52d5e5d..27c4b37 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -170,7 +170,8 @@ function levelTest (name, level) {
         type: 'Error',
         message: err.message,
         stack: err.stack
-      }
+      },
+      msg: err.message
     })
   })
 
@@ -592,8 +593,7 @@ test('children with same names render in correct order', async ({ equal }) => {
   equal(a, 3, 'last logged object takes precedence')
 })
 
-// https://github.com/pinojs/pino/pull/251 - use this.stringify
-test('use `fast-safe-stringify` to avoid circular dependencies', async ({ same }) => {
+test('use `json-stringify-safe` to avoid circular dependencies', async ({ same }) => {
   const stream = sink()
   const root = pino(stream)
   // circular depth
@@ -601,10 +601,10 @@ test('use `fast-safe-stringify` to avoid circular dependencies', async ({ same }
   obj.a = obj
   root.info(obj)
   const { a } = await once(stream, 'data')
-  same(a, { a: '[Circular]' })
+  same(a, { a: '[Circular ~]' })
 })
 
-test('fast-safe-stringify must be used when interpolating', async (t) => {
+test('json-stringify-safe must be used when interpolating', async (t) => {
   const stream = sink()
   const instance = pino(stream)
 
@@ -613,7 +613,7 @@ test('fast-safe-stringify must be used when interpolating', async (t) => {
   instance.info('test %j', o)
 
   const { msg } = await once(stream, 'data')
-  t.equal(msg, 'test {"a":{"b":{"c":"[Circular]"}}}')
+  t.equal(msg, 'test {"a":{"b":{"c":"[Circular ~.a.b]"}}}')
 })
 
 test('throws when setting useOnlyCustomLevels without customLevels', async ({ throws }) => {
@@ -665,3 +665,36 @@ test('offers a .default() method to please typescript', async ({ equal }) => {
   instance.info('hello world')
   check(equal, await once(stream, 'data'), 30, 'hello world')
 })
+
+test('correctly skip function', async (t) => {
+  const stream = sink()
+  const instance = pino(stream)
+
+  const o = { num: NaN }
+  instance.info(o, () => {})
+
+  const { msg } = await once(stream, 'data')
+  t.equal(msg, undefined)
+})
+
+test('correctly skip Infinity', async (t) => {
+  const stream = sink()
+  const instance = pino(stream)
+
+  const o = { num: NaN }
+  instance.info(o, Infinity)
+
+  const { msg } = await once(stream, 'data')
+  t.equal(msg, null)
+})
+
+test('correctly log number', async (t) => {
+  const stream = sink()
+  const instance = pino(stream)
+
+  const o = { num: NaN }
+  instance.info(o, 42)
+
+  const { msg } = await once(stream, 'data')
+  t.equal(msg, 42)
+})
diff --git a/test/deprecations.test.js b/test/deprecations.test.js
index ce0fb46..48c5892 100644
--- a/test/deprecations.test.js
+++ b/test/deprecations.test.js
@@ -8,99 +8,6 @@ const pino = require('../')
 
 process.removeAllListeners('warning')
 
-test('useLevelLabels', async ({ match, equal }) => {
-  process.on('warning', onWarning)
-  function onWarning (warn) {
-    equal(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, equal }) => {
-  process.on('warning', onWarning)
-  function onWarning (warn) {
-    equal(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, equal }) => {
-  process.on('warning', onWarning)
-  function onWarning (warn) {
-    equal(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, equal }) => {
-  let count = 0
-  process.on('warning', onWarning)
-  function onWarning (warn) {
-    equal(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, equal, pass }) => {
-  process.on('warning', onWarning)
-  function onWarning (warn) {
-    equal(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)
-})
-
 test('child(bindings.serializers)', async ({ match, equal, pass }) => {
   process.on('warning', onWarning)
   function onWarning (warn) {
diff --git a/test/error.test.js b/test/error.test.js
index 09b37db..a0da655 100644
--- a/test/error.test.js
+++ b/test/error.test.js
@@ -25,16 +25,20 @@ test('err is serialized with additional properties set on the Error object', asy
     pid,
     hostname,
     level,
-    type: 'Error',
-    msg: err.message,
-    stack: err.stack,
-    foo: err.foo
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack,
+      foo: err.foo
+    },
+    msg: err.message
   })
 })
 
-test('type should be retained, even if type is a property', async ({ ok, same }) => {
+test('type should be detected based on constructor', async ({ ok, same }) => {
+  class Bar extends Error {}
   const stream = sink()
-  const err = Object.assign(new Error('myerror'), { type: 'bar' })
+  const err = new Bar('myerror')
   const instance = pino(stream)
   instance.level = name
   instance[name](err)
@@ -45,9 +49,12 @@ test('type should be retained, even if type is a property', async ({ ok, same })
     pid,
     hostname,
     level,
-    type: 'bar',
-    msg: err.message,
-    stack: err.stack
+    err: {
+      type: 'Bar',
+      message: err.message,
+      stack: err.stack
+    },
+    msg: err.message
   })
 })
 
@@ -65,10 +72,13 @@ test('type, message and stack should be first level properties', async ({ ok, sa
     pid,
     hostname,
     level,
-    type: 'Error',
-    msg: err.message,
-    stack: err.stack,
-    foo: err.foo
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack,
+      foo: err.foo
+    },
+    msg: err.message
   })
 })
 
@@ -95,7 +105,8 @@ test('err serializer', async ({ ok, same }) => {
       message: err.message,
       stack: err.stack,
       foo: err.foo
-    }
+    },
+    msg: err.message
   })
 })
 
@@ -114,10 +125,13 @@ test('an error with statusCode property is not confused for a http response', as
     pid,
     hostname,
     level,
-    type: 'Error',
-    msg: err.message,
-    stack: err.stack,
-    statusCode: err.statusCode
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack,
+      statusCode: err.statusCode
+    },
+    msg: err.message
   })
 })
 
@@ -143,8 +157,8 @@ test('stack is rendered as any other property if it\'s not a string', t => {
   const instance = pino(sink(function (chunk, enc, cb) {
     t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
     delete chunk.time
-    t.equal(chunk.hasOwnProperty('stack'), true)
-    t.equal(chunk.stack, null)
+    t.equal(chunk.err.hasOwnProperty('stack'), true)
+    t.equal(chunk.err.stack, null)
     cb()
   }))
 
@@ -166,8 +180,195 @@ test('correctly ignores toString on errors', async ({ same }) => {
     pid,
     hostname,
     level: 60,
-    type: 'Error',
-    msg: err.message,
-    stack: err.stack
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack
+    },
+    msg: err.message
+  })
+})
+
+test('assign mixin()', async ({ same }) => {
+  const err = new Error('myerror')
+  const stream = sink()
+  const instance = pino({
+    mixin () {
+      return { hello: 'world' }
+    }
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    hello: 'world',
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack
+    },
+    msg: err.message
+  })
+})
+
+test('no err serializer', async ({ same }) => {
+  const err = new Error('myerror')
+  const stream = sink()
+  const instance = pino({
+    serializers: {}
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack
+    },
+    msg: err.message
+  })
+})
+
+test('empty serializer', async ({ same }) => {
+  const err = new Error('myerror')
+  const stream = sink()
+  const instance = pino({
+    serializers: {
+      err () {}
+    }
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    msg: err.message
+  })
+})
+
+test('assign mixin()', async ({ same }) => {
+  const err = new Error('myerror')
+  const stream = sink()
+  const instance = pino({
+    mixin () {
+      return { hello: 'world' }
+    }
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    hello: 'world',
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack
+    },
+    msg: err.message
+  })
+})
+
+test('no err serializer', async ({ same }) => {
+  const err = new Error('myerror')
+  const stream = sink()
+  const instance = pino({
+    serializers: {}
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    err: {
+      type: 'Error',
+      message: err.message,
+      stack: err.stack
+    },
+    msg: err.message
+  })
+})
+
+test('empty serializer', async ({ same }) => {
+  const err = new Error('myerror')
+  const stream = sink()
+  const instance = pino({
+    serializers: {
+      err () {}
+    }
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    msg: err.message
+  })
+})
+
+test('correctly adds error information when nestedKey is used', async ({ same }) => {
+  const err = new Error('myerror')
+  err.toString = () => undefined
+  const stream = sink()
+  const instance = pino({
+    test: 'this',
+    nestedKey: 'obj'
+  }, stream)
+  instance.fatal(err)
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    obj: {
+      err: {
+        type: 'Error',
+        stack: err.stack,
+        message: err.message
+      }
+    },
+    msg: err.message
+  })
+})
+
+test('correctly adds msg on error when nestedKey is used', async ({ same }) => {
+  const err = new Error('myerror')
+  err.toString = () => undefined
+  const stream = sink()
+  const instance = pino({
+    test: 'this',
+    nestedKey: 'obj'
+  }, stream)
+  instance.fatal(err, 'msg message')
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 60,
+    obj: {
+      err: {
+        type: 'Error',
+        stack: err.stack,
+        message: err.message
+      }
+    },
+    msg: 'msg message'
   })
 })
diff --git a/test/fixtures/to-file-transport.js b/test/fixtures/to-file-transport.js
new file mode 100644
index 0000000..33de462
--- /dev/null
+++ b/test/fixtures/to-file-transport.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const fs = require('fs')
+const { once } = require('events')
+
+async function run (opts) {
+  if (!opts.destination) throw new Error('kaboom')
+  const stream = fs.createWriteStream(opts.destination)
+  await once(stream, 'open')
+  return stream
+}
+
+module.exports = run
diff --git a/test/fixtures/to-file-transport.mjs b/test/fixtures/to-file-transport.mjs
new file mode 100644
index 0000000..e0cbeec
--- /dev/null
+++ b/test/fixtures/to-file-transport.mjs
@@ -0,0 +1,8 @@
+import { createWriteStream } from 'fs'
+import { once } from 'events'
+
+export default async function run (opts) {
+  const stream = createWriteStream(opts.destination)
+  await once(stream, 'open')
+  return stream
+}
diff --git a/test/fixtures/transport-main.js b/test/fixtures/transport-main.js
new file mode 100644
index 0000000..858cf56
--- /dev/null
+++ b/test/fixtures/transport-main.js
@@ -0,0 +1,9 @@
+'use strict'
+
+const { join } = require('path')
+const pino = require('../..')
+const transport = pino.transport({
+  target: join(__dirname, 'transport-worker.js')
+})
+const logger = pino(transport)
+logger.info('Hello')
diff --git a/test/fixtures/transport-worker.js b/test/fixtures/transport-worker.js
new file mode 100644
index 0000000..9a7a6ee
--- /dev/null
+++ b/test/fixtures/transport-worker.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const { Writable } = require('stream')
+module.exports = (options) => {
+  const myTransportStream = new Writable({
+    autoDestroy: true,
+    write (chunk, enc, cb) {
+      console.log(chunk.toString())
+      cb()
+    }
+  })
+  return myTransportStream
+}
diff --git a/test/fixtures/transport/index.js b/test/fixtures/transport/index.js
new file mode 100644
index 0000000..c576a7b
--- /dev/null
+++ b/test/fixtures/transport/index.js
@@ -0,0 +1,12 @@
+'use strict'
+
+const fs = require('fs')
+const { once } = require('events')
+
+async function run (opts) {
+  const stream = fs.createWriteStream(opts.destination)
+  await once(stream, 'open')
+  return stream
+}
+
+module.exports = run
diff --git a/test/fixtures/transport/package.json b/test/fixtures/transport/package.json
new file mode 100644
index 0000000..26beeaa
--- /dev/null
+++ b/test/fixtures/transport/package.json
@@ -0,0 +1,5 @@
+{
+  "name": "transport",
+  "version": "0.0.1",
+  "main": "./index.js"
+}
diff --git a/test/helper.js b/test/helper.js
index dd25d9d..ed80734 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -53,7 +53,7 @@ function sleep (ms) {
 
 function watchFileCreated (filename) {
   return new Promise((resolve, reject) => {
-    const TIMEOUT = 800
+    const TIMEOUT = 2000
     const INTERVAL = 100
     const threshold = TIMEOUT / INTERVAL
     let counter = 0
@@ -72,4 +72,4 @@ function watchFileCreated (filename) {
   })
 }
 
-module.exports = { getPathToNull, sink, check, once, sleep, watchFileCreated }
+module.exports = { getPathToNull, sink, check, once, sleep, watchFileCreated, isWin }
diff --git a/test/levels.test.js b/test/levels.test.js
index 071653d..9dbe87c 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -269,15 +269,6 @@ test('resets levels from labels to numbers', async ({ equal }) => {
   instance.info('hello world')
 })
 
-test('aliases changeLevelName to levelKey', async ({ equal }) => {
-  const instance = pino({ changeLevelName: 'priority' }, sink((result, enc, cb) => {
-    equal(result.priority, 30)
-    cb()
-  }))
-
-  instance.info('hello world')
-})
-
 test('changes label naming when told to', async ({ equal }) => {
   const expected = [{
     priority: 30,
diff --git a/test/metadata.test.js b/test/metadata.test.js
index 4ab770d..1330973 100644
--- a/test/metadata.test.js
+++ b/test/metadata.test.js
@@ -16,7 +16,7 @@ test('metadata works', async ({ ok, same, equal }) => {
       equal(30, this.lastLevel)
       equal('a msg', this.lastMsg)
       ok(Number(this.lastTime) >= now)
-      same(this.lastObj, { hello: 'world', msg: 'a msg' })
+      same(this.lastObj, { hello: 'world' })
       const result = JSON.parse(chunk)
       ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
       delete result.time
@@ -40,7 +40,7 @@ test('child loggers works', async ({ ok, same, equal }) => {
       equal(child, this.lastLogger)
       equal(30, this.lastLevel)
       equal('a msg', this.lastMsg)
-      same(this.lastObj, { from: 'child', msg: 'a msg' })
+      same(this.lastObj, { from: 'child' })
       const result = JSON.parse(chunk)
       ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
       delete result.time
@@ -66,7 +66,7 @@ test('without object', async ({ ok, same, equal }) => {
       equal(instance, this.lastLogger)
       equal(30, this.lastLevel)
       equal('a msg', this.lastMsg)
-      same({ msg: 'a msg' }, this.lastObj)
+      same({ }, this.lastObj)
       const result = JSON.parse(chunk)
       ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
       delete result.time
diff --git a/test/mixin.test.js b/test/mixin.test.js
index 94b521d..ced8ef0 100644
--- a/test/mixin.test.js
+++ b/test/mixin.test.js
@@ -106,7 +106,8 @@ test('mixin can use context', async ({ ok }) => {
   const stream = sink()
   const instance = pino({
     mixin (context) {
-      ok(context !== null && context !== undefined, 'context should be defined')
+      ok(context !== null, 'context should be defined')
+      ok(context !== undefined, 'context should be defined')
       return Object.assign({
         error: context.message,
         stack: context.stack
@@ -124,8 +125,8 @@ test('mixin works without context', async ({ ok }) => {
   const stream = sink()
   const instance = pino({
     mixin (context) {
-      ok(context !== null && context !== undefined, 'context is still defined w/o passing mergeObject')
-
+      ok(context !== null, 'context is still defined w/o passing mergeObject')
+      ok(context !== undefined, 'context is still defined w/o passing mergeObject')
       return {
         something: true
       }
diff --git a/test/multistream.test.js b/test/multistream.test.js
new file mode 100644
index 0000000..6bae9c8
--- /dev/null
+++ b/test/multistream.test.js
@@ -0,0 +1,506 @@
+'use strict'
+
+const writeStream = require('flush-write-stream')
+const { join } = require('path')
+const { readFileSync } = require('fs')
+const os = require('os')
+const test = require('tap').test
+const pino = require('../')
+const multistream = pino.multistream
+
+test('sends to multiple streams using string levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const log = pino({
+    level: 'trace'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 9)
+  t.end()
+})
+
+test('sends to multiple streams using custom levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const log = pino({
+    level: 'trace'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 9)
+  t.end()
+})
+
+test('sends to multiple streams using optionally predefined levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const opts = {
+    levels: {
+      silent: Infinity,
+      fatal: 60,
+      error: 50,
+      warn: 50,
+      info: 30,
+      debug: 20,
+      trace: 10
+    }
+  }
+  const streams = [
+    { stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'info', stream: stream },
+    { level: 'warn', stream: stream },
+    { level: 'error', stream: stream },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const mstream = multistream(streams, opts)
+  const log = pino({
+    level: 'trace'
+  }, mstream)
+  log.trace('trace stream')
+  log.debug('debug stream')
+  log.info('info stream')
+  log.warn('warn stream')
+  log.error('error stream')
+  log.fatal('fatal stream')
+  log.silent('silent stream')
+  t.equal(messageCount, 24)
+  t.end()
+})
+
+test('sends to multiple streams using number levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 20, stream: stream },
+    { level: 60, stream: stream }
+  ]
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 6)
+  t.end()
+})
+
+test('level include higher levels', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const log = pino({}, multistream([{ level: 'info', stream: stream }]))
+  log.fatal('message')
+  t.equal(messageCount, 1)
+  t.end()
+})
+
+test('supports multiple arguments', function (t) {
+  const messages = []
+  const stream = writeStream(function (data, enc, cb) {
+    messages.push(JSON.parse(data))
+    if (messages.length === 2) {
+      const msg1 = messages[0]
+      t.equal(msg1.msg, 'foo bar baz foobar')
+
+      const msg2 = messages[1]
+      t.equal(msg2.msg, 'foo bar baz foobar barfoo foofoo')
+
+      t.end()
+    }
+    cb()
+  })
+  const log = pino({}, multistream({ stream }))
+  log.info('%s %s %s %s', 'foo', 'bar', 'baz', 'foobar') // apply not invoked
+  log.info('%s %s %s %s %s %s', 'foo', 'bar', 'baz', 'foobar', 'barfoo', 'foofoo') // apply invoked
+})
+
+test('supports children', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    const input = JSON.parse(data)
+    t.equal(input.msg, 'child stream')
+    t.equal(input.child, 'one')
+    t.end()
+    cb()
+  })
+  const streams = [
+    { stream: stream }
+  ]
+  const log = pino({}, multistream(streams)).child({ child: 'one' })
+  log.info('child stream')
+})
+
+test('supports grandchildren', function (t) {
+  const messages = []
+  const stream = writeStream(function (data, enc, cb) {
+    messages.push(JSON.parse(data))
+    if (messages.length === 3) {
+      const msg1 = messages[0]
+      t.equal(msg1.msg, 'grandchild stream')
+      t.equal(msg1.child, 'one')
+      t.equal(msg1.grandchild, 'two')
+
+      const msg2 = messages[1]
+      t.equal(msg2.msg, 'grandchild stream')
+      t.equal(msg2.child, 'one')
+      t.equal(msg2.grandchild, 'two')
+
+      const msg3 = messages[2]
+      t.equal(msg3.msg, 'debug grandchild')
+      t.equal(msg3.child, 'one')
+      t.equal(msg3.grandchild, 'two')
+
+      t.end()
+    }
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream }
+  ]
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams)).child({ child: 'one' }).child({ grandchild: 'two' })
+  log.info('grandchild stream')
+  log.debug('debug grandchild')
+})
+
+test('supports custom levels', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    t.equal(JSON.parse(data).msg, 'bar')
+    t.end()
+  })
+  const log = pino({
+    customLevels: {
+      foo: 35
+    }
+  }, multistream([{ level: 35, stream: stream }]))
+  log.foo('bar')
+})
+
+test('supports pretty print', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    t.not(data.toString().match(/INFO.*: pretty print/), null)
+    t.end()
+    cb()
+  })
+  const outStream = pino({
+    prettyPrint: {
+      levelFirst: true,
+      colorize: false
+    }
+  }, stream)
+
+  const log = pino({
+    level: 'debug',
+    name: 'helloName'
+  }, multistream([
+    { stream: outStream[pino.symbols.streamSym] }
+  ]))
+
+  log.info('pretty print')
+})
+
+test('children support custom levels', function (t) {
+  const stream = writeStream(function (data, enc, cb) {
+    t.equal(JSON.parse(data).msg, 'bar')
+    t.end()
+  })
+  const parent = pino({
+    customLevels: {
+      foo: 35
+    }
+  }, multistream([{ level: 35, stream: stream }]))
+  const child = parent.child({ child: 'yes' })
+  child.foo('bar')
+})
+
+test('levelVal ovverides level', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'blabla', levelVal: 15, stream: stream },
+    { level: 60, stream: stream }
+  ]
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 6)
+  t.end()
+})
+
+test('forwards metadata', function (t) {
+  t.plan(4)
+  const streams = [
+    {
+      stream: {
+        [Symbol.for('pino.metadata')]: true,
+        write (chunk) {
+          t.equal(log, this.lastLogger)
+          t.equal(30, this.lastLevel)
+          t.same({ hello: 'world' }, this.lastObj)
+          t.same('a msg', this.lastMsg)
+        }
+      }
+    }
+  ]
+
+  const log = pino({
+    level: 'debug'
+  }, multistream(streams))
+
+  log.info({ hello: 'world' }, 'a msg')
+  t.end()
+})
+
+test('forward name', function (t) {
+  t.plan(2)
+  const streams = [
+    {
+      stream: {
+        [Symbol.for('pino.metadata')]: true,
+        write (chunk) {
+          const line = JSON.parse(chunk)
+          t.equal(line.name, 'helloName')
+          t.equal(line.hello, 'world')
+        }
+      }
+    }
+  ]
+
+  const log = pino({
+    level: 'debug',
+    name: 'helloName'
+  }, multistream(streams))
+
+  log.info({ hello: 'world' }, 'a msg')
+  t.end()
+})
+
+test('forward name with child', function (t) {
+  t.plan(3)
+  const streams = [
+    {
+      stream: {
+        write (chunk) {
+          const line = JSON.parse(chunk)
+          t.equal(line.name, 'helloName')
+          t.equal(line.hello, 'world')
+          t.equal(line.component, 'aComponent')
+        }
+      }
+    }
+  ]
+
+  const log = pino({
+    level: 'debug',
+    name: 'helloName'
+  }, multistream(streams)).child({ component: 'aComponent' })
+
+  log.info({ hello: 'world' }, 'a msg')
+  t.end()
+})
+
+test('clone generates a new multistream with all stream at the same level', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream },
+    { level: 'fatal', stream: stream }
+  ]
+  const ms = multistream(streams)
+  const clone = ms.clone(30)
+
+  t.not(clone, ms)
+
+  clone.streams.forEach((s, i) => {
+    t.not(s, streams[i])
+    t.equal(s.stream, streams[i].stream)
+    t.equal(s.level, 30)
+  })
+
+  const log = pino({
+    level: 'trace'
+  }, clone)
+
+  log.info('info stream')
+  log.debug('debug message not counted')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 8)
+
+  t.end()
+})
+
+test('one stream', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const log = pino({
+    level: 'trace'
+  }, multistream({ stream, level: 'fatal' }))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 1)
+  t.end()
+})
+
+test('dedupe', function (t) {
+  let messageCount = 0
+  const stream1 = writeStream(function (data, enc, cb) {
+    messageCount -= 1
+    cb()
+  })
+
+  const stream2 = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+
+  const streams = [
+    {
+      stream: stream1,
+      level: 'info'
+    },
+    {
+      stream: stream2,
+      level: 'fatal'
+    }
+  ]
+
+  const log = pino({
+    level: 'trace'
+  }, multistream(streams, { dedupe: true }))
+  log.info('info stream')
+  log.fatal('fatal stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 1)
+  t.end()
+})
+
+test('no stream', function (t) {
+  const log = pino({
+    level: 'trace'
+  }, multistream())
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.end()
+})
+
+test('add a stream', function (t) {
+  let messageCount = 0
+  const stream = writeStream(function (data, enc, cb) {
+    messageCount += 1
+    cb()
+  })
+  const log = pino({
+    level: 'trace'
+  }, multistream(stream))
+  log.info('info stream')
+  log.debug('debug stream')
+  log.fatal('fatal stream')
+  t.equal(messageCount, 2)
+  t.end()
+})
+
+test('flushSync', function (t) {
+  const tmp = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const destination = pino.destination({ dest: tmp, sync: false, minLength: 4096 })
+  const log = pino({ level: 'info' }, multistream([{ level: 'info', stream: destination }]))
+  destination.on('ready', () => {
+    log.info('foo')
+    log.info('bar')
+    t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 0)
+    pino.final(log, (err, finalLogger) => {
+      if (err) {
+        t.fail()
+        return t.end()
+      }
+      t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 2)
+      finalLogger.info('biz')
+      t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 3)
+      t.end()
+    })()
+  })
+})
+
+test('ends all streams', function (t) {
+  t.plan(7)
+  const stream = writeStream(function (data, enc, cb) {
+    t.pass('message')
+    cb()
+  })
+  stream.flushSync = function () {
+    t.pass('flushSync')
+  }
+  // stream2 has no flushSync
+  const stream2 = writeStream(function (data, enc, cb) {
+    t.pass('message2')
+    cb()
+  })
+  const streams = [
+    { stream: stream },
+    { level: 'debug', stream: stream },
+    { level: 'trace', stream: stream2 },
+    { level: 'fatal', stream: stream },
+    { level: 'silent', stream: stream }
+  ]
+  const multi = multistream(streams)
+  const log = pino({
+    level: 'trace'
+  }, multi)
+  log.info('info stream')
+  multi.end()
+})
diff --git a/test/pretty.test.js b/test/pretty.test.js
index a3133cb..0f2569e 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -257,8 +257,8 @@ test('errors with props', async ({ not }) => {
   }))
   await once(child, 'close')
   not(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): kaboom/), null)
-  not(strip(actual).match(/code: ENOENT/), null)
-  not(strip(actual).match(/errno: 1/), null)
+  not(strip(actual).match(/"code": "ENOENT"/), null)
+  not(strip(actual).match(/"errno": 1/), null)
   not(strip(actual).match(/.*error-props\.js.*/), null)
 })
 
diff --git a/test/redact.test.js b/test/redact.test.js
index ccfdaf4..f719847 100644
--- a/test/redact.test.js
+++ b/test/redact.test.js
@@ -776,6 +776,18 @@ test('child bindings are redacted using wildcard and plain path keys', async ({
   equal(req.method, '[Redacted]')
 })
 
+test('redacts boolean at the top level', async ({ equal }) => {
+  const stream = sink()
+  const instance = pino({ redact: ['msg'] }, stream)
+  const obj = {
+    s: 's'
+  }
+  instance.info(obj, true)
+  const o = await once(stream, 'data')
+  equal(o.s, 's')
+  equal(o.msg, '[Redacted]')
+})
+
 test('child can customize redact', async ({ equal }) => {
   const stream = sink()
   const instance = pino({ redact: ['req.method', '*.headers.cookie'] }, stream)
diff --git a/test/serializers.test.js b/test/serializers.test.js
index 5655be7..35edee8 100644
--- a/test/serializers.test.js
+++ b/test/serializers.test.js
@@ -1,6 +1,7 @@
 'use strict'
 const { test } = require('tap')
 const { sink, once } = require('./helper')
+const stdSerializers = require('pino-std-serializers')
 const pino = require('../')
 
 const parentSerializers = {
@@ -43,6 +44,29 @@ test('custom serializer overrides default err namespace error serializer', async
   equal(typeof o.err.s, 'string')
 })
 
+test('custom serializer overrides default err namespace error serializer when nestedKey is on', async ({ equal }) => {
+  const stream = sink()
+  const parent = pino({
+    nestedKey: 'obj',
+    serializers: {
+      err: (e) => {
+        return {
+          t: e.constructor.name,
+          m: e.message,
+          s: e.stack
+        }
+      }
+    }
+  }, stream)
+
+  parent.info({ err: ReferenceError('test') })
+  const o = await once(stream, 'data')
+  equal(typeof o.obj.err, 'object')
+  equal(o.obj.err.t, 'ReferenceError')
+  equal(o.obj.err.m, 'test')
+  equal(typeof o.obj.err.s, 'string')
+})
+
 test('null overrides default err namespace error serializer', async ({ equal }) => {
   const stream = sink()
   const parent = pino({ serializers: { err: null } }, stream)
@@ -91,13 +115,17 @@ test('child does not overwrite parent serializers', async ({ equal }) => {
   equal((await o2).test, 'child')
 })
 
-test('Symbol.for(\'pino.serializers\')', async ({ equal, not }) => {
+test('Symbol.for(\'pino.serializers\')', async ({ equal, same, not }) => {
   const stream = sink()
+  const expected = Object.assign({
+    err: stdSerializers.err
+  }, parentSerializers)
   const parent = pino({ serializers: parentSerializers }, stream)
   const child = parent.child({ a: 'property' })
 
-  equal(parent[Symbol.for('pino.serializers')], parentSerializers)
-  equal(child[Symbol.for('pino.serializers')], parentSerializers)
+  same(parent[Symbol.for('pino.serializers')], expected)
+  same(child[Symbol.for('pino.serializers')], expected)
+  equal(parent[Symbol.for('pino.serializers')], child[Symbol.for('pino.serializers')])
 
   const child2 = parent.child({}, {
     serializers: {
@@ -124,14 +152,17 @@ test('children inherit parent serializers', async ({ equal }) => {
   equal(o.test, 'parent')
 })
 
-test('children inherit parent Symbol serializers', async ({ equal, not }) => {
+test('children inherit parent Symbol serializers', async ({ equal, same, not }) => {
   const stream = sink()
   const symbolSerializers = {
-    [Symbol.for('pino.*')]: parentSerializers.test
+    [Symbol.for('b')]: b
   }
+  const expected = Object.assign({
+    err: stdSerializers.err
+  }, symbolSerializers)
   const parent = pino({ serializers: symbolSerializers }, stream)
 
-  equal(parent[Symbol.for('pino.serializers')], symbolSerializers)
+  same(parent[Symbol.for('pino.serializers')], expected)
 
   const child = parent.child({}, {
     serializers: {
@@ -144,10 +175,13 @@ test('children inherit parent Symbol serializers', async ({ equal, not }) => {
     return 'hello'
   }
 
-  not(child[Symbol.for('pino.serializers')], symbolSerializers)
-  equal(child[Symbol.for('pino.serializers')].a, a)
-  equal(child[Symbol.for('pino.serializers')][Symbol.for('a')], a)
-  equal(child[Symbol.for('pino.serializers')][Symbol.for('pino.*')], parentSerializers.test)
+  function b () {
+    return 'world'
+  }
+
+  same(child[Symbol.for('pino.serializers')].a, a)
+  same(child[Symbol.for('pino.serializers')][Symbol.for('b')], b)
+  same(child[Symbol.for('pino.serializers')][Symbol.for('a')], a)
 })
 
 test('children serializers get called', async ({ equal }) => {
@@ -206,3 +240,14 @@ test('non-overridden serializers are available in the children', async ({ equal
   parent.fatal({ onlyChild: 'test' })
   equal((await o4).onlyChild, 'test')
 })
+
+test('custom serializer for messageKey', async (t) => {
+  const stream = sink()
+  const instance = pino({ serializers: { msg: () => '422' } }, stream)
+
+  const o = { num: NaN }
+  instance.info(o, 42)
+
+  const { msg } = await once(stream, 'data')
+  t.equal(msg, '422')
+})
diff --git a/test/targets.test.js b/test/targets.test.js
new file mode 100644
index 0000000..c6bc048
--- /dev/null
+++ b/test/targets.test.js
@@ -0,0 +1,66 @@
+'use strict'
+
+const { test } = require('tap')
+const proxyquire = require('proxyquire')
+const Writable = require('stream').Writable
+
+test('pretty-target mocked', async function ({ equal, same, plan, pass }) {
+  plan(3)
+  let ret
+  const prettyTarget = proxyquire('../lib/pretty-target', {
+    '../pino': {
+      destination (opts) {
+        same(opts, { dest: 1, sync: false })
+
+        ret = new Writable()
+        ret.fd = opts.dest
+
+        process.nextTick(() => {
+          ret.emit('ready')
+        })
+
+        Object.defineProperty(ret, 'end', {
+          get () {
+            return 'unused'
+          },
+          set (end) {
+            pass('prop set')
+            const obj = {
+              emit (ev) {
+                equal(ev, 'close')
+              },
+              end
+            }
+            obj.end()
+          }
+        })
+        return ret
+      }
+    }
+  })
+
+  await prettyTarget()
+})
+
+test('file-target mocked', async function ({ equal, same, plan, pass }) {
+  plan(1)
+  let ret
+  const fileTarget = proxyquire('../lib/file-target', {
+    '../pino': {
+      destination (opts) {
+        same(opts, { dest: 1, sync: false })
+
+        ret = new Writable()
+        ret.fd = opts.dest
+
+        process.nextTick(() => {
+          ret.emit('ready')
+        })
+
+        return ret
+      }
+    }
+  })
+
+  await fileTarget()
+})
diff --git a/test/transport.test.js b/test/transport.test.js
new file mode 100644
index 0000000..4451c55
--- /dev/null
+++ b/test/transport.test.js
@@ -0,0 +1,382 @@
+'use strict'
+
+const os = require('os')
+const { join } = require('path')
+const { once } = require('events')
+const { readFile, symlink, unlink } = require('fs').promises
+const { test } = require('tap')
+const { isWin, watchFileCreated } = require('./helper')
+const pino = require('../')
+const url = require('url')
+const strip = require('strip-ansi')
+const execa = require('execa')
+const writer = require('flush-write-stream')
+
+const { pid } = process
+const hostname = os.hostname()
+
+test('pino.transport with file', async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    target: join(__dirname, 'fixtures', 'to-file-transport.js'),
+    options: { destination }
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with file (no options + error handling)', async ({ equal }) => {
+  const transport = pino.transport({
+    target: join(__dirname, 'fixtures', 'to-file-transport.js')
+  })
+  const [err] = await once(transport, 'error')
+  equal(err.message, 'kaboom')
+})
+
+// TODO make this test pass on Windows
+test('pino.transport with package', { skip: isWin }, async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+
+  try {
+    await unlink(join(__dirname, '..', 'node_modules', 'transport'))
+  } catch {}
+
+  await symlink(
+    join(__dirname, 'fixtures', 'transport'),
+    join(__dirname, '..', 'node_modules', 'transport')
+  )
+
+  const transport = pino.transport({
+    target: 'transport',
+    options: { destination }
+  })
+  teardown(async () => {
+    await unlink(join(__dirname, '..', 'node_modules', 'transport'))
+    transport.end()
+  })
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with file URL', async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    target: url.pathToFileURL(join(__dirname, 'fixtures', 'to-file-transport.js')).href,
+    options: { destination }
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport errors if file does not exists', ({ plan, pass }) => {
+  plan(1)
+  const instance = pino.transport({
+    target: join(__dirname, 'fixtures', 'non-existent-file'),
+    worker: {
+      stdin: true,
+      stdout: true,
+      stderr: true
+    }
+  })
+  instance.on('error', function () {
+    pass('error received')
+  })
+})
+
+test('pino.transport with esm', async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    target: join(__dirname, 'fixtures', 'to-file-transport.mjs'),
+    options: { destination }
+  })
+  const instance = pino(transport)
+  teardown(transport.end.bind(transport))
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with two files', async ({ same, teardown }) => {
+  const dest1 = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const dest2 = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    targets: [{
+      level: 'info',
+      target: join(__dirname, 'fixtures', 'to-file-transport.js'),
+      options: { destination: dest1 }
+    }, {
+      level: 'info',
+      target: join(__dirname, 'fixtures', 'to-file-transport.js'),
+      options: { destination: dest2 }
+    }]
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
+  const result1 = JSON.parse(await readFile(dest1))
+  delete result1.time
+  same(result1, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+  const result2 = JSON.parse(await readFile(dest2))
+  delete result2.time
+  same(result2, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with an array including a prettyPrint destination', async ({ same, match, teardown }) => {
+  const dest1 = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const dest2 = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    targets: [{
+      level: 'info',
+      target: '#pino/file',
+      options: {
+        destination: dest1
+      }
+    }, {
+      level: 'info',
+      target: '#pino/pretty',
+      options: {
+        destination: dest2
+      }
+    }]
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
+  const result1 = JSON.parse(await readFile(dest1))
+  delete result1.time
+  same(result1, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+  const actual = (await readFile(dest2)).toString()
+  match(strip(actual), /\[.*\] INFO.*hello/)
+})
+
+test('no transport.end()', async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    target: join(__dirname, 'fixtures', 'to-file-transport.js'),
+    options: { destination }
+  })
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('autoEnd = false', async ({ equal, same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const count = process.listenerCount('exit')
+  const transport = pino.transport({
+    target: join(__dirname, 'fixtures', 'to-file-transport.js'),
+    options: { destination },
+    worker: { autoEnd: false }
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+
+  equal(count, process.listenerCount('exit'))
+
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with target and targets', async ({ fail, equal }) => {
+  try {
+    pino.transport({
+      target: '/a/file',
+      targets: [{
+        target: '/a/file'
+      }]
+    })
+    fail('must throw')
+  } catch (err) {
+    equal(err.message, 'Only one of target or targets can be specified')
+  }
+})
+
+// TODO make this test pass on Windows
+test('pino.transport with package as a target', { skip: isWin }, async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+
+  try {
+    await unlink(join(__dirname, '..', 'node_modules', 'transport'))
+  } catch {}
+
+  await symlink(
+    join(__dirname, 'fixtures', 'transport'),
+    join(__dirname, '..', 'node_modules', 'transport')
+  )
+
+  const transport = pino.transport({
+    targets: [{
+      target: 'transport',
+      options: { destination }
+    }]
+  })
+  teardown(async () => {
+    await unlink(join(__dirname, '..', 'node_modules', 'transport'))
+    transport.end()
+  })
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with target #pino/file', async ({ same, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    target: '#pino/file',
+    options: { destination }
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const result = JSON.parse(await readFile(destination))
+  delete result.time
+  same(result, {
+    pid,
+    hostname,
+    level: 30,
+    msg: 'hello'
+  })
+})
+
+test('pino.transport with target #pino/pretty', async ({ match, teardown }) => {
+  const destination = join(
+    os.tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+  )
+  const transport = pino.transport({
+    target: '#pino/pretty',
+    options: { destination }
+  })
+  teardown(transport.end.bind(transport))
+  const instance = pino(transport)
+  instance.info('hello')
+  await watchFileCreated(destination)
+  const actual = await readFile(destination, 'utf8')
+  match(strip(actual), /\[.*\] INFO.*hello/)
+})
+
+test('stdout in worker', async ({ not }) => {
+  let actual = ''
+  const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'transport-main.js')])
+
+  child.stdout.pipe(writer((s, enc, cb) => {
+    actual += s
+    cb()
+  }))
+  await once(child, 'close')
+  not(strip(actual).match(/Hello/), null)
+})
diff --git a/test/types/pino-import.test-d.ts b/test/types/pino-import.test-d.ts
new file mode 100644
index 0000000..82f78c0
--- /dev/null
+++ b/test/types/pino-import.test-d.ts
@@ -0,0 +1,29 @@
+import { expectType } from "tsd";
+
+import pino from '../../pino';
+import { pino as pinoNamed, P } from "../../pino";
+import * as pinoStar from "../../pino";
+import pinoCjsImport = require ("../../pino");
+const pinoCjs = require("../../pino");
+const { P: pinoCjsNamed } = require('pino')
+
+const log = pino();
+expectType<P.LogFn>(log.info);
+expectType<P.LogFn>(log.error);
+
+expectType<pino.Logger>(pinoNamed());
+expectType<P.Logger>(pinoNamed());
+expectType<pino.Logger>(pinoStar.default());
+expectType<pino.Logger>(pinoStar.pino());
+expectType<pino.Logger>(pinoCjsImport.default());
+expectType<pino.Logger>(pinoCjsImport.pino());
+expectType<any>(pinoCjsNamed());
+expectType<any>(pinoCjs());
+
+const levelChangeEventListener: P.LevelChangeEventListener = (
+    lvl: P.LevelWithSilent | string,
+    val: number,
+    prevLvl: P.LevelWithSilent | string,
+    prevVal: number,
+) => {}
+expectType<P.LevelChangeEventListener>(levelChangeEventListener)
diff --git a/test/types/pino-transport.test-d.ts b/test/types/pino-transport.test-d.ts
new file mode 100644
index 0000000..292b04b
--- /dev/null
+++ b/test/types/pino-transport.test-d.ts
@@ -0,0 +1,47 @@
+import { pino } from '../../pino'
+
+// Single
+const transport = pino.transport({
+    target: '#pino/pretty',
+    options: { some: 'options for', the: 'transport' }
+})
+pino(transport)
+
+// Multiple
+const transports = pino.transport({targets: [
+    {
+        level: 'info',
+        target: '#pino/pretty',
+        options: { some: 'options for', the: 'transport' }
+    },
+    {
+        level: 'trace',
+        target: '#pino/file',
+        options: { destination: './test.log' }
+    }
+]})
+pino(transports)
+
+type TransportConfig = {
+    id: string
+}
+
+// Custom transport params
+const customTransport = pino.transport<TransportConfig>({
+    target: 'custom',
+    options: { id: 'abc' }
+})
+pino(customTransport)
+
+// Worker
+pino.transport({
+    target: 'custom',
+    worker: {
+        argv: ['a', 'b'],
+        stdin: false,
+        stderr: true,
+        stdout: false,
+        autoEnd: true,
+    },
+    options: { id: 'abc' }
+})
diff --git a/test/types/pino.test-d.ts b/test/types/pino.test-d.ts
new file mode 100644
index 0000000..d6f8c53
--- /dev/null
+++ b/test/types/pino.test-d.ts
@@ -0,0 +1,269 @@
+import P, { pino } from "../../";
+import { IncomingMessage, ServerResponse } from "http";
+import { Socket } from "net";
+
+const log = pino();
+const info = log.info;
+const error = log.error;
+
+info("hello world");
+error("this is at error level");
+info("the answer is %d", 42);
+info({ obj: 42 }, "hello world");
+info({ obj: 42, b: 2 }, "hello world");
+info({ obj: { aa: "bbb" } }, "another");
+setImmediate(info, "after setImmediate");
+error(new Error("an error"));
+
+const writeSym = pino.symbols.writeSym;
+
+const testUniqSymbol = {
+    [pino.symbols.needsMetadataGsym]: true,
+}[pino.symbols.needsMetadataGsym];
+
+const log2: P.Logger = pino({
+    name: "myapp",
+    safe: true,
+    serializers: {
+        req: pino.stdSerializers.req,
+        res: pino.stdSerializers.res,
+        err: pino.stdSerializers.err,
+    },
+});
+
+pino({
+    write(o) {},
+});
+
+pino({
+    mixin() {
+        return { customName: "unknown", customId: 111 };
+    },
+});
+
+pino({
+    mixin: () => ({ customName: "unknown", customId: 111 }),
+});
+
+pino({
+    redact: { paths: [], censor: "SECRET" },
+});
+
+pino({
+    redact: { paths: [], censor: () => "SECRET" },
+});
+
+pino({
+    browser: {
+        write(o) {},
+    },
+});
+
+pino({
+    browser: {
+        write: {
+            info(o) {},
+            error(o) {},
+        },
+        serialize: true,
+        asObject: true,
+        transmit: {
+            level: "fatal",
+            send: (level, logEvent) => {
+                level;
+                logEvent.bindings;
+                logEvent.level;
+                logEvent.ts;
+                logEvent.messages;
+            },
+        },
+    },
+});
+
+pino({ base: null });
+if ("pino" in log) console.log(`pino version: ${log.pino}`);
+
+log.child({ a: "property" }).info("hello child!");
+log.level = "error";
+log.info("nope");
+const child = log.child({ foo: "bar" });
+child.info("nope again");
+child.level = "info";
+child.info("hooray");
+log.info("nope nope nope");
+log.child({ foo: "bar" }, { level: "debug" }).debug("debug!");
+child.bindings();
+const customSerializers = {
+    test() {
+        return "this is my serializer";
+    },
+};
+pino().child({}, { serializers: customSerializers }).info({ test: "should not show up" });
+const child2 = log.child({ father: true });
+const childChild = child2.child({ baby: true });
+const childRedacted = pino().child({}, { redact: ["path"] })
+childRedacted.info({
+  msg: "logged with redacted properties",
+  path: "Not shown",
+});
+const childAnotherRedacted = pino().child({}, { 
+    redact: {
+        paths: ["anotherPath"],
+        censor: "Not the log you\re looking for",
+    }
+})
+childAnotherRedacted.info({
+    msg: "another logged with redacted properties",
+    anotherPath: "Not shown",
+});
+
+log.level = "info";
+if (log.levelVal === 30) {
+    console.log("logger level is `info`");
+}
+
+log.level = "myLevel";
+log.myLevel("a message");
+
+const listener = (lvl: any, val: any, prevLvl: any, prevVal: any) => {
+    console.log(lvl, val, prevLvl, prevVal);
+};
+log.on("level-change", (lvl, val, prevLvl, prevVal) => {
+    console.log(lvl, val, prevLvl, prevVal);
+});
+log.level = "trace";
+log.removeListener("level-change", listener);
+log.level = "info";
+
+pino.levels.values.error === 50;
+pino.levels.labels[50] === "error";
+
+const logstderr: pino.Logger = pino(process.stderr);
+logstderr.error("on stderr instead of stdout");
+
+log.useLevelLabels = true;
+log.info("lol");
+log.level === "info";
+const isEnabled: boolean = log.isLevelEnabled("info");
+
+const redacted = pino({
+    redact: ["path"],
+});
+
+redacted.info({
+    msg: "logged with redacted properties",
+    path: "Not shown",
+});
+
+const anotherRedacted = pino({
+    redact: {
+        paths: ["anotherPath"],
+        censor: "Not the log you\re looking for",
+    },
+});
+
+anotherRedacted.info({
+    msg: "another logged with redacted properties",
+    anotherPath: "Not shown",
+});
+
+const pretty = pino({
+    prettyPrint: {
+        colorize: true,
+        crlf: false,
+        errorLikeObjectKeys: ["err", "error"],
+        errorProps: "",
+        messageFormat: false,
+        ignore: "",
+        levelFirst: false,
+        messageKey: "msg",
+        timestampKey: "timestamp",
+        translateTime: "UTC:h:MM:ss TT Z",
+        search: "foo == `bar`",
+    },
+});
+
+const withMessageFormatFunc = pino({
+    prettyPrint: {
+        ignore: "requestId",
+        messageFormat: (log, messageKey: string) => {
+            const message = log[messageKey] as string;
+            if (log.requestId) return `[${log.requestId}] ${message}`;
+            return message;
+        },
+    },
+});
+
+const withTimeFn = pino({
+    timestamp: pino.stdTimeFunctions.isoTime,
+});
+
+const withNestedKey = pino({
+    nestedKey: "payload",
+});
+
+const withHooks = pino({
+    hooks: {
+        logMethod(args, method, level) {
+            return method.apply(this, ['msg', ...args]);
+        },
+    },
+});
+
+// Properties/types imported from pino-std-serializers
+const wrappedErrSerializer = pino.stdSerializers.wrapErrorSerializer((err: pino.SerializedError) => {
+    return { ...err, newProp: "foo" };
+});
+const wrappedReqSerializer = pino.stdSerializers.wrapRequestSerializer((req: pino.SerializedRequest) => {
+    return { ...req, newProp: "foo" };
+});
+const wrappedResSerializer = pino.stdSerializers.wrapResponseSerializer((res: pino.SerializedResponse) => {
+    return { ...res, newProp: "foo" };
+});
+
+const socket = new Socket();
+const incomingMessage = new IncomingMessage(socket);
+const serverResponse = new ServerResponse(incomingMessage);
+
+const mappedHttpRequest: { req: pino.SerializedRequest } = pino.stdSerializers.mapHttpRequest(incomingMessage);
+const mappedHttpResponse: { res: pino.SerializedResponse } = pino.stdSerializers.mapHttpResponse(serverResponse);
+
+const serializedErr: pino.SerializedError = pino.stdSerializers.err(new Error());
+const serializedReq: pino.SerializedRequest = pino.stdSerializers.req(incomingMessage);
+const serializedRes: pino.SerializedResponse = pino.stdSerializers.res(serverResponse);
+
+/**
+ * Destination static method
+ */
+const destinationViaDefaultArgs = pino.destination();
+const destinationViaStrFileDescriptor = pino.destination("/log/path");
+const destinationViaNumFileDescriptor = pino.destination(2);
+const destinationViaStream = pino.destination(process.stdout);
+const destinationViaOptionsObject = pino.destination({ dest: "/log/path", sync: false });
+
+pino(destinationViaDefaultArgs);
+pino({ name: "my-logger" }, destinationViaDefaultArgs);
+pino(destinationViaStrFileDescriptor);
+pino({ name: "my-logger" }, destinationViaStrFileDescriptor);
+pino(destinationViaNumFileDescriptor);
+pino({ name: "my-logger" }, destinationViaNumFileDescriptor);
+pino(destinationViaStream);
+pino({ name: "my-logger" }, destinationViaStream);
+pino(destinationViaOptionsObject);
+pino({ name: "my-logger" }, destinationViaOptionsObject);
+
+interface StrictShape {
+    activity: string;
+    err?: unknown;
+}
+
+info<StrictShape>({
+    activity: "Required property",
+});
+
+const logLine: pino.LogDescriptor = {
+    level: 20,
+    msg: "A log message",
+    time: new Date().getTime(),
+    aCustomProperty: true,
+};
diff --git a/test/types/pino.ts b/test/types/pino.ts
new file mode 100644
index 0000000..ae7fc36
--- /dev/null
+++ b/test/types/pino.ts
@@ -0,0 +1,40 @@
+import { pino } from '../../pino'
+import { join } from 'path'
+import { tmpdir } from'os'
+
+const destination = join(
+    tmpdir(),
+    '_' + Math.random().toString(36).substr(2, 9)
+)
+
+// Single
+const transport = pino.transport({
+    target: '#pino/pretty',
+    options: { some: 'options for', the: 'transport' }
+})
+const logger = pino(transport)
+logger.info('test2')
+
+const transport2 = pino.transport({
+    target: '#pino/pretty',
+})
+const logger2 = pino(transport2)
+logger2.info('test2')
+
+
+// Multiple
+
+const transports = pino.transport({targets: [
+    {
+        level: 'info',
+        target: '#pino/pretty',
+        options: { some: 'options for', the: 'transport' }
+    },
+    {
+        level: 'trace',
+        target: '#pino/file',
+        options: { destination }
+    }
+]})
+const loggerMulti = pino(transports)
+loggerMulti.info('test2')