From 310da6ab036d3ffe898f223708704afec96ca303 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Sun, 28 Apr 2019 09:46:11 -0400 Subject: [PATCH] [WIP] Plain ndjson (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace fast-json-parse * Initial refactor to generic ndjson prettifier * Improve support for primitives * Move line joiner to utils and apply standard formatting * Move object prettification to utils function * Fix split in prettifyObject * Move "error" log prettification into utility function * Add support for string levels * Add "timestamp" as possible time key * v3.0.0-rc.1 * Remove Node 6 from Travis config * Add tests for utils.formatTime * Fix lint issue * Add tests for colors.js * Add docblock for joinLinesWithIndentation * Add tests for prettifyLevel * Add tests for prettifyMessage * Add tests for prettifyTime * Fix lint error * Pino seems to have changed order of some properties in the log. 🤷‍♂️ * Fix regex for time tests * Remove Node 12 until release * Add tests for prettifyMetadata * Added tests (#59) * test coverage for filtering out with search * obsolete check, ignored keys already filtered * basic tests for prettifyErrorLog & prettifyObject * Improve prettifyObject test and add docblock * Add docblock for prettifyErrorLog * Update readme * Update dependencies * v3.0.0-rc.2 * Remove Node 6 specific tests * Add back Node 12 to test suite * Fix tests for Node 12 --- .travis.yml | 3 +- LICENSE | 2 +- Readme.md | 56 +++--- index.js | 282 +++++++------------------- lib/colors.js | 66 ++++++ lib/constants.js | 35 +++- lib/utils.js | 332 +++++++++++++++++++++++++++++++ package.json | 16 +- test/basic.test.js | 25 +-- test/cli.test.js | 12 ++ test/error-objects.test.js | 16 +- test/lib/colors.test.js | 70 +++++++ test/lib/utils.internals.test.js | 83 ++++++++ test/lib/utils.public.test.js | 247 +++++++++++++++++++++++ 14 files changed, 977 insertions(+), 268 deletions(-) create mode 100644 lib/colors.js create mode 100644 lib/utils.js create mode 100644 test/lib/colors.test.js create mode 100644 test/lib/utils.internals.test.js create mode 100644 test/lib/utils.public.test.js diff --git a/.travis.yml b/.travis.yml index 75e5753a..9ac7e897 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: node_js node_js: - - "11" + - "12" - "10" - "8" - - "6" # before_install: # - curl -L https://unpkg.com/@pnpm/self-installer | node diff --git a/LICENSE b/LICENSE index 81fc574a..e2793b59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 the Pino team +Copyright (c) 2019 the Pino team Pino team listed at https://github.com/pinojs/pino#the-team diff --git a/Readme.md b/Readme.md index 90287aae..1e1de01d 100644 --- a/Readme.md +++ b/Readme.md @@ -1,16 +1,21 @@ # pino-pretty + [![Build Status](https://travis-ci.org/pinojs/pino-pretty.svg?branch=master)](https://travis-ci.org/pinojs/pino-pretty) [![Coverage Status](https://coveralls.io/repos/github/pinojs/pino-pretty/badge.svg?branch=master)](https://coveralls.io/github/pinojs/pino-pretty?branch=master) -This module provides a basic log prettifier for the [Pino](https://getpino.io/) -logging library. It reads a standard Pino log line like: +This module provides a basic [ndjson](http://ndjson.org/) formatter. If an +incoming line looks like it could be a log line from an ndjson logger, in +particular the [Pino](https://getpino.io/) logging library, then it will apply +extra formatting by considering things like the log level and timestamp. + +A standard Pino log line like: ``` {"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo","v":1} ``` -And formats it to: +Will format to: ``` [1522431328992] INFO (42 on foo): hello world @@ -34,12 +39,11 @@ prettified logs will look like: $ npm install -g pino-pretty ``` - ## Usage -It's recommended to use `pino-pretty` with `pino` -by piping output to the CLI tool: +It's recommended to use `pino-pretty` with `pino` +by piping output to the CLI tool: ```sh pino app.js | pino-pretty @@ -48,38 +52,38 @@ pino app.js | pino-pretty ### CLI Arguments -+ `--colorize` (`-c`): Adds terminal color escape sequences to the output. -+ `--crlf` (`-f`): Appends carriage return and line feed, instead of just a line -feed, to the formatted log line. -+ `--errorProps` (`-e`): When formatting an error object, display this list -of properties. The list should be a comma separated list of properties Default: `''`. -+ `--levelFirst` (`-l`): Display the log level name before the logged date and time. -+ `--errorLikeObjectKeys` (`-k`): Define the log keys that are associated with -error like objects. Default: `err,error`. -+ `--messageKey` (`-m`): Define the key that contains the main log message. -Default: `msg`. -+ `--translateTime` (`-t`): Translate the epoch time value into a human readable -date and time string. This flag also can set the format string to apply when -translating the date to human readable format. For a list of available pattern -letters see the [`dateformat` documentation](https://www.npmjs.com/package/dateformat). +- `--colorize` (`-c`): Adds terminal color escape sequences to the output. +- `--crlf` (`-f`): Appends carriage return and line feed, instead of just a line + feed, to the formatted log line. +- `--errorProps` (`-e`): When formatting an error object, display this list + of properties. The list should be a comma separated list of properties Default: `''`. +- `--levelFirst` (`-l`): Display the log level name before the logged date and time. +- `--errorLikeObjectKeys` (`-k`): Define the log keys that are associated with + error like objects. Default: `err,error`. +- `--messageKey` (`-m`): Define the key that contains the main log message. + Default: `msg`. +- `--translateTime` (`-t`): Translate the epoch time value into a human readable + date and time string. This flag also can set the format string to apply when + translating the date to human readable format. For a list of available pattern + letters see the [`dateformat` documentation](https://www.npmjs.com/package/dateformat). - The default format is `yyyy-mm-dd HH:MM:ss.l o` in UTC. - Require a `SYS:` prefix to translate time to the local system's timezone. A shortcut `SYS:standard` to translate time to `yyyy-mm-dd HH:MM:ss.l o` in system timezone. -+ `--search` (`-s`): Specify a search pattern according to +- `--search` (`-s`): Specify a search pattern according to [jmespath](http://jmespath.org/). -+ `--ignore` (`-i`): Ignore one or several keys: (`-i time,hostname`) +- `--ignore` (`-i`): Ignore one or several keys: (`-i time,hostname`) -## Programmatic Integration +## Programmatic Integration We recommend against using `pino-pretty` in production, and highly recommend installing `pino-pretty` as a development dependency. -When installed, `pino-pretty` will be used by `pino` as the default +When installed, `pino-pretty` will be used by `pino` as the default prettifier. -Install `pino-pretty` alongside `pino` and set the +Install `pino-pretty` alongside `pino` and set the `prettyPrint` option to `true`: ```js @@ -109,7 +113,7 @@ See the [Options](#options) section for all possible options. ### Options `pino-pretty` exports a factory function that can be used to format log strings. -This factory function is used internally by pino, and accepts an options argument +This factory function is used internally by Pino, and accepts an options argument with keys corresponding to the options described in [CLI Arguments](#cliargs): ```js diff --git a/index.js b/index.js index fd04c13e..ccb55e35 100644 --- a/index.js +++ b/index.js @@ -1,62 +1,40 @@ 'use strict' const chalk = require('chalk') -const dateformat = require('dateformat') -// remove jsonParser once Node 6 is not supported anymore -const jsonParser = require('fast-json-parse') const jmespath = require('jmespath') -const stringifySafe = require('fast-safe-stringify') - -const CONSTANTS = require('./lib/constants') - -const levels = { - default: 'USERLVL', - 60: 'FATAL', - 50: 'ERROR', - 40: 'WARN ', - 30: 'INFO ', - 20: 'DEBUG', - 10: 'TRACE' +const colors = require('./lib/colors') +const { ERROR_LIKE_KEYS, MESSAGE_KEY } = require('./lib/constants') +const { + isObject, + prettifyErrorLog, + prettifyLevel, + prettifyMessage, + prettifyMetadata, + prettifyObject, + prettifyTime +} = require('./lib/utils') + +const bourne = require('bourne') +const jsonParser = input => { + try { + return { value: bourne.parse(input, { protoAction: 'remove' }) } + } catch (err) { + return { err } + } } const defaultOptions = { colorize: chalk.supportsColor, crlf: false, - errorLikeObjectKeys: ['err', 'error'], + errorLikeObjectKeys: ERROR_LIKE_KEYS, errorProps: '', levelFirst: false, - messageKey: CONSTANTS.MESSAGE_KEY, + messageKey: MESSAGE_KEY, translateTime: false, useMetadata: false, outputStream: process.stdout } -function isObject (input) { - return Object.prototype.toString.apply(input) === '[object Object]' -} - -function isPinoLog (log) { - return log && (log.hasOwnProperty('v') && log.v === 1) -} - -function formatTime (epoch, translateTime) { - const instant = new Date(epoch) - if (translateTime === true) { - return dateformat(instant, 'UTC:' + CONSTANTS.DATE_FORMAT) - } else { - const upperFormat = translateTime.toUpperCase() - return (!upperFormat.startsWith('SYS:')) - ? dateformat(instant, 'UTC:' + translateTime) - : (upperFormat === 'SYS:STANDARD') - ? dateformat(instant, CONSTANTS.DATE_FORMAT) - : dateformat(instant, translateTime.slice(4)) - } -} - -function nocolor (input) { - return input -} - module.exports = function prettyFactory (options) { const opts = Object.assign({}, defaultOptions, options) const EOL = opts.crlf ? '\r\n' : '\n' @@ -66,28 +44,7 @@ module.exports = function prettyFactory (options) { const errorProps = opts.errorProps.split(',') const ignoreKeys = opts.ignore ? new Set(opts.ignore.split(',')) : undefined - const color = { - default: nocolor, - 60: nocolor, - 50: nocolor, - 40: nocolor, - 30: nocolor, - 20: nocolor, - 10: nocolor, - message: nocolor - } - if (opts.colorize) { - const ctx = new chalk.constructor({ enabled: true, level: 3 }) - color.default = ctx.white - color[60] = ctx.bgRed - color[50] = ctx.red - color[40] = ctx.yellow - color[30] = ctx.green - color[20] = ctx.blue - color[10] = ctx.grey - color.message = ctx.cyan - } - + const colorizer = colors(opts.colorize) const search = opts.search return pretty @@ -97,7 +54,7 @@ module.exports = function prettyFactory (options) { if (!isObject(inputData)) { const parsed = jsonParser(inputData) log = parsed.value - if (parsed.err || !isPinoLog(log)) { + if (parsed.err) { // pass through return inputData + EOL } @@ -105,6 +62,11 @@ module.exports = function prettyFactory (options) { log = inputData } + // Short-circuit for spec allowed primitive values. + if ([null, true, false].includes(log)) { + return `${log}\n` + } + if (search && !jmespath.search(log, search)) { return } @@ -118,167 +80,67 @@ module.exports = function prettyFactory (options) { }, {}) } - const standardKeys = [ - 'pid', - 'hostname', - 'name', - 'level', - 'time', - 'v' - ] + const prettifiedLevel = prettifyLevel({ log, colorizer }) + const prettifiedMessage = prettifyMessage({ log, messageKey, colorizer }) + const prettifiedMetadata = prettifyMetadata({ log }) + const prettifiedTime = prettifyTime({ log, translateFormat: opts.translateTime }) - if (opts.translateTime) { - log.time = formatTime(log.time, opts.translateTime) + let line = '' + if (opts.levelFirst && prettifiedLevel) { + line = `${prettifiedLevel}` } - var line = log.time ? `[${log.time}]` : '' - - const coloredLevel = log.level - ? levels.hasOwnProperty(log.level) - ? color[log.level](levels[log.level]) - : color.default(levels.default) - : '' - if (opts.levelFirst) { - line = `${coloredLevel} ${line}` - } else { - // If the line is not empty (timestamps are enabled) output it - // with a space after it - otherwise output the empty string - const lineOrEmpty = line && line + ' ' - line = `${lineOrEmpty}${coloredLevel}` + if (prettifiedTime && line === '') { + line = `${prettifiedTime}` + } else if (prettifiedTime) { + line = `${line} ${prettifiedTime}` } - if (log.name || log.pid || log.hostname) { - line += ' (' - - if (log.name) { - line += log.name - } - - if (log.name && log.pid) { - line += '/' + log.pid - } else if (log.pid) { - line += log.pid - } - - if (log.hostname) { - if (line.slice(-1) !== '(') { - line += ' ' - } - line += 'on ' + log.hostname + if (!opts.levelFirst && prettifiedLevel) { + if (line.length > 0) { + line = `${line} ${prettifiedLevel}` + } else { + line = prettifiedLevel } + } - line += ')' + if (prettifiedMetadata) { + line = `${line} ${prettifiedMetadata}:` } - line += line ? ': ' : '' + if (line.endsWith(':') === false && line !== '') { + line += ':' + } - if (log[messageKey] && typeof log[messageKey] === 'string') { - line += color.message(log[messageKey]) + if (prettifiedMessage) { + line = `${line} ${prettifiedMessage}` } - line += EOL + if (line.length > 0) { + line += EOL + } if (log.type === 'Error' && log.stack) { - const stack = log.stack - line += IDENT + joinLinesWithIndentation(stack) + EOL - - let propsForPrint - if (errorProps && errorProps.length > 0) { - // don't need print these props for 'Error' object - const excludedProps = standardKeys.concat([messageKey, 'type', 'stack']) - - if (errorProps[0] === '*') { - // print all log props excluding 'excludedProps' - propsForPrint = Object.keys(log).filter((prop) => excludedProps.indexOf(prop) < 0) - } else { - // print props from 'errorProps' only - // but exclude 'excludedProps' - propsForPrint = errorProps.filter((prop) => excludedProps.indexOf(prop) < 0) - } - - for (var i = 0; i < propsForPrint.length; i++) { - const key = propsForPrint[i] - if (!log.hasOwnProperty(key)) continue - if (log[key] instanceof Object) { - // call 'filterObjects' with 'excludeStandardKeys' = false - // because nested property might contain property from 'standardKeys' - line += key + ': {' + EOL + filterObjects(log[key], '', errorLikeObjectKeys, false) + '}' + EOL - continue - } - line += key + ': ' + log[key] + EOL - } - } + const prettifiedErrorLog = prettifyErrorLog({ + log, + errorLikeKeys: errorLikeObjectKeys, + errorProperties: errorProps, + ident: IDENT, + eol: EOL + }) + line += prettifiedErrorLog } else { - line += filterObjects(log, typeof log[messageKey] === 'string' ? messageKey : undefined, errorLikeObjectKeys) + const skipKeys = typeof log[messageKey] === 'string' ? [messageKey] : undefined + const prettifiedObject = prettifyObject({ + input: log, + skipKeys, + errorLikeKeys: errorLikeObjectKeys, + eol: EOL, + ident: IDENT + }) + line += prettifiedObject } return line - - function joinLinesWithIndentation (value) { - const lines = value.split(/\r?\n/) - for (var i = 1; i < lines.length; i++) { - lines[i] = IDENT + lines[i] - } - return lines.join(EOL) - } - - function filterObjects (value, messageKey, errorLikeObjectKeys, excludeStandardKeys) { - errorLikeObjectKeys = errorLikeObjectKeys || [] - - const keys = Object.keys(value) - const filteredKeys = [] - - if (messageKey) { - filteredKeys.push(messageKey) - } - - if (excludeStandardKeys !== false) { - Array.prototype.push.apply(filteredKeys, standardKeys) - } - - let result = '' - - for (var i = 0; i < keys.length; i += 1) { - if (errorLikeObjectKeys.indexOf(keys[i]) !== -1 && value[keys[i]] !== undefined) { - const lines = stringifySafe(value[keys[i]], null, 2) - if (lines === undefined) continue - const arrayOfLines = ( - IDENT + keys[i] + ': ' + - joinLinesWithIndentation(lines) + - EOL - ).split('\n') - - for (var j = 0; j < arrayOfLines.length; j += 1) { - if (j !== 0) { - result += '\n' - } - - const line = arrayOfLines[j] - - if (/^\s*"stack"/.test(line)) { - const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line) - - if (matches && matches.length === 3) { - const indentSize = /^\s*/.exec(line)[0].length + 4 - const indentation = ' '.repeat(indentSize) - - result += matches[1] + '\n' + indentation + JSON.parse(matches[2]).replace(/\n/g, '\n' + indentation) - } - } else { - result += line - } - } - } else if (filteredKeys.indexOf(keys[i]) < 0) { - if (value[keys[i]] !== undefined) { - const lines = stringifySafe(value[keys[i]], null, 2) - if (lines !== undefined) { - result += IDENT + keys[i] + ': ' + joinLinesWithIndentation(lines) + EOL - } - } - } - } - - return result - } } } diff --git a/lib/colors.js b/lib/colors.js new file mode 100644 index 00000000..e246852d --- /dev/null +++ b/lib/colors.js @@ -0,0 +1,66 @@ +'use strict' + +const { LEVELS, LEVEL_NAMES } = require('./constants') + +const nocolor = input => input +const plain = { + default: nocolor, + 60: nocolor, + 50: nocolor, + 40: nocolor, + 30: nocolor, + 20: nocolor, + 10: nocolor, + message: nocolor +} + +const chalk = require('chalk') +const ctx = new chalk.constructor({ enabled: true, level: 3 }) +const colored = { + default: ctx.white, + 60: ctx.bgRed, + 50: ctx.red, + 40: ctx.yellow, + 30: ctx.green, + 20: ctx.blue, + 10: ctx.grey, + message: ctx.cyan +} + +function colorizeLevel (level, colorizer) { + if (Number.isInteger(+level)) { + return LEVELS.hasOwnProperty(level) + ? colorizer[level](LEVELS[level]) + : colorizer.default(LEVELS.default) + } + const levelNum = LEVEL_NAMES[level.toLowerCase()] || 'default' + return colorizer[levelNum](LEVELS[levelNum]) +} + +function plainColorizer (level) { + return colorizeLevel(level, plain) +} +plainColorizer.message = plain.message + +function coloredColorizer (level) { + return colorizeLevel(level, colored) +} +coloredColorizer.message = colored.message + +/** + * Factory function get a function to colorized levels. The returned function + * also includes a `.message(str)` method to colorize strings. + * + * @param {bool} [useColors=false] When `true` a function that applies standard + * terminal colors is returned. + * + * @returns {function} `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 + * colors as the integer `level` and will also default to `USERLVL` if the given + * string is not a recognized level name. + */ +module.exports = function getColorizer (useColors = false) { + return useColors ? coloredColorizer : plainColorizer +} diff --git a/lib/constants.js b/lib/constants.js index ff0c4b0a..9cda6e5a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -2,5 +2,38 @@ module.exports = { DATE_FORMAT: 'yyyy-mm-dd HH:MM:ss.l o', - MESSAGE_KEY: 'msg' + + ERROR_LIKE_KEYS: ['err', 'error'], + + MESSAGE_KEY: 'msg', + + LEVELS: { + default: 'USERLVL', + 60: 'FATAL', + 50: 'ERROR', + 40: 'WARN ', + 30: 'INFO ', + 20: 'DEBUG', + 10: 'TRACE' + }, + + LEVEL_NAMES: { + fatal: 60, + error: 50, + warn: 40, + info: 30, + debug: 20, + trace: 10 + }, + + // Object keys that probably came from a logger like Pino or Bunyan. + LOGGER_KEYS: [ + 'pid', + 'hostname', + 'name', + 'level', + 'time', + 'timestamp', + 'v' + ] } diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..bc108e25 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,332 @@ +'use strict' + +const dateformat = require('dateformat') +const stringifySafe = require('fast-safe-stringify') +const defaultColorizer = require('./colors')() +const { + DATE_FORMAT, + ERROR_LIKE_KEYS, + MESSAGE_KEY, + LOGGER_KEYS +} = require('./constants') + +module.exports = { + isObject, + prettifyErrorLog, + prettifyLevel, + prettifyMessage, + prettifyMetadata, + prettifyObject, + prettifyTime +} + +module.exports.internals = { + formatTime, + joinLinesWithIndentation +} + +/** + * Converts a given `epoch` to a desired display format. + * + * @param {number|string} epoch The time to convert. May be any value that is + * valid for `new Date()`. + * @param {bool|string} [translateTime=false] When `false`, the given `epoch` + * will simply be returned. When `true`, the given `epoch` will be converted + * to a string at UTC using the `DATE_FORMAT` constant. If `translateTime` is + * a string, the following rules are available: + * + * - ``: The string is a literal format string. This format + * string will be used to interpret the `epoch` and return a display string + * at UTC. + * - `SYS:STANDARD`: The returned display string will follow the `DATE_FORMAT` + * constant at the system's local timezone. + * - `SYS:`: The returned display string will follow the given + * `` at the system's local timezone. + * - `UTC:`: The returned display string will follow the given + * `` at UTC. + * + * @returns {number|string} The formatted time. + */ +function formatTime (epoch, translateTime = false) { + if (translateTime === false) { + return epoch + } + + const instant = new Date(epoch) + if (translateTime === true) { + return dateformat(instant, 'UTC:' + DATE_FORMAT) + } + + const upperFormat = translateTime.toUpperCase() + if (upperFormat === 'SYS:STANDARD') { + return dateformat(instant, DATE_FORMAT) + } + + const prefix = upperFormat.substr(0, 4) + if (prefix === 'SYS:' || prefix === 'UTC:') { + if (prefix === 'UTC:') { + return dateformat(instant, translateTime) + } + return dateformat(instant, translateTime.slice(4)) + } + + return dateformat(instant, `UTC:${translateTime}`) +} + +function isObject (input) { + return Object.prototype.toString.apply(input) === '[object Object]' +} + +/** + * Given a string with line separators, either `\r\n` or `\n`, add indentation + * to all lines subsequent to the first line and rejoin the lines using an + * end of line sequence. + * + * @param {object} input + * @param {string} input.input The string to split and reformat. + * @param {string} [input.ident] The indentation string. Default: ` ` (4 spaces). + * @param {string} [input.eol] The end of line sequence to use when rejoining + * the lines. Default: `'\n'`. + * + * @returns {string} A string with lines subsequent to the first indented + * with the given indentation sequence. + */ +function joinLinesWithIndentation ({ input, ident = ' ', eol = '\n' }) { + const lines = input.split(/\r?\n/) + for (var i = 1; i < lines.length; i += 1) { + lines[i] = ident + lines[i] + } + return lines.join(eol) +} + +/** + * Given a log object that has a `type: 'Error'` key, prettify the object and + * return the result. In other + * + * @param {object} input + * @param {object} input.log The error log to prettify. + * @param {string} [input.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. + * @param {string} [input.ident] The sequence to use for indentation. Default: `' '`. + * @param {string} [input.eol] The sequence to use for EOL. Default: `'\n'`. + * @param {string[]} [input.errorLikeKeys] A set of keys that should be considered + * to have error objects as values. Default: `ERROR_LIKE_KEYS` constant. + * @param {string[]} [input.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 sibiling properties should be prettified. Default: `[]`. + * + * @returns {string} A sring that represents the prettified error log. + */ +function prettifyErrorLog ({ + log, + messageKey = MESSAGE_KEY, + ident = ' ', + eol = '\n', + errorLikeKeys = ERROR_LIKE_KEYS, + errorProperties = [] +}) { + const stack = log.stack + const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol }) + let result = `${ident}${joinedLines}${eol}` + + if (errorProperties.length > 0) { + const excludeProperties = LOGGER_KEYS.concat(messageKey, 'type', 'stack') + let propertiesToPrint + if (errorProperties[0] === '*') { + // Print all sibling properties except for the standard exclusions. + propertiesToPrint = Object.keys(log).filter(k => excludeProperties.includes(k) === false) + } else { + // Print only sepcified properties unless the property is a standard exclusion. + propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false) + } + + for (var i = 0; i < propertiesToPrint.length; i += 1) { + const key = propertiesToPrint[i] + if (key in log === false) continue + if (isObject(log[key])) { + // 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 }) + result = `${result}${key}: {${eol}${prettifiedObject}}${eol}` + continue + } + result = `${result}${key}: ${log[key]}${eol}` + } + } + + return result +} + +/** + * Checks if the passed in log has a `level` value and returns a prettified + * string for that level if so. + * + * @param {object} input + * @param {object} input.log The log object which should have a `level` property. + * @param {function} [input.colorizer] A colorizer function that accepts a level + * value and returns a colorized string. Default: a no-op colorizer. + * + * @returns {undefined|string} If `log` does not have a `level` property then + * `undefined` will be returned. Otherwise, a string from the specified + * `colorizer` is returned. + */ +function prettifyLevel ({ log, colorizer = defaultColorizer }) { + if ('level' in log === false) return undefined + return colorizer(log.level) +} + +/** + * Prettifies a message string if the given `log` has a message property. + * + * @param {object} input + * @param {object} input.log The log object with the message to colorize. + * @param {string} [input.messageKey='msg'] The property of the `log` that is the + * message to be prettified. + * @param {function} [input.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. + * + * @returns {undefined|string} If the message key is not found, or the message + * key is not a string, then `undefined` will be returned. Otherwise, a string + * that is the prettified message. + */ +function prettifyMessage ({ log, messageKey = MESSAGE_KEY, colorizer = defaultColorizer }) { + if (messageKey in log === false) return undefined + if (typeof log[messageKey] !== 'string') return undefined + return colorizer.message(log[messageKey]) +} + +/** + * Prettifies metadata that is usually present in a Pino log line. It looks for + * fields `name`, `pid`, and `hostname` and returns a formatted string using + * the fields it finds. + * + * @param {object} input + * @param {object} input.log The log that may or may not contain metadata to + * be prettified. + * + * @returns {undefined|string} If no metadata is found then `undefined` is + * returned. Otherwise, a string of prettified metadata is returned. + */ +function prettifyMetadata ({ log }) { + if (log.name || log.pid || log.hostname) { + let line = '(' + + if (log.name) { + line += log.name + } + + if (log.name && log.pid) { + line += '/' + log.pid + } else if (log.pid) { + line += log.pid + } + + if (log.hostname) { + // If `pid` and `name` were in the ignore keys list then we don't need + // the leading space. + line += `${line === '(' ? 'on' : ' on'} ${log.hostname}` + } + + line += ')' + return line + } + return undefined +} + +/** + * Prettifies a standard object. Special care is taken when processing the object + * to handle child objects that are attached to keys known to contain error + * objects. + * + * @param {object} input + * @param {object} input.input The object to prettify. + * @param {string} [input.ident] The identation sequence to use. Default: `' '`. + * @param {string} [input.eol] The EOL sequence to use. Default: `'\n'`. + * @param {string[]} [input.skipKeys] A set of object keys to exclude from the + * prettified result. Default: `[]`. + * @param {string[]} [input.errorLikeKeys] A set of object keys that contain + * error objects. Default: `ERROR_LIKE_KEYS` constant. + * @param {boolean} [input.excludeLoggerKeys] Indicates if known logger specific + * keys should be excluded from prettification. Default: `true`. + * + * @returns {string} The prettified string. This can be as little as `''` if + * there was nothing to prettify. + */ +function prettifyObject ({ + input, + ident = ' ', + eol = '\n', + skipKeys = [], + errorLikeKeys = ERROR_LIKE_KEYS, + excludeLoggerKeys = true +}) { + const objectKeys = Object.keys(input) + const keysToIgnore = [].concat(skipKeys) + + if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS) + + let result = '' + + const keysToIterate = objectKeys.filter(k => keysToIgnore.includes(k) === false) + for (var i = 0; i < objectKeys.length; i += 1) { + const keyName = keysToIterate[i] + const keyValue = input[keyName] + + if (keyValue === undefined) continue + + const lines = stringifySafe(input[keyName], null, 2) + if (lines === undefined) continue + const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol }) + + if (errorLikeKeys.includes(keyName) === true) { + const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol) + for (var j = 0; j < splitLines.length; j += 1) { + if (j !== 0) result += eol + + const line = splitLines[j] + if (/^\s*"stack"/.test(line)) { + const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line) + if (matches && matches.length === 3) { + const indentSize = /^\s*/.exec(line)[0].length + 4 + const indentation = ' '.repeat(indentSize) + const stackMessage = matches[2] + result += matches[1] + eol + indentation + JSON.parse(stackMessage).replace(/\n/g, eol + indentation) + } + } else { + result += line + } + } + } else { + result += `${ident}${keyName}: ${joinedLines}${eol}` + } + } + + return result +} + +/** + * Prettifies a timestamp if the given `log` has either `time` or `timestamp` + * properties. + * + * @param {object} input + * @param {object} input.log The log object with the timestamp to be prettified. + * @param {bool|string} [input.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. + * + * @returns {undefined|string} If a timestamp property cannot be found then + * `undefined` is returned. Otherwise, the prettified time is returned as a + * string. + */ +function prettifyTime ({ log, translateFormat = undefined }) { + if ('time' in log === false && 'timestamp' in log === false) return undefined + if (translateFormat) { + return '[' + formatTime(log.time || log.timestamp, translateFormat) + ']' + } + return `[${log.time || log.timestamp}]` +} diff --git a/package.json b/package.json index 0cc01058..b01a16f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pino-pretty", - "version": "2.6.1", + "version": "3.0.0-rc.2", "description": "Prettifier for Pino log lines", "main": "index.js", "bin": { @@ -30,21 +30,21 @@ "test" ], "dependencies": { - "args": "^5.0.0", - "chalk": "^2.3.2", + "args": "^5.0.1", + "bourne": "^1.1.2", + "chalk": "^2.4.2", "dateformat": "^3.0.3", - "fast-json-parse": "^1.0.3", "fast-safe-stringify": "^2.0.6", "jmespath": "^0.15.0", "pump": "^3.0.0", - "readable-stream": "^3.0.6", - "split2": "^3.0.0" + "readable-stream": "^3.3.0", + "split2": "^3.1.1" }, "devDependencies": { - "pino": "^5.9.0", + "pino": "^5.12.2", "pre-commit": "^1.2.2", "snazzy": "^8.0.0", "standard": "^12.0.1", - "tap": "^12.1.0" + "tap": "^12.6.1" } } diff --git a/test/basic.test.js b/test/basic.test.js index 2a1578ec..ea95bdbb 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -179,11 +179,12 @@ test('basic prettifier tests', (t) => { log.info('foo') }) + // TODO: 2019-03-30 -- We don't really want the indentation in this case? Or at least some better formatting. t.test('handles missing time', (t) => { t.plan(1) const pretty = prettyFactory() const formatted = pretty('{"hello":"world"}') - t.is(formatted, '{"hello":"world"}\n') + t.is(formatted, ' hello: "world"\n') }) t.test('handles missing pid, hostname and name', (t) => { @@ -334,11 +335,18 @@ test('basic prettifier tests', (t) => { log.info('hello world') }) - t.test('handles `null` input', (t) => { - t.plan(1) + t.test('handles spec allowed primitives', (t) => { const pretty = prettyFactory() - const formatted = pretty(null) + let formatted = pretty(null) t.is(formatted, 'null\n') + + formatted = pretty(true) + t.is(formatted, 'true\n') + + formatted = pretty(false) + t.is(formatted, 'false\n') + + t.end() }) t.test('handles `undefined` input', (t) => { @@ -348,13 +356,6 @@ test('basic prettifier tests', (t) => { t.is(formatted, 'undefined\n') }) - t.test('handles `true` input', (t) => { - t.plan(1) - const pretty = prettyFactory() - const formatted = pretty(true) - t.is(formatted, 'true\n') - }) - t.test('handles customLogLevel', (t) => { t.plan(1) const pretty = prettyFactory() @@ -520,7 +521,7 @@ test('basic prettifier tests', (t) => { write (chunk, _, cb) { t.is( chunk + '', - `[${epoch}] INFO (${pid} on ${hostname}): \n a: {\n "b": "c"\n }\n n: null\n` + `[${epoch}] INFO (${pid} on ${hostname}):\n a: {\n "b": "c"\n }\n n: null\n` ) cb() } diff --git a/test/cli.test.js b/test/cli.test.js index 1f628b7e..80afadf6 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -53,6 +53,18 @@ test('cli', (t) => { t.tearDown(() => child.kill()) }) + t.test('does search but finds only 1 out of 2', (t) => { + t.plan(1) + const child = spawn(process.argv0, [bin, '-s', 'msg == `hello world`']) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.is(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`) + }) + child.stdin.write(logLine.replace('hello world', 'hello universe')) + child.stdin.write(logLine) + t.tearDown(() => child.kill()) + }) + t.test('does ignore multiple keys', (t) => { t.plan(1) const child = spawn(process.argv0, [bin, '-i', 'pid,hostname']) diff --git a/test/error-objects.test.js b/test/error-objects.test.js index 798f1e87..636b2abb 100644 --- a/test/error-objects.test.js +++ b/test/error-objects.test.js @@ -89,14 +89,14 @@ test('error like objects tests', (t) => { const formatted = pretty(chunk.toString()) const lines = formatted.split('\n') t.is(lines.length, expected.length + 6) - t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}): `) + t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}):`) t.match(lines[1], /\s{4}err: {/) t.match(lines[2], /\s{6}"type": "Error",/) t.match(lines[3], /\s{6}"message": "hello world",/) t.match(lines[4], /\s{6}"stack":/) t.match(lines[5], /\s{6}Error: hello world/) - // Node 6 starts stack with "at Error (native)" - t.match(lines[6], /\s{10}(at Test.t.test|at Error \(native\))/) + // Node 12 labels the test `` + t.match(lines[6], /\s{10}(at Test.t.test|at Test.)/) cb() } })) @@ -120,7 +120,7 @@ test('error like objects tests', (t) => { const formatted = pretty(chunk.toString()) const lines = formatted.split('\n') t.is(lines.length, expected.length + 6) - t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}): `) + t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}):`) t.match(lines[1], /\s{4}err: {$/) t.match(lines[2], /\s{6}"type": "Error",$/) t.match(lines[3], /\s{6}"message": "hello world",$/) @@ -150,14 +150,14 @@ test('error like objects tests', (t) => { const formatted = pretty(chunk.toString()) const lines = formatted.split('\n') t.is(lines.length, expected.length + 7) - t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}): `) + t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}):`) t.match(lines[1], /\s{4}err: {/) t.match(lines[2], /\s{6}"type": "Error",/) t.match(lines[3], /\s{6}"message": "hello world",/) t.match(lines[4], /\s{6}"stack":/) t.match(lines[5], /\s{6}Error: hello world/) - // Node 6 starts stack with "at Error (native)" - t.match(lines[6], /\s{10}(at Test.t.test|at Error \(native\))/) + // Node 12 labels the test `` + t.match(lines[6], /\s{10}(at Test.t.test|at Test.)/) t.match(lines[lines.length - 3], /\s{6}"anotherField": "dummy value"/) cb() } @@ -236,7 +236,7 @@ test('error like objects tests', (t) => { const lines = formatted.split('\n') lines.shift(); lines.pop() for (var i = 0; i < lines.length; i += 1) { - t.is(lines[i], expectedLines[i]) + t.true(expectedLines.includes(lines[i])) } cb() } diff --git a/test/lib/colors.test.js b/test/lib/colors.test.js new file mode 100644 index 00000000..6da351ba --- /dev/null +++ b/test/lib/colors.test.js @@ -0,0 +1,70 @@ +'use strict' + +const { test } = require('tap') +const getColorizer = require('../../lib/colors') + +test('returns default colorizer', async t => { + const colorizer = getColorizer() + let colorized = colorizer(10) + t.is(colorized, 'TRACE') + + colorized = colorizer(20) + t.is(colorized, 'DEBUG') + + colorized = colorizer(30) + t.is(colorized, 'INFO ') + + colorized = colorizer(40) + t.is(colorized, 'WARN ') + + colorized = colorizer(50) + t.is(colorized, 'ERROR') + + colorized = colorizer(60) + t.is(colorized, 'FATAL') + + colorized = colorizer(900) + t.is(colorized, 'USERLVL') + + colorized = colorizer('info') + t.is(colorized, 'INFO ') + + colorized = colorizer('use-default') + t.is(colorized, 'USERLVL') + + colorized = colorizer.message('foo') + t.is(colorized, 'foo') +}) + +test('returns colorizing colorizer', async t => { + const colorizer = getColorizer(true) + let colorized = colorizer(10) + t.is(colorized, '\u001B[90mTRACE\u001B[39m') + + colorized = colorizer(20) + t.is(colorized, '\u001B[34mDEBUG\u001B[39m') + + colorized = colorizer(30) + t.is(colorized, '\u001B[32mINFO \u001B[39m') + + colorized = colorizer(40) + t.is(colorized, '\u001B[33mWARN \u001B[39m') + + colorized = colorizer(50) + t.is(colorized, '\u001B[31mERROR\u001B[39m') + + colorized = colorizer(60) + t.is(colorized, '\u001B[41mFATAL\u001B[49m') + + colorized = colorizer(900) + t.is(colorized, '\u001B[37mUSERLVL\u001B[39m') + + colorized = colorizer('info') + t.is(colorized, '\u001B[32mINFO \u001B[39m') + + colorized = colorizer('use-default') + t.is(colorized, '\u001B[37mUSERLVL\u001B[39m') + + colorized = colorizer.message('foo') + t.is(colorized, '\u001B[36mfoo\u001B[39m') +}) diff --git a/test/lib/utils.internals.test.js b/test/lib/utils.internals.test.js new file mode 100644 index 00000000..ab5e41d6 --- /dev/null +++ b/test/lib/utils.internals.test.js @@ -0,0 +1,83 @@ +'use strict' + +const tap = require('tap') +const { internals } = require('../../lib/utils') + +tap.test('#joinLinesWithIndentation', t => { + t.test('joinLinesWithIndentation adds indentation to beginning of subsequent lines', async t => { + const input = 'foo\nbar\nbaz' + const result = internals.joinLinesWithIndentation({ input }) + t.is(result, 'foo\n bar\n baz') + }) + + t.test('joinLinesWithIndentation accepts custom indentation, line breaks, and eol', async t => { + const input = 'foo\nbar\r\nbaz' + const result = internals.joinLinesWithIndentation({ input, ident: ' ', eol: '^' }) + t.is(result, 'foo^ bar^ baz') + }) + + t.end() +}) + +tap.test('#formatTime', t => { + const dateStr = '2019-04-06T13:30:00.000-04:00' + const epoch = new Date(dateStr) + const epochMS = epoch.getTime() + + t.test('passes through epoch if `translateTime` is `false`', async t => { + const formattedTime = internals.formatTime(epochMS) + t.is(formattedTime, epochMS) + }) + + t.test('translates epoch milliseconds if `translateTime` is `true`', async t => { + const formattedTime = internals.formatTime(epochMS, true) + t.is(formattedTime, '2019-04-06 17:30:00.000 +0000') + }) + + t.test('translates epoch milliseconds to UTC string given format', async t => { + const formattedTime = internals.formatTime(epochMS, 'd mmm yyyy H:MM') + t.is(formattedTime, '6 Apr 2019 17:30') + }) + + t.test('translates epoch milliseconds to SYS:STANDARD', async t => { + const formattedTime = internals.formatTime(epochMS, 'SYS:STANDARD') + t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/) + }) + + t.test('translates epoch milliseconds to SYS:', async t => { + const formattedTime = internals.formatTime(epochMS, 'SYS:d mmm yyyy H:MM') + t.match(formattedTime, /\d{1} \w{3} \d{4} \d{2}:\d{2}/) + }) + + t.test('passes through date string if `translateTime` is `false`', async t => { + const formattedTime = internals.formatTime(dateStr) + t.is(formattedTime, dateStr) + }) + + t.test('translates date string if `translateTime` is `true`', async t => { + const formattedTime = internals.formatTime(dateStr, true) + t.is(formattedTime, '2019-04-06 17:30:00.000 +0000') + }) + + t.test('translates date string to UTC string given format', async t => { + const formattedTime = internals.formatTime(dateStr, 'd mmm yyyy H:MM') + t.is(formattedTime, '6 Apr 2019 17:30') + }) + + t.test('translates date string to SYS:STANDARD', async t => { + const formattedTime = internals.formatTime(dateStr, 'SYS:STANDARD') + t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/) + }) + + t.test('translates date string to UTC:', async t => { + const formattedTime = internals.formatTime(dateStr, 'UTC:d mmm yyyy H:MM') + t.is(formattedTime, '6 Apr 2019 17:30') + }) + + t.test('translates date string to SYS:', async t => { + const formattedTime = internals.formatTime(dateStr, 'SYS:d mmm yyyy H:MM') + t.match(formattedTime, /\d{1} \w{3} \d{4} \d{2}:\d{2}/) + }) + + t.end() +}) diff --git a/test/lib/utils.public.test.js b/test/lib/utils.public.test.js new file mode 100644 index 00000000..96deddc8 --- /dev/null +++ b/test/lib/utils.public.test.js @@ -0,0 +1,247 @@ +'use strict' + +const tap = require('tap') +const getColorizer = require('../../lib/colors') +const utils = require('../../lib/utils') + +tap.test('prettifyErrorLog', t => { + const { prettifyErrorLog } = utils + + t.test('returns string with default settings', async t => { + const err = Error('Something went wrong') + const str = prettifyErrorLog({ log: err }) + t.true(str.startsWith(' Error: Something went wrong')) + }) + + t.test('returns string with custom ident', async t => { + const err = Error('Something went wrong') + const str = prettifyErrorLog({ log: err, ident: ' ' }) + t.true(str.startsWith(' Error: Something went wrong')) + }) + + t.test('returns string with custom eol', async t => { + const err = Error('Something went wrong') + const str = prettifyErrorLog({ log: err, eol: '\r\n' }) + t.true(str.startsWith(` Error: Something went wrong\r\n`)) + }) + + t.end() +}) + +tap.test('prettifyLevel', t => { + const { prettifyLevel } = utils + + t.test('returns `undefined` for unknown level', async t => { + const colorized = prettifyLevel({ log: {} }) + t.is(colorized, undefined) + }) + + t.test('returns non-colorized value for default colorizer', async t => { + const log = { + level: 30 + } + const colorized = prettifyLevel({ log }) + t.is(colorized, 'INFO ') + }) + + t.test('returns colorized value for color colorizer', async t => { + const log = { + level: 30 + } + const colorizer = getColorizer(true) + const colorized = prettifyLevel({ log, colorizer }) + t.is(colorized, '\u001B[32mINFO \u001B[39m') + }) + + t.end() +}) + +tap.test('prettifyMessage', t => { + const { prettifyMessage } = utils + + t.test('returns `undefined` if `messageKey` not found', async t => { + const str = prettifyMessage({ log: {} }) + t.is(str, undefined) + }) + + t.test('returns `undefined` if `messageKey` not string', async t => { + const str = prettifyMessage({ log: { msg: {} } }) + t.is(str, undefined) + }) + + t.test('returns non-colorized value for default colorizer', async t => { + const str = prettifyMessage({ log: { msg: 'foo' } }) + t.is(str, 'foo') + }) + + t.test('returns non-colorized value for alternate `messageKey`', async t => { + const str = prettifyMessage({ log: { message: 'foo' }, messageKey: 'message' }) + t.is(str, 'foo') + }) + + t.test('returns colorized value for color colorizer', async t => { + const colorizer = getColorizer(true) + const str = prettifyMessage({ log: { msg: 'foo' }, colorizer }) + t.is(str, '\u001B[36mfoo\u001B[39m') + }) + + t.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 }) + t.is(str, '\u001B[36mfoo\u001B[39m') + }) + + t.end() +}) + +tap.test('prettifyMetadata', t => { + const { prettifyMetadata } = utils + + t.test('returns `undefined` if no metadata present', async t => { + const str = prettifyMetadata({ log: {} }) + t.is(str, undefined) + }) + + t.test('works with only `name` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo' } }) + t.is(str, '(foo)') + }) + + t.test('works with only `pid` present', async t => { + const str = prettifyMetadata({ log: { pid: '1234' } }) + t.is(str, '(1234)') + }) + + t.test('works with only `hostname` present', async t => { + const str = prettifyMetadata({ log: { hostname: 'bar' } }) + t.is(str, '(on bar)') + }) + + t.test('works with only `name` & `pid` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' } }) + t.is(str, '(foo/1234)') + }) + + t.test('works with only `name` & `hostname` present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' } }) + t.is(str, '(foo on bar)') + }) + + t.test('works with only `pid` & `hostname` present', async t => { + const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' } }) + t.is(str, '(1234 on bar)') + }) + + t.test('works with all three present', async t => { + const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' } }) + t.is(str, '(foo/1234 on bar)') + }) + + t.end() +}) + +tap.test('prettifyObject', t => { + const { prettifyObject } = utils + + t.test('returns empty string if no properties present', async t => { + const str = prettifyObject({ input: {} }) + t.is(str, '') + }) + + t.test('works with single level properties', async t => { + const str = prettifyObject({ input: { foo: 'bar' } }) + t.is(str, ` foo: "bar"\n`) + }) + + t.test('works with multiple level properties', async t => { + const str = prettifyObject({ input: { foo: { bar: 'baz' } } }) + t.is(str, ` foo: {\n "bar": "baz"\n }\n`) + }) + + t.test('skips specified keys', async t => { + const str = prettifyObject({ input: { foo: 'bar', hello: 'world' }, skipKeys: ['foo'] }) + t.is(str, ` hello: "world"\n`) + }) + + t.test('ignores predefined keys', async t => { + const str = prettifyObject({ input: { foo: 'bar', pid: 12345 } }) + t.is(str, ` foo: "bar"\n`) + }) + + t.test('works with error props', async t => { + const err = Error('Something went wrong') + const serializedError = { + message: err.message, + stack: err.stack + } + const str = prettifyObject({ input: { error: serializedError } }) + t.true(str.startsWith(' error:')) + t.true(str.includes(' "message": "Something went wrong",')) + t.true(str.includes(' Error: Something went wrong')) + }) + + t.end() +}) + +tap.test('prettifyTime', t => { + const { prettifyTime } = utils + + t.test('returns `undefined` if `time` or `timestamp` not in log', async t => { + const str = prettifyTime({ log: {} }) + t.is(str, undefined) + }) + + t.test('returns prettified formatted time', async t => { + let log = { time: 1554642900000 } + let str = prettifyTime({ log, translateFormat: true }) + t.is(str, '[2019-04-07 13:15:00.000 +0000]') + + log = { timestamp: 1554642900000 } + str = prettifyTime({ log, translateFormat: true }) + t.is(str, '[2019-04-07 13:15:00.000 +0000]') + + log = { time: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log, translateFormat: true }) + t.is(str, '[2019-04-07 13:15:00.000 +0000]') + + log = { timestamp: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log, translateFormat: true }) + t.is(str, '[2019-04-07 13:15:00.000 +0000]') + + log = { time: 1554642900000 } + str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + t.is(str, '[7 Apr 2019 13:15]') + + log = { timestamp: 1554642900000 } + str = prettifyTime({ log, translateFormat: 'd mmm yyyy H:MM' }) + t.is(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' }) + t.is(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' }) + t.is(str, '[7 Apr 2019 13:15]') + }) + + t.test('passes through value', async t => { + let log = { time: 1554642900000 } + let str = prettifyTime({ log }) + t.is(str, '[1554642900000]') + + log = { timestamp: 1554642900000 } + str = prettifyTime({ log }) + t.is(str, '[1554642900000]') + + log = { time: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log }) + t.is(str, '[2019-04-07T09:15:00.000-04:00]') + + log = { timestamp: '2019-04-07T09:15:00.000-04:00' } + str = prettifyTime({ log }) + t.is(str, '[2019-04-07T09:15:00.000-04:00]') + }) + + t.end() +})