1
0
Files
DiAL/openspec/changes/enhance-frontend-metrics/design.md

9.3 KiB
Raw Blame History

Context

DiAL 是一个基于 Bun 的全栈拨测监控系统,当前前端统计指标体系存在以下问题:

  1. 计算逻辑缺陷:可用率基于全量历史数据计算(store.ts:getAllTargetStats 无 WHERE 时间条件),随运行时间增长近期变化被稀释;computeTrendStats 从已截断的百分比反推整数有累积精度损失;lastCheckTime 已返回但前端未展示
  2. 指标维度单一Summary 仅 3 个计数卡片Drawer 统计区 4 个指标(总检查/正常/异常/可用率)本质是同一维度的冗余表达
  3. 缺少关键运维指标:无 P95 延迟、无 MTTR、无故障分析、无连续状态信息

当前技术栈:后端 Bun + SQLitebun:sqlite前端 React + TDesign + recharts + TanStack Query。

Goals / Non-Goals

Goals:

  • 修复可用率时间窗口、趋势数据精度损失、lastCheckTime 未展示三个计算逻辑问题
  • Summary 增加「24h 异常事件数」卡片
  • 表格增加「连续状态」列Tag 样式,按次数)
  • Drawer 统计区重构为 2×4 多维度布局(可用率/平均延迟/P95/检查总数 + MTTR/最长故障/故障次数/连续正常)
  • 趋势图增加延迟范围面积min/max去掉可用率线改为异常时刻红色标记点
  • 新增 /api/targets/:id/stats 端点,职责清晰(单目标非时序聚合指标)
  • Drawer 统计区支持时间窗口切换24h/7d/30d联动统计+趋势

Non-Goals:

  • 不做整体可用率(不同分组不同目的的 target 算到一起无意义)
  • 不做延迟 sparkline表格已有状态条信息密度够了
  • 不做趋势对比vs 上周)
  • 不做连续状态按时长展示(不同间隔的目标无法统一)

Decisions

Decision 1P95 在应用层计算,不在 SQL 层

选择:新增 getTargetDurations(targetId, from, to) 方法,一次性取出时间窗口内所有成功检查的 duration_ms,在 TypeScript 层排序取 P95/P99。

理由SQLite 无原生 PERCENTILE 函数,用子查询模拟的 SQL 复杂且性能不可控。应用层排序对于单目标时间窗口内的数据量24h × 每分钟 1 次 = 1440 条)完全可接受。

替代方案SQLite 扩展函数 / 窗口函数模拟 — 复杂度高,可移植性差。

命名:方法名统一为 getTargetDurations(非 getTargetPercentiles),因为该方法职责是取原始数据,百分位计算在调用方完成。

Decision 2新增独立 /api/targets/:id/stats 端点

选择:创建新端点而非扩展现有 /api/targets/:id/trend

理由

  • /trend 的职责是时序聚合数据(按小时分组),返回数组
  • /stats 的职责是非时序聚合指标P95、MTTR、故障分析返回单个对象
  • 两者语义清晰,避免一个大而全的端点
  • /stats 只在 Drawer 打开时请求,不影响列表页性能

替代方案:扩展 /trend 在响应中附加 summary 字段 — 混淆了时序和聚合两种数据语义。

Decision 3异常事件数按「状态翻转」计数

选择:统计 matched 从 1→0 的转换次数(跨所有目标),而非每次 matched=0 的检查次数。

理由:一个目标连续异常 10 次只算 1 次事件,反映的是「发生了几次故障」而非「有多少次检查失败」。后者已经在可用率中体现。

实现SQL 使用 LAG 窗口函数检测前后状态变化:

SELECT COUNT(*) FROM (
  SELECT matched, LAG(matched) OVER (PARTITION BY target_id ORDER BY timestamp) as prev
  FROM check_results WHERE timestamp >= ?
) WHERE matched = 0 AND (prev = 1 OR prev IS NULL)

Decision 4连续状态从 recentSamples 前端计算

选择:不新增 API从已有的 recentSamples30 条)在前端计算连续状态次数。

理由

  • recentSamples 已经按时间倒序返回,遍历到第一个状态不同的即可
  • 无需额外网络请求
  • 30 条样本对于连续状态计数足够(超过 30 次连续正常/异常的场景下,显示 "30+" 即可)

Decision 5趋势图去掉可用率线改为异常标记点

选择:移除 availability 折线和右侧 Y 轴(%),改为单 Y 轴ms。在 avgDurationMs 线上,对 availability < 100 的时间点渲染红色 dot 标记异常。

理由:可用率通常是 100% 或接近 100%,作为连续曲线信息量极低(大部分时间是一条直线)。改为离散标记点后,异常时刻一目了然,且不占用 Y 轴空间。

