[优化] 添加访问日志汇总功能,更新相关数据结构和测试用例

This commit is contained in:
ryan
2026-03-15 14:19:21 +08:00
parent 25004fefae
commit 5bb25d2203
5 changed files with 149 additions and 24 deletions
+20
View File
@@ -31,3 +31,23 @@ func ListNodeAccessLogs(nodeID string, since time.Time, offset int, limit int) (
err = query.Find(&logs).Error
return logs, err
}
func CountNodeAccessLogs(nodeID string, since time.Time) (totalRecords int64, totalIPs int64, err error) {
query := DB.Model(&NodeAccessLog{})
if nodeID != "" {
query = query.Where("node_id = ?", nodeID)
}
if !since.IsZero() {
query = query.Where("logged_at >= ?", since)
}
if err = query.Count(&totalRecords).Error; err != nil {
return 0, 0, err
}
if err = query.
Where("remote_addr <> ''").
Distinct("remote_addr").
Count(&totalIPs).Error; err != nil {
return 0, 0, err
}
return totalRecords, totalIPs, nil
}
+20 -10
View File
@@ -23,25 +23,33 @@ type AccessLogView struct {
}
type AccessLogList struct {
Items []AccessLogView `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
HasMore bool `json:"has_more"`
Items []AccessLogView `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
HasMore bool `json:"has_more"`
TotalRecord int64 `json:"total_record"`
TotalIP int64 `json:"total_ip"`
}
func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, error) {
normalizedPage := normalizeAccessLogPage(page)
normalizedPageSize := normalizeAccessLogPageSize(pageSize)
offset := normalizedPage * normalizedPageSize
trimmedNodeID := strings.TrimSpace(nodeID)
since := time.Now().Add(-nodeAccessLogRetentionWindow)
logs, err := model.ListNodeAccessLogs(
strings.TrimSpace(nodeID),
time.Now().Add(-nodeAccessLogRetentionWindow),
trimmedNodeID,
since,
offset,
normalizedPageSize+1,
)
if err != nil {
return nil, err
}
totalRecords, totalIPs, err := model.CountNodeAccessLogs(trimmedNodeID, since)
if err != nil {
return nil, err
}
nodes, err := model.ListNodes()
if err != nil {
return nil, err
@@ -74,10 +82,12 @@ func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, erro
})
}
return &AccessLogList{
Items: views,
Page: normalizedPage,
PageSize: normalizedPageSize,
HasMore: hasMore,
Items: views,
Page: normalizedPage,
PageSize: normalizedPageSize,
HasMore: hasMore,
TotalRecord: totalRecords,
TotalIP: totalIPs,
}, nil
}
+94
View File
@@ -0,0 +1,94 @@
package service
import (
"atsflare/model"
"testing"
"time"
)
func TestListAccessLogsIncludesSummaryTotals(t *testing.T) {
setupServiceTestDB(t)
now := time.Now()
if err := model.DB.Create(&model.Node{
NodeID: "node-a",
Name: "edge-a",
}).Error; err != nil {
t.Fatalf("failed to seed node-a: %v", err)
}
if err := model.DB.Create(&model.Node{
NodeID: "node-b",
Name: "edge-b",
}).Error; err != nil {
t.Fatalf("failed to seed node-b: %v", err)
}
logs := []*model.NodeAccessLog{
{
NodeID: "node-a",
LoggedAt: now.Add(-5 * time.Minute),
RemoteAddr: "1.1.1.1",
Host: "a.example.com",
Path: "/alpha",
StatusCode: 200,
},
{
NodeID: "node-a",
LoggedAt: now.Add(-4 * time.Minute),
RemoteAddr: "2.2.2.2",
Host: "a.example.com",
Path: "/beta",
StatusCode: 404,
},
{
NodeID: "node-b",
LoggedAt: now.Add(-3 * time.Minute),
RemoteAddr: "1.1.1.1",
Host: "b.example.com",
Path: "/gamma",
StatusCode: 502,
},
{
NodeID: "node-b",
LoggedAt: now.Add(-2 * time.Minute),
RemoteAddr: "",
Host: "b.example.com",
Path: "/delta",
StatusCode: 200,
},
}
if err := model.DB.Create(&logs).Error; err != nil {
t.Fatalf("failed to seed access logs: %v", err)
}
result, err := ListAccessLogs("", 0, 2)
if err != nil {
t.Fatalf("ListAccessLogs failed: %v", err)
}
if result.TotalRecord != 4 {
t.Fatalf("expected total_record=4, got %d", result.TotalRecord)
}
if result.TotalIP != 2 {
t.Fatalf("expected total_ip=2, got %d", result.TotalIP)
}
if len(result.Items) != 2 {
t.Fatalf("expected current page items=2, got %d", len(result.Items))
}
if !result.HasMore {
t.Fatal("expected has_more to be true")
}
filtered, err := ListAccessLogs("node-a", 0, 50)
if err != nil {
t.Fatalf("ListAccessLogs filtered failed: %v", err)
}
if filtered.TotalRecord != 2 {
t.Fatalf("expected filtered total_record=2, got %d", filtered.TotalRecord)
}
if filtered.TotalIP != 2 {
t.Fatalf("expected filtered total_ip=2, got %d", filtered.TotalIP)
}
if len(filtered.Items) != 2 {
t.Fatalf("expected filtered items=2, got %d", len(filtered.Items))
}
}
@@ -25,18 +25,10 @@ function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
}
function buildSummary(logs: AccessLogItem[]) {
const uniqueIPs = new Set(logs.map((item) => item.remote_addr).filter(Boolean));
const uniqueNodes = new Set(logs.map((item) => item.node_id).filter(Boolean));
function buildSummary(totalRecord = 0, totalIP = 0) {
return [
{ label: '访问记录', value: logs.length },
{ label: '来源 IP', value: uniqueIPs.size },
{ label: '命中节点', value: uniqueNodes.size },
{
label: '5xx 响应',
value: logs.filter((item) => item.status_code >= 500).length,
},
{ label: '访问记录', value: totalRecord },
{ label: '来源 IP', value: totalIP },
];
}
@@ -64,7 +56,14 @@ export function AccessLogsPage() {
const logs = useMemo(() => logsQuery.data?.items ?? [], [logsQuery.data]);
const hasMore = logsQuery.data?.has_more ?? false;
const pageSize = logsQuery.data?.page_size ?? 50;
const summary = useMemo(() => buildSummary(logs), [logs]);
const summary = useMemo(
() =>
buildSummary(
logsQuery.data?.total_record ?? 0,
logsQuery.data?.total_ip ?? 0,
),
[logsQuery.data?.total_ip, logsQuery.data?.total_record],
);
return (
<div className="space-y-6">
@@ -83,9 +82,9 @@ export function AccessLogsPage() {
<AppCard
title="日志摘要"
description="帮助快速了解最近访问量、来源范围和异常响应规模。"
description="基于当前筛选条件汇总最近时间窗口内的访问记录与来源 IP 规模。"
>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-2">
{summary.map((item) => (
<div
key={item.label}
@@ -14,4 +14,6 @@ export interface AccessLogList {
page: number;
page_size: number;
has_more: boolean;
total_record: number;
total_ip: number;
}