完善查询日志

This commit is contained in:
2025-08-03 10:52:45 +08:00
parent 313319e2bb
commit 8e340c801f
2 changed files with 308 additions and 220 deletions

View File

@@ -2758,21 +2758,6 @@ async function loadHistoryResults(historyId) {
// 显示结果
displayResults(result.data);
// 加载相关的查询日志
try {
await loadQueryLogsByHistory(historyId);
// 自动切换到查询日志选项卡以显示关联信息
const logsTab = document.getElementById('logs-tab');
if (logsTab) {
// 添加一个小延迟确保结果已经显示
setTimeout(() => {
logsTab.click();
}, 500);
}
} catch (logError) {
console.warn('加载历史记录相关查询日志失败:', logError);
}
// 关闭历史记录modal
const modal = bootstrap.Modal.getInstance(document.getElementById('queryHistoryModal'));
if (modal) {
@@ -2781,7 +2766,7 @@ async function loadHistoryResults(historyId) {
const queryTypeDesc = (result.data.history_info && result.data.history_info.query_type === 'sharding') ? '分表查询' : '单表查询';
const historyName = (result.data.history_info && result.data.history_info.name) || '未知';
showAlert('success', `${queryTypeDesc}历史记录结果 "${historyName}" 加载成功,查询日志已关联显示`);
showAlert('success', `${queryTypeDesc}历史记录结果 "${historyName}" 加载成功`);
} else {
showAlert('danger', result.error || '加载历史记录结果失败');
}
@@ -3199,171 +3184,16 @@ function showAlert(type, message) {
let allQueryLogs = []; // 存储所有日志
async function refreshQueryLogs() {
try {
const response = await fetch('/api/query-logs?grouped=true&from_db=true');
const result = await response.json();
if (result.success && result.data) {
if (result.grouped) {
displayGroupedQueryLogs(result.data);
} else {
// 兼容旧版本的平铺显示
allQueryLogs = result.data;
filterLogsByLevel();
}
} else {
document.getElementById('query-logs').innerHTML = '<div class="alert alert-warning">无法获取查询日志</div>';
}
} catch (error) {
console.error('获取查询日志失败:', error);
document.getElementById('query-logs').innerHTML = '<div class="alert alert-danger">获取查询日志失败</div>';
}
// 这个函数现在不再使用,因为查询日志已经独立到模态框中
// 如果需要刷新查询日志,请使用 refreshModalQueryLogs()
console.warn('refreshQueryLogs() 已弃用,请使用 refreshModalQueryLogs()');
}
// 显示分组查询日志
// 显示分组查询日志 - 已弃用
function displayGroupedQueryLogs(groupedLogs) {
const container = document.getElementById('query-logs');
if (!groupedLogs || groupedLogs.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无查询日志</div>';
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 += `
<div class="card mb-3 border-${batchStatus}">
<div class="card-header bg-${batchStatus} bg-opacity-10" data-bs-toggle="collapse" data-bs-target="#${collapseId}" style="cursor: pointer;">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="${batchIcon} text-${batchStatus} me-2"></i>
<strong>${batchType}查询批次 ${batchId}</strong>
<span class="badge bg-primary ms-2">${totalLogs}条日志</span>
</div>
<div class="d-flex align-items-center">
<small class="text-muted me-3">
<i class="fas fa-clock"></i> ${duration}
</small>
<div>
${logCounts.INFO > 0 ? `<span class="badge bg-info me-1">${logCounts.INFO} INFO</span>` : ''}
${logCounts.WARNING > 0 ? `<span class="badge bg-warning me-1">${logCounts.WARNING} WARN</span>` : ''}
${logCounts.ERROR > 0 ? `<span class="badge bg-danger me-1">${logCounts.ERROR} ERROR</span>` : ''}
</div>
<i class="fas fa-chevron-down ms-2"></i>
</div>
</div>
</div>
<div class="collapse ${isExpanded ? 'show' : ''}" id="${collapseId}">
<div class="card-body p-0">
<div class="log-entries" style="max-height: 400px; overflow-y: auto;">
`;
// 显示该批次的日志条目
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: <br><code class="bg-light d-block p-2 text-dark" style="font-size: 0.9em;">$1;</code>');
}
// 高亮重要信息
message = message.replace(/(\\d+\\.\\d{3}秒)/g, '<strong class="text-success">$1</strong>');
message = message.replace(/(返回记录数=\\d+)/g, '<strong class="text-info">$1</strong>');
message = message.replace(/(执行时间=[\\d.]+秒)/g, '<strong class="text-success">$1</strong>');
html += `
<div class="border-bottom py-2 px-3 log-entry" data-level="${log.level}">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<span class="${levelClass}">
<i class="${levelIcon}"></i>
<strong>[${log.level}]</strong>
</span>
<div class="ms-4 mt-1" style="font-size: 0.9em;">${message}</div>
</div>
<small class="text-muted ms-2 flex-shrink-0" style="min-width: 140px;">${log.timestamp}</small>
</div>
</div>
`;
});
html += `
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// 自动滚动到最新批次
if (groupedLogs.length > 0) {
const latestBatch = container.querySelector('.card:last-child');
if (latestBatch) {
latestBatch.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
}
// 这个函数已弃用,查询日志现在使用独立模态框显示
console.warn('displayGroupedQueryLogs() 已弃用,查询日志现在使用独立模态框显示');
return;
}
// 计算持续时间
@@ -3387,6 +3217,27 @@ function calculateDuration(startTime, endTime) {
}
}
// 格式化日志消息
function formatLogMessage(message) {
// HTML转义
let formatted = escapeHtml(message);
// 高亮SQL查询语句
if (formatted.includes('执行查询SQL:')) {
formatted = formatted.replace(/执行查询SQL: (SELECT.*?);/g,
'执行查询SQL: <br><code class="bg-light d-block p-2 text-dark" style="font-size: 0.85em;">$1;</code>');
}
// 高亮重要信息
formatted = formatted.replace(/([\d.]+秒)/g, '<strong class="text-success">$1</strong>');
formatted = formatted.replace(/(返回记录数=\d+)/g, '<strong class="text-info">$1</strong>');
formatted = formatted.replace(/(执行时间=[\d.]+秒)/g, '<strong class="text-success">$1</strong>');
formatted = formatted.replace(/(分表索引=\d+)/g, '<strong class="text-warning">$1</strong>');
formatted = formatted.replace(/(表名=\w+)/g, '<strong class="text-primary">$1</strong>');
return formatted;
}
function filterLogsByLevel() {
// 刷新分组日志显示,应用过滤器
refreshQueryLogs();
@@ -3456,6 +3307,228 @@ async function cleanupOldLogs() {
}
}
// 显示查询日志对话框
function showQueryLogsDialog() {
// 打开模态框
const modal = new bootstrap.Modal(document.getElementById('queryLogsModal'));
modal.show();
// 加载查询日志
refreshModalQueryLogs();
}
// 刷新模态框中的查询日志
async function refreshModalQueryLogs() {
try {
const response = await fetch('/api/query-logs?grouped=true&from_db=true');
const result = await response.json();
if (result.success && result.data) {
if (result.grouped) {
displayModalGroupedQueryLogs(result.data);
} else {
// 兼容旧版本的平铺显示
displayModalQueryLogs(result.data);
}
} else {
document.getElementById('modal-query-logs').innerHTML = '<div class="alert alert-warning">无法获取查询日志</div>';
}
} catch (error) {
console.error('获取查询日志失败:', error);
document.getElementById('modal-query-logs').innerHTML = '<div class="alert alert-danger">获取查询日志失败</div>';
}
}
// 在模态框中显示分组查询日志
function displayModalGroupedQueryLogs(groupedLogs) {
const container = document.getElementById('modal-query-logs');
if (!groupedLogs || groupedLogs.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无查询日志</div>';
return;
}
let html = '';
// 为每个批次生成折叠面板
groupedLogs.forEach((batchData, index) => {
const [batchId, logs] = batchData;
const isExpanded = index === groupedLogs.length - 1; // 默认展开最新批次
const collapseId = `modal-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] : '未知';
// 检查是否有关联的历史记录ID
const historyId = firstLog?.history_id;
const historyBadge = historyId ?
`<span class="badge bg-secondary ms-1" title="关联历史记录ID: ${historyId}">
ID:${historyId}
<button class="btn btn-sm btn-outline-light ms-1 p-0" style="border: none; font-size: 10px;"
onclick="loadHistoryFromLogs(${historyId})" title="加载此历史记录">
<i class="fas fa-external-link-alt"></i>
</button>
</span>` : '';
html += `
<div class="card mb-3 border-${batchStatus}">
<div class="card-header bg-${batchStatus} bg-opacity-10" data-bs-toggle="collapse" data-bs-target="#${collapseId}" style="cursor: pointer;">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="${batchIcon} text-${batchStatus} me-2"></i>
<strong>${batchType}查询批次 ${batchId}</strong>
<span class="badge bg-primary ms-2">${totalLogs}条日志</span>
${historyBadge}
</div>
<div class="d-flex align-items-center">
<small class="text-muted me-3">
<i class="fas fa-clock"></i> ${duration}
</small>
<div>
${logCounts.INFO > 0 ? `<span class="badge bg-info me-1">${logCounts.INFO} INFO</span>` : ''}
${logCounts.WARNING > 0 ? `<span class="badge bg-warning me-1">${logCounts.WARNING} WARN</span>` : ''}
${logCounts.ERROR > 0 ? `<span class="badge bg-danger me-1">${logCounts.ERROR} ERROR</span>` : ''}
</div>
<i class="fas fa-chevron-down ms-2"></i>
</div>
</div>
</div>
<div class="collapse ${isExpanded ? 'show' : ''}" id="${collapseId}">
<div class="card-body p-0">
<div class="log-entries" style="max-height: 400px; overflow-y: auto;">
`;
// 显示该批次的日志条目
logs.forEach(log => {
const showInfo = document.getElementById('modal-log-level-info').checked;
const showWarning = document.getElementById('modal-log-level-warning').checked;
const showError = document.getElementById('modal-log-level-error').checked;
const shouldShow = (log.level === 'INFO' && showInfo) ||
(log.level === 'WARNING' && showWarning) ||
(log.level === 'ERROR' && showError);
if (shouldShow) {
const levelClass = log.level === 'ERROR' ? 'danger' :
log.level === 'WARNING' ? 'warning' : 'info';
const levelIcon = log.level === 'ERROR' ? 'fas fa-times-circle' :
log.level === 'WARNING' ? 'fas fa-exclamation-triangle' : 'fas fa-info-circle';
// 简化时间戳显示
const timeOnly = log.timestamp.split(' ')[1] || log.timestamp;
html += `
<div class="log-entry p-2 border-bottom">
<div class="d-flex align-items-start">
<span class="badge bg-${levelClass} me-2">
<i class="${levelIcon}"></i> ${log.level}
</span>
<small class="text-muted me-2 flex-shrink-0" style="min-width: 80px;">
${timeOnly}
</small>
<div class="log-message flex-grow-1">
${formatLogMessage(log.message)}
</div>
</div>
</div>
`;
}
});
html += `
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 过滤模态框中的日志级别
function filterModalLogsByLevel() {
// 刷新分组日志显示,应用过滤器
refreshModalQueryLogs();
}
// 显示平铺的查询日志(兼容性)
function displayModalQueryLogs(logs) {
const container = document.getElementById('modal-query-logs');
if (!logs || logs.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无查询日志</div>';
return;
}
const showInfo = document.getElementById('modal-log-level-info').checked;
const showWarning = document.getElementById('modal-log-level-warning').checked;
const showError = document.getElementById('modal-log-level-error').checked;
let html = '<div class="list-group">';
logs.forEach(log => {
const shouldShow = (log.level === 'INFO' && showInfo) ||
(log.level === 'WARNING' && showWarning) ||
(log.level === 'ERROR' && showError);
if (shouldShow) {
const levelClass = log.level === 'ERROR' ? 'danger' :
log.level === 'WARNING' ? 'warning' : 'info';
const levelIcon = log.level === 'ERROR' ? 'fas fa-times-circle' :
log.level === 'WARNING' ? 'fas fa-exclamation-triangle' : 'fas fa-info-circle';
html += `
<div class="list-group-item">
<div class="d-flex align-items-start">
<span class="badge bg-${levelClass} me-2">
<i class="${levelIcon}"></i> ${log.level}
</span>
<small class="text-muted me-2">${log.timestamp}</small>
<div class="flex-grow-1">${formatLogMessage(log.message)}</div>
</div>
</div>
`;
}
});
html += '</div>';
container.innerHTML = html;
}
// 从查询日志模态框中加载历史记录
function loadHistoryFromLogs(historyId) {
// 先关闭查询日志模态框
const logsModal = bootstrap.Modal.getInstance(document.getElementById('queryLogsModal'));
if (logsModal) {
logsModal.hide();
}
// 延迟一下再加载历史记录,确保模态框已关闭
setTimeout(() => {
loadHistoryResults(historyId);
}, 300);
}
// 根据历史记录ID获取相关查询日志
async function loadQueryLogsByHistory(historyId) {
try {

View File

@@ -327,12 +327,17 @@
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<div class="col-4">
<button class="btn btn-warning btn-sm w-100" onclick="showQueryHistoryDialog()">
<i class="fas fa-history"></i> 查询历史
</button>
</div>
<div class="col-6">
<div class="col-4">
<button class="btn btn-info btn-sm w-100" onclick="showQueryLogsDialog()">
<i class="fas fa-file-alt"></i> 查询日志
</button>
</div>
<div class="col-4">
<button class="btn btn-secondary btn-sm w-100" onclick="showSaveHistoryDialog()">
<i class="fas fa-bookmark"></i> 保存历史
</button>
@@ -615,11 +620,6 @@
<i class="fas fa-chart-pie"></i> 比较总结
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="logs-tab" data-bs-toggle="tab" data-bs-target="#logs-panel" type="button" role="tab">
<i class="fas fa-file-alt"></i> 查询日志
</button>
</li>
</ul>
<div class="mt-2">
<button class="btn btn-sm btn-outline-primary" onclick="exportResults()">
@@ -684,41 +684,6 @@
<!-- 总结报告将在这里动态生成 -->
</div>
</div>
<!-- 查询日志面板 -->
<div class="tab-pane fade" id="logs-panel" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<h6 class="mb-0 me-3">查询执行日志</h6>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="log-level-info" checked onchange="filterLogsByLevel()">
<label class="form-check-label text-primary" for="log-level-info">INFO</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="log-level-warning" checked onchange="filterLogsByLevel()">
<label class="form-check-label text-warning" for="log-level-warning">WARNING</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="log-level-error" checked onchange="filterLogsByLevel()">
<label class="form-check-label text-danger" for="log-level-error">ERROR</label>
</div>
</div>
<div>
<button class="btn btn-sm btn-outline-primary me-2" onclick="refreshQueryLogs()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button class="btn btn-sm btn-outline-warning me-2" onclick="cleanupOldLogs()">
<i class="fas fa-broom"></i> 清理旧日志
</button>
<button class="btn btn-sm btn-outline-danger" onclick="clearQueryLogs()">
<i class="fas fa-trash"></i> 清空
</button>
</div>
</div>
<div id="query-logs" style="max-height: 500px; overflow-y: auto;">
<!-- 查询日志将在这里动态生成 -->
</div>
</div>
</div>
</div>
</div>
@@ -727,6 +692,56 @@
</div>
</div>
<!-- 查询日志模态框 -->
<div class="modal fade" id="queryLogsModal" tabindex="-1" aria-labelledby="queryLogsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="queryLogsModalLabel">
<i class="fas fa-file-alt"></i> 查询日志管理
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<h6 class="mb-0 me-3">查询执行日志</h6>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="modal-log-level-info" checked onchange="filterModalLogsByLevel()">
<label class="form-check-label text-primary" for="modal-log-level-info">INFO</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="modal-log-level-warning" checked onchange="filterModalLogsByLevel()">
<label class="form-check-label text-warning" for="modal-log-level-warning">WARNING</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="modal-log-level-error" checked onchange="filterModalLogsByLevel()">
<label class="form-check-label text-danger" for="modal-log-level-error">ERROR</label>
</div>
</div>
<div>
<button class="btn btn-sm btn-outline-primary me-2" onclick="refreshModalQueryLogs()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button class="btn btn-sm btn-outline-warning me-2" onclick="cleanupOldLogs()">
<i class="fas fa-broom"></i> 清理旧日志
</button>
<button class="btn btn-sm btn-outline-danger" onclick="clearQueryLogs()">
<i class="fas fa-trash"></i> 清空
</button>
</div>
</div>
<div id="modal-query-logs" style="max-height: 600px; overflow-y: auto;">
<!-- 查询日志将在这里动态生成 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>