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).