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

@@ -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 必须为正整数"));
}
}
}

View File

@@ -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 },
);
}

View File

@@ -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;