diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e50fb487..dfa7fa6c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,13 @@ version: 2 updates: -- package-ecosystem: github-actions - directory: '/' - ignore: - - dependency-name: 'actions/*' - update-types: - ['version-update:semver-minor', 'version-update:semver-patch'] - schedule: - interval: daily - open-pull-requests-limit: 10 -- package-ecosystem: npm - directory: '/' - schedule: - interval: daily - open-pull-requests-limit: 10 \ No newline at end of file + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fe724ad..724be794 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Restore cached dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: node_modules key: node-modules-${{ hashFiles('package.json') }} diff --git a/Readme.md b/Readme.md index 430a1350..e35d1c90 100644 --- a/Readme.md +++ b/Readme.md @@ -74,6 +74,9 @@ node app.js | pino-pretty - `--levelLabel` (`-b`): Output the log level using the specified label. Default: `levelLabel`. - `--minimumLevel` (`-L`): Hide messages below the specified log level. Accepts a number, `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. If any more filtering is required, consider using [`jq`](https://stedolan.github.io/jq/). +- `--customLevels` (`-x`): Override default levels with custom levels, e.g. `-x err:99,info:1` +- `--customColors` (`-X`): Override default colors with custom colors, e.g. `-X err:red,info:blue` +- `--useOnlyCustomProps` (`-U`): Only use custom levels and colors (if provided) (default: true); else fallback to default levels and colors, e.g. `-U false` - `--messageFormat` (`-o`): Format output of message, e.g. `{levelLabel} - {pid} - url:{request.url}` will output message: `INFO - 1123 - url:localhost:3000/test` Default: `false` - `--timestampKey` (`-a`): Define the key that contains the log timestamp. diff --git a/bin.js b/bin.js index 7927febb..96b496dd 100644 --- a/bin.js +++ b/bin.js @@ -35,6 +35,7 @@ args .option(['L', 'minimumLevel'], 'Hide messages below the specified log level') .option(['x', 'customLevels'], 'Override default levels (`-x err:99,info:1`)') .option(['X', 'customColors'], 'Override default colors using names from https://www.npmjs.com/package/colorette (`-X err:red,info:blue`)') + .option(['U', 'useOnlyCustomProps'], 'Only use custom levels and colors (if provided); don\'t fallback to default levels and colors (-U false)') .option(['k', 'errorLikeObjectKeys'], 'Define which keys contain error objects (`-k err,error`) (defaults to `err,error`)') .option(['m', 'messageKey'], 'Highlight the message under the specified key', CONSTANTS.MESSAGE_KEY) .option('levelKey', 'Detect the log level under the specified key', CONSTANTS.LEVEL_KEY) diff --git a/index.d.ts b/index.d.ts index b270e84b..bc012457 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,7 +13,7 @@ import { DestinationStream } from 'pino'; type LogDescriptor = Record; -declare function PinoPretty(options: PrettyOptions_): PinoPretty.PrettyStream; +declare function PinoPretty(options?: PrettyOptions_): PinoPretty.PrettyStream; interface PrettyOptions_ { /** diff --git a/index.js b/index.js index 87234f9f..3ac3c8bd 100644 --- a/index.js +++ b/index.js @@ -4,9 +4,7 @@ const { isColorSupported } = require('colorette') const pump = require('pump') const { Transform } = require('readable-stream') const abstractTransport = require('pino-abstract-transport') -const sonic = require('sonic-boom') const sjs = require('secure-json-parse') - const colors = require('./lib/colors') const { ERROR_LIKE_KEYS, MESSAGE_KEY, TIMESTAMP_KEY, LEVEL_KEY, LEVEL_NAMES } = require('./lib/constants') const { @@ -17,6 +15,7 @@ const { prettifyMetadata, prettifyObject, prettifyTime, + buildSafeSonicBoom, filterLog } = require('./lib/utils') @@ -35,6 +34,7 @@ const defaultOptions = { errorProps: '', customLevels: null, customColors: null, + useOnlyCustomProps: true, levelFirst: false, messageKey: MESSAGE_KEY, messageFormat: false, @@ -59,6 +59,7 @@ function prettyFactory (options) { const timestampKey = opts.timestampKey const errorLikeObjectKeys = opts.errorLikeObjectKeys const errorProps = opts.errorProps.split(',') + const useOnlyCustomProps = typeof opts.useOnlyCustomProps === 'boolean' ? opts.useOnlyCustomProps : opts.useOnlyCustomProps === 'true' const customLevels = opts.customLevels ? opts.customLevels .split(',') @@ -69,25 +70,26 @@ function prettyFactory (options) { return agg }, { default: 'USERLVL' }) - : undefined + : {} const customLevelNames = opts.customLevels ? opts.customLevels .split(',') .reduce((agg, value, idx) => { const [levelName, levelIdx = idx] = value.split(':') - agg[levelName] = levelIdx + agg[levelName.toLowerCase()] = levelIdx return agg }, {}) - : undefined + : {} const customColors = opts.customColors ? opts.customColors .split(',') .reduce((agg, value) => { const [level, color] = value.split(':') - const levelNum = customLevelNames !== undefined ? customLevelNames[level] : LEVEL_NAMES[level] + const condition = useOnlyCustomProps ? opts.customLevels : customLevelNames[level] !== undefined + const levelNum = condition ? customLevelNames[level] : LEVEL_NAMES[level] const colorIdx = levelNum !== undefined ? levelNum : level agg.push([colorIdx, color]) @@ -95,11 +97,19 @@ function prettyFactory (options) { return agg }, []) : undefined + const customProps = { + customLevels, + customLevelNames + } + if (useOnlyCustomProps && !opts.customLevels) { + customProps.customLevels = undefined + customProps.customLevelNames = undefined + } const customPrettifiers = opts.customPrettifiers const ignoreKeys = opts.ignore ? new Set(opts.ignore.split(',')) : undefined const hideObject = opts.hideObject const singleLine = opts.singleLine - const colorizer = colors(opts.colorize, customColors) + const colorizer = colors(opts.colorize, customColors, useOnlyCustomProps) return pretty @@ -117,18 +127,19 @@ function prettyFactory (options) { } if (minimumLevel) { - const minimum = (customLevelNames === undefined ? LEVEL_NAMES[minimumLevel] : customLevelNames[minimumLevel]) || Number(minimumLevel) + const condition = useOnlyCustomProps ? opts.customLevels : customLevelNames[minimumLevel] !== undefined + const minimum = (condition ? customLevelNames[minimumLevel] : LEVEL_NAMES[minimumLevel]) || Number(minimumLevel) const level = log[levelKey === undefined ? LEVEL_KEY : levelKey] if (level < minimum) return } - const prettifiedMessage = prettifyMessage({ log, messageKey, colorizer, messageFormat, levelLabel }) + const prettifiedMessage = prettifyMessage({ log, messageKey, colorizer, messageFormat, levelLabel, ...customProps, useOnlyCustomProps }) if (ignoreKeys) { log = filterLog(log, ignoreKeys) } - const prettifiedLevel = prettifyLevel({ log, colorizer, levelKey, prettifier: customPrettifiers.level, customLevels, customLevelNames }) + const prettifiedLevel = prettifyLevel({ log, colorizer, levelKey, prettifier: customPrettifiers.level, ...customProps }) const prettifiedMetadata = prettifyMetadata({ log, prettifiers: customPrettifiers }) const prettifiedTime = prettifyTime({ log, translateFormat: opts.translateTime, timestampKey, prettifier: customPrettifiers.time }) @@ -227,7 +238,7 @@ function build (opts = {}) { if (typeof opts.destination === 'object' && typeof opts.destination.write === 'function') { destination = opts.destination } else { - destination = sonic({ + destination = buildSafeSonicBoom({ dest: opts.destination || 1, append: opts.append, mkdir: opts.mkdir, diff --git a/lib/colors.js b/lib/colors.js index 6d74a44a..f6a20b15 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -42,39 +42,51 @@ function resolveCustomColoredColorizer (customColors) { ) } -function colorizeLevel (level, colorizer, { customLevels, customLevelNames } = {}) { - const levels = customLevels || LEVELS - const levelNames = customLevelNames || LEVEL_NAMES - - 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 - } +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] + const levelStr = levels[levelNum] - return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr) + return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr) + } } -function plainColorizer (level, opts) { - return colorizeLevel(level, plain, opts) +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 } -plainColorizer.message = plain.message -plainColorizer.greyMessage = plain.greyMessage -function coloredColorizer (level, opts) { - return colorizeLevel(level, colored, opts) +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 } -coloredColorizer.message = colored.message -coloredColorizer.greyMessage = colored.greyMessage -function customColoredColorizerFactory (customColors) { - const customColored = resolveCustomColoredColorizer(customColors) +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 colorizeLevel(level, customColored, opts) + return colorizeLevelCustom(level, customColored, opts) } customColoredColorizer.message = customColoredColorizer.message || customColored.message customColoredColorizer.greyMessage = customColoredColorizer.greyMessage || customColored.greyMessage @@ -89,6 +101,7 @@ function customColoredColorizerFactory (customColors) { * @param {boolean} [useColors=false] When `true` a function that applies standard * terminal colors is returned. * @param {array[]} [customColors] Touple 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 {function} `function (level) {}` has a `.message(str)` method to * apply colorization to a string. The core function accepts either an integer @@ -97,12 +110,12 @@ function customColoredColorizerFactory (customColors) { * 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) { +module.exports = function getColorizer (useColors = false, customColors, useOnlyCustomProps) { if (useColors && customColors !== undefined) { - return customColoredColorizerFactory(customColors) + return customColoredColorizerFactory(customColors, useOnlyCustomProps) } else if (useColors) { - return coloredColorizer + return coloredColorizer(useOnlyCustomProps) } - return plainColorizer + return plainColorizer(useOnlyCustomProps) } diff --git a/lib/utils.js b/lib/utils.js index 706f5e8f..adae9f23 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,7 +2,9 @@ const clone = require('rfdc')({ circles: true }) const dateformat = require('dateformat') +const SonicBoom = require('sonic-boom') const stringifySafe = require('fast-safe-stringify') +const { isMainThread } = require('worker_threads') const defaultColorizer = require('./colors')() const { DATE_FORMAT, @@ -23,6 +25,7 @@ module.exports = { prettifyMetadata, prettifyObject, prettifyTime, + buildSafeSonicBoom, filterLog } @@ -252,12 +255,13 @@ function prettifyLevel ({ log, colorizer = defaultColorizer, levelKey = LEVEL_KE * key is not a string, then `undefined` will be returned. Otherwise, a string * that is the prettified message. */ -function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL, levelKey = LEVEL_KEY, customLevels }) { +function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL, levelKey = LEVEL_KEY, customLevels, useOnlyCustomProps }) { if (messageFormat && typeof messageFormat === 'string') { const message = String(messageFormat).replace(/{([^{}]+)}/g, function (match, p1) { // return log level as string instead of int if (p1 === levelLabel && log[levelKey]) { - return customLevels === undefined ? LEVELS[log[levelKey]] : customLevels[log[levelKey]] + const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[log[levelKey]] === undefined + return condition ? LEVELS[log[levelKey]] : customLevels[log[levelKey]] } // Parse nested key access, e.g. `{keyA.subKeyB}`. return p1.split('.').reduce(function (prev, curr) { @@ -580,3 +584,67 @@ function filterLog (log, ignoreKeys) { }) return logCopy } + +function 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 + if (!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() + } +} diff --git a/package.json b/package.json index 7416f0b5..5d419a2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pino-pretty", - "version": "7.5.3", + "version": "7.6.1", "description": "Prettifier for Pino log lines", "type": "commonjs", "main": "index.js", @@ -37,6 +37,7 @@ "dateformat": "^4.6.3", "fast-safe-stringify": "^2.0.7", "joycon": "^3.1.1", + "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "^0.5.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", @@ -52,8 +53,8 @@ "rimraf": "^3.0.2", "snazzy": "^9.0.0", "standard": "^16.0.3", - "tap": "^15.0.9", - "tsd": "^0.19.0", + "tap": "^16.0.0", + "tsd": "^0.20.0", "typescript": "^4.4.3" }, "tsd": { diff --git a/test/basic.test.js b/test/basic.test.js index a2af4684..ba70fb4a 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -308,11 +308,12 @@ test('basic prettifier tests', (t) => { write (chunk, enc, cb) { const formatted = pretty(chunk.toString()) const localHour = dateformat(epoch, 'HH') + const localMinute = dateformat(epoch, 'MM') const localDate = dateformat(epoch, 'yyyy-mm-dd') const offset = dateformat(epoch, 'o') t.equal( formatted, - `[${localDate} ${localHour}:35:28.992 ${offset}] INFO (${pid} on ${hostname}): foo\n` + `[${localDate} ${localHour}:${localMinute}:28.992 ${offset}] INFO (${pid} on ${hostname}): foo\n` ) cb() } @@ -329,11 +330,12 @@ test('basic prettifier tests', (t) => { write (chunk, enc, cb) { const formatted = pretty(chunk.toString()) const localHour = dateformat(epoch, 'HH') + const localMinute = dateformat(epoch, 'MM') const localDate = dateformat(epoch, 'yyyy/mm/dd') const offset = dateformat(epoch, 'o') t.equal( formatted, - `[${localDate} ${localHour}:35:28 ${offset}] INFO (${pid} on ${hostname}): foo\n` + `[${localDate} ${localHour}:${localMinute}:28 ${offset}] INFO (${pid} on ${hostname}): foo\n` ) cb() } diff --git a/test/cli.test.js b/test/cli.test.js index 77676a96..93852970 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -96,6 +96,30 @@ test('cli', (t) => { child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n') t.teardown(() => child.kill()) }) + + t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps false`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'false', optionName, 'custom:99,info:1'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] CUSTOM (42 on foo): hello world\n`) + }) + child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n') + child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n') + t.teardown(() => child.kill()) + }) + + t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps true`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'true', optionName, 'custom:99,info:1'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] CUSTOM (42 on foo): hello world\n`) + }) + child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n') + child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n') + t.teardown(() => child.kill()) + }) }) ;['--customColors', '-X'].forEach((optionName) => { @@ -123,6 +147,74 @@ test('cli', (t) => { }) }) + ;['--useOnlyCustomProps', '-U'].forEach((optionName) => { + t.test(`customize levels via ${optionName} false and customColors`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'false'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`) + }) + child.stdin.write(logLine) + t.teardown(() => child.kill()) + }) + + t.test(`customize levels via ${optionName} true and customColors`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'true'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`) + }) + child.stdin.write(logLine) + t.teardown(() => child.kill()) + }) + + t.test(`customize levels via ${optionName} true and customLevels`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:30', optionName, 'true'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] CUSTOM (42 on foo): hello world\n`) + }) + child.stdin.write(logLine) + t.teardown(() => child.kill()) + }) + + t.test(`customize levels via ${optionName} true and no customLevels`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, optionName, 'true'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`) + }) + child.stdin.write(logLine) + t.teardown(() => child.kill()) + }) + + t.test(`customize levels via ${optionName} false and customLevels`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:25', optionName, 'false'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`) + }) + child.stdin.write(logLine) + t.teardown(() => child.kill()) + }) + + t.test(`customize levels via ${optionName} false and no customLevels`, (t) => { + t.plan(1) + const child = spawn(process.argv[0], [bin, optionName, 'false'], { env }) + child.on('error', t.threw) + child.stdout.on('data', (data) => { + t.equal(data.toString(), `[${epoch}] INFO (42 on foo): hello world\n`) + }) + child.stdin.write(logLine) + t.teardown(() => child.kill()) + }) + }) + t.test('does ignore escaped keys', (t) => { t.plan(1) const child = spawn(process.argv[0], [bin, '-i', 'log\\.domain\\.corp/foo'], { env }) diff --git a/test/lib/colors.test.js b/test/lib/colors.test.js index b4d83b6f..2a4d3b12 100644 --- a/test/lib/colors.test.js +++ b/test/lib/colors.test.js @@ -96,6 +96,7 @@ const testCustomColoringColorizer = getColorizer => async t => { } const colorizer = getColorizer(true, customColors) + const colorizerWithCustomPropUse = getColorizer(true, customColors, true) let colorized = colorizer(1, opts) t.equal(colorized, '\u001B[31mERR\u001B[39m') @@ -113,6 +114,12 @@ const testCustomColoringColorizer = getColorizer => async t => { 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(getColorizerPrivate)) diff --git a/test/lib/utils.public.test.js b/test/lib/utils.public.test.js index 0acbc2ef..911ea1cf 100644 --- a/test/lib/utils.public.test.js +++ b/test/lib/utils.public.test.js @@ -3,6 +3,9 @@ const tap = require('tap') const getColorizer = require('../../lib/colors') const utils = require('../../lib/utils') +const rimraf = require('rimraf') +const { join } = require('path') +const fs = require('fs') tap.test('prettifyErrorLog', t => { const { prettifyErrorLog } = utils @@ -101,16 +104,31 @@ tap.test('prettifyMessage', t => { t.equal(str, 'appModule - ') }) - t.test('returns message formatted by `messageFormat` option - levelLabel', async t => { - const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}' }) + t.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: {} }) t.equal(str, '[30] INFO appModule - foo') }) + t.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 30 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 30: 'CHECK' }, useOnlyCustomProps: true }) + t.equal(str, '[30] CHECK appModule - foo') + }) + t.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => { const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' } }) t.equal(str, '[123] CUSTOM appModule - foo') }) + t.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 123 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: true }) + t.equal(str, '[123] CUSTOM appModule - foo') + }) + + t.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => { + const str = prettifyMessage({ log: { msg: 'foo', context: 'appModule', level: 40 }, messageFormat: '[{level}] {levelLabel} {context} - {msg}', customLevels: { 123: 'CUSTOM' }, useOnlyCustomProps: false }) + t.equal(str, '[40] WARN appModule - foo') + }) + t.test('`messageFormat` supports nested curly brackets', async t => { const str = prettifyMessage({ log: { level: 30 }, messageFormat: '{{level}}-{level}-{{level}-{level}}' }) t.equal(str, '{30}-30-{30-30}') @@ -428,3 +446,48 @@ tap.test('#filterLog with circular references', t => { t.end() }) + +tap.test('buildSafeSonicBoom', t => { + const { buildSafeSonicBoom } = utils + + function noop () {} + + const file = () => { + const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`) + const fd = fs.openSync(dest, 'w') + return { dest, fd } + } + + t.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) + }) + + t.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') + }) + + t.end() +}) diff --git a/test/types/pino-pretty.test-d.ts b/test/types/pino-pretty.test-d.ts index 3928b389..20a84178 100644 --- a/test/types/pino-pretty.test-d.ts +++ b/test/types/pino-pretty.test-d.ts @@ -60,6 +60,7 @@ const options2: PrettyOptions = { mkdir: true, }; +expectType(pretty()); // #326 expectType(pretty(options)); expectType(PinoPrettyNamed(options)); expectType(PinoPrettyDefault(options));