feat: 增加 Metadata 失败原因诊断
This commit is contained in:
@@ -348,6 +348,7 @@
|
||||
- [x] 增加配置查询完整校验原子保存并发修订和统一重启提示
|
||||
- [x] 增加 Web 诊断页和配置管理页
|
||||
- [x] 精简诊断页即时指标并由趋势图承担重复的资源和吞吐数据
|
||||
- [x] 在系统诊断中区分 Peer 下载结果和 Metadata 内容有效性并展示 Peer 失败原因构成
|
||||
- [x] 使用页签拆分配置分类并隐藏底层配置存储位置
|
||||
- [x] 将主配置和内容过滤规则合并为唯一 `config.toml`
|
||||
|
||||
|
||||
@@ -104,6 +104,17 @@ pub(crate) async fn stats(State(state): State<ApiState>) -> Json<StatsResponse>
|
||||
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()),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<CurrentDiagnostics | null>(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<EChartsOption>(() => ({
|
||||
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(() => {
|
||||
<section class="mt-6 grid gap-4 lg:grid-cols-2">
|
||||
<article class="chart-card"><div><h2>进程内存</h2><p>{{ sample.process.thread_count ?? '未知' }} 个线程 · {{ sample.process.handle_count ?? '未知' }} 个句柄</p></div><VChart class="diagnostic-chart" :option="memoryChart" autoresize /></article>
|
||||
<article class="chart-card"><div><h2>RocksDB 内存与压缩</h2><p>SST {{ sample.storage.live_sst_bytes === null ? '未知' : formatBytes(sample.storage.live_sst_bytes) }}</p></div><VChart class="diagnostic-chart" :option="storageChart" autoresize /></article>
|
||||
<article class="chart-card lg:col-span-2"><div><h2>网络与 Metadata 吞吐</h2><p>{{ history?.samples.length ?? 0 }} 个采样点 · 最近保存 {{ formatDate(current?.status.last_persisted_at ?? null) }}</p></div><VChart class="diagnostic-chart" :option="throughputChart" autoresize /></article>
|
||||
<article class="chart-card"><div><h2>网络与 Peer 下载吞吐</h2><p>{{ history?.samples.length ?? 0 }} 个采样点 · 最近保存 {{ formatDate(current?.status.last_persisted_at ?? null) }}</p></div><VChart class="diagnostic-chart" :option="throughputChart" autoresize /></article>
|
||||
<article class="chart-card"><div><h2>Peer 失败原因构成</h2><p>{{ metadataPeerSummary }}</p></div><VChart class="diagnostic-chart" :option="metadataFailureChart" autoresize /></article>
|
||||
<article class="chart-card lg:col-span-2"><div><h2>HTTP 请求与错误</h2><p>当前并发 {{ sample.http.active_requests }} · P95 {{ sample.http.latency_p95_millis === null ? '暂无' : `${sample.http.latency_p95_millis} ms` }}</p></div><VChart class="diagnostic-chart" :option="httpChart" autoresize /></article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user