Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ node app.js | pino-pretty
- `--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.
Nested keys are supported with each property delimited by a dot character (`.`).
Keys may be escaped to target property names that contain the delimiter itself
(same rules as `--levelKey`).
Default: `msg`.
- `--levelKey` (`--levelKey`): Define the key that contains the level of the log. Nested keys are supported with each property delimited by a dot character (`.`).
Keys may be escaped to target property names that contains the delimiter itself:
Expand All @@ -81,6 +84,9 @@ node app.js | pino-pretty
- `--messageFormat` (`-o`): Format output of message, e.g. `{levelLabel} - {pid} - url:{req.url}` will output message: `INFO - 1123 - url:localhost:3000/test`
Default: `false`
- `--timestampKey` (`-a`): Define the key that contains the log timestamp.
Nested keys are supported with each property delimited by a dot character (`.`).
Keys may be escaped to target property names that contain the delimiter itself
(same rules as `--levelKey`), e.g. `--timestampKey nested_key.time`.
Default: `time`.
- `--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
Expand Down
64 changes: 55 additions & 9 deletions lib/pretty.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ module.exports = pretty

const sjs = require('secure-json-parse')

const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})

const isObject = require('./utils/is-object')
const deleteLogProperty = require('./utils/delete-log-property')
const getPropertyValue = require('./utils/get-property-value')
const prettifyErrorLog = require('./utils/prettify-error-log')
const prettifyLevel = require('./utils/prettify-level')
const prettifyMessage = require('./utils/prettify-message')
const prettifyMetadata = require('./utils/prettify-metadata')
const prettifyObject = require('./utils/prettify-object')
const prettifyTime = require('./utils/prettify-time')
const filterLog = require('./utils/filter-log')
const splitPropertyKey = require('./utils/split-property-key')

