## Context 当前实现的配置、执行、存储、API 和 Dashboard 都以 HTTP 请求为中心:`target.url` 是必填字段,执行器直接 `fetch(url)`,结果存储包含 `status_code` 与 `latency_ms`,前端展示 URL、method 和 HTTP 状态码。这种模型无法承载本地命令等非 HTTP checker,也让 `expect` 只能表达 HTTP response 的 status/header/body。 项目尚未上线,不需要兼容旧 YAML、旧数据库 schema 或旧 API 契约,因此本次设计选择直接建立 typed target 与领域专用 expect,而不是添加兼容分支。目标是让 HTTP 变成 runner 的一种实现,同时新增 command runner,并为未来其他 checker 类型保留清晰扩展点。 ```text 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 支持 `http` 与 `command`。 - 将 HTTP 配置放入 `target.http`,将命令配置放入 `target.command`,移除顶层 HTTP 字段。 - 为各 checker 类型定义领域专用 expect 名称,HTTP 使用 `status`、`headers`、`body`,command 使用 `exitCode`、`stdout`、`stderr`。 - 为不同 checker 类型提供默认成功语义:HTTP 默认 `status: [200]`,command 默认 `exitCode: [0]`。 - 将可排序内容检查表达为数组,保证 `body`、`stdout`、`stderr` 按配置顺序执行。 - 在 runner 和 expect pipeline 层共同实现快速失败,避免 status/header 已失败时仍读取或解析 body。 - 使用 `durationMs` 表达 checker 执行耗时,替代 HTTP-only 的 `latencyMs`。 - 引入结构化失败信息并入库,区分执行错误和 expect 不匹配,耗时阈值字段统一为 `maxDurationMs`。 - 引入全局并发限制和 100MB 默认读取上限,避免 HTTP body 或 command 输出造成资源失控。 **Non-Goals:** - 不兼容旧的顶层 `url`、`method`、`headers`、`body` 配置。 - 不做旧 SQLite schema 迁移;实现阶段可以按新 schema 初始化和测试。 - 不支持 shell 字符串命令;command v1 仅支持 `exec + args`。 - 不持久化完整 HTTP body、stdout 或 stderr,只持久化结构化失败摘要。 - 不引入新的解析或执行依赖。 - 不在本次实现告警通知、认证鉴权或动态增删目标。 ## Decisions ### 1. 使用判别联合建模 Target 配置和解析后的目标都使用 `type` 判别: ```yaml 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 分为通用和领域分组 建议配置形态: ```yaml runtime: maxConcurrentChecks: 20 defaults: interval: "30s" timeout: "10s" http: method: GET maxBodyBytes: "100MB" command: cwd: "." maxOutputBytes: "100MB" ``` 通用默认值只覆盖所有 checker 都共享的调度与超时字段,领域默认值只覆盖对应 target type。target 自身配置优先级高于 defaults。 替代方案:继续使用 `defaults.method`、`defaults.headers` 等 HTTP 字段。该方案会在 command target 中产生无意义字段,因此不采用。 ### 3. 默认 expect 是逻辑默认值 当用户未显式配置对应状态类 expect 时,runner 在校验阶段应用领域默认值,而不是把默认值写回用户配置。 HTTP 默认:`status: [200]`。 Command 默认:`exitCode: [0]`。 示例: ```yaml expect: body: - contains: "ok" ``` 该 HTTP target 仍然先检查 `status == 200`,再检查 body。这样用户只写内容检查时不会把 HTTP 500 错误响应误判为 UP。 替代方案:只有完全不写 `expect` 时才应用默认值。该方案会让“只写 body”绕过 status 检查,不符合默认成功语义,因此不采用。 ### 4. Expect pipeline 使用固定阶段顺序和有序规则数组 HTTP 顺序: ```text status -> duration -> headers -> body[0] -> body[1] -> ... ``` Command 顺序: ```text exitCode -> duration -> stdout[0] -> stdout[1] -> ... -> stderr[0] -> stderr[1] -> ... ``` `body`、`stdout`、`stderr` 使用数组表达配置顺序: ```yaml 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 名称 保留并扩展现有操作符:`equals`、`contains`、`match`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。这些操作符可用于 HTTP header、HTTP body 提取值、command stdout/stderr 文本等。 领域名称保持专用:HTTP 使用 `status`,command 使用 `exitCode`;HTTP body 可使用 `json/css/xpath`,command 输出只使用文本规则和通用操作符。 替代方案:把所有值统一抽象成 `status`、`metadata`、`payload`。该方案过度泛化,会让 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=false` 和 `failure.kind=error`。 替代方案:runner 总是完整产生所有字段,再交给 expect。该方案实现简单,但无法真正快速失败,也无法避免不必要的资源读取,因此不采用。 ### 7. Command 执行不经过 shell command target 使用 `exec + args`,实现阶段优先使用 Bun 可用的子进程 API,并禁止默认 shell 展开。 ```yaml 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 不匹配: ```ts 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 字段调整为: ```text 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` 持久化解析后的领域配置 JSON;`status_detail` 存储领域状态摘要,例如 `HTTP 200` 或 `exitCode=1`。 API 共享类型使用 `durationMs`、`statusDetail`、`failure`,Dashboard 表格展示“类型、目标、状态、耗时、最近失败原因、趋势”。HTTP 详情可显示 status code,command 详情可显示 exit code,但列表层不使用 HTTP-only 列名。 替代方案:继续保留 `url`、`method`、`status_code`、`latency_ms` 并为 command 填空。该方案会把领域语义混在一起,后续扩展成本高,因此不采用。 ### 11. Size 字符串解析 新增 size 解析支持 `B`、`KB`、`MB`、`GB`,默认 `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 check` 和 `bun run verify`,确保完整质量门禁通过。 ## Open Questions 无。当前讨论已确认默认 HTTP status 使用 `[200]`、默认并发限制使用全局配置、HTTP body 与 command 输出默认上限均为 `100MB`。