1
0
Files
DiAL/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/design.md

12 KiB
Raw Blame History

Context

当前实现的配置、执行、存储、API 和 Dashboard 都以 HTTP 请求为中心:target.url 是必填字段,执行器直接 fetch(url),结果存储包含 status_codelatency_ms,前端展示 URL、method 和 HTTP 状态码。这种模型无法承载本地命令等非 HTTP checker也让 expect 只能表达 HTTP response 的 status/header/body。

项目尚未上线,不需要兼容旧 YAML、旧数据库 schema 或旧 API 契约,因此本次设计选择直接建立 typed target 与领域专用 expect而不是添加兼容分支。目标是让 HTTP 变成 runner 的一种实现,同时新增 command runner并为未来其他 checker 类型保留清晰扩展点。

YAML target
  │
  ▼
ResolvedTarget(type)
  │
  ▼
ProbeEngine + concurrency limit
  │
  ├─ http runner
  │    └─ HTTP expect pipeline
  │
  └─ command runner
       └─ command expect pipeline
  │
  ▼
CheckResult(success, matched, durationMs, statusDetail, failure)
  │
  ▼
SQLite + API + Dashboard

Goals / Non-Goals

Goals:

  • 使用 target.type 建模不同 checker 类型v1 支持 httpcommand
  • 将 HTTP 配置放入 target.http,将命令配置放入 target.command,移除顶层 HTTP 字段。
  • 为各 checker 类型定义领域专用 expect 名称HTTP 使用 statusheadersbodycommand 使用 exitCodestdoutstderr
  • 为不同 checker 类型提供默认成功语义HTTP 默认 status: [200]command 默认 exitCode: [0]
  • 将可排序内容检查表达为数组,保证 bodystdoutstderr 按配置顺序执行。
  • 在 runner 和 expect pipeline 层共同实现快速失败,避免 status/header 已失败时仍读取或解析 body。
  • 使用 durationMs 表达 checker 执行耗时,替代 HTTP-only 的 latencyMs
  • 引入结构化失败信息并入库,区分执行错误和 expect 不匹配,耗时阈值字段统一为 maxDurationMs
  • 引入全局并发限制和 100MB 默认读取上限,避免 HTTP body 或 command 输出造成资源失控。

Non-Goals:

  • 不兼容旧的顶层 urlmethodheadersbody 配置。
  • 不做旧 SQLite schema 迁移;实现阶段可以按新 schema 初始化和测试。
  • 不支持 shell 字符串命令command v1 仅支持 exec + args
  • 不持久化完整 HTTP body、stdout 或 stderr只持久化结构化失败摘要。
  • 不引入新的解析或执行依赖。
  • 不在本次实现告警通知、认证鉴权或动态增删目标。

Decisions

1. 使用判别联合建模 Target

配置和解析后的目标都使用 type 判别:

targets:
  - name: "HTTP 健康检查"
    type: http
    http:
      url: "https://example.com/health"
      method: GET

  - name: "Nginx 进程检查"
    type: command
    command:
      exec: "pgrep"
      args: ["nginx"]

理由HTTP 与 command 的领域字段差异明显,强行把 URL、exec、status、exitCode 抽成统一字段会降低语义清晰度。判别联合可以让 TypeScript 在执行器选择、配置校验和 expect 校验中获得更明确的类型约束。

替代方案:保留顶层 url 并通过字段存在性推断 HTTP。该方案兼容性更好但会继续让 HTTP 成为隐式默认类型,不符合当前无兼容包袱下的最佳模型。

2. defaults 分为通用和领域分组

建议配置形态:

runtime:
  maxConcurrentChecks: 20

defaults:
  interval: "30s"
  timeout: "10s"
  http:
    method: GET
    maxBodyBytes: "100MB"
  command:
    cwd: "."
    maxOutputBytes: "100MB"

通用默认值只覆盖所有 checker 都共享的调度与超时字段,领域默认值只覆盖对应 target type。target 自身配置优先级高于 defaults。

