Files

138 lines
3.8 KiB
JavaScript

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