feat: 重构配置布局,server.listen/storage/logging + probes.execution 分组
- 新增 server.listen (host/port)、server.storage (dataDir/retention)、 server.logging 分组 - 新增 probes.execution (maxConcurrentChecks) 分组,替代顶层 runtime - 旧配置入口 (runtime/logging/server.host/server.port/server.dataDir) 启动期拒绝 - 更新 types.ts、builder.ts、config-loader.ts 适配新路径 - 更新 probe-config.schema.json、probes.example.yaml、README.md、 DEVELOPMENT.md - 补充 config-loader 和 variables 测试覆盖新路径和旧入口拒绝 - 同步 5 个 delta specs 到主规范 (probe-config, config-variables, data-retention, probe-engine, runtime-logging) - 归档 openspec change reorganize-config-layout
This commit is contained in:
@@ -140,7 +140,7 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
|
||||
dev.ts / main.ts → readRuntimeConfig(cli args, 仅提取 configPath)
|
||||
→ bootstrap({ configPath, mode })
|
||||
→ loadConfig(yaml:YAML 解析 → 变量替换 → 契约校验 → 语义校验 → resolve)
|
||||
→ ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets, logging }
|
||||
→ ResolvedConfig{ host(server.listen), port(server.listen), dataDir(server.storage), maxConcurrentChecks(probes.execution), retentionMs(server.storage), targets, logging(server.logging) }
|
||||
→ createRuntimeLogger(logging) → Logger(配置加载失败时使用 ConsoleFallbackLogger)
|
||||
→ ProbeStore(db) → store.syncTargets(targets)
|
||||
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs, logger) → engine.start()
|
||||
@@ -254,16 +254,16 @@ export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode:
|
||||
|
||||
`ResolvedConfig` 包含以下字段:
|
||||
|
||||
| 字段 | 来源 | 默认值 |
|
||||
| --------------------- | -------------------------------------------------- | ---------------- |
|
||||
| `configDir` | 配置文件所在目录 | — |
|
||||
| `dataDir` | `server.dataDir`(基于配置文件目录解析为绝对路径) | `configDir/data` |
|
||||
| `host` | `server.host` | `127.0.0.1` |
|
||||
| `logging` | `logging`(等级继承、路径解析、滚动参数) | 见 logging 配置 |
|
||||
| `port` | `server.port` | `3000` |
|
||||
| `maxConcurrentChecks` | `runtime.maxConcurrentChecks` | `20` |
|
||||
| `retentionMs` | `runtime.retention` | `7d` |
|
||||
| `targets` | `targets[]` 经 resolve 后 | — |
|
||||
| 字段 | 来源 | 默认值 |
|
||||
| --------------------- | ---------------------------------------------------------- | ---------------- |
|
||||
| `configDir` | 配置文件所在目录 | — |
|
||||
| `dataDir` | `server.storage.dataDir`(基于配置文件目录解析为绝对路径) | `configDir/data` |
|
||||
| `host` | `server.listen.host` | `127.0.0.1` |
|
||||
| `logging` | `server.logging`(等级继承、路径解析、滚动参数) | 见 logging 配置 |
|
||||
| `port` | `server.listen.port` | `3000` |
|
||||
| `maxConcurrentChecks` | `probes.execution.maxConcurrentChecks` | `20` |
|
||||
| `retentionMs` | `server.storage.retention` | `7d` |
|
||||
| `targets` | `targets[]` 经 resolve 后 | — |
|
||||
|
||||
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||||
|
||||
|
||||
66
README.md
66
README.md
@@ -117,13 +117,20 @@ dist/release/
|
||||
# yaml-language-server: $schema=./probe-config.schema.json
|
||||
|
||||
server: # 服务配置(均可省略)
|
||||
host: "127.0.0.1"
|
||||
port: 3000
|
||||
dataDir: "/tmp/probes_data"
|
||||
listen:
|
||||
host: "127.0.0.1"
|
||||
port: 3000
|
||||
storage:
|
||||
dataDir: "/tmp/probes_data"
|
||||
retention: "7d"
|
||||
logging:
|
||||
level: "info"
|
||||
file:
|
||||
path: "<dataDir>/logs/dial.log"
|
||||
|
||||
runtime: # 运行时配置
|
||||
maxConcurrentChecks: 20
|
||||
retention: "7d"
|
||||
probes: # 拨测运行时配置(可省略)
|
||||
execution:
|
||||
maxConcurrentChecks: 20
|
||||
|
||||
variables: # 配置变量(可省略)
|
||||
env_name: "生产"
|
||||
@@ -154,32 +161,37 @@ targets: # 拨测目标列表(必填)
|
||||
# ... 更多 targets
|
||||
```
|
||||
|
||||
### server — 服务配置
|
||||
### server.listen — 监听配置
|
||||
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| --------- | ------------------------------------------ | ---- | ----------- |
|
||||
| `host` | 监听地址 | 否 | `127.0.0.1` |
|
||||
| `port` | 监听端口 | 否 | `3000` |
|
||||
| `dataDir` | 数据目录,相对路径基于配置文件所在目录解析 | 否 | `./data` |
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| ------ | -------- | ---- | ----------- |
|
||||
| `host` | 监听地址 | 否 | `127.0.0.1` |
|
||||
| `port` | 监听端口 | 否 | `3000` |
|
||||
|
||||
### runtime — 运行时配置
|
||||
### server.storage — 存储配置
|
||||
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| --------------------- | ------------------------------------------------ | ---- | ------ |
|
||||
| `maxConcurrentChecks` | 最大并发拨测数 | 否 | `20` |
|
||||
| `retention` | 历史数据保留时长,支持 `ms`/`s`/`m`/`h`/`d` 单位 | 否 | `7d` |
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| ----------- | ------------------------------------------------ | ---- | -------- |
|
||||
| `dataDir` | 数据目录,相对路径基于配置文件所在目录解析 | 否 | `./data` |
|
||||
| `retention` | 历史数据保留时长,支持 `ms`/`s`/`m`/`h`/`d` 单位 | 否 | `7d` |
|
||||
|
||||
### logging — 日志配置
|
||||
### probes.execution — 拨测运行时配置
|
||||
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| ------------------------- | ---------------------------------------------- | ---- | ------------------------- |
|
||||
| `level` | 全局日志等级,console 和 file 未指定时继承此值 | 否 | `info` |
|
||||
| `console.level` | 控制台日志等级 | 否 | 继承 `level` |
|
||||
| `file.level` | 文件日志等级 | 否 | 继承 `level` |
|
||||
| `file.path` | 日志文件路径,相对路径基于配置文件目录解析 | 否 | `<dataDir>/logs/dial.log` |
|
||||
| `file.rotation.size` | 按大小滚动,支持 `KB`/`MB`/`GB` 单位 | 否 | `50MB` |
|
||||
| `file.rotation.frequency` | 按时间滚动:`hourly`、`daily`、`weekly` | 否 | `daily` |
|
||||
| `file.rotation.maxFiles` | 保留的归档文件数量(不含活跃日志) | 否 | `14` |
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| --------------------- | -------------- | ---- | ------ |
|
||||
| `maxConcurrentChecks` | 最大并发拨测数 | 否 | `20` |
|
||||
|
||||
### server.logging — 日志配置
|
||||
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| ---------------------------------------- | ---------------------------------------------- | ---- | ------------------------- |
|
||||
| `server.logging.level` | 全局日志等级,console 和 file 未指定时继承此值 | 否 | `info` |
|
||||
| `server.logging.console.level` | 控制台日志等级 | 否 | 继承 `level` |
|
||||
| `server.logging.file.level` | 文件日志等级 | 否 | 继承 `level` |
|
||||
| `server.logging.file.path` | 日志文件路径,相对路径基于配置文件目录解析 | 否 | `<dataDir>/logs/dial.log` |
|
||||
| `server.logging.file.rotation.size` | 按大小滚动,支持 `KB`/`MB`/`GB` 单位 | 否 | `50MB` |
|
||||
| `server.logging.file.rotation.frequency` | 按时间滚动:`hourly`、`daily`、`weekly` | 否 | `daily` |
|
||||
| `server.logging.file.rotation.maxFiles` | 保留的归档文件数量(不含活跃日志) | 否 | `14` |
|
||||
|
||||
日志等级支持:`trace`、`debug`、`info`、`warn`、`error`、`fatal`。
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA
|
||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的 "localhost"
|
||||
|
||||
### Requirement: 替换范围限制
|
||||
变量替换 SHALL 仅作用于 targets 段。`id` 和 `type` 字段 MUST NOT 参与变量替换。`server`、`runtime`、`defaults` 段 MUST NOT 参与变量替换。系统 SHALL 递归遍历 target 对象树中所有字符串值进行替换(包括嵌套对象和数组元素中的字符串)。
|
||||
变量替换 SHALL 仅作用于 targets 段。`id` 和 `type` 字段 MUST NOT 参与变量替换。`server`、`probes`、`defaults` 段 MUST NOT 参与变量替换。系统 SHALL 递归遍历 target 对象树中所有字符串值进行替换(包括嵌套对象和数组元素中的字符串)。
|
||||
|
||||
#### Scenario: target 嵌套对象中的变量替换
|
||||
- **WHEN** target 配置 `http.headers.Authorization: "${token}"` 且 variables 中定义 `token: "Bearer abc"`
|
||||
@@ -144,8 +144,12 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA
|
||||
- **THEN** 系统 SHALL 保持 defaults.interval 为字面量 `"${default_interval}"`,不进行替换
|
||||
|
||||
#### Scenario: server 段不替换
|
||||
- **WHEN** server 配置 `host: "${server_host}"` 且 variables 中定义 `server_host: "0.0.0.0"`
|
||||
- **THEN** 系统 SHALL 保持 server.host 为字面量 `"${server_host}"`,不进行替换
|
||||
- **WHEN** server 配置 `listen.host: "${server_host}"` 且 variables 中定义 `server_host: "0.0.0.0"`
|
||||
- **THEN** 系统 SHALL 保持 server.listen.host 为字面量 `"${server_host}"`,不进行替换
|
||||
|
||||
#### Scenario: probes 段不替换
|
||||
- **WHEN** probes 配置 `execution.maxConcurrentChecks: "${max_checks}"` 且 variables 中定义 `max_checks: 5`
|
||||
- **THEN** 系统 SHALL 保持 probes.execution.maxConcurrentChecks 为字面量 `"${max_checks}"`,不进行替换
|
||||
|
||||
### Requirement: 变量替换错误报告
|
||||
变量替换阶段的错误 SHALL 作为 `ConfigValidationIssue` 输出,code 为 `unresolved-variable`。错误信息 SHALL 包含 target 索引、target id、字段路径和变量名。
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: 数据保留配置
|
||||
系统 SHALL 支持通过 `runtime.retention` 配置项指定历史数据保留时长,格式为持续时间字符串(`<数字><单位>`,单位支持 `d`/`h`/`m`)。
|
||||
系统 SHALL 支持通过 `server.storage.retention` 配置项指定历史数据保留时长,格式为持续时间字符串(`<数字><单位>`,单位支持 `d`/`h`/`m`)。
|
||||
|
||||
#### Scenario: 配置 7 天保留
|
||||
- **WHEN** 配置文件中 `runtime.retention` 设置为 `"7d"`
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 设置为 `"7d"`
|
||||
- **THEN** 系统 SHALL 保留最近 7 天的检查结果,清理更早的数据
|
||||
|
||||
#### Scenario: 配置小时级保留
|
||||
- **WHEN** 配置文件中 `runtime.retention` 设置为 `"24h"`
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 设置为 `"24h"`
|
||||
- **THEN** 系统 SHALL 保留最近 24 小时的检查结果
|
||||
|
||||
#### Scenario: 未配置 retention
|
||||
- **WHEN** 配置文件中未指定 `runtime.retention`
|
||||
- **WHEN** 配置文件中未指定 `server.storage.retention`
|
||||
- **THEN** 系统 SHALL 使用默认值 `"7d"`
|
||||
|
||||
#### Scenario: 无效 retention 格式
|
||||
- **WHEN** 配置文件中 `runtime.retention` 格式不合法(如 `"abc"`、`"7x"`)
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 格式不合法(如 `"abc"`、`"7x"`)
|
||||
- **THEN** 系统 SHALL 在配置校验阶段报错,拒绝启动
|
||||
|
||||
### Requirement: 定时清理调度
|
||||
|
||||
@@ -4,18 +4,37 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 旧配置入口拒绝
|
||||
系统 SHALL 拒绝旧版顶层 `runtime` 和顶层 `logging` 配置入口。系统 SHALL 拒绝旧版 `server.host`、`server.port` 和 `server.dataDir` 入口,并要求使用 `server.listen` 与 `server.storage` 下的新路径。
|
||||
|
||||
#### Scenario: 顶层 runtime 被拒绝
|
||||
- **WHEN** 配置文件声明顶层 `runtime`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `runtime`
|
||||
|
||||
#### Scenario: 顶层 logging 被拒绝
|
||||
- **WHEN** 配置文件声明顶层 `logging`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `logging`
|
||||
|
||||
#### Scenario: server 旧监听字段被拒绝
|
||||
- **WHEN** 配置文件声明 `server.host` 或 `server.port`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段
|
||||
|
||||
#### Scenario: server 旧 dataDir 字段被拒绝
|
||||
- **WHEN** 配置文件声明 `server.dataDir`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir`
|
||||
|
||||
### 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` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。
|
||||
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、probes 执行配置、可选的 variables 段、checker 默认值和 typed target 列表(含可选 group 字段)。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组,将本地数据目录和历史数据保留时长放在 `server.storage` 分组,将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。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` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。
|
||||
|
||||
`defaults.http` 分组 SHALL 仅支持 `headers`(可选)和 `maxBodyBytes`(可选)字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。`defaults.icmp` 分组 SHALL 仅支持空对象。`defaults.udp` 分组 SHALL 仅支持 `encoding`(可选)、`responseEncoding`(可选)和 `maxResponseBytes`(可选)字段。`defaults.llm` 分组 SHALL 仅支持 `mode`(可选)、`headers`(可选)、`ignoreSSL`(可选)、`options`(可选)和 `providerOptions`(可选)字段。
|
||||
|
||||
#### Scenario: 完整配置文件解析
|
||||
- **WHEN** 系统启动并读取包含 server、runtime、variables、defaults、targets(含 id、group 字段)的 YAML 配置文件
|
||||
- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables、defaults、targets(含 id、group 字段)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
|
||||
|
||||
#### Scenario: 最简 HTTP 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、runtime、variables、defaults 和 expect)
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, runtime.maxConcurrentChecks=20, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default"),并保留 name=null、description=null
|
||||
- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、probes、variables、defaults 和 expect)
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default"),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: 最简 cmd 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: cmd` target(含 `id` 和 `cmd.exec`)的 YAML 配置文件
|
||||
@@ -136,8 +155,8 @@
|
||||
- **THEN** 系统 SHALL 以错误退出并提示格式错误
|
||||
|
||||
#### Scenario: maxConcurrentChecks 非法
|
||||
- **WHEN** runtime.maxConcurrentChecks 不是正整数
|
||||
- **THEN** 系统 SHALL 以错误退出并提示 runtime.maxConcurrentChecks 格式错误
|
||||
- **WHEN** probes.execution.maxConcurrentChecks 不是正整数
|
||||
- **THEN** 系统 SHALL 以错误退出并提示 probes.execution.maxConcurrentChecks 格式错误
|
||||
|
||||
#### Scenario: interval 或 timeout 解析结果非法
|
||||
- **WHEN** interval 或 timeout 解析结果不是正整数毫秒(如 `0ms` 或 `1.5ms`)
|
||||
@@ -325,14 +344,14 @@
|
||||
- **THEN** 系统 SHALL 将其解析为 524288 bytes
|
||||
|
||||
### Requirement: runtime 并发配置
|
||||
系统 SHALL 支持 `runtime.maxConcurrentChecks` 配置全局最大并发检查数。
|
||||
系统 SHALL 支持 `probes.execution.maxConcurrentChecks` 配置全局最大并发检查数。`probes` 和 `probes.execution` 配置段均 SHALL 为可选,省略时使用默认值。
|
||||
|
||||
#### Scenario: 使用默认并发限制
|
||||
- **WHEN** YAML 中未配置 runtime.maxConcurrentChecks
|
||||
- **WHEN** YAML 中未配置 `probes` 或未配置 `probes.execution.maxConcurrentChecks`
|
||||
- **THEN** 系统 SHALL 使用默认值 20
|
||||
|
||||
#### Scenario: 配置并发限制
|
||||
- **WHEN** YAML 中配置 `runtime.maxConcurrentChecks: 5`
|
||||
- **WHEN** YAML 中配置 `probes.execution.maxConcurrentChecks: 5`
|
||||
- **THEN** 系统 SHALL 将全局最大并发检查数设置为 5
|
||||
|
||||
### Requirement: YAML 配置使用 Bun 内置解析
|
||||
@@ -428,33 +447,33 @@
|
||||
- **THEN** `targets.expect` SHALL 存储变量替换后的 Raw expect JSON,而不是包含 `kind` 或 resolved matcher 的运行期执行计划
|
||||
|
||||
### Requirement: 数据保留配置字段
|
||||
配置 schema 的 `runtime` 段 SHALL 支持 `retention` 字段,类型为字符串,格式为 `<数字><单位>`(单位:`d` 天、`h` 小时、`m` 分钟),用于指定历史数据保留时长。
|
||||
配置 schema 的 `server.storage` 段 SHALL 支持 `retention` 字段,类型为字符串,格式为 `<数字><单位>`(单位:`d` 天、`h` 小时、`m` 分钟),用于指定历史数据保留时长。
|
||||
|
||||
#### Scenario: retention 字段校验通过
|
||||
- **WHEN** 配置文件中 `runtime.retention` 为合法格式(如 `"7d"`、`"24h"`、`"30m"`)
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 为合法格式(如 `"7d"`、`"24h"`、`"30m"`)
|
||||
- **THEN** 配置校验 SHALL 通过
|
||||
|
||||
#### Scenario: retention 字段格式非法
|
||||
- **WHEN** 配置文件中 `runtime.retention` 为非法格式(如 `"abc"`、`"7x"`、`""`)
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 为非法格式(如 `"abc"`、`"7x"`、`""`)
|
||||
- **THEN** 配置校验 SHALL 失败并报告格式错误
|
||||
|
||||
#### Scenario: retention 字段缺省
|
||||
- **WHEN** 配置文件中未指定 `runtime.retention`
|
||||
- **WHEN** 配置文件中未指定 `server.storage.retention`
|
||||
- **THEN** 系统 SHALL 使用默认值 `"7d"`
|
||||
|
||||
### Requirement: 数据目录路径解析
|
||||
配置加载流程 SHALL 将 `server.dataDir` 相对路径基于配置文件所在目录(configDir)解析为绝对路径。绝对路径 SHALL 保持不变。
|
||||
配置加载流程 SHALL 将 `server.storage.dataDir` 相对路径基于配置文件所在目录(configDir)解析为绝对路径。绝对路径 SHALL 保持不变。
|
||||
|
||||
#### Scenario: dataDir 为相对路径
|
||||
- **WHEN** 配置文件位于 `/opt/dial/probes.yaml`,且 `server.dataDir` 配置为 `./data`
|
||||
- **WHEN** 配置文件位于 `/opt/dial/probes.yaml`,且 `server.storage.dataDir` 配置为 `./data`
|
||||
- **THEN** 系统 SHALL 将 dataDir 解析为 `/opt/dial/data`,而非依赖进程 cwd
|
||||
|
||||
#### Scenario: dataDir 为绝对路径
|
||||
- **WHEN** `server.dataDir` 配置为 `/var/lib/dial/data`
|
||||
- **WHEN** `server.storage.dataDir` 配置为 `/var/lib/dial/data`
|
||||
- **THEN** 系统 SHALL 直接使用该绝对路径,不做额外解析
|
||||
|
||||
#### Scenario: dataDir 使用默认值
|
||||
- **WHEN** 未配置 `server.dataDir`(使用默认值 `./data`)
|
||||
- **WHEN** 未配置 `server.storage.dataDir`(使用默认值 `./data`)
|
||||
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径
|
||||
|
||||
### Requirement: target 通用元信息字段约束
|
||||
@@ -634,51 +653,51 @@
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream.firstTokenMs 格式错误
|
||||
|
||||
### Requirement: 日志配置格式
|
||||
系统 SHALL 支持可选的顶层 `logging` 配置,用于定义运行时日志等级、命令行日志等级、文件日志等级、文件路径和滚动策略。`logging` 未配置时 SHALL 使用内置默认值。系统 SHALL NOT 支持 `logging.console.enabled`、`logging.console.format`、`logging.file.enabled`、`logging.file.format` 或 `logging.file.rotation.enabled` 字段。
|
||||
系统 SHALL 支持可选的 `server.logging` 配置,用于定义运行时日志等级、命令行日志等级、文件日志等级、文件路径和滚动策略。`server.logging` 未配置时 SHALL 使用内置默认值。系统 SHALL NOT 支持 `server.logging.console.enabled`、`server.logging.console.format`、`server.logging.file.enabled`、`server.logging.file.format` 或 `server.logging.file.rotation.enabled` 字段。
|
||||
|
||||
#### Scenario: 未配置 logging 使用默认值
|
||||
- **WHEN** 配置文件未声明 `logging`
|
||||
- **THEN** 系统 SHALL 使用 `logging.level=info`、`logging.console.level=info`、`logging.file.level=info`、`logging.file.path=<resolved dataDir>/logs/dial.log`、`logging.file.rotation.size=50MB`、`logging.file.rotation.frequency=daily` 和 `logging.file.rotation.maxFiles=14`
|
||||
- **WHEN** 配置文件未声明 `server.logging`
|
||||
- **THEN** 系统 SHALL 使用 `server.logging.level=info`、`server.logging.console.level=info`、`server.logging.file.level=info`、`server.logging.file.path=<resolved dataDir>/logs/dial.log`、`server.logging.file.rotation.size=50MB`、`server.logging.file.rotation.frequency=daily` 和 `server.logging.file.rotation.maxFiles=14`
|
||||
|
||||
#### Scenario: console 和 file level 继承全局 level
|
||||
- **WHEN** 配置声明 `logging.level: warn` 且未声明 `logging.console.level` 和 `logging.file.level`
|
||||
- **WHEN** 配置声明 `server.logging.level: warn` 且未声明 `server.logging.console.level` 和 `server.logging.file.level`
|
||||
- **THEN** 系统 SHALL 将 console 和 file 的日志等级均解析为 `warn`
|
||||
|
||||
#### Scenario: 显式配置文件日志路径
|
||||
- **WHEN** 配置声明 `logging.file.path`
|
||||
- **WHEN** 配置声明 `server.logging.file.path`
|
||||
- **THEN** 系统 SHALL 使用该路径作为文件日志路径,而不是默认 `<resolved dataDir>/logs/dial.log`
|
||||
|
||||
#### Scenario: 相对日志路径
|
||||
- **WHEN** `logging.file.path` 是相对路径
|
||||
- **WHEN** `server.logging.file.path` 是相对路径
|
||||
- **THEN** 系统 SHALL 基于配置文件所在目录解析为绝对路径
|
||||
|
||||
#### Scenario: 绝对日志路径
|
||||
- **WHEN** `logging.file.path` 是绝对路径
|
||||
- **WHEN** `server.logging.file.path` 是绝对路径
|
||||
- **THEN** 系统 SHALL 原样使用该绝对路径,并允许该路径位于 `dataDir` 之外
|
||||
|
||||
#### Scenario: 不支持日志开关和格式字段
|
||||
- **WHEN** 配置声明 `logging.console.enabled`、`logging.console.format`、`logging.file.enabled`、`logging.file.format` 或 `logging.file.rotation.enabled`
|
||||
- **WHEN** 配置声明 `server.logging.console.enabled`、`server.logging.console.format`、`server.logging.file.enabled`、`server.logging.file.format` 或 `server.logging.file.rotation.enabled`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段
|
||||
|
||||
### Requirement: 日志配置校验
|
||||
系统 SHALL 在启动期校验 `logging` 配置。日志等级 SHALL 只能是 `trace`、`debug`、`info`、`warn`、`error` 或 `fatal`。`rotation.size` SHALL 使用有效 size 格式且解析为正整数字节数。`rotation.frequency` SHALL 只能是 `hourly`、`daily` 或 `weekly`。`rotation.maxFiles` SHALL 是正整数。
|
||||
系统 SHALL 在启动期校验 `server.logging` 配置。日志等级 SHALL 只能是 `trace`、`debug`、`info`、`warn`、`error` 或 `fatal`。`rotation.size` SHALL 使用有效 size 格式且解析为正整数字节数。`rotation.frequency` SHALL 只能是 `hourly`、`daily` 或 `weekly`。`rotation.maxFiles` SHALL 是正整数。
|
||||
|
||||
#### Scenario: 非法日志等级
|
||||
- **WHEN** 配置声明 `logging.level: verbose`
|
||||
- **WHEN** 配置声明 `server.logging.level: verbose`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示日志等级非法
|
||||
|
||||
#### Scenario: 非法滚动大小
|
||||
- **WHEN** 配置声明 `logging.file.rotation.size: "large"`
|
||||
- **WHEN** 配置声明 `server.logging.file.rotation.size: "large"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 size 格式非法
|
||||
|
||||
#### Scenario: 非法滚动频率
|
||||
- **WHEN** 配置声明 `logging.file.rotation.frequency: monthly`
|
||||
- **WHEN** 配置声明 `server.logging.file.rotation.frequency: monthly`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 frequency 非法
|
||||
|
||||
#### Scenario: 非法归档数量
|
||||
- **WHEN** 配置声明 `logging.file.rotation.maxFiles: 0`
|
||||
- **WHEN** 配置声明 `server.logging.file.rotation.maxFiles: 0`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 maxFiles 必须为正整数
|
||||
|
||||
#### Scenario: 非法日志路径
|
||||
- **WHEN** 配置声明 `logging.file.path` 为空字符串或空白字符串
|
||||
- **WHEN** 配置声明 `server.logging.file.path` 为空字符串或空白字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示日志路径非法
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- **THEN** 系统 SHALL 创建两个独立定时器,分别按各自频率调度
|
||||
|
||||
### Requirement: 组内并发拨测
|
||||
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `runtime.maxConcurrentChecks` 限制。当某个目标的 checker 执行 rejected(非正常 CheckResult 返回,而是 Promise reject)时,系统 SHALL 将该异常记录为 `matched: false` 的 check_result,而非仅 console.warn。
|
||||
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `probes.execution.maxConcurrentChecks` 限制。当某个目标的 checker 执行 rejected(非正常 CheckResult 返回,而是 Promise reject)时,系统 SHALL 将该异常记录为 `matched: false` 的 check_result,而非仅 console.warn。
|
||||
|
||||
#### Scenario: 同组目标并发执行
|
||||
- **WHEN** 调度器触发一次 tick,该组有 3 个目标,且全局并发余量至少为 3
|
||||
@@ -35,7 +35,7 @@
|
||||
- **THEN** 系统 SHALL 通过 Promise.allSettled 的索引关联回 target 数组,获取对应的 targetName 用于写入 check_result
|
||||
|
||||
#### Scenario: 全局并发限制生效
|
||||
- **WHEN** 调度器同时触发 10 个目标且 runtime.maxConcurrentChecks 为 3
|
||||
- **WHEN** 调度器同时触发 10 个目标且 probes.execution.maxConcurrentChecks 为 3
|
||||
- **THEN** 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
|
||||
|
||||
### Requirement: HTTP 拨测执行
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
系统 SHALL 在配置解析成功后初始化统一运行时 logger。logger SHALL 同时输出命令行 pretty 日志和文件 JSONL 日志。命令行输出、文件输出和文件滚动 SHALL 始终启用,不提供关闭开关。
|
||||
|
||||
#### Scenario: 默认初始化 logger
|
||||
- **WHEN** 配置文件未声明 `logging`
|
||||
- **WHEN** 配置文件未声明 `server.logging`
|
||||
- **THEN** 系统 SHALL 使用默认等级 `info` 初始化 console pretty 输出和 `<resolved dataDir>/logs/dial.log` 文件 JSONL 输出
|
||||
|
||||
#### Scenario: 模块 child logger
|
||||
@@ -20,14 +20,14 @@
|
||||
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零状态退出
|
||||
|
||||
### Requirement: 日志等级语义
|
||||
系统 SHALL 支持 `trace`、`debug`、`info`、`warn`、`error` 和 `fatal` 六个日志等级。`logging.level` SHALL 作为全局默认等级,`logging.console.level` 和 `logging.file.level` SHALL 在省略时继承全局等级。
|
||||
系统 SHALL 支持 `trace`、`debug`、`info`、`warn`、`error` 和 `fatal` 六个日志等级。`server.logging.level` SHALL 作为全局默认等级,`server.logging.console.level` 和 `server.logging.file.level` SHALL 在省略时继承全局等级。
|
||||
|
||||
#### Scenario: 目的地等级继承
|
||||
- **WHEN** 配置只声明 `logging.level: warn`
|
||||
- **WHEN** 配置只声明 `server.logging.level: warn`
|
||||
- **THEN** console 和 file 输出均 SHALL 使用 `warn` 作为最低输出等级
|
||||
|
||||
#### Scenario: 目的地等级覆盖
|
||||
- **WHEN** 配置声明 `logging.level: info`、`logging.console.level: warn` 和 `logging.file.level: debug`
|
||||
- **WHEN** 配置声明 `server.logging.level: info`、`server.logging.console.level: warn` 和 `server.logging.file.level: debug`
|
||||
- **THEN** console SHALL 输出 `warn` 及以上日志,file SHALL 输出 `debug` 及以上日志
|
||||
|
||||
#### Scenario: 默认不输出 debug 检查摘要
|
||||
@@ -35,7 +35,7 @@
|
||||
- **THEN** 每次检查的 debug 摘要 SHALL NOT 输出到 console 或 file
|
||||
|
||||
### Requirement: 文件日志滚动
|
||||
系统 SHALL 对文件日志启用滚动策略。滚动 SHALL 在 `logging.file.rotation.size` 或 `logging.file.rotation.frequency` 任一条件满足时触发。`logging.file.rotation.maxFiles` SHALL 表示最多保留的归档文件数量,不包含当前正在写入的日志文件。
|
||||
系统 SHALL 对文件日志启用滚动策略。滚动 SHALL 在 `server.logging.file.rotation.size` 或 `server.logging.file.rotation.frequency` 任一条件满足时触发。`server.logging.file.rotation.maxFiles` SHALL 表示最多保留的归档文件数量,不包含当前正在写入的日志文件。
|
||||
|
||||
#### Scenario: 按大小滚动
|
||||
- **WHEN** 当前日志文件达到配置的 `rotation.size`
|
||||
|
||||
@@ -211,159 +211,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"probes": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"console": {
|
||||
"execution": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "trace",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "debug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "info",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "warn",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "fatal",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"maxConcurrentChecks": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "trace",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "debug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "info",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "warn",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "fatal",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rotation": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"frequency": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "hourly",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "daily",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "weekly",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxFiles": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"size": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "trace",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "debug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "info",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "warn",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "fatal",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxConcurrentChecks": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"retention": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -371,16 +231,174 @@
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dataDir": {
|
||||
"type": "string"
|
||||
"listen": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
"logging": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"console": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "trace",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "debug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "info",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "warn",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "fatal",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "trace",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "debug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "info",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "warn",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "fatal",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rotation": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"frequency": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "hourly",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "daily",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "weekly",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxFiles": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"size": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "trace",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "debug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "info",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "warn",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": "fatal",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
"storage": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dataDir": {
|
||||
"type": "string"
|
||||
},
|
||||
"retention": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
# yaml-language-server: $schema=./probe-config.schema.json
|
||||
|
||||
server:
|
||||
host: "127.0.0.1"
|
||||
port: 3000
|
||||
dataDir: "/tmp/probes_data"
|
||||
listen:
|
||||
host: "127.0.0.1"
|
||||
port: 3000
|
||||
storage:
|
||||
dataDir: "/tmp/probes_data"
|
||||
# logging:
|
||||
# level: "info"
|
||||
# console:
|
||||
# level: "info"
|
||||
# file:
|
||||
# level: "info"
|
||||
# path: "/var/log/dial/dial.log"
|
||||
# rotation:
|
||||
# size: "50MB"
|
||||
# frequency: "daily"
|
||||
# maxFiles: 14
|
||||
|
||||
runtime:
|
||||
maxConcurrentChecks: 20
|
||||
|
||||
# logging:
|
||||
# level: "info"
|
||||
# console:
|
||||
# level: "info"
|
||||
# file:
|
||||
# level: "info"
|
||||
# path: "/var/log/dial/dial.log"
|
||||
# rotation:
|
||||
# size: "50MB"
|
||||
# frequency: "daily"
|
||||
# maxFiles: 14
|
||||
probes:
|
||||
execution:
|
||||
maxConcurrentChecks: 20
|
||||
|
||||
defaults:
|
||||
interval: "30s"
|
||||
|
||||
@@ -4,13 +4,14 @@ import { dirname, resolve } from "node:path";
|
||||
import type { ConfigValidationIssue } from "./schema/issues";
|
||||
import type {
|
||||
DefaultsConfig,
|
||||
EngineRuntimeConfig,
|
||||
ExecutionConfig,
|
||||
LoggingConfig,
|
||||
LogLevel,
|
||||
RawTargetConfig,
|
||||
ResolvedLoggingConfig,
|
||||
ResolvedTargetBase,
|
||||
RotationFrequency,
|
||||
ServerStorageConfig,
|
||||
} from "./types";
|
||||
|
||||
import { checkerRegistry } from "./runner";
|
||||
@@ -87,20 +88,23 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
||||
|
||||
const configDir = dirname(resolve(configPath));
|
||||
const server = validated.server ?? {};
|
||||
const runtime = validated.runtime ?? {};
|
||||
const listen = server.listen ?? {};
|
||||
const storage = server.storage ?? {};
|
||||
const defaults = validated.defaults ?? {};
|
||||
|
||||
const host = server.host ?? DEFAULT_HOST;
|
||||
const port = server.port ?? DEFAULT_PORT;
|
||||
const dataDir = resolve(configDir, server.dataDir ?? DEFAULT_DATA_DIR);
|
||||
const host = listen.host ?? DEFAULT_HOST;
|
||||
const port = listen.port ?? DEFAULT_PORT;
|
||||
const dataDir = resolve(configDir, storage.dataDir ?? DEFAULT_DATA_DIR);
|
||||
|
||||
const maxConcurrentChecks = resolveMaxConcurrentChecks(runtime);
|
||||
const retentionMs = resolveRetention(runtime);
|
||||
const probes = validated.probes ?? {};
|
||||
const execution = probes.execution ?? {};
|
||||
const maxConcurrentChecks = resolveMaxConcurrentChecks(execution);
|
||||
const retentionMs = resolveRetention(storage);
|
||||
|
||||
const logging = resolveLogging(validated.logging ?? {}, dataDir, configDir);
|
||||
const logging = resolveLogging(server.logging ?? {}, dataDir, configDir);
|
||||
|
||||
const allRuntimeIssues = [...allIssues];
|
||||
validateLoggingConfig(validated.logging, allRuntimeIssues);
|
||||
validateLoggingConfig(server.logging, allRuntimeIssues);
|
||||
if (allRuntimeIssues.length > 0) {
|
||||
throwConfigIssues(dedupeIssues(allRuntimeIssues));
|
||||
}
|
||||
@@ -172,19 +176,19 @@ function resolveLogLevel(level: unknown, fallback: LogLevel): LogLevel {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveMaxConcurrentChecks(runtime: EngineRuntimeConfig): number {
|
||||
if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
|
||||
function resolveMaxConcurrentChecks(execution: ExecutionConfig): number {
|
||||
if (execution.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
|
||||
if (
|
||||
!isNumber(runtime.maxConcurrentChecks) ||
|
||||
!Number.isInteger(runtime.maxConcurrentChecks) ||
|
||||
runtime.maxConcurrentChecks <= 0
|
||||
!isNumber(execution.maxConcurrentChecks) ||
|
||||
!Number.isInteger(execution.maxConcurrentChecks) ||
|
||||
execution.maxConcurrentChecks <= 0
|
||||
)
|
||||
return DEFAULT_MAX_CONCURRENT_CHECKS;
|
||||
return runtime.maxConcurrentChecks;
|
||||
return execution.maxConcurrentChecks;
|
||||
}
|
||||
|
||||
function resolveRetention(runtime: EngineRuntimeConfig): number {
|
||||
return parseDuration(runtime.retention ?? DEFAULT_RETENTION);
|
||||
function resolveRetention(storage: ServerStorageConfig): number {
|
||||
return parseDuration(storage.retention ?? DEFAULT_RETENTION);
|
||||
}
|
||||
|
||||
function resolveTarget(
|
||||
@@ -277,8 +281,8 @@ function validateConfig(config: RawProbeConfig): ConfigValidationIssue[] {
|
||||
validateDurationValue(config.defaults?.interval, "defaults.interval", issues);
|
||||
validateDurationValue(config.defaults?.timeout, "defaults.timeout", issues);
|
||||
validateDurationValue(
|
||||
isString(config.runtime?.retention) ? config.runtime.retention : undefined,
|
||||
"runtime.retention",
|
||||
isString(config.server?.storage?.retention) ? config.server.storage.retention : undefined,
|
||||
"server.storage.retention",
|
||||
issues,
|
||||
);
|
||||
for (let i = 0; i < config.targets.length; i++) {
|
||||
@@ -328,7 +332,11 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
|
||||
if (logging.level !== undefined && !VALID_LOG_LEVELS.includes(logging.level)) {
|
||||
issues.push(
|
||||
issue("invalid-value", "logging.level", `日志等级非法: "${logging.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`),
|
||||
issue(
|
||||
"invalid-value",
|
||||
"server.logging.level",
|
||||
`日志等级非法: "${logging.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,7 +344,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
issues.push(
|
||||
issue(
|
||||
"invalid-value",
|
||||
"logging.console.level",
|
||||
"server.logging.console.level",
|
||||
`日志等级非法: "${logging.console.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
|
||||
),
|
||||
);
|
||||
@@ -346,7 +354,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
issues.push(
|
||||
issue(
|
||||
"invalid-value",
|
||||
"logging.file.level",
|
||||
"server.logging.file.level",
|
||||
`日志等级非法: "${logging.file.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
|
||||
),
|
||||
);
|
||||
@@ -354,7 +362,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
|
||||
if (logging.file?.path !== undefined) {
|
||||
if (!isString(logging.file.path) || logging.file.path.trim() === "") {
|
||||
issues.push(issue("invalid-value", "logging.file.path", "日志路径不能为空字符串或空白字符串"));
|
||||
issues.push(issue("invalid-value", "server.logging.file.path", "日志路径不能为空字符串或空白字符串"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,11 +371,15 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
try {
|
||||
const bytes = parseSize(rotation.size);
|
||||
if (bytes <= 0) {
|
||||
issues.push(issue("invalid-value", "logging.file.rotation.size", "滚动大小必须为正整数字节数"));
|
||||
issues.push(issue("invalid-value", "server.logging.file.rotation.size", "滚动大小必须为正整数字节数"));
|
||||
}
|
||||
} catch (error) {
|
||||
issues.push(
|
||||
issue("invalid-value", "logging.file.rotation.size", error instanceof Error ? error.message : "size 格式非法"),
|
||||
issue(
|
||||
"invalid-value",
|
||||
"server.logging.file.rotation.size",
|
||||
error instanceof Error ? error.message : "size 格式非法",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -376,7 +388,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
issues.push(
|
||||
issue(
|
||||
"invalid-value",
|
||||
"logging.file.rotation.frequency",
|
||||
"server.logging.file.rotation.frequency",
|
||||
`滚动频率非法: "${rotation.frequency}",支持: ${VALID_ROTATION_FREQUENCIES.join(", ")}`,
|
||||
),
|
||||
);
|
||||
@@ -384,7 +396,7 @@ function validateLoggingConfig(logging: LoggingConfig | undefined, issues: Confi
|
||||
|
||||
if (rotation?.maxFiles !== undefined) {
|
||||
if (!isNumber(rotation.maxFiles) || !Number.isInteger(rotation.maxFiles) || rotation.maxFiles <= 0) {
|
||||
issues.push(issue("invalid-value", "logging.file.rotation.maxFiles", "maxFiles 必须为正整数"));
|
||||
issues.push(issue("invalid-value", "server.logging.file.rotation.maxFiles", "maxFiles 必须为正整数"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,26 +35,8 @@ export function createProbeConfigSchema(checkers: CheckerDefinition[], external
|
||||
return Type.Object(
|
||||
{
|
||||
defaults: Type.Optional(createDefaultsSchema(checkers)),
|
||||
logging: Type.Optional(createLoggingSchema()),
|
||||
runtime: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
maxConcurrentChecks: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
retention: Type.Optional(durationSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
server: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
dataDir: Type.Optional(Type.String()),
|
||||
host: Type.Optional(Type.String()),
|
||||
port: Type.Optional(Type.Integer({ maximum: 65535, minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
probes: Type.Optional(createProbesSchema()),
|
||||
server: Type.Optional(createServerSchema()),
|
||||
targets: Type.Array(external ? createExternalTargetSchema(checkers) : createBaseTargetSchema(checkers), {
|
||||
minItems: 1,
|
||||
}),
|
||||
@@ -144,3 +126,46 @@ function createLoggingSchema(): TSchema {
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
}
|
||||
|
||||
function createProbesSchema(): TSchema {
|
||||
return Type.Object(
|
||||
{
|
||||
execution: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
maxConcurrentChecks: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
}
|
||||
|
||||
function createServerSchema(): TSchema {
|
||||
return Type.Object(
|
||||
{
|
||||
listen: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
host: Type.Optional(Type.String()),
|
||||
port: Type.Optional(Type.Integer({ maximum: 65535, minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
logging: Type.Optional(createLoggingSchema()),
|
||||
storage: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
dataDir: Type.Optional(Type.String()),
|
||||
retention: Type.Optional(durationSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@ export interface DefaultsConfig {
|
||||
timeout?: string;
|
||||
}
|
||||
|
||||
export interface EngineRuntimeConfig {
|
||||
export interface ExecutionConfig {
|
||||
maxConcurrentChecks?: number;
|
||||
retention?: string;
|
||||
}
|
||||
|
||||
export type JsonValue = boolean | JsonValue[] | null | number | string | { [key: string]: JsonValue };
|
||||
@@ -43,13 +42,16 @@ export type LogLevel = "debug" | "error" | "fatal" | "info" | "trace" | "warn";
|
||||
|
||||
export interface ProbeConfig {
|
||||
defaults?: DefaultsConfig;
|
||||
logging?: LoggingConfig;
|
||||
runtime?: EngineRuntimeConfig;
|
||||
probes?: ProbesConfig;
|
||||
server?: ServerConfig;
|
||||
targets: RawTargetConfig[];
|
||||
variables?: Record<string, VariableValue>;
|
||||
}
|
||||
|
||||
export interface ProbesConfig {
|
||||
execution?: ExecutionConfig;
|
||||
}
|
||||
|
||||
export interface RawTargetConfig {
|
||||
[configKey: string]: unknown;
|
||||
description?: null | string;
|
||||
@@ -88,11 +90,21 @@ export interface ResolvedTargetBase {
|
||||
export type RotationFrequency = "daily" | "hourly" | "weekly";
|
||||
|
||||
export interface ServerConfig {
|
||||
dataDir?: string;
|
||||
listen?: ServerListenConfig;
|
||||
logging?: LoggingConfig;
|
||||
storage?: ServerStorageConfig;
|
||||
}
|
||||
|
||||
export interface ServerListenConfig {
|
||||
host?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface ServerStorageConfig {
|
||||
dataDir?: string;
|
||||
retention?: string;
|
||||
}
|
||||
|
||||
export interface StoredCheckResult {
|
||||
duration_ms: null | number;
|
||||
failure: null | string;
|
||||
|
||||
@@ -182,11 +182,14 @@ describe("loadConfig", () => {
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
dataDir: "./my-data"
|
||||
runtime:
|
||||
maxConcurrentChecks: 5
|
||||
listen:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
storage:
|
||||
dataDir: "./my-data"
|
||||
probes:
|
||||
execution:
|
||||
maxConcurrentChecks: 5
|
||||
defaults:
|
||||
interval: "15s"
|
||||
timeout: "5s"
|
||||
@@ -508,7 +511,8 @@ targets:
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
dataDir: ${JSON.stringify(dataDir)}
|
||||
storage:
|
||||
dataDir: ${JSON.stringify(dataDir)}
|
||||
targets:
|
||||
- name: "test"
|
||||
id: "test"
|
||||
@@ -760,7 +764,8 @@ targets:
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
port: 99999
|
||||
listen:
|
||||
port: 99999
|
||||
targets:
|
||||
- name: "t"
|
||||
id: "t"
|
||||
@@ -769,15 +774,16 @@ targets:
|
||||
url: "http://a.com"
|
||||
`,
|
||||
);
|
||||
await expectConfigLoadError(configPath, "server.port 数值范围不合法");
|
||||
await expectConfigLoadError(configPath, "server.listen.port 数值范围不合法");
|
||||
});
|
||||
|
||||
test("非法 maxConcurrentChecks 抛出错误", async () => {
|
||||
const configPath = join(tempDir, "bad-concurrency.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`runtime:
|
||||
maxConcurrentChecks: -1
|
||||
`probes:
|
||||
execution:
|
||||
maxConcurrentChecks: -1
|
||||
targets:
|
||||
- name: "t"
|
||||
id: "t"
|
||||
@@ -786,7 +792,7 @@ targets:
|
||||
url: "http://a.com"
|
||||
`,
|
||||
);
|
||||
await expectConfigLoadError(configPath, "runtime.maxConcurrentChecks 数值范围不合法");
|
||||
await expectConfigLoadError(configPath, "probes.execution.maxConcurrentChecks 数值范围不合法");
|
||||
});
|
||||
|
||||
test("非法 size 格式抛出错误", async () => {
|
||||
@@ -1621,8 +1627,9 @@ targets:
|
||||
const configPath = join(tempDir, "retention-custom.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`runtime:
|
||||
retention: "24h"
|
||||
`server:
|
||||
storage:
|
||||
retention: "24h"
|
||||
targets:
|
||||
- name: "test"
|
||||
id: "test"
|
||||
@@ -1638,8 +1645,9 @@ targets:
|
||||
test("retention 非法格式抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"bad-retention.yaml",
|
||||
`runtime:
|
||||
retention: "7x"
|
||||
`server:
|
||||
storage:
|
||||
retention: "7x"
|
||||
targets:
|
||||
- name: "test"
|
||||
id: "test"
|
||||
@@ -2113,8 +2121,9 @@ targets:
|
||||
const configPath = join(tempDir, "logging-global-level.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
level: "debug"
|
||||
`server:
|
||||
logging:
|
||||
level: "debug"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2131,10 +2140,11 @@ targets:
|
||||
const configPath = join(tempDir, "logging-console-level.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
level: "warn"
|
||||
console:
|
||||
level: "trace"
|
||||
`server:
|
||||
logging:
|
||||
level: "warn"
|
||||
console:
|
||||
level: "trace"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2151,10 +2161,11 @@ targets:
|
||||
const configPath = join(tempDir, "logging-file-level.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
level: "info"
|
||||
file:
|
||||
level: "error"
|
||||
`server:
|
||||
logging:
|
||||
level: "info"
|
||||
file:
|
||||
level: "error"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2171,9 +2182,10 @@ targets:
|
||||
const configPath = join(tempDir, "logging-abs-path.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
file:
|
||||
path: "/var/log/dial/app.log"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
path: "/var/log/dial/app.log"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2189,9 +2201,10 @@ targets:
|
||||
const configPath = join(tempDir, "logging-rel-path.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
file:
|
||||
path: "custom-logs/app.log"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
path: "custom-logs/app.log"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2207,12 +2220,13 @@ targets:
|
||||
const configPath = join(tempDir, "logging-rotation.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100MB"
|
||||
frequency: "hourly"
|
||||
maxFiles: 30
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100MB"
|
||||
frequency: "hourly"
|
||||
maxFiles: 30
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2230,57 +2244,61 @@ targets:
|
||||
test("logging.level 非法等级抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-level.yaml",
|
||||
`logging:
|
||||
level: "verbose"
|
||||
`server:
|
||||
logging:
|
||||
level: "verbose"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.level",
|
||||
"server.logging.level",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.console.level 非法等级抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-console-level.yaml",
|
||||
`logging:
|
||||
console:
|
||||
level: "nope"
|
||||
`server:
|
||||
logging:
|
||||
console:
|
||||
level: "nope"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.console.level",
|
||||
"server.logging.console.level",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.level 非法等级抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-file-level.yaml",
|
||||
`logging:
|
||||
file:
|
||||
level: 123
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
level: 123
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.level",
|
||||
"server.logging.file.level",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.rotation.size 非法格式抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-rotation-size.yaml",
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100TB"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100TB"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2294,50 +2312,130 @@ targets:
|
||||
test("logging.file.rotation.frequency 非法值抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-frequency.yaml",
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
frequency: "monthly"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
frequency: "monthly"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.rotation.frequency",
|
||||
"server.logging.file.rotation.frequency",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.rotation.maxFiles 非整数抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-maxfiles.yaml",
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
maxFiles: 3.5
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
maxFiles: 3.5
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.rotation.maxFiles",
|
||||
"server.logging.file.rotation.maxFiles",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.path 空字符串抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-empty-path.yaml",
|
||||
`logging:
|
||||
file:
|
||||
path: ""
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
path: ""
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.path",
|
||||
"server.logging.file.path",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("旧路径拒绝", () => {
|
||||
test("顶层 runtime 段应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-runtime.yaml",
|
||||
`runtime:
|
||||
maxConcurrentChecks: 10
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"runtime 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("顶层 logging 段应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-logging.yaml",
|
||||
`logging:
|
||||
level: "info"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("server.host 应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-server-host.yaml",
|
||||
`server:
|
||||
host: "0.0.0.0"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"host 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("server.port 应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-server-port.yaml",
|
||||
`server:
|
||||
port: 8080
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"port 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("server.dataDir 应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-server-datadir.yaml",
|
||||
`server:
|
||||
dataDir: "/tmp/data"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"dataDir 是未知字段",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,9 +220,9 @@ describe("config variables", () => {
|
||||
expect(typeof http2["ignoreSSL"]).toBe("boolean");
|
||||
});
|
||||
|
||||
test("runtime 段不替换", () => {
|
||||
test("server.storage 段不替换", () => {
|
||||
const result = resolveVariables({
|
||||
runtime: { maxConcurrentChecks: 10, retention: "${retention}" },
|
||||
server: { storage: { retention: "${retention}" } },
|
||||
targets: [
|
||||
{
|
||||
http: { url: "${host}" },
|
||||
@@ -234,8 +234,44 @@ describe("config variables", () => {
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { runtime: { retention: string } };
|
||||
expect(config.runtime.retention).toBe("${retention}");
|
||||
const config = result.config as { server: { storage: { retention: string } } };
|
||||
expect(config.server.storage.retention).toBe("${retention}");
|
||||
});
|
||||
|
||||
test("probes 段不替换", () => {
|
||||
const result = resolveVariables({
|
||||
probes: { execution: { maxConcurrentChecks: "${maxConcurrentChecks}" } },
|
||||
targets: [
|
||||
{
|
||||
http: { url: "${host}" },
|
||||
id: "probes-no-replace",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
variables: { host: "https://example.com", maxConcurrentChecks: "20" },
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { probes: { execution: { maxConcurrentChecks: string } } };
|
||||
expect(config.probes.execution.maxConcurrentChecks).toBe("${maxConcurrentChecks}");
|
||||
});
|
||||
|
||||
test("server.logging 段不替换", () => {
|
||||
const result = resolveVariables({
|
||||
server: { logging: { level: "${logLevel}" } },
|
||||
targets: [
|
||||
{
|
||||
http: { url: "${host}" },
|
||||
id: "logging-no-replace",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
variables: { host: "https://example.com", logLevel: "debug" },
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { server: { logging: { level: string } } };
|
||||
expect(config.server.logging.level).toBe("${logLevel}");
|
||||
});
|
||||
|
||||
test("variables 段为非对象时报错", () => {
|
||||
|
||||
Reference in New Issue
Block a user