483 lines
15 KiB
JavaScript
483 lines
15 KiB
JavaScript
// logs.js - 查询日志页面功能
|
||
|
||
// 全局变量
|
||
let currentPage = 1;
|
||
let totalPages = 1;
|
||
let logsPerPage = 30; // 默认显示30条记录
|
||
let currentFilter = '';
|
||
let currentSearch = '';
|
||
let logsChart = null;
|
||
|
||
// 初始化查询日志页面
|
||
function initLogsPage() {
|
||
console.log('初始化查询日志页面');
|
||
|
||
// 加载日志统计数据
|
||
loadLogsStats();
|
||
|
||
// 加载日志详情
|
||
loadLogs();
|
||
|
||
// 初始化图表
|
||
initLogsChart();
|
||
|
||
// 绑定事件
|
||
bindLogsEvents();
|
||
|
||
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||
connectLogsWebSocket();
|
||
|
||
// 在页面卸载时清理资源
|
||
window.addEventListener('beforeunload', cleanupLogsResources);
|
||
}
|
||
|
||
// 清理资源
|
||
function cleanupLogsResources() {
|
||
// 清除WebSocket连接
|
||
if (wsConnection) {
|
||
wsConnection.close();
|
||
wsConnection = null;
|
||
}
|
||
|
||
// 清除重连计时器
|
||
if (wsReconnectTimer) {
|
||
clearTimeout(wsReconnectTimer);
|
||
wsReconnectTimer = null;
|
||
}
|
||
}
|
||
|
||
// 绑定事件
|
||
function bindLogsEvents() {
|
||
// 搜索按钮
|
||
const searchBtn = document.getElementById('logs-search-btn');
|
||
if (searchBtn) {
|
||
searchBtn.addEventListener('click', () => {
|
||
currentSearch = document.getElementById('logs-search').value;
|
||
currentPage = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
// 搜索框回车事件
|
||
const searchInput = document.getElementById('logs-search');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
currentSearch = searchInput.value;
|
||
currentPage = 1;
|
||
loadLogs();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 结果过滤
|
||
const resultFilter = document.getElementById('logs-result-filter');
|
||
if (resultFilter) {
|
||
resultFilter.addEventListener('change', () => {
|
||
currentFilter = resultFilter.value;
|
||
currentPage = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
// 自定义记录数量
|
||
const perPageSelect = document.getElementById('logs-per-page');
|
||
if (perPageSelect) {
|
||
perPageSelect.addEventListener('change', () => {
|
||
logsPerPage = parseInt(perPageSelect.value);
|
||
currentPage = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
// 分页按钮
|
||
const prevBtn = document.getElementById('logs-prev-page');
|
||
const nextBtn = document.getElementById('logs-next-page');
|
||
|
||
if (prevBtn) {
|
||
prevBtn.addEventListener('click', () => {
|
||
if (currentPage > 1) {
|
||
currentPage--;
|
||
loadLogs();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (nextBtn) {
|
||
nextBtn.addEventListener('click', () => {
|
||
if (currentPage < totalPages) {
|
||
currentPage++;
|
||
loadLogs();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 时间范围切换
|
||
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
|
||
timeRangeBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
// 更新按钮样式
|
||
timeRangeBtns.forEach(b => {
|
||
b.classList.remove('bg-primary', 'text-white');
|
||
b.classList.add('bg-gray-200', 'text-gray-700');
|
||
});
|
||
btn.classList.remove('bg-gray-200', 'text-gray-700');
|
||
btn.classList.add('bg-primary', 'text-white');
|
||
|
||
// 更新图表
|
||
const range = btn.getAttribute('data-range');
|
||
updateLogsChart(range);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 加载日志统计数据
|
||
function loadLogsStats() {
|
||
fetch('/api/logs/stats')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// 更新统计卡片
|
||
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||
document.getElementById('logs-active-ips').textContent = data.activeIPs;
|
||
|
||
// 计算屏蔽率
|
||
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
|
||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||
})
|
||
.catch(error => {
|
||
console.error('加载日志统计数据失败:', error);
|
||
});
|
||
}
|
||
|
||
// 加载日志详情
|
||
function loadLogs() {
|
||
// 显示加载状态
|
||
const loadingEl = document.getElementById('logs-loading');
|
||
if (loadingEl) {
|
||
loadingEl.classList.remove('hidden');
|
||
}
|
||
|
||
// 构建请求URL
|
||
let url = `/api/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||
|
||
// 添加过滤条件
|
||
if (currentFilter) {
|
||
url += `&result=${currentFilter}`;
|
||
}
|
||
|
||
// 添加搜索条件
|
||
if (currentSearch) {
|
||
url += `&search=${encodeURIComponent(currentSearch)}`;
|
||
}
|
||
|
||
fetch(url)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// 加载日志总数
|
||
return fetch('/api/logs/count').then(response => response.json()).then(countData => {
|
||
return { logs: data, count: countData.count };
|
||
});
|
||
})
|
||
.then(result => {
|
||
const logs = result.logs;
|
||
const totalLogs = result.count;
|
||
|
||
// 计算总页数
|
||
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||
|
||
// 更新日志表格
|
||
updateLogsTable(logs);
|
||
|
||
// 更新分页信息
|
||
updateLogsPagination();
|
||
|
||
// 隐藏加载状态
|
||
if (loadingEl) {
|
||
loadingEl.classList.add('hidden');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('加载日志详情失败:', error);
|
||
|
||
// 隐藏加载状态
|
||
if (loadingEl) {
|
||
loadingEl.classList.add('hidden');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新日志表格
|
||
function updateLogsTable(logs) {
|
||
const tableBody = document.getElementById('logs-table-body');
|
||
if (!tableBody) return;
|
||
|
||
// 清空表格
|
||
tableBody.innerHTML = '';
|
||
|
||
if (logs.length === 0) {
|
||
// 显示空状态
|
||
const emptyRow = document.createElement('tr');
|
||
emptyRow.innerHTML = `
|
||
<td colspan="7" class="py-8 text-center text-gray-500">
|
||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||
<div>暂无查询日志</div>
|
||
</td>
|
||
`;
|
||
tableBody.appendChild(emptyRow);
|
||
return;
|
||
}
|
||
|
||
// 填充表格
|
||
logs.forEach(log => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
||
|
||
// 格式化时间
|
||
const time = new Date(log.Timestamp);
|
||
const formattedTime = time.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
|
||
// 结果样式
|
||
let resultClass = '';
|
||
let resultText = '';
|
||
switch (log.Result) {
|
||
case 'allowed':
|
||
resultClass = 'text-success';
|
||
resultText = '允许';
|
||
break;
|
||
case 'blocked':
|
||
resultClass = 'text-danger';
|
||
resultText = '屏蔽';
|
||
break;
|
||
case 'error':
|
||
resultClass = 'text-warning';
|
||
resultText = '错误';
|
||
break;
|
||
}
|
||
|
||
// 构建行内容
|
||
row.innerHTML = `
|
||
<td class="py-3 px-4 text-sm">${formattedTime}</td>
|
||
<td class="py-3 px-4 text-sm">${log.ClientIP}</td>
|
||
<td class="py-3 px-4 text-sm font-medium">${log.Domain}</td>
|
||
<td class="py-3 px-4 text-sm">${log.QueryType}</td>
|
||
<td class="py-3 px-4 text-sm"><span class="${resultClass}">${resultText}</span></td>
|
||
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
|
||
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
|
||
`;
|
||
|
||
tableBody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// 更新分页信息
|
||
function updateLogsPagination() {
|
||
// 更新页码显示
|
||
document.getElementById('logs-current-page').textContent = currentPage;
|
||
document.getElementById('logs-total-pages').textContent = totalPages;
|
||
|
||
// 更新按钮状态
|
||
const prevBtn = document.getElementById('logs-prev-page');
|
||
const nextBtn = document.getElementById('logs-next-page');
|
||
|
||
if (prevBtn) {
|
||
prevBtn.disabled = currentPage === 1;
|
||
}
|
||
|
||
if (nextBtn) {
|
||
nextBtn.disabled = currentPage === totalPages;
|
||
}
|
||
}
|
||
|
||
// 初始化日志图表
|
||
function initLogsChart() {
|
||
const ctx = document.getElementById('logs-trend-chart');
|
||
if (!ctx) return;
|
||
|
||
// 获取24小时统计数据
|
||
fetch('/api/hourly-stats')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// 创建图表
|
||
logsChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: data.labels,
|
||
datasets: [{
|
||
label: '查询数',
|
||
data: data.data,
|
||
borderColor: '#3b82f6',
|
||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||
tension: 0.4,
|
||
fill: true
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: 'top'
|
||
},
|
||
tooltip: {
|
||
mode: 'index',
|
||
intersect: false
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
precision: 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('初始化日志图表失败:', error);
|
||
});
|
||
}
|
||
|
||
// 更新日志图表
|
||
function updateLogsChart(range) {
|
||
if (!logsChart) return;
|
||
|
||
let url = '';
|
||
switch (range) {
|
||
case '24h':
|
||
url = '/api/hourly-stats';
|
||
break;
|
||
case '7d':
|
||
url = '/api/daily-stats';
|
||
break;
|
||
case '30d':
|
||
url = '/api/monthly-stats';
|
||
break;
|
||
default:
|
||
url = '/api/hourly-stats';
|
||
}
|
||
|
||
fetch(url)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// 更新图表数据
|
||
logsChart.data.labels = data.labels;
|
||
logsChart.data.datasets[0].data = data.data;
|
||
logsChart.update();
|
||
})
|
||
.catch(error => {
|
||
console.error('更新日志图表失败:', error);
|
||
});
|
||
}
|
||
|
||
// 建立WebSocket连接
|
||
function connectLogsWebSocket() {
|
||
try {
|
||
// 构建WebSocket URL
|
||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
|
||
|
||
console.log('正在连接WebSocket:', wsUrl);
|
||
|
||
// 创建WebSocket连接
|
||
wsConnection = new WebSocket(wsUrl);
|
||
|
||
// 连接打开事件
|
||
wsConnection.onopen = function() {
|
||
console.log('WebSocket连接已建立');
|
||
};
|
||
|
||
// 接收消息事件
|
||
wsConnection.onmessage = function(event) {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.type === 'initial_data' || data.type === 'stats_update') {
|
||
console.log('收到实时数据更新');
|
||
// 只更新统计数据,不更新日志详情
|
||
updateLogsStatsFromWebSocket(data.data);
|
||
}
|
||
} catch (error) {
|
||
console.error('处理WebSocket消息失败:', error);
|
||
}
|
||
};
|
||
|
||
// 连接关闭事件
|
||
wsConnection.onclose = function(event) {
|
||
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||
wsConnection = null;
|
||
|
||
// 设置重连
|
||
setupLogsReconnect();
|
||
};
|
||
|
||
// 连接错误事件
|
||
wsConnection.onerror = function(error) {
|
||
console.error('WebSocket连接错误:', error);
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('创建WebSocket连接失败:', error);
|
||
}
|
||
}
|
||
|
||
// 设置重连逻辑
|
||
function setupLogsReconnect() {
|
||
if (wsReconnectTimer) {
|
||
return; // 已经有重连计时器在运行
|
||
}
|
||
|
||
const reconnectDelay = 5000; // 5秒后重连
|
||
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||
|
||
wsReconnectTimer = setTimeout(() => {
|
||
connectLogsWebSocket();
|
||
}, reconnectDelay);
|
||
}
|
||
|
||
// 从WebSocket更新日志统计数据
|
||
function updateLogsStatsFromWebSocket(stats) {
|
||
try {
|
||
// 更新统计卡片
|
||
if (stats.dns) {
|
||
// 适配不同的数据结构
|
||
const totalQueries = stats.dns.Queries || 0;
|
||
const blockedQueries = stats.dns.Blocked || 0;
|
||
const allowedQueries = stats.dns.Allowed || 0;
|
||
const errorQueries = stats.dns.Errors || 0;
|
||
const avgResponseTime = stats.dns.AvgResponseTime || 0;
|
||
const activeIPs = stats.activeIPs || Object.keys(stats.dns.SourceIPs || {}).length;
|
||
|
||
// 更新统计卡片
|
||
document.getElementById('logs-total-queries').textContent = totalQueries;
|
||
document.getElementById('logs-avg-response-time').textContent = avgResponseTime.toFixed(2) + 'ms';
|
||
document.getElementById('logs-active-ips').textContent = activeIPs;
|
||
|
||
// 计算屏蔽率
|
||
const blockRate = totalQueries > 0 ? (blockedQueries / totalQueries * 100).toFixed(1) : '0';
|
||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||
}
|
||
} catch (error) {
|
||
console.error('从WebSocket更新日志统计数据失败:', error);
|
||
}
|
||
}
|
||
|
||
// 定期更新日志统计数据(备用方案)
|
||
setInterval(() => {
|
||
// 只有在查询日志页面时才更新
|
||
if (window.location.hash === '#logs') {
|
||
loadLogsStats();
|
||
// 不自动更新日志详情,只更新统计数据
|
||
}
|
||
}, 30000); // 每30秒更新一次
|