Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

136
backend/node_modules/pino-pretty/lib/colors.js generated vendored Normal file
View File

@@ -0,0 +1,136 @@
'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,
greyMessage: nocolor
}
const { createColors } = require('colorette')
const availableColors = createColors({ useColor: true })
const { white, bgRed, red, yellow, green, blue, gray, cyan } = availableColors
const colored = {
default: white,
60: bgRed,
50: red,
40: yellow,
30: green,
20: blue,
10: gray,
message: cyan,
greyMessage: gray
}
function resolveCustomColoredColorizer (customColors) {
return customColors.reduce(
function (agg, [level, color]) {
agg[level] = typeof availableColors[color] === 'function' ? availableColors[color] : white
return agg
},
{ default: white, message: cyan, greyMessage: gray }
)
}
function colorizeLevel (useOnlyCustomProps) {
return function (level, colorizer, { customLevels, customLevelNames } = {}) {
const levels = useOnlyCustomProps ? customLevels || LEVELS : Object.assign({}, LEVELS, customLevels)
const levelNames = useOnlyCustomProps ? customLevelNames || LEVEL_NAMES : Object.assign({}, LEVEL_NAMES, customLevelNames)
let levelNum = 'default'
if (Number.isInteger(+level)) {
levelNum = Object.prototype.hasOwnProperty.call(levels, level) ? level : levelNum
} else {
levelNum = Object.prototype.hasOwnProperty.call(levelNames, level.toLowerCase()) ? levelNames[level.toLowerCase()] : levelNum
}
const levelStr = levels[levelNum]
return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr)
}
}
function plainColorizer (useOnlyCustomProps) {
const newPlainColorizer = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return newPlainColorizer(level, plain, opts)
}
customColoredColorizer.message = plain.message
customColoredColorizer.greyMessage = plain.greyMessage
return customColoredColorizer
}
function coloredColorizer (useOnlyCustomProps) {
const newColoredColorizer = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return newColoredColorizer(level, colored, opts)
}
customColoredColorizer.message = colored.message
customColoredColorizer.greyMessage = colored.greyMessage
return customColoredColorizer
}
function customColoredColorizerFactory (customColors, useOnlyCustomProps) {
const onlyCustomColored = resolveCustomColoredColorizer(customColors)
const customColored = useOnlyCustomProps ? onlyCustomColored : Object.assign({}, colored, onlyCustomColored)
const colorizeLevelCustom = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return colorizeLevelCustom(level, customColored, opts)
}
customColoredColorizer.message = customColoredColorizer.message || customColored.message
customColoredColorizer.greyMessage = customColoredColorizer.greyMessage || customColored.greyMessage
return customColoredColorizer
}
/**
* Applies colorization, if possible, to a string representing the passed in
* `level`. For example, the default colorizer will return a "green" colored
* string for the "info" level.
*
* @typedef {function} ColorizerFunc
* @param {string|number} level In either case, the input will map to a color
* for the specified level or to the color for `USERLVL` if the level is not
* recognized.
* @property {function} message Accepts one string parameter that will be
* colorized to a predefined color.
*/
/**
* Factory function get a function to colorized levels. The returned function
* also includes a `.message(str)` method to colorize strings.
*
* @param {boolean} [useColors=false] When `true` a function that applies standard
* terminal colors is returned.
* @param {array[]} [customColors] Tuple where first item of each array is the
* level index and the second item is the color
* @param {boolean} [useOnlyCustomProps] When `true`, only use the provided
* custom colors provided and not fallback to default
*
* @returns {ColorizerFunc} `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, customColors, useOnlyCustomProps) {
if (useColors && customColors !== undefined) {
return customColoredColorizerFactory(customColors, useOnlyCustomProps)
} else if (useColors) {
return coloredColorizer(useOnlyCustomProps)
}
return plainColorizer(useOnlyCustomProps)
}

132
backend/node_modules/pino-pretty/lib/colors.test.js generated vendored Normal file
View File

@@ -0,0 +1,132 @@
'use strict'
const { test } = require('tap')
const getColorizer = require('./colors')
const testDefaultColorizer = getColorizer => async t => {
const colorizer = getColorizer()
let colorized = colorizer(10)
t.equal(colorized, 'TRACE')
colorized = colorizer(20)
t.equal(colorized, 'DEBUG')
colorized = colorizer(30)
t.equal(colorized, 'INFO')
colorized = colorizer(40)
t.equal(colorized, 'WARN')
colorized = colorizer(50)
t.equal(colorized, 'ERROR')
colorized = colorizer(60)
t.equal(colorized, 'FATAL')
colorized = colorizer(900)
t.equal(colorized, 'USERLVL')
colorized = colorizer('info')
t.equal(colorized, 'INFO')
colorized = colorizer('use-default')
t.equal(colorized, 'USERLVL')
colorized = colorizer.message('foo')
t.equal(colorized, 'foo')
colorized = colorizer.greyMessage('foo')
t.equal(colorized, 'foo')
}
const testColoringColorizer = getColorizer => async t => {
const colorizer = getColorizer(true)
let colorized = colorizer(10)
t.equal(colorized, '\u001B[90mTRACE\u001B[39m')
colorized = colorizer(20)
t.equal(colorized, '\u001B[34mDEBUG\u001B[39m')
colorized = colorizer(30)
t.equal(colorized, '\u001B[32mINFO\u001B[39m')
colorized = colorizer(40)
t.equal(colorized, '\u001B[33mWARN\u001B[39m')
colorized = colorizer(50)
t.equal(colorized, '\u001B[31mERROR\u001B[39m')
colorized = colorizer(60)
t.equal(colorized, '\u001B[41mFATAL\u001B[49m')
colorized = colorizer(900)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer('info')
t.equal(colorized, '\u001B[32mINFO\u001B[39m')
colorized = colorizer('use-default')
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer.message('foo')
t.equal(colorized, '\u001B[36mfoo\u001B[39m')
colorized = colorizer.greyMessage('foo')
t.equal(colorized, '\u001B[90mfoo\u001B[39m')
}
const testCustomColoringColorizer = getColorizer => async t => {
const customLevels = {
0: 'INFO',
1: 'ERR',
default: 'USERLVL'
}
const customLevelNames = {
info: 0,
err: 1
}
const customColors = [
[0, 'not-a-color'],
[1, 'red']
]
const opts = {
customLevels,
customLevelNames
}
const colorizer = getColorizer(true, customColors)
const colorizerWithCustomPropUse = getColorizer(true, customColors, true)
let colorized = colorizer(1, opts)
t.equal(colorized, '\u001B[31mERR\u001B[39m')
colorized = colorizer(0, opts)
t.equal(colorized, '\u001B[37mINFO\u001B[39m')
colorized = colorizer(900)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer('err', opts)
t.equal(colorized, '\u001B[31mERR\u001B[39m')
colorized = colorizer('info', opts)
t.equal(colorized, '\u001B[37mINFO\u001B[39m')
colorized = colorizer('use-default')
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer(40, opts)
t.equal(colorized, '\u001B[33mWARN\u001B[39m')
colorized = colorizerWithCustomPropUse(50, opts)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
}
test('returns default colorizer - private export', testDefaultColorizer(getColorizer))
test('returns colorizing colorizer - private export', testColoringColorizer(getColorizer))
test('returns custom colorizing colorizer - private export', testCustomColoringColorizer(getColorizer))
test('custom props defaults to standard levels', async t => {
const colorizer = getColorizer(true, [], true)
const colorized = colorizer('info')
t.equal(colorized, '\u001B[37mINFO\u001B[39m')
})

55
backend/node_modules/pino-pretty/lib/constants.js generated vendored Normal file
View File

@@ -0,0 +1,55 @@
'use strict'
/**
* A set of property names that indicate the value represents an error object.
*
* @typedef {string[]} K_ERROR_LIKE_KEYS
*/
module.exports = {
DATE_FORMAT: 'yyyy-mm-dd HH:MM:ss.l o',
DATE_FORMAT_SIMPLE: 'HH:MM:ss.l',
/**
* @type {K_ERROR_LIKE_KEYS}
*/
ERROR_LIKE_KEYS: ['err', 'error'],
MESSAGE_KEY: 'msg',
LEVEL_KEY: 'level',
LEVEL_LABEL: 'levelLabel',
TIMESTAMP_KEY: 'time',
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',
'caller'
]
}

169
backend/node_modules/pino-pretty/lib/pretty.js generated vendored Normal file
View File

