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
|
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
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 { 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 {
|
||||||
|
|||||||
20
server.js
20
server.js
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user