const {
LEVELS,
Expand Down Expand Up @@ -143,20 +149,31 @@ function pretty (inputData) {
if (this.singleLine) line += this.EOL
line += prettifiedErrorLog
} else if (this.hideObject === false) {
const skipKeys = [
// Skip message/level/time keys that were already rendered in the header.
// Nested keys (e.g. nested_key.time) must be removed via path delete so they
// are not printed again in the object dump (levelKey already documented this).
const candidateSkipKeys = [
this.messageKey,
this.levelKey,
this.timestampKey
]
.map((key) => key.replaceAll(/\\/g, ''))
.filter(key => {
return typeof log[key] === 'string' ||
typeof log[key] === 'number' ||
typeof log[key] === 'boolean'
})
let objectLog = log
let mutated = false
for (const key of candidateSkipKeys) {
const value = getPropertyValue(log, key)
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
continue
}
if (!mutated) {
objectLog = fastCopy(log)
mutated = true
}
deleteLogProperty(objectLog, key)
pruneEmptyParents(objectLog, key)
}
const prettifiedObject = prettifyObject({
log,
skipKeys,
log: objectLog,
skipKeys: [],
context: this.context
})

Expand All @@ -169,3 +186,32 @@ function pretty (inputData) {

return line
}

/**
* After removing a nested property, drop parent objects that became empty so
* the object dump does not print `nested_key: {}`.
*
* @param {object} log
* @param {string} property
*/
function pruneEmptyParents (log, property) {
const props = splitPropertyKey(property)
while (props.length > 1) {
props.pop()
const parent = getPropertyValue(log, props)
if (
parent === null ||
typeof parent !== 'object' ||
Array.isArray(parent) ||
Object.keys(parent).length > 0
) {
break
}
const parentKey = props[props.length - 1]
const grandProps = props.slice(0, -1)
const grand = grandProps.length === 0 ? log : getPropertyValue(log, grandProps)
if (grand !== null && typeof grand === 'object' && Object.prototype.hasOwnProperty.call(grand, parentKey)) {
delete grand[parentKey]
}
}
}
7 changes: 4 additions & 3 deletions lib/utils/prettify-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ function prettifyMessage ({ log, context }) {
const msg = messageFormat(log, messageKey, levelLabel, { colors: colorizer.colors })
return colorizer.message(msg)
}
if (messageKey in log === false) return undefined
if (typeof log[messageKey] !== 'string' && typeof log[messageKey] !== 'number' && typeof log[messageKey] !== 'boolean') return undefined
return colorizer.message(stripUnsafeControlChars(log[messageKey]))
const message = getPropertyValue(log, messageKey)
if (message === undefined) return undefined
if (typeof message !== 'string' && typeof message !== 'number' && typeof message !== 'boolean') return undefined
return colorizer.message(stripUnsafeControlChars(message))
}
8 changes: 8 additions & 0 deletions lib/utils/prettify-message.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ test('returns non-colorized value for alternate `messageKey`', t => {
t.assert.strictEqual(str, 'foo')
})

test('returns non-colorized value for nested `messageKey`', t => {
const str = prettifyMessage({
log: { payload: { text: 'foo' } },
context: { ...context, messageKey: 'payload.text' }
})
t.assert.strictEqual(str, 'foo')
})

test('returns colorized value for color colorizer', t => {
const colorizer = getColorizer(true)
const str = prettifyMessage({
Expand Down
11 changes: 6 additions & 5 deletions lib/utils/prettify-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module.exports = prettifyTime

const formatTime = require('./format-time')
const getPropertyValue = require('./get-property-value')
const stripUnsafeControlChars = require('./strip-unsafe-control-chars')

/**
Expand All @@ -28,15 +29,15 @@ function prettifyTime ({ log, context }) {
translateTime: translateFormat
} = context
const prettifier = context.customPrettifiers?.time
let time = null
// Nested keys (e.g. nested_key.time) use the same delimiter/escape rules as levelKey.
let time = getPropertyValue(log, timestampKey)

if (timestampKey in log) {
time = log[timestampKey]
} else if ('timestamp' in log) {
// Preserve previous fallback: when the configured key is absent, try `timestamp`.
if (time === undefined && timestampKey !== 'timestamp' && 'timestamp' in log) {
time = log.timestamp
}

if (time === null) return undefined
if (time === undefined || time === null) return undefined
const output = translateFormat ? formatTime(time, translateFormat) : time

return prettifier ? prettifier(output) : `[${stripUnsafeControlChars(output)}]`
Expand Down
35 changes: 35 additions & 0 deletions lib/utils/prettify-time.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ test('returns prettified formatted time from custom field', t => {
t.assert.strictEqual(str, '[1554642900000]')
})

test('returns prettified formatted time from nested timestampKey', t => {
const log = { nested_key: { time: 1554642900000 } }
let str = prettifyTime({
log,
context: {
...context,
timestampKey: 'nested_key.time'
}
})
t.assert.strictEqual(str, '[13:15:00.000]')

str = prettifyTime({
log,
context: {
...context,
translateTime: false,
timestampKey: 'nested_key.time'
}
})
t.assert.strictEqual(str, '[1554642900000]')
})

test('nested timestampKey does not incorrectly use root time when nested path is missing', t => {
// Root `time` must not shadow a missing nested path; only the `timestamp` fallback applies.
const str = prettifyTime({
log: { time: 1554642900000 },
context: {
...context,
translateTime: false,
timestampKey: 'nested_key.time'
}
})
t.assert.strictEqual(str, undefined)
})

test('returns prettified formatted time', t => {
let log = { time: 1554642900000 }
let str = prettifyTime({
Expand Down
14 changes: 14 additions & 0 deletions test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,20 @@ describe('basic prettifier tests', () => {
t.assert.strictEqual(arst, `[${formattedEpoch}] INFO: hello world\n`)
})

test('handles nested timestampKey', (t) => {
t.plan(1)
const pretty = prettyFactory({ timestampKey: 'nested_key.time' })
const arst = pretty(`{"msg":"hello world", "nested_key":{"time":${epoch}}, "level":30}`)
t.assert.strictEqual(arst, `[${formattedEpoch}] INFO: hello world\n`)
})

test('handles nested messageKey', (t) => {
t.plan(1)
const pretty = prettyFactory({ messageKey: 'payload.text', ignore: 'time' })
const arst = pretty(`{"payload":{"text":"hello world"}, "time":${epoch}, "level":30}`)
t.assert.strictEqual(arst, 'INFO: hello world\n')
})

test('keeps "v" key in log', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'time' })
Expand Down