From 393e8da5fd24448ea49c9f934608e64964fb45bf Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Mon, 18 May 2026 00:33:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20ICMP/Ping=20checke?= =?UTF-8?q?r=20=E8=AE=BE=E8=AE=A1=E6=8F=90=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定义 ping target 配置:host、count、packetSize - 定义 ping expect 断言:alive、maxPacketLoss、maxAvgLatencyMs、maxMaxLatencyMs - 设计跨平台 ping 输出解析器(Linux/macOS/Windows 含多语言支持) - 双重超时保障:ping 命令自身超时 + AbortSignal 兜底 - 扩展 checker-runner-abstraction spec 支持 ping checker 子进程控制 - 更新 probe-config spec 支持 ping type 配置 --- .../changes/add-icmp-checker/.openspec.yaml | 2 + openspec/changes/add-icmp-checker/design.md | 190 +++++++++++++++++ openspec/changes/add-icmp-checker/proposal.md | 28 +++ .../specs/checker-runner-abstraction/spec.md | 16 ++ .../specs/icmp-checker/spec.md | 192 ++++++++++++++++++ .../specs/probe-config/spec.md | 38 ++++ openspec/changes/add-icmp-checker/tasks.md | 44 ++++ 7 files changed, 510 insertions(+) create mode 100644 openspec/changes/add-icmp-checker/.openspec.yaml create mode 100644 openspec/changes/add-icmp-checker/design.md create mode 100644 openspec/changes/add-icmp-checker/proposal.md create mode 100644 openspec/changes/add-icmp-checker/specs/checker-runner-abstraction/spec.md create mode 100644 openspec/changes/add-icmp-checker/specs/icmp-checker/spec.md create mode 100644 openspec/changes/add-icmp-checker/specs/probe-config/spec.md create mode 100644 openspec/changes/add-icmp-checker/tasks.md diff --git a/openspec/changes/add-icmp-checker/.openspec.yaml b/openspec/changes/add-icmp-checker/.openspec.yaml new file mode 100644 index 0000000..66da1ae --- /dev/null +++ b/openspec/changes/add-icmp-checker/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-17 diff --git a/openspec/changes/add-icmp-checker/design.md b/openspec/changes/add-icmp-checker/design.md new file mode 100644 index 0000000..600f097 --- /dev/null +++ b/openspec/changes/add-icmp-checker/design.md @@ -0,0 +1,190 @@ +## Context + +项目当前有 HTTP、CMD、DB、TCP 四种 checker,均遵循 `CheckerDefinition` 接口规范。TCP checker 是最近实现的网络层 checker,其模式(Bun.spawn / 原生 socket + AbortSignal + 断言链)是 ICMP checker 的直接参考。 + +ICMP Ping 是最基础的网络探测手段,但 Node.js/Bun 生态中没有合适的纯 JS ICMP 实现(均依赖 `raw-socket` 原生 addon,Bun N-API 兼容性不确定且需要 root 权限)。现有的命令封装库(`ping`、`pingman`)虽然提供了跨平台解析,但它们封装了 `child_process.spawn` 且不暴露子进程引用,无法配合我们的 `ctx.signal` 超时控制机制。 + +经过调研和讨论,确定自行实现:用 `Bun.spawn` 调用系统 `ping` 命令 + 自行编写跨平台输出解析器。 + +## Goals / Non-Goals + +**Goals:** +- 实现 `type: ping` checker,支持主机存活检测、延迟监控、丢包率检查 +- 跨平台支持 Linux、macOS、Windows(含中文 Windows) +- 完全复用现有 checker 架构(registry、schema、expect、failure) +- 零外部依赖 + +**Non-Goals:** +- 不实现 traceroute 功能 +- 不实现 IPv6 专项支持(系统 ping 命令会自动处理 IPv6 地址) +- 不实现原始 ICMP socket(权限要求过高) +- 不提供 per-packet 逐包结果(只提供 summary 统计) + +## Decisions + +### Decision 1: 调用系统 `ping` 命令而非原始 ICMP socket + +**选择**: 通过 `Bun.spawn` 调用系统 `ping` 可执行文件 + +**替代方案**: +- 原始 ICMP socket(`raw-socket` addon):需要 root/CAP_NET_RAW 权限,Bun N-API 兼容性不确定 +- 三方库 `pingman`/`ping`:封装了 spawn 但不暴露子进程引用,无法配合 AbortSignal 超时控制 + +**理由**: 系统 `ping` 命令无需特殊权限(大多数系统),`Bun.spawn` 给我们完全的子进程生命周期控制,与现有 cmd checker 模式一致。 + +### Decision 2: 自行实现跨平台解析器 + +**选择**: 在 `parse.ts` 中实现 `parsePingOutput(stdout, platform)` 函数,用正则匹配 summary 统计行 + +**替代方案**: +- 引入 `pingman` 作为依赖使用其 parser:模块不是公开 API,deep import 不稳定 +- 引入 `ping`(danielzzz)的 parser:同上,且返回值类型全是 string + +**理由**: 我们只需要 summary 行的统计数据(transmitted/received/loss/min/avg/max),不需要逐包 body 解析。三套正则约 40-50 行代码,完全可控且零依赖。 + +### Decision 3: 跨平台命令构建策略 + +``` +平台判断: process.platform (win32 vs 其他) + +Linux: + ping -c -s -W + +macOS: + ping -c -s -W + +Windows: + ping -n -l -w +``` + +**超时参数传递策略:双重保障** + +传递平台对应的超时参数(`-W`/`-w`),同时保留外层 `ctx.signal` + `proc.kill()` 作为兜底: +- **ping 命令自身超时**:确保每个 ICMP 包在指定时间内超时返回,避免 ping 进程因网络异常而无限等待 +- **外层 AbortSignal**:作为最终兜底,防止 ping 命令因任何原因卡死不退出 + +各平台超时参数单位差异: +- Linux `-W`:单位为**秒**(整数),需将 timeoutMs 转换为秒(向上取整) +- macOS `-W`:单位为**毫秒**(整数) +- Windows `-w`:单位为**毫秒**(整数) + +超时值计算:使用外层 `timeoutMs` 作为 ping 命令的超时参数值。这样 ping 命令自身会在 timeout 内完成,外层 signal 作为额外保障。 + +### Decision 4: Windows 多语言输出解析策略 + +Windows `ping` 输出语言跟随系统 locale(中文系统输出中文、英文系统输出英文、日文系统输出日文等)。 + +**选择**: 基于数字模式和行结构匹配,不依赖关键词 + +具体策略: +- **丢包行**: 匹配 `(\d+).*?(\d+).*?(\d+).*?(\d+(?:\.\d+)?%)` 模式——提取"已发送"、"已接收"、"丢失"和百分比数字,不依赖中英文关键词 +- **延迟行**: 匹配 `(\d+)ms.*?(\d+)ms.*?(\d+)ms` 模式——提取 min/max/avg 三个数字(Windows 输出顺序固定为 Minimum/Maximum/Average) + +**替代方案**: 枚举所有语言的关键词——维护成本高,且无法覆盖所有 locale + +### Decision 5: 解析结果数据结构 + +```typescript +interface PingStats { + alive: boolean; + transmitted: number; + received: number; + packetLoss: number; // 0-100 + minLatencyMs: number | null; + avgLatencyMs: number | null; + maxLatencyMs: number | null; +} +``` + +`latencyMs` 字段为 `null` 表示主机不可达时无延迟数据。 + +### Decision 6: 断言执行顺序(短路) + +``` +alive → packetLoss → avgLatency → maxLatency → duration +``` + +理由: +1. `alive` 是最基础的判断,不可达时后续断言无意义 +2. `packetLoss` 比延迟更严重(丢包意味着部分请求完全失败) +3. `avgLatency` 和 `maxLatency` 是延迟质量指标 +4. `duration` 是整体执行时间兜底 + +### Decision 7: ping 命令不存在时的错误处理 + +当系统未安装 `ping` 命令时(常见于精简容器镜像如 Alpine),`Bun.spawn` 会抛出 ENOENT 错误。 + +处理方式:在 spawn 阶段 try/catch,返回结构化错误: +```typescript +failure: errorFailure("ping", "spawn", `ping 命令不可用: ${error.message}`) +statusDetail: "ping command not found" +``` + +文档中注明系统依赖:容器环境需确保 `ping` 命令可用(如 Alpine 需安装 `iputils-ping`)。 + +### Decision 8: configKey 和 type 命名 + +**选择**: `type: "ping"`, `configKey: "ping"` + +**替代方案**: `type: "icmp"` — 但 `ping` 更贴近用户认知,且配置中 `ping.host` 比 `icmp.host` 更直观。 + +### Decision 9: 超时控制与子进程生命周期 + +与 cmd checker 相同的模式: +```typescript +ctx.signal.addEventListener("abort", () => { + try { proc.kill(); } catch { /* best-effort */ } +}, { once: true }); +``` + +当 signal abort 时 kill 子进程,然后在结果中记录超时错误。这需要修改 `checker-runner-abstraction` spec 中"仅 cmd checker 可在 signal abort 时 proc.kill()"的约束。 + +### Decision 10: Linux/macOS 解析正则 + +``` +丢包统计行: +Linux: "3 packets transmitted, 3 received, 0% packet loss, time 2003ms" +macOS: "3 packets transmitted, 3 packets received, 0.0% packet loss" +正则: /(\d+)\s+packets?\s+transmitted.*?(\d+)\s+(?:packets?\s+)?received.*?(\d+(?:\.\d+)?)%\s+packet\s+loss/ + +延迟统计行: +Linux: "rtt min/avg/max/mdev = 1.234/2.345/3.456/0.567 ms" +macOS: "round-trip min/avg/max/stddev = 1.234/2.345/3.456/0.567 ms" +正则: /(?:rtt|round-trip).*?=\s*([\d.]+)\/([\d.]+)\/([\d.]+)/ +``` + +### Decision 11: Windows 解析正则 + +``` +丢包统计行(数字模式,语言无关): +英文: "Packets: Sent = 3, Received = 3, Lost = 0 (0% loss)" +中文: "数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失)" +正则: /=\s*(\d+).*?=\s*(\d+).*?=\s*(\d+).*?(\d+(?:\.\d+)?)%/ + +延迟统计行(数字模式,语言无关): +英文: "Minimum = 1ms, Maximum = 3ms, Average = 2ms" +中文: "最短 = 1ms,最长 = 3ms,平均 = 2ms" +正则: /=\s*(\d+)ms.*?=\s*(\d+)ms.*?=\s*(\d+)ms/ +``` + +Windows 延迟顺序固定为 min/max/avg(注意与 Linux/macOS 的 min/avg/max 不同)。 + +## Risks / Trade-offs + +### [Risk] 系统未安装 ping 命令 → 清晰的错误提示 + 文档说明 +容器环境(Alpine、scratch)可能不包含 ping。通过 spawn 阶段 catch ENOENT 给出明确提示,并在 README 中注明依赖。 + +### [Risk] 未知 locale 的 Windows 输出无法解析 → 降级为 alive=false + 原始输出 +如果正则无法匹配任何统计行,将 alive 判定为 `received > 0`(通过检查 exit code:Windows ping 在全部丢包时 exit code 为 1),延迟字段为 null。statusDetail 展示原始输出前 80 字符供用户排查。 + +### [Risk] ping 命令被防火墙/网络策略阻断 → 用户可预期的行为 +ICMP 在某些网络环境中被阻断。这不是 checker 的 bug,而是网络配置问题。checker 会正确报告 `alive=false` 和 100% packet loss。 + +### [Trade-off] 双重超时保障 +传递 `-W`/`-w` 超时参数给 ping 命令,同时保留外层 AbortSignal + proc.kill() 兜底。 +优势:ping 命令自身会在超时后正常退出,不依赖外部 kill;即使 ping 命令因异常卡死,外层 signal 仍能强制终止。 +劣势:需要处理三平台超时参数的单位差异(Linux 秒 vs macOS/Windows 毫秒),增加少量命令构建复杂度。 + +### [Trade-off] 不引入三方库 +优势:零依赖、完全可控、与项目规范一致。 +劣势:需要自行维护跨平台解析正则。但 ping 输出格式极其稳定(几十年未变),维护成本极低。 diff --git a/openspec/changes/add-icmp-checker/proposal.md b/openspec/changes/add-icmp-checker/proposal.md new file mode 100644 index 0000000..858e73a --- /dev/null +++ b/openspec/changes/add-icmp-checker/proposal.md @@ -0,0 +1,28 @@ +## Why + +项目当前支持 HTTP、CMD、DB、TCP 四种 checker,缺少最基础的网络层存活检测能力。ICMP Ping 是运维监控的基石——主机存活、网络延迟、丢包率是判断网络健康的第一手指标,也是区分"网络层故障"与"应用层故障"的关键手段。 + +## What Changes + +- 新增 `type: ping` checker,通过调用系统 `ping` 命令实现 ICMP 探测 +- 支持配置 host、count(包数量)、packetSize(包大小,用于 MTU 测试) +- 支持断言:alive(可达性)、maxAvgLatencyMs(平均延迟)、maxMaxLatencyMs(最大延迟/抖动)、maxPacketLoss(丢包率)、maxDurationMs(整体耗时) +- 自行实现跨平台(Linux/macOS/Windows)ping 输出解析器,不引入三方库 +- 文档注明 ICMP checker 依赖系统 `ping` 命令存在(容器环境需确保已安装,如 Alpine 需 `iputils-ping`) + +## Capabilities + +### New Capabilities +- `icmp-checker`: 定义 ICMP/Ping checker 的配置格式、命令执行、跨平台输出解析、expect 校验和状态摘要 + +### Modified Capabilities +- `checker-runner-abstraction`: 超时控制 requirement 中"仅 cmd checker 可在 signal abort 时 proc.kill()"需扩展为"cmd checker 和 ping checker",因为 ping checker 同样 spawn 子进程 +- `probe-config`: 配置格式需扩展支持 `type: ping` 的 target 配置、`ping` 领域分组和对应的 expect 字段 + +## Impact + +- 后端代码:新增 `src/server/checker/runner/icmp/` 模块,注册到 CheckerRegistry +- 配置 schema:`probe-config.schema.json` 需更新,新增 ping target 和 expect 的 schema 片段 +- 测试:新增 `tests/server/checker/runner/icmp/` 测试套件 +- 文档:README.md 和 DEVELOPMENT.md 需更新,注明 ping 命令的系统依赖 +- 无新增三方依赖 diff --git a/openspec/changes/add-icmp-checker/specs/checker-runner-abstraction/spec.md b/openspec/changes/add-icmp-checker/specs/checker-runner-abstraction/spec.md new file mode 100644 index 0000000..c644ddb --- /dev/null +++ b/openspec/changes/add-icmp-checker/specs/checker-runner-abstraction/spec.md @@ -0,0 +1,16 @@ +## MODIFIED Requirements + +### Requirement: 超时控制由引擎注入 signal +Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController` 或 `setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。 + +#### Scenario: HTTP checker 使用 signal +- **WHEN** HttpChecker 执行 HTTP 请求 +- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()` 的 `signal` 选项,不自行创建 `AbortController` + +#### Scenario: Cmd checker 响应 signal +- **WHEN** CommandChecker 执行命令且 signal 被 abort +- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误 + +#### Scenario: Ping checker 响应 signal +- **WHEN** IcmpChecker 执行 ping 命令且 signal 被 abort +- **THEN** SHALL 调用 `proc.kill()` 终止 ping 子进程,并在 CheckResult 中记录超时错误 diff --git a/openspec/changes/add-icmp-checker/specs/icmp-checker/spec.md b/openspec/changes/add-icmp-checker/specs/icmp-checker/spec.md new file mode 100644 index 0000000..0ac6298 --- /dev/null +++ b/openspec/changes/add-icmp-checker/specs/icmp-checker/spec.md @@ -0,0 +1,192 @@ +## Purpose + +定义 ICMP/Ping checker 的配置格式、命令执行、跨平台输出解析、expect 校验、失败结构和状态摘要。 + +## Requirements + +### Requirement: ping target 配置 +系统 SHALL 支持 `type: ping` 的 target 配置,通过 `ping.host` 描述目标主机地址,并通过可选字段控制探测行为。 + +#### Scenario: 解析最简 ping target +- **WHEN** YAML 中 target 配置 `type: ping` 和 `ping.host: "10.0.0.1"` +- **THEN** 系统 SHALL 将其解析为 ping checker,并填充 `count=3`、`packetSize=56`、interval、timeout、group 和 expect 配置 + +#### Scenario: ping target 缺少 host +- **WHEN** YAML 中 target 配置 `type: ping` 但缺少 `ping.host` +- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 ping.host 字段 + +#### Scenario: ping host 类型非法 +- **WHEN** YAML 中 ping target 的 `ping.host` 不是非空字符串 +- **THEN** 系统 SHALL 以配置错误退出,提示 ping.host 必须为非空字符串 + +#### Scenario: ping count 配置 +- **WHEN** YAML 中 ping target 配置 `ping.count: 5` +- **THEN** 系统 SHALL 使用 5 作为 ICMP 包发送数量 + +#### Scenario: ping count 非法 +- **WHEN** YAML 中 ping target 的 `ping.count` 不是 1 到 100 之间的正整数 +- **THEN** 系统 SHALL 以配置错误退出,提示 ping.count 必须为 1-100 的正整数 + +#### Scenario: ping packetSize 配置 +- **WHEN** YAML 中 ping target 配置 `ping.packetSize: 1472` +- **THEN** 系统 SHALL 使用 1472 作为 ICMP 包大小(bytes) + +#### Scenario: ping packetSize 非法 +- **WHEN** YAML 中 ping target 的 `ping.packetSize` 不是 1 到 65500 之间的正整数 +- **THEN** 系统 SHALL 以配置错误退出,提示 ping.packetSize 必须为 1-65500 的正整数 + +#### Scenario: ping 分组未知字段失败 +- **WHEN** YAML 中 ping target 的 `ping` 分组包含 `timeout: 5` 等未知字段 +- **THEN** 系统 SHALL 以配置错误退出,提示 ping 分组包含未知字段 + +#### Scenario: ping 序列化展示摘要 +- **WHEN** 系统同步 ping target 到 targets 表 +- **THEN** `target` 展示摘要 SHALL 为 `ping `,`config` JSON SHALL 包含 resolved 后的 host、count 和 packetSize + +### Requirement: ping checker 执行 +系统 SHALL 通过调用系统 `ping` 命令执行 ICMP 探测,记录完整执行耗时,并在命令不可用、超时或解析失败时产生结构化失败信息。 + +#### Scenario: ping 命令构建(Linux) +- **WHEN** 系统平台为 linux,ping target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000 +- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10 10.0.0.1`(-W 单位为秒,向上取整) + +#### Scenario: ping 命令构建(macOS) +- **WHEN** 系统平台为 darwin,ping target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000 +- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10000 10.0.0.1`(-W 单位为毫秒) + +#### Scenario: ping 命令构建(Windows) +- **WHEN** 系统平台为 win32,ping target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000 +- **THEN** 系统 SHALL 执行 `ping -n 3 -l 56 -w 10000 10.0.0.1`(-w 单位为毫秒) + +#### Scenario: ping 命令不存在 +- **WHEN** 系统未安装 `ping` 命令(spawn 抛出 ENOENT) +- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `ping`,path 为 `spawn`,message 包含 "ping 命令不可用" 和原始错误信息 + +#### Scenario: ping 执行超时 +- **WHEN** 引擎注入的 `ctx.signal` 在 ping 命令执行过程中 abort +- **THEN** 系统 SHALL 调用 `proc.kill()` 终止子进程,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `ping`,message 包含超时信息 + +#### Scenario: ping 目标可达 +- **WHEN** ping target 指向可达主机,且 ping 命令正常返回 +- **THEN** 系统 SHALL 解析 stdout 获取统计数据,并按断言链执行 expect 校验 + +#### Scenario: ping 目标不可达 +- **WHEN** ping target 指向不可达主机,且 ping 命令返回 100% packet loss +- **THEN** 系统 SHALL 解析 stdout 获取统计数据,`alive` 为 false,延迟字段为 null + +#### Scenario: duration 覆盖完整执行 +- **WHEN** ping 命令执行完成 +- **THEN** 结果中的 `durationMs` SHALL 覆盖从 spawn 到进程退出的完整耗时 + +### Requirement: 跨平台 ping 输出解析 +系统 SHALL 实现跨平台 ping 输出解析器,支持 Linux、macOS 和 Windows(含多语言 locale),从 stdout 中提取 transmitted、received、packetLoss、minLatencyMs、avgLatencyMs、maxLatencyMs。 + +#### Scenario: 解析 Linux ping 输出 +- **WHEN** 平台为 linux,stdout 包含 "3 packets transmitted, 3 received, 0% packet loss" 和 "rtt min/avg/max/mdev = 1.234/2.345/3.456/0.567 ms" +- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1.234, avgLatencyMs=2.345, maxLatencyMs=3.456 + +#### Scenario: 解析 macOS ping 输出 +- **WHEN** 平台为 darwin,stdout 包含 "3 packets transmitted, 3 packets received, 0.0% packet loss" 和 "round-trip min/avg/max/stddev = 1.234/2.345/3.456/0.567 ms" +- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1.234, avgLatencyMs=2.345, maxLatencyMs=3.456 + +#### Scenario: 解析 Windows 英文 ping 输出 +- **WHEN** 平台为 win32,stdout 包含 "Packets: Sent = 3, Received = 3, Lost = 0 (0% loss)" 和 "Minimum = 1ms, Maximum = 3ms, Average = 2ms" +- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1, avgLatencyMs=2, maxLatencyMs=3 + +#### Scenario: 解析 Windows 中文 ping 输出 +- **WHEN** 平台为 win32,stdout 包含 "数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失)" 和 "最短 = 1ms,最长 = 3ms,平均 = 2ms" +- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1, avgLatencyMs=2, maxLatencyMs=3 + +#### Scenario: 解析全部丢包(无延迟行) +- **WHEN** stdout 包含丢包统计行但无延迟统计行(100% packet loss) +- **THEN** 系统 SHALL 解析为 alive=false,延迟字段(min/avg/max)均为 null + +#### Scenario: 输出无法解析 +- **WHEN** stdout 不匹配任何已知的统计行格式 +- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `ping`,path 为 `parse`,message 包含 "无法解析 ping 输出" + +### Requirement: ping expect 校验 +系统 SHALL 支持 ping 专属 expect,包括 `alive`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs` 和 `maxDurationMs`,并按 alive、packetLoss、avgLatency、maxLatency、duration 的阶段顺序快速失败。 + +#### Scenario: 默认 alive 成功语义 +- **WHEN** ping target 未显式配置 `expect.alive` +- **THEN** 系统 SHALL 使用默认 `expect.alive: true` 进行校验 + +#### Scenario: alive 校验通过 +- **WHEN** ping target 配置 `expect.alive: true`,且目标主机可达 +- **THEN** 系统 SHALL 判定 alive 阶段通过 + +#### Scenario: alive 校验失败 +- **WHEN** ping target 配置 `expect.alive: true`,且目标主机不可达 +- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `alive` + +#### Scenario: 反向 alive 断言 +- **WHEN** ping target 配置 `expect.alive: false`,且目标主机不可达 +- **THEN** 系统 SHALL 判定 alive 阶段通过(`matched=true`) + +#### Scenario: 反向 alive 断言失败 +- **WHEN** ping target 配置 `expect.alive: false`,但目标主机可达 +- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `alive` + +#### Scenario: maxPacketLoss 校验通过 +- **WHEN** ping target 配置 `expect.maxPacketLoss: 10`,且实际丢包率为 0% +- **THEN** 系统 SHALL 判定 packetLoss 阶段通过 + +#### Scenario: maxPacketLoss 校验失败 +- **WHEN** ping target 配置 `expect.maxPacketLoss: 10`,且实际丢包率为 33% +- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `packetLoss` + +#### Scenario: maxAvgLatencyMs 校验通过 +- **WHEN** ping target 配置 `expect.maxAvgLatencyMs: 200`,且实际平均延迟为 12ms +- **THEN** 系统 SHALL 判定 avgLatency 阶段通过 + +#### Scenario: maxAvgLatencyMs 校验失败 +- **WHEN** ping target 配置 `expect.maxAvgLatencyMs: 100`,且实际平均延迟为 156ms +- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `avgLatency` + +#### Scenario: maxMaxLatencyMs 校验通过 +- **WHEN** ping target 配置 `expect.maxMaxLatencyMs: 500`,且实际最大延迟为 340ms +- **THEN** 系统 SHALL 判定 maxLatency 阶段通过 + +#### Scenario: maxMaxLatencyMs 校验失败 +- **WHEN** ping target 配置 `expect.maxMaxLatencyMs: 200`,且实际最大延迟为 340ms +- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `maxLatency` + +#### Scenario: maxDurationMs 校验 +- **WHEN** ping target 配置 `expect.maxDurationMs: 5000`,且完整执行耗时超过 5000ms +- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration` + +#### Scenario: alive=false 时跳过延迟断言 +- **WHEN** ping target 配置 `expect.alive: true` 和 `expect.maxAvgLatencyMs: 100`,且目标不可达 +- **THEN** 系统 SHALL 在 alive 阶段即返回失败,不执行后续延迟断言 + +#### Scenario: ping expect 未知字段失败 +- **WHEN** YAML 中 ping target 的 expect 包含 `status: [200]` 或其他非 ping expect 字段 +- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段 + +#### Scenario: maxPacketLoss 类型非法 +- **WHEN** YAML 中 ping target 的 `expect.maxPacketLoss` 不是 0 到 100 之间的数字 +- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxPacketLoss 必须为 0-100 的数字 + +#### Scenario: maxAvgLatencyMs 类型非法 +- **WHEN** YAML 中 ping target 的 `expect.maxAvgLatencyMs` 不是非负有限数字 +- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxAvgLatencyMs 格式错误 + +#### Scenario: maxMaxLatencyMs 类型非法 +- **WHEN** YAML 中 ping target 的 `expect.maxMaxLatencyMs` 不是非负有限数字 +- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxMaxLatencyMs 格式错误 + +### Requirement: ping statusDetail 摘要 +系统 SHALL 在 ping 执行成功后生成结构化 statusDetail 摘要,展示关键指标。 + +#### Scenario: 目标可达无丢包 +- **WHEN** ping 结果为 alive=true, avg=12ms, packetLoss=0%, transmitted=3, received=3 +- **THEN** statusDetail SHALL 为 `alive, avg 12ms, loss 0% (3/3)` + +#### Scenario: 目标可达有丢包 +- **WHEN** ping 结果为 alive=true, avg=156ms, max=340ms, packetLoss=33%, transmitted=3, received=2 +- **THEN** statusDetail SHALL 包含 avg、max 和 loss 信息 + +#### Scenario: 目标不可达 +- **WHEN** ping 结果为 alive=false, transmitted=3, received=0 +- **THEN** statusDetail SHALL 为 `unreachable (0/3 received)` diff --git a/openspec/changes/add-icmp-checker/specs/probe-config/spec.md b/openspec/changes/add-icmp-checker/specs/probe-config/spec.md new file mode 100644 index 0000000..90c84a6 --- /dev/null +++ b/openspec/changes/add-icmp-checker/specs/probe-config/spec.md @@ -0,0 +1,38 @@ +## MODIFIED Requirements + +### Requirement: YAML 配置文件格式 +系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、可选的 variables 段、checker 默认值和 typed target 列表(含可选 group 字段)。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,ping 领域字段 MUST 放在 `ping` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Ping target 的 `ping` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。 + +`defaults.http` 分组 SHALL 仅支持 `headers`(可选)和 `maxBodyBytes`(可选)字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。 + +#### Scenario: 最简 ping 配置文件解析 +- **WHEN** 系统读取只包含一个 `type: ping` target(含 `id` 和 `ping.host`)的 YAML 配置文件 +- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", ping.count=3, ping.packetSize=56),并保留 name=null、description=null + +### Requirement: 配置校验 +系统 SHALL 在启动时对 YAML 配置进行完整校验,校验失败时以非零状态退出并输出清晰的错误信息。系统 SHALL 使用 TypeBox 定义配置契约和 raw config TypeScript 类型,由 Ajv 校验 TypeBox 生成的 JSON Schema,再执行启动期语义 validator。配置加载流程 SHALL 明确区分 `RawProbeConfig`、`ValidatedProbeConfig`、`ResolvedConfig` 三段生命周期,并在 YAML 解析之后、AJV 校验之前执行变量替换阶段。JSON Schema 契约 SHALL 覆盖业务无关的结构规则,包括字段类型、必填字段、枚举、数组与对象形状、数值范围和未知字段。语义 validator SHALL 覆盖契约不适合表达的业务规则,包括 target id 唯一性、id 命名规则校验、checker type 注册状态、时长和大小解析、HTTP URL、正则可编译、JSONPath 子集和 XPath 可编译。 + +契约校验和语义 validator SHALL 统一产出 `ConfigValidationIssue`,最终由配置加载流程统一渲染为中文错误信息。 + +系统 SHALL 导出完整 `probe-config.schema.json`,该文件 SHALL 与运行期 TypeBox fragments 生成的 JSON Schema 保持一致,用于用户配置引用和编辑器提示。 + +除 `headers`、`env`、`variables` 等明确声明为动态键值表的对象外,配置中的未知字段 SHALL 导致启动期配置错误。系统 MUST NOT 静默忽略未知字段。 + +#### Scenario: ping target 缺少 host +- **WHEN** YAML 中某个 target 配置 `type: ping` 但缺少 `ping.host` +- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 ping.host 字段 + +#### Scenario: ping expect 未知字段 +- **WHEN** YAML 中 ping target 的 expect 包含非 ping expect 字段 +- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段 + +### Requirement: expect 配置增强 +系统 SHALL 支持 typed target 的领域专用 expect 配置,包括 HTTP 的 `status`(支持精确数字和范围模式)、`headers`、`body`,cmd 的 `exitCode`、`stdout`、`stderr`,tcp 的 `connected`、`banner`,以及 ping 的 `alive`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs`。内容类 expect MUST 使用数组表达配置顺序。 + +#### Scenario: 解析 ping expect 配置 +- **WHEN** YAML 配置文件中 ping target 的 expect 包含 alive、maxPacketLoss、maxAvgLatencyMs、maxMaxLatencyMs 和 maxDurationMs +- **THEN** 系统 SHALL 正确解析并存储为 ping target 的 expect 字段 + +#### Scenario: 不配置 ping expect +- **WHEN** ping target 未配置任何 expect 规则 +- **THEN** 系统 SHALL 正常处理,expect 字段为 undefined,执行时使用默认 alive=true 语义 diff --git a/openspec/changes/add-icmp-checker/tasks.md b/openspec/changes/add-icmp-checker/tasks.md new file mode 100644 index 0000000..156e9a8 --- /dev/null +++ b/openspec/changes/add-icmp-checker/tasks.md @@ -0,0 +1,44 @@ +## 1. 类型与 Schema 定义 + +- [ ] 1.1 创建 `src/server/checker/runner/icmp/types.ts`,定义 PingTargetConfig、PingExpectConfig、ResolvedPingConfig、ResolvedPingTarget、PingStats 接口 +- [ ] 1.2 创建 `src/server/checker/runner/icmp/schema.ts`,定义 TypeBox schema(config、expect),导出 icmpCheckerSchemas + +## 2. 跨平台解析器 + +- [ ] 2.1 创建 `src/server/checker/runner/icmp/parse.ts`,实现 parsePingOutput(stdout, platform) 函数,支持 Linux/macOS/Windows 三平台解析 +- [ ] 2.2 创建 `tests/server/checker/runner/icmp/parse.test.ts`,覆盖 Linux、macOS、Windows 英文、Windows 中文、全部丢包、无法解析等场景 + +## 3. 断言函数 + +- [ ] 3.1 创建 `src/server/checker/runner/icmp/expect.ts`,实现 checkAlive、checkPacketLoss、checkAvgLatency、checkMaxLatency 函数 +- [ ] 3.2 创建 `tests/server/checker/runner/icmp/expect.test.ts`,覆盖各断言函数的通过和失败场景 + +## 4. 配置校验 + +- [ ] 4.1 创建 `src/server/checker/runner/icmp/validate.ts`,实现 validatePingConfig 语义校验 +- [ ] 4.2 创建 `tests/server/checker/runner/icmp/validate.test.ts`,覆盖 host 缺失、count/packetSize 非法、未知字段等场景 + +## 5. Checker 主体实现 + +- [ ] 5.1 创建 `src/server/checker/runner/icmp/execute.ts`,实现 IcmpChecker class(execute、resolve、serialize、validate) +- [ ] 5.2 创建 `src/server/checker/runner/icmp/index.ts`,导出 IcmpChecker +- [ ] 5.3 在 `src/server/checker/runner/index.ts` 中 import IcmpChecker 并将 `new IcmpChecker()` 添加到 checkers 数组 + +## 6. 集成测试 + +- [ ] 6.1 创建 `tests/server/checker/runner/icmp/execute.test.ts`,测试 IcmpChecker 的 execute 方法(mock Bun.spawn) +- [ ] 6.2 更新 `tests/server/checker/runner/registry.test.ts`,验证 ping type 已注册 +- [ ] 6.3 更新 `tests/server/checker/config-loader.test.ts`,添加 ping target 配置解析和校验测试 + +## 7. Schema 导出与文档 + +- [ ] 7.1 更新 `probe-config.schema.json`,包含 ping target 和 expect 的 schema 片段 +- [ ] 7.2 更新 `probes.example.yaml`,添加 ping checker 配置示例 +- [ ] 7.3 更新 README.md,添加 ping checker 说明和系统依赖(ping 命令)注明 +- [ ] 7.4 更新 DEVELOPMENT.md(如有必要) + +## 8. 质量保障 + +- [ ] 8.1 执行完整测试套件(bun test),确保所有测试通过 +- [ ] 8.2 执行代码检查(lint)和格式检查(format),确保无错误 +- [ ] 8.3 验证 probe-config.schema.json 与运行期 schema 一致(运行 schema 生成脚本)