Skip to content

Commit

Permalink
Add singleLine config option (#158)
Browse files Browse the repository at this point in the history
* test: multiline option

* Add singleLine option with tests (#97)

Co-authored-by: Gregor Martynus <gregor@martynus.net>
  • Loading branch information
Lucas Pickering and gr2m authored Mar 2, 2021
1 parent fb94320 commit d724808
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 43 deletions.
6 changes: 4 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ node app.js | pino-pretty
[jmespath](http://jmespath.org/).
- `--ignore` (`-i`): Ignore one or several keys: (`-i time,hostname`)
- `--hideObject` (`-H`): Hide objects from output (but not error object)
- `--singleLine` (`-S`): Print each log message on a single line (errors will still be multi-line)
- `--config`: Specify a path to a config file containing the pino-pretty options. pino-pretty will attempt to read from a `.pino-prettyrc` in your current directory (`process.cwd`) if not specified

<a id="integration"></a>
Expand Down Expand Up @@ -142,8 +143,9 @@ with keys corresponding to the options described in [CLI Arguments](#cliargs):
timestampKey: 'time', // --timestampKey
translateTime: false, // --translateTime
search: 'foo == `bar`', // --search
ignore: 'pid,hostname', // --ignore,
hideObject: false // --hideObject
ignore: 'pid,hostname', // --ignore
hideObject: false, // --hideObject
singleLine: false, // --singleLine
customPrettifiers: {}
}
```
Expand Down
1 change: 1 addition & 0 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ args
.option(['s', 'search'], 'Specify a search pattern according to jmespath')
.option(['i', 'ignore'], 'Ignore one or several keys: (`-i time,hostname`)')
.option(['H', 'hideObject'], 'Hide objects from output (but not error object)')
.option(['S', 'singleLine'], 'Print all non-error objects on a single line')
.option('config', 'specify a path to a json file containing the pino-pretty options')

args
Expand Down
9 changes: 6 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const defaultOptions = {
useMetadata: false,
outputStream: process.stdout,
customPrettifiers: {},
hideObject: false
hideObject: false,
singleLine: false
}

module.exports = function prettyFactory (options) {
Expand All @@ -53,6 +54,7 @@ module.exports = function prettyFactory (options) {
const customPrettifiers = opts.customPrettifiers
const ignoreKeys = opts.ignore ? new Set(opts.ignore.split(',')) : undefined
const hideObject = opts.hideObject
const singleLine = opts.singleLine

const colorizer = colors(opts.colorize)
const search = opts.search
Expand Down Expand Up @@ -131,7 +133,7 @@ module.exports = function prettyFactory (options) {
}

if (line.length > 0) {
line += EOL
line += (singleLine ? ' ' : EOL)
}

if (log.type === 'Error' && log.stack) {
Expand All @@ -151,7 +153,8 @@ module.exports = function prettyFactory (options) {
customPrettifiers,
errorLikeKeys: errorLikeObjectKeys,
eol: EOL,
ident: IDENT
ident: IDENT,
singleLine
})
line += prettifiedObject
}
Expand Down
123 changes: 85 additions & 38 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ module.exports = {

module.exports.internals = {
formatTime,
joinLinesWithIndentation
joinLinesWithIndentation,
prettifyError
}

/**
Expand Down Expand Up @@ -291,6 +292,9 @@ function prettifyMetadata ({ log }) {
* error objects. Default: `ERROR_LIKE_KEYS` constant.
* @param {boolean} [input.excludeLoggerKeys] Indicates if known logger specific
* keys should be excluded from prettification. Default: `true`.
* @param {boolean} [input.singleLine] Should non-error keys all be formatted
* on a single line? This does NOT apply to errors, which will still be
* multi-line. Default: `false`
*
* @returns {string} The prettified string. This can be as little as `''` if
* there was nothing to prettify.
Expand All @@ -302,56 +306,64 @@ function prettifyObject ({
skipKeys = [],
customPrettifiers = {},
errorLikeKeys = ERROR_LIKE_KEYS,
excludeLoggerKeys = true
excludeLoggerKeys = true,
singleLine = false
}) {
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 (let i = 0; i < objectKeys.length; i += 1) {
const keyName = keysToIterate[i]
const keyValue = input[keyName]

if (keyValue === undefined) continue
// Split object keys into two categories: error and non-error
const { plain, errors } = Object.entries(input).reduce(({ plain, errors }, [k, v]) => {
if (keysToIgnore.includes(k) === false) {
// Pre-apply custom prettifiers, because all 3 cases below will need this
const pretty = typeof customPrettifiers[k] === 'function'
? customPrettifiers[k](v, k, input)
: v
if (errorLikeKeys.includes(k)) {
errors[k] = pretty
} else {
plain[k] = pretty
}
}
return { plain, errors }
}, { plain: {}, errors: {} })

let lines
if (typeof customPrettifiers[keyName] === 'function') {
lines = customPrettifiers[keyName](keyValue, keyName, input)
} else {
lines = stringifySafe(keyValue, null, 2)
if (singleLine) {
// Stringify the entire object as a single JSON line
if (Object.keys(plain).length > 0) {
result += stringifySafe(plain)
}
result += eol
} else {
// Put each object entry on its own line
Object.entries(plain).forEach(([keyName, keyValue]) => {
// custom prettifiers are already applied above, so we can skip it now
const lines = typeof customPrettifiers[keyName] === 'function'
? keyValue
: stringifySafe(keyValue, 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 (let 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)
/* istanbul ignore else */
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 {
if (lines === undefined) return

const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
result += `${ident}${keyName}: ${joinedLines}${eol}`
}
})
}

// Errors
Object.entries(errors).forEach(([keyName, keyValue]) => {
// custom prettifiers are already applied above, so we can skip it now
const lines = typeof customPrettifiers[keyName] === 'function'
? keyValue
: stringifySafe(keyValue, null, 2)

if (lines === undefined) return

result += prettifyError({ keyName, lines, eol, ident })
})

return result
}

Expand Down Expand Up @@ -387,3 +399,38 @@ function prettifyTime ({ log, timestampKey = TIMESTAMP_KEY, translateFormat = un

return `[${time}]`
}

/**
* Prettifies an error string into a multi-line format.
* @param {object} input
* @param {string} input.keyName The key assigned to this error in the log object
* @param {string} input.lines The STRINGIFIED error. If the error field has a
* custom prettifier, that should be pre-applied as well
* @param {string} input.ident The indentation sequence to use
* @param {string} input.eol The EOL sequence to use
*/
function prettifyError ({ keyName, lines, eol, ident }) {
let result = ''
const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol)

for (let 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)
/* istanbul ignore else */
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
}
}

return result
}
34 changes: 34 additions & 0 deletions test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -697,5 +697,39 @@ test('basic prettifier tests', (t) => {
log.info({ key: 'value' }, 'hello world')
})

t.test('Prints extra objects on one line with singleLine=true', (t) => {
t.plan(1)
const pretty = prettyFactory({
singleLine: true,
colorize: false,
customPrettifiers: {
upper: val => val.toUpperCase(),
undef: () => undefined
}
})
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.is(formatted, `[${epoch}] INFO (${pid} on ${hostname}): message {"extra":{"foo":"bar","number":42},"upper":"FOOBAR"}\n`)

cb()
}
}))
log.info({ msg: 'message', extra: { foo: 'bar', number: 42 }, upper: 'foobar', undef: 'this will not show up' })
})

t.test('Does not print empty object with singleLine=true', (t) => {
t.plan(1)
const pretty = prettyFactory({ singleLine: true, colorize: false })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.is(formatted, `[${epoch}] INFO (${pid} on ${hostname}): message \n`)
cb()
}
}))
log.info({ msg: 'message' })
})

t.end()
})
20 changes: 20 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,25 @@ test('cli', (t) => {
t.tearDown(() => child.kill())
})

t.test('singleLine=true', (t) => {
t.plan(1)

const logLineWithExtra = JSON.stringify(Object.assign(JSON.parse(logLine), {
extra: {
foo: 'bar',
number: 42
}
})) + '\n'

const env = { TERM: 'dumb' }
const child = spawn(process.argv[0], [bin, '--singleLine'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.is(data.toString(), `[${epoch}] INFO (42 on foo): hello world {"extra":{"foo":"bar","number":42}}\n`)
})
child.stdin.write(logLineWithExtra)
t.tearDown(() => child.kill())
})

t.end()
})
60 changes: 60 additions & 0 deletions test/error-objects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,66 @@ test('error like objects tests', (t) => {
log.info({ err })
})

t.test('prettifies Error in property with singleLine=true', (t) => {
// singleLine=true doesn't apply to errors
t.plan(8)
const pretty = prettyFactory({
singleLine: true,
errorLikeObjectKeys: ['err']
})

const err = Error('hello world')
const expected = [
'{"extra":{"a":1,"b":2}}',
err.message,
...err.stack.split('\n')
]

const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.is(lines.length, expected.length + 5)
t.is(lines[0], `[${epoch}] INFO (${pid} on ${hostname}): {"extra":{"a":1,"b":2}}`)
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 12 labels the test `<anonymous>`
t.match(lines[6], /\s{10}(at Test.t.test|at Test.<anonymous>)/)
cb()
}
}))

log.info({ err, extra: { a: 1, b: 2 } })
})

t.test('prettifies Error in property within errorLikeObjectKeys with custom function', (t) => {
t.plan(1)
const pretty = prettyFactory({
errorLikeObjectKeys: ['err'],
customPrettifiers: {
err: val => `error is ${val.message}`
}
})

const err = Error('hello world')
err.stack = 'Error: hello world\n at anonymous (C:\\project\\node_modules\\example\\index.js)'
const expected = err.stack.split('\n')
expected.unshift(err.message)

const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.is(formatted, `[${epoch}] INFO (${pid} on ${hostname}):\n err: error is hello world\n`)
cb()
}
}))

log.info({ err })
})

t.test('prettifies Error in property within errorLikeObjectKeys when stack has escaped characters', (t) => {
t.plan(8)
const pretty = prettyFactory({
Expand Down
14 changes: 14 additions & 0 deletions test/lib/utils.internals.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const tap = require('tap')
const stringifySafe = require('fast-safe-stringify')
const { internals } = require('../../lib/utils')

tap.test('#joinLinesWithIndentation', t => {
Expand Down Expand Up @@ -81,3 +82,16 @@ tap.test('#formatTime', t => {

t.end()
})

tap.test('#prettifyError', t => {
t.test('prettifies error', t => {
const error = Error('Bad error!')
const lines = stringifySafe(error, Object.getOwnPropertyNames(error), 2)

const prettyError = internals.prettifyError({ keyName: 'errorKey', lines, ident: ' ', eol: '\n' })
t.match(prettyError, /\s*errorKey: {\n\s*"stack":[\s\S]*"message": "Bad error!"/)
t.end()
})

t.end()
})

0 comments on commit d724808

Please sign in to comment.