Repository: pinojs/pino
Pull request #1901 (2024-02-09 12:17:01)

`setLevel` should respect level comparison option
@mcollina @jsumners 

Previous PR #1883 contained only a half of a change, i just realized that `setLevel` also should respect `levelComparison`, not only `isLevelEnabled`


Patch:
diff --git a/browser.js b/browser.js
index 9e98720..e16ee74 100644
--- a/browser.js
+++ b/browser.js
@@ -109,6 +109,7 @@ function pino (opts) {
     transmit,
     serialize,
     asObject: opts.browser.asObject,
+    formatters: opts.browser.formatters,
     levels,
     timestamp: getTimeFunction(opts)
   }
@@ -299,8 +300,9 @@ function createWrap (self, opts, rootLogger, level) {
       if (opts.serialize && !opts.asObject) {
         applySerializers(args, this._serialize, this.serializers, this._stdErrSerialize)
       }
-      if (opts.asObject) write.call(proto, asObject(this, level, args, ts))
-      else write.apply(proto, args)
+      if (opts.asObject || opts.formatters) {
+        write.call(proto, asObject(this, level, args, ts, opts.formatters))
+      } else write.apply(proto, args)
 
       if (opts.transmit) {
         const transmitLevel = opts.transmit.level || self._level
@@ -321,7 +323,10 @@ function createWrap (self, opts, rootLogger, level) {
   })(self[baseLogFunctionSymbol][level])
 }
 
