From 177578ef4e0c60df6cd1e63fc4c2bd84950453ab Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 18 Jul 2026 20:58:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(access-logs):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E6=97=A5=E5=BF=97=E4=B8=BA=E6=A6=82=E8=A7=88?= =?UTF-8?q?=E4=B8=8E=E6=98=8E=E7=BB=86=E5=8F=8C=20Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增访问日志概览 API 与前端页面:汇总请求量/访问量/带宽趋势与 Top 排行, 明细列表保留检索;排行榜改为紧凑列表样式。 --- docs/changelog/index.md | 4 + docs/docs.go | 164 ++++ docs/swagger.json | 164 ++++ docs/swagger.yaml | 103 +++ .../components/access-log-filters.tsx | 36 +- .../components/access-log-utils.ts | 48 +- .../access-logs/components/detail-tab.tsx | 164 ++++ .../access-logs/components/overview-tab.tsx | 348 +++++++ frontend/app/(main)/access-logs/page.tsx | 611 ++---------- .../dashboard/data/country-centroids.json | 875 ++++-------------- frontend/components/data/rank-chart.tsx | 138 +-- frontend/lib/services/index.ts | 1 + .../services/openflare/access-log.service.ts | 8 + frontend/lib/services/openflare/index.ts | 3 + frontend/lib/services/openflare/types.ts | 29 + .../observability/access_log_logics.go | 174 ++++ .../apps/openflare/observability/analytics.go | 5 +- .../apps/openflare/observability/routers.go | 27 + internal/model/openflare_access_log.go | 2 +- .../openflare_access_log_store_memory.go | 15 +- .../repository/analytics/node_access_log.go | 29 +- .../v1/openflare/register_observability.go | 1 + pkg/geoip/country_centroid_test.go | 12 +- pkg/protocol/agent.go | 4 +- 24 files changed, 1546 insertions(+), 1419 deletions(-) create mode 100644 frontend/app/(main)/access-logs/components/detail-tab.tsx create mode 100644 frontend/app/(main)/access-logs/components/overview-tab.tsx diff --git a/docs/changelog/index.md b/docs/changelog/index.md index c5f4c1b0..938a5e27 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -21,6 +21,10 @@ sidebar: false ## [unreleased] +### 变更 + +- 访问日志页面重构为「概览」与「日志明细」两个标签页:概览展示请求量、访问量、带宽趋势与 Top Paths/Hosts/IPs,明细列表保留检索能力;已移除时间折叠与 IP 汇总视图。 + ## [v3.4.0] - 2026-07-18 ### 新增 diff --git a/docs/docs.go b/docs/docs.go index e8a11a8b..ea3eced7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5408,6 +5408,87 @@ const docTemplate = `{ } } }, + "/api/v1/d/access-logs/overview": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回访问日志汇总指标、趋势与 Top 排行,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "获取访问日志概览", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "统计时间范围(小时)", + "name": "hours", + "in": "query" + } + ], + "responses": { + "200": { + "description": "访问日志概览", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogOverview" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/d/acme-accounts/default": { "get": { "security": [ @@ -16148,6 +16229,89 @@ const docTemplate = `{ } } }, + "observability.AccessLogOverview": { + "type": "object", + "properties": { + "generated_at": { + "type": "string" + }, + "hours": { + "type": "integer" + }, + "summary": { + "$ref": "#/definitions/observability.AccessLogOverviewSummary" + }, + "top_hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "top_ips": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "top_paths": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "trends": { + "$ref": "#/definitions/observability.AccessLogOverviewTrends" + } + } + }, + "observability.AccessLogOverviewMetricPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "observability.AccessLogOverviewSummary": { + "type": "object", + "properties": { + "bandwidth_served": { + "type": "integer" + }, + "total_requests": { + "type": "integer" + }, + "total_visits": { + "type": "integer" + } + } + }, + "observability.AccessLogOverviewTrends": { + "type": "object", + "properties": { + "bandwidth": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogOverviewMetricPoint" + } + }, + "requests": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogOverviewMetricPoint" + } + }, + "visits": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogOverviewMetricPoint" + } + } + } + }, "observability.AccessLogView": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index c370a52f..5e1813d6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5401,6 +5401,87 @@ } } }, + "/api/v1/d/access-logs/overview": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回访问日志汇总指标、趋势与 Top 排行,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "获取访问日志概览", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "统计时间范围(小时)", + "name": "hours", + "in": "query" + } + ], + "responses": { + "200": { + "description": "访问日志概览", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogOverview" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/d/acme-accounts/default": { "get": { "security": [ @@ -16141,6 +16222,89 @@ } } }, + "observability.AccessLogOverview": { + "type": "object", + "properties": { + "generated_at": { + "type": "string" + }, + "hours": { + "type": "integer" + }, + "summary": { + "$ref": "#/definitions/observability.AccessLogOverviewSummary" + }, + "top_hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "top_ips": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "top_paths": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "trends": { + "$ref": "#/definitions/observability.AccessLogOverviewTrends" + } + } + }, + "observability.AccessLogOverviewMetricPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "observability.AccessLogOverviewSummary": { + "type": "object", + "properties": { + "bandwidth_served": { + "type": "integer" + }, + "total_requests": { + "type": "integer" + }, + "total_visits": { + "type": "integer" + } + } + }, + "observability.AccessLogOverviewTrends": { + "type": "object", + "properties": { + "bandwidth": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogOverviewMetricPoint" + } + }, + "requests": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogOverviewMetricPoint" + } + }, + "visits": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogOverviewMetricPoint" + } + } + } + }, "observability.AccessLogView": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5020e0dd..63b596dd 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1937,6 +1937,60 @@ definitions: total_record: type: integer type: object + observability.AccessLogOverview: + properties: + generated_at: + type: string + hours: + type: integer + summary: + $ref: '#/definitions/observability.AccessLogOverviewSummary' + top_hosts: + items: + $ref: '#/definitions/observability.DistributionItem' + type: array + top_ips: + items: + $ref: '#/definitions/observability.DistributionItem' + type: array + top_paths: + items: + $ref: '#/definitions/observability.DistributionItem' + type: array + trends: + $ref: '#/definitions/observability.AccessLogOverviewTrends' + type: object + observability.AccessLogOverviewMetricPoint: + properties: + bucket_started_at: + type: string + value: + type: integer + type: object + observability.AccessLogOverviewSummary: + properties: + bandwidth_served: + type: integer + total_requests: + type: integer + total_visits: + type: integer + type: object + observability.AccessLogOverviewTrends: + properties: + bandwidth: + items: + $ref: '#/definitions/observability.AccessLogOverviewMetricPoint' + type: array + requests: + items: + $ref: '#/definitions/observability.AccessLogOverviewMetricPoint' + type: array + visits: + items: + $ref: '#/definitions/observability.AccessLogOverviewMetricPoint' + type: array + type: object observability.AccessLogView: properties: host: @@ -7160,6 +7214,55 @@ paths: summary: 获取访问日志 IP 趋势 tags: - openflare-observability + /api/v1/d/access-logs/overview: + get: + description: 返回访问日志汇总指标、趋势与 Top 排行,需要管理员权限 + parameters: + - description: 节点 ID + in: query + name: node_id + type: string + - description: 请求 Host + in: query + name: host + type: string + - description: 统计时间范围(小时) + in: query + name: hours + type: integer + produces: + - application/json + responses: + "200": + description: 访问日志概览 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.AccessLogOverview' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取访问日志概览 + tags: + - openflare-observability /api/v1/d/acme-accounts/default: get: description: 返回系统默认 ACME 账号配置,需要管理员权限 diff --git a/frontend/app/(main)/access-logs/components/access-log-filters.tsx b/frontend/app/(main)/access-logs/components/access-log-filters.tsx index 938c183b..c06fd6d9 100644 --- a/frontend/app/(main)/access-logs/components/access-log-filters.tsx +++ b/frontend/app/(main)/access-logs/components/access-log-filters.tsx @@ -11,11 +11,10 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import type { AccessLogTab, SearchDraft } from './access-log-utils'; +import type { SearchDraft } from './access-log-utils'; import { PAGE_SIZE_OPTIONS } from './access-log-utils'; interface AccessLogFiltersProps { - tab: AccessLogTab; draft: SearchDraft; pageSize: number; onDraftChange: (draft: SearchDraft) => void; @@ -25,7 +24,6 @@ interface AccessLogFiltersProps { } export function AccessLogFilters({ - tab, draft, pageSize, onDraftChange, @@ -33,8 +31,6 @@ export function AccessLogFilters({ onSearch, onReset, }: AccessLogFiltersProps) { - const showPath = tab === 'list' || tab === 'folds'; - return (
@@ -81,24 +77,18 @@ export function AccessLogFilters({ className='h-9 text-xs' />
- {showPath ? ( -
-

- 请求路径 -

- - onDraftChange({ ...draft, path: e.target.value }) - } - onKeyDown={(e) => { - if (e.key === 'Enter') onSearch(); - }} - placeholder='按路径搜索' - className='h-9 text-xs' - /> -
- ) : null} +
+

请求路径

+ onDraftChange({ ...draft, path: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter') onSearch(); + }} + placeholder='按路径搜索' + className='h-9 text-xs' + /> +
diff --git a/frontend/app/(main)/access-logs/components/access-log-utils.ts b/frontend/app/(main)/access-logs/components/access-log-utils.ts index 426a1da8..c60852c7 100644 --- a/frontend/app/(main)/access-logs/components/access-log-utils.ts +++ b/frontend/app/(main)/access-logs/components/access-log-utils.ts @@ -1,4 +1,4 @@ -export type AccessLogTab = 'list' | 'folds' | 'ip-summary' | 'ip-trend'; +export type AccessLogTab = 'overview' | 'list'; export type SearchDraft = { nodeId: string; @@ -7,8 +7,20 @@ export type SearchDraft = { path: string; }; +export type OverviewRangeHours = 24 | 168 | 360 | 720; + export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200]; +export const OVERVIEW_RANGE_OPTIONS: { + value: OverviewRangeHours; + label: string; +}[] = [ + { value: 24, label: '24 小时' }, + { value: 168, label: '7 天' }, + { value: 360, label: '15 天' }, + { value: 720, label: '30 天' }, +]; + export const DETAIL_SORT_OPTIONS = [ { value: 'logged_at:desc', label: '时间从新到旧' }, { value: 'logged_at:asc', label: '时间从旧到新' }, @@ -18,20 +30,6 @@ export const DETAIL_SORT_OPTIONS = [ { value: 'remote_addr:desc', label: 'IP 倒序' }, ]; -export const FOLD_SORT_OPTIONS = [ - { value: 'bucket_started_at:desc', label: '时间桶从新到旧' }, - { value: 'bucket_started_at:asc', label: '时间桶从旧到新' }, - { value: 'request_count:desc', label: '访问次数从高到低' }, - { value: 'request_count:asc', label: '访问次数从低到高' }, -]; - -export const IP_SORT_OPTIONS = [ - { value: 'total_requests:desc', label: '总访问次数从高到低' }, - { value: 'total_requests:asc', label: '总访问次数从低到高' }, - { value: 'recent_requests:desc', label: '3 小时访问次数从高到低' }, - { value: 'last_seen_at:desc', label: '最后访问时间从新到旧' }, -]; - export function parseSortValue(value: string) { const [sortBy = 'logged_at', sortOrder = 'desc'] = value.split(':'); return { @@ -46,3 +44,23 @@ export function formatCompactNumber(value: number) { maximumFractionDigits: 1, }).format(value); } + +export function formatOverviewRangeHint(hours: number) { + if (hours <= 24) return '近 24 小时'; + if (hours % 24 === 0) return `近 ${hours / 24} 天`; + return `近 ${hours} 小时`; +} + +export function formatOverviewTrendLabel(value: string, hours: number) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return '—'; + } + const month = `${date.getMonth() + 1}`.padStart(2, '0'); + const day = `${date.getDate()}`.padStart(2, '0'); + const hour = `${date.getHours()}`.padStart(2, '0'); + if (hours <= 24) { + return `${hour}:00`; + } + return `${month}/${day} ${hour}:00`; +} diff --git a/frontend/app/(main)/access-logs/components/detail-tab.tsx b/frontend/app/(main)/access-logs/components/detail-tab.tsx new file mode 100644 index 00000000..30ddfceb --- /dev/null +++ b/frontend/app/(main)/access-logs/components/detail-tab.tsx @@ -0,0 +1,164 @@ +'use client'; + +import { EmptyStateWithBorder } from '@/components/layout/empty'; +import { ErrorInline } from '@/components/layout/error'; +import { LoadingStateWithBorder } from '@/components/layout/loading'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import type { AccessLogList } from '@/lib/services/openflare'; +import { formatDateTime } from '@/lib/utils'; + +import { DETAIL_SORT_OPTIONS } from './access-log-utils'; + +function PaginationBar({ + page, + hasMore, + loading, + onPrev, + onNext, +}: { + page: number; + hasMore: boolean; + loading: boolean; + onPrev: () => void; + onNext: () => void; +}) { + return ( +
+

当前第 {page + 1} 页

+
+ + +
+
+ ); +} + +export function DetailTab({ + data, + loading, + error, + page, + detailSort, + onDetailSortChange, + onRetry, + onPrevPage, + onNextPage, + isFetching, +}: { + data?: AccessLogList; + loading: boolean; + error: Error | null; + page: number; + detailSort: string; + onDetailSortChange: (value: string) => void; + onRetry: () => void; + onPrevPage: () => void; + onNextPage: () => void; + isFetching: boolean; +}) { + return ( +
+
+

日志明细

+ +
+ {error ? ( +
+ +
+ ) : loading ? ( + + ) : (data?.items ?? []).length === 0 ? ( + + ) : ( + + + + 时间 + 节点 + IP + 域名 + 路径 + 状态码 + + + + {(data?.items ?? []).map((item) => ( + + + {formatDateTime(item.logged_at)} + + + {item.node_name || item.node_id} + + + {item.remote_addr} + + {item.host} + + {item.path} + + + + {item.status_code} + + + + ))} + +
+ )} + +
+ ); +} diff --git a/frontend/app/(main)/access-logs/components/overview-tab.tsx b/frontend/app/(main)/access-logs/components/overview-tab.tsx new file mode 100644 index 00000000..5c3bd202 --- /dev/null +++ b/frontend/app/(main)/access-logs/components/overview-tab.tsx @@ -0,0 +1,348 @@ +'use client'; + +import { useMemo } from 'react'; +import type { EChartsOption } from 'echarts'; +import ReactECharts from 'echarts-for-react'; + +import { RankChart } from '@/components/data/rank-chart'; +import { TrendChart } from '@/components/data/trend-chart'; +import { EmptyStateWithBorder } from '@/components/layout/empty'; +import { ErrorInline } from '@/components/layout/error'; +import { LoadingStateWithBorder } from '@/components/layout/loading'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import type { AccessLogOverview } from '@/lib/services/openflare'; +import { formatBytes, formatCompactNumber } from '@/lib/utils/metrics'; + +import { + formatOverviewRangeHint, + formatOverviewTrendLabel, + OVERVIEW_RANGE_OPTIONS, + type OverviewRangeHours, +} from './access-log-utils'; + +function SparklineMetricCard({ + title, + value, + hint, + color, + fillColor, + labels, + values, + valueFormatter, +}: { + title: string; + value: string; + hint: string; + color: string; + fillColor: string; + labels: string[]; + values: number[]; + valueFormatter?: (value: number) => string; +}) { + const option = useMemo( + () => ({ + animationDuration: 400, + grid: { + left: 0, + right: 0, + top: 8, + bottom: 0, + }, + xAxis: { + type: 'category', + show: false, + boundaryGap: false, + data: labels, + }, + yAxis: { + type: 'value', + show: false, + min: 0, + }, + tooltip: { + trigger: 'axis', + backgroundColor: 'rgba(15, 23, 42, 0.92)', + borderWidth: 0, + textStyle: { + color: '#e2e8f0', + fontSize: 12, + }, + formatter: (params: unknown) => { + const items = Array.isArray(params) ? params : []; + const item = items[0] as + { axisValueLabel?: string; value?: number } | undefined; + if (!item) return ''; + const raw = typeof item.value === 'number' ? item.value : 0; + const formatted = valueFormatter + ? valueFormatter(raw) + : formatCompactNumber(raw); + return `${item.axisValueLabel ?? ''}
${formatted}`; + }, + }, + series: [ + { + type: 'line', + data: values, + smooth: true, + showSymbol: false, + lineStyle: { + color, + width: 2, + }, + areaStyle: { + color: fillColor, + }, + }, + ], + }), + [color, fillColor, labels, valueFormatter, values], + ); + + return ( + + +
+
+

+ {title} +

+

{value}

+

{hint}

+
+
+ {labels.length > 0 ? ( + + ) : null} +
+
+
+
+ ); +} + +function RankCard({ + title, + description, + items, + color, +}: { + title: string; + description: string; + items: { label: string; value: number }[]; + color: string; +}) { + return ( + + + + {title} + + + {description} + + + + + + + ); +} + +function OverviewRangeSwitch({ + hours, + onHoursChange, +}: { + hours: OverviewRangeHours; + onHoursChange: (hours: OverviewRangeHours) => void; +}) { + return ( +
+ { + if (!value) return; + onHoursChange(Number.parseInt(value, 10) as OverviewRangeHours); + }} + variant='outline' + size='sm' + className='justify-end' + > + {OVERVIEW_RANGE_OPTIONS.map((option) => ( + + {option.label} + + ))} + +
+ ); +} + +export function OverviewTab({ + data, + loading, + error, + hours, + onHoursChange, + onRetry, +}: { + data?: AccessLogOverview; + loading: boolean; + error: Error | null; + hours: OverviewRangeHours; + onHoursChange: (hours: OverviewRangeHours) => void; + onRetry: () => void; +}) { + return ( +
+ + + {loading ? ( + + ) : error ? ( + + ) : !data ? ( + + ) : ( + + )} +
+ ); +} + +function OverviewContent({ + data, + hours, +}: { + data: AccessLogOverview; + hours: number; +}) { + const requestLabels = data.trends.requests.map((point) => + formatOverviewTrendLabel(point.bucket_started_at, hours), + ); + const requestValues = data.trends.requests.map((point) => point.value); + const visitValues = data.trends.visits.map((point) => point.value); + const bandwidthValues = data.trends.bandwidth.map((point) => point.value); + const hint = formatOverviewRangeHint(hours); + + return ( + <> +
+ + + +
+ + + + + Requests over time + + + 观察请求量是否出现异常抬升或回落。 + + + + + + + +
+ ({ + label: item.key, + value: item.value, + }))} + /> + ({ + label: item.key, + value: item.value, + }))} + /> + ({ + label: item.key, + value: item.value, + }))} + /> +
+ + ); +} diff --git a/frontend/app/(main)/access-logs/page.tsx b/frontend/app/(main)/access-logs/page.tsx index 9974c4af..dfb5ae81 100644 --- a/frontend/app/(main)/access-logs/page.tsx +++ b/frontend/app/(main)/access-logs/page.tsx @@ -1,56 +1,24 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'; import { RefreshCw, ScrollText, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; -import { EmptyStateWithBorder } from '@/components/layout/empty'; -import { ErrorInline } from '@/components/layout/error'; -import { LoadingStateWithBorder } from '@/components/layout/loading'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from '@/components/ui/chart'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AccessLogService } from '@/lib/services/openflare'; -import { formatDateTime } from '@/lib/utils'; import { AccessLogFilters } from './components/access-log-filters'; import { type AccessLogTab, - DETAIL_SORT_OPTIONS, - FOLD_SORT_OPTIONS, - formatCompactNumber, - IP_SORT_OPTIONS, + type OverviewRangeHours, parseSortValue, type SearchDraft, } from './components/access-log-utils'; import { CleanupDialog } from './components/cleanup-dialog'; - -const trendChartConfig = { - requests: { label: '请求数', color: 'hsl(var(--primary))' }, -} satisfies ChartConfig; +import { DetailTab } from './components/detail-tab'; +import { OverviewTab } from './components/overview-tab'; const emptyDraft: SearchDraft = { nodeId: '', @@ -59,62 +27,27 @@ const emptyDraft: SearchDraft = { path: '', }; -function PaginationBar({ - page, - hasMore, - loading, - onPrev, - onNext, -}: { - page: number; - hasMore: boolean; - loading: boolean; - onPrev: () => void; - onNext: () => void; -}) { - return ( -
-

