/** * 日志记录模块 * 提供统一的日志记录、存储和导出功能 */ export enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', FATAL = 'FATAL', } export enum LogCategory { USER_ACTION = 'USER_ACTION', // 用户操作 API_CALL = 'API_CALL', // API调用 SYSTEM = 'SYSTEM', // 系统事件 CONSOLE_ERROR = 'CONSOLE_ERROR', // 控制台错误 } export interface LogEntry { id: string; timestamp: number; level: LogLevel; category: LogCategory; message: string; data?: any; stack?: string; userAgent?: string; url?: string; userId?: string; } class Logger { private logs: LogEntry[] = []; private maxLogs = 1000; // 最多保存1000条日志 private storageKey = 'app_logs'; private listeners: Set<(log: LogEntry) => void> = new Set(); private isEnabled = true; constructor() { this.loadLogsFromStorage(); this.setupConsoleInterceptor(); this.setupErrorHandler(); this.setupUnhandledRejectionHandler(); } /** * 从localStorage加载历史日志 */ private loadLogsFromStorage() { try { const stored = localStorage.getItem(this.storageKey); if (stored) { this.logs = JSON.parse(stored); } } catch (error) { console.error('Failed to load logs from storage:', error); } } /** * 保存日志到localStorage */ private saveLogsToStorage() { try { // 只保存最近的日志 const logsToSave = this.logs.slice(-this.maxLogs); localStorage.setItem(this.storageKey, JSON.stringify(logsToSave)); } catch (error) { console.error('Failed to save logs to storage:', error); } } /** * 拦截console方法 */ private setupConsoleInterceptor() { const originalError = console.error; // 只拦截错误,不拦截警告 console.error = (...args: any[]) => { originalError.apply(console, args); // 过滤掉一些常见的无关错误 const message = args.join(' '); if (!message.includes('ResizeObserver') && !message.includes('favicon') && !message.includes('Download the React DevTools')) { this.log(LogLevel.ERROR, LogCategory.CONSOLE_ERROR, message, { args }); } }; } /** * 设置全局错误处理 */ private setupErrorHandler() { window.addEventListener('error', (event) => { this.log( LogLevel.ERROR, LogCategory.CONSOLE_ERROR, event.message, { filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error, }, event.error?.stack ); }); } /** * 设置未处理的Promise拒绝处理 */ private setupUnhandledRejectionHandler() { window.addEventListener('unhandledrejection', (event) => { this.log( LogLevel.ERROR, LogCategory.CONSOLE_ERROR, `Unhandled Promise Rejection: ${event.reason}`, { reason: event.reason }, event.reason?.stack ); }); } /** * 记录日志 */ log( level: LogLevel, category: LogCategory, message: string, data?: any, stack?: string ) { if (!this.isEnabled) return; const entry: LogEntry = { id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: Date.now(), level, category, message, data, stack, userAgent: navigator.userAgent, url: window.location.href, }; this.logs.push(entry); // 限制日志数量 if (this.logs.length > this.maxLogs) { this.logs = this.logs.slice(-this.maxLogs); } // 保存到localStorage this.saveLogsToStorage(); // 通知监听器 this.listeners.forEach(listener => listener(entry)); // 在开发环境输出到控制台 if (import.meta.env.DEV) { this.logToConsole(entry); } } /** * 输出到控制台 */ private logToConsole(entry: LogEntry) { // 生产环境只输出ERROR和FATAL级别 if (!import.meta.env.DEV && entry.level !== LogLevel.ERROR && entry.level !== LogLevel.FATAL) { return; } const style = this.getConsoleStyle(entry.level); console.log( `%c[${entry.level}] [${entry.category}] ${new Date(entry.timestamp).toISOString()}`, style, entry.message, entry.data || '' ); } /** * 获取控制台样式 */ private getConsoleStyle(level: LogLevel): string { const styles = { [LogLevel.DEBUG]: 'color: #888', [LogLevel.INFO]: 'color: #0066cc', [LogLevel.WARN]: 'color: #ff9900', [LogLevel.ERROR]: 'color: #cc0000; font-weight: bold', [LogLevel.FATAL]: 'color: #fff; background: #cc0000; font-weight: bold', }; return styles[level]; } /** * 便捷方法 */ debug(category: LogCategory, message: string, data?: any) { this.log(LogLevel.DEBUG, category, message, data); } info(category: LogCategory, message: string, data?: any) { this.log(LogLevel.INFO, category, message, data); } warn(category: LogCategory, message: string, data?: any) { this.log(LogLevel.WARN, category, message, data); } error(category: LogCategory, message: string, data?: any, stack?: string) { this.log(LogLevel.ERROR, category, message, data, stack); } fatal(category: LogCategory, message: string, data?: any, stack?: string) { this.log(LogLevel.FATAL, category, message, data, stack); } /** * 记录用户操作 */ logUserAction(action: string, details?: any) { this.info(LogCategory.USER_ACTION, action, details); } /** * 记录API调用 */ logApiCall(method: string, url: string, status?: number, duration?: number, error?: any) { const level = error ? LogLevel.ERROR : LogLevel.INFO; this.log(level, LogCategory.API_CALL, `${method} ${url}`, { method, url, status, duration, error, }); } /** * 记录性能指标 */ logPerformance(metric: string, value: number, unit = 'ms') { this.info(LogCategory.SYSTEM, `${metric}: ${value}${unit}`, { metric, value, unit }); } /** * 获取所有日志 */ getLogs(filter?: { level?: LogLevel; category?: LogCategory; startTime?: number; endTime?: number; search?: string; }): LogEntry[] { let filtered = [...this.logs]; if (filter) { if (filter.level) { filtered = filtered.filter(log => log.level === filter.level); } if (filter.category) { filtered = filtered.filter(log => log.category === filter.category); } if (filter.startTime) { filtered = filtered.filter(log => log.timestamp >= filter.startTime!); } if (filter.endTime) { filtered = filtered.filter(log => log.timestamp <= filter.endTime!); } if (filter.search) { const search = filter.search.toLowerCase(); filtered = filtered.filter(log => log.message.toLowerCase().includes(search) || JSON.stringify(log.data).toLowerCase().includes(search) ); } } return filtered; } /** * 清空日志 */ clearLogs() { this.logs = []; localStorage.removeItem(this.storageKey); } /** * 导出日志为JSON */ exportLogsAsJson(): string { return JSON.stringify(this.logs, null, 2); } /** * 导出日志为CSV */ exportLogsAsCsv(): string { const headers = ['Timestamp', 'Level', 'Category', 'Message', 'Data', 'URL']; const rows = this.logs.map(log => [ new Date(log.timestamp).toISOString(), log.level, log.category, log.message, JSON.stringify(log.data || {}), log.url || '', ]); return [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell}"`).join(',')), ].join('\n'); } /** * 下载日志文件 */ downloadLogs(format: 'json' | 'csv' = 'json') { const content = format === 'json' ? this.exportLogsAsJson() : this.exportLogsAsCsv(); const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `logs-${new Date().toISOString()}.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * 添加日志监听器 */ addListener(listener: (log: LogEntry) => void) { this.listeners.add(listener); return () => this.listeners.delete(listener); } /** * 启用/禁用日志记录 */ setEnabled(enabled: boolean) { this.isEnabled = enabled; } /** * 获取日志统计 */ getStats() { const stats = { total: this.logs.length, byLevel: {} as Record, byCategory: {} as Record, errors: 0, }; this.logs.forEach(log => { stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1; stats.byCategory[log.category] = (stats.byCategory[log.category] || 0) + 1; if (log.level === LogLevel.ERROR || log.level === LogLevel.FATAL) { stats.errors++; } }); return stats; } } // 导出单例 export const logger = new Logger();