Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Providing additional functionality to customPrettifiers and messageFormat #495

Merged
merged 7 commits into from Mar 20, 2024
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