pino-princess
Version:
Prettiest Pino Prettifier in all the land
217 lines (216 loc) • 6.69 kB
JavaScript
/* 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;