替代方案:继续使用 defaults.methoddefaults.headers 等 HTTP 字段。该方案会在 command target 中产生无意义字段,因此不采用。

3. 默认 expect 是逻辑默认值

当用户未显式配置对应状态类 expect 时runner 在校验阶段应用领域默认值,而不是把默认值写回用户配置。

HTTP 默认:status: [200]

Command 默认:exitCode: [0]

示例:

expect:
  body:
    - contains: "ok"

该 HTTP target 仍然先检查 status == 200,再检查 body。这样用户只写内容检查时不会把 HTTP 500 错误响应误判为 UP。

替代方案:只有完全不写 expect 时才应用默认值。该方案会让“只写 body”绕过 status 检查,不符合默认成功语义,因此不采用。

4. Expect pipeline 使用固定阶段顺序和有序规则数组

HTTP 顺序:

status -> duration -> headers -> body[0] -> body[1] -> ...

Command 顺序:

exitCode -> duration -> stdout[0] -> stdout[1] -> ... -> stderr[0] -> stderr[1] -> ...

bodystdoutstderr 使用数组表达配置顺序:

expect:
  body:
    - contains: "healthy"
    - json:
        path: "$.status"
        equals: "ok"
    - regex: '"version":"\\d+\\.\\d+"'

理由:对象字段天然更像无序集合,不适合表达用户指定的检查顺序。数组规则可以直接生成 path,例如 expect.body[1].json($.status),方便失败定位。

替代方案:保留对象结构并约定 contains/regex/json/css/xpath 固定顺序。该方案无法满足“按配置文件中的配置顺序依次检查”的要求,因此不采用。

5. 复用通用值操作符,但保持领域 expect 名称

保留并扩展现有操作符:equalscontainsmatchemptyexistsgteltegtlt。这些操作符可用于 HTTP header、HTTP body 提取值、command stdout/stderr 文本等。

领域名称保持专用HTTP 使用 statuscommand 使用 exitCodeHTTP body 可使用 json/css/xpathcommand 输出只使用文本规则和通用操作符。

替代方案:把所有值统一抽象成 statusmetadatapayload。该方案过度泛化,会让 YAML 对使用者不直观,因此不采用。

6. Runner 负责按需产生 Observation

HTTP runner 不应总是读取完整 response body。它先发起请求并取得 status、headers 和 duration再运行 status/duration/headers 阶段;只有配置中存在 body 规则且前置阶段通过时,才读取 body并受 maxBodyBytes 限制。

Command runner 需要执行命令并收集 exitCode、duration、stdout、stderr。stdout 和 stderr 合计受 maxOutputBytes 限制,默认 100MB。命令超时或输出超限时runner 产生 success=falsefailure.kind=error

替代方案runner 总是完整产生所有字段,再交给 expect。该方案实现简单但无法真正快速失败也无法避免不必要的资源读取因此不采用。

7. Command 执行不经过 shell

command target 使用 exec + args,实现阶段优先使用 Bun 可用的子进程 API并禁止默认 shell 展开。

command:
  exec: "pgrep"
  args: ["nginx"]
  cwd: "."
  env:
    LANG: "C"

cwd 相对配置文件所在目录解析。env 默认继承当前进程环境并允许覆盖指定键。v1 不支持 stdin避免命令阻塞。

替代方案:允许 shell: "pgrep nginx | wc -l"。该方案更灵活,但引入转义、注入和跨平台 shell 差异,不适合作为第一版默认能力。

8. 全局并发限制由 ProbeEngine 统一执行

runtime.maxConcurrentChecks 默认 20。调度仍按 interval 分组触发,但每个目标进入全局并发池后再执行,避免同一 tick 或多个 tick 同时启动过多 HTTP 请求和本地进程。

理由command target 可能启动本地进程,继续无限 Promise.allSettled 会有资源风险。全局限制比按组限制更容易理解,也能覆盖不同 interval 组同时触发的情况。

