mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
feat(frontend): 观测网络字节字段与 UV 文案对齐
网络图仅使用 bytes_provided/received;看板与节点 UV 改为 24h/查询窗口 独立访客;清理 traffic_reports 与 openresty 吞吐兼容字段;运维清理目标 改为 node_edge_health。
This commit is contained in:
@@ -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,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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({
|
||||
}
|
||||
/>
|
||||
<SummaryStat
|
||||
label='请求/分钟'
|
||||
label='查询窗口请求'
|
||||
value={formatMetricCount(trafficSummary?.request_count)}
|
||||
hint={
|
||||
trafficSummary
|
||||
? `近 60 秒 · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
|
||||
? `查询窗口独立访客 ${formatMetricCount(trafficSummary.unique_visitor_count)} · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
|
||||
: '当前没有可展示的请求窗口摘要'
|
||||
}
|
||||
/>
|
||||
@@ -721,22 +719,15 @@ export function NodeObservability({
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div className='rounded-lg border px-3 py-3'>
|
||||
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
|
||||
OpenResty 吞吐
|
||||
OpenResty 连接
|
||||
</p>
|
||||
<div className='mt-3 space-y-2 text-sm text-muted-foreground'>
|
||||
<p>
|
||||
入站:
|
||||
{formatBytesPerSecond(
|
||||
latestMetric.openresty_rx_bytes,
|
||||
60,
|
||||
)}
|
||||
当前连接:
|
||||
{latestMetric.openresty_connections ?? 0}
|
||||
</p>
|
||||
<p>
|
||||
出站:
|
||||
{formatBytesPerSecond(
|
||||
latestMetric.openresty_tx_bytes,
|
||||
60,
|
||||
)}
|
||||
<p className='text-xs'>
|
||||
业务吞吐请看访问日志趋势(已提供/接收数据)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -754,20 +745,20 @@ export function NodeObservability({
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div className='rounded-lg border px-3 py-3'>
|
||||
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
|
||||
请求/分钟
|
||||
查询窗口独立访客
|
||||
</p>
|
||||
<p className='mt-3 text-2xl font-semibold'>
|
||||
{formatMetricCount(trafficSummary?.request_count)}
|
||||
{formatMetricCount(trafficSummary?.unique_visitor_count)}
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-muted-foreground'>
|
||||
{trafficSummary
|
||||
? `近 60 秒 · 窗口UV ${formatMetricCount(trafficSummary.unique_visitor_count)}`
|
||||
? `请求 ${formatMetricCount(trafficSummary.request_count)} · 估算 QPS ${trafficSummary.estimated_qps.toFixed(2)}`
|
||||
: '暂无窗口流量摘要'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='rounded-lg border px-3 py-3'>
|
||||
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
|
||||
近 60 秒错误
|
||||
查询窗口错误
|
||||
</p>
|
||||
<p className='mt-3 text-2xl font-semibold'>
|
||||
{formatMetricCount(trafficSummary?.error_count)}
|
||||
|
||||
@@ -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<string, number>;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value) as Record<string, number>;
|
||||
return Object.entries(parsed).reduce<Record<string, number>>(
|
||||
(result, [key, count]) => {
|
||||
if (typeof count === 'number' && Number.isFinite(count)) {
|
||||
result[key] = count;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{},
|
||||
);
|
||||
} catch {
|
||||
return {} as Record<string, number>;
|
||||
}
|
||||
}
|
||||
|
||||
export function aggregateTrafficBreakdown(
|
||||
reports: NodeTrafficReport[] | undefined,
|
||||
field: 'status_codes_json' | 'top_domains_json',
|
||||
) {
|
||||
const summary = new Map<string, number>();
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user