Skip to content

Commit

Permalink
Add new option to fallback custom levels and colors to default values (
Browse files Browse the repository at this point in the history
  • Loading branch information
bitDaft committed Mar 24, 2022
1 parent b0c6c64 commit 86715d8
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 41 deletions.
3 changes: 3 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ node app.js | pino-pretty
- `--levelLabel` (`-b`): Output the log level using the specified label.
Default: `levelLabel`.
- `--minimumLevel` (`-L`): Hide messages below the specified log level. Accepts a number, `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. If any more filtering is required, consider using [`jq`](https://stedolan.github.io/jq/).
- `--customLevels` (`-x`): Override default levels with custom levels, e.g. `-x err:99,info:1`
- `--customColors` (`-X`): Override default colors with custom colors, e.g. `-X err:red,info:blue`
- `--useOnlyCustomProps` (`-U`): Only use custom levels and colors (if provided) (default: true); else fallback to default levels and colors, e.g. `-U false`
- `--messageFormat` (`-o`): Format output of message, e.g. `{levelLabel} - {pid} - url:{request.url}` will output message: `INFO - 1123 - url:localhost:3000/test`
Default: `false`
- `--timestampKey` (`-a`): Define the key that contains the log timestamp.
Expand Down
1 change: 1 addition & 0 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ args
.option(['L', 'minimumLevel'], 'Hide messages below the specified log level')
.option(['x', 'customLevels'], 'Override default levels (`-x err:99,info:1`)')
.option(['X', 'customColors'], 'Override default colors using names from https://www.npmjs.com/package/colorette (`-X err:red,info:blue`)')
.option(['U', 'useOnlyCustomProps'], 'Only use custom levels and colors (if provided); don\'t fallback to default levels and colors (-U false)')
.option(['k', 'errorLikeObjectKeys'], 'Define which keys contain error objects (`-k err,error`) (defaults to `err,error`)')
.option(['m', 'messageKey'], 'Highlight the message under the specified key', CONSTANTS.MESSAGE_KEY)
.option('levelKey', 'Detect the log level under the specified key', CONSTANTS.LEVEL_KEY)
Expand Down
28 changes: 20 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const defaultOptions = {
errorProps: '',
customLevels: null,
customColors: null,
useOnlyCustomProps: true,
levelFirst: false,
messageKey: MESSAGE_KEY,
messageFormat: false,
Expand All @@ -58,6 +59,7 @@ function prettyFactory (options) {
const timestampKey = opts.timestampKey
const errorLikeObjectKeys = opts.errorLikeObjectKeys
const errorProps = opts.errorProps.split(',')
const useOnlyCustomProps = typeof opts.useOnlyCustomProps === 'boolean' ? opts.useOnlyCustomProps : opts.useOnlyCustomProps === 'true'
const customLevels = opts.customLevels
? opts.customLevels
.split(',')
Expand All @@ -68,37 +70,46 @@ function prettyFactory (options) {

return agg
}, { default: 'USERLVL' })
: undefined
: {}
const customLevelNames = opts.customLevels
? opts.customLevels
.split(',')
.reduce((agg, value, idx) => {
const [levelName, levelIdx = idx] = value.split(':')

agg[levelName] = levelIdx
agg[levelName.toLowerCase()] = levelIdx

return agg
}, {})
: undefined
: {}
const customColors = opts.customColors
? opts.customColors
.split(',')
.reduce((agg, value) => {
const [level, color] = value.split(':')

const levelNum = customLevelNames !== undefined ? customLevelNames[level] : LEVEL_NAMES[level]
const condition = useOnlyCustomProps ? opts.customLevels : customLevelNames[level] !== undefined
const levelNum = condition ? customLevelNames[level] : LEVEL_NAMES[level]
const colorIdx = levelNum !== undefined ? levelNum : level

agg.push([colorIdx, color])

return agg
}, [])
: undefined
const customProps = {
customLevels,
customLevelNames
}
if (useOnlyCustomProps && !opts.customLevels) {
customProps.customLevels = undefined
customProps.customLevelNames = undefined
}
const customPrettifiers = opts.customPrettifiers
const ignoreKeys = opts.ignore ? new Set(opts.ignore.split(',')) : undefined
const hideObject = opts.hideObject
const singleLine = opts.singleLine
const colorizer = colors(opts.colorize, customColors)
const colorizer = colors(opts.colorize, customColors, useOnlyCustomProps)

return pretty

Expand All @@ -116,18 +127,19 @@ function prettyFactory (options) {
}

if (minimumLevel) {
const minimum = (customLevelNames === undefined ? LEVEL_NAMES[minimumLevel] : customLevelNames[minimumLevel]) || Number(minimumLevel)
const condition = useOnlyCustomProps ? opts.customLevels : customLevelNames[minimumLevel] !== undefined
const minimum = (condition ? customLevelNames[minimumLevel] : LEVEL_NAMES[minimumLevel]) || Number(minimumLevel)
const level = log[levelKey === undefined ? LEVEL_KEY : levelKey]
if (level < minimum) return
}

const prettifiedMessage = prettifyMessage({ log, messageKey, colorizer, messageFormat, levelLabel })
const prettifiedMessage = prettifyMessage({ log, messageKey, colorizer, messageFormat, levelLabel, ...customProps, useOnlyCustomProps })

if (ignoreKeys) {
log = filterLog(log, ignoreKeys)
}

const prettifiedLevel = prettifyLevel({ log, colorizer, levelKey, prettifier: customPrettifiers.level, customLevels, customLevelNames })
const prettifiedLevel = prettifyLevel({ log, colorizer, levelKey, prettifier: customPrettifiers.level, ...customProps })
const prettifiedMetadata = prettifyMetadata({ log, prettifiers: customPrettifiers })
const prettifiedTime = prettifyTime({ log, translateFormat: opts.translateTime, timestampKey, prettifier: customPrettifiers.time })

Expand Down
67 changes: 40 additions & 27 deletions lib/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,39 +42,51 @@ function resolveCustomColoredColorizer (customColors) {
)
}

function colorizeLevel (level, colorizer, { customLevels, customLevelNames } = {}) {
const levels = customLevels || LEVELS
const levelNames = customLevelNames || LEVEL_NAMES

let levelNum = 'default'
if (Number.isInteger(+level)) {
levelNum = Object.prototype.hasOwnProperty.call(levels, level) ? level : levelNum
} else {
levelNum = Object.prototype.hasOwnProperty.call(levelNames, level.toLowerCase()) ? levelNames[level.toLowerCase()] : levelNum
}
function colorizeLevel (useOnlyCustomProps) {
return function (level, colorizer, { customLevels, customLevelNames } = {}) {
const levels = useOnlyCustomProps ? customLevels || LEVELS : Object.assign({}, LEVELS, customLevels)
const levelNames = useOnlyCustomProps ? customLevelNames || LEVEL_NAMES : Object.assign({}, LEVEL_NAMES, customLevelNames)

let levelNum = 'default'
if (Number.isInteger(+level)) {
levelNum = Object.prototype.hasOwnProperty.call(levels, level) ? level : levelNum
} else {
levelNum = Object.prototype.hasOwnProperty.call(levelNames, level.toLowerCase()) ? levelNames[level.toLowerCase()] : levelNum
}

const levelStr = levels[levelNum]
const levelStr = levels[levelNum]

return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr)
return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr)
}
}

function plainColorizer (level, opts) {
return colorizeLevel(level, plain, opts)
function plainColorizer (useOnlyCustomProps) {
const newPlainColorizer = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return newPlainColorizer(level, plain, opts)
}
customColoredColorizer.message = plain.message
customColoredColorizer.greyMessage = plain.greyMessage
return customColoredColorizer
}
plainColorizer.message = plain.message
plainColorizer.greyMessage = plain.greyMessage

function coloredColorizer (level, opts) {
return colorizeLevel(level, colored, opts)
function coloredColorizer (useOnlyCustomProps) {
const newColoredColorizer = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return newColoredColorizer(level, colored, opts)
}
customColoredColorizer.message = colored.message
customColoredColorizer.greyMessage = colored.greyMessage
return customColoredColorizer
}
coloredColorizer.message = colored.message
coloredColorizer.greyMessage = colored.greyMessage

function customColoredColorizerFactory (customColors) {
const customColored = resolveCustomColoredColorizer(customColors)
function customColoredColorizerFactory (customColors, useOnlyCustomProps) {
const onlyCustomColored = resolveCustomColoredColorizer(customColors)
const customColored = useOnlyCustomProps ? onlyCustomColored : Object.assign({}, colored, onlyCustomColored)
const colorizeLevelCustom = colorizeLevel(useOnlyCustomProps)

const customColoredColorizer = function (level, opts) {
return colorizeLevel(level, customColored, opts)
return colorizeLevelCustom(level, customColored, opts)
}
customColoredColorizer.message = customColoredColorizer.message || customColored.message
customColoredColorizer.greyMessage = customColoredColorizer.greyMessage || customColored.greyMessage
Expand All @@ -89,6 +101,7 @@ function customColoredColorizerFactory (customColors) {
* @param {boolean} [useColors=false] When `true` a function that applies standard
* terminal colors is returned.
* @param {array[]} [customColors] Touple where first item of each array is the level index and the second item is the color
* @param {boolean} [useOnlyCustomProps] When `true`, only use the provided custom colors provided and not fallback to default
*
* @returns {function} `function (level) {}` has a `.message(str)` method to
* apply colorization to a string. The core function accepts either an integer
Expand All @@ -97,12 +110,12 @@ function customColoredColorizerFactory (customColors) {
* colors as the integer `level` and will also default to `USERLVL` if the given
* string is not a recognized level name.
*/
module.exports = function getColorizer (useColors = false, customColors) {
module.exports = function getColorizer (useColors = false, customColors, useOnlyCustomProps) {
if (useColors && customColors !== undefined) {
return customColoredColorizerFactory(customColors)
return customColoredColorizerFactory(customColors, useOnlyCustomProps)
} else if (useColors) {
return coloredColorizer
return coloredColorizer(useOnlyCustomProps)
}

return plainColorizer
return plainColorizer(useOnlyCustomProps)
}
5 changes: 3 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,13 @@ function prettifyLevel ({ log, colorizer = defaultColorizer, levelKey = LEVEL_KE
* key is not a string, then `undefined` will be returned. Otherwise, a string
* that is the prettified message.
*/
function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL, levelKey = LEVEL_KEY, customLevels }) {
function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL, levelKey = LEVEL_KEY, customLevels, useOnlyCustomProps }) {
if (messageFormat && typeof messageFormat === 'string') {
const message = String(messageFormat).replace(/{([^{}]+)}/g, function (match, p1) {
// return log level as string instead of int
if (p1 === levelLabel && log[levelKey]) {
return customLevels === undefined ? LEVELS[log[levelKey]] : customLevels[log[levelKey]]
const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[log[levelKey]] === undefined
return condition ? LEVELS[log[levelKey]] : customLevels[log[levelKey]]
}
// Parse nested key access, e.g. `{keyA.subKeyB}`.
return p1.split('.').reduce(function (prev, curr) {
Expand Down
6 changes: 4 additions & 2 deletions test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,12 @@ test('basic prettifier tests', (t) => {
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const localHour = dateformat(epoch, 'HH')
const localMinute = dateformat(epoch, 'MM')
const localDate = dateformat(epoch, 'yyyy-mm-dd')
const offset = dateformat(epoch, 'o')
t.equal(
formatted,
`[${localDate} ${localHour}:35:28.992 ${offset}] INFO (${pid} on ${hostname}): foo\n`
`[${localDate} ${localHour}:${localMinute}:28.992 ${offset}] INFO (${pid} on ${hostname}): foo\n`
)
cb()
}
Expand All @@ -329,11 +330,12 @@ test('basic prettifier tests', (t) => {
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const localHour = dateformat(epoch, 'HH')
const localMinute = dateformat(epoch, 'MM')
const localDate = dateformat(epoch, 'yyyy/mm/dd')
const offset = dateformat(epoch, 'o')
t.equal(
formatted,
`[${localDate} ${localHour}:35:28 ${offset}] INFO (${pid} on ${hostname}): foo\n`
`[${localDate} ${localHour}:${localMinute}:28 ${offset}] INFO (${pid} on ${hostname}): foo\n`
)
cb()
}
Expand Down
92 changes: 92 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ test('cli', (t) => {
child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps false`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'false', optionName, 'custom:99,info:1'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] CUSTOM (42 on foo): hello world\n`)
})
child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps true`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'true', optionName, 'custom:99,info:1'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] CUSTOM (42 on foo): hello world\n`)
})
child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
t.teardown(() => child.kill())
})
})

;['--customColors', '-X'].forEach((optionName) => {
Expand Down Expand Up @@ -123,6 +147,74 @@ test('cli', (t) => {
})
})

;['--useOnlyCustomProps', '-U'].forEach((optionName) => {
t.test(`customize levels via ${optionName} false and customColors`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'false'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} true and customColors`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'true'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} true and customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:30', optionName, 'true'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] CUSTOM (42 on foo): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} true and no customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, 'true'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} false and customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:25', optionName, 'false'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})

t.test(`customize levels via ${optionName} false and no customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, 'false'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})

t.test('does ignore escaped keys', (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '-i', 'log\\.domain\\.corp/foo'], { env })
Expand Down
7 changes: 7 additions & 0 deletions test/lib/colors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const testCustomColoringColorizer = getColorizer => async t => {
}

const colorizer = getColorizer(true, customColors)
const colorizerWithCustomPropUse = getColorizer(true, customColors, true)
let colorized = colorizer(1, opts)
t.equal(colorized, '\u001B[31mERR\u001B[39m')

Expand All @@ -113,6 +114,12 @@ const testCustomColoringColorizer = getColorizer => async t => {

colorized = colorizer('use-default')
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')

colorized = colorizer(40, opts)
t.equal(colorized, '\u001B[33mWARN\u001B[39m')

colorized = colorizerWithCustomPropUse(50, opts)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
}

test('returns default colorizer - private export', testDefaultColorizer(getColorizerPrivate))
Expand Down

0 comments on commit 86715d8

Please sign in to comment.