Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Plain ndjson #58

Merged
merged 34 commits into from Apr 28, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2b0fcc3
Replace fast-json-parse
jsumners Mar 30, 2019
6e7bf6b
Initial refactor to generic ndjson prettifier
jsumners Mar 30, 2019
20e9107
Merge branch 'master' into plain-ndjson
jsumners Mar 30, 2019
b16e208
Improve support for primitives
jsumners Mar 30, 2019
7a32b52
Move line joiner to utils and apply standard formatting
jsumners Mar 30, 2019
a1cd48c
Move object prettification to utils function
jsumners Mar 31, 2019
ccd2848
Fix split in prettifyObject
jsumners Mar 31, 2019
bf02c96
Move "error" log prettification into utility function
jsumners Mar 31, 2019
572226d
Add support for string levels
jsumners Mar 31, 2019
dd5f386
Add "timestamp" as possible time key
jsumners Mar 31, 2019
64db81d
v3.0.0-rc.1
jsumners Mar 31, 2019
7ef851c
Remove Node 6 from Travis config
jsumners Apr 6, 2019
413d1d2
Add tests for utils.formatTime
jsumners Apr 6, 2019
fda9682
Fix lint issue
jsumners Apr 6, 2019
55e5eab
Add tests for colors.js
jsumners Apr 6, 2019
11c42cc
Add docblock for joinLinesWithIndentation
jsumners Apr 6, 2019
6ebee3c
Add tests for prettifyLevel
jsumners Apr 7, 2019
42e5bc7
Add tests for prettifyMessage
jsumners Apr 7, 2019
438f8fb
Add tests for prettifyTime
jsumners Apr 7, 2019
8bed001
Fix lint error
jsumners Apr 9, 2019
20d9a05
Pino seems to have changed order of some properties in the log. 🤷‍♂️
jsumners Apr 9, 2019
ca7767e
Fix regex for time tests
jsumners Apr 9, 2019
ff3cb0e
Remove Node 12 until release
jsumners Apr 9, 2019
9a997e2
Add tests for prettifyMetadata
jsumners Apr 9, 2019
87b65bf
Added tests (#59)
ovhemert Apr 11, 2019
613d250
Improve prettifyObject test and add docblock
jsumners Apr 13, 2019
b08e946
Add docblock for prettifyErrorLog
jsumners Apr 13, 2019
e5b2176
Update readme
jsumners Apr 13, 2019
af3e853
Update dependencies
jsumners Apr 13, 2019
410c1e8
v3.0.0-rc.2
jsumners Apr 21, 2019
6c38b01
Merge branch 'master' into plain-ndjson
jsumners Apr 21, 2019
1635a78
Remove Node 6 specific tests
jsumners Apr 28, 2019
6c78469
Add back Node 12 to test suite
jsumners Apr 28, 2019
e22b11a
Fix tests for Node 12
jsumners Apr 28, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
280 changes: 72 additions & 208 deletions 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'
Expand All @@ -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
Expand All @@ -97,14 +54,19 @@ 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
}
} else {
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
}
Expand All @@ -118,165 +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 = 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 += ': '
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
}
}
}
52 changes: 52 additions & 0 deletions lib/colors.js
@@ -0,0 +1,52 @@
'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

module.exports = function getColorizer (useColors) {
return useColors ? coloredColorizer : plainColorizer
}