- 新增 AI 监控插件 (ai-monitor),支持全链路协议转换监控 - 捕获 AI 接口请求参数(转换前后) - 监控流式和非流式响应(转换前后) - 支持内部请求转换监控 - 新增日志清空功能,支持前端和服务器端同时清空当日日志 - 默认禁用 api-potluck 和 ai-monitor 插件 - 更新多语言文档和配置示例 - 优化提供商适配器开发指南
386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
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 };
|