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