diff --git a/atsf_server/controller/access_log.go b/atsf_server/controller/access_log.go index ffd6e53d..5e2ee5cf 100644 --- a/atsf_server/controller/access_log.go +++ b/atsf_server/controller/access_log.go @@ -1,8 +1,11 @@ package controller -import "atsflare/service" +import ( + "atsflare/service" + "strconv" -import "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" +) // GetAccessLogs godoc // @Summary List access logs @@ -10,10 +13,14 @@ import "github.com/gin-gonic/gin" // @Produce json // @Security BearerAuth // @Param node_id query string false "Node ID" +// @Param p query int false "Page index" +// @Param page_size query int false "Page size" // @Success 200 {object} map[string]interface{} // @Router /api/access-logs/ [get] func GetAccessLogs(c *gin.Context) { - logs, err := service.ListAccessLogs(c.Query("node_id")) + page, _ := strconv.Atoi(c.DefaultQuery("p", "0")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "0")) + logs, err := service.ListAccessLogs(c.Query("node_id"), page, pageSize) if err != nil { respondFailure(c, err.Error()) return diff --git a/atsf_server/model/node_access_log.go b/atsf_server/model/node_access_log.go index 87086b5c..7aee1257 100644 --- a/atsf_server/model/node_access_log.go +++ b/atsf_server/model/node_access_log.go @@ -14,7 +14,7 @@ type NodeAccessLog struct { CreatedAt time.Time `json:"created_at"` } -func ListNodeAccessLogs(nodeID string, since time.Time, limit int) (logs []*NodeAccessLog, err error) { +func ListNodeAccessLogs(nodeID string, since time.Time, offset int, limit int) (logs []*NodeAccessLog, err error) { query := DB.Order("logged_at desc, id desc") if nodeID != "" { query = query.Where("node_id = ?", nodeID) @@ -22,6 +22,9 @@ func ListNodeAccessLogs(nodeID string, since time.Time, limit int) (logs []*Node if !since.IsZero() { query = query.Where("logged_at >= ?", since) } + if offset > 0 { + query = query.Offset(offset) + } if limit > 0 { query = query.Limit(limit) } diff --git a/atsf_server/service/access_log.go b/atsf_server/service/access_log.go index 2555e5f3..78a3c175 100644 --- a/atsf_server/service/access_log.go +++ b/atsf_server/service/access_log.go @@ -6,7 +6,10 @@ import ( "time" ) -const accessLogListLimit = 500 +const ( + defaultAccessLogPageSize = 50 + maxAccessLogPageSize = 200 +) type AccessLogView struct { ID uint `json:"id"` @@ -19,8 +22,23 @@ type AccessLogView struct { StatusCode int `json:"status_code"` } -func ListAccessLogs(nodeID string) ([]AccessLogView, error) { - logs, err := model.ListNodeAccessLogs(strings.TrimSpace(nodeID), time.Now().Add(-nodeAccessLogRetentionWindow), accessLogListLimit) +type AccessLogList struct { + Items []AccessLogView `json:"items"` + Page int `json:"page"` + PageSize int `json:"page_size"` + HasMore bool `json:"has_more"` +} + +func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, error) { + normalizedPage := normalizeAccessLogPage(page) + normalizedPageSize := normalizeAccessLogPageSize(pageSize) + offset := normalizedPage * normalizedPageSize + logs, err := model.ListNodeAccessLogs( + strings.TrimSpace(nodeID), + time.Now().Add(-nodeAccessLogRetentionWindow), + offset, + normalizedPageSize+1, + ) if err != nil { return nil, err } @@ -35,6 +53,10 @@ func ListAccessLogs(nodeID string) ([]AccessLogView, error) { } nodeNames[node.NodeID] = node.Name } + hasMore := len(logs) > normalizedPageSize + if hasMore { + logs = logs[:normalizedPageSize] + } views := make([]AccessLogView, 0, len(logs)) for _, item := range logs { if item == nil { @@ -51,5 +73,27 @@ func ListAccessLogs(nodeID string) ([]AccessLogView, error) { StatusCode: item.StatusCode, }) } - return views, nil + return &AccessLogList{ + Items: views, + Page: normalizedPage, + PageSize: normalizedPageSize, + HasMore: hasMore, + }, nil +} + +func normalizeAccessLogPage(page int) int { + if page < 0 { + return 0 + } + return page +} + +func normalizeAccessLogPageSize(pageSize int) int { + if pageSize <= 0 { + return defaultAccessLogPageSize + } + if pageSize > maxAccessLogPageSize { + return maxAccessLogPageSize + } + return pageSize } diff --git a/atsf_server/service/node_update_test.go b/atsf_server/service/node_update_test.go index 635dd9d2..c5e5cad3 100644 --- a/atsf_server/service/node_update_test.go +++ b/atsf_server/service/node_update_test.go @@ -673,7 +673,7 @@ func TestHeartbeatNodePersistsObservabilityPayload(t *testing.T) { t.Fatalf("unexpected request reports: %+v", reports) } - accessLogs, err := model.ListNodeAccessLogs(node.NodeID, time.Time{}, 10) + accessLogs, err := model.ListNodeAccessLogs(node.NodeID, time.Time{}, 0, 10) if err != nil { t.Fatalf("expected node access logs query to succeed: %v", err) } @@ -780,7 +780,7 @@ func TestHeartbeatNodePersistsBufferedObservabilityPayload(t *testing.T) { t.Fatalf("expected current and buffered reports, got %+v", reports) } - accessLogs, err := model.ListNodeAccessLogs(node.NodeID, time.Time{}, 10) + accessLogs, err := model.ListNodeAccessLogs(node.NodeID, time.Time{}, 0, 10) if err != nil { t.Fatalf("expected node access logs query to succeed: %v", err) } @@ -836,6 +836,75 @@ func TestHeartbeatNodePersistsBufferedObservabilityPayload(t *testing.T) { } } +func TestListAccessLogsUsesPagination(t *testing.T) { + setupServiceTestDB(t) + + node := &model.Node{ + NodeID: "node-access-log-page", + Name: "access-log-edge", + IP: "10.0.0.40", + AgentToken: "token-access-log-page", + AgentVersion: "v0.6.0", + NginxVersion: "1.27.1.2", + Status: NodeStatusOnline, + } + if err := node.Insert(); err != nil { + t.Fatalf("failed to seed node: %v", err) + } + + now := time.Now().UTC() + if err := model.DB.Create([]*model.NodeAccessLog{ + { + NodeID: node.NodeID, + LoggedAt: now.Add(-10 * time.Second), + RemoteAddr: "203.0.113.1", + Host: "example.com", + Path: "/one", + StatusCode: 200, + }, + { + NodeID: node.NodeID, + LoggedAt: now.Add(-9 * time.Second), + RemoteAddr: "203.0.113.2", + Host: "example.com", + Path: "/two", + StatusCode: 200, + }, + { + NodeID: node.NodeID, + LoggedAt: now.Add(-8 * time.Second), + RemoteAddr: "203.0.113.3", + Host: "example.com", + Path: "/three", + StatusCode: 502, + }, + }).Error; err != nil { + t.Fatalf("failed to seed access logs: %v", err) + } + + pageOne, err := ListAccessLogs(node.NodeID, 0, 2) + if err != nil { + t.Fatalf("ListAccessLogs page 1 failed: %v", err) + } + if len(pageOne.Items) != 2 || !pageOne.HasMore { + t.Fatalf("unexpected first page: %+v", pageOne) + } + if pageOne.Items[0].Path != "/three" || pageOne.Items[1].Path != "/two" { + t.Fatalf("unexpected first page ordering: %+v", pageOne.Items) + } + + pageTwo, err := ListAccessLogs(node.NodeID, 1, 2) + if err != nil { + t.Fatalf("ListAccessLogs page 2 failed: %v", err) + } + if len(pageTwo.Items) != 1 || pageTwo.HasMore { + t.Fatalf("unexpected second page: %+v", pageTwo) + } + if pageTwo.Items[0].Path != "/one" { + t.Fatalf("unexpected second page ordering: %+v", pageTwo.Items) + } +} + func TestHeartbeatNodeResolvesMissingHealthEvents(t *testing.T) { setupServiceTestDB(t) diff --git a/atsf_server/web/features/access-logs/api/access-logs.ts b/atsf_server/web/features/access-logs/api/access-logs.ts index adf8edaf..ee3a9c34 100644 --- a/atsf_server/web/features/access-logs/api/access-logs.ts +++ b/atsf_server/web/features/access-logs/api/access-logs.ts @@ -1,11 +1,15 @@ import { apiRequest } from '@/lib/api/client'; -import type { AccessLogItem } from '@/features/access-logs/types'; +import type { AccessLogList } from '@/features/access-logs/types'; -export function getAccessLogs(nodeId?: string) { +export function getAccessLogs(page: number, nodeId?: string, pageSize = 50) { const normalizedNodeId = nodeId?.trim(); - const query = normalizedNodeId - ? `?node_id=${encodeURIComponent(normalizedNodeId)}` - : ''; - return apiRequest(`/access-logs/${query}`); + const searchParams = new URLSearchParams({ + p: String(Math.max(page, 0)), + page_size: String(pageSize), + }); + if (normalizedNodeId) { + searchParams.set('node_id', normalizedNodeId); + } + return apiRequest(`/access-logs/?${searchParams.toString()}`); } diff --git a/atsf_server/web/features/access-logs/components/access-logs-page.tsx b/atsf_server/web/features/access-logs/components/access-logs-page.tsx index dbd45661..150d844f 100644 --- a/atsf_server/web/features/access-logs/components/access-logs-page.tsx +++ b/atsf_server/web/features/access-logs/components/access-logs-page.tsx @@ -54,13 +54,16 @@ export function AccessLogsPage() { const queryClient = useQueryClient(); const [nodeFilterInput, setNodeFilterInput] = useState(''); const [nodeFilter, setNodeFilter] = useState(''); + const [page, setPage] = useState(0); const logsQuery = useQuery({ - queryKey: accessLogsQueryKey(nodeFilter), - queryFn: () => getAccessLogs(nodeFilter), + queryKey: [...accessLogsQueryKey(nodeFilter), page], + queryFn: () => getAccessLogs(page, nodeFilter), }); - const logs = useMemo(() => logsQuery.data ?? [], [logsQuery.data]); + 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]); return ( @@ -126,7 +129,10 @@ export function AccessLogsPage() {
setNodeFilter(nodeFilterInput.trim())} + onClick={() => { + setNodeFilter(nodeFilterInput.trim()); + setPage(0); + }} > 筛选 @@ -135,6 +141,7 @@ export function AccessLogsPage() { onClick={() => { setNodeFilter(''); setNodeFilterInput(''); + setPage(0); }} > 清空 @@ -209,6 +216,27 @@ export function AccessLogsPage() {
)} +
+

+ 第 {page + 1} 页,每页 {pageSize} 条。 +

+
+ setPage((value) => Math.max(value - 1, 0))} + > + 上一页 + + setPage((value) => value + 1)} + > + 下一页 + +
+
diff --git a/atsf_server/web/features/access-logs/types.ts b/atsf_server/web/features/access-logs/types.ts index a3c2b214..65a0b080 100644 --- a/atsf_server/web/features/access-logs/types.ts +++ b/atsf_server/web/features/access-logs/types.ts @@ -8,3 +8,10 @@ export interface AccessLogItem { path: string; status_code: number; } + +export interface AccessLogList { + items: AccessLogItem[]; + page: number; + page_size: number; + has_more: boolean; +} diff --git a/atsf_server/web/features/nodes/components/node-detail-page.tsx b/atsf_server/web/features/nodes/components/node-detail-page.tsx index 3e6e2fe4..f68693b9 100644 --- a/atsf_server/web/features/nodes/components/node-detail-page.tsx +++ b/atsf_server/web/features/nodes/components/node-detail-page.tsx @@ -733,10 +733,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { /> -
+
{observabilityQuery.isLoading ? (