From 4671a1d0ede68cc7f595bc633db180fb3c49c581 Mon Sep 17 00:00:00 2001 From: chuan Date: Mon, 10 Aug 2026 18:01:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20Metadata=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODOS.md | 1 + src/search/src/api/handlers.rs | 11 ++++++ src/search/src/api/mod.rs | 10 ++++++ src/search/src/api/response.rs | 11 ++++++ src/web/src/pages/DiagnosticsPage.vue | 49 ++++++++++++++++++++++++--- src/web/src/types/api.ts | 12 +++++++ 6 files changed, 89 insertions(+), 5 deletions(-) diff --git a/TODOS.md b/TODOS.md index bd3dc5e..9811c65 100644 --- a/TODOS.md +++ b/TODOS.md @@ -348,6 +348,7 @@ - [x] 增加配置查询完整校验原子保存并发修订和统一重启提示 - [x] 增加 Web 诊断页和配置管理页 - [x] 精简诊断页即时指标并由趋势图承担重复的资源和吞吐数据 +- [x] 在系统诊断中区分 Peer 下载结果和 Metadata 内容有效性并展示 Peer 失败原因构成 - [x] 使用页签拆分配置分类并隐藏底层配置存储位置 - [x] 将主配置和内容过滤规则合并为唯一 `config.toml` diff --git a/src/search/src/api/handlers.rs b/src/search/src/api/handlers.rs index f51c0a0..6a8e904 100644 --- a/src/search/src/api/handlers.rs +++ b/src/search/src/api/handlers.rs @@ -104,6 +104,17 @@ pub(crate) async fn stats(State(state): State) -> Json metadata_ok_from_sample_snapshot: dht.metadata_success_from_sample_snapshot, metadata_ok_from_active_lookup: dht.metadata_success_from_active_lookup, metadata_failed: dht.metadata_peer_failed, + metadata_peer_canceled: dht.metadata_peer_canceled, + metadata_peer_failure_cache_hits: dht.metadata_peer_failure_cache_hits, + metadata_peer_failure_cache_entries: dht.metadata_peer_failure_cache_entries, + metadata_failure_timeout: observability.metadata_failure_timeout, + metadata_failure_connect: observability.metadata_failure_connect, + metadata_failure_no_extension: observability.metadata_failure_no_extension, + metadata_failure_send: observability.metadata_failure_send, + metadata_failure_size_limit: observability.metadata_failure_size_limit, + metadata_failure_sha1: observability.metadata_failure_sha1, + metadata_failure_parse: observability.metadata_failure_parse, + metadata_failure_other: observability.metadata_failure_other, metadata_filtered: observability .metadata_failure_size_limit .saturating_add(filtered.total()), diff --git a/src/search/src/api/mod.rs b/src/search/src/api/mod.rs index d6d6706..48cb1ac 100644 --- a/src/search/src/api/mod.rs +++ b/src/search/src/api/mod.rs @@ -205,6 +205,16 @@ mod tests { .unwrap(); assert_eq!(json["metadata_filtered"], 0); assert_eq!(json["metadata_filtered_too_many_files"], 0); + assert_eq!(json["metadata_failure_timeout"], 0); + assert_eq!(json["metadata_failure_connect"], 0); + assert_eq!(json["metadata_failure_no_extension"], 0); + assert_eq!(json["metadata_failure_send"], 0); + assert_eq!(json["metadata_failure_size_limit"], 0); + assert_eq!(json["metadata_failure_sha1"], 0); + assert_eq!(json["metadata_failure_parse"], 0); + assert_eq!(json["metadata_failure_other"], 0); + assert_eq!(json["metadata_peer_canceled"], 0); + assert_eq!(json["metadata_peer_failure_cache_hits"], 0); assert_eq!(json["disk_state"], "normal"); assert!(json["disk_available_bytes"].is_null()); assert_eq!(json["backup_created"], 0); diff --git a/src/search/src/api/response.rs b/src/search/src/api/response.rs index 471836e..1f98fda 100644 --- a/src/search/src/api/response.rs +++ b/src/search/src/api/response.rs @@ -68,6 +68,17 @@ pub(crate) struct StatsResponse { pub(crate) metadata_ok_from_sample_snapshot: u64, pub(crate) metadata_ok_from_active_lookup: u64, pub(crate) metadata_failed: u64, + pub(crate) metadata_peer_canceled: u64, + pub(crate) metadata_peer_failure_cache_hits: u64, + pub(crate) metadata_peer_failure_cache_entries: usize, + pub(crate) metadata_failure_timeout: u64, + pub(crate) metadata_failure_connect: u64, + pub(crate) metadata_failure_no_extension: u64, + pub(crate) metadata_failure_send: u64, + pub(crate) metadata_failure_size_limit: u64, + pub(crate) metadata_failure_sha1: u64, + pub(crate) metadata_failure_parse: u64, + pub(crate) metadata_failure_other: u64, pub(crate) metadata_filtered: u64, pub(crate) metadata_filtered_too_large: u64, pub(crate) metadata_filtered_invalid_info_hash: u64, diff --git a/src/web/src/pages/DiagnosticsPage.vue b/src/web/src/pages/DiagnosticsPage.vue index c922e77..499d1db 100644 --- a/src/web/src/pages/DiagnosticsPage.vue +++ b/src/web/src/pages/DiagnosticsPage.vue @@ -3,7 +3,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { AlertTriangle, Database, Gauge, HardDrive, LoaderCircle, Network } from '@lucide/vue' import { use } from 'echarts/core' import { CanvasRenderer } from 'echarts/renderers' -import { LineChart } from 'echarts/charts' +import { LineChart, PieChart } from 'echarts/charts' import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components' import type { EChartsOption } from 'echarts' import VChart from 'vue-echarts' @@ -13,7 +13,7 @@ import { getCurrentDiagnostics, getDiagnosticHistory, getStats } from '@/lib/api import { formatBytes, formatDate } from '@/lib/format' import type { CurrentDiagnostics, DiagnosticHistory, DiagnosticSample, ServiceStats } from '@/types/api' -use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent]) +use([CanvasRenderer, LineChart, PieChart, GridComponent, LegendComponent, TooltipComponent]) const rangeSecs = ref(3_600) const current = ref(null) @@ -66,10 +66,48 @@ const storageChart = computed(() => chartOption([ const throughputChart = computed(() => chartOption([ { name: 'UDP 发送', values: perSecond(samples.value, (item) => item.runtime.udp_tx_packets) }, - { name: 'Metadata 成功', values: perSecond(samples.value, (item) => item.runtime.metadata_succeeded) }, - { name: 'Metadata 失败', values: perSecond(samples.value, (item) => item.runtime.metadata_failed) }, + { name: 'Peer 下载成功', values: perSecond(samples.value, (item) => item.runtime.metadata_succeeded) }, + { name: 'Peer 尝试失败', values: perSecond(samples.value, (item) => item.runtime.metadata_failed) }, ], '/s')) +const metadataFailureReasons = computed(() => { + if (!stats.value) return [] + return [ + { name: '连接失败', value: stats.value.metadata_failure_connect }, + { name: '下载超时', value: stats.value.metadata_failure_timeout }, + { name: '不支持扩展', value: stats.value.metadata_failure_no_extension }, + { name: '发送失败', value: stats.value.metadata_failure_send }, + { name: '超过大小限制', value: stats.value.metadata_failure_size_limit }, + { name: 'SHA1 不一致', value: stats.value.metadata_failure_sha1 }, + { name: '解析失败', value: stats.value.metadata_failure_parse }, + { name: '接收或分片错误', value: stats.value.metadata_failure_other }, + ].filter((item) => item.value > 0) +}) + +const metadataFailureChart = computed(() => ({ + animation: false, + color: ['#ef4444', '#f59e0b', '#8b5cf6', '#ec4899', '#6366f1', '#14b8a6', '#84cc16', '#64748b'], + tooltip: { trigger: 'item', valueFormatter: (value) => `${Number(value).toLocaleString()} 次` }, + legend: { type: 'scroll', bottom: 0, textStyle: { color: '#737373' } }, + series: [{ + type: 'pie', + radius: ['38%', '66%'], + center: ['50%', '42%'], + minAngle: 2, + stillShowZeroSum: false, + avoidLabelOverlap: true, + label: { color: '#737373', formatter: '{b}\n{d}%' }, + data: metadataFailureReasons.value, + }], +})) + +const metadataPeerSummary = computed(() => { + if (!stats.value) return '' + const completed = stats.value.metadata_ok + stats.value.metadata_failed + const successRate = completed === 0 ? 0 : stats.value.metadata_ok * 100 / completed + return `成功率 ${successRate.toFixed(2)}% · 成功后取消 ${stats.value.metadata_peer_canceled.toLocaleString()} 次` +}) + const httpChart = computed(() => chartOption([ { name: 'HTTP 请求', values: perSecond(samples.value, (item) => item.http.requests) }, { name: '客户端错误', values: perSecond(samples.value, (item) => item.http.client_errors) }, @@ -184,7 +222,8 @@ onBeforeUnmount(() => {

进程内存

{{ sample.process.thread_count ?? '未知' }} 个线程 · {{ sample.process.handle_count ?? '未知' }} 个句柄

RocksDB 内存与压缩

SST {{ sample.storage.live_sst_bytes === null ? '未知' : formatBytes(sample.storage.live_sst_bytes) }}

-

网络与 Metadata 吞吐

{{ history?.samples.length ?? 0 }} 个采样点 · 最近保存 {{ formatDate(current?.status.last_persisted_at ?? null) }}

+

网络与 Peer 下载吞吐

{{ history?.samples.length ?? 0 }} 个采样点 · 最近保存 {{ formatDate(current?.status.last_persisted_at ?? null) }}

+

Peer 失败原因构成

{{ metadataPeerSummary }}

HTTP 请求与错误

当前并发 {{ sample.http.active_requests }} · P95 {{ sample.http.latency_p95_millis === null ? '暂无' : `${sample.http.latency_p95_millis} ms` }}

diff --git a/src/web/src/types/api.ts b/src/web/src/types/api.ts index 42ce64e..11afad2 100644 --- a/src/web/src/types/api.ts +++ b/src/web/src/types/api.ts @@ -112,8 +112,20 @@ export interface ServiceStats { disk_transitions: number disk_rejected_new_work: number nodes: number + metadata_peer_attempts: number metadata_ok: number metadata_failed: number + metadata_peer_canceled: number + metadata_peer_failure_cache_hits: number + metadata_peer_failure_cache_entries: number + metadata_failure_timeout: number + metadata_failure_connect: number + metadata_failure_no_extension: number + metadata_failure_send: number + metadata_failure_size_limit: number + metadata_failure_sha1: number + metadata_failure_parse: number + metadata_failure_other: number metadata_in_flight: number metadata_filtered: number persistence_inserted: number