feat: target 时间配置校验,interval 最小 10s,timeout 不大于 interval
在配置加载阶段新增通用 target 时间字段语义校验: - interval 解析后不得小于 10s - timeout 解析后不得大于同一 target 的 interval - 默认值(30s / 10s)参与校验 - 变量引用先解析再校验 - 格式错误优先于关系错误,避免级联提示
This commit is contained in:
@@ -80,10 +80,10 @@ targets:
|
|||||||
|
|
||||||
## 内置默认值
|
## 内置默认值
|
||||||
|
|
||||||
| 字段 | 默认值 |
|
| 字段 | 默认值 | 约束 |
|
||||||
| ---------- | ------ |
|
| ---------- | ------ | ----------------------- |
|
||||||
| `interval` | `30s` |
|
| `interval` | `30s` | 最小 `10s` |
|
||||||
| `timeout` | `10s` |
|
| `timeout` | `10s` | 必须小于等于 `interval` |
|
||||||
|
|
||||||
各 checker 专属默认值见 [Checker 参考](checkers/README.md)。
|
各 checker 专属默认值见 [Checker 参考](checkers/README.md)。
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ targets:
|
|||||||
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | 无 |
|
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | 无 |
|
||||||
| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`dns`、`icmp`、`llm`、`ws` | 是 | 无 |
|
| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`dns`、`icmp`、`llm`、`ws` | 是 | 无 |
|
||||||
| `group` | 分组名称 | 否 | `default` |
|
| `group` | 分组名称 | 否 | `default` |
|
||||||
| `interval` | 拨测间隔 | 否 | `30s` |
|
| `interval` | 拨测间隔,最小 `10s` | 否 | `30s` |
|
||||||
| `timeout` | 超时时间 | 否 | `10s` |
|
| `timeout` | 超时时间,必须小于等于 `interval` | 否 | `10s` |
|
||||||
|
|
||||||
## Checker 专属配置
|
## Checker 专属配置
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ const DEFAULT_ROTATION_SIZE = "50MB";
|
|||||||
const DEFAULT_ROTATION_FREQUENCY: RotationFrequency = "daily";
|
const DEFAULT_ROTATION_FREQUENCY: RotationFrequency = "daily";
|
||||||
const DEFAULT_ROTATION_MAX_FILES = 14;
|
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_LOG_LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"];
|
||||||
const VALID_ROTATION_FREQUENCIES: RotationFrequency[] = ["hourly", "daily", "weekly"];
|
const VALID_ROTATION_FREQUENCIES: RotationFrequency[] = ["hourly", "daily", "weekly"];
|
||||||
|
|
||||||
@@ -208,6 +210,14 @@ function resolveTarget(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tryParseDuration(value: string): null | number {
|
||||||
|
try {
|
||||||
|
return parseDuration(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateConfig(config: NormalizedProbeConfig): ConfigValidationIssue[] {
|
function validateConfig(config: NormalizedProbeConfig): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
if (!Array.isArray(config.targets) || config.targets.length === 0) {
|
if (!Array.isArray(config.targets) || config.targets.length === 0) {
|
||||||
@@ -291,18 +301,21 @@ function validateConfig(config: NormalizedProbeConfig): ConfigValidationIssue[]
|
|||||||
: isString(targetIdValue)
|
: isString(targetIdValue)
|
||||||
? targetIdValue
|
? targetIdValue
|
||||||
: undefined;
|
: undefined;
|
||||||
validateDurationValue(
|
const intervalRaw = isString(targetRecord["interval"]) ? targetRecord["interval"] : undefined;
|
||||||
isString(targetRecord["interval"]) ? targetRecord["interval"] : undefined,
|
const timeoutRaw = isString(targetRecord["timeout"]) ? targetRecord["timeout"] : undefined;
|
||||||
`targets[${i}].interval`,
|
validateDurationValue(intervalRaw, `targets[${i}].interval`, issues, targetName);
|
||||||
issues,
|
validateDurationValue(timeoutRaw, `targets[${i}].timeout`, issues, targetName);
|
||||||
targetName,
|
|
||||||
);
|
const intervalMs = tryParseDuration(intervalRaw ?? DEFAULT_INTERVAL);
|
||||||
validateDurationValue(
|
const timeoutMs = tryParseDuration(timeoutRaw ?? DEFAULT_TIMEOUT);
|
||||||
isString(targetRecord["timeout"]) ? targetRecord["timeout"] : undefined,
|
|
||||||
`targets[${i}].timeout`,
|
if (intervalMs !== null && intervalMs < MINIMUM_INTERVAL_MS) {
|
||||||
issues,
|
issues.push(issue("invalid-value", `targets[${i}].interval`, "interval 不能小于 10s", targetName));
|
||||||
targetName,
|
}
|
||||||
);
|
|
||||||
|
if (intervalMs !== null && timeoutMs !== null && timeoutMs > intervalMs) {
|
||||||
|
issues.push(issue("invalid-value", `targets[${i}].timeout`, "timeout 不能大于 interval", targetName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues;
|
return issues;
|
||||||
|
|||||||
@@ -886,6 +886,144 @@ targets:
|
|||||||
await expectConfigLoadError(configPath, "无效的时长格式");
|
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 () => {
|
test("解析 expect 配置", async () => {
|
||||||
const configPath = join(tempDir, "expect.yaml");
|
const configPath = join(tempDir, "expect.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
|
|||||||
Reference in New Issue
Block a user