refactor: modularize backend security and observability
This commit is contained in:
137
backend/utils/logger.js
Normal file
137
backend/utils/logger.js
Normal file
@@ -0,0 +1,137 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user