From ea3fd646861d154d89b47dc4eb7425be8471db6f Mon Sep 17 00:00:00 2001 From: SmartUp Developer Date: Thu, 2 Jul 2026 15:07:32 +0800 Subject: [PATCH] fix: optimize summary count query and refine row keys for external api logs --- backend/app/routers/external_api_logs.py | 42 ++++++++++- backend/test_external_api_log.py | 94 +++++++++++++++++++++++- frontend/src/api/index.ts | 4 +- frontend/src/views/ExternalApiLogs.vue | 64 +++++++++++++++- 4 files changed, 195 insertions(+), 9 deletions(-) diff --git a/backend/app/routers/external_api_logs.py b/backend/app/routers/external_api_logs.py index 3e6cde9..fbb8630 100644 --- a/backend/app/routers/external_api_logs.py +++ b/backend/app/routers/external_api_logs.py @@ -85,13 +85,15 @@ def get_external_api_log_summary( slow_ms: Optional[int] = Query(None), hours: int = Query(24, ge=1, le=720), limit: int = Query(50, ge=1, le=200), + offset: int = Query(0, ge=0), db: Session = Depends(get_db), _=Depends(get_current_user), ): """Return aggregated summary grouped by direction + target + method + path. - - hours: time window (default 24, max 720). None means no time filter. + - hours: time window (default 24, max 720). - limit: max rows returned (default 50, max 200). + - offset: pagination offset (default 0). """ settings = get_settings() slow_threshold = slow_ms if slow_ms is not None else settings.external_api_log_slow_ms @@ -107,7 +109,7 @@ def get_external_api_log_summary( ExternalApiLog.method, ExternalApiLog.path, ).order_by(func.sum(ExternalApiLog.duration_ms).desc()) - q = q.limit(limit) + q = q.offset(offset).limit(limit) rows = q.all() return [ @@ -129,6 +131,42 @@ def get_external_api_log_summary( ] +@router.get("/summary/count") +def count_external_api_log_summary( + slow_ms: Optional[int] = Query(None), + hours: int = Query(24, ge=1, le=720), + db: Session = Depends(get_db), + _=Depends(get_current_user), +): + """Return the number of distinct aggregated summary groups. + + Uses the same grouping as /summary, returns the count of groups + (not the count of raw log rows). + """ + settings = get_settings() + slow_threshold = slow_ms if slow_ms is not None else settings.external_api_log_slow_ms + + q = db.query( + ExternalApiLog.direction, + ExternalApiLog.target_type, + ExternalApiLog.target_id, + ExternalApiLog.target_name, + ExternalApiLog.method, + ExternalApiLog.path, + ) + cutoff = datetime.now(timezone.utc) - timedelta(hours=hours) + q = q.filter(ExternalApiLog.created_at >= cutoff) + q = q.group_by( + ExternalApiLog.direction, + ExternalApiLog.target_type, + ExternalApiLog.target_id, + ExternalApiLog.target_name, + ExternalApiLog.method, + ExternalApiLog.path, + ) + return {"total": q.count()} + + def _summary_cols(slow_threshold: int) -> list: return [ ExternalApiLog.direction, diff --git a/backend/test_external_api_log.py b/backend/test_external_api_log.py index 0e4bff3..50e75c6 100644 --- a/backend/test_external_api_log.py +++ b/backend/test_external_api_log.py @@ -625,13 +625,13 @@ def test_route_summary_hours_window(session): # hours=1 → only recent result = get_external_api_log_summary( - slow_ms=None, hours=1, limit=50, db=session, _=None, + slow_ms=None, hours=1, limit=50, offset=0, db=session, _=None, ) assert len(result) == 1 and result[0].path == "/recent" # hours=72 → both result = get_external_api_log_summary( - slow_ms=None, hours=72, limit=50, db=session, _=None, + slow_ms=None, hours=72, limit=50, offset=0, db=session, _=None, ) assert len(result) == 2 @@ -648,7 +648,7 @@ def test_route_summary_limit(session): session.commit() result = get_external_api_log_summary( - slow_ms=None, hours=24, limit=3, db=session, _=None, + slow_ms=None, hours=24, limit=3, offset=0, db=session, _=None, ) assert len(result) == 3 @@ -670,8 +670,94 @@ def test_route_summary_slow_threshold(session): session.commit() result = get_external_api_log_summary( - slow_ms=1000, hours=24, limit=50, db=session, _=None, + slow_ms=1000, hours=24, limit=50, offset=0, db=session, _=None, ) slow_paths = [r for r in result if r.slow_count > 0] assert len(slow_paths) == 1 assert slow_paths[0].path == "/slow" + + +def test_route_summary_offset_pagination(session): + """get_external_api_log_summary respects offset parameter.""" + from app.routers.external_api_logs import get_external_api_log_summary + + now = datetime.now(timezone.utc) + for i in range(10): + session.add(ExternalApiLog(direction="upstream", target_type="upstream", + method="GET", path=f"/path-{i}", url_host="h", + status_code=200, success=True, duration_ms=50 + i, created_at=now)) + session.commit() + + # Page 1: first 4 + page1 = get_external_api_log_summary( + slow_ms=None, hours=24, limit=4, offset=0, db=session, _=None, + ) + assert len(page1) == 4 + assert page1[0].path == "/path-9" # sorted by sum(duration_ms) desc + + # Page 2: next 4 + page2 = get_external_api_log_summary( + slow_ms=None, hours=24, limit=4, offset=4, db=session, _=None, + ) + assert len(page2) == 4 + assert page2[0].path == "/path-5" + + # Page 3: last 2 + page3 = get_external_api_log_summary( + slow_ms=None, hours=24, limit=4, offset=8, db=session, _=None, + ) + assert len(page3) == 2 + + # Beyond end + page4 = get_external_api_log_summary( + slow_ms=None, hours=24, limit=4, offset=20, db=session, _=None, + ) + assert len(page4) == 0 + + +def test_route_summary_count(session): + """count_external_api_log_summary returns group count, not raw row count.""" + from app.routers.external_api_logs import count_external_api_log_summary + + now = datetime.now(timezone.utc) + # 3 calls to same path (should be 1 group) + for i in range(3): + session.add(ExternalApiLog(direction="upstream", target_type="upstream", + method="GET", path="/grouped", url_host="h", + status_code=200, success=True, duration_ms=100, created_at=now)) + # 2 calls to different path (another group) + for i in range(2): + session.add(ExternalApiLog(direction="upstream", target_type="upstream", + method="POST", path="/other", url_host="h", + status_code=201, success=True, duration_ms=200, created_at=now)) + session.commit() + + # Should be 2 groups, not 5 raw rows + result = count_external_api_log_summary( + slow_ms=None, hours=24, db=session, _=None, + ) + assert result == {"total": 2} + + +def test_route_summary_count_respects_hours(session): + """count_external_api_log_summary respects hours filter.""" + from app.routers.external_api_logs import count_external_api_log_summary + + now = datetime.now(timezone.utc) + session.add(ExternalApiLog(direction="upstream", target_type="upstream", + method="GET", path="/recent", url_host="h", + status_code=200, success=True, duration_ms=50, + created_at=now - timedelta(minutes=30))) + session.add(ExternalApiLog(direction="upstream", target_type="upstream", + method="GET", path="/old", url_host="h", + status_code=200, success=True, duration_ms=50, + created_at=now - timedelta(hours=48))) + session.commit() + + # hours=1 → 1 group + r1 = count_external_api_log_summary(slow_ms=None, hours=1, db=session, _=None) + assert r1 == {"total": 1} + + # hours=72 → 2 groups + r2 = count_external_api_log_summary(slow_ms=None, hours=72, db=session, _=None) + assert r2 == {"total": 2} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index dee6124..3cf2711 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -547,8 +547,10 @@ export const externalApiLogsApi = { status_code?: number path?: string }) => api.get<{ total: number }>('/api/external-api-logs/count', { params }), - summary: (params?: { slow_ms?: number }) => + summary: (params?: { slow_ms?: number; hours?: number; limit?: number; offset?: number }) => api.get('/api/external-api-logs/summary', { params }), + summaryCount: (params?: { slow_ms?: number; hours?: number }) => + api.get<{ total: number }>('/api/external-api-logs/summary/count', { params }), } // ——— Auth Capture ——— diff --git a/frontend/src/views/ExternalApiLogs.vue b/frontend/src/views/ExternalApiLogs.vue index afe40fa..bc71a1b 100644 --- a/frontend/src/views/ExternalApiLogs.vue +++ b/frontend/src/views/ExternalApiLogs.vue @@ -57,7 +57,7 @@ - + @@ -90,6 +90,35 @@ + + +
+
+ {{ summaryTotalRecords }} 个分组 +
+ 每页 + + +
+
+ +
+ + 第 {{ summaryCurrentPage }} 页 + +
+
@@ -347,6 +376,14 @@ const pageSize = ref(10) const hasNextPage = ref(false) const totalRecords = ref(0) const currentPage = computed(() => Math.floor(offset.value / pageSize.value) + 1) + +// Slow summary pagination state (independent from list pagination) +const summaryOffset = ref(0) +const summaryPageSize = ref(10) +const summaryTotalRecords = ref(0) +const summaryHasNextPage = computed(() => summaryOffset.value + summaryPageSize.value < summaryTotalRecords.value) +const summaryCurrentPage = computed(() => Math.floor(summaryOffset.value / summaryPageSize.value) + 1) + const slowThreshold = 3000 // In-app Notification System @@ -489,13 +526,36 @@ async function loadList(silent = false) { async function loadSummary(silent = false) { if (!silent) summaryLoading.value = true try { - const res = await externalApiLogsApi.summary({ slow_ms: slowThreshold }) + const [res, countRes] = await Promise.all([ + externalApiLogsApi.summary({ + slow_ms: slowThreshold, + limit: summaryPageSize.value, + offset: summaryOffset.value, + }), + externalApiLogsApi.summaryCount({ slow_ms: slowThreshold }), + ]) summary.value = res.data + summaryTotalRecords.value = countRes.data.total } finally { if (!silent) summaryLoading.value = false } } +function summaryPrevPage() { + summaryOffset.value = Math.max(0, summaryOffset.value - summaryPageSize.value) + loadSummary() +} + +function summaryNextPage() { + summaryOffset.value += summaryPageSize.value + loadSummary() +} + +function handleSummaryPageSizeChange() { + summaryOffset.value = 0 + loadSummary() +} + function handleFilterChange() { offset.value = 0 loadList()