UNPKG

pino-princess

Version:

Prettiest Pino Prettifier in all the land

217 lines (216 loc) 6.69 kB
/* eslint-disable @typescript-eslint/naming-convention */ import { logLineFactory } from 'json-log-line'; import _highlight from 'cli-highlight'; import chalk from 'chalk'; import prettyMs from 'pretty-ms'; import pcStringify from 'json-stringify-pretty-compact'; import { format } from 'date-fns'; import isObject from './utils/is-object.js'; const highlight = _highlight.default; const nl = '\n'; const defaultTimeFormat = 'h:mm:ss.SSS aaa'; const stringify = (obj, indent, theme) => { const stringified = highlight(pcStringify(obj, { indent }), { language: 'json', ignoreIllegals: true, theme: { attr: chalk.cyanBright, string: chalk.yellow, ...theme, }, }); if (indent === Infinity || indent === '') { return stringified; } return /^{.*"/.test(stringified) ? ' ' + stringified.replace(/^{/, '').replace(/}$/, '') : stringified.replace(/^{\n/, '').replace(/\n}$/, ''); }; const emojiMap = { warn: '⚠️', info: '✨', userlvl: '👤', error: '🚨', debug: '🐛', fatal: '💀', trace: '🔍', }; const colorMap = { warn: 'yellow', info: 'cyan', userlvl: 'cyan', error: 'red', debug: 'blue', trace: 'white', fatal: 'red', }; const numLevelsMapping = { 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal', }; function isWideEmoji(character) { return character !== '⚠️'; } export function formatLevel(_level) { const level = numLevelsMapping[_level] || _level; if (!emojiMap?.[level]) return ''; const endlen = 5; const emoji = emojiMap[level]; const padding = isWideEmoji(emoji) ? ' ' : ' '; const formattedLevel = chalk[colorMap[level]](level.toUpperCase()); const endPadding = endlen - level.length; return emoji + padding + formattedLevel + ''.padEnd(endPadding, ' '); } export function formatLoadTime(elapsedTime) { const elapsed = typeof elapsedTime === 'string' ? Number.parseInt(elapsedTime, 10) : elapsedTime; const time = prettyMs(elapsed); return elapsed > 750 ? chalk.red(time) : elapsed > 450 ? chalk.yellow(time) : chalk.green(time); } export function formatTime(instant, timeFormat = defaultTimeFormat) { return chalk.gray(`[${format(new Date(instant), timeFormat)}]`); } export function formatName(name) { if (!name) return ''; return chalk.blue(`[${name}]`); } export function formatMessage(message, { level }) { if (message === undefined) return ''; let pretty = ''; if (level === 50 || level === 'error') pretty = chalk.red(message); if (level === 10 || level === 'trace') pretty = chalk.cyan(message); if (level === 40 || level === 'warn') pretty = chalk.yellow(message); if (level === 20 || level === 'debug') pretty = chalk.white(message); if (level === 30 || level === 'info') pretty = chalk.white(message); if (level === 60 || level === 'fatal') pretty = chalk.white.bgRedBright(message); return pretty || message; } export function formatBundleSize(bundle) { const bytes = Number.parseInt(bundle, 10); const size = `${bytes}B`; return chalk.gray(size); } export function formatUrl(url, { res: { statusCode } = {} } = {}) { return statusCode ? chalk.magenta(url) : ` ${chalk.magenta(url)}`; } export function formatMethod(method) { return method ? chalk.white(method.padEnd(4)) : ''; } export function formatStatusCode(statusCode = 'xxx') { return chalk[typeof statusCode === 'number' && statusCode < 300 ? 'green' : typeof statusCode === 'number' && statusCode < 500 ? 'yellow' : 'red'](statusCode); } export function formatStack(stack) { return stack ? chalk.grey(nl + ' ' + stack) : ''; } export function formatErrorProp(errorPropValue) { if (Array.isArray(errorPropValue.aggregateErrors)) { const { aggregateErrors, ...ogErr } = errorPropValue; return ([isObject(ogErr) ? formatErrorProp(ogErr) : undefined] // eslint-disable-next-line unicorn/prefer-spread .concat(aggregateErrors.map((err) => ' ' + formatErrorProp(err))) .filter(Boolean) .join(nl)); } let stack = ''; if (errorPropValue.type) delete errorPropValue.type; if (errorPropValue.stack) { stack += formatStack(errorPropValue.stack); delete errorPropValue.stack; } if (errorPropValue.message) delete errorPropValue.message; const hasExtraData = Object.keys(errorPropValue).length > 0; if (!stack && !hasExtraData) return ''; return (stack + (stack ? nl : '') + (hasExtraData ? chalk.grey(stringify(errorPropValue, 4)) : '')); } export function formatExtraFields(extraFields, options) { if (options?.singleLine) { return (' ' + chalk.grey(stringify(extraFields, '', options?.theme?.(chalk)))); } return (nl + chalk.grey(stringify(extraFields, undefined, options?.theme?.(chalk)))); } export function formatId(id) { return id ? chalk.yellow(`[ID:${id}]`) : ''; } export function prettify({ /** * The key to use for the error object. Defaults to `err`. */ errorKey = 'err', /** * The key to use for the message object. Defaults to `msg`. */ messageKey = 'msg', /** * Format string for time display using date-fns format */ timeFormat = defaultTimeFormat, /** * Whether to format the output as a single line */ singleLine = false, /** * include and exclude both take keys with dot notation */ exclude = [], /** * include always overrides exclude */ include = [], /** * Theme for the extra fields object */ theme = (chalk) => ({}), /** * Format functions for any given key */ format = {}, } = {}) { const formatters = { name: formatName, time: (time) => formatTime(time, timeFormat), level: formatLevel, 'req.id': formatId, 'req.method': formatMethod, 'res.statusCode': formatStatusCode, 'req.url': formatUrl, [messageKey]: formatMessage, responseTime: formatLoadTime, extraFields: (fields) => formatExtraFields(fields, { theme, singleLine }), [errorKey]: formatErrorProp, [`${errorKey}.stack`]: formatStack, ...format, }; const opts = { include: [...include, ...Object.keys(formatters)], exclude: ['req', 'res', 'hostname', 'pid', ...exclude], format: formatters, }; return logLineFactory(opts); } export default prettify;