feat: add SLS toggle and sanitize logs
This commit is contained in:
@@ -7,6 +7,7 @@ FACTORY_API_KEY=your_factory_api_key_here
|
||||
DROID_REFRESH_KEY=your_refresh_token_here
|
||||
|
||||
# 阿里云日志服务配置
|
||||
SLS_ENABLED=false
|
||||
ALIYUN_ACCESS_KEY_ID=your_access_key_id
|
||||
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
|
||||
ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com
|
||||
|
||||
87
log-sanitizer.js
Normal file
87
log-sanitizer.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const REDACTION = '[REDACTED]';
|
||||
|
||||
const REDACT_KEY_RE = /(authorization|x-api-key|api[-_]?key|access_token|refresh_token|client_secret|private_key|set-cookie|cookie|password|secret)/i;
|
||||
const EMAIL_KEY_RE = /email/i;
|
||||
const IP_KEY_RE = /(^ip$|ip_address|remote_address|x-forwarded-for)/i;
|
||||
|
||||
function maskEmail(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
return value.replace(/([A-Za-z0-9._%+-])([A-Za-z0-9._%+-]*)(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g, '$1***$3');
|
||||
}
|
||||
|
||||
function maskIp(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
let masked = value.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3})\.\d{1,3}\b/g, '$1.xxx');
|
||||
masked = masked.replace(/\b([A-Fa-f0-9]{0,4}:){2,7}[A-Fa-f0-9]{0,4}\b/g, '****');
|
||||
return masked;
|
||||
}
|
||||
|
||||
function maskTokensInString(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
let masked = value.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+\b/g, 'Bearer ' + REDACTION);
|
||||
masked = masked.replace(/\b(api_key|apikey|access_token|refresh_token|client_secret|password)=([^\s&]+)/gi, '$1=' + REDACTION);
|
||||
return masked;
|
||||
}
|
||||
|
||||
function sanitizeString(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
let masked = value;
|
||||
masked = maskTokensInString(masked);
|
||||
masked = maskEmail(masked);
|
||||
masked = maskIp(masked);
|
||||
return masked;
|
||||
}
|
||||
|
||||
function sanitizeValue(value, key, seen) {
|
||||
if (value === null || value === undefined) return value;
|
||||
|
||||
if (key && REDACT_KEY_RE.test(key)) {
|
||||
return REDACTION;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (key && EMAIL_KEY_RE.test(key)) {
|
||||
return maskEmail(value);
|
||||
}
|
||||
if (key && IP_KEY_RE.test(key)) {
|
||||
return maskIp(value);
|
||||
}
|
||||
return sanitizeString(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => sanitizeValue(item, key, seen));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return sanitizeObject(value, seen);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function sanitizeObject(value, seen) {
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
if (!seen) seen = new WeakSet();
|
||||
if (seen.has(value)) return '[Circular]';
|
||||
seen.add(value);
|
||||
|
||||
const output = Array.isArray(value) ? [] : {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
output[key] = sanitizeValue(val, key, seen);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function sanitizeForLog(value) {
|
||||
return sanitizeValue(value, null, new WeakSet());
|
||||
}
|
||||
|
||||
export function sanitizeLogMessage(message) {
|
||||
if (typeof message !== 'string') return message;
|
||||
return sanitizeString(message);
|
||||
}
|
||||
27
logger.js
27
logger.js
@@ -1,28 +1,29 @@
|
||||
import { isDevMode } from './config.js';
|
||||
import { sanitizeForLog, sanitizeLogMessage } from './log-sanitizer.js';
|
||||
|
||||
export function logInfo(message, data = null) {
|
||||
console.log(`[INFO] ${message}`);
|
||||
console.log(`[INFO] ${sanitizeLogMessage(message)}`);
|
||||
if (data && isDevMode()) {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
console.log(JSON.stringify(sanitizeForLog(data), null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
export function logDebug(message, data = null) {
|
||||
if (isDevMode()) {
|
||||
console.log(`[DEBUG] ${message}`);
|
||||
console.log(`[DEBUG] ${sanitizeLogMessage(message)}`);
|
||||
if (data) {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
console.log(JSON.stringify(sanitizeForLog(data), null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logError(message, error = null) {
|
||||
console.error(`[ERROR] ${message}`);
|
||||
console.error(`[ERROR] ${sanitizeLogMessage(message)}`);
|
||||
if (error) {
|
||||
if (isDevMode()) {
|
||||
console.error(error);
|
||||
console.error(sanitizeForLog(error));
|
||||
} else {
|
||||
console.error(error.message || error);
|
||||
console.error(sanitizeLogMessage(error.message || String(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,16 +31,16 @@ export function logError(message, error = null) {
|
||||
export function logRequest(method, url, headers = null, body = null) {
|
||||
if (isDevMode()) {
|
||||
console.log(`\n${'='.repeat(80)}`);
|
||||
console.log(`[REQUEST] ${method} ${url}`);
|
||||
console.log(`[REQUEST] ${sanitizeLogMessage(method)} ${sanitizeLogMessage(url)}`);
|
||||
if (headers) {
|
||||
console.log('[HEADERS]', JSON.stringify(headers, null, 2));
|
||||
console.log('[HEADERS]', JSON.stringify(sanitizeForLog(headers), null, 2));
|
||||
}
|
||||
if (body) {
|
||||
console.log('[BODY]', JSON.stringify(body, null, 2));
|
||||
console.log('[BODY]', JSON.stringify(sanitizeForLog(body), null, 2));
|
||||
}
|
||||
console.log('='.repeat(80) + '\n');
|
||||
} else {
|
||||
console.log(`[REQUEST] ${method} ${url}`);
|
||||
console.log(`[REQUEST] ${sanitizeLogMessage(method)} ${sanitizeLogMessage(url)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +49,10 @@ export function logResponse(status, headers = null, body = null) {
|
||||
console.log(`\n${'-'.repeat(80)}`);
|
||||
console.log(`[RESPONSE] Status: ${status}`);
|
||||
if (headers) {
|
||||
console.log('[HEADERS]', JSON.stringify(headers, null, 2));
|
||||
console.log('[HEADERS]', JSON.stringify(sanitizeForLog(headers), null, 2));
|
||||
}
|
||||
if (body) {
|
||||
console.log('[BODY]', JSON.stringify(body, null, 2));
|
||||
console.log('[BODY]', JSON.stringify(sanitizeForLog(body), null, 2));
|
||||
}
|
||||
console.log('-'.repeat(80) + '\n');
|
||||
} else {
|
||||
|
||||
20
server.js
20
server.js
@@ -5,6 +5,7 @@ import router from './routes.js';
|
||||
import { initializeAuth } from './auth.js';
|
||||
import { initializeUserAgentUpdater } from './user-agent-updater.js';
|
||||
import './sls-logger.js'; // 初始化阿里云日志服务
|
||||
import { sanitizeForLog } from './log-sanitizer.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -58,6 +59,11 @@ app.use((req, res, next) => {
|
||||
ip: req.ip || req.connection.remoteAddress
|
||||
};
|
||||
|
||||
const safeQuery = sanitizeForLog(errorInfo.query);
|
||||
const safeBody = sanitizeForLog(errorInfo.body);
|
||||
const safeHeaders = sanitizeForLog(errorInfo.headers);
|
||||
const safeIp = sanitizeForLog(errorInfo.ip);
|
||||
|
||||
console.error('\n' + '='.repeat(80));
|
||||
console.error('❌ 非法请求地址');
|
||||
console.error('='.repeat(80));
|
||||
@@ -67,23 +73,23 @@ app.use((req, res, next) => {
|
||||
console.error(`路径: ${errorInfo.path}`);
|
||||
|
||||
if (Object.keys(errorInfo.query).length > 0) {
|
||||
console.error(`查询参数: ${JSON.stringify(errorInfo.query, null, 2)}`);
|
||||
console.error(`查询参数: ${JSON.stringify(safeQuery, null, 2)}`);
|
||||
}
|
||||
|
||||
if (errorInfo.body && Object.keys(errorInfo.body).length > 0) {
|
||||
console.error(`请求体: ${JSON.stringify(errorInfo.body, null, 2)}`);
|
||||
console.error(`请求体: ${JSON.stringify(safeBody, null, 2)}`);
|
||||
}
|
||||
|
||||
console.error(`客户端IP: ${errorInfo.ip}`);
|
||||
console.error(`User-Agent: ${errorInfo.headers['user-agent'] || 'N/A'}`);
|
||||
console.error(`客户端IP: ${safeIp}`);
|
||||
console.error(`User-Agent: ${safeHeaders['user-agent'] || 'N/A'}`);
|
||||
|
||||
if (errorInfo.headers.referer) {
|
||||
console.error(`来源: ${errorInfo.headers.referer}`);
|
||||
if (safeHeaders.referer) {
|
||||
console.error(`来源: ${safeHeaders.referer}`);
|
||||
}
|
||||
|
||||
console.error('='.repeat(80) + '\n');
|
||||
|
||||
logError('Invalid request path', errorInfo);
|
||||
logError('Invalid request path', sanitizeForLog(errorInfo));
|
||||
|
||||
res.status(404).json({
|
||||
error: 'Not Found',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import ALSClient from 'aliyun-log';
|
||||
import { sanitizeForLog } from './log-sanitizer.js';
|
||||
|
||||
// SLS 配置
|
||||
const SLS_CONFIG = {
|
||||
@@ -18,8 +19,22 @@ const SLS_CONFIG = {
|
||||
logstore: process.env.ALIYUN_SLS_LOGSTORE
|
||||
};
|
||||
|
||||
function resolveSlsEnabled() {
|
||||
const raw = process.env.SLS_ENABLED;
|
||||
if (raw === undefined || raw === null || String(raw).trim() === '') {
|
||||
return process.env.NODE_ENV !== 'production';
|
||||
}
|
||||
|
||||
const value = String(raw).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(value)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(value)) return false;
|
||||
return process.env.NODE_ENV !== 'production';
|
||||
}
|
||||
|
||||
const SLS_ENABLED = resolveSlsEnabled();
|
||||
|
||||
// 检查配置是否完整
|
||||
const isConfigured = Object.values(SLS_CONFIG).every(v => v);
|
||||
const isConfigured = SLS_ENABLED && Object.values(SLS_CONFIG).every(v => v);
|
||||
|
||||
let client = null;
|
||||
let logQueue = [];
|
||||
@@ -28,6 +43,9 @@ const FLUSH_INTERVAL_MS = 5000;
|
||||
|
||||
// 初始化 SLS Client
|
||||
function initClient() {
|
||||
if (!SLS_ENABLED) {
|
||||
return null;
|
||||
}
|
||||
if (!isConfigured) {
|
||||
console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台');
|
||||
return null;
|
||||
@@ -73,7 +91,7 @@ async function flushLogs() {
|
||||
// 定时刷新
|
||||
let flushTimer = null;
|
||||
function startFlushTimer() {
|
||||
if (flushTimer || !isConfigured) return;
|
||||
if (!SLS_ENABLED || flushTimer || !isConfigured) return;
|
||||
flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
@@ -90,10 +108,11 @@ function startFlushTimer() {
|
||||
* @param {string} [logData.error] - 错误信息
|
||||
*/
|
||||
export function logRequest(logData) {
|
||||
const enrichedLog = {
|
||||
if (!SLS_ENABLED) return;
|
||||
const enrichedLog = sanitizeForLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
...logData
|
||||
};
|
||||
});
|
||||
|
||||
// 始终输出到控制台
|
||||
console.log('[SLS]', JSON.stringify(enrichedLog));
|
||||
@@ -112,6 +131,7 @@ export function logRequest(logData) {
|
||||
* 优雅关闭,刷新剩余日志
|
||||
*/
|
||||
export async function shutdown() {
|
||||
if (!SLS_ENABLED) return;
|
||||
if (flushTimer) {
|
||||
clearInterval(flushTimer);
|
||||
flushTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user