@@ -0,0 +1,169 @@
'use strict'
module.exports = pretty
const sjs = require('secure-json-parse')
const isObject = require('./utils/is-object')
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 {
LEVELS,
LEVEL_KEY,
LEVEL_NAMES
} = require('./constants')
const jsonParser = input => {
try {
return { value: sjs.parse(input, { protoAction: 'remove' }) }
} catch (err) {
return { err }
}
}
/**
* Orchestrates processing the received log data according to the provided
* configuration and returns a prettified log string.
*
* @typedef {function} LogPrettifierFunc
* @param {string|object} inputData A log string or a log-like object.
* @returns {string} A string that represents the prettified log data.
*/
function pretty (inputData) {
let log
if (!isObject(inputData)) {
const parsed = jsonParser(inputData)
if (parsed.err || !isObject(parsed.value)) {
// pass through
return inputData + this.EOL
}
log = parsed.value
} else {
log = inputData
}
if (this.minimumLevel) {
// We need to figure out if the custom levels has the desired minimum
// level & use that one if found. If not, determine if the level exists
// in the standard levels. In both cases, make sure we have the level
// number instead of the level name.
let condition
if (this.useOnlyCustomProps) {
condition = this.customLevels
} else {
condition = this.customLevelNames[this.minimumLevel] !== undefined
}
let minimum
if (condition) {
minimum = this.customLevelNames[this.minimumLevel]
} else {
minimum = LEVEL_NAMES[this.minimumLevel]
}
if (!minimum) {
minimum = typeof this.minimumLevel === 'string'
? LEVEL_NAMES[this.minimumLevel]
: LEVEL_NAMES[LEVELS[this.minimumLevel].toLowerCase()]
}
const level = log[this.levelKey === undefined ? LEVEL_KEY : this.levelKey]
if (level < minimum) return
}
const prettifiedMessage = prettifyMessage({ log, context: this.context })
if (this.ignoreKeys || this.includeKeys) {
log = filterLog({ log, context: this.context })
}
const prettifiedLevel = prettifyLevel({
log,
context: {
...this.context,
// This is odd. The colorizer ends up relying on the value of
// `customProperties` instead of the original `customLevels` and
// `customLevelNames`.
...this.context.customProperties
}
})
const prettifiedMetadata = prettifyMetadata({ log, context: this.context })
const prettifiedTime = prettifyTime({ log, context: this.context })
let line = ''
if (this.levelFirst && prettifiedLevel) {
line = `${prettifiedLevel}`
}
if (prettifiedTime && line === '') {
line = `${prettifiedTime}`
} else if (prettifiedTime) {
line = `${line} ${prettifiedTime}`
}
if (!this.levelFirst && prettifiedLevel) {
if (line.length > 0) {
line = `${line} ${prettifiedLevel}`
} else {
line = prettifiedLevel
}
}
if (prettifiedMetadata) {
if (line.length > 0) {
line = `${line} ${prettifiedMetadata}:`
} else {
line = prettifiedMetadata
}
}
if (line.endsWith(':') === false && line !== '') {
line += ':'
}
if (prettifiedMessage !== undefined) {
if (line.length > 0) {
line = `${line} ${prettifiedMessage}`
} else {
line = prettifiedMessage
}
}
if (line.length > 0 && !this.singleLine) {
line += this.EOL
}
// pino@7+ does not log this anymore
if (log.type === 'Error' && log.stack) {
const prettifiedErrorLog = prettifyErrorLog({ log, context: this.context })
if (this.singleLine) line += this.EOL
line += prettifiedErrorLog
} else if (this.hideObject === false) {
const skipKeys = [
this.messageKey,
this.levelKey,
this.timestampKey
].filter(key => {
return typeof log[key] === 'string' ||
typeof log[key] === 'number' ||
typeof log[key] === 'boolean'
})
const prettifiedObject = prettifyObject({
log,
skipKeys,
context: this.context
})
// In single line mode, include a space only if prettified version isn't empty
if (this.singleLine && !/^\s$/.test(prettifiedObject)) {
line += ' '
}
line += prettifiedObject
}
return line
}

View File

@@ -0,0 +1,71 @@
'use strict'
module.exports = buildSafeSonicBoom
const { isMainThread } = require('worker_threads')
const SonicBoom = require('sonic-boom')
const noop = require('./noop')
/**
* Creates a safe SonicBoom instance
*
* @param {object} opts Options for SonicBoom
*
* @returns {object} A new SonicBoom stream
*/
function buildSafeSonicBoom (opts) {
const stream = new SonicBoom(opts)
stream.on('error', filterBrokenPipe)
// if we are sync: false, we must flush on exit
// NODE_V8_COVERAGE must breaks everything
// https://github.com/nodejs/node/issues/49344
if (!process.env.NODE_V8_COVERAGE && !opts.sync && isMainThread) {
setupOnExit(stream)
}
return stream
function filterBrokenPipe (err) {
if (err.code === 'EPIPE') {
stream.write = noop
stream.end = noop
stream.flushSync = noop
stream.destroy = noop
return
}
stream.removeListener('error', filterBrokenPipe)
}
}
function setupOnExit (stream) {
/* istanbul ignore next */
if (global.WeakRef && global.WeakMap && global.FinalizationRegistry) {
// This is leak free, it does not leave event handlers
const onExit = require('on-exit-leak-free')
onExit.register(stream, autoEnd)
stream.on('close', function () {
onExit.unregister(stream)
})
}
}
/* istanbul ignore next */
function autoEnd (stream, eventName) {
// This check is needed only on some platforms
if (stream.destroyed) {
return
}
if (eventName === 'beforeExit') {
// We still have an event loop, let's use it
stream.flush()
stream.on('drain', function () {
stream.end()
})
} else {
// We do not have an event loop, so flush synchronously
stream.flushSync()
}
}

View File

@@ -0,0 +1,86 @@
'use strict'
const tap = require('tap')
const rimraf = require('rimraf')
const fs = require('fs')
const { join } = require('path')
const buildSafeSonicBoom = require('./build-safe-sonic-boom')
function noop () {}
const file = () => {
const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`)
const fd = fs.openSync(dest, 'w')
return { dest, fd }
}
tap.test('should not write when error emitted and code is "EPIPE"', async t => {
t.plan(1)
const { fd, dest } = file()
const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
t.teardown(() => rimraf(dest, noop))
stream.emit('error', { code: 'EPIPE' })
stream.write('will not work')
const dataFile = fs.readFileSync(dest)
t.equal(dataFile.length, 0)
})
tap.test('should stream.write works when error code is not "EPIPE"', async t => {
t.plan(3)
const { fd, dest } = file()
const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
t.teardown(() => rimraf(dest, noop))
stream.on('error', () => t.pass('error emitted'))
stream.emit('error', 'fake error description')
t.ok(stream.write('will work'))
const dataFile = fs.readFileSync(dest)
t.equal(dataFile.toString(), 'will work')
})
tap.test('cover setupOnExit', async t => {
t.plan(3)
const { fd, dest } = file()
const stream = buildSafeSonicBoom({ sync: false, fd, mkdir: true })
t.teardown(() => rimraf(dest, noop))
stream.on('error', () => t.pass('error emitted'))
stream.emit('error', 'fake error description')
t.ok(stream.write('will work'))
await watchFileCreated(dest)
const dataFile = fs.readFileSync(dest)
t.equal(dataFile.toString(), 'will work')
})
function watchFileCreated (filename) {
return new Promise((resolve, reject) => {
const TIMEOUT = 2000
const INTERVAL = 100
const threshold = TIMEOUT / INTERVAL
let counter = 0
const interval = setInterval(() => {
// On some CI runs file is created but not filled
if (fs.existsSync(filename) && fs.statSync(filename).size !== 0) {
clearInterval(interval)
resolve()
} else if (counter <= threshold) {
counter++
} else {
clearInterval(interval)
reject(new Error(`${filename} was not created.`))
}
}, INTERVAL)
})
}

View File

@@ -0,0 +1,26 @@
'use strict'
module.exports = createDate
const isValidDate = require('./is-valid-date')
/**
* Constructs a JS Date from a number or string. Accepts any single number
* or single string argument that is valid for the Date() constructor,
* or an epoch as a string.
*
* @param {string|number} epoch The representation of the Date.
*
* @returns {Date} The constructed Date.
*/
function createDate (epoch) {
// If epoch is already a valid argument, return the valid Date
let date = new Date(epoch)
if (isValidDate(date)) {
return date
}
// Convert to a number to permit epoch as a string
date = new Date(+epoch)
return date
}

View File

@@ -0,0 +1,20 @@
'use strict'
const tap = require('tap')
const createDate = require('./create-date')
const wanted = 1624450038567
tap.test('accepts arguments the Date constructor would accept', async t => {
t.plan(2)
t.same(createDate(1624450038567).getTime(), wanted)
t.same(createDate('2021-06-23T12:07:18.567Z').getTime(), wanted)
})
tap.test('accepts epoch as a string', async t => {
// If Date() accepts this argument, the createDate function is not needed
// and can be replaced with Date()
t.plan(2)
t.notSame(new Date('16244500385-67').getTime(), wanted)
t.same(createDate('1624450038567').getTime(), wanted)
})

View File

@@ -0,0 +1,28 @@
'use strict'
module.exports = deleteLogProperty
const getPropertyValue = require('./get-property-value')
const splitPropertyKey = require('./split-property-key')
/**
* Deletes a specified property from a log object if it exists.
* This function mutates the passed in `log` object.
*
* @param {object} log The log object to be modified.
* @param {string} property A string identifying the property to be deleted from
* the log object. Accepts nested properties delimited by a `.`
* Delimiter can be escaped to preserve property names that contain the delimiter.
* e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`
*/
function deleteLogProperty (log, property) {
const props = splitPropertyKey(property)
const propToDelete = props.pop()
log = getPropertyValue(log, props)
/* istanbul ignore else */
if (log !== null && typeof log === 'object' && Object.prototype.hasOwnProperty.call(log, propToDelete)) {
delete log[propToDelete]
}
}

View File

@@ -0,0 +1,31 @@
'use strict'
const tap = require('tap')
const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})
const deleteLogProperty = require('./delete-log-property')
const logData = {
level: 30,
data1: {
data2: { 'data-3': 'bar' }
}
}
tap.test('deleteLogProperty deletes property of depth 1', async t => {
const log = fastCopy(logData)
deleteLogProperty(log, 'data1')
t.same(log, { level: 30 })
})
tap.test('deleteLogProperty deletes property of depth 2', async t => {
const log = fastCopy(logData)
deleteLogProperty(log, 'data1.data2')
t.same(log, { level: 30, data1: { } })
})
tap.test('deleteLogProperty deletes property of depth 3', async t => {
const log = fastCopy(logData)
deleteLogProperty(log, 'data1.data2.data-3')
t.same(log, { level: 30, data1: { data2: { } } })
})

