Repository: pinojs/pino
Pull request #515 (2018-09-07 15:01:31)

Allow custom levels to override default and allow to remove default levels
Solves:

- Allows custom levels to override default levels provided by Pino 
- If needed default values from Pino can be removed 

Reference : #507

mcollina: Can you add this option to the docs?
Arun4rangan: @mcollina sure!
Arun4rangan: @mcollina Added to docs, let me know if you have any other requests!
mcollina: CI is failing. It seems Object.values is not available.
Arun4rangan: @mcollina I added additional tests as well, I am not sure why CI is failing. Its failing because there isn't enough tests for a function initialising line. Should I write more tests for it?
mcollina: On Node 6: https://travis-ci.org/pinojs/pino/jobs/426109727. You are using some
syntax that is not available there.
mcollina: You can use `npm run cov-ui` to generate the code coverage report. There is a condition in 1-2 branches that it is not covered.
Arun4rangan: @mcollina I figured out which branches wasn't covered. All the tests are passing, let me know if you have any comments on the implementation!
Arun4rangan: @jsumners  added the warning!

Patch:
diff --git a/docs/api.md b/docs/api.md
index 0eee870..39c1bea 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -73,6 +73,27 @@ const logger = pino({
 logger.foo('hi')
 ```
 
+<a id=opt-useOnlyCustomLevels></a>
+#### `useOnlyCustomLevels` (Boolean)
+
+Default: `false`
+
+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.
+
+```js
+const logger = pino({
+  customLevels: {
+    foo: 35
+  },
+  useOnlyCustomLevels: true,
+  level: 'foo'
+})
+logger.foo('hi')
+logger.info('hello') // Will throw an error saying info in not found in logger object
+```
+
 #### `redact` (Array | Object):
 
 Default: `undefined`
diff --git a/lib/levels.js b/lib/levels.js
index d871e6f..1b4bacd 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -94,7 +94,7 @@ function isLevelEnabled (logLevel) {
   return logLevelVal !== undefined && (logLevelVal >= this[levelValSym])
 }
 
-function mappings (customLevels = null) {
+function mappings (customLevels = null, useOnlyCustomLevels = false) {
   const customNums = customLevels ? Object.keys(customLevels).reduce((o, k) => {
     o[customLevels[k]] = k
     return o
@@ -102,16 +102,40 @@ function mappings (customLevels = null) {
 
   const labels = Object.assign(
     Object.create(Object.prototype, { Infinity: { value: 'silent' } }),
-    nums,
+    useOnlyCustomLevels ? null : nums,
     customNums
   )
   const values = Object.assign(
     Object.create(Object.prototype, { silent: { value: Infinity } }),
-    levels,
+    useOnlyCustomLevels ? null : levels,
     customLevels
   )
   return { labels, values }
 }
+
+function assertDefaultLevelFound (defaultLevel, customLevels, useOnlyCustomLevels) {
+  if (typeof defaultLevel === 'number') {
+    const values = [].concat(
+      Object.keys(customLevels || {}).map(key => customLevels[key]),
+      useOnlyCustomLevels ? [] : Object.keys(nums),
+      Infinity
+    )
+    if (!values.includes(defaultLevel)) {
+      throw Error(`default level:${defaultLevel} must be included in custom levels`)
+    }
+    return
+  }
+
+  const labels = Object.assign(
+    Object.create(Object.prototype, { silent: { value: Infinity } }),
+    useOnlyCustomLevels ? null : levels,
+    customLevels
+  )
+  if (!(defaultLevel in labels)) {
+    throw Error(`default level:${defaultLevel} must be included in custom levels`)
+  }
+}
+
 function assertNoLevelCollisions (levels, customLevels) {
   const { labels, values } = levels
   for (const k in customLevels) {
@@ -125,12 +149,13 @@ function assertNoLevelCollisions (levels, customLevels) {
 }
 
 module.exports = {
-  assertNoLevelCollisions,
   initialLsCache,
   genLsCache,
   levelMethods,
   getLevel,
   setLevel,
   isLevelEnabled,
-  mappings
+  mappings,
+  assertNoLevelCollisions,
+  assertDefaultLevelFound
 }
diff --git a/lib/proto.js b/lib/proto.js
index 3ab4b03..0389e4c 100644
--- a/lib/proto.js
+++ b/lib/proto.js
@@ -13,6 +13,7 @@ const {
   timeSym,
   streamSym,
   serializersSym,
+  useOnlyCustomLevelsSym,
   needsMetadataGsym
 } = require('./symbols')
 const {
@@ -73,7 +74,7 @@ function child (bindings) {
   } else instance[serializersSym] = serializers
   if (bindings.hasOwnProperty('customLevels') === true) {
     assertNoLevelCollisions(this.levels, bindings.customLevels)
-    instance.levels = mappings(bindings.customLevels)
+    instance.levels = mappings(bindings.customLevels, instance[useOnlyCustomLevelsSym])
     genLsCache(instance)
   }
   instance[chindingsSym] = chindings
diff --git a/lib/symbols.js b/lib/symbols.js
index f30dd2a..8b8ff88 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -5,6 +5,7 @@ const getLevelSym = Symbol('pino.getLevel')
 const levelValSym = Symbol('pino.levelVal')
 const useLevelLabelsSym = Symbol('pino.useLevelLabels')
 const changeLevelNameSym = Symbol('pino.changeLevelName')
+const useOnlyCustomLevelsSym = Symbol('pino.useOnlyCustomLevels')
 
 const lsCacheSym = Symbol('pino.lsCache')
 const chindingsSym = Symbol('pino.chindings')
@@ -49,5 +50,6 @@ module.exports = {
   messageKeyStringSym,
   changeLevelNameSym,
   wildcardGsym,
-  needsMetadataGsym
+  needsMetadataGsym,
+  useOnlyCustomLevelsSym
 }
diff --git a/pino.js b/pino.js
index f6fead7..467e104 100644
--- a/pino.js
+++ b/pino.js
@@ -6,7 +6,7 @@ const redaction = require('./lib/redaction')
 const time = require('./lib/time')
 const proto = require('./lib/proto')
 const symbols = require('./lib/symbols')
-const { mappings, genLsCache, assertNoLevelCollisions } = require('./lib/levels')
+const { assertDefaultLevelFound, mappings, genLsCache } = require('./lib/levels')
 const {
   createArgsNormalizer,
   asChindings,
@@ -27,7 +27,8 @@ const {
   formatOptsSym,
   messageKeyStringSym,
   useLevelLabelsSym,
-  changeLevelNameSym
+  changeLevelNameSym,
+  useOnlyCustomLevelsSym
 } = symbols
 const { epochTime, nullTime } = time
 const { pid } = process
@@ -45,7 +46,8 @@ const defaultOptions = {
   name: undefined,
   redact: null,
   customLevels: null,
-  changeLevelName: 'level'
+  changeLevelName: 'level',
+  useOnlyCustomLevels: false
 }
 
 const normalize = createArgsNormalizer(defaultOptions)
@@ -63,11 +65,10 @@ function pino (...args) {
     level,
     customLevels,
     useLevelLabels,
-    changeLevelName
+    changeLevelName,
+    useOnlyCustomLevels
   } = opts
 
-  assertNoLevelCollisions(pino.levels, customLevels)
-
   const stringifiers = redact ? redaction(redact, stringify) : {}
   const formatOpts = redact
     ? { stringify: stringifiers[redactFmtSym] }
@@ -85,12 +86,16 @@ function pino (...args) {
   const time = (timestamp instanceof Function)
     ? timestamp : (timestamp ? epochTime : nullTime)
 
-  const levels = mappings(customLevels)
+  if (useOnlyCustomLevels && !customLevels) throw Error('customLevels is required if useOnlyCustomLevels is set true')
+
+  assertDefaultLevelFound(level, customLevels, useOnlyCustomLevels)
+  const levels = mappings(customLevels, useOnlyCustomLevels)
 
   const instance = {
     levels,
     [useLevelLabelsSym]: useLevelLabels,
     [changeLevelNameSym]: changeLevelName,
+    [useOnlyCustomLevelsSym]: useOnlyCustomLevels,
     [streamSym]: stream,
     [timeSym]: time,
     [stringifySym]: stringify,


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index b241140..2165006 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -515,3 +515,18 @@ test('fast-safe-stringify must be used when interpolating', async (t) => {
   const { msg } = await once(stream, 'data')
   t.is(msg, 'test {"a":{"b":{"c":"[Circular]"}}}')
 })
+
+test('throws when setting useOnlyCustomLevels without customLevels', async ({ is, throws }) => {
+  throws(() => {
+    pino({
+      useOnlyCustomLevels: true
+    })
+  })
+  try {
+    pino({
+      useOnlyCustomLevels: true
+    })
+  } catch ({ message }) {
+    is(message, 'customLevels is required if useOnlyCustomLevels is set true')
+  }
+})
diff --git a/test/custom-levels.test.js b/test/custom-levels.test.js
index 6f7efcc..f044061 100644
--- a/test/custom-levels.test.js
+++ b/test/custom-levels.test.js
@@ -18,42 +18,43 @@ test('adds additional levels', async ({ is }) => {
   is(level, 35)
 })
 
-test('throws when specifying existing levels', async ({ is, throws }) => {
+test('custom levels does not override default levels', async ({ is }) => {
   const stream = sink()
-  throws(() => pino({
+  const logger = pino({
     customLevels: {
-      info: 35
+      foo: 35
     }
   }, stream)
-  )
-  try {
-    pino({
-      customLevels: {
-        info: 35
-      }
-    })
-  } catch ({ message }) {
-    is(message, 'levels cannot be overridden')
-  }
+
+  logger.info('test')
+  const { level } = await once(stream, 'data')
+  is(level, 30)
 })
 
-test('throws when specifying existing values', async ({ is, throws }) => {
+test('custom levels overrides default level label if use useOnlyCustomLevels', async ({ is }) => {
   const stream = sink()
-  throws(() => pino({
+  const logger = pino({
     customLevels: {
-      foo: 30
-    }
+      foo: 35
+    },
+    useOnlyCustomLevels: true,
+    level: 'foo'
   }, stream)
-  )
-  try {
-    pino({
-      customLevels: {
-        foo: 30
-      }
-    })
-  } catch ({ message }) {
-    is(message, 'pre-existing level values cannot be used for new levels')
-  }
+
+  is(logger.hasOwnProperty('info'), false)
+})
+
+test('custom levels overrides default level value if use useOnlyCustomLevels', async ({ is }) => {
+  const stream = sink()
+  const logger = pino({
+    customLevels: {
+      foo: 35
+    },
+    useOnlyCustomLevels: true,
+    level: 35
+  }, stream)
+
+  is(logger.hasOwnProperty('info'), false)
 })
 
 test('custom levels are inherited by children', async ({ is }) => {
@@ -101,45 +102,7 @@ test('customLevels property child bindings does not get logged', async ({ is })
   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 }) => {
+test('throws when specifying pre-existing parent labels via child bindings', async ({ is, throws }) => {
   const stream = sink()
   throws(() => pino({
     customLevels: {
@@ -193,6 +156,40 @@ test('throws when specifying pre-existing parent values via child bindings', asy
   }
 })
 
+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 useOnlyCustomLevels is set true without customLevels', async ({ is, throws }) => {
+  const stream = sink()
+  throws(() => pino({
+    useOnlyCustomLevels: true
+  }, stream)
+  )
+  try {
+    pino({
+      useOnlyCustomLevels: true
+    }, stream)
+  } catch ({ message }) {
+    is(message, 'customLevels is required if useOnlyCustomLevels is set true')
+  }
+})
+
 test('custom level on one instance does not affect other instances', async ({ is }) => {
   pino({ customLevels: {
     foo: 37
diff --git a/test/levels.test.js b/test/levels.test.js
index c35bb9c..064be32 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -322,3 +322,62 @@ test('setting changeLevelName does not affect labels when told to', async ({ is
 
   instance.info('hello world')
 })
+
+test('throws when creating a default label that does not exist in logger levels', async ({ is, throws }) => {
+  const defaultLevel = 'foo'
+  throws(() => {
+    pino({
+      customLevels: {
+        bar: 5
+      },
+      level: defaultLevel
+    })
+  })
+  try {
+    pino({
+      level: defaultLevel
+    })
+  } catch ({ message }) {
+    is(message, `default level:${defaultLevel} must be included in custom levels`)
+  }
+})
+
+test('throws when creating a default value that does not exist in logger levels', async ({ is, throws }) => {
+  const defaultLevel = 15
+  throws(() => {
+    pino({
+      customLevels: {
+        bar: 5
+      },
+      level: defaultLevel
+    })
+  })
+  try {
+    pino({
+      level: defaultLevel
+    })
+  } catch ({ message }) {
+    is(message, `default level:${defaultLevel} must be included in custom levels`)
+  }
+})
+
+test('throws when creating a default value that does not exist in logger levels', async ({ is, throws }) => {
+  throws(() => {
+    pino({
+      customLevels: {
+        foo: 5
+      },
+      useOnlyCustomLevels: true
+    })
+  })
+  try {
+    pino({
+      customLevels: {
+        foo: 5
+      },
+      useOnlyCustomLevels: true
+    })
+  } catch ({ message }) {
+    is(message, `default level:info must be included in custom levels`)
+  }
+})