refactor: 后端架构加固 — 泛型化、批量查询、bootstrap 统一、路径修复与 pageSize 上限
- CheckerDefinition 泛型化,HTTP/Command checker 移除 resolved target 断言 - 新增 ProbeStore.getAllRecentSamples 消除 targets 路由 N+1 查询 - 统一 getAllTargetStats 与 getTargetStats 的 availability 精度 - Engine rejected 结果写入 internal error 记录,提升可观测性 - 新增 bootstrap.ts 统一 dev/production 启动序列 - dataDir 相对路径改为基于配置文件目录解析 - validatePagination 增加 pageSize 上限 200 校验 - 修复 ErrorBoundary override 标记 - 更新 README/DEVELOPMENT 文档,新增完整测试覆盖
This commit is contained in:
@@ -67,7 +67,7 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
||||
|
||||
const host = server.host ?? DEFAULT_HOST;
|
||||
const port = server.port ?? DEFAULT_PORT;
|
||||
const dataDir = server.dataDir ?? DEFAULT_DATA_DIR;
|
||||
const dataDir = resolve(configDir, server.dataDir ?? DEFAULT_DATA_DIR);
|
||||
|
||||
const maxConcurrentChecks = resolveMaxConcurrentChecks(runtime);
|
||||
const retentionMs = resolveRetention(runtime);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { groupBy, Semaphore } from "es-toolkit";
|
||||
import { groupBy, isError, Semaphore } from "es-toolkit";
|
||||
|
||||
import type { ProbeStore } from "./store";
|
||||
import type { CheckResult, ResolvedTargetBase } from "./types";
|
||||
|
||||
import { errorFailure } from "./expect/failure";
|
||||
import { checkerRegistry } from "./runner";
|
||||
|
||||
const PRUNE_INTERVAL_MS = 3600000;
|
||||
@@ -64,11 +65,21 @@ export class ProbeEngine {
|
||||
}),
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
for (const [index, result] of results.entries()) {
|
||||
if (result.status === "fulfilled") {
|
||||
this.writeResult(result.value);
|
||||
} else {
|
||||
const target = targets[index];
|
||||
console.warn("探针执行失败:", result.reason);
|
||||
if (!target) continue;
|
||||
this.writeResult({
|
||||
durationMs: null,
|
||||
failure: errorFailure("internal", "engine", formatReason(result.reason)),
|
||||
matched: false,
|
||||
statusDetail: null,
|
||||
targetName: target.name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,3 +117,7 @@ export class ProbeEngine {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatReason(reason: unknown): string {
|
||||
return isError(reason) ? reason.message : String(reason);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isError } from "es-toolkit";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../types";
|
||||
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { CheckResult, RawTargetConfig } from "../../types";
|
||||
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { CommandExpectConfig, CommandTargetConfig, ResolvedCommandTarget } from "./types";
|
||||
|
||||
import { checkDuration } from "../../expect/duration";
|
||||
@@ -13,15 +13,14 @@ import { commandCheckerSchemas } from "./schema";
|
||||
import { checkTextRules } from "./text";
|
||||
import { validateCommandConfig } from "./validate";
|
||||
|
||||
export class CommandChecker implements Checker {
|
||||
export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget> {
|
||||
readonly configKey = "command";
|
||||
|
||||
readonly schemas = commandCheckerSchemas;
|
||||
|
||||
readonly type = "command";
|
||||
|
||||
async execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const t = target as ResolvedCommandTarget;
|
||||
async execute(t: ResolvedCommandTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const start = performance.now();
|
||||
|
||||
@@ -169,7 +168,7 @@ export class CommandChecker implements Checker {
|
||||
};
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase {
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
|
||||
const t = target as RawTargetConfig & { command: CommandTargetConfig; type: "command" };
|
||||
const commandDefaults = context.defaults["command"] as undefined | { cwd?: string; maxOutputBytes?: string };
|
||||
|
||||
@@ -197,8 +196,7 @@ export class CommandChecker implements Checker {
|
||||
} satisfies ResolvedCommandTarget;
|
||||
}
|
||||
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string } {
|
||||
const t = target as ResolvedCommandTarget;
|
||||
serialize(t: ResolvedCommandTarget): { config: string; target: string } {
|
||||
const parts = [t.command.exec, ...t.command.args];
|
||||
return {
|
||||
config: JSON.stringify({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isError } from "es-toolkit";
|
||||
|
||||
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../types";
|
||||
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { CheckResult, RawTargetConfig } from "../../types";
|
||||
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { HttpExpectConfig, HttpTargetConfig, ResolvedHttpTarget } from "./types";
|
||||
|
||||
import { checkDuration } from "../../expect/duration";
|
||||
@@ -16,15 +16,14 @@ const CHARSET_RE = /charset="?([^";\s]+)"?/i;
|
||||
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
||||
const SENSITIVE_HEADERS = new Set(["authorization", "cookie"]);
|
||||
|
||||
export class HttpChecker implements Checker {
|
||||
export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
|
||||
readonly configKey = "http";
|
||||
|
||||
readonly schemas = httpCheckerSchemas;
|
||||
|
||||
readonly type = "http";
|
||||
|
||||
async execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const t = target as ResolvedHttpTarget;
|
||||
async execute(t: ResolvedHttpTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const expect = t.expect;
|
||||
const start = performance.now();
|
||||
@@ -117,7 +116,7 @@ export class HttpChecker implements Checker {
|
||||
}
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase {
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedHttpTarget {
|
||||
const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" };
|
||||
const httpDefaults = context.defaults["http"] as
|
||||
| undefined
|
||||
@@ -145,8 +144,7 @@ export class HttpChecker implements Checker {
|
||||
} satisfies ResolvedHttpTarget;
|
||||
}
|
||||
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string } {
|
||||
const t = target as ResolvedHttpTarget;
|
||||
serialize(t: ResolvedHttpTarget): { config: string; target: string } {
|
||||
return {
|
||||
config: JSON.stringify({
|
||||
body: t.http.body,
|
||||
|
||||
@@ -3,18 +3,18 @@ import type { TSchema } from "@sinclair/typebox";
|
||||
import type { ConfigValidationIssue } from "../schema/issues";
|
||||
import type { CheckResult, DefaultsConfig, RawTargetConfig, ResolvedTargetBase } from "../types";
|
||||
|
||||
export type Checker = CheckerDefinition;
|
||||
export type Checker<TResolved extends ResolvedTargetBase = ResolvedTargetBase> = CheckerDefinition<TResolved>;
|
||||
|
||||
export interface CheckerContext {
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface CheckerDefinition {
|
||||
export interface CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase> {
|
||||
readonly configKey: string;
|
||||
execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult>;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase;
|
||||
execute(target: TResolved, ctx: CheckerContext): Promise<CheckResult>;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): TResolved;
|
||||
readonly schemas: CheckerSchemas;
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string };
|
||||
serialize(target: TResolved): { config: string; target: string };
|
||||
readonly type: string;
|
||||
validate(input: CheckerValidationInput): ConfigValidationIssue[];
|
||||
}
|
||||
|
||||
@@ -57,6 +57,40 @@ export class ProbeStore {
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
getAllRecentSamples(
|
||||
limit: number,
|
||||
): Map<number, Array<{ duration_ms: null | number; matched: number; timestamp: string }>> {
|
||||
const rows = this.db
|
||||
.query(
|
||||
`SELECT target_id, timestamp, duration_ms, matched
|
||||
FROM (
|
||||
SELECT
|
||||
target_id,
|
||||
timestamp,
|
||||
duration_ms,
|
||||
matched,
|
||||
ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) as row_num
|
||||
FROM check_results
|
||||
)
|
||||
WHERE row_num <= ?
|
||||
ORDER BY target_id, timestamp DESC`,
|
||||
)
|
||||
.all(limit) as Array<{
|
||||
duration_ms: null | number;
|
||||
matched: number;
|
||||
target_id: number;
|
||||
timestamp: string;
|
||||
}>;
|
||||
|
||||
const result = new Map<number, Array<{ duration_ms: null | number; matched: number; timestamp: string }>>();
|
||||
for (const row of rows) {
|
||||
const samples = result.get(row.target_id) ?? [];
|
||||
samples.push({ duration_ms: row.duration_ms, matched: row.matched, timestamp: row.timestamp });
|
||||
result.set(row.target_id, samples);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getAllTargetStats(): Map<number, { availability: number; totalChecks: number }> {
|
||||
const rows = this.db
|
||||
.query(
|
||||
@@ -69,7 +103,7 @@ export class ProbeStore {
|
||||
|
||||
const result = new Map<number, { availability: number; totalChecks: number }>();
|
||||
for (const row of rows) {
|
||||
const availability = row.totalChecks > 0 ? Math.round((row.upCount / row.totalChecks) * 10000) / 100 : 0;
|
||||
const availability = row.totalChecks > 0 ? Math.round((row.upCount / row.totalChecks) * 100 * 100) / 100 : 0;
|
||||
result.set(row.target_id, { availability, totalChecks: row.totalChecks });
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user