refactor: ICMP checker type 从 ping 统一改为 icmp,修复前端 UI 细节
- ICMP checker 的 type/configKey/YAML 配置键/接口属性名从 ping 改为 icmp - IcmpChecker 添加 platform 构造函数注入,修复 Windows 测试兼容性 - 前端 target 表格延迟列优化:标题简化为「延迟」,单位下移到单元格,宽度 80px - Drawer 概览页 Descriptions 添加 tableLayout=auto 收窄 label 宽度 - 同步更新 README.md、DEVELOPMENT.md、probes.example.yaml、JSON Schema 和全部测试
This commit is contained in:
@@ -5,14 +5,14 @@ export function buildPingCommand(t: ResolvedPingTarget, platform: NodeJS.Platfor
|
||||
return [
|
||||
"ping",
|
||||
"-n",
|
||||
String(t.ping.count),
|
||||
String(t.icmp.count),
|
||||
"-l",
|
||||
String(t.ping.packetSize),
|
||||
String(t.icmp.packetSize),
|
||||
"-w",
|
||||
String(t.timeoutMs),
|
||||
t.ping.host,
|
||||
t.icmp.host,
|
||||
];
|
||||
}
|
||||
const timeout = platform === "linux" ? String(Math.ceil(t.timeoutMs / 1000)) : String(t.timeoutMs);
|
||||
return ["ping", "-c", String(t.ping.count), "-s", String(t.ping.packetSize), "-W", timeout, t.ping.host];
|
||||
return ["ping", "-c", String(t.icmp.count), "-s", String(t.icmp.packetSize), "-W", timeout, t.icmp.host];
|
||||
}
|
||||
|
||||
@@ -16,11 +16,17 @@ const DEFAULT_COUNT = 3;
|
||||
const DEFAULT_PACKET_SIZE = 56;
|
||||
|
||||
export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
readonly configKey = "ping";
|
||||
readonly configKey = "icmp";
|
||||
|
||||
readonly platform: NodeJS.Platform;
|
||||
|
||||
readonly schemas = icmpCheckerSchemas;
|
||||
|
||||
readonly type = "ping";
|
||||
readonly type = "icmp";
|
||||
|
||||
constructor(platform: NodeJS.Platform = process.platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
buildDetail(observation: Record<string, unknown>): null | string {
|
||||
const alive = observation["alive"];
|
||||
@@ -67,7 +73,7 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: errorFailure("ping", "spawn", `ping 命令不可用: ${isError(error) ? error.message : String(error)}`),
|
||||
failure: errorFailure("icmp", "spawn", `icmp 命令不可用: ${isError(error) ? error.message : String(error)}`),
|
||||
matched: false,
|
||||
observation: null,
|
||||
targetId: t.id,
|
||||
@@ -95,7 +101,7 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: errorFailure("ping", "timeout", `ping 执行超时 (${t.timeoutMs}ms)`),
|
||||
failure: errorFailure("icmp", "timeout", `icmp 执行超时 (${t.timeoutMs}ms)`),
|
||||
matched: false,
|
||||
observation: null,
|
||||
targetId: t.id,
|
||||
@@ -103,12 +109,12 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
};
|
||||
}
|
||||
|
||||
const stats = parsePingOutput(stdout, process.platform);
|
||||
const stats = parsePingOutput(stdout, this.platform);
|
||||
if (!stats) {
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: errorFailure("ping", "parse", "无法解析 ping 输出"),
|
||||
failure: errorFailure("icmp", "parse", "无法解析 icmp 输出"),
|
||||
matched: false,
|
||||
observation: {
|
||||
alive: false,
|
||||
@@ -148,28 +154,28 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
|
||||
const t = target as RawTargetConfig & { ping: PingTargetConfig; type: "ping" };
|
||||
const t = target as RawTargetConfig & { icmp: PingTargetConfig; type: "icmp" };
|
||||
return {
|
||||
description: null,
|
||||
expect: target.expect as PingExpectConfig | undefined,
|
||||
group: target.group ?? "default",
|
||||
icmp: {
|
||||
count: t.icmp.count ?? DEFAULT_COUNT,
|
||||
host: t.icmp.host,
|
||||
packetSize: t.icmp.packetSize ?? DEFAULT_PACKET_SIZE,
|
||||
},
|
||||
id: t.id,
|
||||
intervalMs: context.defaultIntervalMs,
|
||||
name: t.name ?? null,
|
||||
ping: {
|
||||
count: t.ping.count ?? DEFAULT_COUNT,
|
||||
host: t.ping.host,
|
||||
packetSize: t.ping.packetSize ?? DEFAULT_PACKET_SIZE,
|
||||
},
|
||||
timeoutMs: context.defaultTimeoutMs,
|
||||
type: "ping",
|
||||
type: "icmp",
|
||||
} satisfies ResolvedPingTarget;
|
||||
}
|
||||
|
||||
serialize(t: ResolvedPingTarget): { config: string; target: string } {
|
||||
return {
|
||||
config: JSON.stringify(t.ping),
|
||||
target: `ping ${t.ping.host}`,
|
||||
config: JSON.stringify(t.icmp),
|
||||
target: `icmp ${t.icmp.host}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export function checkAlive(actual: boolean, expected: boolean): ExpectResult {
|
||||
"alive",
|
||||
expected,
|
||||
actual,
|
||||
expected ? "期望主机可达但 ping 不可达" : "期望主机不可达但 ping 可达",
|
||||
expected ? "期望主机可达但 icmp 不可达" : "期望主机不可达但 icmp 可达",
|
||||
),
|
||||
matched: false,
|
||||
};
|
||||
|
||||
@@ -34,9 +34,9 @@ export interface ResolvedPingConfig {
|
||||
export interface ResolvedPingTarget extends ResolvedTargetBase {
|
||||
expect?: PingExpectConfig;
|
||||
group: string;
|
||||
icmp: ResolvedPingConfig;
|
||||
intervalMs: number;
|
||||
name: null | string;
|
||||
ping: ResolvedPingConfig;
|
||||
timeoutMs: number;
|
||||
type: "ping";
|
||||
type: "icmp";
|
||||
}
|
||||
|
||||
@@ -10,15 +10,15 @@ import { issue, joinPath } from "../../schema/issues";
|
||||
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
|
||||
const defaults = input.defaults["ping"];
|
||||
const defaults = input.defaults["icmp"];
|
||||
if (defaults !== undefined && defaults !== null) {
|
||||
const targetName = "defaults.ping";
|
||||
const targetName = "defaults.icmp";
|
||||
if (!isPlainObject(defaults)) {
|
||||
issues.push(issue("invalid-type", "defaults.ping", "必须为对象", targetName));
|
||||
issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName));
|
||||
} else {
|
||||
const pingDefaults = defaults as Record<string, unknown>;
|
||||
for (const key of Object.keys(pingDefaults)) {
|
||||
issues.push(issue("unknown-field", joinPath("defaults.ping", key), "是未知字段", targetName));
|
||||
const icmpDefaults = defaults as Record<string, unknown>;
|
||||
for (const key of Object.keys(icmpDefaults)) {
|
||||
issues.push(issue("unknown-field", joinPath("defaults.icmp", key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export function validatePingConfig(input: CheckerValidationInput): ConfigValidat
|
||||
const target = input.targets[i] as unknown;
|
||||
if (!isPlainObject(target)) continue;
|
||||
const targetRecord = target as Record<string, unknown>;
|
||||
if (targetRecord["type"] !== "ping") continue;
|
||||
if (targetRecord["type"] !== "icmp") continue;
|
||||
issues.push(...validatePingTarget(targetRecord, `targets[${i}]`));
|
||||
}
|
||||
|
||||
@@ -71,39 +71,39 @@ function validatePingExpect(target: Record<string, unknown>, path: string): Conf
|
||||
function validatePingTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const targetName = getTargetName(target);
|
||||
const rawPing = target["ping"];
|
||||
const rawIcmp = target["icmp"];
|
||||
|
||||
if (!isPlainObject(rawPing)) {
|
||||
issues.push(issue("required", joinPath(path, "ping"), "缺少 ping 配置分组", targetName));
|
||||
if (!isPlainObject(rawIcmp)) {
|
||||
issues.push(issue("required", joinPath(path, "icmp"), "缺少 icmp 配置分组", targetName));
|
||||
issues.push(...validatePingExpect(target, path));
|
||||
return issues;
|
||||
}
|
||||
const ping = rawPing as Record<string, unknown>;
|
||||
const icmp = rawIcmp as Record<string, unknown>;
|
||||
|
||||
if (!isString(ping["host"]) || ping["host"].trim() === "") {
|
||||
issues.push(issue("required", joinPath(joinPath(path, "ping"), "host"), "缺少 ping.host 字段", targetName));
|
||||
if (!isString(icmp["host"]) || icmp["host"].trim() === "") {
|
||||
issues.push(issue("required", joinPath(joinPath(path, "icmp"), "host"), "缺少 icmp.host 字段", targetName));
|
||||
}
|
||||
if (ping["count"] !== undefined) {
|
||||
const count = ping["count"];
|
||||
if (icmp["count"] !== undefined) {
|
||||
const count = icmp["count"];
|
||||
if (!isNumber(count) || !Number.isInteger(count) || count < 1 || count > 100) {
|
||||
issues.push(
|
||||
issue("invalid-value", joinPath(joinPath(path, "ping"), "count"), "必须为 1-100 的正整数", targetName),
|
||||
issue("invalid-value", joinPath(joinPath(path, "icmp"), "count"), "必须为 1-100 的正整数", targetName),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ping["packetSize"] !== undefined) {
|
||||
const packetSize = ping["packetSize"];
|
||||
if (icmp["packetSize"] !== undefined) {
|
||||
const packetSize = icmp["packetSize"];
|
||||
if (!isNumber(packetSize) || !Number.isInteger(packetSize) || packetSize < 1 || packetSize > 65500) {
|
||||
issues.push(
|
||||
issue("invalid-value", joinPath(joinPath(path, "ping"), "packetSize"), "必须为 1-65500 的正整数", targetName),
|
||||
issue("invalid-value", joinPath(joinPath(path, "icmp"), "packetSize"), "必须为 1-65500 的正整数", targetName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const allowedPingKeys = new Set(["count", "host", "packetSize"]);
|
||||
for (const key of Object.keys(ping)) {
|
||||
if (!allowedPingKeys.has(key)) {
|
||||
issues.push(issue("unknown-field", joinPath(joinPath(path, "ping"), key), "是未知字段", targetName));
|
||||
const allowedIcmpKeys = new Set(["count", "host", "packetSize"]);
|
||||
for (const key of Object.keys(icmp)) {
|
||||
if (!allowedIcmpKeys.has(key)) {
|
||||
issues.push(issue("unknown-field", joinPath(joinPath(path, "icmp"), key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export function OverviewTab({ metricsData, metricsLoading, target }: OverviewTab
|
||||
{ content: target.latestCheck?.detail ?? "-", label: "状态详情" },
|
||||
{ content: target.description ?? "", label: "描述", span: 2 },
|
||||
]}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
|
||||
<Divider align="left">统计</Divider>
|
||||
|
||||
@@ -89,13 +89,13 @@ export function createTargetTableColumns(checkerTypes: string[]): Array<PrimaryT
|
||||
if (ms === null || ms === undefined) return <span className="text-disabled">-</span>;
|
||||
const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error";
|
||||
const latencyText = ms > 9999 ? "9999+" : `${Math.round(ms)}`;
|
||||
return <span className={`${colorClass} latency-value tabular-nums`}>{latencyText}</span>;
|
||||
return <span className={`${colorClass} latency-value tabular-nums`}>{latencyText} ms</span>;
|
||||
},
|
||||
colKey: "latestCheck.durationMs",
|
||||
sorter: latencySorter,
|
||||
sortType: "all",
|
||||
title: "延迟(ms)",
|
||||
width: 75,
|
||||
title: "延迟",
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user