feat: add SLS toggle and sanitize logs

This commit is contained in:
empty
2025-12-27 15:07:28 +08:00
parent b186f9b80e
commit 5e01993120
5 changed files with 139 additions and 24 deletions

View File

@@ -7,6 +7,7 @@ FACTORY_API_KEY=your_factory_api_key_here
DROID_REFRESH_KEY=your_refresh_token_here DROID_REFRESH_KEY=your_refresh_token_here
# 阿里云日志服务配置 # 阿里云日志服务配置
SLS_ENABLED=false
ALIYUN_ACCESS_KEY_ID=your_access_key_id ALIYUN_ACCESS_KEY_ID=your_access_key_id
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com

87
log-sanitizer.js Normal file
View 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);
}

View File

@@ -1,28 +1,29 @@
import { isDevMode } from './config.js'; import { isDevMode } from './config.js';
import { sanitizeForLog, sanitizeLogMessage } from './log-sanitizer.js';
export function logInfo(message, data = null) { export function logInfo(message, data = null) {
console.log(`[INFO] ${message}`); console.log(`[INFO] ${sanitizeLogMessage(message)}`);
if (data && isDevMode()) { if (data && isDevMode()) {
console.log(JSON.stringify(data, null, 2)); console.log(JSON.stringify(sanitizeForLog(data), null, 2));
} }
} }
export function logDebug(message, data = null) { export function logDebug(message, data = null) {
if (isDevMode()) { if (isDevMode()) {
console.log(`[DEBUG] ${message}`); console.log(`[DEBUG] ${sanitizeLogMessage(message)}`);
if (data) { if (data) {
console.log(JSON.stringify(data, null, 2)); console.log(JSON.stringify(sanitizeForLog(data), null, 2));
} }
} }
} }
export function logError(message, error = null) { export function logError(message, error = null) {
console.error(`[ERROR] ${message}`); console.error(`[ERROR] ${sanitizeLogMessage(message)}`);
if (error) { if (error) {
if (isDevMode()) { if (isDevMode()) {
console.error(error); console.error(sanitizeForLog(error));
} else { } 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) { export function logRequest(method, url, headers = null, body = null) {
if (isDevMode()) { if (isDevMode()) {
console.log(`\n${'='.repeat(80)}`); console.log(`\n${'='.repeat(80)}`);
console.log(`[REQUEST] ${method} ${url}`); console.log(`[REQUEST] ${sanitizeLogMessage(method)} ${sanitizeLogMessage(url)}`);
if (headers) { if (headers) {
console.log('[HEADERS]', JSON.stringify(headers, null, 2)); console.log('[HEADERS]', JSON.stringify(sanitizeForLog(headers), null, 2));
} }
if (body) { if (body) {
console.log('[BODY]', JSON.stringify(body, null, 2)); console.log('[BODY]', JSON.stringify(sanitizeForLog(body), null, 2));
} }
console.log('='.repeat(80) + '\n'); console.log('='.repeat(80) + '\n');
} else { } 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(`\n${'-'.repeat(80)}`);
console.log(`[RESPONSE] Status: ${status}`); console.log(`[RESPONSE] Status: ${status}`);
if (headers) { if (headers) {
console.log('[HEADERS]', JSON.stringify(headers, null, 2)); console.log('[HEADERS]', JSON.stringify(sanitizeForLog(headers), null, 2));
} }
if (body) { if (body) {
console.log('[BODY]', JSON.stringify(body, null, 2)); console.log('[BODY]', JSON.stringify(sanitizeForLog(body), null, 2));
} }
console.log('-'.repeat(80) + '\n'); console.log('-'.repeat(80) + '\n');
} else { } else {

View File

@@ -5,6 +5,7 @@ import router from './routes.js';
import { initializeAuth } from './auth.js'; import { initializeAuth } from './auth.js';
import { initializeUserAgentUpdater } from './user-agent-updater.js'; import { initializeUserAgentUpdater } from './user-agent-updater.js';
import './sls-logger.js'; // 初始化阿里云日志服务 import './sls-logger.js'; // 初始化阿里云日志服务
import { sanitizeForLog } from './log-sanitizer.js';
const app = express(); const app = express();
@@ -58,6 +59,11 @@ app.use((req, res, next) => {
ip: req.ip || req.connection.remoteAddress 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('\n' + '='.repeat(80));
console.error('❌ 非法请求地址'); console.error('❌ 非法请求地址');
console.error('='.repeat(80)); console.error('='.repeat(80));
@@ -67,23 +73,23 @@ app.use((req, res, next) => {
console.error(`路径: ${errorInfo.path}`); console.error(`路径: ${errorInfo.path}`);
if (Object.keys(errorInfo.query).length > 0) { 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) { 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(`客户端IP: ${safeIp}`);
console.error(`User-Agent: ${errorInfo.headers['user-agent'] || 'N/A'}`); console.error(`User-Agent: ${safeHeaders['user-agent'] || 'N/A'}`);
if (errorInfo.headers.referer) { if (safeHeaders.referer) {
console.error(`来源: ${errorInfo.headers.referer}`); console.error(`来源: ${safeHeaders.referer}`);
} }
console.error('='.repeat(80) + '\n'); console.error('='.repeat(80) + '\n');
logError('Invalid request path', errorInfo); logError('Invalid request path', sanitizeForLog(errorInfo));
res.status(404).json({ res.status(404).json({
error: 'Not Found', error: 'Not Found',

View File

@@ -8,6 +8,7 @@
*/ */
import ALSClient from 'aliyun-log'; import ALSClient from 'aliyun-log';
import { sanitizeForLog } from './log-sanitizer.js';
// SLS 配置 // SLS 配置
const SLS_CONFIG = { const SLS_CONFIG = {
@@ -18,8 +19,22 @@ const SLS_CONFIG = {
logstore: process.env.ALIYUN_SLS_LOGSTORE 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 client = null;
let logQueue = []; let logQueue = [];
@@ -28,6 +43,9 @@ const FLUSH_INTERVAL_MS = 5000;
// 初始化 SLS Client // 初始化 SLS Client
function initClient() { function initClient() {
if (!SLS_ENABLED) {
return null;
}
if (!isConfigured) { if (!isConfigured) {
console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台'); console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台');
return null; return null;
@@ -73,7 +91,7 @@ async function flushLogs() {
// 定时刷新 // 定时刷新
let flushTimer = null; let flushTimer = null;
function startFlushTimer() { function startFlushTimer() {
if (flushTimer || !isConfigured) return; if (!SLS_ENABLED || flushTimer || !isConfigured) return;
flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS); flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS);
} }
@@ -90,10 +108,11 @@ function startFlushTimer() {
* @param {string} [logData.error] - 错误信息 * @param {string} [logData.error] - 错误信息
*/ */
export function logRequest(logData) { export function logRequest(logData) {
const enrichedLog = { if (!SLS_ENABLED) return;
const enrichedLog = sanitizeForLog({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
...logData ...logData
}; });
// 始终输出到控制台 // 始终输出到控制台
console.log('[SLS]', JSON.stringify(enrichedLog)); console.log('[SLS]', JSON.stringify(enrichedLog));
@@ -112,6 +131,7 @@ export function logRequest(logData) {
* 优雅关闭,刷新剩余日志 * 优雅关闭,刷新剩余日志
*/ */
export async function shutdown() { export async function shutdown() {
if (!SLS_ENABLED) return;
if (flushTimer) { if (flushTimer) {
clearInterval(flushTimer); clearInterval(flushTimer);
flushTimer = null; flushTimer = null;