/**
* 日志服务实现
*/
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '@/utils/logger';
import { ILogService } from '@/types';
/**
* 日志级别
*/
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
/**
* 日志条目
*/
interface LogEntry {
id: string;
timestamp: string;
level: LogLevel;
message: string;
meta?: any;
port?: string;
sessionId?: string;
component?: string;
duration?: number;
}
/**
* 日志查询选项
*/
interface LogQuery {
port?: string;
since?: string;
until?: string;
level?: LogLevel;
component?: string;
sessionId?: string;
limit?: number;
offset?: number;
}
/**
* 日志服务配置
*/
interface LogServiceConfig {
logLevel: LogLevel;
logFile: string;
maxBufferSize: number;
rotationSize: number;
maxFiles: number;
compressionEnabled: boolean;
format: 'json' | 'text';
}
/**
* 日志服务实现类
*/
export class LogService implements ILogService {
/**
* 解析日志级别字符串
*/
private parseLogLevel(level: string): LogLevel {
switch (level.toLowerCase()) {
case 'debug': return LogLevel.DEBUG;
case 'info': return LogLevel.INFO;
case 'warn': return LogLevel.WARN;
case 'error': return LogLevel.ERROR;
default: return LogLevel.INFO;
}
}
private logBuffer: LogEntry[] = [];
private config: LogServiceConfig;
private currentFileSize: number = 0;
private currentFileNumber: number = 0;
private writeQueue: LogEntry[] = [];
private isWriting: boolean = false;
private flushInterval?: NodeJS.Timeout;
constructor(config?: Partial<LogServiceConfig>) {
// 设置默认配置
this.config = {
logLevel: LogLevel.INFO,
logFile: 'logs/mcp2serial.log',
maxBufferSize: 10000,
rotationSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
compressionEnabled: true,
format: 'json',
...config
};
// 确保日志目录存在
this.ensureLogDirectory();
// 启动定期刷新
this.startFlushInterval();
logger.debug('LogService initialized', this.config);
}
/**
* 记录日志
*/
log(level: LogLevel | string, message: string, meta?: any): void {
try {
// 转换日志级别
let logLevel: LogLevel;
if (typeof level === 'string') {
logLevel = this.parseLogLevel(level);
} else {
logLevel = level;
}
// 检查日志级别
if (logLevel < this.config.logLevel) {
return;
}
const logEntry: LogEntry = {
id: uuidv4(),
timestamp: new Date().toISOString(),
level: logLevel,
message,
meta,
component: meta?.component,
port: meta?.port,
sessionId: meta?.sessionId,
duration: meta?.duration
};
// 添加到缓冲区
this.addToBuffer(logEntry);
// 添加到写入队列
this.addToWriteQueue(logEntry);
// 如果是错误级别,立即刷新
if (logLevel >= LogLevel.ERROR) {
this.flush().catch(error => {
logger.error('Failed to create log stream', error as Error);
});
}
} catch (error) {
logger.error('Failed to log message', error as Error);
}
}
/**
* 调试日志
*/
debug(message: string, meta?: any): void {
this.log(LogLevel.DEBUG, message, meta);
}
/**
* 信息日志
*/
info(message: string, meta?: any): void {
this.log(LogLevel.INFO, message, meta);
}
/**
* 警告日志
*/
warn(message: string, meta?: any): void {
this.log(LogLevel.WARN, message, meta);
}
/**
* 错误日志
*/
error(message: string, error?: Error, meta?: any): void {
const errorMeta = {
...meta,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack
} : undefined
};
this.log(LogLevel.ERROR, message, errorMeta);
}
/**
* 获取日志
*/
async getLogs(query: LogQuery): Promise<any> {
try {
// 从缓冲区查询
const bufferLogs = this.queryBuffer(query);
// 从文件查询(如果需要)
const fileLogs = await this.queryFiles(query);
// 合并结果
const allLogs = [...fileLogs, ...bufferLogs];
// 应用排序和限制
const sortedLogs = allLogs.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
const offset = query.offset || 0;
const limit = query.limit || 1000;
const paginatedLogs = sortedLogs.slice(offset, offset + limit);
return {
logs: paginatedLogs,
total: allLogs.length,
offset,
limit,
hasMore: offset + limit < allLogs.length
};
} catch (error) {
logger.error('Failed to get logs', error as Error);
throw error;
}
}
/**
* 清空日志
*/
async clearLogs(port?: string): Promise<void> {
try {
if (port) {
// 清空特定端口的日志
this.logBuffer = this.logBuffer.filter(entry => entry.port !== port);
} else {
// 清空所有日志
this.logBuffer = [];
}
// 清空写入队列
this.writeQueue = [];
// 删除日志文件
if (!port) {
await this.deleteLogFiles();
}
logger.info(`Logs cleared${port ? ` for port: ${port}` : ''}`);
} catch (error) {
logger.error('Failed to clear logs', error as Error);
throw error;
}
}
/**
* 轮转日志
*/
async rotateLogs(): Promise<void> {
try {
logger.info('Starting log rotation');
// 压缩旧文件
if (this.config.compressionEnabled) {
await this.compressOldFiles();
}
// 删除超出限制的文件
await this.cleanupOldFiles();
// 重置文件计数
this.currentFileNumber = 0;
this.currentFileSize = 0;
logger.info('Log rotation completed');
} catch (error) {
logger.error('Failed to schedule log rotation', error as Error);
throw error;
}
}
/**
* 获取日志统计
*/
getLogStats(): any {
const levelCounts = this.logBuffer.reduce((acc, entry) => {
acc[entry.level] = (acc[entry.level] || 0) + 1;
return acc;
}, {} as Record<LogLevel, number>);
const portCounts = this.logBuffer.reduce((acc, entry) => {
if (entry.port) {
acc[entry.port] = (acc[entry.port] || 0) + 1;
}
return acc;
}, {} as Record<string, number>);
return {
bufferSize: this.logBuffer.length,
maxBufferSize: this.config.maxBufferSize,
currentFileSize: this.currentFileSize,
currentFileNumber: this.currentFileNumber,
levelCounts,
portCounts,
writeQueueSize: this.writeQueue.length
};
}
/**
* 设置日志级别
*/
setLogLevel(level: LogLevel): void {
this.config.logLevel = level;
logger.info(`Log level set to: ${LogLevel[level]}`);
}
/**
* 添加到缓冲区
*/
private addToBuffer(entry: LogEntry): void {
this.logBuffer.push(entry);
// 检查缓冲区大小
if (this.logBuffer.length > this.config.maxBufferSize) {
// 移除最旧的条目
const removed = this.logBuffer.shift();
if (removed) {
logger.debug('Log buffer overflow, removed oldest entry', {
removedId: removed.id,
bufferSize: this.logBuffer.length
});
}
}
}
/**
* 添加到写入队列
*/
private addToWriteQueue(entry: LogEntry): void {
this.writeQueue.push(entry);
// 如果队列过大或当前没有在写入,触发写入
if (this.writeQueue.length >= 100 || !this.isWriting) {
this.flush().catch(error => {
logger.error('Failed to flush log queue', error);
});
}
}
/**
* 刷新写入队列
*/
private async flush(): Promise<void> {
if (this.isWriting || this.writeQueue.length === 0) {
return;
}
this.isWriting = true;
try {
const entriesToWrite = [...this.writeQueue];
this.writeQueue = [];
// 批量写入
await this.writeToFile(entriesToWrite);
logger.debug(`Flushed ${entriesToWrite.length} log entries`);
} catch (error) {
logger.error('Failed to flush logs', error as Error);
} finally {
this.isWriting = false;
}
}
/**
* 写入文件
*/
private async writeToFile(entries: LogEntry[]): Promise<void> {
try {
// 检查是否需要轮转
if (this.currentFileSize >= this.config.rotationSize) {
await this.rotateFile();
}
const logFilePath = this.getLogFilePath();
const logLines = entries.map(entry => this.formatLogEntry(entry));
const logContent = logLines.join('\n') + '\n';
// 异步写入文件
await fs.promises.appendFile(logFilePath, logContent, 'utf8');
// 更新文件大小
this.currentFileSize += Buffer.byteLength(logContent, 'utf8');
} catch (error) {
logger.error('Failed to write to log file', error as Error);
throw error;
}
}
/**
* 格式化日志条目
*/
private formatLogEntry(entry: LogEntry): string {
if (this.config.format === 'json') {
return JSON.stringify(entry);
} else {
const levelStr = LogLevel[entry.level].padEnd(5);
const timestamp = entry.timestamp;
const portStr = entry.port ? `[${entry.port}]` : '';
const sessionIdStr = entry.sessionId ? `[${entry.sessionId}]` : '';
const componentStr = entry.component ? `[${entry.component}]` : '';
return `${timestamp} ${levelStr} ${componentStr}${portStr}${sessionIdStr} ${entry.message}`;
}
}
/**
* 轮转文件
*/
private async rotateFile(): Promise<void> {
try {
const currentPath = this.getLogFilePath();
if (fs.existsSync(currentPath)) {
// 压缩当前文件
if (this.config.compressionEnabled) {
await this.compressFile(currentPath);
} else {
// 重命名文件
const rotatedPath = this.getLogFilePath(this.currentFileNumber);
await fs.promises.rename(currentPath, rotatedPath);
}
}
this.currentFileNumber++;
this.currentFileSize = 0;
} catch (error) {
logger.error('Failed to rotate log file', error as Error);
}
}
/**
* 压缩文件
*/
private async compressFile(filePath: string): Promise<void> {
try {
const content = await fs.promises.readFile(filePath);
const compressed = await new Promise<Buffer>((resolve, reject) => {
zlib.gzip(content, { level: 9 }, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
const compressedPath = `${filePath}.gz`;
await fs.promises.writeFile(compressedPath, compressed);
await fs.promises.unlink(filePath);
logger.debug(`Log file compressed: ${filePath}`);
} catch (error) {
logger.error('Failed to compress log file', error as Error);
}
}
/**
* 获取日志文件路径
*/
private getLogFilePath(fileNumber: number = 0): string {
const dir = path.dirname(this.config.logFile);
const name = path.basename(this.config.logFile, path.extname(this.config.logFile));
const ext = path.extname(this.config.logFile);
if (fileNumber === 0) {
return path.join(dir, `${name}${ext}`);
} else {
return path.join(dir, `${name}.${fileNumber}${ext}`);
}
}
/**
* 查询缓冲区
*/
private queryBuffer(query: LogQuery): LogEntry[] {
let logs = [...this.logBuffer];
// 应用过滤条件
if (query.port) {
logs = logs.filter(entry => entry.port === query.port);
}
if (query.level !== undefined) {
logs = logs.filter(entry => entry.level >= query.level!);
}
if (query.component) {
logs = logs.filter(entry => entry.component === query.component);
}
if (query.sessionId) {
logs = logs.filter(entry => entry.sessionId === query.sessionId);
}
if (query.since) {
const since = new Date(query.since);
logs = logs.filter(entry => new Date(entry.timestamp) >= since);
}
if (query.until) {
const until = new Date(query.until);
logs = logs.filter(entry => new Date(entry.timestamp) <= until);
}
return logs;
}
/**
* 查询文件
*/
private async queryFiles(query: LogQuery): Promise<LogEntry[]> {
// TODO: 实现文件查询
// 这里简化实现,实际应该读取日志文件并解析
return [];
}
/**
* 删除日志文件
*/
private async deleteLogFiles(): Promise<void> {
try {
const dir = path.dirname(this.config.logFile);
const name = path.basename(this.config.logFile, path.extname(this.config.logFile));
const ext = path.extname(this.config.logFile);
const files = await fs.promises.readdir(dir);
const logFiles = files.filter(file =>
file.startsWith(name) && (file.endsWith(ext) || file.endsWith(`${ext}.gz`))
);
for (const file of logFiles) {
await fs.promises.unlink(path.join(dir, file));
}
logger.info(`Deleted ${logFiles.length} log files`);
} catch (error) {
logger.error('Failed to delete log files', error as Error);
}
}
/**
* 压缩旧文件
*/
private async compressOldFiles(): Promise<void> {
try {
const dir = path.dirname(this.config.logFile);
const name = path.basename(this.config.logFile, path.extname(this.config.logFile));
const ext = path.extname(this.config.logFile);
const files = await fs.promises.readdir(dir);
const logFiles = files.filter(file =>
file.startsWith(name) && file.endsWith(ext) && !file.includes('.gz')
);
for (const file of logFiles) {
const filePath = path.join(dir, file);
await this.compressFile(filePath);
}
} catch (error) {
logger.error('Failed to compress old files', error as Error);
}
}
/**
* 清理旧文件
*/
private async cleanupOldFiles(): Promise<void> {
try {
const dir = path.dirname(this.config.logFile);
const files = await fs.promises.readdir(dir);
// 按修改时间排序
const fileStats = await Promise.all(
files.map(async file => {
const filePath = path.join(dir, file);
const stats = await fs.promises.stat(filePath);
return { file, filePath, mtime: stats.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
// 保留最新的文件,删除超出限制的
const filesToDelete = fileStats.slice(this.config.maxFiles);
for (const { filePath } of filesToDelete) {
await fs.promises.unlink(filePath);
}
if (filesToDelete.length > 0) {
logger.info(`Cleaned up ${filesToDelete.length} old log files`);
}
} catch (error) {
logger.error('Failed to cleanup old files', error as Error);
}
}
/**
* 确保日志目录存在
*/
private ensureLogDirectory(): void {
const dir = path.dirname(this.config.logFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 启动定期刷新
*/
private startFlushInterval(): void {
this.flushInterval = setInterval(() => {
this.flush().catch(error => {
logger.error('Failed to flush logs in interval', error);
});
}, 5000); // 每5秒刷新一次
}
/**
* 停止定期刷新
*/
private stopFlushInterval(): void {
if (this.flushInterval) {
clearInterval(this.flushInterval);
this.flushInterval = undefined;
}
}
/**
* 销毁日志服务
*/
async dispose(): Promise<void> {
try {
logger.info('Disposing LogService...');
// 停止定期刷新
this.stopFlushInterval();
// 最后一次刷新
await this.flush();
// 清理资源
this.logBuffer = [];
this.writeQueue = [];
logger.info('LogService disposed');
} catch (error) {
logger.error('Failed to dispose LogService', error as Error);
}
}
}