1
0

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:
2026-05-21 13:54:41 +08:00
parent 5238dbe77d
commit e448cb4654
14 changed files with 614 additions and 376 deletions

View File

@@ -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 是未知字段",
);
});
});

View File

@@ -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 段为非对象时报错", () => {