Files
droid2api/sls-logger.js
2025-12-27 15:07:28 +08:00

155 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 阿里云日志服务SLS日志模块
*
* 功能:
* - 将 API 请求/响应日志上报到阿里云 SLS
* - 批量上报,减少 API 调用
* - 环境变量缺失时静默降级
*/
import ALSClient from 'aliyun-log';
import { sanitizeForLog } from './log-sanitizer.js';
// SLS 配置
const SLS_CONFIG = {
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
endpoint: process.env.ALIYUN_SLS_ENDPOINT,
project: process.env.ALIYUN_SLS_PROJECT,
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 = SLS_ENABLED && Object.values(SLS_CONFIG).every(v => v);
let client = null;
let logQueue = [];
const BATCH_SIZE = 10;
const FLUSH_INTERVAL_MS = 5000;
// 初始化 SLS Client
function initClient() {
if (!SLS_ENABLED) {
return null;
}
if (!isConfigured) {
console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台');
return null;
}
try {
client = new ALSClient({
accessKeyId: SLS_CONFIG.accessKeyId,
accessKeySecret: SLS_CONFIG.accessKeySecret,
endpoint: SLS_CONFIG.endpoint
});
console.log('[SLS] 阿里云日志服务客户端初始化成功');
return client;
} catch (error) {
console.error('[SLS] 初始化失败:', error.message);
return null;
}
}
// 刷新日志队列
async function flushLogs() {
if (!client || logQueue.length === 0) return;
const logsToSend = logQueue.splice(0, BATCH_SIZE);
try {
const logs = logsToSend.map(log => ({
timestamp: Math.floor(Date.now() / 1000),
content: Object.fromEntries(
Object.entries(log).map(([key, value]) => [key, String(value ?? '')])
)
}));
await client.postLogStoreLogs(SLS_CONFIG.project, SLS_CONFIG.logstore, { logs });
console.log(`[SLS] 成功上报 ${logsToSend.length} 条日志`);
} catch (error) {
console.error('[SLS] 日志上报失败:', error.message);
// 失败的日志重新入队(可选:限制重试次数)
logQueue.unshift(...logsToSend);
}
}
// 定时刷新
let flushTimer = null;
function startFlushTimer() {
if (!SLS_ENABLED || flushTimer || !isConfigured) return;
flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS);
}
/**
* 记录 API 请求日志
* @param {Object} logData - 日志数据
* @param {string} logData.method - HTTP 方法
* @param {string} logData.endpoint - 请求路径
* @param {string} logData.model - 模型 ID
* @param {number} logData.status - 响应状态码
* @param {number} logData.duration_ms - 请求耗时
* @param {number} [logData.input_tokens] - 输入 Token 数
* @param {number} [logData.output_tokens] - 输出 Token 数
* @param {string} [logData.error] - 错误信息
*/
export function logRequest(logData) {
if (!SLS_ENABLED) return;
const enrichedLog = sanitizeForLog({
timestamp: new Date().toISOString(),
...logData
});
// 始终输出到控制台
console.log('[SLS]', JSON.stringify(enrichedLog));
if (!isConfigured) return;
logQueue.push(enrichedLog);
// 队列满时立即刷新
if (logQueue.length >= BATCH_SIZE) {
flushLogs();
}
}
/**
* 优雅关闭,刷新剩余日志
*/
export async function shutdown() {
if (!SLS_ENABLED) return;
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
await flushLogs();
console.log('[SLS] 已关闭');
}
// 初始化
initClient();
startFlushTimer();
// 进程退出时优雅关闭
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
export default {
logRequest,
shutdown
};