Add compact colored prettyJson helper for request logs.
This commit is contained in:
@@ -1,33 +1,16 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { isEmpty, prettyJson } from '../../util/prettyJson.js';
|
||||||
|
|
||||||
const BINARY_PREVIEW_BYTES = 16;
|
function logField(label, value, { skipEmpty = false } = {}) {
|
||||||
|
if (skipEmpty && isEmpty(value)) return;
|
||||||
function summarizeBuffer(buf) {
|
const rendered = prettyJson(value);
|
||||||
const head = buf.subarray(0, BINARY_PREVIEW_BYTES);
|
const prefix = chalk.gray(` ${label}: `);
|
||||||
const hex = [...head].map((b) => b.toString(16).padStart(2, '0')).join(' ');
|
if (!rendered.includes('\n')) {
|
||||||
return {
|
console.log(prefix + rendered);
|
||||||
type: 'Buffer',
|
return;
|
||||||
length: buf.length,
|
|
||||||
headHex: hex,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeJson(value) {
|
|
||||||
try {
|
|
||||||
if (Buffer.isBuffer(value)) {
|
|
||||||
return JSON.stringify(summarizeBuffer(value), null, 2);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
Array.isArray(value) &&
|
|
||||||
value.length > 64 &&
|
|
||||||
value.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)
|
|
||||||
) {
|
|
||||||
return JSON.stringify(summarizeBuffer(Buffer.from(value)), null, 2);
|
|
||||||
}
|
|
||||||
return JSON.stringify(value, null, 2);
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
}
|
||||||
|
console.log(prefix);
|
||||||
|
console.log(rendered.replace(/^/gm, ' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureResponseBody(res) {
|
function captureResponseBody(res) {
|
||||||
@@ -66,11 +49,13 @@ export const loggerMiddleware = (req, res, next) => {
|
|||||||
else status = chalk.green(status);
|
else status = chalk.green(status);
|
||||||
|
|
||||||
console.log(`${method} ${url} ${status} ${chalk.gray(`(${duration}ms)`)}`);
|
console.log(`${method} ${url} ${status} ${chalk.gray(`(${duration}ms)`)}`);
|
||||||
console.log(chalk.gray(' params:'), safeJson(req.params ?? {}));
|
logField('params', req.params ?? {}, { skipEmpty: true });
|
||||||
console.log(chalk.gray(' query:'), safeJson(req.query ?? {}));
|
logField('query', req.query ?? {}, { skipEmpty: true });
|
||||||
console.log(chalk.gray(' content-type:'), req.headers['content-type'] ?? '(none)');
|
if (req.headers['content-type']) {
|
||||||
console.log(chalk.gray(' body:'), safeJson(req.body ?? {}));
|
console.log(chalk.gray(' content-type:'), chalk.cyan(req.headers['content-type']));
|
||||||
console.log(chalk.gray(' response:'), safeJson(getBody()));
|
}
|
||||||
|
logField('body', req.body ?? {}, { skipEmpty: true });
|
||||||
|
logField('response', getBody());
|
||||||
});
|
});
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|||||||
198
src/util/prettyJson.js
Normal file
198
src/util/prettyJson.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
const BINARY_PREVIEW_BYTES = 16;
|
||||||
|
const DEFAULTS = {
|
||||||
|
maxLine: 100,
|
||||||
|
indent: ' ',
|
||||||
|
maxString: 96,
|
||||||
|
maxArray: 24,
|
||||||
|
colors: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function summarizeBuffer(buf) {
|
||||||
|
const head = buf.subarray(0, BINARY_PREVIEW_BYTES);
|
||||||
|
const hex = [...head].map((b) => b.toString(16).padStart(2, '0')).join(' ');
|
||||||
|
return { type: 'Buffer', length: buf.length, headHex: hex };
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareForLog(value) {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
if (Buffer.isBuffer(value)) return summarizeBuffer(value);
|
||||||
|
if (
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.length > 64 &&
|
||||||
|
value.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)
|
||||||
|
) {
|
||||||
|
return summarizeBuffer(Buffer.from(value));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmpty(value) {
|
||||||
|
if (value == null) return true;
|
||||||
|
if (Buffer.isBuffer(value)) return value.length === 0;
|
||||||
|
if (Array.isArray(value)) return value.length === 0;
|
||||||
|
if (typeof value === 'object') return Object.keys(value).length === 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripAnsi(s) {
|
||||||
|
return s.replace(/\u001b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScalar(value) {
|
||||||
|
return value === null || typeof value !== 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object/array only contains scalars (or empty containers) — always keep on one line. */
|
||||||
|
function isFlat(value) {
|
||||||
|
if (isScalar(value)) return true;
|
||||||
|
if (Array.isArray(value)) return value.every(isScalar);
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
return Object.values(value).every(
|
||||||
|
(v) =>
|
||||||
|
isScalar(v) ||
|
||||||
|
(Array.isArray(v) && v.length === 0) ||
|
||||||
|
(isPlainObject(v) && Object.keys(v).length === 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint(enabled, style, text) {
|
||||||
|
return enabled ? style(text) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFormatter(options) {
|
||||||
|
const maxLine = options.maxLine ?? DEFAULTS.maxLine;
|
||||||
|
const indent = options.indent ?? DEFAULTS.indent;
|
||||||
|
const maxString = options.maxString ?? DEFAULTS.maxString;
|
||||||
|
const maxArray = options.maxArray ?? DEFAULTS.maxArray;
|
||||||
|
const colors = options.colors ?? DEFAULTS.colors;
|
||||||
|
|
||||||
|
const key = (name) => paint(colors, chalk.white, name);
|
||||||
|
const punct = (s) => paint(colors, chalk.white, s);
|
||||||
|
const sep = (s) => paint(colors, chalk.gray, s);
|
||||||
|
|
||||||
|
function formatPrimitive(value) {
|
||||||
|
if (value === null) return paint(colors, chalk.bold.magenta, 'null');
|
||||||
|
if (value === undefined) return paint(colors, chalk.dim, 'undefined');
|
||||||
|
if (typeof value === 'boolean') return paint(colors, chalk.yellow, String(value));
|
||||||
|
if (typeof value === 'number' || typeof value === 'bigint') {
|
||||||
|
return paint(colors, chalk.yellow, String(value));
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const shown = value.length > maxString ? `${value.slice(0, maxString)}…` : value;
|
||||||
|
return paint(colors, chalk.green, JSON.stringify(shown));
|
||||||
|
}
|
||||||
|
return paint(colors, chalk.cyan, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFlatObject(value) {
|
||||||
|
const fields = Object.entries(value).map(
|
||||||
|
([k, v]) => `${key(k)}${sep(':')} ${formatValue(v, 0)}`,
|
||||||
|
);
|
||||||
|
if (fields.length === 0) return punct('{}');
|
||||||
|
return `${punct('{')} ${fields.join(sep(', '))} ${punct('}')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFlatArray(value) {
|
||||||
|
if (value.length === 0) return punct('[]');
|
||||||
|
const items = value.slice(0, maxArray).map((item) => formatValue(item, 0));
|
||||||
|
if (value.length > maxArray) {
|
||||||
|
items.push(paint(colors, chalk.dim, `…${value.length - maxArray} more`));
|
||||||
|
}
|
||||||
|
return `${punct('[')} ${items.join(sep(', '))} ${punct(']')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(value, depth) {
|
||||||
|
if (isScalar(value)) return formatPrimitive(value);
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) return punct('[]');
|
||||||
|
if (isFlat(value) || value.every(isFlat)) {
|
||||||
|
const inline = formatFlatArray(value);
|
||||||
|
if (stripAnsi(inline).length <= maxLine || value.every(isScalar)) return inline;
|
||||||
|
const pad = indent.repeat(depth + 1);
|
||||||
|
const close = indent.repeat(depth);
|
||||||
|
const items = value.slice(0, maxArray).map((item) => formatValue(item, depth + 1));
|
||||||
|
if (value.length > maxArray) {
|
||||||
|
items.push(paint(colors, chalk.dim, `…${value.length - maxArray} more`));
|
||||||
|
}
|
||||||
|
return [punct('['), ...items.map((l) => `${pad}${l}`), `${close}${punct(']')}`].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = indent.repeat(depth + 1);
|
||||||
|
const close = indent.repeat(depth);
|
||||||
|
const items = value.slice(0, maxArray).map((item) => formatValue(item, depth + 1));
|
||||||
|
if (value.length > maxArray) {
|
||||||
|
items.push(paint(colors, chalk.dim, `…${value.length - maxArray} more`));
|
||||||
|
}
|
||||||
|
return [punct('['), ...items.map((l) => `${pad}${l}`), `${close}${punct(']')}`].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFlat(value)) return formatFlatObject(value);
|
||||||
|
|
||||||
|
const pad = indent.repeat(depth + 1);
|
||||||
|
const close = indent.repeat(depth);
|
||||||
|
const entries = Object.entries(value);
|
||||||
|
const lines = [];
|
||||||
|
let run = [];
|
||||||
|
|
||||||
|
const flushRun = () => {
|
||||||
|
if (!run.length) return;
|
||||||
|
let current = '';
|
||||||
|
for (const field of run) {
|
||||||
|
const next = current ? `${current}${sep(', ')}${field}` : field;
|
||||||
|
if (current && stripAnsi(next).length > maxLine) {
|
||||||
|
lines.push(`${pad}${current}`);
|
||||||
|
current = field;
|
||||||
|
} else {
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current) lines.push(`${pad}${current}`);
|
||||||
|
run = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [k, v] of entries) {
|
||||||
|
if (isFlat(v)) {
|
||||||
|
run.push(`${key(k)}${sep(':')} ${formatValue(v, depth + 1)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
flushRun();
|
||||||
|
const nested = formatValue(v, depth + 1);
|
||||||
|
if (!nested.includes('\n')) {
|
||||||
|
lines.push(`${pad}${key(k)}${sep(':')} ${nested}`);
|
||||||
|
} else {
|
||||||
|
const [first, ...rest] = nested.split('\n');
|
||||||
|
lines.push(`${pad}${key(k)}${sep(':')} ${first}`);
|
||||||
|
lines.push(...rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushRun();
|
||||||
|
|
||||||
|
return [punct('{'), ...lines, `${close}${punct('}')}`].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact, colored pretty-printer for logs (flat objects stay on one line).
|
||||||
|
* @param {unknown} value
|
||||||
|
* @param {{ maxLine?: number, indent?: string, maxString?: number, maxArray?: number, colors?: boolean }} [options]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function prettyJson(value, options = {}) {
|
||||||
|
try {
|
||||||
|
const formatValue = createFormatter(options);
|
||||||
|
return formatValue(prepareForLog(value), 0);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user