155 lines
4.2 KiB
JavaScript
155 lines
4.2 KiB
JavaScript
/**
|
||
* 阿里云日志服务(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
|
||
};
|