Files
dashback/src/api/middleware/logger.js
2026-10-08 03:43:28 +02:00

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();
};