View File

@@ -0,0 +1,45 @@
'use strict'
module.exports = filterLog
const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})
const deleteLogProperty = require('./delete-log-property')
/**
* @typedef {object} FilterLogParams
* @property {object} log The log object to be modified.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Filter a log object by removing or including keys accordingly.
* When `includeKeys` is passed, `ignoredKeys` will be ignored.
* One of ignoreKeys or includeKeys must be pass in.
*
* @param {FilterLogParams} input
*
* @returns {object} A new `log` object instance that
* either only includes the keys in ignoreKeys
* or does not include those in ignoredKeys.
*/
function filterLog ({ log, context }) {
const { ignoreKeys, includeKeys } = context
const logCopy = fastCopy(log)
if (includeKeys) {
const logIncluded = {}
includeKeys.forEach((key) => {
logIncluded[key] = logCopy[key]
})
return logIncluded
}
ignoreKeys.forEach((ignoreKey) => {
deleteLogProperty(logCopy, ignoreKey)
})
return logCopy
}

View File

@@ -0,0 +1,190 @@
'use strict'
const tap = require('tap')
const filterLog = require('./filter-log')
const context = {
includeKeys: undefined,
ignoreKeys: undefined
}
const logData = {
level: 30,
time: 1522431328992,
data1: {
data2: { 'data-3': 'bar' },
error: new Error('test')
}
}
const logData2 = Object.assign({
'logging.domain.corp/operation': {
id: 'foo',
producer: 'bar'
}
}, logData)
tap.test('#filterLog with an ignoreKeys option', t => {
t.test('filterLog removes single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: ['data1.data2.data-3']
}
})
t.same(result, { level: 30, time: 1522431328992, data1: { data2: { }, error: new Error('test') } })
})
t.test('filterLog removes multiple entries', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: ['time', 'data1']
}
})
t.same(result, { level: 30 })
})
t.test('filterLog keeps error instance', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: []
}
})
t.equal(logData.data1.error, result.data1.error)
})
t.test('filterLog removes entry with escape sequence', async t => {
const result = filterLog({
log: logData2,
context: {
...context,
ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation']
}
})
t.same(result, { level: 30, time: 1522431328992 })
})
t.test('filterLog removes entry with escape sequence nested', async t => {
const result = filterLog({
log: logData2,
context: {
...context,
ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer']
}
})
t.same(result, { level: 30, time: 1522431328992, 'logging.domain.corp/operation': { id: 'foo' } })
})
t.end()
})
const ignoreKeysArray = [
undefined,
['level'],
['level', 'data1.data2.data-3']
]
ignoreKeysArray.forEach(ignoreKeys => {
tap.test(`#filterLog with an includeKeys option when the ignoreKeys being ${ignoreKeys}`, t => {
t.test('filterLog include nothing', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys,
includeKeys: []
}
})
t.same(result, {})
})
t.test('filterLog include single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys,
includeKeys: ['time']
}
})
t.same(result, { time: 1522431328992 })
})
t.test('filterLog include multiple entries', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys,
includeKeys: ['time', 'data1']
}
})
t.same(result, {
time: 1522431328992,
data1: {
data2: { 'data-3': 'bar' },
error: new Error('test')
}
})
})
t.end()
})
})
tap.test('#filterLog with circular references', t => {
const logData = {
level: 30,
time: 1522431328992,
data1: 'test'
}
logData.circular = logData
t.test('filterLog removes single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: ['data1']
}
})
t.same(result.circular.level, result.level)
t.same(result.circular.time, result.time)
delete result.circular
t.same(result, { level: 30, time: 1522431328992 })
})
t.test('filterLog includes single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
includeKeys: ['data1']
}
})
t.same(result, { data1: 'test' })
})
t.test('filterLog includes circular keys', async t => {
const result = filterLog({
log: logData,
context: {
...context,
includeKeys: ['level', 'circular']
}
})
t.same(result.circular.level, logData.level)
t.same(result.circular.time, logData.time)
delete result.circular
t.same(result, { level: 30 })
})
t.end()
})

View File

@@ -0,0 +1,66 @@
'use strict'
module.exports = formatTime
const {
DATE_FORMAT,
DATE_FORMAT_SIMPLE
} = require('../constants')
const dateformat = require('dateformat')
const createDate = require('./create-date')
const isValidDate = require('./is-valid-date')
/**
* 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 {boolean|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:
*
* - `<format string>`: 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:<format string>`: The returned display string will follow the given
* `<format string>` at the system's local timezone.
* - `UTC:<format string>`: The returned display string will follow the given
* `<format string>` at UTC.
*
* @returns {number|string} The formatted time.
*/
function formatTime (epoch, translateTime = false) {
if (translateTime === false) {
return epoch
}
const instant = createDate(epoch)
// If the Date is invalid, do not attempt to format
if (!isValidDate(instant)) {
return epoch
}
if (translateTime === true) {
return dateformat(instant, DATE_FORMAT_SIMPLE)
}
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}`)
}

View File

@@ -0,0 +1,71 @@
'use strict'
process.env.TZ = 'UTC'
const tap = require('tap')
const formatTime = require('./format-time')
const dateStr = '2019-04-06T13:30:00.000-04:00'
const epoch = new Date(dateStr)
const epochMS = epoch.getTime()
tap.test('passes through epoch if `translateTime` is `false`', async t => {
const formattedTime = formatTime(epochMS)
t.equal(formattedTime, epochMS)
})
tap.test('passes through epoch if date is invalid', async t => {
const input = 'this is not a date'
const formattedTime = formatTime(input, true)
t.equal(formattedTime, input)
})
tap.test('translates epoch milliseconds if `translateTime` is `true`', async t => {
const formattedTime = formatTime(epochMS, true)
t.equal(formattedTime, '17:30:00.000')
})
tap.test('translates epoch milliseconds to UTC string given format', async t => {
const formattedTime = formatTime(epochMS, 'd mmm yyyy H:MM')
t.equal(formattedTime, '6 Apr 2019 17:30')
})
tap.test('translates epoch milliseconds to SYS:STANDARD', async t => {
const formattedTime = formatTime(epochMS, 'SYS:STANDARD')
t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/)
})
tap.test('translates epoch milliseconds to SYS:<FORMAT>', async t => {
const formattedTime = formatTime(epochMS, 'SYS:d mmm yyyy H:MM')
t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/)
})
tap.test('passes through date string if `translateTime` is `false`', async t => {
const formattedTime = formatTime(dateStr)
t.equal(formattedTime, dateStr)
})
tap.test('translates date string if `translateTime` is `true`', async t => {
const formattedTime = formatTime(dateStr, true)
t.equal(formattedTime, '17:30:00.000')
})
tap.test('translates date string to UTC string given format', async t => {
const formattedTime = formatTime(dateStr, 'd mmm yyyy H:MM')
t.equal(formattedTime, '6 Apr 2019 17:30')
})
tap.test('translates date string to SYS:STANDARD', async t => {
const formattedTime = formatTime(dateStr, 'SYS:STANDARD')
t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/)
})
tap.test('translates date string to UTC:<FORMAT>', async t => {
const formattedTime = formatTime(dateStr, 'UTC:d mmm yyyy H:MM')
t.equal(formattedTime, '6 Apr 2019 17:30')
})
tap.test('translates date string to SYS:<FORMAT>', async t => {
const formattedTime = formatTime(dateStr, 'SYS:d mmm yyyy H:MM')
t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/)
})

View File

@@ -0,0 +1,30 @@
'use strict'
module.exports = getPropertyValue
const splitPropertyKey = require('./split-property-key')
/**
* Gets a specified property from an object if it exists.
*
* @param {object} obj The object to be searched.
* @param {string|string[]} property A string, or an array of strings, identifying
* the property to be retrieved from the object.
* Accepts nested properties delimited by a `.`.
* Delimiter can be escaped to preserve property names that contain the delimiter.
* e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`.
*
* @returns {*}
*/
function getPropertyValue (obj, property) {
const props = Array.isArray(property) ? property : splitPropertyKey(property)
for (const prop of props) {
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
return
}
obj = obj[prop]
}
return obj
}

View File

@@ -0,0 +1,31 @@
'use strict'
const tap = require('tap')
const getPropertyValue = require('./get-property-value')
tap.test('getPropertyValue returns the value of the property', async t => {
const result = getPropertyValue({
foo: 'bar'
}, 'foo')
t.same(result, 'bar')
})
tap.test('getPropertyValue returns the value of the nested property', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value')
t.same(result, 'bar')
})
tap.test('getPropertyValue returns the value of the nested property using the array of nested property keys', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value'])
t.same(result, 'bar')
})
tap.test('getPropertyValue returns undefined for non-existing properties', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value-2')
t.same(result, undefined)
})
tap.test('getPropertyValue returns undefined for non-existing properties using the array of nested property keys', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value-2'])
t.same(result, undefined)
})

