From c120690cf1c0b3c6ed19444af64b3b390a094f80 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Mon, 25 May 2026 17:48:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20target=20=E6=97=B6=E9=97=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=A0=A1=E9=AA=8C=EF=BC=8Cinterval=20=E6=9C=80?= =?UTF-8?q?=E5=B0=8F=2010s=EF=BC=8Ctimeout=20=E4=B8=8D=E5=A4=A7=E4=BA=8E?= =?UTF-8?q?=20interval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在配置加载阶段新增通用 target 时间字段语义校验: - interval 解析后不得小于 10s - timeout 解析后不得大于同一 target 的 interval - 默认值(30s / 10s)参与校验 - 变量引用先解析再校验 - 格式错误优先于关系错误,避免级联提示 --- docs/user/configuration.md | 12 +- src/server/checker/config-loader.ts | 37 ++++-- tests/server/checker/config-loader.test.ts | 138 +++++++++++++++++++++ 3 files changed, 169 insertions(+), 18 deletions(-) diff --git a/docs/user/configuration.md b/docs/user/configuration.md index e8fe6fa..ff8ef4f 100644 --- a/docs/user/configuration.md +++ b/docs/user/configuration.md @@ -80,10 +80,10 @@ targets: ## 内置默认值 -| 字段 | 默认值 | -| ---------- | ------ | -| `interval` | `30s` | -| `timeout` | `10s` | +| 字段 | 默认值 | 约束 | +| ---------- | ------ | ----------------------- | +| `interval` | `30s` | 最小 `10s` | +| `timeout` | `10s` | 必须小于等于 `interval` | 各 checker 专属默认值见 [Checker 参考](checkers/README.md)。 @@ -123,8 +123,8 @@ targets: | `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | 无 | | `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`dns`、`icmp`、`llm`、`ws` | 是 | 无 | | `group` | 分组名称 | 否 | `default` | -| `interval` | 拨测间隔 | 否 | `30s` | -| `timeout` | 超时时间 | 否 | `10s` | +| `interval` | 拨测间隔,最小 `10s` | 否 | `30s` | +| `timeout` | 超时时间,必须小于等于 `interval` | 否 | `10s` | ## Checker 专属配置 diff --git a/src/server/checker/config-loader.ts b/src/server/checker/config-loader.ts index a9bfd5b..fcbdd4e 100644 --- a/src/server/checker/config-loader.ts +++ b/src/server/checker/config-loader.ts @@ -32,6 +32,8 @@ const DEFAULT_ROTATION_SIZE = "50MB"; const DEFAULT_ROTATION_FREQUENCY: RotationFrequency = "daily"; const DEFAULT_ROTATION_MAX_FILES = 14; +const MINIMUM_INTERVAL_MS = parseDuration("10s"); + const VALID_LOG_LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"]; const VALID_ROTATION_FREQUENCIES: RotationFrequency[] = ["hourly", "daily", "weekly"]; @@ -208,6 +210,14 @@ function resolveTarget( return result; } +function tryParseDuration(value: string): null | number { + try { + return parseDuration(value); + } catch { + return null; + } +} + function validateConfig(config: NormalizedProbeConfig): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; if (!Array.isArray(config.targets) || config.targets.length === 0) { @@ -291,18 +301,21 @@ function validateConfig(config: NormalizedProbeConfig): ConfigValidationIssue[] : isString(targetIdValue) ? targetIdValue : undefined; - validateDurationValue( - isString(targetRecord["interval"]) ? targetRecord["interval"] : undefined, - `targets[${i}].interval`, - issues, - targetName, - ); - validateDurationValue( - isString(targetRecord["timeout"]) ? targetRecord["timeout"] : undefined, - `targets[${i}].timeout`, - issues, - targetName, - ); + const intervalRaw = isString(targetRecord["interval"]) ? targetRecord["interval"] : undefined; + const timeoutRaw = isString(targetRecord["timeout"]) ? targetRecord["timeout"] : undefined; + validateDurationValue(intervalRaw, `targets[${i}].interval`, issues, targetName); + validateDurationValue(timeoutRaw, `targets[${i}].timeout`, issues, targetName); + + const intervalMs = tryParseDuration(intervalRaw ?? DEFAULT_INTERVAL); + const timeoutMs = tryParseDuration(timeoutRaw ?? DEFAULT_TIMEOUT); + + if (intervalMs !== null && intervalMs < MINIMUM_INTERVAL_MS) { + issues.push(issue("invalid-value", `targets[${i}].interval`, "interval 不能小于 10s", targetName)); + } + + if (intervalMs !== null && timeoutMs !== null && timeoutMs > intervalMs) { + issues.push(issue("invalid-value", `targets[${i}].timeout`, "timeout 不能大于 interval", targetName)); + } } return issues; diff --git a/tests/server/checker/config-loader.test.ts b/tests/server/checker/config-loader.test.ts index 2ee89e1..a7e06b8 100644 --- a/tests/server/checker/config-loader.test.ts +++ b/tests/server/checker/config-loader.test.ts @@ -886,6 +886,144 @@ targets: await expectConfigLoadError(configPath, "无效的时长格式"); }); + test("interval 小于 10s 抛出错误", async () => { + const configPath = join(tempDir, "interval-too-small.yaml"); + await writeFile( + configPath, + `targets: + - name: "t" + id: "t" + type: http + interval: "9s" + http: + url: "http://a.com" +`, + ); + await expectConfigLoadError(configPath, "interval 不能小于 10s"); + }); + + test("interval 9999ms 抛出错误", async () => { + const configPath = join(tempDir, "interval-9999ms.yaml"); + await writeFile( + configPath, + `targets: + - name: "t" + id: "t" + type: http + interval: "9999ms" + http: + url: "http://a.com" +`, + ); + await expectConfigLoadError(configPath, "interval 不能小于 10s"); + }); + + test("interval 10s 通过", async () => { + const configPath = join(tempDir, "interval-10s.yaml"); + await writeFile( + configPath, + `targets: + - name: "t" + id: "t" + type: http + interval: "10s" + http: + url: "http://a.com" +`, + ); + const config = await loadConfig(configPath); + expect(config.targets[0]!.intervalMs).toBe(10000); + }); + + test("interval 10000ms 通过", async () => { + const configPath = join(tempDir, "interval-10000ms.yaml"); + await writeFile( + configPath, + `targets: + - name: "t" + id: "t" + type: http + interval: "10000ms" + http: + url: "http://a.com" +`, + ); + const config = await loadConfig(configPath); + expect(config.targets[0]!.intervalMs).toBe(10000); + }); + + test("timeout 大于 interval 抛出错误", async () => { + const configPath = join(tempDir, "timeout-gt-interval.yaml"); + await writeFile( + configPath, + `targets: + - name: "t" + id: "t" + type: http + interval: "10s" + timeout: "30s" + http: + url: "http://a.com" +`, + ); + await expectConfigLoadError(configPath, "timeout 不能大于 interval"); + }); + + test("timeout 等于 interval 通过", async () => { + const configPath = join(tempDir, "timeout-eq-interval.yaml"); + await writeFile( + configPath, + `targets: + - name: "t" + id: "t" + type: http + interval: "30s" + timeout: "30s" + http: + url: "http://a.com" +`, + ); + const config = await loadConfig(configPath); + expect(config.targets[0]!.intervalMs).toBe(30000); + expect(config.targets[0]!.timeoutMs).toBe(30000); + }); + + test("变量解析后 interval 小于 10s 抛出错误", async () => { + const configPath = join(tempDir, "var-interval-too-small.yaml"); + await writeFile( + configPath, + `variables: + check_interval: "5s" +targets: + - name: "t" + id: "t" + type: http + interval: "\${check_interval}" + http: + url: "http://a.com" +`, + ); + await expectConfigLoadError(configPath, "interval 不能小于 10s"); + }); + + test("变量解析后 timeout 大于 interval 抛出错误", async () => { + const configPath = join(tempDir, "var-timeout-gt-interval.yaml"); + await writeFile( + configPath, + `variables: + check_timeout: "60s" +targets: + - name: "t" + id: "t" + type: http + timeout: "\${check_timeout}" + http: + url: "http://a.com" +`, + ); + await expectConfigLoadError(configPath, "timeout 不能大于 interval"); + }); + test("解析 expect 配置", async () => { const configPath = join(tempDir, "expect.yaml"); await writeFile(