实现:使用 recharts <Line>dot 回调函数,对 availability < 100 的点渲染红色圆点(fill: var(--td-error-color)),其余点不渲染 dot。移除右侧 Y 轴和 availability Line 组件。

Decision 6时间窗口切换联动机制

选择Drawer 中的时间窗口切换同时影响统计区和趋势图stats 和 trend 同时刷新。

实现

  • stats 请求直接复用 Drawer 现有的 timeFrom/timeTo 状态,不引入额外时间状态
  • 统计区数据来自 /api/targets/:id/stats?from=&to=
  • 趋势图数据来自 /api/targets/:id/trend?from=&to=
  • 切换快捷按钮1h/6h/24h/7dtimeFrom/timeTo 更新stats 和 trend 的 queryKey 变化触发同时刷新
  • 默认选中 24h
  • 表格的可用率固定 24h 窗口:前端 useTargets 请求 /api/targets?window=24h,后端解析 window 查询参数并转换为时间范围传递给 getAllTargetStats(from, to),列标题改为"可用率(24h)"

Decision 7Drawer 统计区 2×4 布局

选择:统计区和可靠性区合并为一个 2 行 × 4 列的 Statistic 网格。

┌────────────┬────────────┬────────────┬────────────┐
│  可用率     │  平均延迟   │  P95延迟    │  检查总数   │
├────────────┼────────────┼────────────┼────────────┤
│  MTTR      │  最长故障   │  故障次数    │  连续正常   │
└────────────┴────────────┴────────────┴────────────┘

理由:原来的「总检查/正常/异常/可用率」4 指标信息冗余,正常/异常计数已在环形图中展示。重构后每个格子都是独立维度,信息密度大幅提升。

Decision 8TrendPoint 增加 min/max 延迟字段

选择:在 SQL 聚合层直接计算 MIN(duration_ms)MAX(duration_ms),零额外成本。

实现:趋势图使用 recharts <Area> 组件渲染 min-max 范围(半透明品牌色填充),叠加 avg 实线。

Decision 9Summary lastCheckTime 展示为相对时间

选择:在 Summary 区域底部展示 "最后更新: X秒前" 文本,前端每秒更新。

实现:使用 useState + setInterval 每秒计算相对时间差。超过 60 秒时文字变为警告色(--td-warning-color提示数据可能不新鲜。

Decision 10StatusDonut 数据来源改为 statsData

选择StatusDonut 的 up/down 改为使用 statsData.upChecks / statsData.downChecks,不再从 trendData 反推。

理由statsData 的 upChecks 是精确值(直接从 SQL COUNT 返回),与统计区的"检查总数"一致,消除了之前从百分比反推的精度损失。

影响computeTrendStats 工具函数不再有调用方,直接删除。

Decision 11MTTR 窗口边界截断处理

选择:如果时间窗口内第一条记录即为 matched=0故障跨越了 from 边界),该故障段不计入 MTTR 平均值,但计入 incidentCount。

理由:无法确定故障的真实开始时间,计入 MTTR 会低估实际恢复时间。incidentCount 计数是因为用户确实在窗口内经历了这次故障。

Decision 12getIncidents24h 作为独立方法

选择getIncidents24h() 是 ProbeStore 的独立方法(单条 SQLhandleSummary 路由中调用并附加到响应。

理由职责分离getSummary() 保持原有的目标状态快照逻辑incidents24h 是独立的时序分析查询。

Risks / Trade-offs

  • [P95 数据量] 30d 窗口下单目标可能有 ~43200 条记录需要排序 → 对于内存排序仍然可接受(<1MB但如果未来数据量增长可考虑近似算法
  • [异常事件计数的 LAG 查询] 窗口函数在大数据量下可能较慢 → 24h 窗口内数据量有限(所有目标 × 24h ÷ 间隔),可接受;如果性能不佳可改为应用层遍历
  • [前端连续状态上限 30] recentSamples 固定 30 条,连续状态超过 30 次时显示 "30+" → 对于运维场景足够,真正需要精确值时可查看 Drawer 详情
  • [趋势图去掉可用率线] 用户可能习惯看可用率曲线 → 异常标记点提供了等价信息且更直观,环形图仍展示可用率分布
  • [LAG 窗口边界误差] 使用 LAG 窗口函数检测状态翻转时,若故障跨越时间窗口 from 边界(窗口内第一条即为 matched=0会被计为一次新事件实际可能是窗口外已开始的故障延续 → 对于 24h 窗口内的事件计数,该误差可接受且难以避免(需要额外查询窗口外数据才能消除)