View File

@@ -0,0 +1,38 @@
'use strict'
module.exports = handleCustomLevelsNamesOpts
/**
* Parse a CSV string or options object that maps level
* labels to level values.
*
* @param {string|object} cLevels An object mapping level
* names to level values, e.g. `{ info: 30, debug: 65 }`, or a
* CSV string in the format `level_name:level_value`, e.g.
* `info:30,debug:65`.
*
* @returns {object} An object mapping levels names to level values
* e.g. `{ info: 30, debug: 65 }`.
*/
function handleCustomLevelsNamesOpts (cLevels) {
if (!cLevels) return {}
if (typeof cLevels === 'string') {
return cLevels
.split(',')
.reduce((agg, value, idx) => {
const [levelName, levelNum = idx] = value.split(':')
agg[levelName.toLowerCase()] = levelNum
return agg
}, {})
} else if (Object.prototype.toString.call(cLevels) === '[object Object]') {
return Object
.keys(cLevels)
.reduce((agg, levelName) => {
agg[levelName.toLowerCase()] = cLevels[levelName]
return agg
}, {})
} else {
return {}
}
}

View File

@@ -0,0 +1,44 @@
'use strict'
const tap = require('tap')
const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts')
tap.test('returns a empty object `{}` for undefined parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts()
t.same(handledCustomLevelNames, {})
})
tap.test('returns a empty object `{}` for unknown parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts(123)
t.same(handledCustomLevelNames, {})
})
tap.test('returns a filled object for string parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts('ok:10,warn:20,error:35')
t.same(handledCustomLevelNames, {
ok: 10,
warn: 20,
error: 35
})
})
tap.test('returns a filled object for object parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts({
ok: 10,
warn: 20,
error: 35
})
t.same(handledCustomLevelNames, {
ok: 10,
warn: 20,
error: 35
})
})
tap.test('defaults missing level num to first index', async t => {
const result = handleCustomLevelsNamesOpts('ok:10,info')
t.same(result, {
ok: 10,
info: 1
})
})

View File

@@ -0,0 +1,39 @@
'use strict'
module.exports = handleCustomLevelsOpts
/**
* Parse a CSV string or options object that specifies
* configuration for custom levels.
*
* @param {string|object} cLevels An object mapping level
* names to values, e.g. `{ info: 30, debug: 65 }`, or a
* CSV string in the format `level_name:level_value`, e.g.
* `info:30,debug:65`.
*
* @returns {object} An object mapping levels to labels that
* appear in logs, e.g. `{ '30': 'INFO', '65': 'DEBUG' }`.
*/
function handleCustomLevelsOpts (cLevels) {
if (!cLevels) return {}
if (typeof cLevels === 'string') {
return cLevels
.split(',')
.reduce((agg, value, idx) => {
const [levelName, levelNum = idx] = value.split(':')
agg[levelNum] = levelName.toUpperCase()
return agg
},
{ default: 'USERLVL' })
} else if (Object.prototype.toString.call(cLevels) === '[object Object]') {
return Object
.keys(cLevels)
.reduce((agg, levelName) => {
agg[cLevels[levelName]] = levelName.toUpperCase()
return agg
}, { default: 'USERLVL' })
} else {
return {}
}
}

View File

@@ -0,0 +1,47 @@
'use strict'
const tap = require('tap')
const handleCustomLevelsOpts = require('./handle-custom-levels-opts')
tap.test('returns a empty object `{}` for undefined parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts()
t.same(handledCustomLevel, {})
})
tap.test('returns a empty object `{}` for unknown parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts(123)
t.same(handledCustomLevel, {})
})
tap.test('returns a filled object for string parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts('ok:10,warn:20,error:35')
t.same(handledCustomLevel, {
10: 'OK',
20: 'WARN',
35: 'ERROR',
default: 'USERLVL'
})
})
tap.test('returns a filled object for object parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts({
ok: 10,
warn: 20,
error: 35
})
t.same(handledCustomLevel, {
10: 'OK',
20: 'WARN',
35: 'ERROR',
default: 'USERLVL'
})
})
tap.test('defaults missing level num to first index', async t => {
const result = handleCustomLevelsOpts('ok:10,info')
t.same(result, {
10: 'OK',
1: 'INFO',
default: 'USERLVL'
})
})

99
backend/node_modules/pino-pretty/lib/utils/index.js generated vendored Normal file
View File

@@ -0,0 +1,99 @@
'use strict'
module.exports = {
buildSafeSonicBoom: require('./build-safe-sonic-boom.js'),
createDate: require('./create-date.js'),
deleteLogProperty: require('./delete-log-property.js'),
filterLog: require('./filter-log.js'),
formatTime: require('./format-time.js'),
getPropertyValue: require('./get-property-value.js'),
handleCustomLevelsNamesOpts: require('./handle-custom-levels-names-opts.js'),
handleCustomLevelsOpts: require('./handle-custom-levels-opts.js'),
interpretConditionals: require('./interpret-conditionals.js'),
isObject: require('./is-object.js'),
isValidDate: require('./is-valid-date.js'),
joinLinesWithIndentation: require('./join-lines-with-indentation.js'),
noop: require('./noop.js'),
parseFactoryOptions: require('./parse-factory-options.js'),
prettifyErrorLog: require('./prettify-error-log.js'),
prettifyError: require('./prettify-error.js'),
prettifyLevel: require('./prettify-level.js'),
prettifyMessage: require('./prettify-message.js'),
prettifyMetadata: require('./prettify-metadata.js'),
prettifyObject: require('./prettify-object.js'),
prettifyTime: require('./prettify-time.js'),
splitPropertyKey: require('./split-property-key.js')
}
// The remainder of this file consists of jsdoc blocks that are difficult to
// determine a more appropriate "home" for. As an example, the blocks associated
// with custom prettifiers could live in either the `prettify-level`,
// `prettify-metadata`, or `prettify-time` files since they are the primary
// files where such code is used. But we want a central place to define common
// doc blocks, so we are picking this file as the answer.
/**
* A hash of log property names mapped to prettifier functions. When the
* incoming log data is being processed for prettification, any key on the log
* that matches a key in a custom prettifiers hash will be prettified using
* that matching custom prettifier. The value passed to the custom prettifier
* will the value associated with the corresponding log key.
*
* The hash may contain any arbitrary keys for arbitrary log properties, but it
* may also contain a set of predefined key names that map to well-known log
* properties. These keys are:
*
* + `time` (for the timestamp field)
* + `level` (for the level label field; value may be a level number instead
* of a level label)
* + `hostname`
* + `pid`
* + `name`
* + `caller`
*
* @typedef {Object.<string, CustomPrettifierFunc>} CustomPrettifiers
*/
/**
* A synchronous function to be used for prettifying a log property. It must
* return a string.
*
* @typedef {function} CustomPrettifierFunc
* @param {any} value The value to be prettified for the key associated with
* the prettifier.
* @returns {string}
*/
/**
* A tokenized string that indicates how the prettified log line should be
* formatted. Tokens are either log properties enclosed in curly braces, e.g.
* `{levelLabel}`, `{pid}`, or `{req.url}`, or conditional directives in curly
* braces. The only conditional directives supported are `if` and `end`, e.g.
* `{if pid}{pid}{end}`; every `if` must have a matching `end`. Nested
* conditions are not supported.
*
* @typedef {string} MessageFormatString
*
* @example
* `{levelLabel} - {if pid}{pid} - {end}url:{req.url}`
*/
/**
* A function that accepts a log object, name of the message key, and name of
* the level label key and returns a formatted log line.
*
* Note: this function must be synchronous.
*
* @typedef {function} MessageFormatFunction
* @param {object} log The log object to be processed.
* @param {string} messageKey The name of the key in the `log` object that
* contains the log message.
* @param {string} levelLabel The name of the key in the `log` object that
* contains the log level name.
* @returns {string}
*
* @example
* function (log, messageKey, levelLabel) {
* return `${log[levelLabel]} - ${log[messageKey]}`
* }
*/

View File

@@ -0,0 +1,37 @@
'use strict'
const tap = require('tap')
const index = require('./index.js')
const { readdirSync } = require('fs')
const { basename } = require('path')
tap.test(
'index exports exactly all non-test files excluding itself',
async t => {
// Read all files in the `util` directory
const files = readdirSync(__dirname)
for (const file of files) {
const kebabName = basename(file, '.js')
const snakeName = kebabName.split('-').map((part, idx) => {
if (idx === 0) return part
return part[0].toUpperCase() + part.slice(1)
}).join('')
if (file.endsWith('.test.js') === false && file !== 'index.js') {
// We expect all files to be exported except…
t.ok(index[snakeName], `exports ${snakeName}`)
} else {
// …test files and the index file itself those must not be exported
t.notOk(index[snakeName], `does not export ${snakeName}`)
}
// Remove the exported file from the index object
delete index[snakeName]
}
// Now the index is expected to be empty, as nothing else should be
// exported from it
t.same(index, {}, 'does not export anything else')
}
)

