Repository: pinojs/pino
Pull request #1382 (2022-03-22 15:34:26)

Bumped thread-stream version


Patch:
diff --git a/.taprc b/.taprc.yaml
similarity index 60%
rename from .taprc
rename to .taprc.yaml
index fa972ed..7c11ab9 100644
--- a/.taprc
+++ b/.taprc.yaml
@@ -1,3 +1,6 @@
 coverage: true
 timeout: 480
 check-coverage: false
+
+files:
+  - 'test/**/*.test.js'
diff --git a/docs/transports.md b/docs/transports.md
index 9a1295d..506fd7c 100644
--- a/docs/transports.md
+++ b/docs/transports.md
@@ -96,6 +96,19 @@ const transport = pino.transport({
 pino(transport)
 ```
 
+If we're using custom levels, they should be passed in when using more than one transport.
+```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' }
+  ],
+  levels: { foo: 35 }
+})
+pino(transport)
+```
+
 For more details on `pino.transport` see the [API docs for `pino.transport`][pino-transport].
 
 [pino-transport]: /docs/api.md#pino-transport
diff --git a/docs/web.md b/docs/web.md
index 29ecf74..080350d 100644
--- a/docs/web.md
+++ b/docs/web.md
@@ -11,6 +11,7 @@ web framework ecosystem.
   - [Pino with Koa](#pino-with-koa)
   - [Pino with Node core `http`](#pino-with-node-core-http)
   - [Pino with Nest](#pino-with-nest)
+  - [Pino with H3](#pino-with-h3)
 
 <a id="fastify"></a>
 ## Pino with Fastify
@@ -227,3 +228,30 @@ bootstrap()
 ```
 
 See the [nestjs-pino readme](https://npm.im/nestjs-pino) for more info.
+
+
+<a id="h3"></a>
+## Pino with H3
+
+```sh
+npm install pino-http
+```
+
+```js
+import { createServer } from 'http'
+import { createApp } from 'h3'
+import pino from 'pino-http'
+
+const app = createApp()
+
+app.use(pino())
+
+app.use('/', (req) => {
+  req.log.info('something')
+  return 'hello world'
+})
+
+createServer(app).listen(process.env.PORT || 3000)
+```
+
+See the [pino-http readme](https://npm.im/pino-http) for more info.
diff --git a/lib/tools.js b/lib/tools.js
index 91df051..3dd5e18 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -108,7 +108,6 @@ function asJson (obj, msg, num, time) {
   data = data + chindings
 
   let value
-  const notHasOwnProperty = obj.hasOwnProperty === undefined
   if (formatters.log) {
     obj = formatters.log(obj)
   }
@@ -116,7 +115,7 @@ function asJson (obj, msg, num, time) {
   let propStr = ''
   for (const key in obj) {
     value = obj[key]
-    if ((notHasOwnProperty || obj.hasOwnProperty(key)) && value !== undefined) {
+    if (Object.prototype.hasOwnProperty.call(obj, key) && value !== undefined) {
       value = serializers[key] ? serializers[key](value) : value
 
       const stringifier = stringifiers[key] || wildcardStringifier
@@ -291,7 +290,7 @@ function prettifierMetaWrapper (pretty, dest, opts) {
       const formattedObj = formatters.log ? formatters.log(lastObj) : lastObj
 
       const messageKey = lastLogger[messageKeySym]
-      if (lastMsg && formattedObj && !formattedObj.hasOwnProperty(messageKey)) {
+      if (lastMsg && formattedObj && !Object.prototype.hasOwnProperty.call(formattedObj, messageKey)) {
         formattedObj[messageKey] = lastMsg
       }
 
@@ -412,7 +411,12 @@ function createArgsNormalizer (defaultOptions) {
       if (opts.transport.targets && opts.transport.targets.length && opts.formatters && typeof opts.formatters.level === 'function') {
         throw Error('option.transport.targets do not allow custom level formatters')
       }
-      stream = transport({ caller, ...opts.transport })
+
+      let customLevels
+      if (opts.customLevels) {
+        customLevels = opts.useOnlyCustomLevels ? opts.customLevels : Object.assign({}, opts.levels, opts.customLevels)
+      }
+      stream = transport({ caller, ...opts.transport, levels: customLevels })
     }
     opts = Object.assign({}, defaultOptions, opts)
     opts.serializers = Object.assign({}, defaultOptions.serializers, opts.serializers)
diff --git a/lib/transport.js b/lib/transport.js
index 8ec4b7a..d818bc4 100644
--- a/lib/transport.js
+++ b/lib/transport.js
@@ -3,6 +3,7 @@
 const { createRequire } = require('module')
 const getCallers = require('./caller')
 const { join, isAbsolute } = require('path')
+const sleep = require('atomic-sleep')
 
 let onExit
 
@@ -63,6 +64,11 @@ function buildStream (filename, workerData, workerOpts) {
       return
     }
     stream.flushSync()
+    // Apparently there is a very sporadic race condition
+    // that in certain OS would prevent the messages to be flushed
+    // because the thread might not have been created still.
+    // Unfortunately we need to sleep(100) in this case.
+    sleep(100)
     stream.end()
   }
 
@@ -79,7 +85,7 @@ function autoEnd (stream) {
 }
 
 function transport (fullOptions) {
-  const { pipeline, targets, options = {}, worker = {}, caller = getCallers() } = fullOptions
+  const { pipeline, targets, levels, options = {}, worker = {}, caller = getCallers() } = fullOptions
 
   // Backwards compatibility
   const callers = typeof caller === 'string' ? [caller] : caller
@@ -101,7 +107,7 @@ function transport (fullOptions) {
         target: fixTarget(dest.target)
       }
     })
-  } else if (fullOptions.pipeline) {
+  } else if (pipeline) {
     target = bundlerOverrides['pino-pipeline-worker'] || join(__dirname, 'worker-pipeline.js')
     options.targets = pipeline.map((dest) => {
       return {
@@ -111,6 +117,10 @@ function transport (fullOptions) {
     })
   }
 
+  if (levels) {
+    options.levels = levels
+  }
+
   return buildStream(fixTarget(target), options, worker)
 
   function fixTarget (origin) {
diff --git a/lib/worker.js b/lib/worker.js
index 9bfabcf..917bffe 100644
--- a/lib/worker.js
+++ b/lib/worker.js
@@ -9,7 +9,7 @@ const { realImport, realRequire } = require('real-require')
 
 /* istanbul ignore file */
 
-module.exports = async function ({ targets }) {
+module.exports = async function ({ targets, levels }) {
   targets = await Promise.all(targets.map(async (t) => {
     let fn
     try {
@@ -49,7 +49,7 @@ module.exports = async function ({ targets }) {
   })
 
   function process (stream) {
-    const multi = pino.multistream(targets)
+    const multi = pino.multistream(targets, { levels })
     // TODO manage backpressure
     stream.on('data', function (chunk) {
       const { lastTime, lastMsg, lastObj, lastLevel } = this
diff --git a/package.json b/package.json
index a799255..f2d6cc7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pino",
-  "version": "7.9.1",
+  "version": "7.9.2",
   "description": "super fast, all natural json logger",
   "main": "pino.js",
   "type": "commonjs",
@@ -23,12 +23,12 @@
     "docs": "docsify serve",
     "browser-test": "airtap --local 8080 test/browser*test.js",
     "lint": "eslint .",
-    "test": "npm run lint && tap test/*test.js test/*/*test.js && jest test/jest && 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-ci-pnpm": "pnpm run lint && tap --no-coverage --no-check-coverage test/*test.js test/*/*test.js && pnpm run test-types",
-    "test-ci-yarn-pnp": "yarn run lint && tap --no-check-coverage test/*test.js test/*/*test.js --coverage-report=lcovonly",
+    "test": "npm run lint && tap && jest test/jest && npm run test-types",
+    "test-ci": "npm run lint && tap --no-check-coverage --coverage-report=lcovonly && npm run test-types",
+    "test-ci-pnpm": "pnpm run lint && tap --no-coverage --no-check-coverage && pnpm run test-types",
+    "test-ci-yarn-pnp": "yarn run lint && tap --no-check-coverage --coverage-report=lcovonly",
     "test-types": "tsc && tsd && ts-node test/types/pino.ts",
-    "cov-ui": "tap --coverage-report=html test/*test.js test/*/*test.js",
+    "cov-ui": "tap --coverage-report=html",
     "bench": "node benchmarks/utils/runbench all",
     "bench-basic": "node benchmarks/utils/runbench basic",
     "bench-object": "node benchmarks/utils/runbench object",
@@ -89,6 +89,7 @@
     "pre-commit": "^1.2.2",
     "proxyquire": "^2.1.3",
     "pump": "^3.0.0",
+    "rimraf": "^3.0.2",
     "semver": "^7.0.0",
     "split2": "^4.0.0",
     "steed": "^1.1.3",
@@ -97,21 +98,22 @@
     "tape": "^5.0.0",
     "through2": "^4.0.0",
     "ts-node": "^10.3.0",
-    "tsd": "^0.19.0",
+    "tsd": "^0.20.0",
     "typescript": "^4.4.4",
     "winston": "^3.3.3"
   },
   "dependencies": {
+    "atomic-sleep": "^1.0.0",
     "fast-redact": "^3.0.0",
-    "process-warning": "^1.0.0",
     "on-exit-leak-free": "^0.2.0",
     "pino-abstract-transport": "v0.5.0",
     "pino-std-serializers": "^4.0.0",
+    "process-warning": "^1.0.0",
     "quick-format-unescaped": "^4.0.3",
     "real-require": "^0.1.0",
     "safe-stable-stringify": "^2.1.0",
     "sonic-boom": "^2.2.1",
-    "thread-stream": "^0.15.0"
+    "thread-stream": "^0.15.1"
   },
   "tsd": {
     "directory": "test/types"
diff --git a/pino.d.ts b/pino.d.ts
index 2f2431b..20fbd8f 100644
--- a/pino.d.ts
+++ b/pino.d.ts
@@ -104,6 +104,13 @@ interface LoggerExtras<Options = LoggerOptions> extends EventEmitter {
      */
     bindings(): pino.Bindings;
 
+    /**
+     * Overwrites the bindings of this logger instance.
+     *
+     * @param bindings: an object of key-value pairs to include in log lines as properties.
+     */
+    setBindings(bindings: pino.Bindings): void;
+
     /**
      * Flushes the content of the buffer when using pino.destination({ sync: false }).
      */
@@ -225,7 +232,7 @@ declare namespace pino {
     interface TransportTargetOptions<TransportOptions = Record<string, any>> {
         target: string
         options: TransportOptions
-        level: LevelWithSilent
+        level: LevelWithSilent | string
     }
 
     interface TransportBaseOptions<TransportOptions = Record<string, any>> {
@@ -242,7 +249,8 @@ declare namespace pino {
     }
 
     interface TransportMultiOptions<TransportOptions = Record<string, any>> extends TransportBaseOptions<TransportOptions>{
-        targets: readonly TransportTargetOptions<TransportOptions>[]
+        targets: readonly TransportTargetOptions<TransportOptions>[],
+        levels?: Record<string, number>
     }
 
     interface MultiStreamOptions {
@@ -781,6 +789,7 @@ export const version: typeof pino.version;
 // Types
 export type Bindings = pino.Bindings;
 export type Level = pino.Level;
+export type LevelWithSilent = pino.LevelWithSilent;
 export type LevelChangeEventListener = pino.LevelChangeEventListener;
 export type LogDescriptor = pino.LogDescriptor;
 export type Logger<Options = LoggerOptions> = pino.Logger<Options>;
diff --git a/pino.js b/pino.js
index 2afe6d2..33bdde0 100644
--- a/pino.js
+++ b/pino.js
@@ -8,7 +8,7 @@ const time = require('./lib/time')
 const proto = require('./lib/proto')
 const symbols = require('./lib/symbols')
 const { configure } = require('safe-stable-stringify')
-const { assertDefaultLevelFound, mappings, genLsCache } = require('./lib/levels')
+const { assertDefaultLevelFound, mappings, genLsCache, levels } = require('./lib/levels')
 const {
   createArgsNormalizer,
   asChindings,
@@ -48,6 +48,7 @@ const hostname = os.hostname()
 const defaultErrorSerializer = stdSerializers.err
 const defaultOptions = {
   level: 'info',
+  levels,
   messageKey: 'msg',
   nestedKey: null,
   enabled: true,


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index 6995431..5a46beb 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -1,9 +1,8 @@
 'use strict'
 const os = require('os')
-const { join } = require('path')
 const { readFileSync } = require('fs')
 const { test } = require('tap')
-const { sink, check, once, watchFileCreated } = require('./helper')
+const { sink, check, once, watchFileCreated, file } = require('./helper')
 const pino = require('../')
 const { version } = require('../package.json')
 const { pid } = process
@@ -511,10 +510,7 @@ test('handles objects with null prototype', async ({ same }) => {
 })
 
 test('pino.destination', async ({ same }) => {
-  const tmp = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const tmp = file()
   const instance = pino(pino.destination(tmp))
   instance.info('hello')
   await watchFileCreated(tmp)
@@ -529,10 +525,7 @@ test('pino.destination', async ({ same }) => {
 })
 
 test('auto pino.destination with a string', async ({ same }) => {
-  const tmp = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const tmp = file()
   const instance = pino(tmp)
   instance.info('hello')
   await watchFileCreated(tmp)
@@ -547,10 +540,7 @@ test('auto pino.destination with a string', async ({ same }) => {
 })
 
 test('auto pino.destination with a string as second argument', async ({ same }) => {
-  const tmp = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const tmp = file()
   const instance = pino(null, tmp)
   instance.info('hello')
   await watchFileCreated(tmp)
@@ -565,10 +555,7 @@ test('auto pino.destination with a string as second argument', async ({ same })
 })
 
 test('does not override opts with a string as second argument', async ({ same }) => {
-  const tmp = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const tmp = file()
   const instance = pino({
     timestamp: () => ',"time":"none"'
   }, tmp)
diff --git a/test/esm/named-exports.mjs b/test/esm/named-exports.mjs
index d7a00d2..326d34f 100644
--- a/test/esm/named-exports.mjs
+++ b/test/esm/named-exports.mjs
@@ -1,6 +1,6 @@
 import { tmpdir, hostname } from 'os'
 import t from 'tap'
-import { sink, check, once, watchFileCreated } from '../helper.js'
+import { sink, check, once, watchFileCreated, file } from '../helper.js'
 import { pino, destination } from '../../pino.js'
 import { join } from 'path'
 import { readFileSync } from 'fs'
@@ -13,10 +13,7 @@ t.test('named exports support', async ({ equal }) => {
 })
 
 t.test('destination', async ({ same }) => {
-  const tmp = join(
-    tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const tmp = file()
   const instance = pino(destination(tmp))
   instance.info('hello')
   await watchFileCreated(tmp)
diff --git a/test/fixtures/pretty/null-prototype.js b/test/fixtures/pretty/null-prototype.js
new file mode 100644
index 0000000..a74ca2c
--- /dev/null
+++ b/test/fixtures/pretty/null-prototype.js
@@ -0,0 +1,8 @@
+global.process = { __proto__: process, pid: 123456 }
+Date.now = function () { return 1459875739796 }
+require('os').hostname = function () { return 'abcdefghijklmnopqr' }
+const pino = require(require.resolve('./../../../'))
+const log = pino({ prettyPrint: true })
+const obj = Object.create(null)
+Object.assign(obj, { foo: 'bar' })
+log.info(obj, 'hello')
diff --git a/test/formatters.test.js b/test/formatters.test.js
index 75d05cd..39a0013 100644
--- a/test/formatters.test.js
+++ b/test/formatters.test.js
@@ -1,12 +1,11 @@
 'use strict'
 /* eslint no-prototype-builtins: 0 */
 
-const os = require('os')
 const { hostname } = require('os')
 const { join } = require('path')
 const { readFile } = require('fs').promises
 const { test } = require('tap')
-const { sink, once, watchFileCreated } = require('./helper')
+const { sink, once, watchFileCreated, file } = require('./helper')
 const pino = require('../')
 
 test('level formatter', async ({ match }) => {
@@ -304,10 +303,7 @@ test('elastic common schema format', async ({ match, type }) => {
 })
 
 test('formatter with transport', async ({ match, equal }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const logger = pino({
     formatters: {
       log (obj) {
@@ -337,11 +333,6 @@ test('formatter with transport', async ({ match, equal }) => {
 })
 
 test('throws when custom level formatter is used with transport.targets', async ({ throws }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
-
   throws(() => {
     pino({
       formatters: {
@@ -353,7 +344,7 @@ test('throws when custom level formatter is used with transport.targets', async
         targets: [
           {
             target: 'pino/file',
-            options: { destination }
+            options: { destination: 'foo.log' }
           }
         ]
       }
diff --git a/test/helper.js b/test/helper.js
index 3d2019b..2aca1aa 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -1,11 +1,15 @@
 'use strict'
 
+const crypto = require('crypto')
 const os = require('os')
 const writer = require('flush-write-stream')
 const split = require('split2')
-const { existsSync, readFileSync, statSync } = require('fs')
+const { existsSync, readFileSync, statSync, unlinkSync } = require('fs')
 const pid = process.pid
 const hostname = os.hostname()
+const t = require('tap')
+const { join } = require('path')
+const { tmpdir } = os
 
 const isWin = process.platform === 'win32'
 const isYarnPnp = process.versions.pnp !== undefined
@@ -97,4 +101,28 @@ function watchForWrite (filename, testString) {
   })
 }
 
-module.exports = { getPathToNull, sink, check, once, sleep, watchFileCreated, watchForWrite, isWin, isYarnPnp }
+let files = []
+
+function file () {
+  const hash = crypto.randomBytes(12).toString('hex')
+  const file = join(tmpdir(), `pino-${pid}-${hash}`)
+  files.push(file)
+  return file
+}
+
+process.on('beforeExit', () => {
+  if (files.length === 0) return
+  t.comment('unlink files')
+  for (const file of files) {
+    try {
+      t.comment(`unliking ${file}`)
+      unlinkSync(file)
+    } catch (e) {
+      console.log(e)
+    }
+  }
+  files = []
+  t.comment('unlink completed')
+})
+
+module.exports = { getPathToNull, sink, check, once, sleep, watchFileCreated, watchForWrite, isWin, isYarnPnp, file }
diff --git a/test/multistream.test.js b/test/multistream.test.js
index ff406e4..0957fff 100644
--- a/test/multistream.test.js
+++ b/test/multistream.test.js
@@ -1,14 +1,13 @@
 '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
 const proxyquire = require('proxyquire')
 const strip = require('strip-ansi')
+const { file } = require('./helper')
 
 test('sends to multiple streams using string levels', function (t) {
   let messageCount = 0
@@ -540,10 +539,7 @@ test('multistream throws if not a stream', function (t) {
 })
 
 test('flushSync', function (t) {
-  const tmp = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const tmp = file()
   const destination = pino.destination({ dest: tmp, sync: false, minLength: 4096 })
   const log = pino({ level: 'info' }, multistream([{ level: 'info', stream: destination }]))
   destination.on('ready', () => {
diff --git a/test/pretty.test.js b/test/pretty.test.js
index 3fe30af..43e25a2 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -341,6 +341,18 @@ test('works as expected with an object with the msg prop', async ({ not }) => {
   not(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): hello/), null)
 })
 
+test('handles objects with null prototypes', async ({ not }) => {
+  let actual = ''
+  const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'pretty', 'null-prototype.js')])
+
+  child.stdout.pipe(writer((s, enc, cb) => {
+    actual += s
+    cb()
+  }))
+  await once(child, 'close')
+  not(strip(actual).match(/\(123456 on abcdefghijklmnopqr\): hello\s+foo: "bar"/), null)
+})
+
 test('should not lose stream metadata for streams with `needsMetadataGsym` flag', async ({ not }) => {
   const dest = new Writable({
     objectMode: true,
diff --git a/test/transport/big.test.js b/test/transport/big.test.js
index 762c594..aac2a24 100644
--- a/test/transport/big.test.js
+++ b/test/transport/big.test.js
@@ -4,21 +4,18 @@ const { test } = require('tap')
 const { join } = require('path')
 const { once } = require('events')
 const { createReadStream } = require('fs')
-const os = require('os')
 const { promisify } = require('util')
 const execa = require('execa')
 const split = require('split2')
 const stream = require('stream')
+const { file } = require('../helper')
 
 const pipeline = promisify(stream.pipeline)
 const { Writable } = stream
 const sleep = promisify(setTimeout)
 
 test('eight million lines', async ({ equal, comment }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-many-lines.js'), destination])
 
   await once(child, 'exit')
diff --git a/test/transport/bundlers-support.test.js b/test/transport/bundlers-support.test.js
index 0e856ea..364dc0a 100644
--- a/test/transport/bundlers-support.test.js
+++ b/test/transport/bundlers-support.test.js
@@ -3,7 +3,7 @@
 const os = require('os')
 const { join } = require('path')
 const { readFile } = require('fs').promises
-const { watchFileCreated } = require('../helper')
+const { watchFileCreated, file } = require('../helper')
 const { test } = require('tap')
 const pino = require('../../pino')
 
@@ -15,10 +15,7 @@ test('pino.transport with destination overriden by bundler', async ({ same, tear
     foobar: join(__dirname, '..', 'fixtures', 'to-file-transport.js')
   }
 
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: 'foobar',
     options: { destination }
@@ -44,10 +41,7 @@ test('pino.transport with worker destination overriden by bundler', async ({ sam
     'pino-worker': join(__dirname, '..', '..', 'lib/worker.js')
   }
 
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     targets: [
       {
@@ -77,10 +71,7 @@ test('pino.transport with worker-pipeline destination overriden by bundler', asy
     'pino-pipeline-worker': join(__dirname, '..', '..', 'lib/worker-pipeline.js')
   }
 
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     pipeline: [
       {
diff --git a/test/transport/core.test.js b/test/transport/core.test.js
index fdda91b..c53eac4 100644
--- a/test/transport/core.test.js
+++ b/test/transport/core.test.js
@@ -4,22 +4,21 @@ const os = require('os')
 const { join } = require('path')
 const { once } = require('events')
 const { readFile, writeFile } = require('fs').promises
-const { watchFileCreated, watchForWrite } = require('../helper')
+const { watchFileCreated, watchForWrite, file } = require('../helper')
 const { test } = require('tap')
 const pino = require('../../')
 const url = require('url')
 const strip = require('strip-ansi')
 const execa = require('execa')
 const writer = require('flush-write-stream')
+const rimraf = require('rimraf')
+const { tmpdir } = os
 
-const { pid } = process
+const pid = process.pid
 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 destination = file()
   const transport = pino.transport({
     target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
     options: { destination }
@@ -47,10 +46,7 @@ test('pino.transport with file (no options + error handling)', async ({ equal })
 })
 
 test('pino.transport with file URL', async ({ same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: url.pathToFileURL(join(__dirname, '..', 'fixtures', 'to-file-transport.js')).href,
     options: { destination }
@@ -85,10 +81,7 @@ test('pino.transport errors if file does not exists', ({ plan, pass }) => {
 })
 
 test('pino.transport with esm', async ({ same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: join(__dirname, '..', 'fixtures', 'to-file-transport.mjs'),
     options: { destination }
@@ -108,14 +101,8 @@ test('pino.transport with esm', async ({ same, teardown }) => {
 })
 
 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 dest1 = file()
+  const dest2 = file()
   const transport = pino.transport({
     targets: [{
       level: 'info',
@@ -149,15 +136,46 @@ test('pino.transport with two files', async ({ same, teardown }) => {
   })
 })
 
+test('pino.transport with two files and custom levels', async ({ same, teardown }) => {
+  const dest1 = file()
+  const dest2 = file()
+  const transport = pino.transport({
+    targets: [{
+      level: 'info',
+      target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
+      options: { destination: dest1 }
+    }, {
+      level: 'foo',
+      target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
+      options: { destination: dest2 }
+    }],
+    levels: { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, foo: 25 }
+  })
+  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 pino-pretty 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 dest1 = file()
+  const dest2 = file()
   const transport = pino.transport({
     targets: [{
       level: 'info',
@@ -190,10 +208,7 @@ test('pino.transport with an array including a pino-pretty destination', async (
 })
 
 test('no transport.end()', async ({ same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
     options: { destination }
@@ -212,10 +227,7 @@ test('no transport.end()', async ({ same, teardown }) => {
 })
 
 test('autoEnd = false', async ({ equal, same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const count = process.listenerCount('exit')
   const transport = pino.transport({
     target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
@@ -257,10 +269,7 @@ test('pino.transport with target and targets', async ({ fail, equal }) => {
 })
 
 test('pino.transport with target pino/file', async ({ same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: 'pino/file',
     options: { destination }
@@ -280,8 +289,15 @@ test('pino.transport with target pino/file', async ({ same, teardown }) => {
 })
 
 test('pino.transport with target pino/file and mkdir option', async ({ same, teardown }) => {
-  const folder = '_' + Math.random().toString(36).substr(2, 9)
-  const destination = join(os.tmpdir(), folder, folder)
+  const folder = join(tmpdir(), `pino-${process.pid}-mkdir-transport-file`)
+  const destination = join(folder, 'log.txt')
+  teardown(() => {
+    try {
+      rimraf.sync(folder)
+    } catch (err) {
+      // ignore
+    }
+  })
   const transport = pino.transport({
     target: 'pino/file',
     options: { destination, mkdir: true }
@@ -301,10 +317,7 @@ test('pino.transport with target pino/file and mkdir option', async ({ same, tea
 })
 
 test('pino.transport with target pino/file and append option', async ({ same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   await writeFile(destination, JSON.stringify({ pid, hostname, time: Date.now(), level: 30, msg: 'hello' }))
   const transport = pino.transport({
     target: 'pino/file',
@@ -337,10 +350,7 @@ test('pino.transport should error with unknown target', async ({ fail, equal })
 })
 
 test('pino.transport with target pino-pretty', async ({ match, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: 'pino-pretty',
     options: { destination }
@@ -390,10 +400,7 @@ test('log and exit before ready', async ({ not }) => {
 })
 
 test('log and exit before ready with async dest', async ({ not }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-exit-immediately-with-async-dest.js'), destination])
 
   await once(child, 'exit')
@@ -416,10 +423,7 @@ test('string integer destination', async ({ not }) => {
 })
 
 test('pino transport options with target', async ({ teardown, same }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const instance = pino({
     transport: {
       target: 'pino/file',
@@ -441,14 +445,8 @@ test('pino transport options with target', async ({ teardown, same }) => {
 })
 
 test('pino transport options with targets', async ({ teardown, same }) => {
-  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 dest1 = file()
+  const dest2 = file()
   const instance = pino({
     transport: {
       targets: [
@@ -509,10 +507,7 @@ test('transport options with target and stream', async ({ fail, equal }) => {
 
 test('transport options with stream', async ({ fail, equal, teardown }) => {
   try {
-    const dest1 = join(
-      os.tmpdir(),
-      '_' + Math.random().toString(36).substr(2, 9)
-    )
+    const dest1 = file()
     const transportStream = pino.transport({ target: 'pino/file', options: { destination: dest1 } })
     teardown(transportStream.end.bind(transportStream))
     pino({
diff --git a/test/transport/module-link.test.js b/test/transport/module-link.test.js
index cfa9cc4..16a17fc 100644
--- a/test/transport/module-link.test.js
+++ b/test/transport/module-link.test.js
@@ -4,10 +4,11 @@ const os = require('os')
 const { join } = require('path')
 const { readFile, symlink, unlink, mkdir, writeFile } = require('fs').promises
 const { test } = require('tap')
-const { isWin, isYarnPnp, watchFileCreated } = require('../helper')
+const { isWin, isYarnPnp, watchFileCreated, file } = require('../helper')
 const { once } = require('events')
 const execa = require('execa')
 const pino = require('../../')
+const rimraf = require('rimraf')
 
 const { pid } = process
 const hostname = os.hostname()
@@ -39,10 +40,7 @@ async function uninstallTransportModule () {
 
 // 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)
-  )
+  const destination = file()
 
   await installTransportModule()
 
@@ -70,10 +68,7 @@ test('pino.transport with package', { skip: isWin }, async ({ same, teardown })
 
 // 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)
-  )
+  const destination = file()
 
   await installTransportModule()
 
@@ -107,6 +102,10 @@ test('pino({ transport })', { skip: isWin || isYarnPnp }, async ({ same, teardow
     '_' + Math.random().toString(36).substr(2, 9)
   )
 
+  teardown(() => {
+    rimraf.sync(folder)
+  })
+
   const destination = join(folder, 'output')
 
   await mkdir(join(folder, 'node_modules'), { recursive: true })
@@ -149,7 +148,7 @@ test('pino({ transport })', { skip: isWin || isYarnPnp }, async ({ same, teardow
 })
 
 // TODO make this test pass on Windows
-test('pino({ transport }) from a wrapped dependency', { skip: isWin || isYarnPnp, only: true }, async ({ same, teardown }) => {
+test('pino({ transport }) from a wrapped dependency', { skip: isWin || isYarnPnp }, async ({ same, teardown }) => {
   const folder = join(
     os.tmpdir(),
     '_' + Math.random().toString(36).substr(2, 9)
@@ -165,6 +164,11 @@ test('pino({ transport }) from a wrapped dependency', { skip: isWin || isYarnPnp
   await mkdir(join(folder, 'node_modules'), { recursive: true })
   await mkdir(join(wrappedFolder, 'node_modules'), { recursive: true })
 
+  teardown(() => {
+    rimraf.sync(wrappedFolder)
+    rimraf.sync(folder)
+  })
+
   // Link pino
   await symlink(
     join(__dirname, '..', '..'),
diff --git a/test/transport/pipeline.test.js b/test/transport/pipeline.test.js
index c69b75d..25b244e 100644
--- a/test/transport/pipeline.test.js
+++ b/test/transport/pipeline.test.js
@@ -3,7 +3,7 @@
 const os = require('os')
 const { join } = require('path')
 const { readFile } = require('fs').promises
-const { watchFileCreated } = require('../helper')
+const { watchFileCreated, file } = require('../helper')
 const { test } = require('tap')
 const pino = require('../../')
 
@@ -11,10 +11,7 @@ const { pid } = process
 const hostname = os.hostname()
 
 test('pino.transport with a pipeline', async ({ same, teardown }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     pipeline: [{
       target: join(__dirname, '..', 'fixtures', 'transport-transform.js')
diff --git a/test/transport/syncfalse.test.js b/test/transport/syncfalse.test.js
index 605d283..42ada5a 100644
--- a/test/transport/syncfalse.test.js
+++ b/test/transport/syncfalse.test.js
@@ -5,16 +5,13 @@ const pino = require('../..')
 const { join } = require('path')
 const { test } = require('tap')
 const { readFile } = require('fs').promises
-const { watchFileCreated } = require('../helper')
+const { watchFileCreated, file } = require('../helper')
 
 const { pid } = process
 const hostname = os.hostname()
 
 test('thread-stream async flush', async ({ same }) => {
-  const destination = join(
-    os.tmpdir(),
-    '_' + Math.random().toString(36).substr(2, 9)
-  )
+  const destination = file()
   const transport = pino.transport({
     target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
     options: { destination }
diff --git a/test/types/pino-transport.test-d.ts b/test/types/pino-transport.test-d.ts
index 242d159..60f452f 100644
--- a/test/types/pino-transport.test-d.ts
+++ b/test/types/pino-transport.test-d.ts
@@ -44,6 +44,35 @@ expectType<pino.Logger>(pino({
         ]},
 }))
 
+const transportsWithCustomLevels = pino.transport({targets: [
+    {
+        level: 'info',
+        target: '#pino/pretty',
+        options: { some: 'options for', the: 'transport' }
+    },
+    {
+        level: 'foo',
+        target: '#pino/file',
+        options: { destination: './test.log' }
+    }
+], levels: { foo: 35 }})
+pino(transports)
+
+expectType<pino.Logger>(pino({
+    transport: {targets: [
+            {
+                level: 'info',
+                target: '#pino/pretty',
+                options: { some: 'options for', the: 'transport' }
+            },
+            {
+                level: 'trace',
+                target: '#pino/file',
+                options: { destination: './test.log' }
+            }
+        ], levels: { foo: 35 }
+    },
+}))
 
 const pipelineTransport = pino.transport({
     pipeline: [{
diff --git a/test/types/pino-type-only.test-d.ts b/test/types/pino-type-only.test-d.ts
index 89deebb..54ea04b 100644
--- a/test/types/pino-type-only.test-d.ts
+++ b/test/types/pino-type-only.test-d.ts
@@ -1,7 +1,7 @@
-import { expectType } from "tsd";
+import { expectAssignable, expectType } from "tsd";
 
 import pino from "../../";
-import type {Logger, LogFn, P} from "../../pino";
+import type {LevelWithSilent, Logger, LogFn, P} from "../../pino";
 
 // NB: can also use `import * as pino`, but that form is callable as `pino()`
 // under `esModuleInterop: false` or `pino.default()` under `esModuleInterop: true`.
@@ -10,4 +10,7 @@ expectType<Logger>(log);
 expectType<LogFn>(log.info);
 
 expectType<P.Logger>(log);
-expectType<P.LogFn>(log.info);
\ No newline at end of file
+expectType<P.LogFn>(log.info);
+
+const level: LevelWithSilent = 'silent';
+expectAssignable<P.LevelWithSilent>(level);
diff --git a/test/types/pino.ts b/test/types/pino.ts
index 9065bee..5ac2d93 100644
--- a/test/types/pino.ts
+++ b/test/types/pino.ts
@@ -13,6 +13,7 @@ const transport = pino.transport({
     options: { some: 'options for', the: 'transport' }
 })
 const logger = pino(transport)
+logger.setBindings({ some: 'bindings' })
 logger.info('test2')
 logger.flush()