Add logging functionality and new log-related endpoints
- Integrated a logging service to redirect console outputs. - Added new endpoints for managing logs: - Retrieve log file list - Read specific log file content - Fetch recent logs - Stream logs via Server-Sent Events - Search logs by keyword - Cleanup old log files - Updated application metadata to include new log management features.
This commit is contained in:
		
							parent
							
								
									76683fb519
								
							
						
					
					
						commit
						f96a69e846
					
				
							
								
								
									
										367
									
								
								public/js/logs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								public/js/logs.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,367 @@
 | 
			
		||||
// 实时日志查看页面JavaScript
 | 
			
		||||
 | 
			
		||||
class LogViewer {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.eventSource = null;
 | 
			
		||||
        this.logs = [];
 | 
			
		||||
        this.maxLogs = 1000; // 最多保留1000条日志
 | 
			
		||||
        this.autoScroll = true;
 | 
			
		||||
        this.currentFilter = {
 | 
			
		||||
            level: '',
 | 
			
		||||
            keyword: ''
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        this.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async init() {
 | 
			
		||||
        console.log('初始化日志查看器...');
 | 
			
		||||
        
 | 
			
		||||
        // 绑定事件
 | 
			
		||||
        this.bindEvents();
 | 
			
		||||
        
 | 
			
		||||
        // 加载日志文件列表
 | 
			
		||||
        await this.loadLogFiles();
 | 
			
		||||
        
 | 
			
		||||
        // 加载最近日志
 | 
			
		||||
        await this.loadRecentLogs();
 | 
			
		||||
        
 | 
			
		||||
        console.log('日志查看器初始化完成');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bindEvents() {
 | 
			
		||||
        // 开始/停止实时日志流
 | 
			
		||||
        document.getElementById('startStreamBtn').addEventListener('click', () => {
 | 
			
		||||
            this.startStream();
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        document.getElementById('stopStreamBtn').addEventListener('click', () => {
 | 
			
		||||
            this.stopStream();
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 清空日志
 | 
			
		||||
        document.getElementById('clearLogsBtn').addEventListener('click', () => {
 | 
			
		||||
            this.clearLogs();
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 自动滚动切换
 | 
			
		||||
        document.getElementById('autoScrollCheck').addEventListener('change', (e) => {
 | 
			
		||||
            this.autoScroll = e.target.checked;
 | 
			
		||||
            if (this.autoScroll) {
 | 
			
		||||
                this.scrollToBottom();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 日志级别过滤
 | 
			
		||||
        document.getElementById('logLevelFilter').addEventListener('change', (e) => {
 | 
			
		||||
            this.currentFilter.level = e.target.value;
 | 
			
		||||
            this.renderLogs();
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 搜索
 | 
			
		||||
        document.getElementById('searchBtn').addEventListener('click', () => {
 | 
			
		||||
            this.searchLogs();
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        document.getElementById('searchKeyword').addEventListener('keypress', (e) => {
 | 
			
		||||
            if (e.key === 'Enter') {
 | 
			
		||||
                this.searchLogs();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 日志文件选择
 | 
			
		||||
        document.getElementById('logFileSelect').addEventListener('change', async (e) => {
 | 
			
		||||
            const filename = e.target.value;
 | 
			
		||||
            if (filename) {
 | 
			
		||||
                await this.loadLogFile(filename);
 | 
			
		||||
            } else {
 | 
			
		||||
                await this.loadRecentLogs();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadLogFiles() {
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch('/api/history/logs/files?days=7');
 | 
			
		||||
            const result = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                const select = document.getElementById('logFileSelect');
 | 
			
		||||
                // 保留"今日日志"选项
 | 
			
		||||
                select.innerHTML = '<option value="">今日日志</option>';
 | 
			
		||||
                
 | 
			
		||||
                result.data.forEach(file => {
 | 
			
		||||
                    const option = document.createElement('option');
 | 
			
		||||
                    option.value = file.filename;
 | 
			
		||||
                    option.textContent = `${file.date} (${this.formatFileSize(file.size)})`;
 | 
			
		||||
                    select.appendChild(option);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('加载日志文件列表失败:', error);
 | 
			
		||||
            this.showAlert('加载日志文件列表失败', 'danger');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadRecentLogs() {
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch('/api/history/logs/recent?limit=100');
 | 
			
		||||
            const result = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                this.logs = result.data.map(log => ({
 | 
			
		||||
                    timestamp: log.timestamp,
 | 
			
		||||
                    level: log.level.toUpperCase(),
 | 
			
		||||
                    message: log.message
 | 
			
		||||
                }));
 | 
			
		||||
                this.renderLogs();
 | 
			
		||||
                this.updateLogCount();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('加载最近日志失败:', error);
 | 
			
		||||
            this.showAlert('加载日志失败', 'danger');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadLogFile(filename) {
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch(`/api/history/logs/file/${filename}?limit=500`);
 | 
			
		||||
            const result = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                this.logs = result.data.map(log => ({
 | 
			
		||||
                    timestamp: log.timestamp,
 | 
			
		||||
                    level: log.level.toUpperCase(),
 | 
			
		||||
                    message: log.message
 | 
			
		||||
                }));
 | 
			
		||||
                this.renderLogs();
 | 
			
		||||
                this.updateLogCount();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('加载日志文件失败:', error);
 | 
			
		||||
            this.showAlert('加载日志文件失败', 'danger');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    startStream() {
 | 
			
		||||
        if (this.eventSource) {
 | 
			
		||||
            return; // 已经在运行
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            this.eventSource = new EventSource('/api/history/logs/stream');
 | 
			
		||||
            
 | 
			
		||||
            this.eventSource.onopen = () => {
 | 
			
		||||
                this.updateConnectionStatus(true);
 | 
			
		||||
                document.getElementById('startStreamBtn').disabled = true;
 | 
			
		||||
                document.getElementById('stopStreamBtn').disabled = false;
 | 
			
		||||
                this.showAlert('实时日志流已连接', 'success');
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            this.eventSource.onmessage = (event) => {
 | 
			
		||||
                try {
 | 
			
		||||
                    const data = JSON.parse(event.data);
 | 
			
		||||
                    
 | 
			
		||||
                    if (data.type === 'connected') {
 | 
			
		||||
                        console.log('实时日志流连接成功');
 | 
			
		||||
                    } else if (data.type === 'log') {
 | 
			
		||||
                        this.addLog(data.data);
 | 
			
		||||
                    } else if (data.type === 'heartbeat') {
 | 
			
		||||
                        // 心跳包,保持连接
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('解析日志数据失败:', error);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            this.eventSource.onerror = (error) => {
 | 
			
		||||
                console.error('实时日志流错误:', error);
 | 
			
		||||
                this.updateConnectionStatus(false);
 | 
			
		||||
                this.stopStream();
 | 
			
		||||
                this.showAlert('实时日志流连接断开', 'warning');
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('启动实时日志流失败:', error);
 | 
			
		||||
            this.showAlert('启动实时日志流失败: ' + error.message, 'danger');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stopStream() {
 | 
			
		||||
        if (this.eventSource) {
 | 
			
		||||
            this.eventSource.close();
 | 
			
		||||
            this.eventSource = null;
 | 
			
		||||
            
 | 
			
		||||
            this.updateConnectionStatus(false);
 | 
			
		||||
            document.getElementById('startStreamBtn').disabled = false;
 | 
			
		||||
            document.getElementById('stopStreamBtn').disabled = true;
 | 
			
		||||
            
 | 
			
		||||
            this.showAlert('实时日志流已停止', 'info');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addLog(logEntry) {
 | 
			
		||||
        // 添加新日志
 | 
			
		||||
        this.logs.push({
 | 
			
		||||
            timestamp: logEntry.timestamp,
 | 
			
		||||
            level: logEntry.level ? logEntry.level.toUpperCase() : 'INFO',
 | 
			
		||||
            message: logEntry.message
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 限制日志数量
 | 
			
		||||
        if (this.logs.length > this.maxLogs) {
 | 
			
		||||
            this.logs.shift();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // 更新显示
 | 
			
		||||
        this.renderLogs();
 | 
			
		||||
        this.updateLogCount();
 | 
			
		||||
        
 | 
			
		||||
        // 自动滚动
 | 
			
		||||
        if (this.autoScroll) {
 | 
			
		||||
            setTimeout(() => this.scrollToBottom(), 10);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderLogs() {
 | 
			
		||||
        const container = document.getElementById('logContainer');
 | 
			
		||||
        
 | 
			
		||||
        // 过滤日志
 | 
			
		||||
        let filteredLogs = this.logs;
 | 
			
		||||
        
 | 
			
		||||
        if (this.currentFilter.level) {
 | 
			
		||||
            filteredLogs = filteredLogs.filter(log => log.level === this.currentFilter.level);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if (this.currentFilter.keyword) {
 | 
			
		||||
            const keyword = this.currentFilter.keyword.toLowerCase();
 | 
			
		||||
            filteredLogs = filteredLogs.filter(log => 
 | 
			
		||||
                log.message.toLowerCase().includes(keyword) ||
 | 
			
		||||
                log.timestamp.toLowerCase().includes(keyword)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // 渲染日志
 | 
			
		||||
        container.innerHTML = filteredLogs.map(log => {
 | 
			
		||||
            const levelClass = log.level || 'INFO';
 | 
			
		||||
            const messageClass = log.level ? log.level.toLowerCase() : 'info';
 | 
			
		||||
            
 | 
			
		||||
            return `
 | 
			
		||||
                <div class="log-entry ${messageClass}">
 | 
			
		||||
                    <span class="log-timestamp">${log.timestamp || ''}</span>
 | 
			
		||||
                    <span class="log-level ${levelClass}">${levelClass}</span>
 | 
			
		||||
                    <span class="log-message">${this.escapeHtml(log.message)}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            `;
 | 
			
		||||
        }).join('');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clearLogs() {
 | 
			
		||||
        if (confirm('确定要清空所有日志吗?')) {
 | 
			
		||||
            this.logs = [];
 | 
			
		||||
            this.renderLogs();
 | 
			
		||||
            this.updateLogCount();
 | 
			
		||||
            this.showAlert('日志已清空', 'info');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    searchLogs() {
 | 
			
		||||
        const keyword = document.getElementById('searchKeyword').value.trim();
 | 
			
		||||
        this.currentFilter.keyword = keyword;
 | 
			
		||||
        this.renderLogs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    scrollToBottom() {
 | 
			
		||||
        const container = document.getElementById('logContainer');
 | 
			
		||||
        container.scrollTop = container.scrollHeight;
 | 
			
		||||
        
 | 
			
		||||
        // 显示自动滚动指示器
 | 
			
		||||
        const indicator = document.getElementById('autoScrollIndicator');
 | 
			
		||||
        if (this.autoScroll) {
 | 
			
		||||
            indicator.classList.add('active');
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                indicator.classList.remove('active');
 | 
			
		||||
            }, 2000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateLogCount() {
 | 
			
		||||
        const count = this.logs.length;
 | 
			
		||||
        document.getElementById('logCount').textContent = count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateConnectionStatus(connected) {
 | 
			
		||||
        const indicator = document.getElementById('connectionStatus');
 | 
			
		||||
        const text = document.getElementById('connectionStatusText');
 | 
			
		||||
        
 | 
			
		||||
        if (connected) {
 | 
			
		||||
            indicator.className = 'status-indicator online';
 | 
			
		||||
            text.textContent = '已连接';
 | 
			
		||||
        } else {
 | 
			
		||||
            indicator.className = 'status-indicator offline';
 | 
			
		||||
            text.textContent = '未连接';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    formatFileSize(bytes) {
 | 
			
		||||
        if (bytes < 1024) return bytes + ' B';
 | 
			
		||||
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
 | 
			
		||||
        return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    escapeHtml(text) {
 | 
			
		||||
        const div = document.createElement('div');
 | 
			
		||||
        div.textContent = text;
 | 
			
		||||
        return div.innerHTML;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showAlert(message, type = 'info') {
 | 
			
		||||
        const alertContainer = document.getElementById('alertContainer');
 | 
			
		||||
        if (!alertContainer) return;
 | 
			
		||||
 | 
			
		||||
        const alertId = 'alert-' + Date.now();
 | 
			
		||||
        const alertHtml = `
 | 
			
		||||
            <div class="alert alert-${type} alert-dismissible fade show" role="alert" id="${alertId}">
 | 
			
		||||
                <i class="bi bi-${this.getAlertIcon(type)}"></i>
 | 
			
		||||
                ${message}
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        alertContainer.insertAdjacentHTML('beforeend', alertHtml);
 | 
			
		||||
 | 
			
		||||
        // 自动移除提示
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            const alertElement = document.getElementById(alertId);
 | 
			
		||||
            if (alertElement) {
 | 
			
		||||
                alertElement.remove();
 | 
			
		||||
            }
 | 
			
		||||
        }, 3000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAlertIcon(type) {
 | 
			
		||||
        const icons = {
 | 
			
		||||
            'success': 'check-circle-fill',
 | 
			
		||||
            'danger': 'exclamation-triangle-fill',
 | 
			
		||||
            'warning': 'exclamation-triangle-fill',
 | 
			
		||||
            'info': 'info-circle-fill'
 | 
			
		||||
        };
 | 
			
		||||
        return icons[type] || 'info-circle-fill';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 全局变量
 | 
			
		||||
let logViewer;
 | 
			
		||||
 | 
			
		||||
// 页面加载完成后初始化
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    logViewer = new LogViewer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 页面卸载时清理
 | 
			
		||||
window.addEventListener('beforeunload', () => {
 | 
			
		||||
    if (logViewer) {
 | 
			
		||||
        logViewer.stopStream();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										234
									
								
								public/logs.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								public/logs.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,234 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>实时日志查看 - 代理IP管理系统</title>
 | 
			
		||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
			
		||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
 | 
			
		||||
    <link href="css/dashboard.css" rel="stylesheet">
 | 
			
		||||
    <style>
 | 
			
		||||
        #logContainer {
 | 
			
		||||
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
 | 
			
		||||
            font-size: 13px;
 | 
			
		||||
            line-height: 1.6;
 | 
			
		||||
            max-height: calc(100vh - 300px);
 | 
			
		||||
            overflow-y: auto;
 | 
			
		||||
            background: #1e1e1e;
 | 
			
		||||
            color: #d4d4d4;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-entry {
 | 
			
		||||
            margin-bottom: 4px;
 | 
			
		||||
            padding: 2px 0;
 | 
			
		||||
            word-wrap: break-word;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-timestamp {
 | 
			
		||||
            color: #858585;
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-level {
 | 
			
		||||
            padding: 2px 6px;
 | 
			
		||||
            border-radius: 3px;
 | 
			
		||||
            font-size: 11px;
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-level.ERROR {
 | 
			
		||||
            background: #f14c4c;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-level.WARN {
 | 
			
		||||
            background: #cca700;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-level.INFO {
 | 
			
		||||
            background: #007acc;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-level.DEBUG {
 | 
			
		||||
            background: #808080;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-message {
 | 
			
		||||
            color: #d4d4d4;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-entry.error .log-message {
 | 
			
		||||
            color: #f48771;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-entry.warn .log-message {
 | 
			
		||||
            color: #dcdcaa;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-entry.info .log-message {
 | 
			
		||||
            color: #4ec9b0;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .log-entry.debug .log-message {
 | 
			
		||||
            color: #808080;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .auto-scroll-indicator {
 | 
			
		||||
            position: fixed;
 | 
			
		||||
            bottom: 20px;
 | 
			
		||||
            right: 20px;
 | 
			
		||||
            background: rgba(0, 0, 0, 0.8);
 | 
			
		||||
            color: white;
 | 
			
		||||
            padding: 10px 15px;
 | 
			
		||||
            border-radius: 20px;
 | 
			
		||||
            font-size: 12px;
 | 
			
		||||
            z-index: 1000;
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .auto-scroll-indicator.active {
 | 
			
		||||
            display: block;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <!-- 导航栏 -->
 | 
			
		||||
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
 | 
			
		||||
        <div class="container-fluid">
 | 
			
		||||
            <a class="navbar-brand" href="index.html">
 | 
			
		||||
                <i class="bi bi-shield-check"></i>
 | 
			
		||||
                代理IP管理系统
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
 | 
			
		||||
                <span class="navbar-toggler-icon"></span>
 | 
			
		||||
            </button>
 | 
			
		||||
 | 
			
		||||
            <div class="collapse navbar-collapse" id="navbarNav">
 | 
			
		||||
                <ul class="navbar-nav me-auto">
 | 
			
		||||
                    <li class="nav-item">
 | 
			
		||||
                        <a class="nav-link" href="index.html">
 | 
			
		||||
                            <i class="bi bi-speedometer2"></i> 仪表板
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="nav-item">
 | 
			
		||||
                        <a class="nav-link" href="proxies.html">
 | 
			
		||||
                            <i class="bi bi-list-ul"></i> 代理管理
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="nav-item">
 | 
			
		||||
                        <a class="nav-link" href="history.html">
 | 
			
		||||
                            <i class="bi bi-clock-history"></i> 执行历史
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="nav-item">
 | 
			
		||||
                        <a class="nav-link" href="monitoring.html">
 | 
			
		||||
                            <i class="bi bi-activity"></i> 系统监控
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="nav-item">
 | 
			
		||||
                        <a class="nav-link active" href="logs.html">
 | 
			
		||||
                            <i class="bi bi-journal-text"></i> 实时日志
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
 | 
			
		||||
                <div class="navbar-nav">
 | 
			
		||||
                    <div class="nav-link">
 | 
			
		||||
                        <span class="status-indicator" id="connectionStatus"></span>
 | 
			
		||||
                        <span id="connectionStatusText">连接中...</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
 | 
			
		||||
    <!-- 主要内容 -->
 | 
			
		||||
    <div class="container-fluid mt-4">
 | 
			
		||||
        <!-- 控制面板 -->
 | 
			
		||||
        <div class="card mb-3">
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                <div class="row align-items-center">
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <div class="btn-group" role="group">
 | 
			
		||||
                            <button type="button" class="btn btn-primary" id="startStreamBtn">
 | 
			
		||||
                                <i class="bi bi-play-fill"></i> 开始实时日志
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <button type="button" class="btn btn-secondary" id="stopStreamBtn" disabled>
 | 
			
		||||
                                <i class="bi bi-stop-fill"></i> 停止
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <button type="button" class="btn btn-outline-secondary" id="clearLogsBtn">
 | 
			
		||||
                                <i class="bi bi-x-circle"></i> 清空
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-check form-switch mt-2">
 | 
			
		||||
                            <input class="form-check-input" type="checkbox" id="autoScrollCheck" checked>
 | 
			
		||||
                            <label class="form-check-label" for="autoScrollCheck">
 | 
			
		||||
                                自动滚动
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-6 text-end">
 | 
			
		||||
                        <div class="input-group" style="max-width: 400px; margin-left: auto;">
 | 
			
		||||
                            <input type="text" class="form-control" id="searchKeyword" placeholder="搜索日志...">
 | 
			
		||||
                            <button class="btn btn-outline-secondary" type="button" id="searchBtn">
 | 
			
		||||
                                <i class="bi bi-search"></i> 搜索
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="mt-2">
 | 
			
		||||
                            <select class="form-select form-select-sm d-inline-block" id="logLevelFilter" style="width: auto;">
 | 
			
		||||
                                <option value="">所有级别</option>
 | 
			
		||||
                                <option value="ERROR">ERROR</option>
 | 
			
		||||
                                <option value="WARN">WARN</option>
 | 
			
		||||
                                <option value="INFO">INFO</option>
 | 
			
		||||
                                <option value="DEBUG">DEBUG</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                            <span class="ms-2 text-muted small">
 | 
			
		||||
                                日志数: <span id="logCount">0</span>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- 日志显示区域 -->
 | 
			
		||||
        <div class="card">
 | 
			
		||||
            <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                <h6 class="mb-0">
 | 
			
		||||
                    <i class="bi bi-terminal"></i> 实时日志流
 | 
			
		||||
                </h6>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <select class="form-select form-select-sm d-inline-block" id="logFileSelect" style="width: auto;">
 | 
			
		||||
                        <option value="">今日日志</option>
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body p-0">
 | 
			
		||||
                <div id="logContainer"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 自动滚动指示器 -->
 | 
			
		||||
    <div class="auto-scroll-indicator" id="autoScrollIndicator">
 | 
			
		||||
        <i class="bi bi-arrow-down-circle"></i> 自动滚动中...
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 提示消息容器 -->
 | 
			
		||||
    <div class="position-fixed top-0 end-0 p-3" style="z-index: 1050">
 | 
			
		||||
        <div id="alertContainer"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- JavaScript -->
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
    <script src="js/logs.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								src/app.js
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/app.js
									
									
									
									
									
								
							@ -1,3 +1,6 @@
 | 
			
		||||
// 首先加载日志服务,这样所有console输出都会被重定向
 | 
			
		||||
const logger = require('./services/logger');
 | 
			
		||||
 | 
			
		||||
const express = require('express');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const Database = require('./database/db');
 | 
			
		||||
@ -76,11 +79,12 @@ class ProxyApp {
 | 
			
		||||
        name: 'Proxy IP Service',
 | 
			
		||||
        version: '1.0.0',
 | 
			
		||||
        description: '代理IP抓取、验证和管理服务',
 | 
			
		||||
        web_interface: {
 | 
			
		||||
          web_interface: {
 | 
			
		||||
          dashboard: '/index.html',
 | 
			
		||||
          proxies: '/proxies.html',
 | 
			
		||||
          history: '/history.html',
 | 
			
		||||
          monitoring: '/monitoring.html'
 | 
			
		||||
          monitoring: '/monitoring.html',
 | 
			
		||||
          logs: '/logs.html'
 | 
			
		||||
        },
 | 
			
		||||
        endpoints: {
 | 
			
		||||
          'GET /api/health': '健康检查',
 | 
			
		||||
@ -100,6 +104,10 @@ class ProxyApp {
 | 
			
		||||
          'DELETE /api/proxies/cleanup': '清理无效代理',
 | 
			
		||||
          'GET /api/history': '执行历史',
 | 
			
		||||
          'GET /api/history/logs/system': '系统日志',
 | 
			
		||||
          'GET /api/history/logs/files': '获取日志文件列表',
 | 
			
		||||
          'GET /api/history/logs/file/:filename': '读取日志文件',
 | 
			
		||||
          'GET /api/history/logs/recent': '获取最近日志',
 | 
			
		||||
          'GET /api/history/logs/stream': '实时日志流(SSE)',
 | 
			
		||||
          'GET /api/history/stats': '历史统计'
 | 
			
		||||
        },
 | 
			
		||||
        scheduler: {
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ const express = require('express');
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
const HistoryModel = require('../database/models/history');
 | 
			
		||||
const LogsModel = require('../database/models/logs');
 | 
			
		||||
const logger = require('../services/logger');
 | 
			
		||||
 | 
			
		||||
// 获取执行历史列表
 | 
			
		||||
router.get('/', async (req, res) => {
 | 
			
		||||
@ -334,4 +335,218 @@ router.get('/export', async (req, res) => {
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// ==================== 文件日志相关接口 ====================
 | 
			
		||||
 | 
			
		||||
// 获取日志文件列表
 | 
			
		||||
router.get('/logs/files', (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const days = parseInt(req.query.days) || 7;
 | 
			
		||||
    const files = logger.getLogFiles(days);
 | 
			
		||||
    
 | 
			
		||||
    res.json({
 | 
			
		||||
      success: true,
 | 
			
		||||
      data: files
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取日志文件列表失败:', error);
 | 
			
		||||
    res.status(500).json({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: '获取日志文件列表失败',
 | 
			
		||||
      message: error.message
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 读取日志文件内容
 | 
			
		||||
router.get('/logs/file/:filename', (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const filename = req.params.filename;
 | 
			
		||||
    const limit = parseInt(req.query.limit) || 1000;
 | 
			
		||||
    const offset = parseInt(req.query.offset) || 0;
 | 
			
		||||
    
 | 
			
		||||
    // 安全检查:确保文件名只包含允许的字符
 | 
			
		||||
    if (!/^app-\d{4}-\d{2}-\d{2}\.log$/.test(filename)) {
 | 
			
		||||
      return res.status(400).json({
 | 
			
		||||
        success: false,
 | 
			
		||||
        error: '无效的日志文件名格式'
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const result = logger.readLogFile(filename, limit, offset);
 | 
			
		||||
    
 | 
			
		||||
    res.json({
 | 
			
		||||
      success: true,
 | 
			
		||||
      data: result.logs,
 | 
			
		||||
      pagination: {
 | 
			
		||||
        total: result.total,
 | 
			
		||||
        limit: result.limit,
 | 
			
		||||
        offset: result.offset,
 | 
			
		||||
        hasMore: result.offset + result.limit < result.total
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('读取日志文件失败:', error);
 | 
			
		||||
    res.status(500).json({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: '读取日志文件失败',
 | 
			
		||||
      message: error.message
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 获取最近的实时日志
 | 
			
		||||
router.get('/logs/recent', (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const limit = Math.min(parseInt(req.query.limit) || 100, 500);
 | 
			
		||||
    const logs = logger.getRecentLogs(limit);
 | 
			
		||||
    
 | 
			
		||||
    res.json({
 | 
			
		||||
      success: true,
 | 
			
		||||
      data: logs,
 | 
			
		||||
      count: logs.length
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取最近日志失败:', error);
 | 
			
		||||
    res.status(500).json({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: '获取最近日志失败',
 | 
			
		||||
      message: error.message
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 实时日志流(Server-Sent Events)
 | 
			
		||||
router.get('/logs/stream', (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 设置SSE响应头
 | 
			
		||||
    res.setHeader('Content-Type', 'text/event-stream');
 | 
			
		||||
    res.setHeader('Cache-Control', 'no-cache');
 | 
			
		||||
    res.setHeader('Connection', 'keep-alive');
 | 
			
		||||
    res.setHeader('X-Accel-Buffering', 'no'); // 禁用Nginx缓冲
 | 
			
		||||
    
 | 
			
		||||
    // 发送初始连接确认
 | 
			
		||||
    res.write('data: {"type":"connected","message":"实时日志流已连接"}\n\n');
 | 
			
		||||
    
 | 
			
		||||
    // 设置日志事件监听器
 | 
			
		||||
    const emitLog = (logEntry) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const data = JSON.stringify({
 | 
			
		||||
          type: 'log',
 | 
			
		||||
          data: logEntry
 | 
			
		||||
        });
 | 
			
		||||
        res.write(`data: ${data}\n\n`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('发送日志事件失败:', error);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    logger.addLogEmitter(emitLog);
 | 
			
		||||
    
 | 
			
		||||
    // 发送最近的日志
 | 
			
		||||
    const recentLogs = logger.getRecentLogs(50);
 | 
			
		||||
    recentLogs.forEach(logEntry => {
 | 
			
		||||
      const data = JSON.stringify({
 | 
			
		||||
        type: 'log',
 | 
			
		||||
        data: logEntry
 | 
			
		||||
      });
 | 
			
		||||
      res.write(`data: ${data}\n\n`);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // 保持连接:定期发送心跳
 | 
			
		||||
    const heartbeat = setInterval(() => {
 | 
			
		||||
      try {
 | 
			
		||||
        res.write('data: {"type":"heartbeat","time":' + Date.now() + '}\n\n');
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        clearInterval(heartbeat);
 | 
			
		||||
      }
 | 
			
		||||
    }, 30000); // 每30秒发送一次心跳
 | 
			
		||||
    
 | 
			
		||||
    // 客户端断开连接时清理
 | 
			
		||||
    req.on('close', () => {
 | 
			
		||||
      clearInterval(heartbeat);
 | 
			
		||||
      logger.removeLogEmitter(emitLog);
 | 
			
		||||
      res.end();
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // 防止连接被意外关闭
 | 
			
		||||
    req.on('error', () => {
 | 
			
		||||
      clearInterval(heartbeat);
 | 
			
		||||
      logger.removeLogEmitter(emitLog);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('建立日志流失败:', error);
 | 
			
		||||
    res.status(500).json({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: '建立日志流失败',
 | 
			
		||||
      message: error.message
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 搜索文件日志
 | 
			
		||||
router.get('/logs/search-file', (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const keyword = req.query.keyword;
 | 
			
		||||
    const date = req.query.date || null;
 | 
			
		||||
    const limit = Math.min(parseInt(req.query.limit) || 500, 1000);
 | 
			
		||||
    
 | 
			
		||||
    if (!keyword || keyword.trim().length === 0) {
 | 
			
		||||
      return res.status(400).json({
 | 
			
		||||
        success: false,
 | 
			
		||||
        error: '搜索关键词不能为空'
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const result = logger.searchLogs(keyword.trim(), date, limit);
 | 
			
		||||
    
 | 
			
		||||
    res.json({
 | 
			
		||||
      success: true,
 | 
			
		||||
      data: result.logs,
 | 
			
		||||
      count: result.total,
 | 
			
		||||
      keyword: keyword.trim(),
 | 
			
		||||
      date: date
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('搜索文件日志失败:', error);
 | 
			
		||||
    res.status(500).json({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: '搜索文件日志失败',
 | 
			
		||||
      message: error.message
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 清理旧日志文件
 | 
			
		||||
router.delete('/logs/cleanup-files', (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const daysToKeep = parseInt(req.body.days) || 30;
 | 
			
		||||
    
 | 
			
		||||
    if (daysToKeep < 1 || daysToKeep > 365) {
 | 
			
		||||
      return res.status(400).json({
 | 
			
		||||
        success: false,
 | 
			
		||||
        error: '保留天数必须在1-365之间'
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const deletedCount = logger.cleanupOldLogs(daysToKeep);
 | 
			
		||||
    
 | 
			
		||||
    res.json({
 | 
			
		||||
      success: true,
 | 
			
		||||
      message: `清理完成,删除了 ${deletedCount} 个旧日志文件`,
 | 
			
		||||
      data: {
 | 
			
		||||
        deleted_count: deletedCount,
 | 
			
		||||
        days_to_keep: daysToKeep
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('清理旧日志文件失败:', error);
 | 
			
		||||
    res.status(500).json({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: '清理旧日志文件失败',
 | 
			
		||||
      message: error.message
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
							
								
								
									
										353
									
								
								src/services/logger.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								src/services/logger.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,353 @@
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
class Logger {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.logsDir = path.join(__dirname, '../../logs');
 | 
			
		||||
    this.currentLogFile = null;
 | 
			
		||||
    this.currentDate = null;
 | 
			
		||||
    this.originalConsole = {
 | 
			
		||||
      log: console.log,
 | 
			
		||||
      error: console.error,
 | 
			
		||||
      warn: console.warn,
 | 
			
		||||
      info: console.info,
 | 
			
		||||
      debug: console.debug
 | 
			
		||||
    };
 | 
			
		||||
    this.logBuffer = []; // 用于实时日志推送
 | 
			
		||||
    this.maxBufferSize = 1000; // 最多缓存1000条日志
 | 
			
		||||
    this.logEmitters = new Set(); // 支持多个日志监听器
 | 
			
		||||
    
 | 
			
		||||
    // 确保logs目录存在
 | 
			
		||||
    this.ensureLogsDirectory();
 | 
			
		||||
    
 | 
			
		||||
    // 初始化今天的日志文件
 | 
			
		||||
    this.initializeTodayLog();
 | 
			
		||||
    
 | 
			
		||||
    // 重定向console
 | 
			
		||||
    this.redirectConsole();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ensureLogsDirectory() {
 | 
			
		||||
    if (!fs.existsSync(this.logsDir)) {
 | 
			
		||||
      fs.mkdirSync(this.logsDir, { recursive: true });
 | 
			
		||||
      // 使用原始console,避免循环调用
 | 
			
		||||
      this.originalConsole.log(`创建日志目录: ${this.logsDir}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getLogFileName(date = null) {
 | 
			
		||||
    const logDate = date || new Date();
 | 
			
		||||
    const dateStr = logDate.toISOString().slice(0, 10); // YYYY-MM-DD
 | 
			
		||||
    return path.join(this.logsDir, `app-${dateStr}.log`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initializeTodayLog() {
 | 
			
		||||
    const today = new Date().toISOString().slice(0, 10);
 | 
			
		||||
    
 | 
			
		||||
    if (this.currentDate !== today) {
 | 
			
		||||
      this.currentDate = today;
 | 
			
		||||
      this.currentLogFile = this.getLogFileName();
 | 
			
		||||
      
 | 
			
		||||
      // 确保文件存在
 | 
			
		||||
      if (!fs.existsSync(this.currentLogFile)) {
 | 
			
		||||
        fs.writeFileSync(this.currentLogFile, '');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  formatTimestamp() {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    return now.toISOString().replace('T', ' ').slice(0, 19);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  writeToFile(level, message) {
 | 
			
		||||
    try {
 | 
			
		||||
      // 检查是否需要切换到新的一天的日志文件
 | 
			
		||||
      const today = new Date().toISOString().slice(0, 10);
 | 
			
		||||
      if (this.currentDate !== today) {
 | 
			
		||||
        this.initializeTodayLog();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const timestamp = this.formatTimestamp();
 | 
			
		||||
      const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
 | 
			
		||||
      
 | 
			
		||||
      // 写入文件
 | 
			
		||||
      fs.appendFileSync(this.currentLogFile, logLine, 'utf8');
 | 
			
		||||
      
 | 
			
		||||
      // 添加到缓冲区用于实时推送
 | 
			
		||||
      const logEntry = {
 | 
			
		||||
        timestamp,
 | 
			
		||||
        level,
 | 
			
		||||
        message,
 | 
			
		||||
        time: Date.now()
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      this.logBuffer.push(logEntry);
 | 
			
		||||
      
 | 
			
		||||
      // 限制缓冲区大小
 | 
			
		||||
      if (this.logBuffer.length > this.maxBufferSize) {
 | 
			
		||||
        this.logBuffer.shift();
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // 触发实时日志事件(如果有监听器)
 | 
			
		||||
      if (this.logEmitters.size > 0) {
 | 
			
		||||
        this.logEmitters.forEach(emitFn => {
 | 
			
		||||
          try {
 | 
			
		||||
            emitFn(logEntry);
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            // 如果某个监听器出错,移除它
 | 
			
		||||
            this.logEmitters.delete(emitFn);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // 如果文件写入失败,至少输出到原始console
 | 
			
		||||
      this.originalConsole.error('写入日志文件失败:', error);
 | 
			
		||||
      this.originalConsole[level](message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  redirectConsole() {
 | 
			
		||||
    // 重定向 console.log
 | 
			
		||||
    console.log = (...args) => {
 | 
			
		||||
      const message = args.map(arg => 
 | 
			
		||||
        typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
 | 
			
		||||
      ).join(' ');
 | 
			
		||||
      
 | 
			
		||||
      this.originalConsole.log(...args);
 | 
			
		||||
      this.writeToFile('info', message);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 重定向 console.error
 | 
			
		||||
    console.error = (...args) => {
 | 
			
		||||
      const message = args.map(arg => 
 | 
			
		||||
        typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
 | 
			
		||||
      ).join(' ');
 | 
			
		||||
      
 | 
			
		||||
      this.originalConsole.error(...args);
 | 
			
		||||
      this.writeToFile('error', message);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 重定向 console.warn
 | 
			
		||||
    console.warn = (...args) => {
 | 
			
		||||
      const message = args.map(arg => 
 | 
			
		||||
        typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
 | 
			
		||||
      ).join(' ');
 | 
			
		||||
      
 | 
			
		||||
      this.originalConsole.warn(...args);
 | 
			
		||||
      this.writeToFile('warn', message);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 重定向 console.info
 | 
			
		||||
    console.info = (...args) => {
 | 
			
		||||
      const message = args.map(arg => 
 | 
			
		||||
        typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
 | 
			
		||||
      ).join(' ');
 | 
			
		||||
      
 | 
			
		||||
      this.originalConsole.info(...args);
 | 
			
		||||
      this.writeToFile('info', message);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 重定向 console.debug(如果有的话)
 | 
			
		||||
    if (console.debug) {
 | 
			
		||||
      console.debug = (...args) => {
 | 
			
		||||
        const message = args.map(arg => 
 | 
			
		||||
          typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
 | 
			
		||||
        ).join(' ');
 | 
			
		||||
        
 | 
			
		||||
        this.originalConsole.debug(...args);
 | 
			
		||||
        this.writeToFile('debug', message);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取日志文件列表
 | 
			
		||||
  getLogFiles(days = 7) {
 | 
			
		||||
    try {
 | 
			
		||||
      const files = fs.readdirSync(this.logsDir);
 | 
			
		||||
      const logFiles = files
 | 
			
		||||
        .filter(file => file.startsWith('app-') && file.endsWith('.log'))
 | 
			
		||||
        .map(file => {
 | 
			
		||||
          const filePath = path.join(this.logsDir, file);
 | 
			
		||||
          const stats = fs.statSync(filePath);
 | 
			
		||||
          return {
 | 
			
		||||
            filename: file,
 | 
			
		||||
            date: file.replace('app-', '').replace('.log', ''),
 | 
			
		||||
            size: stats.size,
 | 
			
		||||
            modified: stats.mtime.toISOString()
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
        .sort((a, b) => b.date.localeCompare(a.date))
 | 
			
		||||
        .slice(0, days);
 | 
			
		||||
      
 | 
			
		||||
      return logFiles;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('获取日志文件列表失败:', error);
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 读取日志文件内容
 | 
			
		||||
  readLogFile(filename, limit = 1000, offset = 0) {
 | 
			
		||||
    try {
 | 
			
		||||
      const filePath = path.join(this.logsDir, filename);
 | 
			
		||||
      
 | 
			
		||||
      if (!fs.existsSync(filePath)) {
 | 
			
		||||
        throw new Error('日志文件不存在');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // 读取文件内容
 | 
			
		||||
      const content = fs.readFileSync(filePath, 'utf8');
 | 
			
		||||
      const lines = content.split('\n').filter(line => line.trim());
 | 
			
		||||
      
 | 
			
		||||
      // 反转数组以获取最新的日志(最后的行)
 | 
			
		||||
      const reversedLines = lines.reverse();
 | 
			
		||||
      
 | 
			
		||||
      // 分页
 | 
			
		||||
      const paginatedLines = reversedLines.slice(offset, offset + limit);
 | 
			
		||||
      
 | 
			
		||||
      // 解析日志行
 | 
			
		||||
      const logs = paginatedLines
 | 
			
		||||
        .map(line => {
 | 
			
		||||
          // 解析格式: [YYYY-MM-DD HH:mm:ss] [LEVEL] message
 | 
			
		||||
          const match = line.match(/^\[([^\]]+)\] \[([^\]]+)\] (.+)$/);
 | 
			
		||||
          if (match) {
 | 
			
		||||
            return {
 | 
			
		||||
              timestamp: match[1],
 | 
			
		||||
              level: match[2].toLowerCase(),
 | 
			
		||||
              message: match[3],
 | 
			
		||||
              raw: line
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            timestamp: null,
 | 
			
		||||
            level: 'info',
 | 
			
		||||
            message: line,
 | 
			
		||||
            raw: line
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
        .reverse(); // 再次反转,以时间正序显示
 | 
			
		||||
      
 | 
			
		||||
      return {
 | 
			
		||||
        logs,
 | 
			
		||||
        total: lines.length,
 | 
			
		||||
        limit,
 | 
			
		||||
        offset
 | 
			
		||||
      };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('读取日志文件失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取实时日志流(从缓冲区)
 | 
			
		||||
  getRecentLogs(limit = 100) {
 | 
			
		||||
    return this.logBuffer.slice(-limit);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 添加日志事件监听器(用于SSE)
 | 
			
		||||
  addLogEmitter(emitFn) {
 | 
			
		||||
    if (emitFn && typeof emitFn === 'function') {
 | 
			
		||||
      this.logEmitters.add(emitFn);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 移除日志事件监听器
 | 
			
		||||
  removeLogEmitter(emitFn) {
 | 
			
		||||
    this.logEmitters.delete(emitFn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 兼容旧API
 | 
			
		||||
  setLogEmitter(emitFn) {
 | 
			
		||||
    if (emitFn) {
 | 
			
		||||
      this.addLogEmitter(emitFn);
 | 
			
		||||
    } else {
 | 
			
		||||
      // 如果传入null,清除所有监听器(为了兼容旧代码)
 | 
			
		||||
      this.logEmitters.clear();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 搜索日志
 | 
			
		||||
  searchLogs(keyword, date = null, limit = 500) {
 | 
			
		||||
    try {
 | 
			
		||||
      const filename = date ? `app-${date}.log` : this.getLogFileName().split(path.sep).pop();
 | 
			
		||||
      const filePath = path.join(this.logsDir, filename);
 | 
			
		||||
      
 | 
			
		||||
      if (!fs.existsSync(filePath)) {
 | 
			
		||||
        return { logs: [], total: 0 };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const content = fs.readFileSync(filePath, 'utf8');
 | 
			
		||||
      const lines = content.split('\n');
 | 
			
		||||
      
 | 
			
		||||
      const matchedLines = lines
 | 
			
		||||
        .map((line, index) => {
 | 
			
		||||
          if (line.toLowerCase().includes(keyword.toLowerCase())) {
 | 
			
		||||
            const match = line.match(/^\[([^\]]+)\] \[([^\]]+)\] (.+)$/);
 | 
			
		||||
            if (match) {
 | 
			
		||||
              return {
 | 
			
		||||
                timestamp: match[1],
 | 
			
		||||
                level: match[2].toLowerCase(),
 | 
			
		||||
                message: match[3],
 | 
			
		||||
                raw: line,
 | 
			
		||||
                lineNumber: index + 1
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
            return {
 | 
			
		||||
              timestamp: null,
 | 
			
		||||
              level: 'info',
 | 
			
		||||
              message: line,
 | 
			
		||||
              raw: line,
 | 
			
		||||
              lineNumber: index + 1
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
          return null;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(line => line !== null)
 | 
			
		||||
        .slice(-limit); // 只返回最新的匹配结果
 | 
			
		||||
      
 | 
			
		||||
      return {
 | 
			
		||||
        logs: matchedLines,
 | 
			
		||||
        total: matchedLines.length
 | 
			
		||||
      };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('搜索日志失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 清理旧日志文件(保留指定天数)
 | 
			
		||||
  cleanupOldLogs(daysToKeep = 30) {
 | 
			
		||||
    try {
 | 
			
		||||
      const files = fs.readdirSync(this.logsDir);
 | 
			
		||||
      const cutoffDate = new Date();
 | 
			
		||||
      cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
 | 
			
		||||
      const cutoffDateStr = cutoffDate.toISOString().slice(0, 10);
 | 
			
		||||
 | 
			
		||||
      let deletedCount = 0;
 | 
			
		||||
      
 | 
			
		||||
      files.forEach(file => {
 | 
			
		||||
        if (file.startsWith('app-') && file.endsWith('.log')) {
 | 
			
		||||
          const dateStr = file.replace('app-', '').replace('.log', '');
 | 
			
		||||
          if (dateStr < cutoffDateStr) {
 | 
			
		||||
            const filePath = path.join(this.logsDir, file);
 | 
			
		||||
            fs.unlinkSync(filePath);
 | 
			
		||||
            deletedCount++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return deletedCount;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('清理旧日志失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 创建单例
 | 
			
		||||
const logger = new Logger();
 | 
			
		||||
 | 
			
		||||
module.exports = logger;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user