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

148 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 窗口函数检测前后状态变化:
```sql
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从已有的 `recentSamples`30 条)在前端计算连续状态次数。
**理由**
- `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/7d`timeFrom`/`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 的独立方法(单条 SQL`handleSummary` 路由中调用并附加到响应。
**理由**职责分离getSummary() 保持原有的目标状态快照逻辑incidents24h 是独立的时序分析查询。
## Risks / Trade-offs
- **[P95 数据量]** 30d 窗口下单目标可能有 ~43200 条记录需要排序 → 对于内存排序仍然可接受(<1MB但如果未来数据量增长可考虑近似算法
- **[异常事件计数的 LAG 查询]** 窗口函数在大数据量下可能较慢 → 24h 窗口内数据量有限(所有目标 × 24h ÷ 间隔),可接受;如果性能不佳可改为应用层遍历
- **[前端连续状态上限 30]** recentSamples 固定 30 条,连续状态超过 30 次时显示 "30+" → 对于运维场景足够,真正需要精确值时可查看 Drawer 详情
- **[趋势图去掉可用率线]** 用户可能习惯看可用率曲线 → 异常标记点提供了等价信息且更直观,环形图仍展示可用率分布
- **[LAG 窗口边界误差]** 使用 LAG 窗口函数检测状态翻转时,若故障跨越时间窗口 from 边界(窗口内第一条即为 matched=0会被计为一次新事件实际可能是窗口外已开始的故障延续 → 对于 24h 窗口内的事件计数,该误差可接受且难以避免(需要额外查询窗口外数据才能消除)