From 711720a69dd8baf7bd97f0024e0c8fb36cfab282 Mon Sep 17 00:00:00 2001 From: Matt Foxx Date: Wed, 20 Mar 2024 09:48:02 -0400 Subject: [PATCH] Providing additional functionality to customPrettifiers and messageFormat (#495) * feat: Add label and colorized output for level customPrettifier func Same implementation as pinojs/pino-pretty#493 but using object for extras argument for compatibility with future signature expansion * feat: Provide colorette object to message format function Enables users to use available colors based on `colorize` context of the pino-pretty instance Variant of pinojs/pino-pretty#494 that provides colors as extras object for compatibility with future customPrettifier extras argument * feat: Provide colorette object to prettifyObject All keys for customPrettifier, other than logs, now provide colors as property on an additional `extras` object for the prettifyObject function signature * feat: Add colors to level prettifier function signature * Modify level prettifier function signature to match other prettifiers * Since only the first argument was documented anyway this shouldn't be a breaking change. * docs: Update docs to showcase colors and standard customPrettifiers function signature * fix: Missing colors on colorizer when custom level colors are provided --- Readme.md | 51 ++++++++++++++++++------------ index.d.ts | 13 ++++++-- lib/colors.js | 19 ++++------- lib/utils/get-level-label-data.js | 29 +++++++++++++++++ lib/utils/index.js | 10 +++++- lib/utils/parse-factory-options.js | 4 +++ lib/utils/prettify-level.js | 10 ++++-- lib/utils/prettify-level.test.js | 4 ++- lib/utils/prettify-message.js | 2 +- lib/utils/prettify-message.test.js | 51 ++++++++++++++++++++++++++++++ lib/utils/prettify-object.js | 5 +-- lib/utils/prettify-object.test.js | 4 ++- test/basic.test.js | 42 ++++++++++++++++++++++++ test/types/pino-pretty.test-d.ts | 3 ++ 14 files changed, 202 insertions(+), 45 deletions(-) create mode 100644 lib/utils/get-level-label-data.js diff --git a/Readme.md b/Readme.md index 41a504a4..d09e66c3 100644 --- a/Readme.md +++ b/Readme.md @@ -297,8 +297,23 @@ const prettifyQuery = value => { } ``` +All prettifiers use this function signature: + +```js +['logObjKey']: (output, keyName, logObj, extras) => string +``` + +* `logObjKey` - name of the key of the property in the log object that should have this function applied to it +* `output` - the value of the property in the log object +* `keyName` - the name of the property (useful for `level` and `message` when `levelKey` or `messageKey` is used) +* `logObj` - the full log object, for context +* `extras` - an object containing **additional** data/functions created in the context of this pino-pretty logger or specific to the key (see `level` prettifying below) + * All `extras` objects contain `colors` which is a [Colorette](https://github.com/jorgebucaran/colorette?tab=readme-ov-file#supported-colors) object containing color functions. Colors are enabled based on `colorize` provided to pino-pretty or `colorette.isColorSupported` if `colorize` was not provided. + Additionally, `customPrettifiers` can be used to format the `time`, `hostname`, -`pid`, `name`, `caller` and `level` outputs: +`pid`, `name`, `caller` and `level` outputs AS WELL AS any arbitrary key-value that exists on a given log object. + +An example usage of `customPrettifiers` using all parameters from the function signature: ```js { @@ -311,29 +326,21 @@ Additionally, `customPrettifiers` can be used to format the `time`, `hostname`, // on if the levelKey option is used or not. // By default this will be the same numerics as the Pino default: level: logLevel => `LEVEL: ${logLevel}`, + // level provides additional data in `extras`: + // * label => derived level label string + // * labelColorized => derived level label string with colorette colors applied based on customColors and whether colors are supported + level: (logLevel, key, log, { label, labelColorized, colors }) => `LEVEL: ${logLevel} LABEL: ${levelLabel} COLORIZED LABEL: ${labelColorized}`, // other prettifiers can be used for the other keys if needed, for example - hostname: hostname => colorGreen(hostname), - pid: pid => colorRed(pid), - name: name => colorBlue(name), - caller: caller => colorCyan(caller) + hostname: hostname => `MY HOST: ${hostname}`, + pid: pid => pid, + name: (name, key, log, { colors }) => `${colors.blue(name)}`, + caller: (caller, key, log, { colors }) => `${colors.greenBright(caller)}`, + myCustomLogProp: (value, key, log, { colors }) => `My Prop -> ${colors.bold(value)} <--` } } ``` -Note that prettifiers do not include any coloring, if the stock coloring on -`level` is desired, it can be accomplished using the following: - -```js -const { colorizerFactory } = require('pino-pretty') -const levelColorize = colorizerFactory(true) -const levelPrettifier = logLevel => `LEVEL: ${levelColorize(logLevel)}` -//... -{ - customPrettifiers: { level: levelPrettifier } -} -``` - `messageFormat` option allows you to customize the message output. A template `string` like this can define the format: @@ -352,13 +359,15 @@ Else statements and nested conditions are not supported. } ``` -This option can also be defined as a `function` with this prototype: +This option can also be defined as a `function` with this function signature: ```js { - messageFormat: (log, messageKey, levelLabel) => { + messageFormat: (log, messageKey, levelLabel, { colors }) => { // do some log message customization - return customized_message; + // + // `colors` is a Colorette object with colors enabled based on `colorize` option + return `This is a ${color.red('colorized')}, custom message: ${log[messageKey]}`; } } ``` diff --git a/index.d.ts b/index.d.ts index 614d3750..07dc628d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,6 +10,8 @@ import { Transform } from 'stream'; import { OnUnknown } from 'pino-abstract-transport'; // @ts-ignore fall back to any if pino is not available, i.e. when running pino tests import { DestinationStream, Level } from 'pino'; +import LevelPrettifierExtras = PinoPretty.LevelPrettifierExtras; +import * as Colorette from "colorette"; type LogDescriptor = Record; @@ -179,7 +181,10 @@ interface PrettyOptions_ { * } * ``` */ - customPrettifiers?: Record; + customPrettifiers?: Record & + { + level?: PinoPretty.Prettifier + }; /** * Change the level names and values to an user custom preset. * @@ -204,8 +209,10 @@ interface PrettyOptions_ { declare function build(options: PrettyOptions_): PinoPretty.PrettyStream; declare namespace PinoPretty { - type Prettifier = (inputData: string | object) => string; - type MessageFormatFunc = (log: LogDescriptor, messageKey: string, levelLabel: string) => string; + type Prettifier = (inputData: string | object, key: string, log: object, extras: PrettifierExtras) => string; + type PrettifierExtras = {colors: Colorette.Colorette} & T; + type LevelPrettifierExtras = {label: string, labelColorized: string} + type MessageFormatFunc = (log: LogDescriptor, messageKey: string, levelLabel: string, extras: PrettifierExtras) => string; type PrettyOptions = PrettyOptions_; type PrettyStream = Transform & OnUnknown; type ColorizerFactory = typeof colorizerFactory; diff --git a/lib/colors.js b/lib/colors.js index 5bb38042..f135c338 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -1,7 +1,5 @@ 'use strict' -const { LEVELS, LEVEL_NAMES } = require('./constants') - const nocolor = input => input const plain = { default: nocolor, @@ -16,6 +14,7 @@ const plain = { } const { createColors } = require('colorette') +const getLevelLabelData = require('./utils/get-level-label-data') const availableColors = createColors({ useColor: true }) const { white, bgRed, red, yellow, green, blue, gray, cyan } = availableColors @@ -44,17 +43,7 @@ function resolveCustomColoredColorizer (customColors) { 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, levelNum] = getLevelLabelData(useOnlyCustomProps, customLevels, customLevelNames)(level) return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr) } @@ -67,6 +56,7 @@ function plainColorizer (useOnlyCustomProps) { } customColoredColorizer.message = plain.message customColoredColorizer.greyMessage = plain.greyMessage + customColoredColorizer.colors = createColors({ useColor: false }) return customColoredColorizer } @@ -77,6 +67,7 @@ function coloredColorizer (useOnlyCustomProps) { } customColoredColorizer.message = colored.message customColoredColorizer.greyMessage = colored.greyMessage + customColoredColorizer.colors = availableColors return customColoredColorizer } @@ -88,6 +79,7 @@ function customColoredColorizerFactory (customColors, useOnlyCustomProps) { const customColoredColorizer = function (level, opts) { return colorizeLevelCustom(level, customColored, opts) } + customColoredColorizer.colors = availableColors customColoredColorizer.message = customColoredColorizer.message || customColored.message customColoredColorizer.greyMessage = customColoredColorizer.greyMessage || customColored.greyMessage @@ -105,6 +97,7 @@ function customColoredColorizerFactory (customColors, useOnlyCustomProps) { * recognized. * @property {function} message Accepts one string parameter that will be * colorized to a predefined color. + * @property {Colorette.Colorette} colors Available color functions based on `useColor` (or `colorize`) context */ /** diff --git a/lib/utils/get-level-label-data.js b/lib/utils/get-level-label-data.js new file mode 100644 index 00000000..5b27cabb --- /dev/null +++ b/lib/utils/get-level-label-data.js @@ -0,0 +1,29 @@ +'use strict' + +module.exports = getLevelLabelData +const { LEVELS, LEVEL_NAMES } = require('../constants') + +/** + * Given initial settings for custom levels/names and use of only custom props + * get the level label that corresponds with a given level number + * + * @param {boolean} useOnlyCustomProps + * @param {object} customLevels + * @param {object} customLevelNames + * + * @returns {function} A function that takes a number level and returns the level's label string + */ +function getLevelLabelData (useOnlyCustomProps, customLevels, customLevelNames) { + const levels = useOnlyCustomProps ? customLevels || LEVELS : Object.assign({}, LEVELS, customLevels) + const levelNames = useOnlyCustomProps ? customLevelNames || LEVEL_NAMES : Object.assign({}, LEVEL_NAMES, customLevelNames) + return function (level) { + 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 + } + + return [levels[levelNum], levelNum] + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index ec69d968..e083d737 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -22,7 +22,8 @@ module.exports = { prettifyMetadata: require('./prettify-metadata.js'), prettifyObject: require('./prettify-object.js'), prettifyTime: require('./prettify-time.js'), - splitPropertyKey: require('./split-property-key.js') + splitPropertyKey: require('./split-property-key.js'), + getLevelLabelData: require('./get-level-label-data') } // The remainder of this file consists of jsdoc blocks that are difficult to @@ -78,6 +79,12 @@ module.exports = { * `{levelLabel} - {if pid}{pid} - {end}url:{req.url}` */ +/** + * @typedef {object} PrettifyMessageExtras + * @property {object} colors Available color functions based on `useColor` (or `colorize`) context + * the options. + */ + /** * A function that accepts a log object, name of the message key, and name of * the level label key and returns a formatted log line. @@ -90,6 +97,7 @@ module.exports = { * contains the log message. * @param {string} levelLabel The name of the key in the `log` object that * contains the log level name. + * @param {PrettifyMessageExtras} extras Additional data available for message context * @returns {string} * * @example diff --git a/lib/utils/parse-factory-options.js b/lib/utils/parse-factory-options.js index 33ffb374..0b8ec748 100644 --- a/lib/utils/parse-factory-options.js +++ b/lib/utils/parse-factory-options.js @@ -8,6 +8,7 @@ const { const colors = require('../colors') const handleCustomLevelsOpts = require('./handle-custom-levels-opts') const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts') +const handleLevelLabelData = require('./get-level-label-data') /** * A `PrettyContext` is an object to be used by the various functions that @@ -32,6 +33,7 @@ const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts') * should be considered as holding error objects. * @property {string[]} errorProps A list of error object keys that should be * included in the output. + * @property {function} getLevelLabelData Pass a numeric level to return [levelLabelString,levelNum] * @property {boolean} hideObject Indicates the prettifier should omit objects * in the output. * @property {string[]} ignoreKeys Set of log data keys to omit. @@ -84,6 +86,7 @@ function parseFactoryOptions (options) { : (options.useOnlyCustomProps === 'true') const customLevels = handleCustomLevelsOpts(options.customLevels) const customLevelNames = handleCustomLevelsNamesOpts(options.customLevels) + const getLevelLabelData = handleLevelLabelData(useOnlyCustomProps, customLevels, customLevelNames) let customColors if (options.customColors) { @@ -135,6 +138,7 @@ function parseFactoryOptions (options) { customProperties, errorLikeObjectKeys, errorProps, + getLevelLabelData, hideObject, ignoreKeys, includeKeys, diff --git a/lib/utils/prettify-level.js b/lib/utils/prettify-level.js index 213ba06c..2e4dce84 100644 --- a/lib/utils/prettify-level.js +++ b/lib/utils/prettify-level.js @@ -26,10 +26,16 @@ function prettifyLevel ({ log, context }) { colorizer, customLevels, customLevelNames, - levelKey + levelKey, + getLevelLabelData } = context const prettifier = context.customPrettifiers?.level const output = getPropertyValue(log, levelKey) if (output === undefined) return undefined - return prettifier ? prettifier(output) : colorizer(output, { customLevels, customLevelNames }) + const labelColorized = colorizer(output, { customLevels, customLevelNames }) + if (prettifier) { + const [label] = getLevelLabelData(output) + return prettifier(output, levelKey, log, { label, labelColorized, colors: colorizer.colors }) + } + return labelColorized } diff --git a/lib/utils/prettify-level.test.js b/lib/utils/prettify-level.test.js index e735b0a9..e6e5a3c1 100644 --- a/lib/utils/prettify-level.test.js +++ b/lib/utils/prettify-level.test.js @@ -3,6 +3,7 @@ const tap = require('tap') const prettifyLevel = require('./prettify-level') const getColorizer = require('../colors') +const getLevelLabelData = require('./get-level-label-data') const { LEVEL_KEY } = require('../constants') @@ -12,7 +13,8 @@ const context = { customLevelNames: undefined, customLevels: undefined, levelKey: LEVEL_KEY, - customPrettifiers: undefined + customPrettifiers: undefined, + getLevelLabelData: getLevelLabelData(false, {}, {}) } tap.test('returns `undefined` for unknown level', async t => { diff --git a/lib/utils/prettify-message.js b/lib/utils/prettify-message.js index 7758f1d8..ee2f3dc8 100644 --- a/lib/utils/prettify-message.js +++ b/lib/utils/prettify-message.js @@ -54,7 +54,7 @@ function prettifyMessage ({ log, context }) { return colorizer.message(message) } if (messageFormat && typeof messageFormat === 'function') { - const msg = messageFormat(log, messageKey, levelLabel) + const msg = messageFormat(log, messageKey, levelLabel, { colors: colorizer.colors }) return colorizer.message(msg) } if (messageKey in log === false) return undefined diff --git a/lib/utils/prettify-message.test.js b/lib/utils/prettify-message.test.js index 8faf4b12..dadf8b12 100644 --- a/lib/utils/prettify-message.test.js +++ b/lib/utils/prettify-message.test.js @@ -185,3 +185,54 @@ tap.test('`messageFormat` supports function definition', async t => { }) t.equal(str, '--> localhost/test') }) + +tap.test('`messageFormat` supports function definition with colorizer object', async t => { + const colorizer = getColorizer(true) + const str = prettifyMessage({ + log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' }, + context: { + ...context, + colorizer, + messageFormat: (log, messageKey, levelLabel, { colors }) => { + let msg = log[messageKey] + if (msg === 'incoming request') msg = `--> ${colors.red(log.request.url)}` + return msg + } + } + }) + t.equal(str, '\u001B[36m--> \u001B[31mlocalhost/test\u001B[36m\u001B[39m') +}) + +tap.test('`messageFormat` supports function definition with colorizer object when using custom colors', async t => { + const colorizer = getColorizer(true, [[30, 'brightGreen']], false) + const str = prettifyMessage({ + log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' }, + context: { + ...context, + colorizer, + messageFormat: (log, messageKey, levelLabel, { colors }) => { + let msg = log[messageKey] + if (msg === 'incoming request') msg = `--> ${colors.red(log.request.url)}` + return msg + } + } + }) + t.equal(str, '\u001B[36m--> \u001B[31mlocalhost/test\u001B[36m\u001B[39m') +}) + +tap.test('`messageFormat` supports function definition with colorizer object when no color is supported', async t => { + const colorizer = getColorizer(false) + const str = prettifyMessage({ + log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' }, + context: { + ...context, + colorizer, + messageFormat: (log, messageKey, levelLabel, { colors }) => { + let msg = log[messageKey] + if (msg === 'incoming request') msg = `--> ${colors.red(log.request.url)}` + return msg + } + } + }) + t.equal(str, '--> localhost/test') +}) diff --git a/lib/utils/prettify-object.js b/lib/utils/prettify-object.js index e4bbec1c..5de0f5cb 100644 --- a/lib/utils/prettify-object.js +++ b/lib/utils/prettify-object.js @@ -43,7 +43,8 @@ function prettifyObject ({ customPrettifiers, errorLikeObjectKeys: errorLikeKeys, objectColorizer, - singleLine + singleLine, + colorizer } = context const keysToIgnore = [].concat(skipKeys) @@ -57,7 +58,7 @@ function prettifyObject ({ if (keysToIgnore.includes(k) === false) { // Pre-apply custom prettifiers, because all 3 cases below will need this const pretty = typeof customPrettifiers[k] === 'function' - ? customPrettifiers[k](v, k, log) + ? customPrettifiers[k](v, k, log, { colors: colorizer.colors }) : v if (errorLikeKeys.includes(k)) { errors[k] = pretty diff --git a/lib/utils/prettify-object.test.js b/lib/utils/prettify-object.test.js index 61f64244..2dde4a9a 100644 --- a/lib/utils/prettify-object.test.js +++ b/lib/utils/prettify-object.test.js @@ -6,6 +6,7 @@ const prettifyObject = require('./prettify-object') const { ERROR_LIKE_KEYS } = require('../constants') +const getColorizer = require('../colors') const context = { EOL: '\n', @@ -13,7 +14,8 @@ const context = { customPrettifiers: {}, errorLikeObjectKeys: ERROR_LIKE_KEYS, objectColorizer: colors(), - singleLine: false + singleLine: false, + colorizer: getColorizer() } tap.test('returns empty string if no properties present', async t => { diff --git a/test/basic.test.js b/test/basic.test.js index 9986811c..d8307b58 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -256,6 +256,48 @@ test('basic prettifier tests', (t) => { log.info({ msg: 'foo', bar: 'warn' }) }) + t.test('can use a customPrettifier to get final level label (no color)', (t) => { + t.plan(1) + const customPrettifiers = { + level: (level, key, logThis, { label }) => { + return `LEVEL: ${label}` + } + } + const pretty = prettyFactory({ customPrettifiers, colorize: false, useOnlyCustomProps: false }) + const log = pino({}, new Writable({ + write (chunk, enc, cb) { + const formatted = pretty(chunk.toString()) + t.equal( + formatted, + `[${formattedEpoch}] LEVEL: INFO (${pid}): foo\n` + ) + cb() + } + })) + log.info({ msg: 'foo' }) + }) + + t.test('can use a customPrettifier to get final level label (colorized)', (t) => { + t.plan(1) + const customPrettifiers = { + level: (level, key, logThis, { label, labelColorized }) => { + return `LEVEL: ${labelColorized}` + } + } + const pretty = prettyFactory({ customPrettifiers, colorize: true, useOnlyCustomProps: false }) + const log = pino({}, new Writable({ + write (chunk, enc, cb) { + const formatted = pretty(chunk.toString()) + t.equal( + formatted, + `[${formattedEpoch}] LEVEL: INFO (${pid}): foo\n` + ) + cb() + } + })) + log.info({ msg: 'foo' }) + }) + t.test('can use a customPrettifier on name output', (t) => { t.plan(1) const customPrettifiers = { diff --git a/test/types/pino-pretty.test-d.ts b/test/types/pino-pretty.test-d.ts index 75f4d31e..ee33506e 100644 --- a/test/types/pino-pretty.test-d.ts +++ b/test/types/pino-pretty.test-d.ts @@ -32,6 +32,9 @@ const options: PinoPretty.PrettyOptions = { customPrettifiers: { key: (value) => { return value.toString().toUpperCase(); + }, + level: (level, label, colorized) => { + return level.toString(); } }, customLevels: 'verbose:5',