View File

@@ -0,0 +1,37 @@
'use strict'
module.exports = interpretConditionals
const getPropertyValue = require('./get-property-value')
/**
* Translates all conditional blocks from within the messageFormat. Translates
* any matching {if key}{key}{end} statements and returns everything between
* if and else blocks if the key provided was found in log.
*
* @param {MessageFormatString|MessageFormatFunction} messageFormat A format
* string or function that defines how the logged message should be
* conditionally formatted.
* @param {object} log The log object to be modified.
*
* @returns {string} The parsed messageFormat.
*/
function interpretConditionals (messageFormat, log) {
messageFormat = messageFormat.replace(/{if (.*?)}(.*?){end}/g, replacer)
// Remove non-terminated if blocks
messageFormat = messageFormat.replace(/{if (.*?)}/g, '')
// Remove floating end blocks
messageFormat = messageFormat.replace(/{end}/g, '')
return messageFormat.replace(/\s+/g, ' ').trim()
function replacer (_, key, value) {
const propertyValue = getPropertyValue(log, key)
if (propertyValue && value.includes(key)) {
return value.replace(new RegExp('{' + key + '}', 'g'), propertyValue)
} else {
return ''
}
}
}

View File

@@ -0,0 +1,69 @@
'use strict'
const tap = require('tap')
const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})
const interpretConditionals = require('./interpret-conditionals')
const logData = {
level: 30,
data1: {
data2: 'bar'
},
msg: 'foo'
}
tap.test('interpretConditionals translates if / else statement to found property value', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}{end}', log), '{level} - bar')
})
tap.test('interpretConditionals translates if / else statement to found property value and leave unmatched property key untouched', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2} ({msg}){end}', log), '{level} - bar ({msg})')
})
tap.test('interpretConditionals removes non-terminated if statements', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}', log), '{level} - {data1.data2}')
})
tap.test('interpretConditionals removes floating end statements', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {data1.data2}{end}', log), '{level} - {data1.data2}')
})
tap.test('interpretConditionals removes floating end statements within translated if / end statements', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if msg}({msg}){end}{end}', log), '{level} - (foo)')
})
tap.test('interpretConditionals removes if / end blocks if existent condition key does not match existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if msg}{data1.data2}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if foo}{msg}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if existent condition key does not match non-existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if msg}{foo}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match non-existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if foo}{bar}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if nested condition key does not match property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if data1.msg}{data1.data2}{end}', log), '{level}')
})
tap.test('interpretConditionals removes nested if / end statement blocks', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{if msg}{if data1.data2}{msg}{data1.data2}{end}{end}', log), 'foo{data1.data2}')
})

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = isObject
function isObject (input) {
return Object.prototype.toString.apply(input) === '[object Object]'
}

View File

@@ -0,0 +1,10 @@
'use strict'
const tap = require('tap')
const isObject = require('./is-object')
tap.test('returns correct answer', async t => {
t.equal(isObject({}), true)
t.equal(isObject([]), false)
t.equal(isObject(42), false)
})

View File

@@ -0,0 +1,14 @@
'use strict'
module.exports = isValidDate
/**
* Checks if the argument is a JS Date and not 'Invalid Date'.
*
* @param {Date} date The date to check.
*
* @returns {boolean} true if the argument is a JS Date and not 'Invalid Date'.
*/
function isValidDate (date) {
return date instanceof Date && !Number.isNaN(date.getTime())
}

View File

@@ -0,0 +1,16 @@
'use strict'
process.env.TZ = 'UTC'
const tap = require('tap')
const isValidDate = require('./is-valid-date')
tap.test('returns true for valid dates', async t => {
t.same(isValidDate(new Date()), true)
})
tap.test('returns false for non-dates and invalid dates', async t => {
t.plan(2)
t.same(isValidDate('20210621'), false)
t.same(isValidDate(new Date('2021-41-99')), false)
})

View File

@@ -0,0 +1,29 @@
'use strict'
module.exports = joinLinesWithIndentation
/**
* @typedef {object} JoinLinesWithIndentationParams
* @property {string} input The string to split and reformat.
* @property {string} [ident] The indentation string. Default: ` ` (4 spaces).
* @property {string} [eol] The end of line sequence to use when rejoining
* the lines. Default: `'\n'`.
*/
/**
* 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 {JoinLinesWithIndentationParams} input
*
* @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 (let i = 1; i < lines.length; i += 1) {
lines[i] = ident + lines[i]
}
return lines.join(eol)
}

View File

@@ -0,0 +1,16 @@
'use strict'
const tap = require('tap')
const joinLinesWithIndentation = require('./join-lines-with-indentation')
tap.test('joinLinesWithIndentation adds indentation to beginning of subsequent lines', async t => {
const input = 'foo\nbar\nbaz'
const result = joinLinesWithIndentation({ input })
t.equal(result, 'foo\n bar\n baz')
})
tap.test('joinLinesWithIndentation accepts custom indentation, line breaks, and eol', async t => {
const input = 'foo\nbar\r\nbaz'
const result = joinLinesWithIndentation({ input, ident: ' ', eol: '^' })
t.equal(result, 'foo^ bar^ baz')
})

3
backend/node_modules/pino-pretty/lib/utils/noop.js generated vendored Normal file
View File

@@ -0,0 +1,3 @@
'use strict'
module.exports = function noop () {}

View File

@@ -0,0 +1,12 @@
'use strict'
const tap = require('tap')
const noop = require('./noop')
tap.test('is a function', async t => {
t.type(noop, Function)
})
tap.test('does nothing', async t => {
t.equal(noop('stuff'), undefined)
})

View File

@@ -0,0 +1,153 @@
'use strict'
module.exports = parseFactoryOptions
const {
LEVEL_NAMES
} = require('../constants')
const colors = require('../colors')
const handleCustomLevelsOpts = require('./handle-custom-levels-opts')
const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts')
/**
* A `PrettyContext` is an object to be used by the various functions that
* process log data. It is derived from the provided {@link PinoPrettyOptions}.
* It may be used as a `this` context.
*
* @typedef {object} PrettyContext
* @property {string} EOL The escape sequence chosen as the line terminator.
* @property {string} IDENT The string to use as the indentation sequence.
* @property {ColorizerFunc} colorizer A configured colorizer function.
* @property {Array[Array<number, string>]} customColors A set of custom color
* names associated with level numbers.
* @property {object} customLevelNames A hash of level numbers to level names,
* e.g. `{ 30: "info" }`.
* @property {object} customLevels A hash of level names to level numbers,
* e.g. `{ info: 30 }`.
* @property {CustomPrettifiers} customPrettifiers A hash of custom prettifier
* functions.
* @property {object} customProperties Comprised of `customLevels` and
* `customLevelNames` if such options are provided.
* @property {string[]} errorLikeObjectKeys The key names in the log data that
* should be considered as holding error objects.
* @property {string[]} errorProps A list of error object keys that should be
* included in the output.
* @property {boolean} hideObject Indicates the prettifier should omit objects
* in the output.
* @property {string[]} ignoreKeys Set of log data keys to omit.
* @property {string[]} includeKeys Opposite of `ignoreKeys`.
* @property {boolean} levelFirst Indicates the level should be printed first.
* @property {string} levelKey Name of the key in the log data that contains
* the message.
* @property {string} levelLabel Format token to represent the position of the
* level name in the output string.
* @property {MessageFormatString|MessageFormatFunction} messageFormat
* @property {string} messageKey Name of the key in the log data that contains
* the message.
* @property {string|number} minimumLevel The minimum log level to process
* and output.
* @property {ColorizerFunc} objectColorizer
* @property {boolean} singleLine Indicates objects should be printed on a
* single output line.
* @property {string} timestampKey The name of the key in the log data that
* contains the log timestamp.
* @property {boolean} translateTime Indicates if timestamps should be
* translated to a human-readable string.
* @property {boolean} useOnlyCustomProps
*/
/**
* @param {PinoPrettyOptions} options The user supplied object of options.
*
* @returns {PrettyContext}
*/
function parseFactoryOptions (options) {
const EOL = options.crlf ? '\r\n' : '\n'
const IDENT = ' '
const {
customPrettifiers,
errorLikeObjectKeys,
hideObject,
levelFirst,
levelKey,
levelLabel,
messageFormat,
messageKey,
minimumLevel,
singleLine,
timestampKey,
translateTime
} = options
const errorProps = options.errorProps.split(',')
const useOnlyCustomProps = typeof options.useOnlyCustomProps === 'boolean'
? options.useOnlyCustomProps
: (options.useOnlyCustomProps === 'true')
const customLevels = handleCustomLevelsOpts(options.customLevels)
const customLevelNames = handleCustomLevelsNamesOpts(options.customLevels)
let customColors
if (options.customColors) {
customColors = options.customColors.split(',').reduce((agg, value) => {
const [level, color] = value.split(':')
const condition = useOnlyCustomProps
? options.customLevels
: customLevelNames[level] !== undefined
const levelNum = condition
? customLevelNames[level]
: LEVEL_NAMES[level]
const colorIdx = levelNum !== undefined
? levelNum
: level
agg.push([colorIdx, color])
return agg
}, [])
}
const customProperties = { customLevels, customLevelNames }
if (useOnlyCustomProps === true && !options.customLevels) {
customProperties.customLevels = undefined
customProperties.customLevelNames = undefined
}
const includeKeys = options.include !== undefined
? new Set(options.include.split(','))
: undefined
const ignoreKeys = (!includeKeys && options.ignore)
? new Set(options.ignore.split(','))
: undefined
const colorizer = colors(options.colorize, customColors, useOnlyCustomProps)
const objectColorizer = options.colorizeObjects
? colorizer
: colors(false, [], false)
return {
EOL,
IDENT,
colorizer,
customColors,
customLevelNames,
customLevels,
customPrettifiers,
customProperties,
errorLikeObjectKeys,
errorProps,
hideObject,
ignoreKeys,
includeKeys,
levelFirst,
levelKey,
levelLabel,
messageFormat,
messageKey,
minimumLevel,
objectColorizer,
singleLine,
timestampKey,
translateTime,
useOnlyCustomProps
}
}