替代方案:为 HTTP 和 command 分别设置并发上限。该方案更精细,但增加配置复杂度,当前需求只要求全局默认值。

9. CheckResult 使用结构化 failure

结果模型区分 runner 执行失败和 expect 不匹配:

interface CheckFailure {
  kind: "error" | "mismatch";
  phase: "status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr";
  path: string;
  expected?: unknown;
  actual?: unknown;
  message: string;
}

success=false 表示 runner 未能正常产生可校验结果,例如网络错误、超时、命令启动失败、输出超限。matched=false 表示 runner 执行成功但 expect 不匹配。failure 字段存储首个失败原因,实际值需要截断,避免超长内容或敏感内容进入数据库和 API。

替代方案:继续只存 error 字符串。该方案无法区分执行失败与规则不匹配,也不能准确定位失败 path因此不采用。

10. 存储、API、Dashboard 改为 checker 通用语义

SQLite schema 建议从 HTTP-only 字段调整为:

targets:
  id, name, type, target, config, interval_ms, timeout_ms, expect

check_results:
  id, target_id, timestamp, success, matched, duration_ms, status_detail, failure

target 是用于展示和搜索的目标摘要,例如 HTTP URL 或 command 命令行摘要;config 持久化解析后的领域配置 JSONstatus_detail 存储领域状态摘要,例如 HTTP 200exitCode=1

API 共享类型使用 durationMsstatusDetailfailureDashboard 表格展示“类型、目标、状态、耗时、最近失败原因、趋势”。HTTP 详情可显示 status codecommand 详情可显示 exit code但列表层不使用 HTTP-only 列名。

替代方案:继续保留 urlmethodstatus_codelatency_ms 并为 command 填空。该方案会把领域语义混在一起,后续扩展成本高,因此不采用。

11. Size 字符串解析

新增 size 解析支持 BKBMBGB,默认 100MB 等于 104857600 bytes。HTTP maxBodyBytes 限制单次 body 读取command maxOutputBytes 限制 stdout 和 stderr 合计读取。

理由YAML 直接写字节数可读性差,二进制单位更适合内存和 buffer 限制。

替代方案:复用 duration 解析或只接受 number。前者语义不匹配后者配置可读性差。

Risks / Trade-offs

  • [Risk] maxConcurrentChecks=20 且单次读取上限为 100MB 时理论内存峰值较高 → [Mitigation] 提供全局并发限制和 per-target/per-default 读取上限,文档明确资源上限由用户配置共同决定。
  • [Risk] 结构化失败信息可能包含敏感响应片段或命令输出 → [Mitigation] 只存首个失败原因,actual 做长度截断,默认不持久化完整 body/stdout/stderr。
  • [Risk] command checker 允许执行本地命令,有误配置或高开销命令风险 → [Mitigation] 不支持 shell强制 timeout限制输出大小使用全局并发限制。
  • [Risk] 不兼容旧配置会导致现有样例和测试全部失效 → [Mitigation] 项目未上线,实施时同步更新 README、示例配置、单元测试和 smoke test。
  • [Risk] SQLite schema 重建会丢失旧数据 → [Mitigation] 当前无上线数据,不做迁移;若后续需要升级已部署实例,应另起兼容迁移 change。

Migration Plan

  • 更新类型定义、配置解析和 README 示例,先让新 YAML 契约成为唯一入口。
  • 重构存储 schema 和共享 API 类型,再更新 Dashboard 使用新字段。
  • 引入 expect 规则数组和结构化 failure迁移 HTTP runner 到新 pipeline。
  • 添加 command runner并接入 ProbeEngine 的 runner 选择与全局并发限制。
  • 更新测试覆盖配置、HTTP expect、command expect、存储、API、Dashboard 和 smoke test。
  • 运行 bun run checkbun run verify,确保完整质量门禁通过。

Open Questions

无。当前讨论已确认默认 HTTP status 使用 [200]、默认并发限制使用全局配置、HTTP body 与 command 输出默认上限均为 100MB