AIClient-2-API/src/utils/logger.js
hex2077 ce7d78f7d0 feat(plugins): 新增 AI 监控插件并优化日志管理
- 新增 AI 监控插件 (ai-monitor),支持全链路协议转换监控
  - 捕获 AI 接口请求参数(转换前后)
  - 监控流式和非流式响应(转换前后)
  - 支持内部请求转换监控
- 新增日志清空功能,支持前端和服务器端同时清空当日日志
- 默认禁用 api-potluck 和 ai-monitor 插件
- 更新多语言文档和配置示例
- 优化提供商适配器开发指南
2026-01-25 19:40:04 +08:00

386 lines
12 KiB
JavaScript
Raw 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.

import * as fs from 'fs';
import * as path from 'path';
import { randomUUID } from 'crypto';
/**
* 统一日志工具类
* 支持控制台和文件输出自动添加请求ID和时间戳
*/
class Logger {
constructor() {
this.config = {
enabled: true,
outputMode: 'all', // 'console', 'file', 'all', 'none'
logDir: 'logs',
logLevel: 'info', // 'debug', 'info', 'warn', 'error'
includeRequestId: true,
includeTimestamp: true,
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 10
};
this.currentLogFile = null;
this.logStream = null;
this.currentRequestId = null; // 当前请求ID
this.requestContext = new Map(); // 存储请求上下文
this.levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
}
/**
* 初始化日志配置
* @param {Object} config - 日志配置对象
*/
initialize(config = {}) {
this.config = { ...this.config, ...config };
if (this.config.outputMode === 'none') {
this.config.enabled = false;
return;
}
if (this.config.outputMode === 'file' || this.config.outputMode === 'all') {
this.initializeFileLogging();
}
}
/**
* 初始化文件日志
*/
initializeFileLogging() {
try {
// 确保日志目录存在
if (!fs.existsSync(this.config.logDir)) {
fs.mkdirSync(this.config.logDir, { recursive: true });
}
// 创建日志文件名(按本地日期)
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
this.currentLogFile = path.join(this.config.logDir, `app-${dateStr}.log`);
// 创建写入流
this.logStream = fs.createWriteStream(this.currentLogFile, { flags: 'a' });
// 监听错误
this.logStream.on('error', (err) => {
console.error('[Logger] Failed to write to log file:', err.message);
});
} catch (error) {
console.error('[Logger] Failed to initialize file logging:', error.message);
}
}
/**
* 设置请求上下文
* @param {string} requestId - 请求ID
* @param {Object} context - 上下文信息
*/
setRequestContext(requestId, context = {}) {
if (!requestId) {
requestId = randomUUID().substring(0, 8);
}
this.currentRequestId = requestId;
this.requestContext.set(requestId, context);
return requestId;
}
/**
* 获取当前请求ID
* @returns {string} 请求ID
*/
getCurrentRequestId() {
// 从上下文中获取当前请求ID
return this.currentRequestId;
}
/**
* 获取当前请求上下文
* @param {string} requestId - 请求ID
* @returns {Object} 上下文信息
*/
getRequestContext(requestId) {
return this.requestContext.get(requestId) || {};
}
/**
* 清除请求上下文
* @param {string} requestId - 请求ID
*/
clearRequestContext(requestId) {
if (requestId) {
this.requestContext.delete(requestId);
}
this.currentRequestId = null;
}
/**
* 格式化日志消息
* @param {string} level - 日志级别
* @param {Array} args - 日志参数
* @param {string} requestId - 请求ID
* @returns {string} 格式化后的日志
*/
formatMessage(level, args, requestId) {
const parts = [];
// 添加本地时间戳
if (this.config.includeTimestamp) {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
const timestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
parts.push(`[${timestamp}]`);
}
// 添加请求ID
if (this.config.includeRequestId && requestId) {
parts.push(`[Req:${requestId}]`);
}
// 添加日志级别
parts.push(`[${level.toUpperCase()}]`);
// 添加消息内容
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch (e) {
return String(arg);
}
}
return String(arg);
}).join(' ');
parts.push(message);
return parts.join(' ');
}
/**
* 检查是否应该输出该级别的日志
* @param {string} level - 日志级别
* @returns {boolean}
*/
shouldLog(level) {
if (!this.config.enabled) return false;
const currentLevel = this.levels[this.config.logLevel] ?? 1;
const targetLevel = this.levels[level] ?? 1;
return targetLevel >= currentLevel;
}
/**
* 检查并轮转日志文件
*/
checkAndRotateLogFile() {
try {
if (!this.currentLogFile || !fs.existsSync(this.currentLogFile)) {
return;
}
const stats = fs.statSync(this.currentLogFile);
if (stats.size >= this.config.maxFileSize) {
// 关闭当前日志流
if (this.logStream && !this.logStream.destroyed) {
this.logStream.end();
}
// 重命名当前日志文件,添加时间戳
const timestamp = new Date().getTime();
const ext = path.extname(this.currentLogFile);
const basename = path.basename(this.currentLogFile, ext);
const newName = path.join(this.config.logDir, `${basename}-${timestamp}${ext}`);
fs.renameSync(this.currentLogFile, newName);
// 重新创建日志流
this.logStream = fs.createWriteStream(this.currentLogFile, { flags: 'a' });
this.logStream.on('error', (err) => {
console.error('[Logger] Failed to write to log file:', err.message);
});
// 清理旧日志文件
this.cleanupOldLogs();
}
} catch (error) {
console.error('[Logger] Failed to rotate log file:', error.message);
}
}
/**
* 输出日志
* @param {string} level - 日志级别
* @param {Array} args - 日志参数
* @param {string} requestId - 请求ID
*/
log(level, args, requestId = null) {
if (!this.shouldLog(level)) return;
const message = this.formatMessage(level, args, requestId);
// 输出到控制台
if (this.config.outputMode === 'console' || this.config.outputMode === 'all') {
const consoleMethod = level === 'error' ? console.error :
level === 'warn' ? console.warn :
level === 'debug' ? console.debug : console.log;
consoleMethod(message);
}
// 输出到文件
if (this.config.outputMode === 'file' || this.config.outputMode === 'all') {
if (this.logStream && !this.logStream.destroyed) {
// 检查文件大小并轮转
this.checkAndRotateLogFile();
this.logStream.write(message + '\n');
}
}
}
/**
* Debug 级别日志
* @param {...any} args - 日志参数
*/
debug(...args) {
const requestId = this.getCurrentRequestId();
this.log('debug', args, requestId);
}
/**
* Info 级别日志
* @param {...any} args - 日志参数
*/
info(...args) {
const requestId = this.getCurrentRequestId();
this.log('info', args, requestId);
}
/**
* Warn 级别日志
* @param {...any} args - 日志参数
*/
warn(...args) {
const requestId = this.getCurrentRequestId();
this.log('warn', args, requestId);
}
/**
* Error 级别日志
* @param {...any} args - 日志参数
*/
error(...args) {
const requestId = this.getCurrentRequestId();
this.log('error', args, requestId);
}
/**
* 创建带请求ID的日志记录器
* @param {string} requestId - 请求ID
* @returns {Object} 带请求上下文的日志方法
*/
withRequest(requestId) {
if (!requestId) {
requestId = this.getCurrentRequestId();
}
return {
debug: (...args) => this.log('debug', args, requestId),
info: (...args) => this.log('info', args, requestId),
warn: (...args) => this.log('warn', args, requestId),
error: (...args) => this.log('error', args, requestId)
};
}
/**
* 关闭日志流
*/
close() {
if (this.logStream && !this.logStream.destroyed) {
this.logStream.end();
this.logStream = null;
}
}
/**
* 清理旧日志文件
*/
cleanupOldLogs() {
try {
if (!fs.existsSync(this.config.logDir)) {
return;
}
const files = fs.readdirSync(this.config.logDir)
.filter(file => file.startsWith('app-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(this.config.logDir, file),
time: fs.statSync(path.join(this.config.logDir, file)).mtime.getTime()
}))
.sort((a, b) => b.time - a.time);
// 保留最新的 maxFiles 个文件,删除其他的
if (files.length > this.config.maxFiles) {
for (let i = this.config.maxFiles; i < files.length; i++) {
try {
fs.unlinkSync(files[i].path);
} catch (err) {
console.error('[Logger] Failed to delete old log file:', files[i].name, err.message);
}
}
}
} catch (error) {
console.error('[Logger] Failed to cleanup old logs:', error.message);
}
}
/**
* 清空当日日志文件
* @returns {boolean} 是否成功清空
*/
clearTodayLog() {
try {
if (!this.currentLogFile || !fs.existsSync(this.currentLogFile)) {
console.warn('[Logger] No current log file to clear');
return false;
}
// 关闭当前日志流
if (this.logStream && !this.logStream.destroyed) {
this.logStream.end();
}
// 清空文件内容
fs.writeFileSync(this.currentLogFile, '');
// 重新创建日志流
this.logStream = fs.createWriteStream(this.currentLogFile, { flags: 'a' });
this.logStream.on('error', (err) => {
console.error('[Logger] Failed to write to log file:', err.message);
});
console.log('[Logger] Today\'s log file cleared successfully');
return true;
} catch (error) {
console.error('[Logger] Failed to clear today\'s log file:', error.message);
return false;
}
}
}
// 创建单例实例
const logger = new Logger();
// 导出实例和类
export default logger;
export { Logger };