View File

@@ -0,0 +1,73 @@
'use strict'
module.exports = prettifyErrorLog
const {
LOGGER_KEYS
} = require('../constants')
const isObject = require('./is-object')
const joinLinesWithIndentation = require('./join-lines-with-indentation')
const prettifyObject = require('./prettify-object')
/**
* @typedef {object} PrettifyErrorLogParams
* @property {object} log The error log to prettify.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Given a log object that has a `type: 'Error'` key, prettify the object and
* return the result. In other
*
* @param {PrettifyErrorLogParams} input
*
* @returns {string} A string that represents the prettified error log.
*/
function prettifyErrorLog ({ log, context }) {
const {
EOL: eol,
IDENT: ident,
errorProps: errorProperties,
messageKey
} = context
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 specified properties unless the property is a standard exclusion.
propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false)
}
for (let 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({
log: log[key],
excludeLoggerKeys: false,
context: {
...context,
IDENT: ident + ident
}
})
result = `${result}${ident}${key}: {${eol}${prettifiedObject}${ident}}${eol}`
continue
}
result = `${result}${ident}${key}: ${log[key]}${eol}`
}
}
return result
}

View File

@@ -0,0 +1,110 @@
'use strict'
const tap = require('tap')
const prettifyErrorLog = require('./prettify-error-log')
const {
ERROR_LIKE_KEYS,
MESSAGE_KEY
} = require('../constants')
const context = {
EOL: '\n',
IDENT: ' ',
customPrettifiers: {},
errorLikeObjectKeys: ERROR_LIKE_KEYS,
errorProps: [],
messageKey: MESSAGE_KEY
}
tap.test('returns string with default settings', async t => {
const err = Error('Something went wrong')
const str = prettifyErrorLog({ log: err, context })
t.ok(str.startsWith(' Error: Something went wrong'))
})
tap.test('returns string with custom ident', async t => {
const err = Error('Something went wrong')
const str = prettifyErrorLog({
log: err,
context: {
...context,
IDENT: ' '
}
})
t.ok(str.startsWith(' Error: Something went wrong'))
})
tap.test('returns string with custom eol', async t => {
const err = Error('Something went wrong')
const str = prettifyErrorLog({
log: err,
context: {
...context,
EOL: '\r\n'
}
})
t.ok(str.startsWith(' Error: Something went wrong\r\n'))
})
tap.test('errorProperties', t => {
t.test('excludes all for wildcard', async t => {
const err = Error('boom')
err.foo = 'foo'
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['*']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: "foo"'), false)
})
t.test('excludes only selected properties', async t => {
const err = Error('boom')
err.foo = 'foo'
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['foo']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: foo'), true)
})
t.test('ignores specified properties if not present', async t => {
const err = Error('boom')
err.foo = 'foo'
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['foo', 'bar']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: foo'), true)
t.equal(str.includes('bar'), false)
})
t.test('processes nested objects', async t => {
const err = Error('boom')
err.foo = { bar: 'bar', message: 'included' }
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['foo']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: {'), true)
t.equal(str.includes('bar: "bar"'), true)
t.equal(str.includes('message: "included"'), true)
})
t.end()
})

View File

@@ -0,0 +1,49 @@
'use strict'
module.exports = prettifyError
const joinLinesWithIndentation = require('./join-lines-with-indentation')
/**
* @typedef {object} PrettifyErrorParams
* @property {string} keyName The key assigned to this error in the log object.
* @property {string} lines The STRINGIFIED error. If the error field has a
* custom prettifier, that should be pre-applied as well.
* @property {string} ident The indentation sequence to use.
* @property {string} eol The EOL sequence to use.
*/
/**
* Prettifies an error string into a multi-line format.
*
* @param {PrettifyErrorParams} input
*
* @returns {string}
*/
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
}
} else {
result += line
}
}
return result
}

View File

@@ -0,0 +1,14 @@
'use strict'
const tap = require('tap')
const stringifySafe = require('fast-safe-stringify')
const prettifyError = require('./prettify-error')
tap.test('prettifies error', t => {
const error = Error('Bad error!')
const lines = stringifySafe(error, Object.getOwnPropertyNames(error), 2)
const prettyError = prettifyError({ keyName: 'errorKey', lines, ident: ' ', eol: '\n' })
t.match(prettyError, /\s*errorKey: {\n\s*"stack":[\s\S]*"message": "Bad error!"/)
t.end()
})

View File

@@ -0,0 +1,35 @@
'use strict'
module.exports = prettifyLevel
const getPropertyValue = require('./get-property-value')
/**
* @typedef {object} PrettifyLevelParams
* @property {object} log The log object.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Checks if the passed in log has a `level` value and returns a prettified
* string for that level if so.
*
* @param {PrettifyLevelParams} input
*
* @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, context }) {
const {
colorizer,
customLevels,
customLevelNames,
levelKey
} = context
const prettifier = context.customPrettifiers?.level
const output = getPropertyValue(log, levelKey)
if (output === undefined) return undefined
return prettifier ? prettifier(output) : colorizer(output, { customLevels, customLevelNames })
}

View File

@@ -0,0 +1,68 @@
'use strict'
const tap = require('tap')
const prettifyLevel = require('./prettify-level')
const getColorizer = require('../colors')
const {
LEVEL_KEY
} = require('../constants')
const context = {
colorizer: getColorizer(),
customLevelNames: undefined,
customLevels: undefined,
levelKey: LEVEL_KEY,
customPrettifiers: undefined
}
tap.test('returns `undefined` for unknown level', async t => {
const colorized = prettifyLevel({
log: {},
context: {
...context
}
})
t.equal(colorized, undefined)
})
tap.test('returns non-colorized value for default colorizer', async t => {
const log = {
level: 30
}
const colorized = prettifyLevel({
log,
context: {
...context
}
})
t.equal(colorized, 'INFO')
})
tap.test('returns colorized value for color colorizer', async t => {
const log = {
level: 30
}
const colorizer = getColorizer(true)
const colorized = prettifyLevel({
log,
context: {
...context,
colorizer
}
})
t.equal(colorized, '\u001B[32mINFO\u001B[39m')
})
tap.test('passes output through provided prettifier', async t => {
const log = {
level: 30
}
const colorized = prettifyLevel({
log,
context: {
...context,
customPrettifiers: { level () { return 'modified' } }
}
})
t.equal(colorized, 'modified')
})

View File

@@ -0,0 +1,63 @@
'use strict'
module.exports = prettifyMessage
const {
LEVELS
} = require('../constants')
const getPropertyValue = require('./get-property-value')
const interpretConditionals = require('./interpret-conditionals')
/**
* @typedef {object} PrettifyMessageParams
* @property {object} log The log object with the message to colorize.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies a message string if the given `log` has a message property.
*
* @param {PrettifyMessageParams} input
*
* @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, context }) {
const {
colorizer,
customLevels,
levelKey,
levelLabel,
messageFormat,
messageKey,
useOnlyCustomProps
} = context
if (messageFormat && typeof messageFormat === 'string') {
const parsedMessageFormat = interpretConditionals(messageFormat, log)
const message = String(parsedMessageFormat).replace(
/{([^{}]+)}/g,
function (match, p1) {
// return log level as string instead of int
let level
if (p1 === levelLabel && (level = getPropertyValue(log, levelKey)) !== undefined) {
const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[level] === undefined
return condition ? LEVELS[level] : customLevels[level]
}
// Parse nested key access, e.g. `{keyA.subKeyB}`.
return getPropertyValue(log, p1) || ''
})
return colorizer.message(message)
}
if (messageFormat && typeof messageFormat === 'function') {
const msg = messageFormat(log, messageKey, levelLabel)
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(log[messageKey])
}

View File

@@ -0,0 +1,187 @@
'use strict'
const tap = require('tap')
const prettifyMessage = require('./prettify-message')
const getColorizer = require('../colors')
const {
LEVEL_KEY,
LEVEL_LABEL
} = require('../constants')
const context = {
colorizer: getColorizer(),
levelKey: LEVEL_KEY,
levelLabel: LEVEL_LABEL,
messageKey: 'msg'
}
tap.test('returns `undefined` if `messageKey` not found', async t => {
const str = prettifyMessage({ log: {}, context })
t.equal(str, undefined)
})
tap.test('returns `undefined` if `messageKey` not string', async t => {
const str = prettifyMessage({ log: { msg: {} }, context })
t.equal(str, undefined)
})
tap.test('returns non-colorized value for default colorizer', async t => {
const colorizer = getColorizer()
const str = prettifyMessage({
log: { msg: 'foo' },
context: { ...context, colorizer }
})
t.equal(str, 'foo')
})
tap.test('returns non-colorized value for alternate `messageKey`', async t => {
const str = prettifyMessage({
log: { message: 'foo' },
context: { ...context, messageKey: 'message' }
})
t.equal(str, 'foo')
})
tap.test('returns colorized value for color colorizer', async t => {
const colorizer = getColorizer(true)
const str = prettifyMessage({
log: { msg: 'foo' },
context: { ...context, colorizer }
})
t.equal(str, '\u001B[36mfoo\u001B[39m')
})
tap.test('returns colorized value for color colorizer for alternate `messageKey`', async t => {
const colorizer = getColorizer(true)
const str = prettifyMessage({
log: { message: 'foo' },
context: { ...context, messageKey: 'message', colorizer }
})
t.equal(str, '\u001B[36mfoo\u001B[39m')
})
tap.test('returns message formatted by `messageFormat` option', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule' },
context: { ...context, messageFormat: '{context} - {msg}' }
})
t.equal(str, 'appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - missing prop', async t => {
const str = prettifyMessage({
log: { context: 'appModule' },
context: { ...context, messageFormat: '{context} - {msg}' }
})
t.equal(str, 'appModule - ')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 30 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: {}
}
})
t.equal(str, '[30] INFO appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 30 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 30: 'CHECK' },
useOnlyCustomProps: true
}
})
t.equal(str, '[30] CHECK appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 123 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 123: 'CUSTOM' }
}
})
t.equal(str, '[123] CUSTOM appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 123 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 123: 'CUSTOM' },
useOnlyCustomProps: true
}
})
t.equal(str, '[123] CUSTOM appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 40 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 123: 'CUSTOM' },
useOnlyCustomProps: false
}
})
t.equal(str, '[40] WARN appModule - foo')
})
tap.test('`messageFormat` supports nested curly brackets', async t => {
const str = prettifyMessage({
log: { level: 30 },
context: {
...context,
messageFormat: '{{level}}-{level}-{{level}-{level}}'
}
})
t.equal(str, '{30}-30-{30-30}')
})
tap.test('`messageFormat` supports nested object', async t => {
const str = prettifyMessage({
log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' },
context: {
...context,
messageFormat: '{request.url} - param: {request.params.process} - {msg}'
}
})
t.equal(str, 'localhost/test - param: - foo')
})
tap.test('`messageFormat` supports conditional blocks', async t => {
const str = prettifyMessage({
log: { level: 30, req: { id: 'foo' } },
context: {
...context,
messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}'
}
})
t.equal(str, '30 | (foo)')
})
tap.test('`messageFormat` supports function definition', async t => {
const str = prettifyMessage({
log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' },
context: {
...context,
messageFormat: (log, messageKey, levelLabel) => {
let msg = log[messageKey]
if (msg === 'incoming request') msg = `--> ${log.request.url}`
return msg
}
}
})
t.equal(str, '--> localhost/test')
})

View File

@@ -0,0 +1,61 @@
'use strict'
module.exports = prettifyMetadata
/**
* @typedef {object} PrettifyMetadataParams
* @property {object} log The log that may or may not contain metadata to
* be prettified.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies metadata that is usually present in a Pino log line. It looks for
* fields `name`, `pid`, `hostname`, and `caller` and returns a formatted string using
* the fields it finds.
*
* @param {PrettifyMetadataParams} input
*
* @returns {undefined|string} If no metadata is found then `undefined` is
* returned. Otherwise, a string of prettified metadata is returned.
*/
function prettifyMetadata ({ log, context }) {
const prettifiers = context.customPrettifiers
let line = ''
if (log.name || log.pid || log.hostname) {
line += '('
if (log.name) {
line += prettifiers.name ? prettifiers.name(log.name) : log.name
}
if (log.pid) {
const prettyPid = prettifiers.pid ? prettifiers.pid(log.pid) : log.pid
if (log.name && log.pid) {
line += '/' + prettyPid
} else {
line += prettyPid
}
}
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'} ${prettifiers.hostname ? prettifiers.hostname(log.hostname) : log.hostname}`
}
line += ')'
}
if (log.caller) {
line += `${line === '' ? '' : ' '}<${prettifiers.caller ? prettifiers.caller(log.caller) : log.caller}>`
}
if (line === '') {
return undefined
} else {
return line
}
}

