Skip to content

Commit

Permalink
Providing additional functionality to customPrettifiers and messageFo…
Browse files Browse the repository at this point in the history
…rmat (#495)

* feat: Add label and colorized output for level customPrettifier func

Same implementation as #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 #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
  • Loading branch information
FoxxMD committed Mar 20, 2024
1 parent 7121303 commit 711720a
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 45 deletions.
51 changes: 30 additions & 21 deletions Readme.md
Expand Up @@ -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
{
Expand All @@ -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:

Expand All @@ -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]}`;
}
}
```
Expand Down
13 changes: 10 additions & 3 deletions index.d.ts
Expand Up @@ -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<string, unknown>;

Expand Down Expand Up @@ -179,7 +181,10 @@ interface PrettyOptions_ {
* }
* ```
*/
customPrettifiers?: Record<string, PinoPretty.Prettifier>;
customPrettifiers?: Record<string, PinoPretty.Prettifier> &
{
level?: PinoPretty.Prettifier<PinoPretty.LevelPrettifierExtras>
};
/**
* Change the level names and values to an user custom preset.
*
Expand All @@ -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<T = object> = (inputData: string | object, key: string, log: object, extras: PrettifierExtras<T>) => string;
type PrettifierExtras<T = object> = {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;
Expand Down
19 changes: 6 additions & 13 deletions lib/colors.js
@@ -1,7 +1,5 @@
'use strict'

const { LEVELS, LEVEL_NAMES } = require('./constants')

const nocolor = input => input
const plain = {
default: nocolor,
Expand All @@ -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

Expand Down Expand Up @@ -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)
}
Expand All @@ -67,6 +56,7 @@ function plainColorizer (useOnlyCustomProps) {
}
customColoredColorizer.message = plain.message
customColoredColorizer.greyMessage = plain.greyMessage
customColoredColorizer.colors = createColors({ useColor: false })
return customColoredColorizer
}

Expand All @@ -77,6 +67,7 @@ function coloredColorizer (useOnlyCustomProps) {
}
customColoredColorizer.message = colored.message
customColoredColorizer.greyMessage = colored.greyMessage
customColoredColorizer.colors = availableColors
return customColoredColorizer
}

Expand All @@ -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

Expand All @@ -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
*/

/**
Expand Down
29 changes: 29 additions & 0 deletions 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]
}
}
10 changes: 9 additions & 1 deletion lib/utils/index.js
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/utils/parse-factory-options.js
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -135,6 +138,7 @@ function parseFactoryOptions (options) {
customProperties,
errorLikeObjectKeys,
errorProps,
getLevelLabelData,
hideObject,
ignoreKeys,
includeKeys,
Expand Down
10 changes: 8 additions & 2 deletions lib/utils/prettify-level.js
Expand Up @@ -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
}
4 changes: 3 additions & 1 deletion lib/utils/prettify-level.test.js
Expand Up @@ -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')
Expand All @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/prettify-message.js
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions lib/utils/prettify-message.test.js
Expand Up @@ -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')
})
5 changes: 3 additions & 2 deletions lib/utils/prettify-object.js
Expand Up @@ -43,7 +43,8 @@ function prettifyObject ({
customPrettifiers,
errorLikeObjectKeys: errorLikeKeys,
objectColorizer,
singleLine
singleLine,
colorizer
} = context
const keysToIgnore = [].concat(skipKeys)

Expand All @@ -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
Expand Down

0 comments on commit 711720a

Please sign in to comment.