From 9cfc3632274664b27cf178e189adf8ee27ab0cdd Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sat, 2 Aug 2025 22:33:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=9F=A5=E8=AF=A2=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 90 +++++++++++++++-- static/js/app.js | 249 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 256 insertions(+), 83 deletions(-) diff --git a/app.py b/app.py index c51a411..b380830 100644 --- a/app.py +++ b/app.py @@ -21,13 +21,33 @@ class QueryLogCollector: def __init__(self, max_logs=1000): self.logs = [] self.max_logs = max_logs + self.current_batch_id = None + self.batch_counter = 0 - def add_log(self, level, message): + def start_new_batch(self, query_type='single'): + """开始新的查询批次""" + self.batch_counter += 1 + self.current_batch_id = f"batch_{self.batch_counter}_{datetime.now().strftime('%H%M%S')}" + + # 添加批次开始标记 + self.add_log('INFO', f"=== 开始{query_type}查询批次 (ID: {self.current_batch_id}) ===", force_batch_id=self.current_batch_id) + return self.current_batch_id + + def end_current_batch(self): + """结束当前查询批次""" + if self.current_batch_id: + self.add_log('INFO', f"=== 查询批次完成 (ID: {self.current_batch_id}) ===", force_batch_id=self.current_batch_id) + self.current_batch_id = None + + def add_log(self, level, message, force_batch_id=None): timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + batch_id = force_batch_id or self.current_batch_id + log_entry = { 'timestamp': timestamp, 'level': level, - 'message': message + 'message': message, + 'batch_id': batch_id } self.logs.append(log_entry) # 保持日志数量在限制内 @@ -39,8 +59,26 @@ class QueryLogCollector: return self.logs[-limit:] return self.logs + def get_logs_grouped_by_batch(self, limit=None): + """按批次分组获取日志""" + logs = self.get_logs(limit) + grouped_logs = {} + batch_order = [] + + for log in logs: + batch_id = log.get('batch_id', 'unknown') + if batch_id not in grouped_logs: + grouped_logs[batch_id] = [] + batch_order.append(batch_id) + grouped_logs[batch_id].append(log) + + # 返回按时间顺序排列的批次 + return [(batch_id, grouped_logs[batch_id]) for batch_id in batch_order] + def clear_logs(self): self.logs.clear() + self.current_batch_id = None + self.batch_counter = 0 # 全局日志收集器实例 query_log_collector = QueryLogCollector() @@ -1251,6 +1289,10 @@ def sharding_query_compare(): """分表查询比对API""" try: data = request.json + + # 开始新的查询批次 + batch_id = query_log_collector.start_new_batch('分表') + logger.info("开始执行分表数据库比对查询") # 解析配置 @@ -1352,10 +1394,14 @@ def sharding_query_compare(): logger.info(f"分表比对完成:发现 {len(differences)} 处差异") + # 结束查询批次 + query_log_collector.end_current_batch() return jsonify(result) except Exception as e: logger.error(f"分表查询执行失败:{str(e)}") + # 结束查询批次(出错情况) + query_log_collector.end_current_batch() return jsonify({'error': f'分表查询执行失败:{str(e)}'}), 500 finally: # 关闭连接 @@ -1366,12 +1412,18 @@ def sharding_query_compare(): except Exception as e: logger.error(f"分表查询请求处理失败:{str(e)}") + # 结束查询批次(请求处理出错) + query_log_collector.end_current_batch() return jsonify({'error': f'分表查询请求处理失败:{str(e)}'}), 500 @app.route('/api/query', methods=['POST']) def query_compare(): try: data = request.json + + # 开始新的查询批次 + batch_id = query_log_collector.start_new_batch('单表') + logger.info("开始执行数据库比对查询") # 解析配置 @@ -1482,10 +1534,14 @@ def query_compare(): except Exception as e: logger.warning(f"保存查询历史记录失败: {e}") + # 结束查询批次 + query_log_collector.end_current_batch() return jsonify(result) except Exception as e: logger.error(f"查询执行失败:{str(e)}") + # 结束查询批次(出错情况) + query_log_collector.end_current_batch() return jsonify({'error': f'查询执行失败:{str(e)}'}), 500 finally: # 关闭连接 @@ -1496,6 +1552,8 @@ def query_compare(): except Exception as e: logger.error(f"请求处理失败:{str(e)}") + # 结束查询批次(请求处理出错) + query_log_collector.end_current_batch() return jsonify({'error': f'请求处理失败:{str(e)}'}), 500 @app.route('/api/default-config') @@ -1645,15 +1703,29 @@ def api_delete_query_history(history_id): @app.route('/api/query-logs', methods=['GET']) def api_get_query_logs(): - """获取查询日志""" + """获取查询日志,支持分组显示""" try: limit = request.args.get('limit', type=int) - logs = query_log_collector.get_logs(limit) - return jsonify({ - 'success': True, - 'data': logs, - 'total': len(query_log_collector.logs) - }) + grouped = request.args.get('grouped', 'true').lower() == 'true' # 默认分组显示 + + if grouped: + # 返回分组日志 + grouped_logs = query_log_collector.get_logs_grouped_by_batch(limit) + return jsonify({ + 'success': True, + 'data': grouped_logs, + 'total': len(query_log_collector.logs), + 'grouped': True + }) + else: + # 返回原始日志列表 + logs = query_log_collector.get_logs(limit) + return jsonify({ + 'success': True, + 'data': logs, + 'total': len(query_log_collector.logs), + 'grouped': False + }) except Exception as e: logger.error(f"获取查询日志失败: {e}") return jsonify({'success': False, 'error': str(e)}), 500 diff --git a/static/js/app.js b/static/js/app.js index b837edf..fa26cb1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3132,12 +3132,17 @@ let allQueryLogs = []; // 存储所有日志 async function refreshQueryLogs() { try { - const response = await fetch('/api/query-logs'); + const response = await fetch('/api/query-logs?grouped=true'); const result = await response.json(); if (result.success && result.data) { - allQueryLogs = result.data; - filterLogsByLevel(); + if (result.grouped) { + displayGroupedQueryLogs(result.data); + } else { + // 兼容旧版本的平铺显示 + allQueryLogs = result.data; + filterLogsByLevel(); + } } else { document.getElementById('query-logs').innerHTML = '
无法获取查询日志
'; } @@ -3147,21 +3152,176 @@ async function refreshQueryLogs() { } } -function filterLogsByLevel() { - const showInfo = document.getElementById('log-level-info').checked; - const showWarning = document.getElementById('log-level-warning').checked; - const showError = document.getElementById('log-level-error').checked; +// 显示分组查询日志 +function displayGroupedQueryLogs(groupedLogs) { + const container = document.getElementById('query-logs'); - const filteredLogs = allQueryLogs.filter(log => { - switch(log.level) { - case 'INFO': return showInfo; - case 'WARNING': return showWarning; - case 'ERROR': return showError; - default: return true; - } + if (!groupedLogs || groupedLogs.length === 0) { + container.innerHTML = '
暂无查询日志
'; + return; + } + + let html = ''; + + // 为每个批次生成折叠面板 + groupedLogs.forEach((batchData, index) => { + const [batchId, logs] = batchData; + const isExpanded = index === groupedLogs.length - 1; // 默认展开最新批次 + const collapseId = `batch-${batchId}`; + + // 统计批次信息 + const logCounts = { + INFO: logs.filter(log => log.level === 'INFO').length, + WARNING: logs.filter(log => log.level === 'WARNING').length, + ERROR: logs.filter(log => log.level === 'ERROR').length + }; + + const totalLogs = logs.length; + const firstLog = logs[0]; + const lastLog = logs[logs.length - 1]; + const duration = firstLog && lastLog ? calculateDuration(firstLog.timestamp, lastLog.timestamp) : '0秒'; + + // 确定批次类型和状态 + const hasErrors = logCounts.ERROR > 0; + const hasWarnings = logCounts.WARNING > 0; + const batchStatus = hasErrors ? 'danger' : hasWarnings ? 'warning' : 'success'; + const batchIcon = hasErrors ? 'fas fa-times-circle' : hasWarnings ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'; + + // 提取查询类型 + const batchTypeMatch = firstLog?.message.match(/开始(\\w+)查询批次/); + const batchType = batchTypeMatch ? batchTypeMatch[1] : '未知'; + + html += ` +
+
+
+
+ + ${batchType}查询批次 ${batchId} + ${totalLogs}条日志 +
+
+ + ${duration} + +
+ ${logCounts.INFO > 0 ? `${logCounts.INFO} INFO` : ''} + ${logCounts.WARNING > 0 ? `${logCounts.WARNING} WARN` : ''} + ${logCounts.ERROR > 0 ? `${logCounts.ERROR} ERROR` : ''} +
+ +
+
+
+
+
+
+ `; + + // 显示该批次的日志条目 + logs.forEach(log => { + const showInfo = document.getElementById('log-level-info').checked; + const showWarning = document.getElementById('log-level-warning').checked; + const showError = document.getElementById('log-level-error').checked; + + // 应用级别过滤 + let shouldShow = false; + switch(log.level) { + case 'INFO': shouldShow = showInfo; break; + case 'WARNING': shouldShow = showWarning; break; + case 'ERROR': shouldShow = showError; break; + default: shouldShow = true; + } + + if (!shouldShow) return; + + const levelClass = { + 'INFO': 'text-primary', + 'WARNING': 'text-warning', + 'ERROR': 'text-danger', + 'DEBUG': 'text-secondary' + }[log.level] || 'text-dark'; + + const levelIcon = { + 'INFO': 'fas fa-info-circle', + 'WARNING': 'fas fa-exclamation-triangle', + 'ERROR': 'fas fa-times-circle', + 'DEBUG': 'fas fa-bug' + }[log.level] || 'fas fa-circle'; + + // 改进SQL高亮显示 + let message = escapeHtml(log.message); + + // 高亮SQL查询语句 + if (message.includes('执行查询SQL:')) { + message = message.replace(/执行查询SQL: (SELECT.*?);/g, + '执行查询SQL:
$1;'); + } + + // 高亮重要信息 + message = message.replace(/(\\d+\\.\\d{3}秒)/g, '$1'); + message = message.replace(/(返回记录数=\\d+)/g, '$1'); + message = message.replace(/(执行时间=[\\d.]+秒)/g, '$1'); + + html += ` +
+
+
+ + + [${log.level}] + +
${message}
+
+ ${log.timestamp} +
+
+ `; + }); + + html += ` +
+
+
+
+ `; }); - displayQueryLogs(filteredLogs); + container.innerHTML = html; + + // 自动滚动到最新批次 + if (groupedLogs.length > 0) { + const latestBatch = container.querySelector('.card:last-child'); + if (latestBatch) { + latestBatch.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + } +} + +// 计算持续时间 +function calculateDuration(startTime, endTime) { + try { + const start = new Date(startTime); + const end = new Date(endTime); + const diffMs = end - start; + + if (diffMs < 1000) { + return `${diffMs}ms`; + } else if (diffMs < 60000) { + return `${(diffMs / 1000).toFixed(1)}秒`; + } else { + const minutes = Math.floor(diffMs / 60000); + const seconds = Math.floor((diffMs % 60000) / 1000); + return `${minutes}分${seconds}秒`; + } + } catch (e) { + return '未知'; + } +} + +function filterLogsByLevel() { + // 刷新分组日志显示,应用过滤器 + refreshQueryLogs(); } async function clearQueryLogs() { @@ -3187,65 +3347,6 @@ async function clearQueryLogs() { } } -function displayQueryLogs(logs) { - const container = document.getElementById('query-logs'); - - if (!logs || logs.length === 0) { - container.innerHTML = '
暂无查询日志
'; - return; - } - - const logHtml = logs.map(log => { - const levelClass = { - 'INFO': 'text-primary', - 'WARNING': 'text-warning', - 'ERROR': 'text-danger', - 'DEBUG': 'text-secondary' - }[log.level] || 'text-dark'; - - const levelIcon = { - 'INFO': 'fas fa-info-circle', - 'WARNING': 'fas fa-exclamation-triangle', - 'ERROR': 'fas fa-times-circle', - 'DEBUG': 'fas fa-bug' - }[log.level] || 'fas fa-circle'; - - // 改进SQL高亮显示 - let message = escapeHtml(log.message); - - // 高亮SQL查询语句 - if (message.includes('执行查询SQL:')) { - message = message.replace(/执行查询SQL: (SELECT.*?);/g, - '执行查询SQL:
$1;'); - } - - // 高亮重要信息 - message = message.replace(/(\d+\.\d{3}秒)/g, '$1'); - message = message.replace(/(返回记录数=\d+)/g, '$1'); - message = message.replace(/(执行时间=[\d.]+秒)/g, '$1'); - - return ` -
-
-
- - - [${log.level}] - -
${message}
-
- ${log.timestamp} -
-
- `; - }).join(''); - - container.innerHTML = logHtml; - - // 自动滚动到底部 - container.scrollTop = container.scrollHeight; -} - // 在查询执行后自动刷新日志 function autoRefreshLogsAfterQuery() { // 延迟一下确保后端日志已经记录