Add request/response logging middleware to enhance auditing and debugging
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
This commit is contained in:
@@ -3,6 +3,7 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import requestResponseLogger from './middleware/logging.js';
|
||||
import { initDatabase } from './config/database.js';
|
||||
import { initMailer } from './config/mailer.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
@@ -31,6 +32,8 @@ const corsOptions = process.env.APP_URL
|
||||
: {};
|
||||
app.use(cors(corsOptions));
|
||||
app.use(express.json());
|
||||
// Request/Response logging (filters sensitive fields)
|
||||
app.use(requestResponseLogger);
|
||||
|
||||
// Initialize database & start server
|
||||
async function start() {
|
||||
|
||||
130
server/middleware/logging.js
Normal file
130
server/middleware/logging.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import util from 'util';
|
||||
|
||||
const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /auth/i, /api[_-]?key/i];
|
||||
const MAX_LOG_BODY_LENGTH = 200000; // 200 KB
|
||||
|
||||
function isSensitiveKey(key) {
|
||||
return SENSITIVE_KEYS.some(rx => rx.test(key));
|
||||
}
|
||||
|
||||
function filterValue(key, value, depth = 0) {
|
||||
if (depth > 5) return '[MAX_DEPTH]';
|
||||
if (key && isSensitiveKey(key)) return '[FILTERED]';
|
||||
if (value === null || value === undefined) return value;
|
||||
if (typeof value === 'string') {
|
||||
if (value.length > MAX_LOG_BODY_LENGTH) return '[TOO_LARGE]';
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
||||
if (Array.isArray(value)) return value.map(v => filterValue(null, v, depth + 1));
|
||||
if (typeof value === 'object') {
|
||||
const out = {};
|
||||
for (const k of Object.keys(value)) {
|
||||
out[k] = filterValue(k, value[k], depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function filterHeaders(headers) {
|
||||
const out = {};
|
||||
for (const k of Object.keys(headers || {})) {
|
||||
if (/^authorization$/i.test(k) || /^cookie$/i.test(k)) {
|
||||
out[k] = '[FILTERED]';
|
||||
continue;
|
||||
}
|
||||
if (isSensitiveKey(k)) {
|
||||
out[k] = '[FILTERED]';
|
||||
continue;
|
||||
}
|
||||
out[k] = headers[k];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function requestResponseLogger(req, res, next) {
|
||||
try {
|
||||
const start = Date.now();
|
||||
const { method, originalUrl } = req;
|
||||
const ip = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
|
||||
const reqHeaders = filterHeaders(req.headers);
|
||||
|
||||
let reqBody = '[not-logged]';
|
||||
const contentType = (req.headers['content-type'] || '').toLowerCase();
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
reqBody = '[multipart/form-data]';
|
||||
} else if (req.body) {
|
||||
try {
|
||||
reqBody = filterValue(null, req.body);
|
||||
} catch (e) {
|
||||
reqBody = '[unserializable]';
|
||||
}
|
||||
}
|
||||
|
||||
// Capture response body by wrapping res.send
|
||||
const oldSend = res.send.bind(res);
|
||||
let responseBody = undefined;
|
||||
res.send = function sendOverWrite(body) {
|
||||
responseBody = body;
|
||||
return oldSend(body);
|
||||
};
|
||||
|
||||
res.on('finish', () => {
|
||||
try {
|
||||
const duration = Date.now() - start;
|
||||
const resHeaders = filterHeaders(res.getHeaders ? res.getHeaders() : {});
|
||||
|
||||
let loggedResponseBody = null;
|
||||
const resContentType = (res.getHeader && (res.getHeader('content-type') || '')).toString().toLowerCase();
|
||||
if (responseBody === undefined) {
|
||||
loggedResponseBody = '[none-captured]';
|
||||
} else if (resContentType.includes('application/json')) {
|
||||
try {
|
||||
const parsed = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody;
|
||||
loggedResponseBody = filterValue(null, parsed);
|
||||
} catch (e) {
|
||||
loggedResponseBody = typeof responseBody === 'string' ? (responseBody.length > 1000 ? responseBody.slice(0, 1000) + '...[truncated]' : responseBody) : util.inspect(responseBody);
|
||||
}
|
||||
} else if (resContentType.includes('text/') || resContentType.includes('application/xml')) {
|
||||
const asStr = typeof responseBody === 'string' ? responseBody : util.inspect(responseBody);
|
||||
loggedResponseBody = asStr.length > 1000 ? asStr.slice(0, 1000) + '...[truncated]' : asStr;
|
||||
} else {
|
||||
loggedResponseBody = '[not-logged-due-to-content-type]';
|
||||
}
|
||||
|
||||
const log = {
|
||||
time: new Date().toISOString(),
|
||||
ip,
|
||||
method,
|
||||
url: originalUrl,
|
||||
status: res.statusCode,
|
||||
duration_ms: duration,
|
||||
request: {
|
||||
headers: reqHeaders,
|
||||
query: filterValue(null, req.query || {}),
|
||||
body: reqBody,
|
||||
},
|
||||
response: {
|
||||
headers: resHeaders,
|
||||
body: loggedResponseBody,
|
||||
},
|
||||
};
|
||||
|
||||
// Output as single-line JSON for log aggregation
|
||||
try {
|
||||
console.info(JSON.stringify(log));
|
||||
} catch (e) {
|
||||
console.info('LOG_ERROR', util.inspect(log));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('RequestLogger error:', e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('RequestLogger setup failure:', e);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
Reference in New Issue
Block a user