const { AsyncLocalStorage } = require('async_hooks'); const { randomUUID } = require('crypto'); const pino = require('pino'); const requestStorage = new AsyncLocalStorage(); const SENSITIVE_KEY_PATTERN = /(authorization|cookie|password|passwd|secret|token|api[_-]?key|access[_-]?key)/i; const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/; const logger = pino({ level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'), base: { service: 'wanwanyun-backend', pid: process.pid }, timestamp: pino.stdTimeFunctions.isoTime, redact: { paths: [ '*.authorization', '*.cookie', '*.password', '*.token', '*.refreshToken', '*.access_key_secret', '*.oss_access_key_secret', '*.smtp_password', 'req.headers.authorization', 'req.headers.cookie' ], censor: '[REDACTED]' } }); function redactString(value) { return value .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]') .replace(/\b(password|passwd|secret|token|api[_-]?key|access[_-]?key)\s*[:=]\s*[^\s,;&]+/gi, '$1=[REDACTED]'); } function redactLogValue(value, depth = 0, seen = new WeakSet()) { if (depth > 5) return '[MAX_DEPTH]'; if (typeof value === 'string') return redactString(value); if (value === null || value === undefined || typeof value !== 'object') return value; if (value instanceof Error) { return { type: value.name, message: redactString(value.message || ''), stack: redactString(value.stack || ''), code: value.code }; } if (seen.has(value)) return '[CIRCULAR]'; seen.add(value); if (Array.isArray(value)) { return value.map((item) => redactLogValue(item, depth + 1, seen)); } const result = {}; for (const [key, child] of Object.entries(value)) { result[key] = SENSITIVE_KEY_PATTERN.test(key) ? '[REDACTED]' : redactLogValue(child, depth + 1, seen); } return result; } function getRequestContext() { return requestStorage.getStore() || {}; } function getRequestId(req) { return req?.id || getRequestContext().requestId || null; } function normalizeRequestId(value) { const candidate = Array.isArray(value) ? value[0] : value; return typeof candidate === 'string' && REQUEST_ID_PATTERN.test(candidate) ? candidate : randomUUID(); } function requestContextMiddleware(req, res, next) { const requestId = normalizeRequestId(req.headers['x-request-id']); const startedAt = process.hrtime.bigint(); req.id = requestId; req.log = logger.child({ requestId }); res.setHeader('X-Request-ID', requestId); requestStorage.run({ requestId }, () => { res.once('finish', () => { const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6; const level = res.statusCode >= 500 ? 'error' : (res.statusCode >= 400 ? 'warn' : 'info'); req.log[level]({ method: req.method, path: req.path, statusCode: res.statusCode, durationMs: Number(durationMs.toFixed(2)) }, 'request completed'); }); next(); }); } let consoleBridgeInstalled = false; function installConsoleBridge() { if (consoleBridgeInstalled) return; if (process.env.NODE_ENV !== 'production' && process.env.LOG_FORMAT !== 'json') return; consoleBridgeInstalled = true; const levels = { log: 'info', info: 'info', warn: 'warn', error: 'error', debug: 'debug' }; for (const [consoleMethod, level] of Object.entries(levels)) { console[consoleMethod] = (...args) => { const values = redactLogValue(args); const firstMessage = typeof values[0] === 'string' ? values[0] : 'console message'; logger[level]({ requestId: getRequestId(), values }, firstMessage); }; } } module.exports = { getRequestId, installConsoleBridge, logger, redactLogValue, requestContextMiddleware };