Repository: pinojs/pino
Pull request #449 (2018-07-13 22:41:22)

v5: custom levels
* Adds `customLevels` config property as per #446 
* `customLevels` also supported in child bindings
* remove `levelVal` from config object (it's only purpose was for adding a customLevel)
* enforces levelVal property as read only (will throw on attempt to write)
* Docs to be added separately (as part of #447)

### API

```js
const logger = pino({
  customLevels: {
    foo: 35,
    bar: 45,
  }
})
logger.foo('hi')
const childLogger = logger.child({})
childLogger.foo('hi')
```

```js
const logger = pino()

const childLogger = logger.child({
  customLevels: {
    foo: 35,
    bar: 45,
  }
})

childLogger.foo('hi')
```



### Cost of customLevels

#### Outcome: Zero overhead.

```sh
$ node benchmarks/internal/custom-levels.js
```

```
benchPinoNoCustomLevel*10000: 691.204ms
benchPinoCustomLevel*10000: 693.940ms
benchChildNoCustomLevel*10000: 684.271ms
benchPinoChildCustomLevel*10000: 687.863ms
benchPinoChildInheritedCustomLevel*10000: 686.649ms
```

### Child Logging Bench Comparison

#### Outcome: Zero overhead.

#### next-major

```
benchPinoChild*10000: 500.976ms
benchPinoChild*10000: 439.672ms
```

#### custom-levels

```
benchPinoChild*10000: 499.220ms
benchPinoChild*10000: 439.289ms
```


### Child Creation Bench Comparison 

#### Outcome: Zero overhead.

#### next-major

```
benchPinoCreation*10000: 231.800ms
benchPinoCreation*10000: 189.613ms
```

#### custom-levels

```
benchPinoCreation*10000: 220.555ms
benchPinoCreation*10000: 189.862ms
```





Patch:
diff --git a/benchmarks/child-creation.bench.js b/benchmarks/child-creation.bench.js
index a229a0a..a663c57 100644
--- a/benchmarks/child-creation.bench.js
+++ b/benchmarks/child-creation.bench.js
@@ -5,8 +5,7 @@ const pino = require('../')
 const bunyan = require('bunyan')
 const bole = require('bole')('bench')
 const fs = require('fs')
-const dest = fs.createWriteStream('/dev/null')
-const plog = pino(dest)
+const plog = pino(pino.destination(('/dev/null')))
 delete require.cache[require.resolve('../')]
 const plogExtreme = require('../')(pino.extreme('/dev/null'))
 
@@ -15,13 +14,13 @@ const blog = bunyan.createLogger({
   name: 'myapp',
   streams: [{
     level: 'trace',
-    stream: dest
+    stream: fs.createWriteStream('/dev/null')
   }]
 })
 
 require('bole').output({
   level: 'info',
-  stream: dest
+  stream: fs.createWriteStream('/dev/null')
 }).setFastTime(true)
 
 const run = bench([
diff --git a/benchmarks/internal/custom-levels.js b/benchmarks/internal/custom-levels.js
new file mode 100644
index 0000000..16abb2e
--- /dev/null
+++ b/benchmarks/internal/custom-levels.js
@@ -0,0 +1,67 @@
+'use strict'
+
+const bench = require('fastbench')
+const pino = require('../../')
+
+const base = pino(pino.destination('/dev/null'))
+const baseCl = pino({
+  customLevels: {foo: 31}
+}, pino.destination('/dev/null'))
+const child = base.child({})
+const childCl = base.child({
+  customLevels: {foo: 31}
+})
+const childOfBaseCl = baseCl.child({})
+
+const max = 100
+
+const run = bench([
+  function benchPinoNoCustomLevel (cb) {
+    for (var i = 0; i < max; i++) {
+      base.info({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchPinoCustomLevel (cb) {
+    for (var i = 0; i < max; i++) {
+      baseCl.foo({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchChildNoCustomLevel (cb) {
+    for (var i = 0; i < max; i++) {
+      child.info({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchPinoChildCustomLevel (cb) {
+    for (var i = 0; i < max; i++) {
+      childCl.foo({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchPinoChildInheritedCustomLevel (cb) {
+    for (var i = 0; i < max; i++) {
+      childOfBaseCl.foo({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchPinoChildCreation (cb) {
+    const child = base.child({})
+    for (var i = 0; i < max; i++) {
+      child.info({ hello: 'world' })
+    }
+    setImmediate(cb)
+  },
+  function benchPinoChildCreationCustomLevel (cb) {
+    const child = base.child({
+      customLevels: {foo: 31}
+    })
+    for (var i = 0; i < max; i++) {
+      child.foo({ hello: 'world' })
+    }
+    setImmediate(cb)
+  }
+], 10000)
+
+run(run)
diff --git a/benchmarks/internal/parent-vs-child.bench.js b/benchmarks/internal/parent-vs-child.bench.js
index 52bcfba..0ad8e60 100644
--- a/benchmarks/internal/parent-vs-child.bench.js
+++ b/benchmarks/internal/parent-vs-child.bench.js
@@ -9,9 +9,9 @@ const childChild = child.child({})
 const childChildChild = childChild.child({})
 const childChildChildChild = childChildChild.child({})
 const child2 = base.child({})
-const baseSerializers = pino({serializers: {err: () => {}}}, pino.destination('/dev/null'))
+const baseSerializers = pino(pino.destination('/dev/null'))
 const baseSerializersChild = baseSerializers.child({})
-const baseSerializersChildSerializers = baseSerializers.child({serializers: {err: () => {}}})
+const baseSerializersChildSerializers = baseSerializers.child({})
 
 const max = 100
 
diff --git a/lib/levels.js b/lib/levels.js
index 4a4f348..1da3a52 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -1,15 +1,15 @@
 'use strict'
 const flatstr = require('flatstr')
-const { setLevelSym, lsCacheSym, levelValSym } = require('./symbols')
+const { lsCacheSym, levelValSym } = require('./symbols')
 const { noop, genLog } = require('./tools')
 
 const levels = {
-  fatal: 60,
-  error: 50,
-  warn: 40,
-  info: 30,
+  trace: 10,
   debug: 20,
-  trace: 10
+  info: 30,
+  warn: 40,
+  error: 50,
+  fatal: 60
 }
 
 const levelMethods = {
@@ -26,14 +26,20 @@ const nums = Object.keys(levels).reduce((o, k) => {
   return o
 }, {})
 
-// level string cache
-const lsCache = Object.keys(nums).reduce((o, k) => {
-  o[k] = flatstr('{"level":' + Number(k))
+const initialLsCache = Object.keys(nums).reduce((o, k) => {
+  if (!(k in o)) o[k] = flatstr('{"level":' + Number(k))
   return o
 }, {})
 
+function genLsCache (instance) {
+  instance[lsCacheSym] = Object.keys(instance.levels.labels).reduce((o, k) => {
+    if (!(k in o)) o[k] = flatstr('{"level":' + Number(k))
+    return o
+  }, instance[lsCacheSym])
+  return instance
+}
+
 function isStandardLevel (level) {
-  if (level === Infinity) return true
   switch (level) {
     case 'fatal':
     case 'error':
@@ -47,57 +53,31 @@ function isStandardLevel (level) {
   }
 }
 
-function isStandardLevelVal (val) {
-  switch (val) {
-    case Infinity:
-    case 60:
-    case 50:
-    case 40:
-    case 30:
-    case 20:
-    case 10:
-      return true
-    default:
-      return false
-  }
-}
-
-function getLevelVal () {
-  return this[levelValSym]
-}
-
-function setLevelVal (num) {
-  if (typeof num !== 'number') throw Error('levelVal must be a number')
-
+function setLevel (level) {
   const { labels, values } = this.levels
-
-  this.emit(
-    'level-change',
-    labels[num],
-    num,
-    labels[this[levelValSym]],
-    this[levelValSym]
-  )
-
-  this[levelValSym] = num
+  if (typeof level === 'number') {
+    if (labels[level] === undefined) throw Error('unknown level value' + level)
+    level = labels[level]
+  }
+  if (values[level] === undefined) throw Error('unknown level ' + level)
+  const preLevelVal = this[levelValSym]
+  const levelVal = this[levelValSym] = values[level]
 
   for (var key in values) {
-    if (num > values[key]) {
+    if (levelVal > values[key]) {
       this[key] = noop
       continue
     }
     this[key] = isStandardLevel(key) ? levelMethods[key] : genLog(values[key])
   }
-}
 
-function setLevel (level) {
-  const { values } = this.levels
-  if (typeof level === 'number') {
-    this.levelVal = level
-    return
-  }
-  if (values[level] === undefined) throw Error('unknown level ' + level)
-  this.levelVal = values[level]
+  this.emit(
+    'level-change',
+    level,
+    levelVal,
+    labels[preLevelVal],
+    preLevelVal
+  )
 }
 
 function getLevel (level) {
@@ -105,53 +85,49 @@ function getLevel (level) {
   return levels.labels[levelVal]
 }
 
-function addLevel (name, lvl) {
-  const { values, labels } = this.levels
-  if (values.hasOwnProperty(name) === true) return false
-  if (labels.hasOwnProperty(lvl) === true) return false
-  values[name] = lvl
-  labels[lvl] = name
-  this[lsCacheSym][lvl] = flatstr('{"level":' + Number(lvl))
-  this[name] = lvl < this[levelValSym] ? noop : genLog(lvl)
-  return true
-}
-
 function isLevelEnabled (logLevel) {
   const { values } = this.levels
   const logLevelVal = values[logLevel]
   return logLevelVal !== undefined && (logLevelVal >= this[levelValSym])
 }
 
-function mappings () {
+function mappings (customLevels = null) {
+  const customNums = customLevels ? Object.keys(customLevels).reduce((o, k) => {
+    o[customLevels[k]] = k
+    return o
+  }, {}) : null
+
   const labels = Object.assign(
     Object.create(Object.prototype, {Infinity: {value: 'silent'}}),
-    nums
+    nums,
+    customNums
   )
   const values = Object.assign(
     Object.create(Object.prototype, {silent: {value: Infinity}}),
-    levels
+    levels,
+    customLevels
   )
   return { labels, values }
 }
-
-function setLevelState (instance, level, levelVal) {
-  const levelIsStandard = isStandardLevel(level)
-  const valIsStandard = isStandardLevelVal(levelVal)
-  if (valIsStandard === true) throw Error('level value is already used: ' + levelVal)
-  if (levelIsStandard === false && valIsStandard === false) instance.addLevel(level, levelVal)
-  instance[setLevelSym](level)
+function assertNoLevelCollisions (levels, customLevels) {
+  const { labels, values } = levels
+  for (const k in customLevels) {
+    if (k in values) {
+      throw Error('levels cannot be overridden')
+    }
+    if (customLevels[k] in labels) {
+      throw Error('pre-existing level values cannot be used for new levels')
+    }
+  }
 }
 
 module.exports = {
-  setLevelState,
-  isStandardLevelVal,
-  lsCache,
+  assertNoLevelCollisions,
+  initialLsCache,
+  genLsCache,
   levelMethods,
-  getLevelVal,
-  setLevelVal,
   getLevel,
   setLevel,
-  addLevel,
   isLevelEnabled,
   mappings
 }
diff --git a/lib/proto.js b/lib/proto.js
index 7f12d84..ff67245 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -4,8 +4,7 @@ const SonicBoom = require('sonic-boom')
 const flatstr = require('flatstr')
 const {
   lsCacheSym,
-  getLevelValSym,
-  setLevelValSym,
+  levelValSym,
   setLevelSym,
   getLevelSym,
   chindingsSym,
@@ -17,18 +16,15 @@ const {
   needsMetadataGsym
 } = require('./symbols')
 const {
-  lsCache,
-  setLevelVal,
-  getLevelVal,
   getLevel,
   setLevel,
-  addLevel,
-  setLevelState,
   isLevelEnabled,
-  isStandardLevelVal
+  mappings,
+  initialLsCache,
+  genLsCache,
+  assertNoLevelCollisions
 } = require('./levels')
 const {
-  noop,
   asChindings,
   asJson
 } = require('./tools')
@@ -43,19 +39,15 @@ const prototype = {
   constructor,
   child,
   flush,
-  addLevel,
   isLevelEnabled,
   version,
-  silent: noop,
-  get levelVal () { return this[getLevelValSym]() },
-  set levelVal (num) { return this[setLevelValSym](num) },
   get level () { return this[getLevelSym]() },
   set level (lvl) { return this[setLevelSym](lvl) },
+  get levelVal () { return this[levelValSym] },
+  set levelVal (n) { throw Error('levelVal is read-only') },
+  [lsCacheSym]: initialLsCache,
   [writeSym]: write,
   [asJsonSym]: asJson,
-  [lsCacheSym]: Object.assign({}, lsCache),
-  [getLevelValSym]: getLevelVal,
-  [setLevelValSym]: setLevelVal,
   [getLevelSym]: getLevel,
   [setLevelSym]: setLevel,
   LOG_VERSION
@@ -66,7 +58,7 @@ Object.setPrototypeOf(prototype, EventEmitter.prototype)
 module.exports = prototype
 
 function child (bindings) {
-  const { level, levelVal } = this
+  const { level } = this
   const serializers = this[serializersSym]
   const chindings = asChindings(this, bindings)
   const instance = Object.create(this)
@@ -79,10 +71,15 @@ function child (bindings) {
       instance[serializersSym][bk] = bindings.serializers[bk]
     }
   } else instance[serializersSym] = serializers
+  if (bindings.hasOwnProperty('customLevels') === true) {
+    assertNoLevelCollisions(this.levels, bindings.customLevels)
+    instance.levels = mappings(bindings.customLevels)
+    genLsCache(instance)
+  }
   instance[chindingsSym] = chindings
   const childLevel = bindings.level || level
-  if (isStandardLevelVal(levelVal) === true) instance[setLevelSym](childLevel)
-  else setLevelState(instance, childLevel, levelVal)
+  instance[setLevelSym](childLevel)
+
   return instance
 }
 
diff --git a/lib/symbols.js b/lib/symbols.js
index 6d0e66a..e6b78fd 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -2,8 +2,6 @@
 
 const setLevelSym = Symbol('pino.setLevel')
 const getLevelSym = Symbol('pino.getLevel')
-const setLevelValSym = Symbol('pino.setLevelVal')
-const getLevelValSym = Symbol('pino.getLevelVal')
 const levelValSym = Symbol('pino.levelVal')
 
 const lsCacheSym = Symbol('pino.lsCache')
@@ -30,8 +28,6 @@ const needsMetadataGsym = Symbol.for('pino.metadata')
 module.exports = {
   setLevelSym,
   getLevelSym,
-  setLevelValSym,
-  getLevelValSym,
   levelValSym,
   lsCacheSym,
   chindingsSym,
diff --git a/lib/tools.js b/lib/tools.js
index 944e4d0..ba346de 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -121,6 +121,7 @@ function asChindings (instance, bindings) {
     value = bindings[key]
     const valid = key !== 'level' &&
       key !== 'serializers' &&
+      key !== 'customLevels' &&
       bindings.hasOwnProperty(key) &&
       value !== undefined
     if (valid === true) {
diff --git a/pino.js b/pino.js
index ef6997b..2f70eb7 100644
--- a/pino.js
+++ b/pino.js
@@ -8,7 +8,7 @@ const redaction = require('./lib/redaction')
 const time = require('./lib/time')
 const proto = require('./lib/proto')
 const symbols = require('./lib/symbols')
-const { setLevelState, mappings } = require('./lib/levels')
+const { mappings, genLsCache, assertNoLevelCollisions } = require('./lib/levels')
 const { createArgsNormalizer, asChindings } = require('./lib/tools')
 const { version, LOG_VERSION } = require('./lib/meta')
 const {
@@ -19,6 +19,7 @@ const {
   streamSym,
   stringifySym,
   stringifiersSym,
+  setLevelSym,
   endSym,
   formatOptsSym,
   onTerminatedSym,
@@ -39,8 +40,8 @@ const defaultOptions = {
   timestamp: epochTime,
   onTerminated: (evt, err) => err ? exit(1) : exit(0),
   name: undefined,
-  levelVal: undefined,
-  redact: null
+  redact: null,
+  customLevels: null
 }
 
 const normalize = createArgsNormalizer(defaultOptions)
@@ -58,9 +59,11 @@ function pino (...args) {
     base,
     name,
     level,
-    levelVal
+    customLevels
   } = opts
 
+  assertNoLevelCollisions(pino.levels, customLevels)
+
   const stringify = safe ? stringifySafe : JSON.stringify
   const stringifiers = redact ? redaction(redact, stringify) : {}
   const formatOpts = redact
@@ -78,7 +81,9 @@ function pino (...args) {
     ? coreChindings(base) : coreChindings(Object.assign({}, base, { name }))
   const time = (timestamp instanceof Function)
     ? timestamp : (timestamp ? epochTime : nullTime)
-  const levels = mappings()
+
+  const levels = mappings(customLevels)
+
   const instance = {
     levels,
     [streamSym]: stream,
@@ -94,8 +99,11 @@ function pino (...args) {
   }
   Object.setPrototypeOf(instance, proto)
 
+  if (customLevels) genLsCache(instance)
+
   events(instance)
-  setLevelState(instance, level, levelVal)
+
+  instance[setLevelSym](level)
 
   return instance
 }


Test patch:
diff --git a/test/add-level.test.js b/test/add-level.test.js
deleted file mode 100644
index dd707c6..0000000
--- a/test/add-level.test.js
+++ /dev/null
@@ -1,133 +0,0 @@
-'use strict'
-
-const { test } = require('tap')
-const { sink, once } = require('./helper')
-const pino = require('../')
-
-test('can add a custom level via exported pino function', async ({is}) => {
-  const stream = sink()
-  const instance = pino({level: 'foo', levelVal: 35}, stream)
-  is(typeof instance.foo, 'function')
-  instance.foo('bar')
-  const { msg } = await once(stream, 'data')
-  is(msg, 'bar')
-})
-
-test('can add a custom level to a prior instance', async ({is}) => {
-  const stream = sink()
-  const instance = pino(stream)
-  instance.addLevel('foo2', 35)
-  is(typeof instance.foo2, 'function')
-  instance.foo2('bar')
-  const { msg } = await once(stream, 'data')
-  is(msg, 'bar')
-})
-
-test('custom level via exported pino function does not affect other instances', async ({is}) => {
-  const instance = pino({level: 'foo3', levelVal: 36})
-  const other = pino()
-  is(typeof instance.foo3, 'function')
-  is(typeof other.foo3, 'undefined')
-})
-
-test('custom level on one instance does not affect other instances', async ({is}) => {
-  const instance = pino()
-  instance.addLevel('foo4', 37)
-  const other = pino()
-  instance.addLevel('foo5', 38)
-  is(typeof other.foo4, 'undefined')
-  is(typeof other.foo5, 'undefined')
-})
-
-test('custom levels encompass higher levels', async ({is}) => {
-  const stream = sink()
-  const instance = pino({level: 'foo', levelVal: 35}, stream)
-  instance.warn('bar')
-  const { msg } = await once(stream, 'data')
-  is(msg, 'bar')
-})
-
-test('after the fact add level does not include lower levels', async ({is}) => {
-  const stream = sink()
-  const instance = pino(stream)
-  instance.addLevel('foo', 35)
-  instance.level = 'foo'
-  instance.info('nope')
-  instance.foo('bar')
-  const { msg } = await once(stream, 'data')
-  is(msg, 'bar')
-})
-
-test('after the fact add of a lower level does not include it', async ({is}) => {
-  const stream = sink()
-  const instance = pino(stream)
-  instance.level = 'info'
-  instance.addLevel('foo', 15)
-  instance.info('bar')
-  instance.foo('nope')
-  const { msg } = await once(stream, 'data')
-  is(msg, 'bar')
-})
-
-test('children can be set to custom level', async ({is}) => {
-  const stream = sink()
-  const parent = pino({level: 'foo', levelVal: 35}, stream)
-  const child = parent.child({childMsg: 'yes'})
-  child.foo('bar')
-  const { msg, childMsg } = await once(stream, 'data')
-  is(msg, 'bar')
-  is(childMsg, 'yes')
-})
-
-test('custom levels exists on children', async ({is}) => {
-  const stream = sink()
-  const parent = pino({}, stream)
-  parent.addLevel('foo', 35)
-  const child = parent.child({childMsg: 'yes'})
-  child.foo('bar')
-  const { msg, childMsg } = await once(stream, 'data')
-  is(msg, 'bar')
-  is(childMsg, 'yes')
-})
-
-test('rejects already known labels', async ({is}) => {
-  const instance = pino({level: 'info', levelVal: 900})
-  is(instance.levelVal, 30)
-  is(instance.addLevel('error', 200), false)
-  is(instance.levels.values.error, 50)
-  is(200 in instance.levels.labels, false)
-})
-
-test('reject already known values', async ({is}) => {
-  try {
-    pino({level: 'foo', levelVal: 30})
-  } catch (e) {
-    is(e.message.indexOf('level value') > -1, true)
-  }
-  const instance = pino()
-  is(instance.addLevel('foo', 50), false)
-  is(instance.levels.labels[50], 'error')
-  is('foo' in instance.levels.values, false)
-})
-
-test('reject values of Infinity', async ({throws}) => {
-  throws(() => {
-    pino({level: 'foo', levelVal: Infinity})
-  }, /.*level value is already used.*/)
-})
-
-test('level numbers are logged correctly after level change', async ({is}) => {
-  const stream = sink()
-  const instance = pino({level: 'foo', levelVal: 25}, stream)
-  instance.level = 'debug'
-  instance.foo('bar')
-  const { level } = await once(stream, 'data')
-  is(level, 25)
-})
-
-test('levels state is not shared between instances', async ({is}) => {
-  const instance1 = pino({level: 'foo', levelVal: 35})
-  is(typeof instance1.foo, 'function')
-  const instance2 = pino()
-  is(instance2.hasOwnProperty('foo'), false)
-})
diff --git a/test/custom-levels.test.js b/test/custom-levels.test.js
new file mode 100644
index 0000000..1a41f59
--- /dev/null
+++ b/test/custom-levels.test.js
@@ -0,0 +1,234 @@
+'use strict'
+
+const { test } = require('tap')
+const { sink, once } = require('./helper')
+const pino = require('../')
+
+test('adds additional levels', async ({is}) => {
+  const stream = sink()
+  const logger = pino({
+    customLevels: {
+      foo: 35,
+      bar: 45
+    }
+  }, stream)
+
+  logger.foo('test')
+  const { level } = await once(stream, 'data')
+  is(level, 35)
+})
+
+test('throws when specifying existing levels', async ({is, throws}) => {
+  const stream = sink()
+  throws(() => pino({
+    customLevels: {
+      info: 35
+    }
+  }, stream)
+  )
+  try {
+    pino({
+      customLevels: {
+        info: 35
+      }
+    })
+  } catch ({message}) {
+    is(message, 'levels cannot be overridden')
+  }
+})
+
+test('throws when specifying existing values', async ({is, throws}) => {
+  const stream = sink()
+  throws(() => pino({
+    customLevels: {
+      foo: 30
+    }
+  }, stream)
+  )
+  try {
+    pino({
+      customLevels: {
+        foo: 30
+      }
+    })
+  } catch ({message}) {
+    is(message, 'pre-existing level values cannot be used for new levels')
+  }
+})
+
+test('custom levels are inherited by children', async ({is}) => {
+  const stream = sink()
+  const logger = pino({
+    customLevels: {
+      foo: 35
+    }
+  }, stream)
+
+  logger.child({childMsg: 'ok'}).foo('test')
+  const { msg, childMsg, level } = await once(stream, 'data')
+  is(level, 35)
+  is(childMsg, 'ok')
+  is(msg, 'test')
+})
+
+test('custom levels can be specified on child bindings', async ({is}) => {
+  const stream = sink()
+  const logger = pino(stream).child({
+    customLevels: {
+      foo: 35
+    },
+    childMsg: 'ok'
+  })
+
+  logger.foo('test')
+  const { msg, childMsg, level } = await once(stream, 'data')
+  is(level, 35)
+  is(childMsg, 'ok')
+  is(msg, 'test')
+})
+
+test('customLevels property child bindings does not get logged', async ({is}) => {
+  const stream = sink()
+  const logger = pino(stream).child({
+    customLevels: {
+      foo: 35
+    },
+    childMsg: 'ok'
+  })
+
+  logger.foo('test')
+  const { customLevels } = await once(stream, 'data')
+  is(customLevels, undefined)
+})
+
+test('throws when specifying core levels via child bindings', async ({is, throws}) => {
+  const stream = sink()
+  throws(() => pino(stream).child({
+    customLevels: {
+      info: 35
+    }
+  })
+  )
+  try {
+    pino(stream).child({
+      customLevels: {
+        info: 35
+      }
+    })
+  } catch ({message}) {
+    is(message, 'levels cannot be overridden')
+  }
+})
+
+test('throws when specifying core values via child bindings', async ({is, throws}) => {
+  const stream = sink()
+  throws(() => pino(stream).child({
+    customLevels: {
+      foo: 30
+    }
+  })
+  )
+  try {
+    pino(stream).child({
+      customLevels: {
+        foo: 30
+      }
+    })
+  } catch ({message}) {
+    is(message, 'pre-existing level values cannot be used for new levels')
+  }
+})
+
+test('throws when specifying pre-existing parent levels via child bindings', async ({is, throws}) => {
+  const stream = sink()
+  throws(() => pino({
+    customLevels: {
+      foo: 35
+    }
+  }, stream).child({
+    customLevels: {
+      foo: 45
+    }
+  })
+  )
+  try {
+    pino({
+      customLevels: {
+        foo: 35
+      }
+    }, stream).child({
+      customLevels: {
+        foo: 45
+      }
+    })
+  } catch ({message}) {
+    is(message, 'levels cannot be overridden')
+  }
+})
+
+test('throws when specifying pre-existing parent values via child bindings', async ({is, throws}) => {
+  const stream = sink()
+  throws(() => pino({
+    customLevels: {
+      foo: 35
+    }
+  }, stream).child({
+    customLevels: {
+      bar: 35
+    }
+  })
+  )
+  try {
+    pino({
+      customLevels: {
+        foo: 35
+      }
+    }, stream).child({
+      customLevels: {
+        bar: 35
+      }
+    })
+  } catch ({message}) {
+    is(message, 'pre-existing level values cannot be used for new levels')
+  }
+})
+
+test('custom level on one instance does not affect other instances', async ({is}) => {
+  pino({customLevels: {
+    foo: 37
+  }})
+  is(typeof pino().foo, 'undefined')
+})
+
+test('setting level below or at custom level will successfully log', async ({is}) => {
+  const stream = sink()
+  const instance = pino({customLevels: {foo: 35}}, stream)
+  instance.level = 'foo'
+  instance.info('nope')
+  instance.foo('bar')
+  const { msg } = await once(stream, 'data')
+  is(msg, 'bar')
+})
+
+test('custom level below level threshold will not log', async ({is}) => {
+  const stream = sink()
+  const instance = pino({customLevels: {foo: 15}}, stream)
+  instance.level = 'info'
+  instance.info('bar')
+  instance.foo('nope')
+  const { msg } = await once(stream, 'data')
+  is(msg, 'bar')
+})
+
+test('does not share custom level state across siblings', async ({doesNotThrow}) => {
+  const stream = sink()
+  const logger = pino(stream)
+  logger.child({
+    customLevels: {foo: 35}
+  })
+  doesNotThrow(() => {
+    logger.child({
+      customLevels: {foo: 35}
+    })
+  })
+})
diff --git a/test/is-level-enabled.test.js b/test/is-level-enabled.test.js
index 32ca8ce..f27b263 100644
--- a/test/is-level-enabled.test.js
+++ b/test/is-level-enabled.test.js
@@ -33,8 +33,10 @@ test('can check if child has current level enabled', async ({is}) => {
 })
 
 test('can check if custom level is enabled', async ({is}) => {
-  const log = pino({level: 'debug'})
-  log.addLevel('foo', 35)
+  const log = pino({
+    customLevels: {foo: 35},
+    level: 'debug'
+  })
   is(true, log.isLevelEnabled('foo'))
   is(true, log.isLevelEnabled('error'))
   is(false, log.isLevelEnabled('trace'))
diff --git a/test/levels.test.js b/test/levels.test.js
index 6db4b1d..3f5b069 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -1,8 +1,7 @@
 'use strict'
 
 const { test } = require('tap')
-const { sink, once } = require('./helper')
-const { check } = require('./helper')
+const { sink, once, check } = require('./helper')
 const pino = require('../')
 
 test('set the level by string', async ({is}) => {
@@ -42,31 +41,10 @@ test('set the level by number', async ({is}) => {
   const stream = sink()
   const instance = pino(stream)
 
-  instance.levelVal = 50
-  instance.info('hello world')
-  instance.error('this is an error')
-  instance.fatal('this is fatal')
-  const result = await once(stream, 'data')
-  const current = expected.shift()
-  check(is, result, current.level, current.msg)
-})
-
-test('set the level by number via string method', async ({is}) => {
-  const expected = [{
-    level: 50,
-    msg: 'this is an error'
-  }, {
-    level: 60,
-    msg: 'this is fatal'
-  }]
-  const stream = sink()
-  const instance = pino(stream)
-
   instance.level = 50
   instance.info('hello world')
   instance.error('this is an error')
   instance.fatal('this is fatal')
-
   const result = await once(stream, 'data')
   const current = expected.shift()
   check(is, result, current.level, current.msg)
@@ -168,16 +146,6 @@ test('silent level', async ({fail}) => {
   })
 })
 
-test('silent is a noop', async ({fail}) => {
-  const instance = pino({
-    level: 'silent'
-  }, sink((result, enc) => {
-    fail('no data should be logged')
-  }))
-
-  instance['silent']('hello world')
-})
-
 test('set silent via Infinity', async ({fail}) => {
   const instance = pino({
     level: Infinity
@@ -190,30 +158,14 @@ test('set silent via Infinity', async ({fail}) => {
   })
 })
 
-test('silent stays a noop after level changes', async ({is, isNot, fail}) => {
-  const noop = require('../lib/tools').noop
-  const instance = pino({
-    level: 'silent'
-  }, sink((result, enc) => {
-    fail('no data should be logged')
-  }))
-
-  instance.level = 'trace'
-  isNot(instance[instance.level], noop)
-
-  instance.level = 'silent'
-  instance['silent']('hello world')
-  is(instance[instance.level], noop)
-})
-
 test('exposed levels', async ({same}) => {
   same(Object.keys(pino.levels.values), [
-    'fatal',
-    'error',
-    'warn',
-    'info',
+    'trace',
     'debug',
-    'trace'
+    'info',
+    'warn',
+    'error',
+    'fatal'
   ])
 })
 
@@ -248,22 +200,18 @@ test('setting level in child', async ({is}) => {
   instance.fatal('this is fatal')
 })
 
-test('setting level by assigning a number to levelVal', async ({is}) => {
+test('setting level by assigning a number to level', async ({is}) => {
   const instance = pino()
   is(instance.levelVal, 30)
   is(instance.level, 'info')
-  instance.levelVal = 50
+  instance.level = 50
   is(instance.levelVal, 50)
   is(instance.level, 'error')
 })
 
-test('setting level by assigning a number to level', async ({is}) => {
+test('setting level by number to unknown value results in a throw', async ({throws}) => {
   const instance = pino()
-  is(instance.levelVal, 30)
-  is(instance.level, 'info')
-  instance.level = 50
-  is(instance.levelVal, 50)
-  is(instance.level, 'error')
+  throws(() => { instance.level = 973 })
 })
 
 test('setting level by assigning a known label to level', async ({is}) => {
@@ -275,7 +223,7 @@ test('setting level by assigning a known label to level', async ({is}) => {
   is(instance.level, 'error')
 })
 
-test('assigning a string to levelVal will throw', async ({throws}) => {
+test('levelVal is read only', async ({throws}) => {
   const instance = pino()
-  throws(() => { instance.levelVal = 'error' })
+  throws(() => { instance.levelVal = 20 })
 })