1
0
Files
DiAL/openspec/specs/probe-data-store/spec.md

220 lines
12 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.
## Purpose
定义基于 SQLite 的拨测数据持久化存储targets 同步含分组信息、check_results 追加写入、Dashboard 和 Metrics 数据查询支持、延迟百分位取数、时间范围和分页查询、索引与聚合查询。
## Requirements
### Requirement: SQLite 数据库初始化
系统 SHALL 使用 Bun 内置 `bun:sqlite` 模块在配置的数据目录下创建 SQLite 数据库文件,并以 WAL 模式运行。数据库 schema MUST 支持 typed checker target 和结构化检查结果targets 表 MUST 包含 `grp` 列存储分组信息、`active` 列标记活跃状态,且 targets 表的 `name``description` 列 MUST 允许 NULL。check_results 表的外键约束 SHALL 使用 `ON DELETE RESTRICT`
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表(含 active INTEGER NOT NULL DEFAULT 1 列)和 check_results 表(外键约束为 ON DELETE RESTRICTcheck_results 表包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idTEXT NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREAL、observationTEXT、failureTEXT不包含 status_detail 列,不包含 success 列
#### Scenario: targets name 列允许 NULL
- **WHEN** 系统首次创建 targets 表
- **THEN** targets.name 列 SHALL 允许存储 NULL
#### Scenario: 数据目录不存在
- **WHEN** 配置的数据目录路径不存在
- **THEN** 系统 SHALL 自动创建该目录
#### Scenario: 数据库已存在时启动
- **WHEN** 数据库文件已存在
- **THEN** 系统 SHALL 直接打开数据库,不重新建表
#### Scenario: 外键约束
- **THEN** 系统 SHALL 启用 `PRAGMA foreign_keys = ON`
#### Scenario: 级联删除改为限制删除
- **THEN** check_results 表的外键约束 SHALL 使用 `ON DELETE RESTRICT`,确保删除 target 时数据库层面阻止操作而非级联删除关联记录
### Requirement: targets 表同步
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示名称元信息、展示摘要、领域配置、调度配置、expect 配置、分组信息和目标说明。配置中不存在的 target SHALL 被标记为非活跃而非删除。
#### Scenario: 首次同步目标
- **WHEN** 数据库为空且 YAML 中定义了 N 个 typed target
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、description、type、target、config、interval_ms、timeout_ms、expect、grp 和 active=1其中 name 和 description 均可为 NULL
#### Scenario: 配置变更后重新同步
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
- **THEN** 系统 SHALL 根据 id 字段匹配新增的插入active=1、删除的设置 active=0不删除行、修改的更新含 name、description 和 grp 字段),已存在的目标 SHALL 设置 active=1
#### Scenario: 配置中移除目标
- **WHEN** YAML 配置中移除了某个 target该 target 在数据库中存在且 active=1
- **THEN** 系统 SHALL 将该 target 的 active 设置为 0保留该行及所有关联的 check_results
#### Scenario: 配置中恢复已移除目标
- **WHEN** YAML 配置中重新添加了之前移除的 target数据库中 active=0
- **THEN** 系统 SHALL 将该 target 的 active 设置为 1并更新其他字段历史 check_results 保留不变
#### Scenario: 未配置 name
- **WHEN** YAML target 未配置 `name`
- **THEN** targets 表 SHALL 将该目标的 name 存储为 NULL
#### Scenario: name 显式 null
- **WHEN** YAML target 配置 `name: null`
- **THEN** targets 表 SHALL 将该目标的 name 存储为 NULL
#### Scenario: 未配置 description
- **WHEN** YAML target 未配置 `description`
- **THEN** targets 表 SHALL 将该目标的 description 存储为 NULL
#### Scenario: description 显式 null
- **WHEN** YAML target 配置 `description: null`
- **THEN** targets 表 SHALL 将该目标的 description 存储为 NULL
### Requirement: check_results 表追加写入
系统 SHALL 将每次检查结果追加写入 check_results 表,不更新或删除已有记录。
#### Scenario: 写入检查结果
- **WHEN** 一次 checker 执行完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、observation、failure 的记录,其中 observation 使用 JSON.stringify 序列化为 TEXT
#### Scenario: 查询检查结果
- **WHEN** 系统查询 latest check 或历史 check_results
- **THEN** 存储层 SHALL 返回 observation 字段而非 status_detail 字段,供 API 序列化层反序列化并构造 detail
#### Scenario: 写入结构化失败信息
- **WHEN** checker 执行失败或 expect 不匹配
- **THEN** 系统 SHALL 将首个失败原因序列化写入 failure 字段
### Requirement: 时间范围查询索引
系统 SHALL 在 check_results 表上创建 (target_id, timestamp) 复合索引,加速按目标和时间范围的查询。
#### Scenario: 查询某目标的历史记录
- **WHEN** 查询指定 target_id 的最近 N 条记录
- **THEN** 系统 SHALL 使用索引快速定位,无需全表扫描
### Requirement: 目标列表按分组排序
系统 SHALL 保证 targets 查询结果按分组排序返回,且仅返回活跃目标。
#### Scenario: 分组排序查询
- **WHEN** 查询所有 targets
- **THEN** 结果 SHALL 仅返回 active=1 的目标,将 "default" 分组目标排在首位,其余分组按 YAML 配置中首次出现的顺序(即 id 自增顺序)排列
### Requirement: 聚合查询支持
数据存储 SHALL 支持按时间段获取指标计算所需数据,用于后端应用层计算可用率、平均耗时、延迟范围、趋势分桶和可靠性指标。
#### Scenario: 轻数据库计算边界
- **WHEN** 实现指标相关数据查询
- **THEN** 数据库 SHALL 主要负责存储、过滤、排序、分页、LIMIT 和标准 SQL 基础聚合,业务指标语义 SHALL 在后端应用层计算
#### Scenario: 可使用的基础 SQL 聚合
- **WHEN** 查询需要减少返回数据量
- **THEN** 系统 MAY 使用标准 SQL 的 COUNT、SUM(CASE)、AVG、MIN、MAX、GROUP BY 等基础能力
#### Scenario: 避免数据库承载业务语义
- **WHEN** 实现状态翻转、故障段、MTTR、最长故障、连续状态、百分位或趋势分桶
- **THEN** 系统 SHALL 在后端应用层实现这些规则,不依赖 SQLite 专有函数或复杂窗口函数承载业务语义
#### Scenario: UP/DOWN 判定
- **WHEN** 系统需要判定目标当前状态
- **THEN** 系统 SHALL 基于 latestCheck.matched 判定目标 UP 或 DOWNmatched=true 为 UPmatched=false 为 DOWN
### Requirement: Dashboard 数据查询支持
ProbeStore SHALL 提供 Dashboard 聚合响应所需的批量取数能力,且所有查询 SHALL 仅涉及活跃 target。
#### Scenario: 批量获取最新检查
- **WHEN** Dashboard API 需要计算当前 up/down 和 lastCheckTime
- **THEN** Store SHALL 支持批量获取每个活跃 target 的最新检查记录,避免 N+1 查询
#### Scenario: 批量获取窗口统计基础数据
- **WHEN** Dashboard API 需要计算各 target 在指定 window 内的 totalChecks、upChecks、downChecks 和 availability
- **THEN** Store SHALL 支持按 target_id 批量返回指定时间窗口内的基础计数数据,仅包含活跃 target
#### Scenario: 批量获取最近样本
- **WHEN** Dashboard API 需要展示 recentSamples 和计算 capped currentStreak
- **THEN** Store SHALL 支持批量获取每个活跃 target 最近 recentLimit 条检查记录,按 target_id 分组且每组按 timestamp 降序排列
#### Scenario: 获取 Dashboard 异常事件序列
- **WHEN** Dashboard API 需要计算 incidents
- **THEN** Store SHALL 支持获取指定时间窗口内所有活跃 target 的 `{ target_id, timestamp, matched }` 序列,按 target_id 和 timestamp 升序排列,供后端应用层计算状态翻转
### Requirement: 单目标指标取数支持
ProbeStore SHALL 提供单目标 metrics 响应所需的取数能力。仅活跃 target 的指标 SHALL 可查询。
#### Scenario: 获取目标检查点序列
- **WHEN** Metrics API 需要计算趋势分桶、故障段、MTTR、最长故障、故障次数和连续状态
- **THEN** Store SHALL 支持获取指定活跃 target 在 from 到 to 时间范围内的 `{ timestamp, matched, duration_ms }` 数组,按 timestamp 升序排列
#### Scenario: 目标不活跃
- **WHEN** 查询 inactive target 的指标
- **THEN** Store SHALL 返回 nullgetTargetById 不匹配 active=1 的条件)
#### Scenario: 无检查记录
- **WHEN** 时间窗口内无检查记录
- **THEN** Store SHALL 返回空数组
### Requirement: 目标延迟百分位取数
ProbeStore SHALL 提供 `getTargetDurations(targetId, from, to)` 方法,返回时间窗口内所有成功检查的 duration_ms 数组。
#### Scenario: 获取延迟数据
- **WHEN** 调用 `getTargetDurations(targetId, from, to)`
- **THEN** 系统 SHALL 返回该目标在时间范围内所有 matched=1 且 duration_ms 不为 null 的 duration_ms 值数组
#### Scenario: 延迟数据排序
- **WHEN** 获取延迟数据
- **THEN** 返回数组 SHALL 按 duration_ms 升序排列,供后端应用层计算 P95/P99
#### Scenario: 无成功检查
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
- **THEN** 系统 SHALL 返回空数组
### Requirement: 历史记录时间范围和分页查询
系统 SHALL 继续支持按时间范围筛选并分页查询历史记录。
#### Scenario: 按时间范围筛选历史记录
- **WHEN** 查询指定 target 在 from 到 to 时间范围内的历史记录
- **THEN** 系统 SHALL 返回该时间范围内的记录,按 timestamp 降序排列
#### Scenario: 分页查询历史记录
- **WHEN** 查询指定 page 和 pageSize 的历史记录
- **THEN** 系统 SHALL 返回对应页的数据和总记录数
### Requirement: 目标展示摘要持久化
数据存储 SHALL 为每个 target 持久化一个领域无关的展示摘要字段 `target`
#### Scenario: HTTP target 展示摘要
- **WHEN** 同步 HTTP target
- **THEN** targets.target SHALL 存储该 target 的 URL
#### Scenario: cmd target 展示摘要
- **WHEN** 同步 cmd target
- **THEN** targets.target SHALL 存储由 exec 和 args 组成的命令摘要
#### Scenario: HTTP target config 序列化
- **WHEN** 同步 HTTP target
- **THEN** targets.config SHALL 存储 JSON包含 url、method、headers、body、maxBodyBytes、ignoreSSL、maxRedirects
#### Scenario: cmd target config 序列化
- **WHEN** 同步 cmd target
- **THEN** targets.config SHALL 存储 JSON包含 exec、args、cwd、env、maxOutputBytes
### Requirement: 数据清理方法
ProbeStore SHALL 提供 `prune(retentionMs: number)` 方法,删除超过保留时长的历史检查结果并返回删除行数,同时清理已无关联检查结果的非活跃目标行。
#### Scenario: 清理过期数据
- **WHEN** 调用 `prune(604800000)`7 天毫秒数)
- **THEN** 系统 SHALL 删除 `check_results` 表中 `timestamp` 早于当前时间减去 604800000 毫秒的所有记录,并返回实际删除的行数
#### Scenario: 无过期数据
- **WHEN** 调用 `prune()` 但所有记录都在保留期内
- **THEN** 系统 SHALL 返回 0不删除任何记录
#### Scenario: 清理不影响保留期内数据
- **WHEN** 调用 `prune()` 且存在保留期内和保留期外的记录
- **THEN** 系统 SHALL 仅删除保留期外的记录,保留期内的记录 SHALL 不受影响
#### Scenario: 清理空壳非活跃目标
- **WHEN** prune 执行完毕后,存在 active=0 的 target 且该 target 在 check_results 表中无任何关联记录
- **THEN** 系统 SHALL 删除该空壳 target 行
#### Scenario: 非活跃目标仍有历史数据时不清理
- **WHEN** 存在 active=0 的 target 但该 target 在 check_results 表中仍有关联记录
- **THEN** 系统 SHALL NOT 删除该 target 行
#### Scenario: 活跃目标永不清理
- **WHEN** 存在 active=1 的 target 且该 target 在 check_results 表中无关联记录
- **THEN** 系统 SHALL NOT 删除该 target 行