View File

@@ -0,0 +1,111 @@
'use strict'
const tap = require('tap')
const prettifyMetadata = require('./prettify-metadata')
const context = {
customPrettifiers: {}
}
tap.test('returns `undefined` if no metadata present', async t => {
const str = prettifyMetadata({ log: {}, context })
t.equal(str, undefined)
})
tap.test('works with only `name` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo' }, context })
t.equal(str, '(foo)')
})
tap.test('works with only `pid` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234' }, context })
t.equal(str, '(1234)')
})
tap.test('works with only `hostname` present', async t => {
const str = prettifyMetadata({ log: { hostname: 'bar' }, context })
t.equal(str, '(on bar)')
})
tap.test('works with only `name` & `pid` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' }, context })
t.equal(str, '(foo/1234)')
})
tap.test('works with only `name` & `hostname` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' }, context })
t.equal(str, '(foo on bar)')
})
tap.test('works with only `pid` & `hostname` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' }, context })
t.equal(str, '(1234 on bar)')
})
tap.test('works with only `name`, `pid`, & `hostname` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' }, context })
t.equal(str, '(foo/1234 on bar)')
})
tap.test('works with only `name` & `caller` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' }, context })
t.equal(str, '(foo) <baz>')
})
tap.test('works with only `pid` & `caller` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' }, context })
t.equal(str, '(1234) <baz>')
})
tap.test('works with only `hostname` & `caller` present', async t => {
const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(on bar) <baz>')
})
tap.test('works with only `name`, `pid`, & `caller` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' }, context })
t.equal(str, '(foo/1234) <baz>')
})
tap.test('works with only `name`, `hostname`, & `caller` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(foo on bar) <baz>')
})
tap.test('works with only `caller` present', async t => {
const str = prettifyMetadata({ log: { caller: 'baz' }, context })
t.equal(str, '<baz>')
})
tap.test('works with only `pid`, `hostname`, & `caller` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(1234 on bar) <baz>')
})
tap.test('works with all four present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(foo/1234 on bar) <baz>')
})
tap.test('uses prettifiers from passed prettifiers object', async t => {
const prettifiers = {
name (input) {
return input.toUpperCase()
},
pid (input) {
return input + '__'
},
hostname (input) {
return input.toUpperCase()
},
caller (input) {
return input.toUpperCase()
}
}
const str = prettifyMetadata({
log: { pid: '1234', hostname: 'bar', caller: 'baz', name: 'joe' },
context: {
customPrettifiers: prettifiers
}
})
t.equal(str, '(JOE/1234__ on BAR) <BAZ>')
})

View File

