diff --git a/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts b/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts index eecb073f..b274e16b 100644 --- a/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts +++ b/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts @@ -25,9 +25,9 @@ export type OpenFlareOpsFields = { }; export const defaultOpenFlareOpsFields: OpenFlareOpsFields = { - agent_heartbeat_interval: '10000', + agent_heartbeat_interval: '3000', agent_websocket_upgrade_enabled: true, - node_offline_threshold: '120000', + node_offline_threshold: '60000', agent_update_repo: 'Rain-kl/OpenFlare', geoip_provider: 'ipinfo', server_address: '', @@ -68,12 +68,12 @@ export function mapOptionsToOpsFields( serverAddress = '', ): OpenFlareOpsFields { return { - agent_heartbeat_interval: optionMap.agent_heartbeat_interval ?? '10000', + agent_heartbeat_interval: optionMap.agent_heartbeat_interval ?? '3000', agent_websocket_upgrade_enabled: toBoolean( optionMap.agent_websocket_upgrade_enabled, true, ), - node_offline_threshold: optionMap.node_offline_threshold ?? '120000', + node_offline_threshold: optionMap.node_offline_threshold ?? '60000', agent_update_repo: optionMap.agent_update_repo ?? 'Rain-kl/OpenFlare', geoip_provider: optionMap.geoip_provider ?? 'ipinfo', server_address: optionMap.server_address || serverAddress, @@ -129,12 +129,15 @@ export function buildDiscoveryCommand( export function validateAgentFields(fields: OpenFlareOpsFields) { const heartbeat = Number.parseInt(fields.agent_heartbeat_interval, 10); const offline = Number.parseInt(fields.node_offline_threshold, 10); - if (Number.isNaN(heartbeat) || heartbeat < 5000) { - throw new Error('心跳间隔不能小于 5000 毫秒。'); + if (Number.isNaN(heartbeat) || heartbeat < 1000) { + throw new Error('心跳间隔不能小于 1000 毫秒。'); } if (Number.isNaN(offline) || offline < 10000) { throw new Error('离线阈值不能小于 10000 毫秒。'); } + if (offline < heartbeat * 3) { + throw new Error('离线阈值建议至少为心跳间隔的 3 倍。'); + } } export function validateUptimeKumaFields(fields: OpenFlareOpsFields) { diff --git a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx index 7c378b4e..c81f6218 100644 --- a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx +++ b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx @@ -90,10 +90,10 @@ const cleanupTargets: Array<{ '清理 node_metric_snapshots,影响节点资源趋势;表 TTL 为 30 天。', }, { - target: 'node_request_reports', - label: '请求聚合', + target: 'node_edge_health', + label: 'OpenResty 健康', description: - '清理 node_request_reports,影响请求量与错误量统计;表 TTL 为 30 天。', + '清理 node_edge_health(OpenResty 连接/健康快照);表 TTL 为 30 天。业务流量请清理访问日志。', }, ]; diff --git a/frontend/app/(main)/components/dashboard/network-disk-trend-chart.tsx b/frontend/app/(main)/components/dashboard/network-disk-trend-chart.tsx index 42997e0e..912e3bb9 100644 --- a/frontend/app/(main)/components/dashboard/network-disk-trend-chart.tsx +++ b/frontend/app/(main)/components/dashboard/network-disk-trend-chart.tsx @@ -20,7 +20,7 @@ export function NetworkDiskTrendChart({ - 24 小时网络与磁盘趋势 + 24 小时业务流量与宿主机磁盘 @@ -30,21 +30,47 @@ export function NetworkDiskTrendChart({ )} height={180} summaryScope='total' - summaryHint='近 24 小时' + summaryHint='近 24 小时 · 来自访问日志' yAxisValueFormatter={formatBytes} series={[ { - label: 'OpenResty 入站', + label: '接收数据', color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.14)', variant: 'area', - values: networkPoints.map((point) => point.openresty_rx_bytes), + values: networkPoints.map((point) => point.bytes_received), valueFormatter: formatBytes, }, { - label: 'OpenResty 出站', + label: '已提供数据', color: '#38bdf8', - values: networkPoints.map((point) => point.openresty_tx_bytes), + values: networkPoints.map((point) => point.bytes_provided), + valueFormatter: formatBytes, + }, + ]} + /> + + + formatTrendHour(point.bucket_started_at), + )} + height={180} + summaryScope='total' + summaryHint='近 24 小时 · 宿主机网卡' + yAxisValueFormatter={formatBytes} + series={[ + { + label: '宿主机网卡入站', + color: '#a3e635', + fillColor: 'rgba(163, 230, 53, 0.12)', + variant: 'area', + values: networkPoints.map((point) => point.network_rx_bytes), + valueFormatter: formatBytes, + }, + { + label: '宿主机网卡出站', + color: '#f97316', + values: networkPoints.map((point) => point.network_tx_bytes), valueFormatter: formatBytes, }, ]} @@ -56,7 +82,7 @@ export function NetworkDiskTrendChart({ )} height={180} summaryScope='total' - summaryHint='近 24 小时' + summaryHint='近 24 小时 · 宿主机磁盘' yAxisValueFormatter={formatBytes} series={[ { diff --git a/frontend/app/(main)/components/dashboard/stat-cards.tsx b/frontend/app/(main)/components/dashboard/stat-cards.tsx index da5b4cb5..6828c51f 100644 --- a/frontend/app/(main)/components/dashboard/stat-cards.tsx +++ b/frontend/app/(main)/components/dashboard/stat-cards.tsx @@ -40,7 +40,7 @@ export function DashboardStatCards({ {formatCompactNumber(traffic.request_count)}

- 窗口UV(估) {formatCompactNumber(traffic.unique_visitors)} · 错误{' '} + 24h 独立访客 {formatCompactNumber(traffic.unique_visitors)} · 错误{' '} {formatCompactNumber(traffic.error_count)} · 估算 QPS{' '} {traffic.estimated_qps.toFixed(2)}

diff --git a/frontend/app/(main)/nodes/components/network-trend-chart.tsx b/frontend/app/(main)/nodes/components/network-trend-chart.tsx index 6d0eca0b..0e361783 100644 --- a/frontend/app/(main)/nodes/components/network-trend-chart.tsx +++ b/frontend/app/(main)/nodes/components/network-trend-chart.tsx @@ -18,7 +18,7 @@ import { export function NetworkTrendChart({ points, title = '24 小时网络趋势', - description = '按小时展示 OpenResty 入站/出站流量累计,摘要为近 24 小时总量。', + description = '按小时展示已提供/接收数据(访问日志)与宿主机网卡;摘要为近 24 小时总量。', }: { points: NetworkTrendPoint[]; title?: string; @@ -40,17 +40,29 @@ export function NetworkTrendChart({ yAxisValueFormatter={formatBytes} series={[ { - label: 'OpenResty 入站', + label: '接收数据', color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.14)', variant: 'area', - values: points.map((point) => point.openresty_rx_bytes), + values: points.map((point) => point.bytes_received), valueFormatter: formatBytes, }, { - label: 'OpenResty 出站', + label: '已提供数据', color: '#38bdf8', - values: points.map((point) => point.openresty_tx_bytes), + values: points.map((point) => point.bytes_provided), + valueFormatter: formatBytes, + }, + { + label: '宿主机网卡入站', + color: '#a3e635', + values: points.map((point) => point.network_rx_bytes), + valueFormatter: formatBytes, + }, + { + label: '宿主机网卡出站', + color: '#f97316', + values: points.map((point) => point.network_tx_bytes), valueFormatter: formatBytes, }, ]} diff --git a/frontend/app/(main)/nodes/components/node-observability.tsx b/frontend/app/(main)/nodes/components/node-observability.tsx index 4ee6b792..45ecf4fb 100644 --- a/frontend/app/(main)/nodes/components/node-observability.tsx +++ b/frontend/app/(main)/nodes/components/node-observability.tsx @@ -38,9 +38,7 @@ import { DistributionList } from './distribution-list'; import { NetworkTrendChart } from './network-trend-chart'; import { NodeStatusBadge } from './node-status-badge'; import { - aggregateTrafficBreakdown, formatBytes, - formatBytesPerSecond, formatMetricCount, formatPercent, formatRelativeTime, @@ -395,26 +393,26 @@ export function NodeObservability({ resolvedHealthEvents, ]); + const trafficSummary = observability?.analytics?.traffic ?? null; + const healthSummary = observability?.analytics?.health ?? null; + const distributions = observability?.analytics?.distributions; const statusCodeDistribution = useMemo( () => - aggregateTrafficBreakdown( - observability?.traffic_reports, - 'status_codes_json', - ), - [observability?.traffic_reports], + (distributions?.status_codes ?? []).map((item) => ({ + label: item.key, + value: item.value, + })), + [distributions?.status_codes], ); const topDomains = useMemo( () => - aggregateTrafficBreakdown( - observability?.traffic_reports, - 'top_domains_json', - ), - [observability?.traffic_reports], + (distributions?.top_domains ?? []).map((item) => ({ + label: item.key, + value: item.value, + })), + [distributions?.top_domains], ); - const trafficSummary = observability?.analytics?.traffic ?? null; - const healthSummary = observability?.analytics?.health ?? null; - const topSourceCountry = - observability?.analytics?.distributions?.source_countries?.[0] ?? null; + const topSourceCountry = distributions?.source_countries?.[0] ?? null; const latestHealthEvent = activeHealthEvents[0] ?? null; const dominantStatusCode = statusCodeDistribution[0] ?? null; const dominantDomain = topDomains[0] ?? null; @@ -604,11 +602,11 @@ export function NodeObservability({ } /> @@ -721,22 +719,15 @@ export function NodeObservability({

- OpenResty 吞吐 + OpenResty 连接

- 入站: - {formatBytesPerSecond( - latestMetric.openresty_rx_bytes, - 60, - )} + 当前连接: + {latestMetric.openresty_connections ?? 0}

-

- 出站: - {formatBytesPerSecond( - latestMetric.openresty_tx_bytes, - 60, - )} +

+ 业务吞吐请看访问日志趋势(已提供/接收数据)

@@ -754,20 +745,20 @@ export function NodeObservability({

- 请求/分钟 + 查询窗口独立访客

- {formatMetricCount(trafficSummary?.request_count)} + {formatMetricCount(trafficSummary?.unique_visitor_count)}

{trafficSummary - ? `近 60 秒 · 窗口UV ${formatMetricCount(trafficSummary.unique_visitor_count)}` + ? `请求 ${formatMetricCount(trafficSummary.request_count)} · 估算 QPS ${trafficSummary.estimated_qps.toFixed(2)}` : '暂无窗口流量摘要'}

- 近 60 秒错误 + 查询窗口错误

{formatMetricCount(trafficSummary?.error_count)} diff --git a/frontend/app/(main)/nodes/components/node-utils.ts b/frontend/app/(main)/nodes/components/node-utils.ts index 0c98c741..1c29c8d7 100644 --- a/frontend/app/(main)/nodes/components/node-utils.ts +++ b/frontend/app/(main)/nodes/components/node-utils.ts @@ -2,7 +2,6 @@ import type { ApplyResult, NodeItem, NodeStatus, - NodeTrafficReport, OpenrestyStatus, } from '@/lib/services/openflare'; @@ -238,48 +237,6 @@ export function formatBytesPerSecond(value?: number | null, windowSeconds = 1) { return `${formatBytes(value / windowSeconds)}/s`; } -export function parseTrafficMap(value?: string | null) { - if (!value) { - return {} as Record; - } - try { - const parsed = JSON.parse(value) as Record; - return Object.entries(parsed).reduce>( - (result, [key, count]) => { - if (typeof count === 'number' && Number.isFinite(count)) { - result[key] = count; - } - return result; - }, - {}, - ); - } catch { - return {} as Record; - } -} - -export function aggregateTrafficBreakdown( - reports: NodeTrafficReport[] | undefined, - field: 'status_codes_json' | 'top_domains_json', -) { - const summary = new Map(); - for (const report of reports ?? []) { - const parsed = parseTrafficMap(report[field]); - for (const [key, value] of Object.entries(parsed)) { - summary.set(key, (summary.get(key) ?? 0) + value); - } - } - return Array.from(summary.entries()) - .sort((left, right) => { - if (right[1] === left[1]) { - return left[0].localeCompare(right[0]); - } - return right[1] - left[1]; - }) - .slice(0, 6) - .map(([label, value]) => ({ label, value })); -} - export function formatUsageRatio(used?: number | null, total?: number | null) { if (!used || !total || total <= 0) { return null; diff --git a/frontend/lib/services/openflare/dashboard.service.ts b/frontend/lib/services/openflare/dashboard.service.ts index 9699b075..2dba8819 100644 --- a/frontend/lib/services/openflare/dashboard.service.ts +++ b/frontend/lib/services/openflare/dashboard.service.ts @@ -130,11 +130,19 @@ function normalizeNetworkTrendPoints( bucket_started_at: String(item[0] ?? ''), network_rx_bytes: Number(item[1] ?? 0), network_tx_bytes: Number(item[2] ?? 0), - openresty_rx_bytes: Number(item[3] ?? 0), - openresty_tx_bytes: Number(item[4] ?? 0), + bytes_received: Number(item[3] ?? 0), + bytes_provided: Number(item[4] ?? 0), reported_nodes: Number(item[5] ?? 0), } - : item, + : { + ...item, + bytes_received: Number( + (item as { bytes_received?: number }).bytes_received ?? 0, + ), + bytes_provided: Number( + (item as { bytes_provided?: number }).bytes_provided ?? 0, + ), + }, ); } diff --git a/frontend/lib/services/openflare/index.ts b/frontend/lib/services/openflare/index.ts index 5e8f1225..ae704112 100644 --- a/frontend/lib/services/openflare/index.ts +++ b/frontend/lib/services/openflare/index.ts @@ -40,7 +40,6 @@ export type { NodeObservabilityAnalytics, NodeObservabilityTrends, NodeSystemProfile, - NodeTrafficReport, NodeStatus, NodeType, OpenrestyStatus, diff --git a/frontend/lib/services/openflare/types.ts b/frontend/lib/services/openflare/types.ts index b5f77d9f..589e302a 100644 --- a/frontend/lib/services/openflare/types.ts +++ b/frontend/lib/services/openflare/types.ts @@ -122,8 +122,6 @@ export interface NodeMetricSnapshot { disk_write_bytes: number; network_rx_bytes: number; network_tx_bytes: number; - openresty_rx_bytes: number; - openresty_tx_bytes: number; openresty_connections: number; } @@ -139,17 +137,6 @@ export interface NodeHealthEvent { resolved_at?: string | null; } -export interface NodeTrafficReport { - window_started_at: string; - window_ended_at: string; - request_count: number; - error_count: number; - unique_visitor_count: number; - status_codes_json: string; - top_domains_json: string; - source_countries_json: string; -} - export interface NodeTrafficSummary { window_started_at: string; window_ended_at: string; @@ -194,7 +181,6 @@ export interface NodeObservability { node_id: string; profile: NodeSystemProfile | null; metric_snapshots: NodeMetricSnapshot[]; - traffic_reports?: NodeTrafficReport[]; health_events: NodeHealthEvent[]; analytics?: NodeObservabilityAnalytics; trends?: NodeObservabilityTrends; @@ -665,7 +651,11 @@ export interface GeoIPLookupResult { } export type DatabaseCleanupTarget = - 'node_access_logs' | 'node_metric_snapshots' | 'node_request_reports'; + | 'node_access_logs' + | 'node_metric_snapshots' + | 'node_edge_health' + | 'node_obs_frps' + | 'node_obs_frpc'; export interface DatabaseCleanupPayload { target: DatabaseCleanupTarget; @@ -936,8 +926,10 @@ export interface NetworkTrendPoint { bucket_started_at: string; network_rx_bytes: number; network_tx_bytes: number; - openresty_rx_bytes: number; - openresty_tx_bytes: number; + /** L1 接收数据 sum(request_length) */ + bytes_received: number; + /** L1 已提供数据 sum(bytes_sent) */ + bytes_provided: number; reported_nodes: number; }