// logs.js - 查询日志页面功能
// 全局变量
let currentPage = 1;
let totalPages = 1;
let logsPerPage = 30; // 默认显示30条记录
let currentFilter = '';
let currentSearch = '';
let logsChart = null;
let currentSortField = '';
let currentSortDirection = 'desc'; // 默认降序
// IP地理位置缓存
let ipGeolocationCache = {};
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
// WebSocket连接和重连计时器
let logsWsConnection = null;
let logsWsReconnectTimer = null;
// 初始化查询日志页面
function initLogsPage() {
console.log('初始化查询日志页面');
// 加载日志统计数据
loadLogsStats();
// 加载日志详情
loadLogs();
// 初始化图表
initLogsChart();
// 绑定事件
bindLogsEvents();
// 建立WebSocket连接,用于实时更新统计数据和图表
connectLogsWebSocket();
// 在页面卸载时清理资源
window.addEventListener('beforeunload', cleanupLogsResources);
}
// 清理资源
function cleanupLogsResources() {
// 清除WebSocket连接
if (logsWsConnection) {
logsWsConnection.close();
logsWsConnection = null;
}
// 清除重连计时器
if (logsWsReconnectTimer) {
clearTimeout(logsWsReconnectTimer);
logsWsReconnectTimer = 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);
});
});
// 刷新按钮事件
const refreshBtn = document.getElementById('logs-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
// 重新加载日志
currentPage = 1;
loadLogs();
});
}
// 排序按钮事件
const sortHeaders = document.querySelectorAll('th[data-sort]');
sortHeaders.forEach(header => {
header.addEventListener('click', () => {
const sortField = header.getAttribute('data-sort');
// 如果点击的是当前排序字段,则切换排序方向
if (sortField === currentSortField) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
// 否则,设置新的排序字段,默认降序
currentSortField = sortField;
currentSortDirection = 'desc';
}
// 更新排序图标
updateSortIcons();
// 重新加载日志
currentPage = 1;
loadLogs();
});
});
}
// 更新排序图标
function updateSortIcons() {
const sortHeaders = document.querySelectorAll('th[data-sort]');
sortHeaders.forEach(header => {
const sortField = header.getAttribute('data-sort');
const icon = header.querySelector('i');
// 重置所有图标
icon.className = 'fa fa-sort ml-1 text-xs';
// 设置当前排序字段的图标
if (sortField === currentSortField) {
if (currentSortDirection === 'asc') {
icon.className = 'fa fa-sort-asc ml-1 text-xs';
} else {
icon.className = 'fa fa-sort-desc ml-1 text-xs';
}
}
});
}
// 加载日志统计数据
function loadLogsStats() {
// 使用封装的apiRequest函数进行API调用
apiRequest('/logs/stats')
.then(data => {
if (data && data.error) {
console.error('加载日志统计数据失败:', data.error);
return;
}
// 更新统计卡片
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 endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
// 添加过滤条件
if (currentFilter) {
endpoint += `&result=${currentFilter}`;
}
// 添加搜索条件
if (currentSearch) {
endpoint += `&search=${encodeURIComponent(currentSearch)}`;
}
// 添加排序条件
if (currentSortField) {
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
}
// 使用封装的apiRequest函数进行API调用
apiRequest(endpoint)
.then(data => {
if (data && data.error) {
console.error('加载日志详情失败:', data.error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
// 加载日志总数
return apiRequest('/logs/count').then(countData => {
return { logs: data, count: countData.count };
});
})
.then(result => {
if (!result || !result.logs) {
console.error('加载日志详情失败: 无效的响应数据');
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
const logs = result.logs;
const totalLogs = result.count;
// 计算总页数
totalPages = Math.ceil(totalLogs / logsPerPage);
// 更新日志表格
updateLogsTable(logs);
// 绑定操作按钮事件
bindActionButtonsEvents();
// 更新分页信息
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 = `
暂无查询日志
|
`;
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 formattedDate = time.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const formattedTime = time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// 根据结果添加不同的背景色
let rowClass = '';
switch (log.Result) {
case 'blocked':
rowClass = 'bg-red-50'; // 淡红色填充
break;
case 'allowed':
// 检查是否是规则允许项目
if (log.BlockRule && log.BlockRule.includes('allow')) {
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
} else {
rowClass = ''; // 允许的不填充
}
break;
default:
rowClass = '';
}
// 添加行背景色
if (rowClass) {
row.classList.add(rowClass);
}
// 添加被屏蔽或允许显示,并增加颜色
let statusText = '';
let statusClass = '';
switch (log.Result) {
case 'blocked':
statusText = '被屏蔽';
statusClass = 'text-danger';
break;
case 'allowed':
statusText = '允许';
statusClass = 'text-success';
break;
case 'error':
statusText = '错误';
statusClass = 'text-warning';
break;
default:
statusText = '';
statusClass = '';
}
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
// 添加缓存状态显示
const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500';
const cacheStatusText = log.FromCache ? '缓存' : '非缓存';
// 检查域名是否被拦截
const isBlocked = log.Result === 'blocked';
row.innerHTML = `
${formattedTime}
${formattedDate}
|
${log.ClientIP}
${log.Location || '未知 未知'}
|
${log.Domain}
类型: ${log.QueryType}, ${statusText}, ${log.FromCache ? '缓存' : '实时'}${log.DNSSEC ? ', DNSSEC' : ''}${log.EDNS ? ', EDNS' : ''}
DNS 服务器: ${log.DNSServer || '无'}, DNSSEC专用: ${log.DNSSECServer || '无'}
|
${log.ResponseTime}ms |
${log.BlockRule || '-'} |
${isBlocked ?
`` :
``
}
|
`;
// 绑定按钮事件
const blockBtn = row.querySelector('.block-btn');
if (blockBtn) {
blockBtn.addEventListener('click', (e) => {
e.preventDefault();
const domain = e.currentTarget.dataset.domain;
blockDomain(domain);
});
}
const unblockBtn = row.querySelector('.unblock-btn');
if (unblockBtn) {
unblockBtn.addEventListener('click', (e) => {
e.preventDefault();
const domain = e.currentTarget.dataset.domain;
unblockDomain(domain);
});
}
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小时统计数据
apiRequest('/hourly-stats')
.then(data => {
if (data && data.error) {
console.error('初始化日志图表失败:', data.error);
return;
}
// 创建图表
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 endpoint = '';
switch (range) {
case '24h':
endpoint = '/hourly-stats';
break;
case '7d':
endpoint = '/daily-stats';
break;
case '30d':
endpoint = '/monthly-stats';
break;
default:
endpoint = '/hourly-stats';
}
// 使用封装的apiRequest函数进行API调用
apiRequest(endpoint)
.then(data => {
if (data && data.error) {
console.error('更新日志图表失败:', data.error);
return;
}
// 更新图表数据
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连接
logsWsConnection = new WebSocket(wsUrl);
// 连接打开事件
logsWsConnection.onopen = function() {
console.log('WebSocket连接已建立');
};
// 接收消息事件
logsWsConnection.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);
}
};
// 连接关闭事件
logsWsConnection.onclose = function(event) {
console.warn('WebSocket连接已关闭,代码:', event.code);
logsWsConnection = null;
// 设置重连
setupLogsReconnect();
};
// 连接错误事件
logsWsConnection.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
}
}
// 设置重连逻辑
function setupLogsReconnect() {
if (logsWsReconnectTimer) {
return; // 已经有重连计时器在运行
}
const reconnectDelay = 5000; // 5秒后重连
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
logsWsReconnectTimer = 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);
}
}
// 拦截域名
async function blockDomain(domain) {
try {
console.log(`开始拦截域名: ${domain}`);
// 创建拦截规则,使用AdBlock Plus格式
const blockRule = `||${domain}^`;
console.log(`创建的拦截规则: ${blockRule}`);
// 调用API添加拦截规则
console.log(`调用API添加拦截规则,路径: /shield, 方法: POST`);
const response = await apiRequest('/shield', 'POST', { rule: blockRule });
console.log(`API响应:`, response);
// 处理不同的响应格式
if (response && (response.success || response.status === 'success')) {
// 重新加载日志,显示更新后的状态
loadLogs();
// 刷新规则列表
refreshRulesList();
// 显示成功通知
if (typeof window.showNotification === 'function') {
window.showNotification(`已成功拦截域名: ${domain}`, 'success');
}
} else {
const errorMsg = response ? (response.message || '添加拦截规则失败') : '添加拦截规则失败: 无效的API响应';
console.error(`拦截域名失败: ${errorMsg}`);
throw new Error(errorMsg);
}
} catch (error) {
console.error('拦截域名失败:', error);
// 显示错误通知
if (typeof window.showNotification === 'function') {
window.showNotification(`拦截域名失败: ${error.message}`, 'danger');
}
}
}
// 绑定操作按钮事件
function bindActionButtonsEvents() {
// 绑定拦截按钮事件
const blockBtns = document.querySelectorAll('.block-btn');
blockBtns.forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
const domain = e.currentTarget.dataset.domain;
await blockDomain(domain);
});
});
// 绑定放行按钮事件
const unblockBtns = document.querySelectorAll('.unblock-btn');
unblockBtns.forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
const domain = e.currentTarget.dataset.domain;
await unblockDomain(domain);
});
});
}
// 刷新规则列表
async function refreshRulesList() {
try {
// 调用API重新加载规则
const response = await apiRequest('/shield', 'GET');
if (response) {
// 处理规则列表响应
let allRules = [];
if (response && typeof response === 'object') {
// 合并所有类型的规则到一个数组
if (Array.isArray(response.domainRules)) allRules = allRules.concat(response.domainRules);
if (Array.isArray(response.domainExceptions)) allRules = allRules.concat(response.domainExceptions);
if (Array.isArray(response.regexRules)) allRules = allRules.concat(response.regexRules);
if (Array.isArray(response.regexExceptions)) allRules = allRules.concat(response.regexExceptions);
}
// 更新规则列表
if (window.rules) {
rules = allRules;
filteredRules = [...rules];
// 更新规则数量统计
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
}
}
} catch (error) {
console.error('刷新规则列表失败:', error);
}
}
// 放行域名
async function unblockDomain(domain) {
try {
console.log(`开始放行域名: ${domain}`);
// 创建放行规则,使用AdBlock Plus格式
const allowRule = `@@||${domain}^`;
console.log(`创建的放行规则: ${allowRule}`);
// 调用API添加放行规则
console.log(`调用API添加放行规则,路径: /shield, 方法: POST`);
const response = await apiRequest('/shield', 'POST', { rule: allowRule });
console.log(`API响应:`, response);
// 处理不同的响应格式
if (response && (response.success || response.status === 'success')) {
// 重新加载日志,显示更新后的状态
loadLogs();
// 刷新规则列表
refreshRulesList();
// 显示成功通知
if (typeof window.showNotification === 'function') {
window.showNotification(`已成功放行域名: ${domain}`, 'success');
}
} else {
const errorMsg = response ? (response.message || '添加放行规则失败') : '添加放行规则失败: 无效的API响应';
console.error(`放行域名失败: ${errorMsg}`);
throw new Error(errorMsg);
}
} catch (error) {
console.error('放行域名失败:', error);
// 显示错误通知
if (typeof window.showNotification === 'function') {
window.showNotification(`放行域名失败: ${error.message}`, 'danger');
}
}
}
// 定期更新日志统计数据(备用方案)
setInterval(() => {
// 只有在查询日志页面时才更新
if (window.location.hash === '#logs') {
loadLogsStats();
// 不自动更新日志详情,只更新统计数据
}
}, 30000); // 每30秒更新一次