134 lines
5.0 KiB
JavaScript
134 lines
5.0 KiB
JavaScript
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();
|
|
};
|