138 lines
3.8 KiB
JavaScript
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
|
|
};
|