256 lines
12 KiB
Markdown
256 lines
12 KiB
Markdown
## 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`。
|