优化日志查询页面
This commit is contained in:
@@ -638,7 +638,7 @@ func (s *Server) GetStats() *Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetQueryLogs 获取查询日志
|
// GetQueryLogs 获取查询日志
|
||||||
func (s *Server) GetQueryLogs(limit, offset int) []QueryLog {
|
func (s *Server) GetQueryLogs(limit, offset int, sortField, sortDirection string) []QueryLog {
|
||||||
s.queryLogsMutex.RLock()
|
s.queryLogsMutex.RLock()
|
||||||
defer s.queryLogsMutex.RUnlock()
|
defer s.queryLogsMutex.RUnlock()
|
||||||
|
|
||||||
@@ -650,17 +650,90 @@ func (s *Server) GetQueryLogs(limit, offset int) []QueryLog {
|
|||||||
limit = 100 // 默认返回100条日志
|
limit = 100 // 默认返回100条日志
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建日志副本用于排序
|
||||||
|
logsCopy := make([]QueryLog, len(s.queryLogs))
|
||||||
|
copy(logsCopy, s.queryLogs)
|
||||||
|
|
||||||
|
// 排序日志
|
||||||
|
if sortField != "" {
|
||||||
|
sort.Slice(logsCopy, func(i, j int) bool {
|
||||||
|
var a, b interface{}
|
||||||
|
switch sortField {
|
||||||
|
case "time":
|
||||||
|
a = logsCopy[i].Timestamp
|
||||||
|
b = logsCopy[j].Timestamp
|
||||||
|
case "clientIp":
|
||||||
|
a = logsCopy[i].ClientIP
|
||||||
|
b = logsCopy[j].ClientIP
|
||||||
|
case "domain":
|
||||||
|
a = logsCopy[i].Domain
|
||||||
|
b = logsCopy[j].Domain
|
||||||
|
case "responseTime":
|
||||||
|
a = logsCopy[i].ResponseTime
|
||||||
|
b = logsCopy[j].ResponseTime
|
||||||
|
case "blockRule":
|
||||||
|
a = logsCopy[i].BlockRule
|
||||||
|
b = logsCopy[j].BlockRule
|
||||||
|
default:
|
||||||
|
// 默认按时间排序
|
||||||
|
a = logsCopy[i].Timestamp
|
||||||
|
b = logsCopy[j].Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据排序方向比较
|
||||||
|
if sortDirection == "asc" {
|
||||||
|
return compareValues(a, b) < 0
|
||||||
|
}
|
||||||
|
return compareValues(a, b) > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 计算返回范围
|
// 计算返回范围
|
||||||
start := offset
|
start := offset
|
||||||
end := offset + limit
|
end := offset + limit
|
||||||
if end > len(s.queryLogs) {
|
if end > len(logsCopy) {
|
||||||
end = len(s.queryLogs)
|
end = len(logsCopy)
|
||||||
}
|
}
|
||||||
if start >= len(s.queryLogs) {
|
if start >= len(logsCopy) {
|
||||||
return []QueryLog{} // 没有数据,返回空切片
|
return []QueryLog{} // 没有数据,返回空切片
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.queryLogs[start:end]
|
return logsCopy[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareValues 比较两个值
|
||||||
|
func compareValues(a, b interface{}) int {
|
||||||
|
switch v1 := a.(type) {
|
||||||
|
case time.Time:
|
||||||
|
v2 := b.(time.Time)
|
||||||
|
if v1.Before(v2) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if v1.After(v2) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case string:
|
||||||
|
v2 := b.(string)
|
||||||
|
if v1 < v2 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if v1 > v2 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case int64:
|
||||||
|
v2 := b.(int64)
|
||||||
|
if v1 < v2 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if v1 > v2 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQueryLogsCount 获取查询日志总数
|
// GetQueryLogsCount 获取查询日志总数
|
||||||
|
|||||||
@@ -1245,6 +1245,8 @@ func (s *Server) handleLogsQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 获取查询参数
|
// 获取查询参数
|
||||||
limit := 100 // 默认返回100条日志
|
limit := 100 // 默认返回100条日志
|
||||||
offset := 0
|
offset := 0
|
||||||
|
sortField := r.URL.Query().Get("sort")
|
||||||
|
sortDirection := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||||
fmt.Sscanf(limitStr, "%d", &limit)
|
fmt.Sscanf(limitStr, "%d", &limit)
|
||||||
@@ -1255,7 +1257,7 @@ func (s *Server) handleLogsQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取日志数据
|
// 获取日志数据
|
||||||
logs := s.dnsServer.GetQueryLogs(limit, offset)
|
logs := s.dnsServer.GetQueryLogs(limit, offset, sortField, sortDirection)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(logs)
|
json.NewEncoder(w).Encode(logs)
|
||||||
|
|||||||
16466
logs/dns-server.log
Normal file
16466
logs/dns-server.log
Normal file
File diff suppressed because it is too large
Load Diff
@@ -902,11 +902,24 @@
|
|||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200">
|
<tr class="border-b border-gray-200">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">时间</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="time">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">客户端IP</th>
|
<div class="flex items-center">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th>
|
时间
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">查询类型</th>
|
<i class="fa fa-sort ml-1 text-xs"></i>
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">结果</th>
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="clientIp">
|
||||||
|
<div class="flex items-center">
|
||||||
|
客户端IP
|
||||||
|
<i class="fa fa-sort ml-1 text-xs"></i>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="domain">
|
||||||
|
<div class="flex items-center">
|
||||||
|
请求
|
||||||
|
<i class="fa fa-sort ml-1 text-xs"></i>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ let logsPerPage = 30; // 默认显示30条记录
|
|||||||
let currentFilter = '';
|
let currentFilter = '';
|
||||||
let currentSearch = '';
|
let currentSearch = '';
|
||||||
let logsChart = null;
|
let logsChart = null;
|
||||||
|
let currentSortField = '';
|
||||||
|
let currentSortDirection = 'desc'; // 默认降序
|
||||||
|
|
||||||
// 初始化查询日志页面
|
// 初始化查询日志页面
|
||||||
function initLogsPage() {
|
function initLogsPage() {
|
||||||
@@ -129,6 +131,51 @@ function bindLogsEvents() {
|
|||||||
updateLogsChart(range);
|
updateLogsChart(range);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 排序按钮事件
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载日志统计数据
|
// 加载日志统计数据
|
||||||
@@ -171,6 +218,11 @@ function loadLogs() {
|
|||||||
url += `&search=${encodeURIComponent(currentSearch)}`;
|
url += `&search=${encodeURIComponent(currentSearch)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加排序条件
|
||||||
|
if (currentSortField) {
|
||||||
|
url += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
||||||
|
}
|
||||||
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -219,7 +271,7 @@ function updateLogsTable(logs) {
|
|||||||
// 显示空状态
|
// 显示空状态
|
||||||
const emptyRow = document.createElement('tr');
|
const emptyRow = document.createElement('tr');
|
||||||
emptyRow.innerHTML = `
|
emptyRow.innerHTML = `
|
||||||
<td colspan="7" class="py-8 text-center text-gray-500">
|
<td colspan="5" class="py-8 text-center text-gray-500">
|
||||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||||
<div>暂无查询日志</div>
|
<div>暂无查询日志</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -233,42 +285,74 @@ function updateLogsTable(logs) {
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
|
||||||
const time = new Date(log.Timestamp);
|
const time = new Date(log.Timestamp);
|
||||||
const formattedTime = time.toLocaleString('zh-CN', {
|
const formattedDate = time.toLocaleDateString('zh-CN', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
const formattedTime = time.toLocaleTimeString('zh-CN', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 结果样式
|
// 根据结果添加不同的背景色
|
||||||
let resultClass = '';
|
let rowClass = '';
|
||||||
let resultText = '';
|
|
||||||
switch (log.Result) {
|
switch (log.Result) {
|
||||||
case 'allowed':
|
|
||||||
resultClass = 'text-success';
|
|
||||||
resultText = '允许';
|
|
||||||
break;
|
|
||||||
case 'blocked':
|
case 'blocked':
|
||||||
resultClass = 'text-danger';
|
rowClass = 'bg-red-50'; // 淡红色填充
|
||||||
resultText = '屏蔽';
|
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'allowed':
|
||||||
resultClass = 'text-warning';
|
// 检查是否是规则允许项目
|
||||||
resultText = '错误';
|
if (log.BlockRule && log.BlockRule.includes('allow')) {
|
||||||
|
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
|
||||||
|
} else {
|
||||||
|
rowClass = ''; // 允许的不填充
|
||||||
|
}
|
||||||
break;
|
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 = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td class="py-3 px-4 text-sm">${formattedTime}</td>
|
<td class="py-3 px-4">
|
||||||
|
<div class="text-sm font-medium">${formattedTime}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||||
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">${log.ClientIP}</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">
|
||||||
<td class="py-3 px-4 text-sm">${log.QueryType}</td>
|
<div class="text-sm font-medium">${log.Domain}</div>
|
||||||
<td class="py-3 px-4 text-sm"><span class="${resultClass}">${resultText}</span></td>
|
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||||
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</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>
|
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user