diff --git a/docs/api.md b/docs/api.md index 137afc35a..25cd588f3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -65,6 +65,32 @@ Additional levels can be added to the instance via the `customLevels` option. * See [`customLevels` option](#opt-customlevels) + +#### `levelComparison` ("ASC", "DESC", Function) + +Default: `ASC` + +Use this option to customize levels order. +In order to be able to define custom levels ordering pass a function which will accept `current` and `expected` values and return `boolean` which shows should `current` level to be shown or not. + +```js +const logger = pino({ + levelComparison: 'DESC', + customLevels: { + foo: 20, // `foo` is more valuable than `bar` + bar: 10 + }, +}) + +// OR + +const logger = pino({ + levelComparison: function(current, expected) { + return current >= expected; + } +}) +``` + #### `customLevels` (Object) Default: `undefined` diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 000000000..f91f73157 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,28 @@ +/** + * Represents default log level values + * + * @enum {number} + */ +const DEFAULT_LEVELS = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60 +} + +/** + * Represents sort order direction: `ascending` or `descending` + * + * @enum {string} + */ +const SORTING_ORDER = { + ASC: 'ASC', + DESC: 'DESC' +} + +module.exports = { + DEFAULT_LEVELS, + SORTING_ORDER +} diff --git a/lib/levels.js b/lib/levels.js index 3f1acdaf7..5555da49c 100644 --- a/lib/levels.js +++ b/lib/levels.js @@ -6,21 +6,15 @@ const { useOnlyCustomLevelsSym, streamSym, formattersSym, - hooksSym + hooksSym, + levelCompSym } = require('./symbols') const { noop, genLog } = require('./tools') +const { DEFAULT_LEVELS, SORTING_ORDER } = require('./constants') -const levels = { - trace: 10, - debug: 20, - info: 30, - warn: 40, - error: 50, - fatal: 60 -} const levelMethods = { fatal: (hook) => { - const logFatal = genLog(levels.fatal, hook) + const logFatal = genLog(DEFAULT_LEVELS.fatal, hook) return function (...args) { const stream = this[streamSym] logFatal.call(this, ...args) @@ -33,15 +27,15 @@ const levelMethods = { } } }, - error: (hook) => genLog(levels.error, hook), - warn: (hook) => genLog(levels.warn, hook), - info: (hook) => genLog(levels.info, hook), - debug: (hook) => genLog(levels.debug, hook), - trace: (hook) => genLog(levels.trace, hook) + error: (hook) => genLog(DEFAULT_LEVELS.error, hook), + warn: (hook) => genLog(DEFAULT_LEVELS.warn, hook), + info: (hook) => genLog(DEFAULT_LEVELS.info, hook), + debug: (hook) => genLog(DEFAULT_LEVELS.debug, hook), + trace: (hook) => genLog(DEFAULT_LEVELS.trace, hook) } -const nums = Object.keys(levels).reduce((o, k) => { - o[levels[k]] = k +const nums = Object.keys(DEFAULT_LEVELS).reduce((o, k) => { + o[DEFAULT_LEVELS[k]] = k return o }, {}) @@ -119,7 +113,39 @@ function getLevel (level) { function isLevelEnabled (logLevel) { const { values } = this.levels const logLevelVal = values[logLevel] - return logLevelVal !== undefined && (logLevelVal >= this[levelValSym]) + return logLevelVal !== undefined && this[levelCompSym](logLevelVal, this[levelValSym]) +} + +/** + * Determine if the given `current` level is enabled by comparing it + * against the current threshold (`expected`). + * + * @param {SORTING_ORDER} direction comparison direction "ASC" or "DESC" + * @param {number} current current log level number representatiton + * @param {number} expected threshold value to compare with + * @returns {boolean} + */ +function compareLevel (direction, current, expected) { + if (direction === SORTING_ORDER.DESC) { + return current <= expected + } + + return current >= expected +} + +/** + * Create a level comparison function based on `levelComparison` + * it could a default function which compares levels either in "ascending" or "descending" order or custom comparison function + * + * @param {SORTING_ORDER | Function} levelComparison sort levels order direction or custom comparison function + * @returns Function + */ +function genLevelComparison (levelComparison) { + if (typeof levelComparison === 'string') { + return compareLevel.bind(null, levelComparison) + } + + return levelComparison } function mappings (customLevels = null, useOnlyCustomLevels = false) { @@ -139,7 +165,7 @@ function mappings (customLevels = null, useOnlyCustomLevels = false) { ) const values = Object.assign( Object.create(Object.prototype, { silent: { value: Infinity } }), - useOnlyCustomLevels ? null : levels, + useOnlyCustomLevels ? null : DEFAULT_LEVELS, customLevels ) return { labels, values } @@ -160,7 +186,7 @@ function assertDefaultLevelFound (defaultLevel, customLevels, useOnlyCustomLevel const labels = Object.assign( Object.create(Object.prototype, { silent: { value: Infinity } }), - useOnlyCustomLevels ? null : levels, + useOnlyCustomLevels ? null : DEFAULT_LEVELS, customLevels ) if (!(defaultLevel in labels)) { @@ -180,6 +206,25 @@ function assertNoLevelCollisions (levels, customLevels) { } } +/** + * Validates whether `levelComparison` is correct + * + * @throws Error + * @param {SORTING_ORDER | Function} levelComparison - value to validate + * @returns + */ +function assertLevelComparison (levelComparison) { + if (typeof levelComparison === 'function') { + return + } + + if (typeof levelComparison === 'string' && Object.values(SORTING_ORDER).includes(levelComparison)) { + return + } + + throw new Error('Levels comparison should be one of "ASC", "DESC" or "function" type') +} + module.exports = { initialLsCache, genLsCache, @@ -188,7 +233,8 @@ module.exports = { setLevel, isLevelEnabled, mappings, - levels, assertNoLevelCollisions, - assertDefaultLevelFound + assertDefaultLevelFound, + genLevelComparison, + assertLevelComparison } diff --git a/lib/multistream.js b/lib/multistream.js index 48ea7a5ad..4de6ab633 100644 --- a/lib/multistream.js +++ b/lib/multistream.js @@ -1,16 +1,16 @@ 'use strict' const metadata = Symbol.for('pino.metadata') -const { levels } = require('./levels') +const { DEFAULT_LEVELS } = require('./constants') -const DEFAULT_INFO_LEVEL = levels.info +const DEFAULT_INFO_LEVEL = DEFAULT_LEVELS.info function multistream (streamsArray, opts) { let counter = 0 streamsArray = streamsArray || [] opts = opts || { dedupe: false } - const streamLevels = Object.create(levels) + const streamLevels = Object.create(DEFAULT_LEVELS) streamLevels.silent = Infinity if (opts.levels && typeof opts.levels === 'object') { Object.keys(opts.levels).forEach(i => { diff --git a/lib/symbols.js b/lib/symbols.js index 08c0e88b8..69f1a9d25 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -3,6 +3,7 @@ const setLevelSym = Symbol('pino.setLevel') const getLevelSym = Symbol('pino.getLevel') const levelValSym = Symbol('pino.levelVal') +const levelCompSym = Symbol('pino.levelComp') const useLevelLabelsSym = Symbol('pino.useLevelLabels') const useOnlyCustomLevelsSym = Symbol('pino.useOnlyCustomLevels') const mixinSym = Symbol('pino.mixin') @@ -42,6 +43,7 @@ module.exports = { setLevelSym, getLevelSym, levelValSym, + levelCompSym, useLevelLabelsSym, mixinSym, lsCacheSym, diff --git a/pino.d.ts b/pino.d.ts index 988a2b8e3..e21145827 100644 --- a/pino.d.ts +++ b/pino.d.ts @@ -354,6 +354,12 @@ declare namespace pino { * The keys of the object correspond the namespace of the log level, and the values should be the numerical value of the level. */ customLevels?: { [level in CustomLevels]: number }; + /** + * Use this option to define custom comparison of log levels. + * Usefull to compare custom log levels or non-standard level values. + * Default: "ASC" + */ + levelComparison?: "ASC" | "DESC" | ((current: number, expected: number) => boolean); /** * 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` @@ -853,4 +859,5 @@ export { pino as default, pino }; // Export just the type side of the namespace as "P", allows // `import {P} from "pino"; const log: P.Logger;`. // (Legacy support for early 7.x releases, remove in 8.x.) -export type { pino as P }; + export type { pino as P }; + diff --git a/pino.js b/pino.js index 63500e46f..13f122193 100644 --- a/pino.js +++ b/pino.js @@ -8,7 +8,8 @@ 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, levels } = require('./lib/levels') +const { assertDefaultLevelFound, mappings, genLsCache, genLevelComparison, assertLevelComparison } = require('./lib/levels') +const { DEFAULT_LEVELS, SORTING_ORDER } = require('./lib/constants') const { createArgsNormalizer, asChindings, @@ -36,6 +37,7 @@ const { errorKeySym, nestedKeySym, mixinSym, + levelCompSym, useOnlyCustomLevelsSym, formattersSym, hooksSym, @@ -49,7 +51,8 @@ const hostname = os.hostname() const defaultErrorSerializer = stdSerializers.err const defaultOptions = { level: 'info', - levels, + levelComparison: SORTING_ORDER.ASC, + levels: DEFAULT_LEVELS, messageKey: 'msg', errorKey: 'err', nestedKey: null, @@ -97,6 +100,7 @@ function pino (...args) { name, level, customLevels, + levelComparison, mixin, mixinMergeStrategy, useOnlyCustomLevels, @@ -157,8 +161,12 @@ function pino (...args) { assertDefaultLevelFound(level, customLevels, useOnlyCustomLevels) const levels = mappings(customLevels, useOnlyCustomLevels) + assertLevelComparison(levelComparison) + const levelCompFunc = genLevelComparison(levelComparison) + Object.assign(instance, { levels, + [levelCompSym]: levelCompFunc, [useOnlyCustomLevelsSym]: useOnlyCustomLevels, [streamSym]: stream, [timeSym]: time, diff --git a/test/is-level-enabled.test.js b/test/is-level-enabled.test.js index 8df8b8887..29c2bab60 100644 --- a/test/is-level-enabled.test.js +++ b/test/is-level-enabled.test.js @@ -3,41 +3,183 @@ const { test } = require('tap') const pino = require('../') -test('can check if current level enabled', async ({ equal }) => { - const log = pino({ level: 'debug' }) - equal(true, log.isLevelEnabled('debug')) -}) +const descLevels = { + trace: 60, + debug: 50, + info: 40, + warn: 30, + error: 20, + fatal: 10 +} -test('can check if level enabled after level set', async ({ equal }) => { - const log = pino() - equal(false, log.isLevelEnabled('debug')) - log.level = 'debug' - equal(true, log.isLevelEnabled('debug')) -}) +const ascLevels = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60 +} + +test('Default levels suite', ({ test, end }) => { + test('can check if current level enabled', async ({ equal }) => { + const log = pino({ level: 'debug' }) + equal(true, log.isLevelEnabled('debug')) + }) + + test('can check if level enabled after level set', async ({ equal }) => { + const log = pino() + equal(false, log.isLevelEnabled('debug')) + log.level = 'debug' + equal(true, log.isLevelEnabled('debug')) + }) + + test('can check if higher level enabled', async ({ equal }) => { + const log = pino({ level: 'debug' }) + equal(true, log.isLevelEnabled('error')) + }) + + test('can check if lower level is disabled', async ({ equal }) => { + const log = pino({ level: 'error' }) + equal(false, log.isLevelEnabled('trace')) + }) + + test('ASC: can check if child has current level enabled', async ({ equal }) => { + const log = pino().child({}, { level: 'debug' }) + equal(true, log.isLevelEnabled('debug')) + equal(true, log.isLevelEnabled('error')) + equal(false, log.isLevelEnabled('trace')) + }) -test('can check if higher level enabled', async ({ equal }) => { - const log = pino({ level: 'debug' }) - equal(true, log.isLevelEnabled('error')) + test('can check if custom level is enabled', async ({ equal }) => { + const log = pino({ + customLevels: { foo: 35 }, + level: 'debug' + }) + equal(true, log.isLevelEnabled('foo')) + equal(true, log.isLevelEnabled('error')) + equal(false, log.isLevelEnabled('trace')) + }) + + end() }) -test('can check if lower level is disabled', async ({ equal }) => { - const log = pino({ level: 'error' }) - equal(false, log.isLevelEnabled('trace')) +test('Ascending levels suite', ({ test, end }) => { + const customLevels = ascLevels + const levelComparison = 'ASC' + + test('can check if current level enabled', async ({ equal }) => { + const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(true, log.isLevelEnabled('debug')) + }) + + test('can check if level enabled after level set', async ({ equal }) => { + const log = pino({ levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(false, log.isLevelEnabled('debug')) + log.level = 'debug' + equal(true, log.isLevelEnabled('debug')) + }) + + test('can check if higher level enabled', async ({ equal }) => { + const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(true, log.isLevelEnabled('error')) + }) + + test('can check if lower level is disabled', async ({ equal }) => { + const log = pino({ level: 'error', customLevels, useOnlyCustomLevels: true }) + equal(false, log.isLevelEnabled('trace')) + }) + + test('can check if child has current level enabled', async ({ equal }) => { + const log = pino().child({ levelComparison, customLevels, useOnlyCustomLevels: true }, { level: 'debug' }) + equal(true, log.isLevelEnabled('debug')) + equal(true, log.isLevelEnabled('error')) + equal(false, log.isLevelEnabled('trace')) + }) + + test('can check if custom level is enabled', async ({ equal }) => { + const log = pino({ + levelComparison, + useOnlyCustomLevels: true, + customLevels: { foo: 35, ...customLevels }, + level: 'debug' + }) + equal(true, log.isLevelEnabled('foo')) + equal(true, log.isLevelEnabled('error')) + equal(false, log.isLevelEnabled('trace')) + }) + + end() }) -test('can check if child has current level enabled', async ({ equal }) => { - const log = pino().child({}, { level: 'debug' }) - equal(true, log.isLevelEnabled('debug')) - equal(true, log.isLevelEnabled('error')) - equal(false, log.isLevelEnabled('trace')) +test('Descending levels suite', ({ test, end }) => { + const customLevels = descLevels + const levelComparison = 'DESC' + + test('can check if current level enabled', async ({ equal }) => { + const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(true, log.isLevelEnabled('debug')) + }) + + test('can check if level enabled after level set', async ({ equal }) => { + const log = pino({ levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(false, log.isLevelEnabled('debug')) + log.level = 'debug' + equal(true, log.isLevelEnabled('debug')) + }) + + test('can check if higher level enabled', async ({ equal }) => { + const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(true, log.isLevelEnabled('error')) + }) + + test('can check if lower level is disabled', async ({ equal }) => { + const log = pino({ level: 'error', levelComparison, customLevels, useOnlyCustomLevels: true }) + equal(false, log.isLevelEnabled('trace')) + }) + + test('can check if child has current level enabled', async ({ equal }) => { + const log = pino({ levelComparison, customLevels, useOnlyCustomLevels: true }).child({}, { level: 'debug' }) + equal(true, log.isLevelEnabled('debug')) + equal(true, log.isLevelEnabled('error')) + equal(false, log.isLevelEnabled('trace')) + }) + + test('can check if custom level is enabled', async ({ equal }) => { + const log = pino({ + levelComparison, + customLevels: { foo: 35, ...customLevels }, + useOnlyCustomLevels: true, + level: 'debug' + }) + equal(true, log.isLevelEnabled('foo')) + equal(true, log.isLevelEnabled('error')) + equal(false, log.isLevelEnabled('trace')) + }) + + end() }) -test('can check if custom level is enabled', async ({ equal }) => { - const log = pino({ - customLevels: { foo: 35 }, - level: 'debug' +test('Custom levels comparison', async ({ test, end }) => { + test('Custom comparison returns true cause level is enabled', async ({ equal }) => { + const log = pino({ level: 'error', levelComparison: () => true }) + equal(true, log.isLevelEnabled('debug')) + }) + + test('Custom comparison returns false cause level is disabled', async ({ equal }) => { + const log = pino({ level: 'error', levelComparison: () => false }) + equal(false, log.isLevelEnabled('debug')) }) - equal(true, log.isLevelEnabled('foo')) - equal(true, log.isLevelEnabled('error')) - equal(false, log.isLevelEnabled('trace')) + + test('Custom comparison returns true cause child level is enabled', async ({ equal }) => { + const log = pino({ levelComparison: () => true }).child({ level: 'error' }) + equal(true, log.isLevelEnabled('debug')) + }) + + test('Custom comparison returns false cause child level is disabled', async ({ equal }) => { + const log = pino({ levelComparison: () => false }).child({ level: 'error' }) + equal(false, log.isLevelEnabled('debug')) + }) + + end() }) diff --git a/test/types/pino.test-d.ts b/test/types/pino.test-d.ts index ee78e3d34..957610201 100644 --- a/test/types/pino.test-d.ts +++ b/test/types/pino.test-d.ts @@ -1,7 +1,7 @@ -import P, { pino } from "../../"; import { IncomingMessage, ServerResponse } from "http"; import { Socket } from "net"; -import { expectError, expectType } from 'tsd' +import { expectError, expectType } from 'tsd'; +import P, { pino } from "../../"; import Logger = P.Logger; const log = pino(); @@ -424,3 +424,16 @@ expectError(childLogger2.doesntExist); expectError(pino({ onChild: (child) => { const a = child.doesntExist; } }, process.stdout)); + +const pinoWithoutLevelsSorting = pino({}); +const pinoWithDescSortingLevels = pino({ levelComparison: 'DESC' }); +const pinoWithAscSortingLevels = pino({ levelComparison: 'ASC' }); +const pinoWithCustomSortingLevels = pino({ levelComparison: () => false }); +// with wrong level comparison direction +expectError(pino({ levelComparison: 'SOME'}), process.stdout); +// with wrong level comparison type +expectError(pino({ levelComparison: 123}), process.stdout); +// with wrong custom level comparison return type +expectError(pino({ levelComparison: () => null }), process.stdout); +expectError(pino({ levelComparison: () => 1 }), process.stdout); +expectError(pino({ levelComparison: () => 'string' }), process.stdout); \ No newline at end of file