import { isEmpty, prettyJson } from '../../util/prettyJson.js'; import { writeErrorLog } from '../../util/errorLog.js'; import { createLogger, chalk } from '../../util/logger.js'; const httpLog = createLogger('http'); function logField(label, value, { skipEmpty = false } = {}) { if (skipEmpty && isEmpty(value)) return; const rendered = prettyJson(value); const prefix = chalk.gray(` ${label}: `); if (!rendered.includes('\n')) { httpLog.debug(prefix + rendered); return; } httpLog.debug(prefix + '\n' + rendered.replace(/^/gm, ' ')); } function captureResponseBody(res) { let body; const originalJson = res.json.bind(res); const originalSend = res.send.bind(res); res.json = (payload) => { let out = payload; if (res.statusCode >= 500 && payload && typeof payload === 'object' && 'error' in payload) { out = { ...payload, error: 'Internal server error' }; } body = out; return originalJson(out); }; res.send = (payload) => { if (body === undefined) { body = payload; } return originalSend(payload); }; return () => body; } export const loggerMiddleware = (req, res, next) => { const start = Date.now(); const getBody = captureResponseBody(res); res.on('finish', () => { const duration = Date.now() - start; const method = chalk.bold.blue(req.method.padEnd(7)); const url = req.originalUrl; let status = res.statusCode; if (status >= 500) status = chalk.red(status); else if (status >= 400) status = chalk.yellow(status); else if (status >= 300) status = chalk.cyan(status); else status = chalk.green(status); const summary = `${method} ${url} ${status} ${chalk.gray(`(${duration}ms)`)}`; if (res.statusCode >= 500) { httpLog.error(summary); } else if (res.statusCode >= 400) { httpLog.warn(summary); } else { httpLog.info(summary); } logField('params', req.params ?? {}, { skipEmpty: true }); logField('query', req.query ?? {}, { skipEmpty: true }); if (req.headers['content-type']) { httpLog.debug(chalk.gray(' content-type: ') + chalk.cyan(req.headers['content-type'])); } if (req.headers.authorization) { httpLog.debug(chalk.gray(' authorization: ') + chalk.cyan(req.headers.authorization)); } logField('body', req.body ?? {}, { skipEmpty: true }); logField('response', getBody()); // Dump full request/response/error details to a log file on 5xx. const statusCode = res.statusCode; if (statusCode >= 500) { const body = getBody(); writeErrorLog( `${req.method} ${req.originalUrl} ${statusCode}`, { request: { method: req.method, url: req.originalUrl, headers: req.headers, params: req.params, query: req.query, }, body: req.body, files: Array.isArray(req.files) ? req.files.map((f) => ({ fieldname: f.fieldname, originalname: f.originalname, mimetype: f.mimetype, size: f.size, encoding: f.encoding, bufferHeadHex: f.buffer ? [...f.buffer.subarray(0, 32)] .map((b) => b.toString(16).padStart(2, '0')) .join(' ') : null, })) : undefined, response: { status: statusCode, body, }, error: res.locals?.error ? { message: res.locals.error.message, code: res.locals.error.code, number: res.locals.error.number, state: res.locals.error.state, class: res.locals.error.class, lineNumber: res.locals.error.lineNumber, procName: res.locals.error.procName, stack: res.locals.error.stack, precedingErrors: res.locals.error.precedingErrors?.map((e) => ({ message: e.message, number: e.number, state: e.state, })), } : undefined, }, ); } }); next(); };