当前第 {page + 1} 页

-
- - -
-
- ); -} - export default function AccessLogsPage() { const queryClient = useQueryClient(); - const [tab, setTab] = useState('list'); + const [tab, setTab] = useState('overview'); const [draft, setDraft] = useState(emptyDraft); const [filters, setFilters] = useState(emptyDraft); const [pageSize, setPageSize] = useState(20); const [page, setPage] = useState(0); const [detailSort, setDetailSort] = useState('logged_at:desc'); - const [foldSort, setFoldSort] = useState('bucket_started_at:desc'); - const [ipSort, setIpSort] = useState('total_requests:desc'); - const [foldMinutes, setFoldMinutes] = useState<3 | 5>(3); - const [trendIp, setTrendIp] = useState(''); - const [appliedTrendIp, setAppliedTrendIp] = useState(''); + const [overviewHours, setOverviewHours] = useState(24); const [cleanupOpen, setCleanupOpen] = useState(false); const detailSortState = parseSortValue(detailSort); - const foldSortState = parseSortValue(foldSort); - const ipSortState = parseSortValue(ipSort); + + const overviewQuery = useQuery({ + queryKey: ['openflare', 'access-logs', 'overview', overviewHours], + queryFn: () => + AccessLogService.getOverview({ + hours: overviewHours, + }), + enabled: tab === 'overview', + }); const listQuery = useQuery({ queryKey: [ @@ -140,68 +73,6 @@ export default function AccessLogsPage() { enabled: tab === 'list', }); - const foldsQuery = useQuery({ - queryKey: [ - 'openflare', - 'access-logs', - 'folds', - filters, - page, - pageSize, - foldSort, - foldMinutes, - ], - queryFn: () => - AccessLogService.listFolds({ - node_id: filters.nodeId || undefined, - remote_addr: filters.remoteAddr || undefined, - host: filters.host || undefined, - path: filters.path || undefined, - p: page, - page_size: pageSize, - sort_by: foldSortState.sortBy, - sort_order: foldSortState.sortOrder, - fold_minutes: foldMinutes, - }), - enabled: tab === 'folds', - }); - - const ipSummaryQuery = useQuery({ - queryKey: [ - 'openflare', - 'access-logs', - 'ip-summary', - filters, - page, - pageSize, - ipSort, - ], - queryFn: () => - AccessLogService.listIPSummaries({ - node_id: filters.nodeId || undefined, - remote_addr: filters.remoteAddr || undefined, - host: filters.host || undefined, - p: page, - page_size: pageSize, - sort_by: ipSortState.sortBy, - sort_order: ipSortState.sortOrder, - }), - enabled: tab === 'ip-summary', - }); - - const ipTrendQuery = useQuery({ - queryKey: ['openflare', 'access-logs', 'ip-trend', filters, appliedTrendIp], - queryFn: () => - AccessLogService.getIPTrend({ - node_id: filters.nodeId || undefined, - remote_addr: appliedTrendIp, - host: filters.host || undefined, - hours: 24, - bucket_minutes: 30, - }), - enabled: tab === 'ip-trend' && appliedTrendIp !== '', - }); - const cleanupMutation = useMutation({ mutationFn: (retentionDays: number) => AccessLogService.cleanup({ retention_days: retentionDays }), @@ -217,35 +88,6 @@ export default function AccessLogsPage() { }, }); - const activeSummary = useMemo(() => { - if (tab === 'list' && listQuery.data) { - return { - totalRecord: listQuery.data.total_record, - totalIp: listQuery.data.total_ip, - }; - } - if (tab === 'folds' && foldsQuery.data) { - return { - totalRecord: foldsQuery.data.total_record, - totalIp: foldsQuery.data.total_ip, - }; - } - if (tab === 'ip-summary' && ipSummaryQuery.data) { - return { - totalRecord: 0, - totalIp: ipSummaryQuery.data.total_ip, - }; - } - return { totalRecord: 0, totalIp: 0 }; - }, [tab, listQuery.data, foldsQuery.data, ipSummaryQuery.data]); - - const trendChartData = useMemo(() => { - return (ipTrendQuery.data?.points ?? []).map((point) => ({ - label: formatDateTime(point.bucket_started_at).slice(5), - requests: point.request_count, - })); - }, [ipTrendQuery.data?.points]); - const handleSearch = useCallback(() => { setFilters({ nodeId: draft.nodeId.trim(), @@ -260,8 +102,6 @@ export default function AccessLogsPage() { setDraft(emptyDraft); setFilters(emptyDraft); setPage(0); - setTrendIp(''); - setAppliedTrendIp(''); }; useEffect(() => { @@ -269,17 +109,11 @@ export default function AccessLogsPage() { }, [tab, pageSize]); const refreshActive = () => { + if (tab === 'overview') void overviewQuery.refetch(); if (tab === 'list') void listQuery.refetch(); - if (tab === 'folds') void foldsQuery.refetch(); - if (tab === 'ip-summary') void ipSummaryQuery.refetch(); - if (tab === 'ip-trend') void ipTrendQuery.refetch(); }; - const isFetching = - listQuery.isFetching || - foldsQuery.isFetching || - ipSummaryQuery.isFetching || - ipTrendQuery.isFetching; + const isFetching = overviewQuery.isFetching || listQuery.isFetching; return (
@@ -289,7 +123,7 @@ export default function AccessLogsPage() {

访问日志

- 按节点、IP、域名与路径检索,支持时间折叠、IP 汇总与趋势分析。 + 查看访问概览、排行榜与明细日志。

@@ -316,392 +150,51 @@ export default function AccessLogsPage() {
-
- {[ - { - label: '访问记录', - value: formatCompactNumber(activeSummary.totalRecord), - }, - { - label: '来源 IP', - value: formatCompactNumber(activeSummary.totalIp), - }, - { - label: '当前视图', - value: - tab === 'list' - ? '明细日志' - : tab === 'folds' - ? '时间折叠' - : tab === 'ip-summary' - ? 'IP 汇总' - : 'IP 趋势', - }, - ].map((item) => ( -
-

- {item.label} -

-

{item.value}

-
- ))} -
- setTab(value as AccessLogTab)} > - - 明细列表 - 时间折叠 - IP 汇总 - IP 趋势 + + 概览 + 日志明细 -
- + void overviewQuery.refetch()} /> -
- - -
-
-

明细日志

- -
- {listQuery.isError ? ( -
- void listQuery.refetch()} - /> -
- ) : listQuery.isLoading ? ( - - ) : (listQuery.data?.items ?? []).length === 0 ? ( - - ) : ( - - - - 时间 - 节点 - IP - 域名 - 路径 - 状态码 - - - - {(listQuery.data?.items ?? []).map((item) => ( - - - {formatDateTime(item.logged_at)} - - - {item.node_name || item.node_id} - - - {item.remote_addr} - - {item.host} - - {item.path} - - - - {item.status_code} - - - - ))} - -
- )} - setPage((p) => Math.max(0, p - 1))} - onNext={() => setPage((p) => p + 1)} - /> -
- -
-
-
-

时间折叠

- -
- -
- {foldsQuery.isError ? ( -
- void foldsQuery.refetch()} - /> -
- ) : foldsQuery.isLoading ? ( - - ) : (foldsQuery.data?.items ?? []).length === 0 ? ( - - ) : ( - - - - 时间桶 - 请求数 - 独立 IP - 独立域名 - 2xx - 4xx - 5xx - - - - {(foldsQuery.data?.items ?? []).map((item) => ( - - - {formatDateTime(item.bucket_started_at)} - - - {item.request_count} - - - {item.unique_ip_count} - - - {item.unique_host_count} - - - {item.success_count} - - - {item.client_error_count} - - - {item.server_error_count} - - - ))} - -
- )} - setPage((p) => Math.max(0, p - 1))} - onNext={() => setPage((p) => p + 1)} + +
+
-
- - -
-
-

IP 汇总

- -
- {ipSummaryQuery.isError ? ( -
- void ipSummaryQuery.refetch()} - /> -
- ) : ipSummaryQuery.isLoading ? ( - - ) : (ipSummaryQuery.data?.items ?? []).length === 0 ? ( - - ) : ( - - - - IP - 总请求数 - 近 3 小时 - 最后访问 - - - - {(ipSummaryQuery.data?.items ?? []).map((item) => ( - - - {item.remote_addr} - - - {item.total_requests} - - - {item.recent_requests} - - - {formatDateTime(item.last_seen_at)} - - - ))} - -
- )} - setPage((p) => Math.max(0, p - 1))} - onNext={() => setPage((p) => p + 1)} - /> -
-
- - -
-
-

- 趋势 IP -

- setTrendIp(e.target.value)} - placeholder='输入要分析趋势的 IP 地址' - /> -
- -
- -
- {!appliedTrendIp ? ( - - ) : ipTrendQuery.isLoading ? ( - - ) : ipTrendQuery.isError ? ( - void ipTrendQuery.refetch()} - /> - ) : trendChartData.length === 0 ? ( - - ) : ( -
-

- {appliedTrendIp} · 近 {ipTrendQuery.data?.hours ?? 24} 小时 -

- - - - - - } /> - - - -
- )} -
+ void listQuery.refetch()} + onPrevPage={() => setPage((p) => Math.max(0, p - 1))} + onNextPage={() => setPage((p) => p + 1)} + isFetching={listQuery.isFetching} + />
diff --git a/frontend/app/(main)/components/dashboard/data/country-centroids.json b/frontend/app/(main)/components/dashboard/data/country-centroids.json index 6bbf19cf..d53802fb 100644 --- a/frontend/app/(main)/components/dashboard/data/country-centroids.json +++ b/frontend/app/(main)/components/dashboard/data/country-centroids.json @@ -1,702 +1,177 @@ { - "Afghanistan": [ - 65.99254367483304, - 33.83349410470984 - ], - "Albania": [ - 20.035803636476235, - 41.179283471608585 - ], - "Algeria": [ - 2.6175172555729325, - 28.158202786113698 - ], - "Angola": [ - 17.563198329790602, - -12.334557680834592 - ], - "Argentina": [ - -65.15670576062132, - -35.178771590782524 - ], - "Armenia": [ - 44.929379894609674, - 40.29884592895426 - ], - "Australia": [ - 134.361180723562, - -25.574955774991995 - ], - "Austria": [ - 14.10165807418182, - 47.57042831528493 - ], - "Azerbaijan": [ - 47.636777782137415, - 40.3311508093757 - ], - "Bangladesh": [ - 90.21009840315206, - 23.88793565572049 - ], - "Belarus": [ - 28.04895281233843, - 53.53432933090482 - ], - "Belgium": [ - 4.628129604331132, - 50.646494813466056 - ], - "Belize": [ - -88.72096138346826, - 17.17073148225863 - ], - "Benin": [ - 2.323507643298733, - 9.63048313950481 - ], - "Bhutan": [ - 90.40562361681495, - 27.40133623508629 - ], - "Bolivia": [ - -64.69104972918655, - -16.70141507507877 - ], - "Bosnia and Herzegovina": [ - 17.762461846176542, - 44.16398853722456 - ], - "Botswana": [ - 23.791788508036337, - -22.185333985163894 - ], - "Brazil": [ - -53.113858151998855, - -10.827995867833263 - ], - "Brunei": [ - 114.54229268997476, - 4.451218476864121 - ], - "Bulgaria": [ - 25.225364036830744, - 42.77785706066581 - ], - "Burkina Faso": [ - -1.7757430610210276, - 12.260370131848955 - ], - "Burundi": [ - 29.873065896828848, - -3.3802825292327188 - ], - "Cambodia": [ - 104.90702345739547, - 12.717932969523966 - ], - "Cameroon": [ - 12.739868035578002, - 5.692692453534944 - ], - "Canada": [ - -102.31777616484295, - 57.94457487676815 - ], - "Central African Republic": [ - 20.490781591297914, - 6.563625165826992 - ], - "Chad": [ - 18.645815630757973, - 15.32729896930035 - ], - "Chile": [ - -71.34891661948937, - -35.98028631773855 - ], - "China": [ - 103.79804331240364, - 36.61094167680319 - ], - "Colombia": [ - -73.07794094917405, - 3.9235618261252663 - ], - "Costa Rica": [ - -84.21742467558424, - 9.986463158070535 - ], - "Croatia": [ - 16.458685366615978, - 45.18567759026696 - ], - "Cuba": [ - -78.8954025482625, - 21.62136808001903 - ], - "Cyprus": [ - 33.02409208254226, - 34.91304424540028 - ], - "Czech Republic": [ - 15.330767057694615, - 49.70160772219921 - ], - "Democratic Republic of the Congo": [ - 23.64811688254194, - -2.8775533309965056 - ], - "Denmark": [ - -41.29088868109839, - 74.72535220460091 - ], - "Djibouti": [ - 42.56788631653962, - 11.745988995518626 - ], - "Dominican Republic": [ - -70.48595892120278, - 18.896879062003393 - ], - "East Timor": [ - 125.78222644041995, - -8.858373836074785 - ], - "Ecuador": [ - -78.38384779532852, - -1.4454571869341688 - ], - "Egypt": [ - 29.86739912685152, - 26.49286512797635 - ], - "El Salvador": [ - -88.86593554283397, - 13.734573052971413 - ], - "Equatorial Guinea": [ - 10.481670786028316, - 1.5660796013647418 - ], - "Eritrea": [ - 38.835037148217566, - 15.359809600209143 - ], - "Estonia": [ - 25.811350172874032, - 58.69303019372469 - ], - "Ethiopia": [ - 39.60372599651433, - 8.621595995236198 - ], - "Fiji": [ - 177.98415179869738, - -17.827004078184217 - ], - "Finland": [ - 26.28958351526606, - 64.52279699210293 - ], - "France": [ - 165.43817082439585, - -21.285375782391018 - ], - "Gabon": [ - 11.774048593462979, - -0.5859798536474902 - ], - "Gambia": [ - -16.21304964555284, - 13.37062695465985 - ], - "Georgia": [ - 43.48650319000057, - 42.175027555818694 - ], - "Germany": [ - 10.380496796525602, - 51.083853487429664 - ], - "Ghana": [ - -1.2150159067556041, - 7.94078880601944 - ], - "Greece": [ - 22.53435219348475, - 39.517734832602365 - ], - "Guatemala": [ - -90.35344030476544, - 15.679044168855697 - ], - "Guinea": [ - -10.943568403941383, - 10.44078010915303 - ], - "Guinea Bissau": [ - -14.929646023453557, - 12.05088889829685 - ], - "Guyana": [ - -58.978019347627985, - 4.800329726037504 - ], - "Haiti": [ - -72.67866559866681, - 18.916195586860297 - ], - "Honduras": [ - -86.65651796671759, - 14.821029518594349 - ], - "Hong Kong": [ - 114.1747, - 22.2783 - ], - "Hungary": [ - 19.410867145831855, - 47.168542742918525 - ], - "Iceland": [ - -18.509670948264166, - 65.03034972428229 - ], - "India": [ - 79.58636900833257, - 22.901586798523514 - ], - "Indonesia": [ - 101.52237404492608, - -0.44681277438152994 - ], - "Iran": [ - 54.266246557433305, - 32.57616445788996 - ], - "Iraq": [ - 43.74006501790808, - 33.0341456153028 - ], - "Ireland": [ - -8.117695195030898, - 53.2341315203433 - ], - "Israel": [ - 35.241573878810335, - 31.94758544099519 - ], - "Italy": [ - 12.16233842748749, - 43.51281131035235 - ], - "Ivory Coast": [ - -5.569771898429842, - 7.626172775954684 - ], - "Jamaica": [ - -77.32850498837172, - 18.172957893241634 - ], - "Japan": [ - 137.97352453982398, - 36.589925009528365 - ], - "Jordan": [ - 36.76131486689134, - 31.237145277490367 - ], - "Kashmir": [ - 77.08733723957188, - 35.415738932286466 - ], - "Kazakhstan": [ - 67.27815427190085, - 48.155989088140196 - ], - "Kenya": [ - 37.795078751163835, - 0.5967194222965451 - ], - "Kosovo": [ - 20.851519165302378, - 42.51783231278167 - ], - "Kuwait": [ - 47.566177395781736, - 29.316145203902064 - ], - "Kyrgyzstan": [ - 74.58224715420873, - 41.48363475100716 - ], - "Laos": [ - 103.75263811609112, - 18.494354885305256 - ], - "Latvia": [ - 24.89496154477673, - 56.853143698564296 - ], - "Lebanon": [ - 35.88502277707876, - 33.91528991779158 - ], - "Lesotho": [ - 28.24303757633595, - -29.57351232727891 - ], - "Liberia": [ - -9.325867879201954, - 6.453996218817175 - ], - "Libya": [ - 18.008785486000946, - 27.031457363755713 - ], - "Lithuania": [ - 23.872793668340726, - 55.3142460584835 - ], - "Luxembourg": [ - 6.070340274408408, - 49.76746140228473 - ], - "Macao": [ - 113.5439, - 22.1987 - ], - "Macau": [ - 113.5439, - 22.1987 - ], - "Macedonia": [ - 21.704642152886493, - 41.60373573782414 - ], - "Madagascar": [ - 46.70172536870331, - -19.374776628372288 - ], - "Malawi": [ - 34.288911351438394, - -13.200363393826267 - ], - "Malaysia": [ - 114.69201508943004, - 3.57724156754415 - ], - "Mali": [ - -3.5321833143642736, - 17.347810147640345 - ], - "Mauritania": [ - -10.34307896458181, - 20.259793585678146 - ], - "Mexico": [ - -102.51849775988345, - 23.943713432682014 - ], - "Moldova": [ - 28.446472517363826, - 47.2017924696845 - ], - "Mongolia": [ - 103.06423533091326, - 46.82560864032323 - ], - "Montenegro": [ - 19.233365413523355, - 42.78395456212118 - ], - "Morocco": [ - -8.459192831832421, - 29.83932361976915 - ], - "Mozambique": [ - 35.53173663088568, - -17.27297477254694 - ], - "Myanmar": [ - 96.49588816533908, - 21.22201469728619 - ], - "Namibia": [ - 17.208945346875193, - -22.132302890947404 - ], - "Nepal": [ - 83.9234099472594, - 28.243335814044734 - ], - "Netherlands": [ - 5.646194278986089, - 52.27734132368374 - ], - "New Zealand": [ - 170.54047059084724, - -43.955381757717255 - ], - "Nicaragua": [ - -85.03229930746807, - 12.835548864379284 - ], - "Niger": [ - 9.384787308614277, - 17.41758190683491 - ], - "Nigeria": [ - 8.074235448092892, - 9.58958974174772 - ], - "North Korea": [ - 127.21551783512186, - 40.144856985239926 - ], - "Northern Cyprus": [ - 33.39079188058263, - 35.225753269439686 - ], - "Norway": [ - 13.994247649210363, - 64.32249798565987 - ], - "Oman": [ - 56.08271927344639, - 20.573738446088598 - ], - "Pakistan": [ - 69.32988114530586, - 29.951585862562073 - ], - "Panama": [ - -80.05970573973497, - 8.521692369504816 - ], - "Papua New Guinea": [ - 144.22896759838426, - -6.607045399379529 - ], - "Paraguay": [ - -58.39900641582761, - -23.223895588708068 - ], - "Peru": [ - -74.38850244798846, - -9.156316252308201 - ], - "Philippines": [ - 121.45754828266669, - 15.948824272939182 - ], - "Poland": [ - 19.379892383012464, - 52.11963086628078 - ], - "Portugal": [ - -7.977838817691485, - 39.6854900148663 - ], - "Qatar": [ - 51.170816022848, - 25.32876608728452 - ], - "Republic of Serbia": [ - 20.766249889622852, - 44.19998417744864 - ], - "Republic of the Congo": [ - 15.21207418503586, - -0.8564004333096924 - ], - "Romania": [ - 24.971632366248883, - 45.84560772065747 - ], - "Russia": [ - 99.05228241954488, - 61.677043847105494 - ], - "Rwanda": [ - 29.91765145538139, - -1.9953628964377972 - ], - "Saudi Arabia": [ - 44.5342871199886, - 24.122854227726673 - ], - "Senegal": [ - -14.472411019597182, - 14.338613732351318 - ], - "Sierra Leone": [ - -11.82013204896783, - 8.575093438325089 - ], - "Singapore": [ - 103.8198, - 1.3521 - ], - "Slovakia": [ - 19.47296461136286, - 48.71540683451534 - ], - "Slovenia": [ - 14.771193461021065, - 46.10807043333343 - ], - "Solomon Islands": [ - 160.156396875585, - -9.628589831637283 - ], - "Somalia": [ - 45.706539856671256, - 4.747960517798881 - ], - "Somaliland": [ - 46.25513292885051, - 9.729995248781385 - ], - "South Africa": [ - 25.160630055943862, - -29.008774283210798 - ], - "South Korea": [ - 127.87280447602005, - 36.475157799464604 - ], - "South Sudan": [ - 30.253827921202955, - 7.307277152400154 - ], - "Spain": [ - -3.5708365129134876, - 40.391564705240064 - ], - "Sri Lanka": [ - 80.70055059693914, - 7.649883097905021 - ], - "Sudan": [ - 29.946243665370346, - 15.99205876641883 - ], - "Suriname": [ - -55.91029765824739, - 4.126766010517478 - ], - "Swaziland": [ - 31.47664514492297, - -26.563545981108437 - ], - "Sweden": [ - 16.752551082675275, - 62.8354879894394 - ], - "Switzerland": [ - 8.205812860319684, - 46.805544047201984 - ], - "Syria": [ - 38.50449031631095, - 35.01769370592341 - ], - "Taiwan": [ - 120.9605, - 23.6978 - ], - "Tajikistan": [ - 70.98712700685866, - 38.55204366339685 - ], - "Thailand": [ - 101.01060253224972, - 15.132319216387051 - ], - "The Bahamas": [ - -78.02733865351972, - 24.719920319716515 - ], - "Togo": [ - 0.949513544830056, - 8.576560980195756 - ], - "Trinidad and Tobago": [ - -61.270458833911896, - 10.339930045317969 - ], - "Tunisia": [ - 9.546832905099203, - 34.12673069029241 - ], - "Turkey": [ - 35.424986274948736, - 38.998407240706534 - ], - "Turkmenistan": [ - 59.39292286662992, - 39.1098173228857 - ], - "Uganda": [ - 32.36575488492427, - 1.2757695499320103 - ], - "Ukraine": [ - 31.381193375918794, - 49.01345298489402 - ], - "United Arab Emirates": [ - 54.30345828396012, - 23.905698042805522 - ], - "United Kingdom": [ - -36.367082569942795, - -54.468348288665524 - ], - "United Republic of Tanzania": [ - 34.79490109621088, - -6.276146393386636 - ], - "United States of America": [ - -99.14669730710926, - 39.52689381707972 - ], - "Uruguay": [ - -56.01665573850212, - -32.80651677440885 - ], - "Uzbekistan": [ - 63.148826727968626, - 41.7502616897204 - ], - "Vanuatu": [ - 166.84228659210697, - -15.266627310669035 - ], - "Venezuela": [ - -66.1882174671549, - 7.118299933056809 - ], - "Vietnam": [ - 106.29939096235873, - 16.63088189612042 - ], - "Western Sahara": [ - -12.218085234851758, - 24.221135708939354 - ], - "Yemen": [ - 47.546351590294876, - 15.935825615412996 - ], - "Zambia": [ - 27.775321721593084, - -13.460954386993254 - ], - "Zimbabwe": [ - 29.850564118889274, - -19.00677451616593 - ] + "Afghanistan": [65.99254367483304, 33.83349410470984], + "Albania": [20.035803636476235, 41.179283471608585], + "Algeria": [2.6175172555729325, 28.158202786113698], + "Angola": [17.563198329790602, -12.334557680834592], + "Argentina": [-65.15670576062132, -35.178771590782524], + "Armenia": [44.929379894609674, 40.29884592895426], + "Australia": [134.361180723562, -25.574955774991995], + "Austria": [14.10165807418182, 47.57042831528493], + "Azerbaijan": [47.636777782137415, 40.3311508093757], + "Bangladesh": [90.21009840315206, 23.88793565572049], + "Belarus": [28.04895281233843, 53.53432933090482], + "Belgium": [4.628129604331132, 50.646494813466056], + "Belize": [-88.72096138346826, 17.17073148225863], + "Benin": [2.323507643298733, 9.63048313950481], + "Bhutan": [90.40562361681495, 27.40133623508629], + "Bolivia": [-64.69104972918655, -16.70141507507877], + "Bosnia and Herzegovina": [17.762461846176542, 44.16398853722456], + "Botswana": [23.791788508036337, -22.185333985163894], + "Brazil": [-53.113858151998855, -10.827995867833263], + "Brunei": [114.54229268997476, 4.451218476864121], + "Bulgaria": [25.225364036830744, 42.77785706066581], + "Burkina Faso": [-1.7757430610210276, 12.260370131848955], + "Burundi": [29.873065896828848, -3.3802825292327188], + "Cambodia": [104.90702345739547, 12.717932969523966], + "Cameroon": [12.739868035578002, 5.692692453534944], + "Canada": [-102.31777616484295, 57.94457487676815], + "Central African Republic": [20.490781591297914, 6.563625165826992], + "Chad": [18.645815630757973, 15.32729896930035], + "Chile": [-71.34891661948937, -35.98028631773855], + "China": [103.79804331240364, 36.61094167680319], + "Colombia": [-73.07794094917405, 3.9235618261252663], + "Costa Rica": [-84.21742467558424, 9.986463158070535], + "Croatia": [16.458685366615978, 45.18567759026696], + "Cuba": [-78.8954025482625, 21.62136808001903], + "Cyprus": [33.02409208254226, 34.91304424540028], + "Czech Republic": [15.330767057694615, 49.70160772219921], + "Democratic Republic of the Congo": [23.64811688254194, -2.8775533309965056], + "Denmark": [-41.29088868109839, 74.72535220460091], + "Djibouti": [42.56788631653962, 11.745988995518626], + "Dominican Republic": [-70.48595892120278, 18.896879062003393], + "East Timor": [125.78222644041995, -8.858373836074785], + "Ecuador": [-78.38384779532852, -1.4454571869341688], + "Egypt": [29.86739912685152, 26.49286512797635], + "El Salvador": [-88.86593554283397, 13.734573052971413], + "Equatorial Guinea": [10.481670786028316, 1.5660796013647418], + "Eritrea": [38.835037148217566, 15.359809600209143], + "Estonia": [25.811350172874032, 58.69303019372469], + "Ethiopia": [39.60372599651433, 8.621595995236198], + "Fiji": [177.98415179869738, -17.827004078184217], + "Finland": [26.28958351526606, 64.52279699210293], + "France": [165.43817082439585, -21.285375782391018], + "Gabon": [11.774048593462979, -0.5859798536474902], + "Gambia": [-16.21304964555284, 13.37062695465985], + "Georgia": [43.48650319000057, 42.175027555818694], + "Germany": [10.380496796525602, 51.083853487429664], + "Ghana": [-1.2150159067556041, 7.94078880601944], + "Greece": [22.53435219348475, 39.517734832602365], + "Guatemala": [-90.35344030476544, 15.679044168855697], + "Guinea": [-10.943568403941383, 10.44078010915303], + "Guinea Bissau": [-14.929646023453557, 12.05088889829685], + "Guyana": [-58.978019347627985, 4.800329726037504], + "Haiti": [-72.67866559866681, 18.916195586860297], + "Honduras": [-86.65651796671759, 14.821029518594349], + "Hong Kong": [114.1747, 22.2783], + "Hungary": [19.410867145831855, 47.168542742918525], + "Iceland": [-18.509670948264166, 65.03034972428229], + "India": [79.58636900833257, 22.901586798523514], + "Indonesia": [101.52237404492608, -0.44681277438152994], + "Iran": [54.266246557433305, 32.57616445788996], + "Iraq": [43.74006501790808, 33.0341456153028], + "Ireland": [-8.117695195030898, 53.2341315203433], + "Israel": [35.241573878810335, 31.94758544099519], + "Italy": [12.16233842748749, 43.51281131035235], + "Ivory Coast": [-5.569771898429842, 7.626172775954684], + "Jamaica": [-77.32850498837172, 18.172957893241634], + "Japan": [137.97352453982398, 36.589925009528365], + "Jordan": [36.76131486689134, 31.237145277490367], + "Kashmir": [77.08733723957188, 35.415738932286466], + "Kazakhstan": [67.27815427190085, 48.155989088140196], + "Kenya": [37.795078751163835, 0.5967194222965451], + "Kosovo": [20.851519165302378, 42.51783231278167], + "Kuwait": [47.566177395781736, 29.316145203902064], + "Kyrgyzstan": [74.58224715420873, 41.48363475100716], + "Laos": [103.75263811609112, 18.494354885305256], + "Latvia": [24.89496154477673, 56.853143698564296], + "Lebanon": [35.88502277707876, 33.91528991779158], + "Lesotho": [28.24303757633595, -29.57351232727891], + "Liberia": [-9.325867879201954, 6.453996218817175], + "Libya": [18.008785486000946, 27.031457363755713], + "Lithuania": [23.872793668340726, 55.3142460584835], + "Luxembourg": [6.070340274408408, 49.76746140228473], + "Macao": [113.5439, 22.1987], + "Macau": [113.5439, 22.1987], + "Macedonia": [21.704642152886493, 41.60373573782414], + "Madagascar": [46.70172536870331, -19.374776628372288], + "Malawi": [34.288911351438394, -13.200363393826267], + "Malaysia": [114.69201508943004, 3.57724156754415], + "Mali": [-3.5321833143642736, 17.347810147640345], + "Mauritania": [-10.34307896458181, 20.259793585678146], + "Mexico": [-102.51849775988345, 23.943713432682014], + "Moldova": [28.446472517363826, 47.2017924696845], + "Mongolia": [103.06423533091326, 46.82560864032323], + "Montenegro": [19.233365413523355, 42.78395456212118], + "Morocco": [-8.459192831832421, 29.83932361976915], + "Mozambique": [35.53173663088568, -17.27297477254694], + "Myanmar": [96.49588816533908, 21.22201469728619], + "Namibia": [17.208945346875193, -22.132302890947404], + "Nepal": [83.9234099472594, 28.243335814044734], + "Netherlands": [5.646194278986089, 52.27734132368374], + "New Zealand": [170.54047059084724, -43.955381757717255], + "Nicaragua": [-85.03229930746807, 12.835548864379284], + "Niger": [9.384787308614277, 17.41758190683491], + "Nigeria": [8.074235448092892, 9.58958974174772], + "North Korea": [127.21551783512186, 40.144856985239926], + "Northern Cyprus": [33.39079188058263, 35.225753269439686], + "Norway": [13.994247649210363, 64.32249798565987], + "Oman": [56.08271927344639, 20.573738446088598], + "Pakistan": [69.32988114530586, 29.951585862562073], + "Panama": [-80.05970573973497, 8.521692369504816], + "Papua New Guinea": [144.22896759838426, -6.607045399379529], + "Paraguay": [-58.39900641582761, -23.223895588708068], + "Peru": [-74.38850244798846, -9.156316252308201], + "Philippines": [121.45754828266669, 15.948824272939182], + "Poland": [19.379892383012464, 52.11963086628078], + "Portugal": [-7.977838817691485, 39.6854900148663], + "Qatar": [51.170816022848, 25.32876608728452], + "Republic of Serbia": [20.766249889622852, 44.19998417744864], + "Republic of the Congo": [15.21207418503586, -0.8564004333096924], + "Romania": [24.971632366248883, 45.84560772065747], + "Russia": [99.05228241954488, 61.677043847105494], + "Rwanda": [29.91765145538139, -1.9953628964377972], + "Saudi Arabia": [44.5342871199886, 24.122854227726673], + "Senegal": [-14.472411019597182, 14.338613732351318], + "Sierra Leone": [-11.82013204896783, 8.575093438325089], + "Singapore": [103.8198, 1.3521], + "Slovakia": [19.47296461136286, 48.71540683451534], + "Slovenia": [14.771193461021065, 46.10807043333343], + "Solomon Islands": [160.156396875585, -9.628589831637283], + "Somalia": [45.706539856671256, 4.747960517798881], + "Somaliland": [46.25513292885051, 9.729995248781385], + "South Africa": [25.160630055943862, -29.008774283210798], + "South Korea": [127.87280447602005, 36.475157799464604], + "South Sudan": [30.253827921202955, 7.307277152400154], + "Spain": [-3.5708365129134876, 40.391564705240064], + "Sri Lanka": [80.70055059693914, 7.649883097905021], + "Sudan": [29.946243665370346, 15.99205876641883], + "Suriname": [-55.91029765824739, 4.126766010517478], + "Swaziland": [31.47664514492297, -26.563545981108437], + "Sweden": [16.752551082675275, 62.8354879894394], + "Switzerland": [8.205812860319684, 46.805544047201984], + "Syria": [38.50449031631095, 35.01769370592341], + "Taiwan": [120.9605, 23.6978], + "Tajikistan": [70.98712700685866, 38.55204366339685], + "Thailand": [101.01060253224972, 15.132319216387051], + "The Bahamas": [-78.02733865351972, 24.719920319716515], + "Togo": [0.949513544830056, 8.576560980195756], + "Trinidad and Tobago": [-61.270458833911896, 10.339930045317969], + "Tunisia": [9.546832905099203, 34.12673069029241], + "Turkey": [35.424986274948736, 38.998407240706534], + "Turkmenistan": [59.39292286662992, 39.1098173228857], + "Uganda": [32.36575488492427, 1.2757695499320103], + "Ukraine": [31.381193375918794, 49.01345298489402], + "United Arab Emirates": [54.30345828396012, 23.905698042805522], + "United Kingdom": [-36.367082569942795, -54.468348288665524], + "United Republic of Tanzania": [34.79490109621088, -6.276146393386636], + "United States of America": [-99.14669730710926, 39.52689381707972], + "Uruguay": [-56.01665573850212, -32.80651677440885], + "Uzbekistan": [63.148826727968626, 41.7502616897204], + "Vanuatu": [166.84228659210697, -15.266627310669035], + "Venezuela": [-66.1882174671549, 7.118299933056809], + "Vietnam": [106.29939096235873, 16.63088189612042], + "Western Sahara": [-12.218085234851758, 24.221135708939354], + "Yemen": [47.546351590294876, 15.935825615412996], + "Zambia": [27.775321721593084, -13.460954386993254], + "Zimbabwe": [29.850564118889274, -19.00677451616593] } diff --git a/frontend/components/data/rank-chart.tsx b/frontend/components/data/rank-chart.tsx index d39fd616..4921c853 100644 --- a/frontend/components/data/rank-chart.tsx +++ b/frontend/components/data/rank-chart.tsx @@ -1,8 +1,8 @@ 'use client'; import { useMemo } from 'react'; -import type { EChartsOption } from 'echarts'; -import ReactECharts from 'echarts-for-react'; + +import { cn } from '@/lib/utils'; type RankChartItem = { label: string; @@ -14,119 +14,65 @@ type RankChartProps = { color: string; valueFormatter?: (value: number) => string; emptyMessage?: string; + className?: string; }; const defaultFormatter = (value: number) => value.toLocaleString('zh-CN'); -function getChartValue(params: unknown) { - if (typeof params !== 'object' || params === null || !('value' in params)) { - return 0; - } - const rawValue = (params as { value?: unknown }).value; - if (Array.isArray(rawValue)) { - const candidate = rawValue[0]; - return typeof candidate === 'number' ? candidate : 0; - } - return typeof rawValue === 'number' ? rawValue : 0; -} - export function RankChart({ items, color, valueFormatter = defaultFormatter, emptyMessage = '暂无分布数据', + className, }: RankChartProps) { - const option = useMemo( - () => ({ - animationDuration: 400, - grid: { - left: 16, - right: 24, - top: 12, - bottom: 12, - containLabel: true, - }, - tooltip: { - trigger: 'axis', - axisPointer: { - type: 'shadow', - }, - backgroundColor: 'rgba(15, 23, 42, 0.92)', - borderWidth: 0, - textStyle: { - color: '#e2e8f0', - fontSize: 12, - }, - formatter: (params: unknown) => { - const item = Array.isArray(params) ? params[0] : params; - const data = item as { name?: string; value?: number }; - return `${data.name ?? ''}
${valueFormatter(data.value ?? 0)}`; - }, - }, - xAxis: { - type: 'value', - axisLabel: { - color: '#94a3b8', - }, - splitLine: { - lineStyle: { - color: 'rgba(148, 163, 184, 0.16)', - type: 'dashed', - }, - }, - }, - yAxis: { - type: 'category', - data: items.map((item) => item.label), - axisTick: { show: false }, - axisLine: { show: false }, - axisLabel: { - color: '#cbd5e1', - width: 120, - overflow: 'truncate', - }, - }, - series: [ - { - type: 'bar', - data: items.map((item) => item.value), - barWidth: 12, - showBackground: true, - backgroundStyle: { - color: 'rgba(148, 163, 184, 0.12)', - borderRadius: 999, - }, - itemStyle: { - color, - borderRadius: 999, - }, - label: { - show: true, - position: 'right', - color: '#e2e8f0', - formatter: (params: unknown) => - valueFormatter(getChartValue(params)), - }, - }, - ], - }), - [color, items, valueFormatter], + const maxValue = useMemo( + () => Math.max(0, ...items.map((item) => item.value)), + [items], ); if (items.length === 0) { return ( -
+
{emptyMessage}
); } return ( - +
+ {items.map((item) => { + const ratio = maxValue > 0 ? (item.value / maxValue) * 100 : 0; + return ( +
+
+ + {item.label} + + + {valueFormatter(item.value)} + +
+
+
+
+
+ ); + })} +
); } diff --git a/frontend/lib/services/index.ts b/frontend/lib/services/index.ts index e651dc57..6184f565 100644 --- a/frontend/lib/services/index.ts +++ b/frontend/lib/services/index.ts @@ -254,6 +254,7 @@ export type { OriginItem, OriginDetail, AccessLogList, + AccessLogOverview, OptionItem, GeoIPLookupResult, DatabaseCleanupResult, diff --git a/frontend/lib/services/openflare/access-log.service.ts b/frontend/lib/services/openflare/access-log.service.ts index f7bf9017..29cb9790 100644 --- a/frontend/lib/services/openflare/access-log.service.ts +++ b/frontend/lib/services/openflare/access-log.service.ts @@ -8,6 +8,8 @@ import type { AccessLogIPTrend, AccessLogIPTrendFilters, AccessLogList, + AccessLogOverview, + AccessLogOverviewFilters, FoldedAccessLogFilters, FoldedAccessLogIPFilters, FoldedAccessLogIPList, @@ -32,6 +34,12 @@ export class AccessLogService extends OpenFlareBaseService { return this.get('/', buildSearchParams(filters)); } + static getOverview( + filters: AccessLogOverviewFilters = {}, + ): Promise { + return this.get('/overview', buildSearchParams(filters)); + } + static listFolds( filters: FoldedAccessLogFilters, ): Promise { diff --git a/frontend/lib/services/openflare/index.ts b/frontend/lib/services/openflare/index.ts index ae704112..c961cc12 100644 --- a/frontend/lib/services/openflare/index.ts +++ b/frontend/lib/services/openflare/index.ts @@ -89,6 +89,9 @@ export type { AccessLogIPTrendFilters, AccessLogItem, AccessLogList, + AccessLogOverview, + AccessLogOverviewFilters, + AccessLogOverviewMetricPoint, FoldedAccessLogFilters, FoldedAccessLogIPFilters, FoldedAccessLogIPList, diff --git a/frontend/lib/services/openflare/types.ts b/frontend/lib/services/openflare/types.ts index a76bddde..7bc1c1fd 100644 --- a/frontend/lib/services/openflare/types.ts +++ b/frontend/lib/services/openflare/types.ts @@ -500,6 +500,35 @@ export interface AccessLogFilters { sort_order?: 'asc' | 'desc'; } +export interface AccessLogOverviewFilters { + node_id?: string; + host?: string; + hours?: number; +} + +export interface AccessLogOverviewMetricPoint { + bucket_started_at: string; + value: number; +} + +export interface AccessLogOverview { + generated_at: string; + hours: number; + summary: { + total_requests: number; + total_visits: number; + bandwidth_served: number; + }; + trends: { + requests: AccessLogOverviewMetricPoint[]; + visits: AccessLogOverviewMetricPoint[]; + bandwidth: AccessLogOverviewMetricPoint[]; + }; + top_paths: DistributionItem[]; + top_hosts: DistributionItem[]; + top_ips: DistributionItem[]; +} + export interface AccessLogItem { id: string; node_id: string; diff --git a/internal/apps/openflare/observability/access_log_logics.go b/internal/apps/openflare/observability/access_log_logics.go index a7a5788c..ca0416f7 100644 --- a/internal/apps/openflare/observability/access_log_logics.go +++ b/internal/apps/openflare/observability/access_log_logics.go @@ -178,6 +178,44 @@ type AccessLogIPTrendView struct { Points []AccessLogIPTrendPoint `json:"points"` } +// AccessLogOverviewQuery filters access log overview queries. +type AccessLogOverviewQuery struct { + NodeID string `json:"node_id"` + Host string `json:"host"` + Hours int `json:"hours"` +} + +// AccessLogOverviewMetricPoint is a single overview trend bucket. +type AccessLogOverviewMetricPoint struct { + BucketStartedAt time.Time `json:"bucket_started_at"` + Value int64 `json:"value"` +} + +// AccessLogOverviewSummary is the headline totals for the overview window. +type AccessLogOverviewSummary struct { + TotalRequests int64 `json:"total_requests"` + TotalVisits int64 `json:"total_visits"` + BandwidthServed int64 `json:"bandwidth_served"` +} + +// AccessLogOverviewTrends groups sparkline/series data for the overview. +type AccessLogOverviewTrends struct { + Requests []AccessLogOverviewMetricPoint `json:"requests"` + Visits []AccessLogOverviewMetricPoint `json:"visits"` + Bandwidth []AccessLogOverviewMetricPoint `json:"bandwidth"` +} + +// AccessLogOverview is the access-log analytics overview payload. +type AccessLogOverview struct { + GeneratedAt time.Time `json:"generated_at"` + Hours int `json:"hours"` + Summary AccessLogOverviewSummary `json:"summary"` + Trends AccessLogOverviewTrends `json:"trends"` + TopPaths []DistributionItem `json:"top_paths"` + TopHosts []DistributionItem `json:"top_hosts"` + TopIPs []DistributionItem `json:"top_ips"` +} + // AccessLogCleanupInput is the cleanup request payload. type AccessLogCleanupInput struct { RetentionDays int `json:"retention_days"` @@ -190,6 +228,142 @@ type AccessLogCleanupResult struct { Cutoff time.Time `json:"cutoff"` } +const ( + defaultAccessLogOverviewHours = 24 + maxAccessLogOverviewHours = 24 * 30 + accessLogOverviewTopLimit = 10 +) + +// GetAccessLogOverview returns summary metrics, trends, and top rankings. +func GetAccessLogOverview(ctx context.Context, input AccessLogOverviewQuery) (*AccessLogOverview, error) { + normalized := normalizeAccessLogOverviewQuery(input) + now := time.Now().UTC() + since := now.Add(-time.Duration(normalized.Hours) * time.Hour) + query := model.OpenFlareAccessLogQuery{ + NodeID: normalized.NodeID, + Host: normalized.Host, + Since: since, + Until: now, + } + + summaryRow, err := model.TrafficSummaryOpenFlareAccessLogs(ctx, query) + if err != nil { + return nil, err + } + + requestPoints, visitPoints, bandwidthPoints := buildAccessLogOverviewTrends( + ctx, now, normalized.Hours, query, + ) + + return &AccessLogOverview{ + GeneratedAt: now, + Hours: normalized.Hours, + Summary: AccessLogOverviewSummary{ + TotalRequests: summaryRow.RequestCount, + TotalVisits: summaryRow.UniqueIPCount, + BandwidthServed: summaryRow.BytesSent, + }, + Trends: AccessLogOverviewTrends{ + Requests: requestPoints, + Visits: visitPoints, + Bandwidth: bandwidthPoints, + }, + TopPaths: valueCountDistribution(ctx, query, "path", accessLogOverviewTopLimit), + TopHosts: valueCountDistribution(ctx, query, "host", accessLogOverviewTopLimit), + TopIPs: valueCountDistribution(ctx, query, "remote_addr", accessLogOverviewTopLimit), + }, nil +} + +func normalizeAccessLogOverviewQuery(input AccessLogOverviewQuery) AccessLogOverviewQuery { + hours := input.Hours + if hours <= 0 { + hours = defaultAccessLogOverviewHours + } + if hours > maxAccessLogOverviewHours { + hours = maxAccessLogOverviewHours + } + return AccessLogOverviewQuery{ + NodeID: strings.TrimSpace(input.NodeID), + Host: strings.TrimSpace(input.Host), + Hours: hours, + } +} + +func valueCountDistribution( + ctx context.Context, + query model.OpenFlareAccessLogQuery, + column string, + limit int, +) []DistributionItem { + rows, err := model.ValueCountsOpenFlareAccessLogs(ctx, query, column, limit) + if err != nil || len(rows) == 0 { + return []DistributionItem{} + } + items := make([]DistributionItem, 0, len(rows)) + for _, row := range rows { + if strings.TrimSpace(row.Value) == "" || row.Count <= 0 { + continue + } + items = append(items, DistributionItem{Key: row.Value, Value: row.Count}) + } + return items +} + +func buildAccessLogOverviewTrends( + ctx context.Context, + now time.Time, + hours int, + query model.OpenFlareAccessLogQuery, +) ( + requests []AccessLogOverviewMetricPoint, + visits []AccessLogOverviewMetricPoint, + bandwidth []AccessLogOverviewMetricPoint, +) { + if hours <= 0 { + hours = defaultAccessLogOverviewHours + } + start := now.Truncate(time.Hour).Add(-time.Duration(hours-1) * time.Hour) + requests = make([]AccessLogOverviewMetricPoint, hours) + visits = make([]AccessLogOverviewMetricPoint, hours) + bandwidth = make([]AccessLogOverviewMetricPoint, hours) + for index := range requests { + bucketAt := start.Add(time.Duration(index) * time.Hour) + requests[index].BucketStartedAt = bucketAt + visits[index].BucketStartedAt = bucketAt + bandwidth[index].BucketStartedAt = bucketAt + } + + buckets, err := model.ListOpenFlareAccessLogBuckets(ctx, model.OpenFlareAccessLogBucketQuery{ + NodeID: query.NodeID, + Host: query.Host, + Since: query.Since, + Until: query.Until, + FoldMinutes: 60, + SortBy: defaultAccessLogSortBy, + SortOrder: sortOrderAsc, + }) + if err != nil || len(buckets) == 0 { + return requests, visits, bandwidth + } + byEpoch := make(map[int64]*model.OpenFlareAccessLogBucketRow, len(buckets)) + for _, row := range buckets { + if row == nil { + continue + } + byEpoch[row.BucketEpoch] = row + } + for index := range requests { + row, ok := byEpoch[requests[index].BucketStartedAt.Unix()] + if !ok { + continue + } + requests[index].Value = row.RequestCount + visits[index].Value = row.UniqueIPCount + bandwidth[index].Value = row.BytesSent + } + return requests, visits, bandwidth +} + // ListAccessLogs returns paginated access logs. func ListAccessLogs(ctx context.Context, input AccessLogQuery) (*AccessLogList, error) { normalized := normalizeAccessLogQuery(input) diff --git a/internal/apps/openflare/observability/analytics.go b/internal/apps/openflare/observability/analytics.go index e35355de..0df2304b 100644 --- a/internal/apps/openflare/observability/analytics.go +++ b/internal/apps/openflare/observability/analytics.go @@ -130,7 +130,6 @@ type diskCounterState struct { seen bool } - func buildTrafficWindowSummaryFromAccessLogs( ctx context.Context, nodeID string, @@ -333,7 +332,7 @@ func BuildTrafficTrendPointsFromAccessLogs(ctx context.Context, now time.Time, n Since: since, Until: now, FoldMinutes: 60, - SortBy: "logged_at", + SortBy: defaultAccessLogSortBy, SortOrder: sortOrderAsc, }) if err != nil || len(buckets) == 0 { @@ -379,7 +378,7 @@ func applyAccessLogBytesToNetworkTrend(ctx context.Context, now time.Time, nodeI Since: since, Until: now, FoldMinutes: 60, - SortBy: "logged_at", + SortBy: defaultAccessLogSortBy, SortOrder: sortOrderAsc, }) if err != nil || len(buckets) == 0 { diff --git a/internal/apps/openflare/observability/routers.go b/internal/apps/openflare/observability/routers.go index 09942985..7fbd0c9e 100644 --- a/internal/apps/openflare/observability/routers.go +++ b/internal/apps/openflare/observability/routers.go @@ -12,6 +12,33 @@ import ( "github.com/gin-gonic/gin" ) +// GetAccessLogOverviewHandler 获取访问日志概览。 +// @Summary 获取访问日志概览 +// @Description 返回访问日志汇总指标、趋势与 Top 排行,需要管理员权限 +// @Tags openflare-observability +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID" +// @Param host query string false "请求 Host" +// @Param hours query int false "统计时间范围(小时)" +// @Success 200 {object} response.Any{data=observability.AccessLogOverview} "访问日志概览" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/d/access-logs/overview [get] +func GetAccessLogOverviewHandler(c *gin.Context) { + result, err := GetAccessLogOverview(c.Request.Context(), AccessLogOverviewQuery{ + NodeID: c.Query("node_id"), + Host: c.Query("host"), + Hours: readQueryInt(c, "hours"), + }) + if apiutil.AbortBadRequestOnError(c, err) { + return + } + c.JSON(http.StatusOK, response.OK(result)) +} + // GetAccessLogsHandler 分页列出访问日志。 // @Summary 列出访问日志 // @Description 分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限 diff --git a/internal/model/openflare_access_log.go b/internal/model/openflare_access_log.go index 263da291..764aebc3 100644 --- a/internal/model/openflare_access_log.go +++ b/internal/model/openflare_access_log.go @@ -77,7 +77,7 @@ func TrafficSummaryOpenFlareAccessLogs(ctx context.Context, query OpenFlareAcces return currentAccessLogStore().TrafficSummary(ctx, query) } -// ValueCountsOpenFlareAccessLogs groups logs by status_code or host. +// ValueCountsOpenFlareAccessLogs groups logs by status_code, host, path, or remote_addr. func ValueCountsOpenFlareAccessLogs(ctx context.Context, query OpenFlareAccessLogQuery, column string, limit int) ([]OpenFlareAccessLogValueCount, error) { return currentAccessLogStore().ValueCounts(ctx, query, column, limit) } diff --git a/internal/model/openflare_access_log_store_memory.go b/internal/model/openflare_access_log_store_memory.go index 891fa985..c86001ec 100644 --- a/internal/model/openflare_access_log_store_memory.go +++ b/internal/model/openflare_access_log_store_memory.go @@ -20,6 +20,8 @@ import ( const ( accessLogColumnStatusCode = "status_code" accessLogColumnHost = "host" + accessLogColumnPath = "path" + accessLogColumnRemoteAddr = "remote_addr" ) type memoryAccessLogStore struct { @@ -473,17 +475,24 @@ func (s *memoryAccessLogStore) ValueCounts(_ context.Context, filter OpenFlareAc s.mu.RLock() defer s.mu.RUnlock() col := strings.TrimSpace(strings.ToLower(column)) - if col != accessLogColumnStatusCode && col != accessLogColumnHost { + switch col { + case accessLogColumnStatusCode, accessLogColumnHost, accessLogColumnPath, accessLogColumnRemoteAddr: + default: return nil, nil } rows := s.filterRecords(filter) counts := make(map[string]int64) for _, row := range rows { var value string - if col == accessLogColumnStatusCode { + switch col { + case accessLogColumnStatusCode: value = strconv.Itoa(row.StatusCode) - } else { + case accessLogColumnHost: value = strings.TrimSpace(row.Host) + case accessLogColumnPath: + value = strings.TrimSpace(row.Path) + case accessLogColumnRemoteAddr: + value = strings.TrimSpace(row.RemoteAddr) } if value == "" { continue diff --git a/internal/repository/analytics/node_access_log.go b/internal/repository/analytics/node_access_log.go index 56993df9..2369f6a4 100644 --- a/internal/repository/analytics/node_access_log.go +++ b/internal/repository/analytics/node_access_log.go @@ -206,27 +206,19 @@ WHERE %s`, tableName, clause) } // ValueCountsNodeAccessLogs groups logs by a single dimension column. -// Allowed columns: status_code, host. +// Allowed columns: status_code, host, path, remote_addr. func ValueCountsNodeAccessLogs(ctx context.Context, filter NodeAccessLogFilter, column string, limit int) ([]NodeAccessLogValueCount, error) { conn, err := nodeAccessLogConn() if err != nil { return nil, err } col := strings.TrimSpace(strings.ToLower(column)) - switch col { - case nodeAccessLogColumnStatusCode, nodeAccessLogColumnHost: - default: + valueExpr, ok := nodeAccessLogValueCountExpr(col) + if !ok { return nil, fmt.Errorf("unsupported value count column: %s", column) } clause, args := buildNodeAccessLogFilterClause(filter) tableName := nodeAccessLogTableName() - // status_code is numeric; cast to string for a uniform Value field. - var valueExpr string - if col == nodeAccessLogColumnStatusCode { - valueExpr = "toString(" + nodeAccessLogColumnStatusCode + ")" - } else { - valueExpr = "trim(" + nodeAccessLogColumnHost + ")" - } sql := fmt.Sprintf(` SELECT %s AS value, count() AS count FROM %s @@ -259,6 +251,21 @@ ORDER BY count DESC, value ASC`, valueExpr, tableName, clause, valueExpr) return result, nil } +func nodeAccessLogValueCountExpr(column string) (string, bool) { + switch column { + case nodeAccessLogColumnStatusCode: + return "toString(" + nodeAccessLogColumnStatusCode + ")", true + case nodeAccessLogColumnHost: + return "trim(" + nodeAccessLogColumnHost + ")", true + case nodeAccessLogColumnPath: + return "trim(" + nodeAccessLogColumnPath + ")", true + case nodeAccessLogColumnRemoteAddr: + return "trim(" + nodeAccessLogColumnRemoteAddr + ")", true + default: + return "", false + } +} + // NodeAggregatesNodeAccessLogs returns per-node request/error/UV aggregates. func NodeAggregatesNodeAccessLogs(ctx context.Context, filter NodeAccessLogFilter) ([]NodeAccessLogNodeAggregate, error) { conn, err := nodeAccessLogConn() diff --git a/internal/router/v1/openflare/register_observability.go b/internal/router/v1/openflare/register_observability.go index ea927821..38507c21 100644 --- a/internal/router/v1/openflare/register_observability.go +++ b/internal/router/v1/openflare/register_observability.go @@ -14,6 +14,7 @@ func registerObservabilityRoutes(apiGroup *gin.RouterGroup) { accessLogRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(accessLogRoute, "GET", observability.GetAccessLogsHandler) + accessLogRoute.GET("/overview", observability.GetAccessLogOverviewHandler) accessLogRoute.GET("/folds", observability.GetFoldedAccessLogsHandler) accessLogRoute.GET("/folds/ip-summary", observability.GetFoldedAccessLogIPsHandler) accessLogRoute.GET("/ip-summary", observability.GetAccessLogIPSummariesHandler) diff --git a/pkg/geoip/country_centroid_test.go b/pkg/geoip/country_centroid_test.go index 9b7c955f..d70d316d 100644 --- a/pkg/geoip/country_centroid_test.go +++ b/pkg/geoip/country_centroid_test.go @@ -14,12 +14,12 @@ func TestCountryCentroidGermany(t *testing.T) { func TestCountryCentroidHongKongSingaporeTaiwan(t *testing.T) { cases := []struct { - iso string - name string - minLat float64 - maxLat float64 - minLon float64 - maxLon float64 + iso string + name string + minLat float64 + maxLat float64 + minLon float64 + maxLon float64 }{ {iso: "HK", name: "Hong Kong", minLat: 22, maxLat: 23, minLon: 113, maxLon: 115}, {iso: "SG", name: "Singapore", minLat: 1, maxLat: 2, minLon: 103, maxLon: 104}, diff --git a/pkg/protocol/agent.go b/pkg/protocol/agent.go index 01c38f99..dc714e6f 100644 --- a/pkg/protocol/agent.go +++ b/pkg/protocol/agent.go @@ -128,8 +128,8 @@ type NodeMetricSnapshot struct { MemoryTotalBytes int64 `json:"memory_total_bytes"` StorageUsedBytes int64 `json:"storage_used_bytes"` StorageTotalBytes int64 `json:"storage_total_bytes"` - DiskReadBytes int64 `json:"disk_read_bytes"` - DiskWriteBytes int64 `json:"disk_write_bytes"` + DiskReadBytes int64 `json:"disk_read_bytes"` + DiskWriteBytes int64 `json:"disk_write_bytes"` } // NodeAccessLog is an access log entry from agent (L1 business fact).