mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
[优化] 添加访问日志汇总功能,更新相关数据结构和测试用例
This commit is contained in:
@@ -31,3 +31,23 @@ func ListNodeAccessLogs(nodeID string, since time.Time, offset int, limit int) (
|
|||||||
err = query.Find(&logs).Error
|
err = query.Find(&logs).Error
|
||||||
return logs, err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,25 +23,33 @@ type AccessLogView struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccessLogList struct {
|
type AccessLogList struct {
|
||||||
Items []AccessLogView `json:"items"`
|
Items []AccessLogView `json:"items"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
HasMore bool `json:"has_more"`
|
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) {
|
func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, error) {
|
||||||
normalizedPage := normalizeAccessLogPage(page)
|
normalizedPage := normalizeAccessLogPage(page)
|
||||||
normalizedPageSize := normalizeAccessLogPageSize(pageSize)
|
normalizedPageSize := normalizeAccessLogPageSize(pageSize)
|
||||||
offset := normalizedPage * normalizedPageSize
|
offset := normalizedPage * normalizedPageSize
|
||||||
|
trimmedNodeID := strings.TrimSpace(nodeID)
|
||||||
|
since := time.Now().Add(-nodeAccessLogRetentionWindow)
|
||||||
logs, err := model.ListNodeAccessLogs(
|
logs, err := model.ListNodeAccessLogs(
|
||||||
strings.TrimSpace(nodeID),
|
trimmedNodeID,
|
||||||
time.Now().Add(-nodeAccessLogRetentionWindow),
|
since,
|
||||||
offset,
|
offset,
|
||||||
normalizedPageSize+1,
|
normalizedPageSize+1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
totalRecords, totalIPs, err := model.CountNodeAccessLogs(trimmedNodeID, since)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
nodes, err := model.ListNodes()
|
nodes, err := model.ListNodes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -74,10 +82,12 @@ func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, erro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return &AccessLogList{
|
return &AccessLogList{
|
||||||
Items: views,
|
Items: views,
|
||||||
Page: normalizedPage,
|
Page: normalizedPage,
|
||||||
PageSize: normalizedPageSize,
|
PageSize: normalizedPageSize,
|
||||||
HasMore: hasMore,
|
HasMore: hasMore,
|
||||||
|
TotalRecord: totalRecords,
|
||||||
|
TotalIP: totalIPs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 : '请求失败,请稍后重试。';
|
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSummary(logs: AccessLogItem[]) {
|
function buildSummary(totalRecord = 0, totalIP = 0) {
|
||||||
const uniqueIPs = new Set(logs.map((item) => item.remote_addr).filter(Boolean));
|
|
||||||
const uniqueNodes = new Set(logs.map((item) => item.node_id).filter(Boolean));
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: '访问记录', value: logs.length },
|
{ label: '访问记录', value: totalRecord },
|
||||||
{ label: '来源 IP', value: uniqueIPs.size },
|
{ label: '来源 IP', value: totalIP },
|
||||||
{ label: '命中节点', value: uniqueNodes.size },
|
|
||||||
{
|
|
||||||
label: '5xx 响应',
|
|
||||||
value: logs.filter((item) => item.status_code >= 500).length,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +56,14 @@ export function AccessLogsPage() {
|
|||||||
const logs = useMemo(() => logsQuery.data?.items ?? [], [logsQuery.data]);
|
const logs = useMemo(() => logsQuery.data?.items ?? [], [logsQuery.data]);
|
||||||
const hasMore = logsQuery.data?.has_more ?? false;
|
const hasMore = logsQuery.data?.has_more ?? false;
|
||||||
const pageSize = logsQuery.data?.page_size ?? 50;
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -83,9 +82,9 @@ export function AccessLogsPage() {
|
|||||||
|
|
||||||
<AppCard
|
<AppCard
|
||||||
title="日志摘要"
|
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) => (
|
{summary.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ export interface AccessLogList {
|
|||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
has_more: boolean;
|
has_more: boolean;
|
||||||
|
total_record: number;
|
||||||
|
total_ip: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user