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()
+})