From b34e7c70e4081eff577249ac232372da41e80598 Mon Sep 17 00:00:00 2001 From: James Sumners <321201+jsumners@users.noreply.github.com> Date: Sun, 3 Sep 2023 07:22:06 -0400 Subject: [PATCH] Refactor functions to use context object (#452) --- index.js | 266 ++++++--------------------- lib/colors.js | 21 ++- lib/pretty.js | 169 +++++++++++++++++ lib/utils/filter-log.js | 9 +- lib/utils/filter-log.test.js | 95 ++++++++-- lib/utils/parse-factory-options.js | 153 +++++++++++++++ lib/utils/prettify-error-log.js | 41 ++--- lib/utils/prettify-error-log.test.js | 63 ++++++- lib/utils/prettify-level.js | 32 +--- lib/utils/prettify-level.test.js | 45 ++++- lib/utils/prettify-message.js | 40 ++-- lib/utils/prettify-message.test.js | 128 +++++++++++-- lib/utils/prettify-metadata.js | 9 +- lib/utils/prettify-metadata.test.js | 39 ++-- lib/utils/prettify-object.js | 44 ++--- lib/utils/prettify-object.test.js | 82 +++++++-- lib/utils/prettify-time.js | 26 +-- lib/utils/prettify-time.test.js | 159 +++++++++++++--- test/error-objects.test.js | 4 +- 19 files changed, 981 insertions(+), 444 deletions(-) create mode 100644 lib/pretty.js create mode 100644 lib/utils/parse-factory-options.js diff --git a/index.js b/index.js index 812da8fb..141fc8f2 100644 --- a/index.js +++ b/index.js @@ -4,30 +4,19 @@ const { isColorSupported } = require('colorette') const pump = require('pump') const { Transform } = require('readable-stream') const abstractTransport = require('pino-abstract-transport') -const sjs = require('secure-json-parse') const colors = require('./lib/colors') -const { ERROR_LIKE_KEYS, MESSAGE_KEY, TIMESTAMP_KEY, LEVEL_KEY, LEVEL_NAMES } = require('./lib/constants') const { - isObject, - prettifyErrorLog, - prettifyLevel, - prettifyMessage, - prettifyMetadata, - prettifyObject, - prettifyTime, + ERROR_LIKE_KEYS, + LEVEL_KEY, + LEVEL_LABEL, + MESSAGE_KEY, + TIMESTAMP_KEY +} = require('./lib/constants') +const { buildSafeSonicBoom, - filterLog, - handleCustomLevelsOpts, - handleCustomLevelsNamesOpts + parseFactoryOptions } = require('./lib/utils') - -const jsonParser = input => { - try { - return { value: sjs.parse(input, { protoAction: 'remove' }) } - } catch (err) { - return { err } - } -} +const pretty = require('./lib/pretty') /** * @typedef {object} PinoPrettyOptions @@ -37,42 +26,50 @@ const jsonParser = input => { * @property {boolean} [colorizeObjects=true] Apply coloring to rendered objects * when coloring is enabled. * @property {boolean} [crlf=false] End lines with `\r\n` instead of `\n`. + * @property {string|null} [customColors=null] A comma separated list of colors + * to use for specific level labels, e.g. `err:red,info:blue`. + * @property {string|null} [customLevels=null] A comma separated list of user + * defined level names and numbers, e.g. `err:99,info:1`. + * @property {CustomPrettifiers} [customPrettifiers={}] A set of prettifier + * functions to apply to keys defined in this object. * @property {K_ERROR_LIKE_KEYS} [errorLikeObjectKeys] A list of string property * names to consider as error objects. * @property {string} [errorProps=''] A comma separated list of properties on * error objects to include in the output. - * @property {string|null} [customLevels=null] A comma separated list of user - * defined level names and numbers, e.g. `err:99,info:1`. - * @property {string|null} [customColors=null] A comma separated list of colors - * to use for specific level labels, e.g. `err:red,info:blue`. - * @property {boolean} [useOnlyCustomProps=true] When true, only custom levels - * and colors will be used if they have been provided. + * @property {boolean} [hideObject=false] When `true`, data objects will be + * omitted from the output (except for error objects). + * @property {string} [ignore='hostname'] A comma separated list of log keys + * to omit when outputting the prettified log information. + * @property {undefined|string} [include=undefined] A comma separated list of + * log keys to include in the prettified log information. Only the keys in this + * list will be included in the output. * @property {boolean} [levelFirst=false] When true, the log level will be the * first field in the prettified output. - * @property {string} [messageKey='msg'] Defines the key in incoming logs that - * contains the message of the log, if present. + * @property {string} [levelKey='level'] The key name in the log data that + * contains the level value for the log. + * @property {string} [levelLabel='levelLabel'] Token name to use in + * `messageFormat` to represent the name of the logged level. * @property {null|MessageFormatString|MessageFormatFunction} [messageFormat=null] * When a string, defines how the prettified line should be formatted according * to defined tokens. When a function, a synchronous function that returns a * formatted string. - * @property {string} [timestampKey='time'] Defines the key in incoming logs - * that contains the timestamp of the log, if present. - * @property {boolean} [translateTime=true] When true, will translate a - * JavaScript date integer into a human-readable string. + * @property {string} [messageKey='msg'] Defines the key in incoming logs that + * contains the message of the log, if present. + * @property {undefined|string|number} [minimumLevel=undefined] The minimum + * level for logs that should be processed. Any logs below this level will + * be omitted. * @property {object} [outputStream=process.stdout] The stream to write * prettified log lines to. - * @property {CustomPrettifiers} [customPrettifiers={}] A set of prettifier - * functions to apply to keys defined in this object. - * @property {boolean} [hideObject=false] When `true`, data objects will be - * omitted from the output (except for error objects). - * @property {string} [ignore='hostname'] A comma separated list of log keys - * to omit when outputting the prettified log information. - * @property {undefined|string} [include=undefined] A comma separated list of - * log keys to include in the prettified log information. Only the keys in this - * list will be included in the output. * @property {boolean} [singleLine=false] When `true` any objects, except error * objects, in the log data will be printed as a single line instead as multiple * lines. + * @property {string} [timestampKey='time'] Defines the key in incoming logs + * that contains the timestamp of the log, if present. + * @property {boolean|string} [translateTime=true] When true, will translate a + * JavaScript date integer into a human-readable string. If set to a string, + * it must be a format string. + * @property {boolean} [useOnlyCustomProps=true] When true, only custom levels + * and colors will be used if they have been provided. */ /** @@ -84,22 +81,25 @@ const defaultOptions = { colorize: isColorSupported, colorizeObjects: true, crlf: false, + customColors: null, + customLevels: null, + customPrettifiers: {}, errorLikeObjectKeys: ERROR_LIKE_KEYS, errorProps: '', - customLevels: null, - customColors: null, - useOnlyCustomProps: true, + hideObject: false, + ignore: 'hostname', + include: undefined, levelFirst: false, - messageKey: MESSAGE_KEY, + levelKey: LEVEL_KEY, + levelLabel: LEVEL_LABEL, messageFormat: null, + messageKey: MESSAGE_KEY, + minimumLevel: undefined, + outputStream: process.stdout, + singleLine: false, timestampKey: TIMESTAMP_KEY, translateTime: true, - outputStream: process.stdout, - customPrettifiers: {}, - hideObject: false, - ignore: 'hostname', - include: undefined, - singleLine: false + useOnlyCustomProps: true } /** @@ -110,168 +110,8 @@ const defaultOptions = { * @returns {LogPrettifierFunc} */ function prettyFactory (options) { - const opts = Object.assign({}, defaultOptions, options) - const EOL = opts.crlf ? '\r\n' : '\n' - const IDENT = ' ' - const messageKey = opts.messageKey - const levelKey = opts.levelKey - const levelLabel = opts.levelLabel - const minimumLevel = opts.minimumLevel - const messageFormat = opts.messageFormat - 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 = handleCustomLevelsOpts(opts.customLevels) - const customLevelNames = handleCustomLevelsNamesOpts(opts.customLevels) - - const customColors = opts.customColors - ? opts.customColors - .split(',') - .reduce((agg, value) => { - const [level, color] = value.split(':') - - 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 includeKeys = opts.include !== undefined ? new Set(opts.include.split(',')) : undefined - const ignoreKeys = (!includeKeys && opts.ignore) ? new Set(opts.ignore.split(',')) : undefined - const hideObject = opts.hideObject - const singleLine = opts.singleLine - const colorizer = colors(opts.colorize, customColors, useOnlyCustomProps) - const objectColorizer = opts.colorizeObjects ? colorizer : colors(false, [], false) - - return pretty - - /** - * Orchestrates processing the received log data according to the provided - * configuration and returns a prettified log string. - * - * @typedef {function} LogPrettifierFunc - * @param {string|object} inputData A log string or a log-like object. - * @returns {string} A string that represents the prettified log data. - */ - function pretty (inputData) { - let log - if (!isObject(inputData)) { - const parsed = jsonParser(inputData) - if (parsed.err || !isObject(parsed.value)) { - // pass through - return inputData + EOL - } - log = parsed.value - } else { - log = inputData - } - - if (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, ...customProps, useOnlyCustomProps }) - - if (ignoreKeys || includeKeys) { - log = filterLog({ log, ignoreKeys, includeKeys }) - } - - 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 }) - - let line = '' - if (opts.levelFirst && prettifiedLevel) { - line = `${prettifiedLevel}` - } - - if (prettifiedTime && line === '') { - line = `${prettifiedTime}` - } else if (prettifiedTime) { - line = `${line} ${prettifiedTime}` - } - - if (!opts.levelFirst && prettifiedLevel) { - if (line.length > 0) { - line = `${line} ${prettifiedLevel}` - } else { - line = prettifiedLevel - } - } - - if (prettifiedMetadata) { - if (line.length > 0) { - line = `${line} ${prettifiedMetadata}:` - } else { - line = prettifiedMetadata - } - } - - if (line.endsWith(':') === false && line !== '') { - line += ':' - } - - if (prettifiedMessage !== undefined) { - if (line.length > 0) { - line = `${line} ${prettifiedMessage}` - } else { - line = prettifiedMessage - } - } - - if (line.length > 0 && !singleLine) { - line += EOL - } - - // pino@7+ does not log this anymore - if (log.type === 'Error' && log.stack) { - const prettifiedErrorLog = prettifyErrorLog({ - log, - errorLikeKeys: errorLikeObjectKeys, - errorProperties: errorProps, - ident: IDENT, - eol: EOL - }) - if (singleLine) line += EOL - line += prettifiedErrorLog - } else if (!hideObject) { - const skipKeys = [messageKey, levelKey, timestampKey].filter(key => typeof log[key] === 'string' || typeof log[key] === 'number' || typeof log[key] === 'boolean') - const prettifiedObject = prettifyObject({ - input: log, - skipKeys, - customPrettifiers, - errorLikeKeys: errorLikeObjectKeys, - eol: EOL, - ident: IDENT, - singleLine, - colorizer: objectColorizer - }) - - // In single line mode, include a space only if prettified version isn't empty - if (singleLine && !/^\s$/.test(prettifiedObject)) { - line += ' ' - } - line += prettifiedObject - } - - return line - } + const context = parseFactoryOptions(Object.assign({}, defaultOptions, options)) + return pretty.bind({ ...context, context }) } /** diff --git a/lib/colors.js b/lib/colors.js index f6a20b15..5bb38042 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -94,16 +94,31 @@ function customColoredColorizerFactory (customColors, useOnlyCustomProps) { return customColoredColorizer } +/** + * Applies colorization, if possible, to a string representing the passed in + * `level`. For example, the default colorizer will return a "green" colored + * string for the "info" level. + * + * @typedef {function} ColorizerFunc + * @param {string|number} level In either case, the input will map to a color + * for the specified level or to the color for `USERLVL` if the level is not + * recognized. + * @property {function} message Accepts one string parameter that will be + * colorized to a predefined color. + */ + /** * Factory function get a function to colorized levels. The returned function * also includes a `.message(str)` method to colorize strings. * * @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 + * @param {array[]} [customColors] Tuple 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 + * @returns {ColorizerFunc} `function (level) {}` has a `.message(str)` method to * apply colorization to a string. The core function accepts either an integer * `level` or a `string` level. The integer level will map to a known level * string or to `USERLVL` if not known. The string `level` will map to the same diff --git a/lib/pretty.js b/lib/pretty.js new file mode 100644 index 00000000..ebb43800 --- /dev/null +++ b/lib/pretty.js @@ -0,0 +1,169 @@ +'use strict' + +module.exports = pretty + +const sjs = require('secure-json-parse') + +const isObject = require('./utils/is-object') +const prettifyErrorLog = require('./utils/prettify-error-log') +const prettifyLevel = require('./utils/prettify-level') +const prettifyMessage = require('./utils/prettify-message') +const prettifyMetadata = require('./utils/prettify-metadata') +const prettifyObject = require('./utils/prettify-object') +const prettifyTime = require('./utils/prettify-time') +const filterLog = require('./utils/filter-log') + +const { + LEVELS, + LEVEL_KEY, + LEVEL_NAMES +} = require('./constants') + +const jsonParser = input => { + try { + return { value: sjs.parse(input, { protoAction: 'remove' }) } + } catch (err) { + return { err } + } +} + +/** + * Orchestrates processing the received log data according to the provided + * configuration and returns a prettified log string. + * + * @typedef {function} LogPrettifierFunc + * @param {string|object} inputData A log string or a log-like object. + * @returns {string} A string that represents the prettified log data. + */ +function pretty (inputData) { + let log + if (!isObject(inputData)) { + const parsed = jsonParser(inputData) + if (parsed.err || !isObject(parsed.value)) { + // pass through + return inputData + this.EOL + } + log = parsed.value + } else { + log = inputData + } + + if (this.minimumLevel) { + // We need to figure out if the custom levels has the desired minimum + // level & use that one if found. If not, determine if the level exists + // in the standard levels. In both cases, make sure we have the level + // number instead of the level name. + let condition + if (this.useOnlyCustomProps) { + condition = this.customLevels + } else { + condition = this.customLevelNames[this.minimumLevel] !== undefined + } + let minimum + if (condition) { + minimum = this.customLevelNames[this.minimumLevel] + } else { + minimum = LEVEL_NAMES[this.minimumLevel] + } + if (!minimum) { + minimum = typeof this.minimumLevel === 'string' + ? LEVEL_NAMES[this.minimumLevel] + : LEVEL_NAMES[LEVELS[this.minimumLevel].toLowerCase()] + } + + const level = log[this.levelKey === undefined ? LEVEL_KEY : this.levelKey] + if (level < minimum) return + } + + const prettifiedMessage = prettifyMessage({ log, context: this.context }) + + if (this.ignoreKeys || this.includeKeys) { + log = filterLog({ log, context: this.context }) + } + + const prettifiedLevel = prettifyLevel({ + log, + context: { + ...this.context, + // This is odd. The colorizer ends up relying on the value of + // `customProperties` instead of the original `customLevels` and + // `customLevelNames`. + ...this.context.customProperties + } + }) + const prettifiedMetadata = prettifyMetadata({ log, context: this.context }) + const prettifiedTime = prettifyTime({ log, context: this.context }) + + let line = '' + if (this.levelFirst && prettifiedLevel) { + line = `${prettifiedLevel}` + } + + if (prettifiedTime && line === '') { + line = `${prettifiedTime}` + } else if (prettifiedTime) { + line = `${line} ${prettifiedTime}` + } + + if (!this.levelFirst && prettifiedLevel) { + if (line.length > 0) { + line = `${line} ${prettifiedLevel}` + } else { + line = prettifiedLevel + } + } + + if (prettifiedMetadata) { + if (line.length > 0) { + line = `${line} ${prettifiedMetadata}:` + } else { + line = prettifiedMetadata + } + } + + if (line.endsWith(':') === false && line !== '') { + line += ':' + } + + if (prettifiedMessage !== undefined) { + if (line.length > 0) { + line = `${line} ${prettifiedMessage}` + } else { + line = prettifiedMessage + } + } + + if (line.length > 0 && !this.singleLine) { + line += this.EOL + } + + // pino@7+ does not log this anymore + if (log.type === 'Error' && log.stack) { + const prettifiedErrorLog = prettifyErrorLog({ log, context: this.context }) + if (this.singleLine) line += this.EOL + line += prettifiedErrorLog + } else if (this.hideObject === false) { + const skipKeys = [ + this.messageKey, + this.levelKey, + this.timestampKey + ].filter(key => { + return typeof log[key] === 'string' || + typeof log[key] === 'number' || + typeof log[key] === 'boolean' + }) + const prettifiedObject = prettifyObject({ + log, + skipKeys, + context: this.context + }) + + // In single line mode, include a space only if prettified version isn't empty + if (this.singleLine && !/^\s$/.test(prettifiedObject)) { + line += ' ' + } + line += prettifiedObject + } + + return line +} diff --git a/lib/utils/filter-log.js b/lib/utils/filter-log.js index 01a8c6c9..66c1b354 100644 --- a/lib/utils/filter-log.js +++ b/lib/utils/filter-log.js @@ -10,10 +10,8 @@ const deleteLogProperty = require('./delete-log-property') /** * @typedef {object} FilterLogParams * @property {object} log The log object to be modified. - * @property {Set | Array | undefined} ignoreKeys - * An array of strings identifying the properties to be removed. - * @property {Set | Array | undefined} includeKeys - * An array of strings identifying the properties to be included. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -27,7 +25,8 @@ const deleteLogProperty = require('./delete-log-property') * either only includes the keys in ignoreKeys * or does not include those in ignoredKeys. */ -function filterLog ({ log, ignoreKeys, includeKeys }) { +function filterLog ({ log, context }) { + const { ignoreKeys, includeKeys } = context const logCopy = fastCopy(log) if (includeKeys) { diff --git a/lib/utils/filter-log.test.js b/lib/utils/filter-log.test.js index 3ba54298..2099661a 100644 --- a/lib/utils/filter-log.test.js +++ b/lib/utils/filter-log.test.js @@ -3,6 +3,10 @@ const tap = require('tap') const filterLog = require('./filter-log') +const context = { + includeKeys: undefined, + ignoreKeys: undefined +} const logData = { level: 30, time: 1522431328992, @@ -20,27 +24,57 @@ const logData2 = Object.assign({ tap.test('#filterLog with an ignoreKeys option', t => { t.test('filterLog removes single entry', async t => { - const result = filterLog({ log: logData, ignoreKeys: ['data1.data2.data-3'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys: ['data1.data2.data-3'] + } + }) t.same(result, { level: 30, time: 1522431328992, data1: { data2: { }, error: new Error('test') } }) }) t.test('filterLog removes multiple entries', async t => { - const result = filterLog({ log: logData, ignoreKeys: ['time', 'data1'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys: ['time', 'data1'] + } + }) t.same(result, { level: 30 }) }) t.test('filterLog keeps error instance', async t => { - const result = filterLog({ log: logData, ignoreKeys: [] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys: [] + } + }) t.equal(logData.data1.error, result.data1.error) }) t.test('filterLog removes entry with escape sequence', async t => { - const result = filterLog({ log: logData2, ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation'] }) + const result = filterLog({ + log: logData2, + context: { + ...context, + ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation'] + } + }) t.same(result, { level: 30, time: 1522431328992 }) }) t.test('filterLog removes entry with escape sequence nested', async t => { - const result = filterLog({ log: logData2, ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer'] }) + const result = filterLog({ + log: logData2, + context: { + ...context, + ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer'] + } + }) t.same(result, { level: 30, time: 1522431328992, 'logging.domain.corp/operation': { id: 'foo' } }) }) @@ -55,17 +89,38 @@ const ignoreKeysArray = [ ignoreKeysArray.forEach(ignoreKeys => { tap.test(`#filterLog with an includeKeys option when the ignoreKeys being ${ignoreKeys}`, t => { t.test('filterLog include nothing', async t => { - const result = filterLog({ log: logData, ignoreKeys, includeKeys: [] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys, + includeKeys: [] + } + }) t.same(result, {}) }) t.test('filterLog include single entry', async t => { - const result = filterLog({ log: logData, ignoreKeys, includeKeys: ['time'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys, + includeKeys: ['time'] + } + }) t.same(result, { time: 1522431328992 }) }) t.test('filterLog include multiple entries', async t => { - const result = filterLog({ log: logData, ignoreKeys, includeKeys: ['time', 'data1'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys, + includeKeys: ['time', 'data1'] + } + }) t.same(result, { time: 1522431328992, data1: { @@ -88,7 +143,13 @@ tap.test('#filterLog with circular references', t => { logData.circular = logData t.test('filterLog removes single entry', async t => { - const result = filterLog({ log: logData, ignoreKeys: ['data1'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + ignoreKeys: ['data1'] + } + }) t.same(result.circular.level, result.level) t.same(result.circular.time, result.time) @@ -98,13 +159,25 @@ tap.test('#filterLog with circular references', t => { }) t.test('filterLog includes single entry', async t => { - const result = filterLog({ log: logData, includeKeys: ['data1'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + includeKeys: ['data1'] + } + }) t.same(result, { data1: 'test' }) }) t.test('filterLog includes circular keys', async t => { - const result = filterLog({ log: logData, includeKeys: ['level', 'circular'] }) + const result = filterLog({ + log: logData, + context: { + ...context, + includeKeys: ['level', 'circular'] + } + }) t.same(result.circular.level, logData.level) t.same(result.circular.time, logData.time) diff --git a/lib/utils/parse-factory-options.js b/lib/utils/parse-factory-options.js new file mode 100644 index 00000000..33ffb374 --- /dev/null +++ b/lib/utils/parse-factory-options.js @@ -0,0 +1,153 @@ +'use strict' + +module.exports = parseFactoryOptions + +const { + LEVEL_NAMES +} = require('../constants') +const colors = require('../colors') +const handleCustomLevelsOpts = require('./handle-custom-levels-opts') +const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts') + +/** + * A `PrettyContext` is an object to be used by the various functions that + * process log data. It is derived from the provided {@link PinoPrettyOptions}. + * It may be used as a `this` context. + * + * @typedef {object} PrettyContext + * @property {string} EOL The escape sequence chosen as the line terminator. + * @property {string} IDENT The string to use as the indentation sequence. + * @property {ColorizerFunc} colorizer A configured colorizer function. + * @property {Array[Array]} customColors A set of custom color + * names associated with level numbers. + * @property {object} customLevelNames A hash of level numbers to level names, + * e.g. `{ 30: "info" }`. + * @property {object} customLevels A hash of level names to level numbers, + * e.g. `{ info: 30 }`. + * @property {CustomPrettifiers} customPrettifiers A hash of custom prettifier + * functions. + * @property {object} customProperties Comprised of `customLevels` and + * `customLevelNames` if such options are provided. + * @property {string[]} errorLikeObjectKeys The key names in the log data that + * should be considered as holding error objects. + * @property {string[]} errorProps A list of error object keys that should be + * included in the output. + * @property {boolean} hideObject Indicates the prettifier should omit objects + * in the output. + * @property {string[]} ignoreKeys Set of log data keys to omit. + * @property {string[]} includeKeys Opposite of `ignoreKeys`. + * @property {boolean} levelFirst Indicates the level should be printed first. + * @property {string} levelKey Name of the key in the log data that contains + * the message. + * @property {string} levelLabel Format token to represent the position of the + * level name in the output string. + * @property {MessageFormatString|MessageFormatFunction} messageFormat + * @property {string} messageKey Name of the key in the log data that contains + * the message. + * @property {string|number} minimumLevel The minimum log level to process + * and output. + * @property {ColorizerFunc} objectColorizer + * @property {boolean} singleLine Indicates objects should be printed on a + * single output line. + * @property {string} timestampKey The name of the key in the log data that + * contains the log timestamp. + * @property {boolean} translateTime Indicates if timestamps should be + * translated to a human-readable string. + * @property {boolean} useOnlyCustomProps + */ + +/** + * @param {PinoPrettyOptions} options The user supplied object of options. + * + * @returns {PrettyContext} + */ +function parseFactoryOptions (options) { + const EOL = options.crlf ? '\r\n' : '\n' + const IDENT = ' ' + const { + customPrettifiers, + errorLikeObjectKeys, + hideObject, + levelFirst, + levelKey, + levelLabel, + messageFormat, + messageKey, + minimumLevel, + singleLine, + timestampKey, + translateTime + } = options + const errorProps = options.errorProps.split(',') + const useOnlyCustomProps = typeof options.useOnlyCustomProps === 'boolean' + ? options.useOnlyCustomProps + : (options.useOnlyCustomProps === 'true') + const customLevels = handleCustomLevelsOpts(options.customLevels) + const customLevelNames = handleCustomLevelsNamesOpts(options.customLevels) + + let customColors + if (options.customColors) { + customColors = options.customColors.split(',').reduce((agg, value) => { + const [level, color] = value.split(':') + + const condition = useOnlyCustomProps + ? options.customLevels + : customLevelNames[level] !== undefined + const levelNum = condition + ? customLevelNames[level] + : LEVEL_NAMES[level] + const colorIdx = levelNum !== undefined + ? levelNum + : level + + agg.push([colorIdx, color]) + + return agg + }, []) + } + + const customProperties = { customLevels, customLevelNames } + if (useOnlyCustomProps === true && !options.customLevels) { + customProperties.customLevels = undefined + customProperties.customLevelNames = undefined + } + + const includeKeys = options.include !== undefined + ? new Set(options.include.split(',')) + : undefined + const ignoreKeys = (!includeKeys && options.ignore) + ? new Set(options.ignore.split(',')) + : undefined + + const colorizer = colors(options.colorize, customColors, useOnlyCustomProps) + const objectColorizer = options.colorizeObjects + ? colorizer + : colors(false, [], false) + + return { + EOL, + IDENT, + colorizer, + customColors, + customLevelNames, + customLevels, + customPrettifiers, + customProperties, + errorLikeObjectKeys, + errorProps, + hideObject, + ignoreKeys, + includeKeys, + levelFirst, + levelKey, + levelLabel, + messageFormat, + messageKey, + minimumLevel, + objectColorizer, + singleLine, + timestampKey, + translateTime, + useOnlyCustomProps + } +} diff --git a/lib/utils/prettify-error-log.js b/lib/utils/prettify-error-log.js index 21a4089b..2410c650 100644 --- a/lib/utils/prettify-error-log.js +++ b/lib/utils/prettify-error-log.js @@ -3,9 +3,7 @@ module.exports = prettifyErrorLog const { - ERROR_LIKE_KEYS, - LOGGER_KEYS, - MESSAGE_KEY + LOGGER_KEYS } = require('../constants') const isObject = require('./is-object') @@ -15,17 +13,8 @@ const prettifyObject = require('./prettify-object') /** * @typedef {object} PrettifyErrorLogParams * @property {object} log The error log to prettify. - * @property {string} [messageKey] The name of the key that contains a - * general log message. This is not the error's message property but the logger - * messsage property. Default: `MESSAGE_KEY` constant. - * @property {string} [ident] The sequence to use for indentation. Default: `' '`. - * @property {string} [eol] The sequence to use for EOL. Default: `'\n'`. - * @property {string[]} [errorLikeKeys] A set of keys that should be considered - * to have error objects as values. Default: `ERROR_LIKE_KEYS` constant. - * @property {string[]} [errorProperties] A set of specific error object - * properties, that are not the value of `messageKey`, `type`, or `stack`, to - * include in the prettified result. The first entry in the list may be `'*'` - * to indicate that all sibling properties should be prettified. Default: `[]`. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -36,14 +25,13 @@ const prettifyObject = require('./prettify-object') * * @returns {string} A string that represents the prettified error log. */ -function prettifyErrorLog ({ - log, - messageKey = MESSAGE_KEY, - ident = ' ', - eol = '\n', - errorLikeKeys = ERROR_LIKE_KEYS, - errorProperties = [] -}) { +function prettifyErrorLog ({ log, context }) { + const { + EOL: eol, + IDENT: ident, + errorProps: errorProperties, + messageKey + } = context const stack = log.stack const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol }) let result = `${ident}${joinedLines}${eol}` @@ -66,7 +54,14 @@ function prettifyErrorLog ({ // The nested object may have "logger" type keys but since they are not // at the root level of the object being processed, we want to print them. // Thus, we invoke with `excludeLoggerKeys: false`. - const prettifiedObject = prettifyObject({ input: log[key], errorLikeKeys, excludeLoggerKeys: false, eol, ident: ident + ident }) + const prettifiedObject = prettifyObject({ + log: log[key], + excludeLoggerKeys: false, + context: { + ...context, + IDENT: ident + ident + } + }) result = `${result}${ident}${key}: {${eol}${prettifiedObject}${ident}}${eol}` continue } diff --git a/lib/utils/prettify-error-log.test.js b/lib/utils/prettify-error-log.test.js index 5d0f7db1..04e10f82 100644 --- a/lib/utils/prettify-error-log.test.js +++ b/lib/utils/prettify-error-log.test.js @@ -2,22 +2,47 @@ const tap = require('tap') const prettifyErrorLog = require('./prettify-error-log') +const { + ERROR_LIKE_KEYS, + MESSAGE_KEY +} = require('../constants') + +const context = { + EOL: '\n', + IDENT: ' ', + customPrettifiers: {}, + errorLikeObjectKeys: ERROR_LIKE_KEYS, + errorProps: [], + messageKey: MESSAGE_KEY +} tap.test('returns string with default settings', async t => { const err = Error('Something went wrong') - const str = prettifyErrorLog({ log: err }) + const str = prettifyErrorLog({ log: err, context }) t.ok(str.startsWith(' Error: Something went wrong')) }) tap.test('returns string with custom ident', async t => { const err = Error('Something went wrong') - const str = prettifyErrorLog({ log: err, ident: ' ' }) + const str = prettifyErrorLog({ + log: err, + context: { + ...context, + IDENT: ' ' + } + }) t.ok(str.startsWith(' Error: Something went wrong')) }) tap.test('returns string with custom eol', async t => { const err = Error('Something went wrong') - const str = prettifyErrorLog({ log: err, eol: '\r\n' }) + const str = prettifyErrorLog({ + log: err, + context: { + ...context, + EOL: '\r\n' + } + }) t.ok(str.startsWith(' Error: Something went wrong\r\n')) }) @@ -25,7 +50,13 @@ tap.test('errorProperties', t => { t.test('excludes all for wildcard', async t => { const err = Error('boom') err.foo = 'foo' - const str = prettifyErrorLog({ log: err, errorProperties: ['*'] }) + const str = prettifyErrorLog({ + log: err, + context: { + ...context, + errorProps: ['*'] + } + }) t.ok(str.startsWith(' Error: boom')) t.equal(str.includes('foo: "foo"'), false) }) @@ -33,7 +64,13 @@ tap.test('errorProperties', t => { t.test('excludes only selected properties', async t => { const err = Error('boom') err.foo = 'foo' - const str = prettifyErrorLog({ log: err, errorProperties: ['foo'] }) + const str = prettifyErrorLog({ + log: err, + context: { + ...context, + errorProps: ['foo'] + } + }) t.ok(str.startsWith(' Error: boom')) t.equal(str.includes('foo: foo'), true) }) @@ -41,7 +78,13 @@ tap.test('errorProperties', t => { t.test('ignores specified properties if not present', async t => { const err = Error('boom') err.foo = 'foo' - const str = prettifyErrorLog({ log: err, errorProperties: ['foo', 'bar'] }) + const str = prettifyErrorLog({ + log: err, + context: { + ...context, + errorProps: ['foo', 'bar'] + } + }) t.ok(str.startsWith(' Error: boom')) t.equal(str.includes('foo: foo'), true) t.equal(str.includes('bar'), false) @@ -50,7 +93,13 @@ tap.test('errorProperties', t => { t.test('processes nested objects', async t => { const err = Error('boom') err.foo = { bar: 'bar', message: 'included' } - const str = prettifyErrorLog({ log: err, errorProperties: ['foo'] }) + const str = prettifyErrorLog({ + log: err, + context: { + ...context, + errorProps: ['foo'] + } + }) t.ok(str.startsWith(' Error: boom')) t.equal(str.includes('foo: {'), true) t.equal(str.includes('bar: "bar"'), true) diff --git a/lib/utils/prettify-level.js b/lib/utils/prettify-level.js index 616eb255..213ba06c 100644 --- a/lib/utils/prettify-level.js +++ b/lib/utils/prettify-level.js @@ -2,25 +2,13 @@ module.exports = prettifyLevel -const { - LEVEL_KEY -} = require('../constants') -const defaultColorizer = require('../colors')() - const getPropertyValue = require('./get-property-value') /** * @typedef {object} PrettifyLevelParams * @property {object} log The log object. - * @property {function} [colorizer] A colorizer function that accepts a level - * value and returns a colorized string. Default: a no-op colorizer. - * @property {string} [levelKey='level'] The key to find the level under. - * @property {CustomPrettifierFunc} [prettifier] A user-supplied formatter - * to be called instead of colorizer. - * @property {object} [customLevels] The custom levels where key as the level - * index and value as the level name. - * @property {object} [customLevelNames] The custom level names where key is - * the level name and value is the level index. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -33,14 +21,14 @@ const getPropertyValue = require('./get-property-value') * `undefined` will be returned. Otherwise, a string from the specified * `colorizer` is returned. */ -function prettifyLevel ({ - log, - colorizer = defaultColorizer, - levelKey = LEVEL_KEY, - prettifier, - customLevels, - customLevelNames -}) { +function prettifyLevel ({ log, context }) { + const { + colorizer, + customLevels, + customLevelNames, + levelKey + } = context + const prettifier = context.customPrettifiers?.level const output = getPropertyValue(log, levelKey) if (output === undefined) return undefined return prettifier ? prettifier(output) : colorizer(output, { customLevels, customLevelNames }) diff --git a/lib/utils/prettify-level.test.js b/lib/utils/prettify-level.test.js index e127da10..e735b0a9 100644 --- a/lib/utils/prettify-level.test.js +++ b/lib/utils/prettify-level.test.js @@ -3,9 +3,25 @@ const tap = require('tap') const prettifyLevel = require('./prettify-level') const getColorizer = require('../colors') +const { + LEVEL_KEY +} = require('../constants') + +const context = { + colorizer: getColorizer(), + customLevelNames: undefined, + customLevels: undefined, + levelKey: LEVEL_KEY, + customPrettifiers: undefined +} tap.test('returns `undefined` for unknown level', async t => { - const colorized = prettifyLevel({ log: {} }) + const colorized = prettifyLevel({ + log: {}, + context: { + ...context + } + }) t.equal(colorized, undefined) }) @@ -13,7 +29,12 @@ tap.test('returns non-colorized value for default colorizer', async t => { const log = { level: 30 } - const colorized = prettifyLevel({ log }) + const colorized = prettifyLevel({ + log, + context: { + ...context + } + }) t.equal(colorized, 'INFO') }) @@ -22,7 +43,13 @@ tap.test('returns colorized value for color colorizer', async t => { level: 30 } const colorizer = getColorizer(true) - const colorized = prettifyLevel({ log, colorizer }) + const colorized = prettifyLevel({ + log, + context: { + ...context, + colorizer + } + }) t.equal(colorized, '\u001B[32mINFO\u001B[39m') }) @@ -30,10 +57,12 @@ tap.test('passes output through provided prettifier', async t => { const log = { level: 30 } - const colorized = prettifyLevel({ log, prettifier }) + const colorized = prettifyLevel({ + log, + context: { + ...context, + customPrettifiers: { level () { return 'modified' } } + } + }) t.equal(colorized, 'modified') - - function prettifier () { - return 'modified' - } }) diff --git a/lib/utils/prettify-message.js b/lib/utils/prettify-message.js index 835bf0ec..7758f1d8 100644 --- a/lib/utils/prettify-message.js +++ b/lib/utils/prettify-message.js @@ -2,12 +2,8 @@ module.exports = prettifyMessage -const defaultColorizer = require('../colors')() const { - LEVELS, - LEVEL_KEY, - LEVEL_LABEL, - MESSAGE_KEY + LEVELS } = require('../constants') const getPropertyValue = require('./get-property-value') @@ -16,19 +12,8 @@ const interpretConditionals = require('./interpret-conditionals') /** * @typedef {object} PrettifyMessageParams * @property {object} log The log object with the message to colorize. - * @property {string} [messageKey='msg'] The property of the `log` that is the - * message to be prettified. - * @property {MessageFormatString|MessageFormatFunction} [messageFormat=undefined] - * A format string or function that defines how the logged message should be - * formatted, e.g. `'{level} - {pid}'`. - * @property {function} [colorizer] A colorizer function that has a - * `.message(str)` method attached to it. This function should return a colorized - * string which will be the "prettified" message. Default: a no-op colorizer. - * @property {string} [levelLabel='levelLabel'] The label used to output the - * log level. - * @property {string} [levelKey='level'] The key to find the level under. - * @property {object} [customLevels] The custom levels where key as the level - * index and value as the level name. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -40,15 +25,16 @@ const interpretConditionals = require('./interpret-conditionals') * 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, useOnlyCustomProps -}) { +function prettifyMessage ({ log, context }) { + const { + colorizer, + customLevels, + levelKey, + levelLabel, + messageFormat, + messageKey, + useOnlyCustomProps + } = context if (messageFormat && typeof messageFormat === 'string') { const parsedMessageFormat = interpretConditionals(messageFormat, log) diff --git a/lib/utils/prettify-message.test.js b/lib/utils/prettify-message.test.js index 061f7131..8faf4b12 100644 --- a/lib/utils/prettify-message.test.js +++ b/lib/utils/prettify-message.test.js @@ -3,96 +3,184 @@ const tap = require('tap') const prettifyMessage = require('./prettify-message') const getColorizer = require('../colors') +const { + LEVEL_KEY, + LEVEL_LABEL +} = require('../constants') +const context = { + colorizer: getColorizer(), + levelKey: LEVEL_KEY, + levelLabel: LEVEL_LABEL, + messageKey: 'msg' +} tap.test('returns `undefined` if `messageKey` not found', async t => { - const str = prettifyMessage({ log: {} }) + const str = prettifyMessage({ log: {}, context }) t.equal(str, undefined) }) tap.test('returns `undefined` if `messageKey` not string', async t => { - const str = prettifyMessage({ log: { msg: {} } }) + const str = prettifyMessage({ log: { msg: {} }, context }) t.equal(str, undefined) }) tap.test('returns non-colorized value for default colorizer', async t => { - const str = prettifyMessage({ log: { msg: 'foo' } }) + const colorizer = getColorizer() + const str = prettifyMessage({ + log: { msg: 'foo' }, + context: { ...context, colorizer } + }) t.equal(str, 'foo') }) tap.test('returns non-colorized value for alternate `messageKey`', async t => { - const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message' }) + const str = prettifyMessage({ + log: { message: 'foo' }, + context: { ...context, messageKey: 'message' } + }) t.equal(str, 'foo') }) tap.test('returns colorized value for color colorizer', async t => { const colorizer = getColorizer(true) - const str = prettifyMessage({ log: { msg: 'foo' }, colorizer }) + const str = prettifyMessage({ + log: { msg: 'foo' }, + context: { ...context, colorizer } + }) t.equal(str, '\u001B[36mfoo\u001B[39m') }) tap.test('returns colorized value for color colorizer for alternate `messageKey`', async t => { const colorizer = getColorizer(true) - const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message', colorizer }) + const str = prettifyMessage({ + log: { message: 'foo' }, + context: { ...context, messageKey: 'message', colorizer } + }) t.equal(str, '\u001B[36mfoo\u001B[39m') }) tap.test('returns message formatted by `messageFormat` option', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule' }, messageFormat: '{context} - {msg}' }) + const str = prettifyMessage({ + log: { msg: 'foo', context: 'appModule' }, + context: { ...context, messageFormat: '{context} - {msg}' } + }) t.equal(str, 'appModule - foo') }) tap.test('returns message formatted by `messageFormat` option - missing prop', async t => { - const str = prettifyMessage({ log: { context: 'appModule' }, messageFormat: '{context} - {msg}' }) + const str = prettifyMessage({ + log: { context: 'appModule' }, + context: { ...context, messageFormat: '{context} - {msg}' } + }) t.equal(str, 'appModule - ') }) tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: {} }) + const str = prettifyMessage({ + log: { msg: 'foo', context: 'appModule', level: 30 }, + context: { + ...context, + messageFormat: '[{level}] {levelLabel} {context} - {msg}', + customLevels: {} + } + }) t.equal(str, '[30] INFO appModule - foo') }) tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 30: 'CHECK' }, useOnlyCustomProps: true }) + const str = prettifyMessage({ + log: { msg: 'foo', context: 'appModule', level: 30 }, + context: { + ...context, + messageFormat: '[{level}] {levelLabel} {context} - {msg}', + customLevels: { 30: 'CHECK' }, + useOnlyCustomProps: true + } + }) t.equal(str, '[30] CHECK appModule - foo') }) tap.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' } }) + const str = prettifyMessage({ + log: { msg: 'foo', context: 'appModule', level: 123 }, + context: { + ...context, + messageFormat: '[{level}] {levelLabel} {context} - {msg}', + customLevels: { 123: 'CUSTOM' } + } + }) t.equal(str, '[123] CUSTOM appModule - foo') }) tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: true }) + const str = prettifyMessage({ + log: { msg: 'foo', context: 'appModule', level: 123 }, + context: { + ...context, + messageFormat: '[{level}] {levelLabel} {context} - {msg}', + customLevels: { 123: 'CUSTOM' }, + useOnlyCustomProps: true + } + }) t.equal(str, '[123] CUSTOM appModule - foo') }) tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 40 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: false }) + const str = prettifyMessage({ + log: { msg: 'foo', context: 'appModule', level: 40 }, + context: { + ...context, + messageFormat: '[{level}] {levelLabel} {context} - {msg}', + customLevels: { 123: 'CUSTOM' }, + useOnlyCustomProps: false + } + }) t.equal(str, '[40] WARN appModule - foo') }) tap.test('`messageFormat` supports nested curly brackets', async t => { - const str = prettifyMessage({ log: { level: 30 }, messageFormat: '{{level}}-{level}-{{level}-{level}}' }) + const str = prettifyMessage({ + log: { level: 30 }, + context: { + ...context, + messageFormat: '{{level}}-{level}-{{level}-{level}}' + } + }) t.equal(str, '{30}-30-{30-30}') }) tap.test('`messageFormat` supports nested object', async t => { - const str = prettifyMessage({ log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' }, messageFormat: '{request.url} - param: {request.params.process} - {msg}' }) + const str = prettifyMessage({ + log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' }, + context: { + ...context, + messageFormat: '{request.url} - param: {request.params.process} - {msg}' + } + }) t.equal(str, 'localhost/test - param: - foo') }) tap.test('`messageFormat` supports conditional blocks', async t => { - const str = prettifyMessage({ log: { level: 30, req: { id: 'foo' } }, messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}' }) + const str = prettifyMessage({ + log: { level: 30, req: { id: 'foo' } }, + context: { + ...context, + messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}' + } + }) t.equal(str, '30 | (foo)') }) tap.test('`messageFormat` supports function definition', async t => { const str = prettifyMessage({ log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' }, - messageFormat: (log, messageKey, levelLabel) => { - let msg = log[messageKey] - if (msg === 'incoming request') msg = `--> ${log.request.url}` - return msg + context: { + ...context, + messageFormat: (log, messageKey, levelLabel) => { + let msg = log[messageKey] + if (msg === 'incoming request') msg = `--> ${log.request.url}` + return msg + } } }) t.equal(str, '--> localhost/test') diff --git a/lib/utils/prettify-metadata.js b/lib/utils/prettify-metadata.js index 1876c9a0..5fb4a3b0 100644 --- a/lib/utils/prettify-metadata.js +++ b/lib/utils/prettify-metadata.js @@ -6,10 +6,8 @@ module.exports = prettifyMetadata * @typedef {object} PrettifyMetadataParams * @property {object} log The log that may or may not contain metadata to * be prettified. - * @property {CustomPrettifiers} prettifiers A set of functions used to - * prettify each key of the input log's metadata. The keys are the keys of the - * metadata (like `hostname`, `pid`, `name`, etc), and the values are functions - * which take the metadata value and return a string. Each key is optional. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -22,7 +20,8 @@ module.exports = prettifyMetadata * @returns {undefined|string} If no metadata is found then `undefined` is * returned. Otherwise, a string of prettified metadata is returned. */ -function prettifyMetadata ({ log, prettifiers = {} }) { +function prettifyMetadata ({ log, context }) { + const prettifiers = context.customPrettifiers let line = '' if (log.name || log.pid || log.hostname) { diff --git a/lib/utils/prettify-metadata.test.js b/lib/utils/prettify-metadata.test.js index e665db0e..91281a96 100644 --- a/lib/utils/prettify-metadata.test.js +++ b/lib/utils/prettify-metadata.test.js @@ -2,84 +2,87 @@ const tap = require('tap') const prettifyMetadata = require('./prettify-metadata') +const context = { + customPrettifiers: {} +} tap.test('returns `undefined` if no metadata present', async t => { - const str = prettifyMetadata({ log: {} }) + const str = prettifyMetadata({ log: {}, context }) t.equal(str, undefined) }) tap.test('works with only `name` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo' } }) + const str = prettifyMetadata({ log: { name: 'foo' }, context }) t.equal(str, '(foo)') }) tap.test('works with only `pid` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234' } }) + const str = prettifyMetadata({ log: { pid: '1234' }, context }) t.equal(str, '(1234)') }) tap.test('works with only `hostname` present', async t => { - const str = prettifyMetadata({ log: { hostname: 'bar' } }) + const str = prettifyMetadata({ log: { hostname: 'bar' }, context }) t.equal(str, '(on bar)') }) tap.test('works with only `name` & `pid` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' } }) + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' }, context }) t.equal(str, '(foo/1234)') }) tap.test('works with only `name` & `hostname` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' } }) + const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' }, context }) t.equal(str, '(foo on bar)') }) tap.test('works with only `pid` & `hostname` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' } }) + const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' }, context }) t.equal(str, '(1234 on bar)') }) tap.test('works with only `name`, `pid`, & `hostname` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' } }) + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' }, context }) t.equal(str, '(foo/1234 on bar)') }) tap.test('works with only `name` & `caller` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' } }) + const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' }, context }) t.equal(str, '(foo) ') }) tap.test('works with only `pid` & `caller` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' } }) + const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' }, context }) t.equal(str, '(1234) ') }) tap.test('works with only `hostname` & `caller` present', async t => { - const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' } }) + const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' }, context }) t.equal(str, '(on bar) ') }) tap.test('works with only `name`, `pid`, & `caller` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' } }) + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' }, context }) t.equal(str, '(foo/1234) ') }) tap.test('works with only `name`, `hostname`, & `caller` present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' } }) + const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' }, context }) t.equal(str, '(foo on bar) ') }) tap.test('works with only `caller` present', async t => { - const str = prettifyMetadata({ log: { caller: 'baz' } }) + const str = prettifyMetadata({ log: { caller: 'baz' }, context }) t.equal(str, '') }) tap.test('works with only `pid`, `hostname`, & `caller` present', async t => { - const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' } }) + const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' }, context }) t.equal(str, '(1234 on bar) ') }) tap.test('works with all four present', async t => { - const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' } }) + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' }, context }) t.equal(str, '(foo/1234 on bar) ') }) @@ -100,7 +103,9 @@ tap.test('uses prettifiers from passed prettifiers object', async t => { } const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz', name: 'joe' }, - prettifiers + context: { + customPrettifiers: prettifiers + } }) t.equal(str, '(JOE/1234__ on BAR) ') }) diff --git a/lib/utils/prettify-object.js b/lib/utils/prettify-object.js index 17ad2379..e4bbec1c 100644 --- a/lib/utils/prettify-object.js +++ b/lib/utils/prettify-object.js @@ -3,10 +3,8 @@ module.exports = prettifyObject const { - ERROR_LIKE_KEYS, LOGGER_KEYS } = require('../constants') -const defaultColorizer = require('../colors')() const stringifySafe = require('fast-safe-stringify') const joinLinesWithIndentation = require('./join-lines-with-indentation') @@ -14,20 +12,13 @@ const prettifyError = require('./prettify-error') /** * @typedef {object} PrettifyObjectParams - * @property {object} input The object to prettify. - * @property {string} [ident] The indentation sequence to use. Default: `' '`. - * @property {string} [eol] The EOL sequence to use. Default: `'\n'`. - * @property {string[]} [skipKeys] A set of object keys to exclude from the - * prettified result. Default: `[]`. - * @property {CustomPrettifiers} [customPrettifiers] Dictionary of - * custom prettifiers. Default: `{}`. - * @property {string[]} [errorLikeKeys] A set of object keys that contain - * error objects. Default: `ERROR_LIKE_KEYS` constant. + * @property {object} log The object to prettify. * @property {boolean} [excludeLoggerKeys] Indicates if known logger specific * keys should be excluded from prettification. Default: `true`. - * @property {boolean} [singleLine] Should non-error keys all be formatted - * on a single line? This does NOT apply to errors, which will still be - * multi-line. Default: `false` + * @property {string[]} [skipKeys] A set of object keys to exclude from the + * * prettified result. Default: `[]`. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -41,16 +32,19 @@ const prettifyError = require('./prettify-error') * there was nothing to prettify. */ function prettifyObject ({ - input, - ident = ' ', - eol = '\n', - skipKeys = [], - customPrettifiers = {}, - errorLikeKeys = ERROR_LIKE_KEYS, + log, excludeLoggerKeys = true, - singleLine = false, - colorizer = defaultColorizer + skipKeys = [], + context }) { + const { + EOL: eol, + IDENT: ident, + customPrettifiers, + errorLikeObjectKeys: errorLikeKeys, + objectColorizer, + singleLine + } = context const keysToIgnore = [].concat(skipKeys) /* istanbul ignore else */ @@ -59,11 +53,11 @@ function prettifyObject ({ let result = '' // Split object keys into two categories: error and non-error - const { plain, errors } = Object.entries(input).reduce(({ plain, errors }, [k, v]) => { + const { plain, errors } = Object.entries(log).reduce(({ plain, errors }, [k, v]) => { 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, input) + ? customPrettifiers[k](v, k, log) : v if (errorLikeKeys.includes(k)) { errors[k] = pretty @@ -78,7 +72,7 @@ function prettifyObject ({ // Stringify the entire object as a single JSON line /* istanbul ignore else */ if (Object.keys(plain).length > 0) { - result += colorizer.greyMessage(stringifySafe(plain)) + result += objectColorizer.greyMessage(stringifySafe(plain)) } result += eol // Avoid printing the escape character on escaped backslashes. diff --git a/lib/utils/prettify-object.test.js b/lib/utils/prettify-object.test.js index a3492403..61f64244 100644 --- a/lib/utils/prettify-object.test.js +++ b/lib/utils/prettify-object.test.js @@ -1,40 +1,63 @@ 'use strict' const tap = require('tap') +const colors = require('../colors') const prettifyObject = require('./prettify-object') +const { + ERROR_LIKE_KEYS +} = require('../constants') + +const context = { + EOL: '\n', + IDENT: ' ', + customPrettifiers: {}, + errorLikeObjectKeys: ERROR_LIKE_KEYS, + objectColorizer: colors(), + singleLine: false +} tap.test('returns empty string if no properties present', async t => { - const str = prettifyObject({ input: {} }) + const str = prettifyObject({ log: {}, context }) t.equal(str, '') }) tap.test('works with single level properties', async t => { - const str = prettifyObject({ input: { foo: 'bar' } }) + const str = prettifyObject({ log: { foo: 'bar' }, context }) t.equal(str, ' foo: "bar"\n') }) tap.test('works with multiple level properties', async t => { - const str = prettifyObject({ input: { foo: { bar: 'baz' } } }) + const str = prettifyObject({ log: { foo: { bar: 'baz' } }, context }) t.equal(str, ' foo: {\n "bar": "baz"\n }\n') }) tap.test('skips specified keys', async t => { - const str = prettifyObject({ input: { foo: 'bar', hello: 'world' }, skipKeys: ['foo'] }) + const str = prettifyObject({ + log: { foo: 'bar', hello: 'world' }, + skipKeys: ['foo'], + context + }) t.equal(str, ' hello: "world"\n') }) tap.test('ignores predefined keys', async t => { - const str = prettifyObject({ input: { foo: 'bar', pid: 12345 } }) + const str = prettifyObject({ log: { foo: 'bar', pid: 12345 }, context }) t.equal(str, ' foo: "bar"\n') }) tap.test('ignores escaped backslashes in string values', async t => { - const str = prettifyObject({ input: { foo_regexp: '\\[^\\w\\s]\\' } }) + const str = prettifyObject({ log: { foo_regexp: '\\[^\\w\\s]\\' }, context }) t.equal(str, ' foo_regexp: "\\[^\\w\\s]\\"\n') }) tap.test('ignores escaped backslashes in string values (singleLine option)', async t => { - const str = prettifyObject({ input: { foo_regexp: '\\[^\\w\\s]\\' }, singleLine: true }) + const str = prettifyObject({ + log: { foo_regexp: '\\[^\\w\\s]\\' }, + context: { + ...context, + singleLine: true + } + }) t.equal(str, '{"foo_regexp":"\\[^\\w\\s]\\"}\n') }) @@ -44,7 +67,7 @@ tap.test('works with error props', async t => { message: err.message, stack: err.stack } - const str = prettifyObject({ input: { error: serializedError } }) + const str = prettifyObject({ log: { error: serializedError }, context }) t.ok(str.startsWith(' error:')) t.ok(str.includes(' "message": "Something went wrong",')) t.ok(str.includes(' Error: Something went wrong')) @@ -54,7 +77,13 @@ tap.test('customPrettifiers gets applied', async t => { const customPrettifiers = { foo: v => v.toUpperCase() } - const str = prettifyObject({ input: { foo: 'foo' }, customPrettifiers }) + const str = prettifyObject({ + log: { foo: 'foo' }, + context: { + ...context, + customPrettifiers + } + }) t.equal(str.startsWith(' foo: FOO'), true) }) @@ -62,17 +91,26 @@ tap.test('skips lines omitted by customPrettifiers', async t => { const customPrettifiers = { foo: () => { return undefined } } - const str = prettifyObject({ input: { foo: 'foo', bar: 'bar' }, customPrettifiers }) + const str = prettifyObject({ + log: { foo: 'foo', bar: 'bar' }, + context: { + ...context, + customPrettifiers + } + }) t.equal(str.includes('bar: "bar"'), true) t.equal(str.includes('foo: "foo"'), false) }) tap.test('joined lines omits starting eol', async t => { const str = prettifyObject({ - input: { msg: 'doing work', calls: ['step 1', 'step 2', 'step 3'], level: 30 }, - ident: '', - customPrettifiers: { - calls: val => '\n' + val.map(it => ' ' + it).join('\n') + log: { msg: 'doing work', calls: ['step 1', 'step 2', 'step 3'], level: 30 }, + context: { + ...context, + IDENT: '', + customPrettifiers: { + calls: val => '\n' + val.map(it => ' ' + it).join('\n') + } } }) t.equal(str, [ @@ -89,7 +127,13 @@ tap.test('errors skips prettifiers', async t => { const customPrettifiers = { err: () => { return 'is_err' } } - const str = prettifyObject({ input: { err: Error('boom') }, customPrettifiers }) + const str = prettifyObject({ + log: { err: Error('boom') }, + context: { + ...context, + customPrettifiers + } + }) t.equal(str.includes('err: is_err'), true) }) @@ -97,6 +141,12 @@ tap.test('errors skips prettifying if no lines are present', async t => { const customPrettifiers = { err: () => { return undefined } } - const str = prettifyObject({ input: { err: Error('boom') }, customPrettifiers }) + const str = prettifyObject({ + log: { err: Error('boom') }, + context: { + ...context, + customPrettifiers + } + }) t.equal(str, '') }) diff --git a/lib/utils/prettify-time.js b/lib/utils/prettify-time.js index 76a57525..e876b35f 100644 --- a/lib/utils/prettify-time.js +++ b/lib/utils/prettify-time.js @@ -2,23 +2,13 @@ module.exports = prettifyTime -const { - TIMESTAMP_KEY -} = require('../constants') - const formatTime = require('./format-time') /** * @typedef {object} PrettifyTimeParams * @property {object} log The log object with the timestamp to be prettified. - * @property {string} [timestampKey='time'] The log property that should be used - * to resolve timestamp value. - * @property {boolean|string} [translateFormat=undefined] When `true` the - * timestamp will be prettified into a string at UTC using the default - * `DATE_FORMAT`. If a string, then `translateFormat` will be used as the format - * string to determine the output; see the `formatTime` function for details. - * @property {CustomPrettifierFunc} [prettifier] A user-supplied formatter - * for altering output. + * @property {PrettyContext} context The context object built from parsing + * the options. */ /** @@ -31,12 +21,12 @@ const formatTime = require('./format-time') * `undefined` is returned. Otherwise, the prettified time is returned as a * string. */ -function prettifyTime ({ - log, - timestampKey = TIMESTAMP_KEY, - translateFormat = undefined, - prettifier -}) { +function prettifyTime ({ log, context }) { + const { + timestampKey, + translateTime: translateFormat + } = context + const prettifier = context.customPrettifiers?.time let time = null if (timestampKey in log) { diff --git a/lib/utils/prettify-time.test.js b/lib/utils/prettify-time.test.js index 1b3e55e5..cfde6627 100644 --- a/lib/utils/prettify-time.test.js +++ b/lib/utils/prettify-time.test.js @@ -4,80 +4,181 @@ process.env.TZ = 'UTC' const tap = require('tap') const prettifyTime = require('./prettify-time') +const { + TIMESTAMP_KEY +} = require('../constants') +const context = { + timestampKey: TIMESTAMP_KEY, + translateTime: true, + customPrettifiers: {} +} tap.test('returns `undefined` if `time` or `timestamp` not in log', async t => { - const str = prettifyTime({ log: {} }) + const str = prettifyTime({ log: {}, context }) t.equal(str, undefined) }) tap.test('returns prettified formatted time from custom field', async t => { const log = { customtime: 1554642900000 } - let str = prettifyTime({ log, translateFormat: true, timestampKey: 'customtime' }) + let str = prettifyTime({ + log, + context: { + ...context, + timestampKey: 'customtime' + } + }) t.equal(str, '[13:15:00.000]') - str = prettifyTime({ log, translateFormat: false, timestampKey: 'customtime' }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: false, + timestampKey: 'customtime' + } + }) t.equal(str, '[1554642900000]') }) tap.test('returns prettified formatted time', async t => { let log = { time: 1554642900000 } - let str = prettifyTime({ log, translateFormat: true }) + let str = prettifyTime({ + log, + context: { + ...context + } + }) t.equal(str, '[13:15:00.000]') log = { timestamp: 1554642900000 } - str = prettifyTime({ log, translateFormat: true }) + str = prettifyTime({ + log, + context: { + ...context + } + }) t.equal(str, '[13:15:00.000]') log = { time: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: true }) + str = prettifyTime({ + log, + context: { + ...context + } + }) t.equal(str, '[13:15:00.000]') log = { timestamp: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: true }) + str = prettifyTime({ + log, + context: { + ...context + } + }) t.equal(str, '[13:15:00.000]') log = { time: 1554642900000 } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: 'd mmm yyyy H:MM' + } + }) t.equal(str, '[7 Apr 2019 13:15]') log = { timestamp: 1554642900000 } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: 'd mmm yyyy H:MM' + } + }) t.equal(str, '[7 Apr 2019 13:15]') log = { time: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: 'd mmm yyyy H:MM' + } + }) t.equal(str, '[7 Apr 2019 13:15]') log = { timestamp: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: 'd mmm yyyy H:MM' + } + }) t.equal(str, '[7 Apr 2019 13:15]') }) tap.test('passes through value', async t => { let log = { time: 1554642900000 } - let str = prettifyTime({ log }) + let str = prettifyTime({ + log, + context: { + ...context, + translateTime: undefined + } + }) t.equal(str, '[1554642900000]') log = { timestamp: 1554642900000 } - str = prettifyTime({ log }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: undefined + } + }) t.equal(str, '[1554642900000]') log = { time: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: undefined + } + }) t.equal(str, '[2019-04-07T09:15:00.000-04:00]') log = { timestamp: '2019-04-07T09:15:00.000-04:00' } - str = prettifyTime({ log }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: undefined + } + }) t.equal(str, '[2019-04-07T09:15:00.000-04:00]') }) tap.test('handles the 0 timestamp', async t => { let log = { time: 0 } - let str = prettifyTime({ log }) + let str = prettifyTime({ + log, + context: { + ...context, + translateTime: undefined + } + }) t.equal(str, '[0]') log = { timestamp: 0 } - str = prettifyTime({ log }) + str = prettifyTime({ + log, + context: { + ...context, + translateTime: undefined + } + }) t.equal(str, '[0]') }) @@ -86,15 +187,24 @@ tap.test('works with epoch as a number or string', (t) => { const epoch = 1522431328992 const asNumber = prettifyTime({ log: { time: epoch, msg: 'foo' }, - translateFormat: true + context: { + ...context, + translateTime: true + } }) const asString = prettifyTime({ log: { time: `${epoch}`, msg: 'foo' }, - translateFormat: true + context: { + ...context, + translateTime: true + } }) const invalid = prettifyTime({ log: { time: '2 days ago', msg: 'foo' }, - translateFormat: true + context: { + ...context, + translateTime: true + } }) t.same(asString, '[17:35:28.992]') t.same(asNumber, '[17:35:28.992]') @@ -104,8 +214,13 @@ tap.test('works with epoch as a number or string', (t) => { tap.test('uses custom prettifier', async t => { const str = prettifyTime({ log: { time: 0 }, - prettifier () { - return 'done' + context: { + ...context, + customPrettifiers: { + time () { + return 'done' + } + } } }) t.equal(str, 'done') diff --git a/test/error-objects.test.js b/test/error-objects.test.js index 15c11d32..e92e7ddf 100644 --- a/test/error-objects.test.js +++ b/test/error-objects.test.js @@ -313,7 +313,7 @@ test('error like objects tests', (t) => { ' statusCode: 500', ' originalStack: original stack', ' dataBaseSpecificError: {', - ' erroMessage: "some database error message"', + ' errorMessage: "some database error message"', ' evenMoreSpecificStuff: {', ' "someErrorRelatedObject": "error"', ' }', @@ -331,7 +331,7 @@ test('error like objects tests', (t) => { error.statusCode = 500 error.originalStack = 'original stack' error.dataBaseSpecificError = { - erroMessage: 'some database error message', + errorMessage: 'some database error message', evenMoreSpecificStuff: { someErrorRelatedObject: 'error' }