@@ -0,0 +1,111 @@
'use strict'
module.exports = prettifyObject
const {
LOGGER_KEYS
} = require('../constants')
const stringifySafe = require('fast-safe-stringify')
const joinLinesWithIndentation = require('./join-lines-with-indentation')
const prettifyError = require('./prettify-error')
/**
* @typedef {object} PrettifyObjectParams
* @property {object} log The object to prettify.
* @property {boolean} [excludeLoggerKeys] Indicates if known logger specific
* keys should be excluded from prettification. Default: `true`.
* @property {string[]} [skipKeys] A set of object keys to exclude from the
* * prettified result. Default: `[]`.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* 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 {PrettifyObjectParams} input
*
* @returns {string} The prettified string. This can be as little as `''` if
* there was nothing to prettify.
*/
function prettifyObject ({
log,
excludeLoggerKeys = true,
skipKeys = [],
context
}) {
const {
EOL: eol,
IDENT: ident,
customPrettifiers,
errorLikeObjectKeys: errorLikeKeys,
objectColorizer,
singleLine
} = context
const keysToIgnore = [].concat(skipKeys)
/* istanbul ignore else */
if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS)
let result = ''
// Split object keys into two categories: error and non-error
const { plain, errors } = Object.entries(log).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, log)
: v
if (errorLikeKeys.includes(k)) {
errors[k] = pretty
} else {
plain[k] = pretty
}
}
return { plain, errors }
}, { plain: {}, errors: {} })
if (singleLine) {
// Stringify the entire object as a single JSON line
/* istanbul ignore else */
if (Object.keys(plain).length > 0) {
result += objectColorizer.greyMessage(stringifySafe(plain))
}
result += eol
// Avoid printing the escape character on escaped backslashes.
result = result.replace(/\\\\/gi, '\\')
} 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
let lines = typeof customPrettifiers[keyName] === 'function'
? keyValue
: stringifySafe(keyValue, null, 2)
if (lines === undefined) return
// Avoid printing the escape character on escaped backslashes.
lines = lines.replace(/\\\\/gi, '\\')
const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
result += `${ident}${keyName}:${joinedLines.startsWith(eol) ? '' : ' '}${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
}

View File

@@ -0,0 +1,152 @@
'use strict'
const tap = require('tap')
const colors = require('../colors')
const prettifyObject = require('./prettify-object')
const {
ERROR_LIKE_KEYS
} = require('../constants')
const context = {
EOL: '\n',
IDENT: ' ',
customPrettifiers: {},
errorLikeObjectKeys: ERROR_LIKE_KEYS,
objectColorizer: colors(),
singleLine: false
}
tap.test('returns empty string if no properties present', async t => {
const str = prettifyObject({ log: {}, context })
t.equal(str, '')
})
tap.test('works with single level properties', async t => {
const str = prettifyObject({ log: { foo: 'bar' }, context })
t.equal(str, ' foo: "bar"\n')
})
tap.test('works with multiple level properties', async t => {
const str = prettifyObject({ log: { foo: { bar: 'baz' } }, context })
t.equal(str, ' foo: {\n "bar": "baz"\n }\n')
})
tap.test('skips specified keys', async t => {
const str = prettifyObject({
log: { foo: 'bar', hello: 'world' },
skipKeys: ['foo'],
context
})
t.equal(str, ' hello: "world"\n')
})
tap.test('ignores predefined keys', async t => {
const str = prettifyObject({ log: { foo: 'bar', pid: 12345 }, context })
t.equal(str, ' foo: "bar"\n')
})
tap.test('ignores escaped backslashes in string values', async t => {
const str = prettifyObject({ log: { foo_regexp: '\\[^\\w\\s]\\' }, context })
t.equal(str, ' foo_regexp: "\\[^\\w\\s]\\"\n')
})
tap.test('ignores escaped backslashes in string values (singleLine option)', async t => {
const str = prettifyObject({
log: { foo_regexp: '\\[^\\w\\s]\\' },
context: {
...context,
singleLine: true
}
})
t.equal(str, '{"foo_regexp":"\\[^\\w\\s]\\"}\n')
})
tap.test('works with error props', async t => {
const err = Error('Something went wrong')
const serializedError = {
message: err.message,
stack: err.stack
}
const str = prettifyObject({ log: { error: serializedError }, context })
t.ok(str.startsWith(' error:'))
t.ok(str.includes(' "message": "Something went wrong",'))
t.ok(str.includes(' Error: Something went wrong'))
})
tap.test('customPrettifiers gets applied', async t => {
const customPrettifiers = {
foo: v => v.toUpperCase()
}
const str = prettifyObject({
log: { foo: 'foo' },
context: {
...context,
customPrettifiers
}
})
t.equal(str.startsWith(' foo: FOO'), true)
})
tap.test('skips lines omitted by customPrettifiers', async t => {
const customPrettifiers = {
foo: () => { return undefined }
}
const str = prettifyObject({
log: { foo: 'foo', bar: 'bar' },
context: {
...context,
customPrettifiers
}
})
t.equal(str.includes('bar: "bar"'), true)
t.equal(str.includes('foo: "foo"'), false)
})
tap.test('joined lines omits starting eol', async t => {
const str = prettifyObject({
log: { msg: 'doing work', calls: ['step 1', 'step 2', 'step 3'], level: 30 },
context: {
...context,
IDENT: '',
customPrettifiers: {
calls: val => '\n' + val.map(it => ' ' + it).join('\n')
}
}
})
t.equal(str, [
'msg: "doing work"',
'calls:',
' step 1',
' step 2',
' step 3',
''
].join('\n'))
})
tap.test('errors skips prettifiers', async t => {
const customPrettifiers = {
err: () => { return 'is_err' }
}
const str = prettifyObject({
log: { err: Error('boom') },
context: {
...context,
customPrettifiers
}
})
t.equal(str.includes('err: is_err'), true)
})
tap.test('errors skips prettifying if no lines are present', async t => {
const customPrettifiers = {
err: () => { return undefined }
}
const str = prettifyObject({
log: { err: Error('boom') },
context: {
...context,
customPrettifiers
}
})
t.equal(str, '')
})

View File

@@ -0,0 +1,42 @@
'use strict'
module.exports = prettifyTime
const formatTime = require('./format-time')
/**
* @typedef {object} PrettifyTimeParams
* @property {object} log The log object with the timestamp to be prettified.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies a timestamp if the given `log` has either `time`, `timestamp` or custom specified timestamp
* property.
*
* @param {PrettifyTimeParams} input
*
* @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, context }) {
const {
timestampKey,
translateTime: translateFormat
} = context
const prettifier = context.customPrettifiers?.time
let time = null
if (timestampKey in log) {
time = log[timestampKey]
} else if ('timestamp' in log) {
time = log.timestamp
}
if (time === null) return undefined
const output = translateFormat ? formatTime(time, translateFormat) : time
return prettifier ? prettifier(output) : `[${output}]`
}

View File

@@ -0,0 +1,227 @@
'use strict'
process.env.TZ = 'UTC'
const tap = require('tap')
const prettifyTime = require('./prettify-time')
const {
TIMESTAMP_KEY
} = require('../constants')
const context = {
timestampKey: TIMESTAMP_KEY,
translateTime: true,
customPrettifiers: {}
}
tap.test('returns `undefined` if `time` or `timestamp` not in log', async t => {
const str = prettifyTime({ log: {}, context })
t.equal(str, undefined)
})
tap.test('returns prettified formatted time from custom field', async t => {
const log = { customtime: 1554642900000 }
let str = prettifyTime({
log,
context: {
...context,
timestampKey: 'customtime'
}
})
t.equal(str, '[13:15:00.000]')
str = prettifyTime({
log,
context: {
...context,
translateTime: false,
timestampKey: 'customtime'
}
})
t.equal(str, '[1554642900000]')
})
tap.test('returns prettified formatted time', async t => {
let log = { time: 1554642900000 }
let str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { timestamp: 1554642900000 }
str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { time: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { time: 1554642900000 }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
log = { timestamp: 1554642900000 }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
log = { time: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
})
tap.test('passes through value', async t => {
let log = { time: 1554642900000 }
let str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[1554642900000]')
log = { timestamp: 1554642900000 }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[1554642900000]')
log = { time: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[2019-04-07T09:15:00.000-04:00]')
log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[2019-04-07T09:15:00.000-04:00]')
})
tap.test('handles the 0 timestamp', async t => {
let log = { time: 0 }
let str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[0]')
log = { timestamp: 0 }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[0]')
})
tap.test('works with epoch as a number or string', (t) => {
t.plan(3)
const epoch = 1522431328992
const asNumber = prettifyTime({
log: { time: epoch, msg: 'foo' },
context: {
...context,
translateTime: true
}
})
const asString = prettifyTime({
log: { time: `${epoch}`, msg: 'foo' },
context: {
...context,
translateTime: true
}
})
const invalid = prettifyTime({
log: { time: '2 days ago', msg: 'foo' },
context: {
...context,
translateTime: true
}
})
t.same(asString, '[17:35:28.992]')
t.same(asNumber, '[17:35:28.992]')
t.same(invalid, '[2 days ago]')
})
tap.test('uses custom prettifier', async t => {
const str = prettifyTime({
log: { time: 0 },
context: {
...context,
customPrettifiers: {
time () {
return 'done'
}
}
}
})
t.equal(str, 'done')
})

View File

@@ -0,0 +1,49 @@
'use strict'
module.exports = splitPropertyKey
/**
* Splits the property key delimited by a dot character but not when it is preceded
* by a backslash.
*
* @param {string} key A string identifying the property.
*
* @returns {string[]} Returns a list of string containing each delimited property.
* e.g. `'prop2\.domain\.corp.prop2'` should return [ 'prop2.domain.com', 'prop2' ]
*/
function splitPropertyKey (key) {
const result = []
let backslash = false
let segment = ''
for (let i = 0; i < key.length; i++) {
const c = key.charAt(i)
if (c === '\\') {
backslash = true
continue
}
if (backslash) {
backslash = false
segment += c
continue
}
/* Non-escaped dot, push to result */
if (c === '.') {
result.push(segment)
segment = ''
continue
}
segment += c
}
/* Push last entry to result */
if (segment.length) {
result.push(segment)
}
return result
}

View File

@@ -0,0 +1,29 @@
'use strict'
const tap = require('tap')
const splitPropertyKey = require('./split-property-key')
tap.test('splitPropertyKey does not change key', async t => {
const result = splitPropertyKey('data1')
t.same(result, ['data1'])
})
tap.test('splitPropertyKey splits nested key', async t => {
const result = splitPropertyKey('data1.data2.data-3')
t.same(result, ['data1', 'data2', 'data-3'])
})
tap.test('splitPropertyKey splits nested keys ending with a dot', async t => {
const result = splitPropertyKey('data1.data2.data-3.')
t.same(result, ['data1', 'data2', 'data-3'])
})
tap.test('splitPropertyKey splits nested escaped key', async t => {
const result = splitPropertyKey('logging\\.domain\\.corp/operation.foo.bar-2')
t.same(result, ['logging.domain.corp/operation', 'foo', 'bar-2'])
})
tap.test('splitPropertyKey splits nested escaped key with special characters', async t => {
const result = splitPropertyKey('logging\\.domain\\.corp/operation.!\t@#$%^&*()_+=-<>.bar\\.2')
t.same(result, ['logging.domain.corp/operation', '!\t@#$%^&*()_+=-<>', 'bar.2'])
})