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 };