-function asObject (logger, level, args, ts) {
+function asObject (logger, level, args, ts, formatters = {}) {
+  const {
+    level: levelFormatter
+  } = formatters
   if (logger._serialize) applySerializers(args, logger._serialize, logger.serializers, logger._stdErrSerialize)
   const argsCloned = args.slice()
   let msg = argsCloned[0]
@@ -329,7 +334,12 @@ function asObject (logger, level, args, ts) {
   if (ts) {
     o.time = ts
   }
-  o.level = logger.levels.values[level]
+  if (levelFormatter) {
+    const formattedLevel = levelFormatter(level, logger.levels.values[level])
+    Object.assign(o, formattedLevel)
+  } else {
+    o.level = logger.levels.values[level]
+  }
   let lvl = (logger._childLevel | 0) + 1
   if (lvl < 1) lvl = 1
   // deliberate, catching objects, arrays
diff --git a/docs/browser.md b/docs/browser.md
index 394de87..deb465c 100644
--- a/docs/browser.md
+++ b/docs/browser.md
@@ -27,6 +27,25 @@ pino.info('hi') // creates and logs {msg: 'hi', level: 30, time: <ts>}
 
 When `write` is set, `asObject` will always be `true`.
 
+### `formatters` (Object)
+
+An object containing functions for formatting the shape of the log lines. When provided, it enables the logger to produce a pino-like log object with customized formatting. Currently, it supports formatting for the `level` object only.
+
+##### `level`
+
+Changes the shape of the log level. The default shape is `{ level: number }`.
+The function takes two arguments, the label of the level (e.g. `'info'`)
+and the numeric value (e.g. `30`).
+
+```js
+const formatters = {
+  level (label, number) {
+    return { level: number }
+  }
+}
+```
+
+
 ### `write` (Function | Object)
 
 Instead of passing log messages to `console.log` they can be passed to
diff --git a/lib/levels.js b/lib/levels.js
index 5555da4..4a3af01 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -84,10 +84,11 @@ function setLevel (level) {
   const preLevelVal = this[levelValSym]
   const levelVal = this[levelValSym] = values[level]
   const useOnlyCustomLevelsVal = this[useOnlyCustomLevelsSym]
+  const levelComparison = this[levelCompSym]
   const hook = this[hooksSym].logMethod
 
   for (const key in values) {
-    if (levelVal > values[key]) {
+    if (levelComparison(values[key], levelVal) === false) {
       this[key] = noop
       continue
     }


Test patch:
diff --git a/test/browser.test.js b/test/browser.test.js
index 5605321..9f57559 100644
--- a/test/browser.test.js
+++ b/test/browser.test.js
@@ -166,6 +166,29 @@ test('opts.browser.asObject logs pino-like object to console', ({ end, ok, is })
   end()
 })
 
+test('opts.browser.formatters logs pino-like object to console', ({ end, ok, is }) => {
+  const info = console.info
+  console.info = function (o) {
+    is(o.level, 30)
+    is(o.label, 'info')
+    is(o.msg, 'test')
+    ok(o.time)
+    console.info = info
+  }
+  const instance = require('../browser')({
+    browser: {
+      formatters: {
+        level (label, number) {
+          return { label, level: number }
+        }
+      }
+    }
+  })
+
+  instance.info('test')
+  end()
+})
+
 test('opts.browser.write func log single string', ({ end, ok, is }) => {
   const instance = pino({
     browser: {
diff --git a/test/levels.test.js b/test/levels.test.js
index e47945d..1581bc1 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -527,6 +527,177 @@ test('changing level from info to silent and back to info in child logger', asyn
   check(equal, result, expected.level, expected.msg)
 })
 
+test('changing level respects level comparison set to', async ({ test, end }) => {
+  const ascLevels = {
+    debug: 1,
+    info: 2,
+    warn: 3
+  }
+
+  const descLevels = {
+    debug: 3,
+    info: 2,
+    warn: 1
+  }
+
+  const expected = {
+    level: 2,
+    msg: 'hello world'
+  }
+
+  test('ASC in parent logger', async ({ equal }) => {
+    const customLevels = ascLevels
+    const levelComparison = 'ASC'
+
+    const stream = sink()
+    const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
+
+    logger.level = 'warn'
+    logger.info('hello world')
+    let result = stream.read()
+    equal(result, null)
+
+    logger.level = 'debug'
+    logger.info('hello world')
+    result = await once(stream, 'data')
+    check(equal, result, expected.level, expected.msg)
+  })
+
+  test('DESC in parent logger', async ({ equal }) => {
+    const customLevels = descLevels
+    const levelComparison = 'DESC'
+
+    const stream = sink()
+    const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
+
+    logger.level = 'warn'
+    logger.info('hello world')
+    let result = stream.read()
+    equal(result, null)
+
+    logger.level = 'debug'
+    logger.info('hello world')
+    result = await once(stream, 'data')
+    check(equal, result, expected.level, expected.msg)
+  })
+
+  test('custom function in parent logger', async ({ equal }) => {
+    const customLevels = {
+      info: 2,
+      debug: 345,
+      warn: 789
+    }
+    const levelComparison = (current, expected) => {
+      if (expected === customLevels.warn) return false
+      return true
+    }
+
+    const stream = sink()
+    const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
+
+    logger.level = 'warn'
+    logger.info('hello world')
+    let result = stream.read()
+    equal(result, null)
+
+    logger.level = 'debug'
+    logger.info('hello world')
+    result = await once(stream, 'data')
+    check(equal, result, expected.level, expected.msg)
+  })
+
+  test('ASC in child logger', async ({ equal }) => {
+    const customLevels = ascLevels
+    const levelComparison = 'ASC'
+
+    const stream = sink()
+    const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream).child({ })
+
+    logger.level = 'warn'
+    logger.info('hello world')
+    let result = stream.read()
+    equal(result, null)
+
+    logger.level = 'debug'
+    logger.info('hello world')
+    result = await once(stream, 'data')
+    check(equal, result, expected.level, expected.msg)
+  })
+
+  test('DESC in parent logger', async ({ equal }) => {
+    const customLevels = descLevels
+    const levelComparison = 'DESC'
+
+    const stream = sink()
+    const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream).child({ })
+
+    logger.level = 'warn'
+    logger.info('hello world')
+    let result = stream.read()
+    equal(result, null)
+
+    logger.level = 'debug'
+    logger.info('hello world')
+    result = await once(stream, 'data')
+    check(equal, result, expected.level, expected.msg)
+  })
+
+  test('custom function in child logger', async ({ equal }) => {
+    const customLevels = {
+      info: 2,
+      debug: 345,
+      warn: 789
+    }
+    const levelComparison = (current, expected) => {
+      if (expected === customLevels.warn) return false
+      return true
+    }
+
+    const stream = sink()
+    const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream).child({ })
+
+    logger.level = 'warn'
+    logger.info('hello world')
+    let result = stream.read()
+    equal(result, null)
+
+    logger.level = 'debug'
+    logger.info('hello world')
+    result = await once(stream, 'data')
+    check(equal, result, expected.level, expected.msg)
+  })
+
+  end()
+})
+
+test('changing level respects level comparison DESC', async ({ equal }) => {
+  const customLevels = {
+    warn: 1,
+    info: 2,
+    debug: 3
+  }
+
+  const levelComparison = 'DESC'
+
+  const expected = {
+    level: 2,
+    msg: 'hello world'
+  }
+
+  const stream = sink()
+  const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
+
+  logger.level = 'warn'
+  logger.info('hello world')
+  let result = stream.read()
+  equal(result, null)
+
+  logger.level = 'debug'
+  logger.info('hello world')
+  result = await once(stream, 'data')
+  check(equal, result, expected.level, expected.msg)
+})
+
 // testing for potential loss of Pino constructor scope from serializers - an edge case with circular refs see:  https://github.com/pinojs/pino/issues/833
 test('trying to get levels when `this` is no longer a Pino instance returns an empty string', async ({ equal }) => {
   const notPinoInstance = { some: 'object', getLevel: levelsLib.getLevel }