feat: 新增 ICMP/Ping checker,支持跨平台主机存活检测与延迟监控
实现 type: ping checker,通过 Bun.spawn 调用系统 ping 命令,自行实现跨平台 输出解析器(Linux/macOS/Windows 含中文 locale),支持 alive、丢包率、延迟、 耗时等 expect 断言,复用现有 checker 架构零外部依赖。 包含完整的类型定义、TypeBox schema、语义校验、命令构建、解析、断言、执行、 注册、配置加载测试,以及 probe-config.schema.json 更新和文档更新。 审查修复:提取 buildPingCommand 为独立纯函数并补充跨平台单测,补充 maxDurationMs/maxAvgLatencyMs 类型非法和空字符串 host 边界测试用例。 变更已归档,delta specs 已同步至 main specs。
This commit is contained in:
18
src/server/checker/runner/icmp/command.ts
Normal file
18
src/server/checker/runner/icmp/command.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ResolvedPingTarget } from "./types";
|
||||
|
||||
export function buildPingCommand(t: ResolvedPingTarget, platform: NodeJS.Platform = process.platform): string[] {
|
||||
if (platform === "win32") {
|
||||
return [
|
||||
"ping",
|
||||
"-n",
|
||||
String(t.ping.count),
|
||||
"-l",
|
||||
String(t.ping.packetSize),
|
||||
"-w",
|
||||
String(t.timeoutMs),
|
||||
t.ping.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];
|
||||
}
|
||||
182
src/server/checker/runner/icmp/execute.ts
Normal file
182
src/server/checker/runner/icmp/execute.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { isError } from "es-toolkit";
|
||||
|
||||
import type { CheckResult, RawTargetConfig } from "../../types";
|
||||
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { PingExpectConfig, PingStats, PingTargetConfig, ResolvedPingTarget } from "./types";
|
||||
|
||||
import { checkDuration } from "../../expect/duration";
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { buildPingCommand } from "./command";
|
||||
import { checkAlive, checkAvgLatency, checkMaxLatency, checkPacketLoss } from "./expect";
|
||||
import { parsePingOutput } from "./parse";
|
||||
import { icmpCheckerSchemas } from "./schema";
|
||||
import { validatePingConfig } from "./validate";
|
||||
|
||||
const DEFAULT_COUNT = 3;
|
||||
const DEFAULT_PACKET_SIZE = 56;
|
||||
|
||||
export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
readonly configKey = "ping";
|
||||
|
||||
readonly schemas = icmpCheckerSchemas;
|
||||
|
||||
readonly type = "ping";
|
||||
|
||||
async execute(t: ResolvedPingTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const start = performance.now();
|
||||
let proc: ReturnType<typeof Bun.spawn>;
|
||||
|
||||
try {
|
||||
proc = Bun.spawn(buildPingCommand(t), {
|
||||
stderr: "pipe",
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
durationMs,
|
||||
failure: errorFailure("ping", "spawn", `ping 命令不可用: ${isError(error) ? error.message : String(error)}`),
|
||||
matched: false,
|
||||
statusDetail: "ping command not found",
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
ctx.signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
try {
|
||||
proc.kill();
|
||||
} catch {
|
||||
/* best-effort kill */
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
const stdout = await readStream(proc.stdout as ReadableStream<Uint8Array>);
|
||||
await proc.exited;
|
||||
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
if (ctx.signal.aborted) {
|
||||
return {
|
||||
durationMs,
|
||||
failure: errorFailure("ping", "timeout", `ping 执行超时 (${t.timeoutMs}ms)`),
|
||||
matched: false,
|
||||
statusDetail: null,
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
const stats = parsePingOutput(stdout, process.platform);
|
||||
if (!stats) {
|
||||
return {
|
||||
durationMs,
|
||||
failure: errorFailure("ping", "parse", "无法解析 ping 输出"),
|
||||
matched: false,
|
||||
statusDetail: truncateOutput(stdout),
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
const result = checkStats(stats, t.expect, durationMs);
|
||||
return {
|
||||
durationMs,
|
||||
failure: result.failure,
|
||||
matched: result.matched,
|
||||
statusDetail: buildStatusDetail(stats),
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
|
||||
const t = target as RawTargetConfig & { ping: PingTargetConfig; type: "ping" };
|
||||
return {
|
||||
description: null,
|
||||
expect: target.expect as PingExpectConfig | undefined,
|
||||
group: target.group ?? "default",
|
||||
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",
|
||||
} satisfies ResolvedPingTarget;
|
||||
}
|
||||
|
||||
serialize(t: ResolvedPingTarget): { config: string; target: string } {
|
||||
return {
|
||||
config: JSON.stringify(t.ping),
|
||||
target: `ping ${t.ping.host}`,
|
||||
};
|
||||
}
|
||||
|
||||
validate(input: CheckerValidationInput) {
|
||||
return validatePingConfig(input);
|
||||
}
|
||||
}
|
||||
|
||||
function buildStatusDetail(stats: PingStats): string {
|
||||
if (!stats.alive) return `unreachable (${stats.received}/${stats.transmitted} received)`;
|
||||
const avg = stats.avgLatencyMs === null ? "n/a" : formatNumber(stats.avgLatencyMs);
|
||||
const loss = formatNumber(stats.packetLoss);
|
||||
let detail = `alive, avg ${avg}ms, loss ${loss}% (${stats.received}/${stats.transmitted})`;
|
||||
if (stats.packetLoss > 0 && stats.maxLatencyMs !== null) {
|
||||
detail = `${detail}, max ${formatNumber(stats.maxLatencyMs)}ms`;
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
function checkStats(stats: PingStats, expect: PingExpectConfig | undefined, durationMs: number) {
|
||||
const aliveResult = checkAlive(stats.alive, expect?.alive ?? true);
|
||||
if (!aliveResult.matched) return aliveResult;
|
||||
const packetLossResult = checkPacketLoss(stats.packetLoss, expect?.maxPacketLoss);
|
||||
if (!packetLossResult.matched) return packetLossResult;
|
||||
const avgLatencyResult = checkAvgLatency(stats.avgLatencyMs, expect?.maxAvgLatencyMs);
|
||||
if (!avgLatencyResult.matched) return avgLatencyResult;
|
||||
const maxLatencyResult = checkMaxLatency(stats.maxLatencyMs, expect?.maxMaxLatencyMs);
|
||||
if (!maxLatencyResult.matched) return maxLatencyResult;
|
||||
return checkDuration(durationMs, expect?.maxDurationMs);
|
||||
}
|
||||
|
||||
function formatNumber(value: number): string {
|
||||
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(3)));
|
||||
}
|
||||
|
||||
async function readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
text += decoder.decode(value, { stream: true });
|
||||
}
|
||||
text += decoder.decode();
|
||||
} catch {
|
||||
/* stream already closed */
|
||||
} finally {
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch {
|
||||
/* already released */
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function truncateOutput(output: string, maxLen = 80): string {
|
||||
if (output.length <= maxLen) return output;
|
||||
return `${output.slice(0, maxLen)}…`;
|
||||
}
|
||||
44
src/server/checker/runner/icmp/expect.ts
Normal file
44
src/server/checker/runner/icmp/expect.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { ExpectResult } from "../../expect/types";
|
||||
|
||||
import { mismatchFailure } from "../../expect/failure";
|
||||
|
||||
export function checkAlive(actual: boolean, expected: boolean): ExpectResult {
|
||||
if (actual === expected) return { failure: null, matched: true };
|
||||
return {
|
||||
failure: mismatchFailure(
|
||||
"alive",
|
||||
"alive",
|
||||
expected,
|
||||
actual,
|
||||
expected ? "期望主机可达但 ping 不可达" : "期望主机不可达但 ping 可达",
|
||||
),
|
||||
matched: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkAvgLatency(actual: null | number, max: number | undefined): ExpectResult {
|
||||
if (max === undefined) return { failure: null, matched: true };
|
||||
if (actual !== null && actual <= max) return { failure: null, matched: true };
|
||||
return {
|
||||
failure: mismatchFailure("avgLatency", "avgLatencyMs", `<=${max}ms`, actual, `平均延迟超过 ${max}ms`),
|
||||
matched: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkMaxLatency(actual: null | number, max: number | undefined): ExpectResult {
|
||||
if (max === undefined) return { failure: null, matched: true };
|
||||
if (actual !== null && actual <= max) return { failure: null, matched: true };
|
||||
return {
|
||||
failure: mismatchFailure("maxLatency", "maxLatencyMs", `<=${max}ms`, actual, `最大延迟超过 ${max}ms`),
|
||||
matched: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkPacketLoss(actual: number, max: number | undefined): ExpectResult {
|
||||
if (max === undefined) return { failure: null, matched: true };
|
||||
if (actual <= max) return { failure: null, matched: true };
|
||||
return {
|
||||
failure: mismatchFailure("packetLoss", "packetLoss", `<=${max}%`, actual, `丢包率 ${actual}% > ${max}%`),
|
||||
matched: false,
|
||||
};
|
||||
}
|
||||
1
src/server/checker/runner/icmp/index.ts
Normal file
1
src/server/checker/runner/icmp/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { IcmpChecker } from "./execute";
|
||||
50
src/server/checker/runner/icmp/parse.ts
Normal file
50
src/server/checker/runner/icmp/parse.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PingStats } from "./types";
|
||||
|
||||
export type PingPlatform = "darwin" | "linux" | "win32" | NodeJS.Platform;
|
||||
|
||||
export function parsePingOutput(stdout: string, platform: PingPlatform): null | PingStats {
|
||||
return platform === "win32" ? parseWindowsOutput(stdout) : parseUnixOutput(stdout);
|
||||
}
|
||||
|
||||
function parseUnixOutput(stdout: string): null | PingStats {
|
||||
const packetMatch =
|
||||
/(\d+)\s+packets?\s+transmitted.*?(\d+)\s+(?:packets?\s+)?received.*?(\d+(?:\.\d+)?)%\s+packet\s+loss/i.exec(
|
||||
stdout,
|
||||
);
|
||||
if (!packetMatch) return null;
|
||||
|
||||
const transmitted = Number(packetMatch[1]);
|
||||
const received = Number(packetMatch[2]);
|
||||
const packetLoss = Number(packetMatch[3]);
|
||||
const latencyMatch = /(?:rtt|round-trip).*?=\s*([\d.]+)\/([\d.]+)\/([\d.]+)/i.exec(stdout);
|
||||
|
||||
return {
|
||||
alive: received > 0,
|
||||
avgLatencyMs: latencyMatch ? Number(latencyMatch[2]) : null,
|
||||
maxLatencyMs: latencyMatch ? Number(latencyMatch[3]) : null,
|
||||
minLatencyMs: latencyMatch ? Number(latencyMatch[1]) : null,
|
||||
packetLoss,
|
||||
received,
|
||||
transmitted,
|
||||
};
|
||||
}
|
||||
|
||||
function parseWindowsOutput(stdout: string): null | PingStats {
|
||||
const packetMatch = /=\s*(\d+).*?=\s*(\d+).*?=\s*(\d+).*?(\d+(?:\.\d+)?)%/s.exec(stdout);
|
||||
if (!packetMatch) return null;
|
||||
|
||||
const transmitted = Number(packetMatch[1]);
|
||||
const received = Number(packetMatch[2]);
|
||||
const packetLoss = Number(packetMatch[4]);
|
||||
const latencyMatch = /=\s*(\d+)ms.*?=\s*(\d+)ms.*?=\s*(\d+)ms/s.exec(stdout);
|
||||
|
||||
return {
|
||||
alive: received > 0,
|
||||
avgLatencyMs: latencyMatch ? Number(latencyMatch[3]) : null,
|
||||
maxLatencyMs: latencyMatch ? Number(latencyMatch[2]) : null,
|
||||
minLatencyMs: latencyMatch ? Number(latencyMatch[1]) : null,
|
||||
packetLoss,
|
||||
received,
|
||||
transmitted,
|
||||
};
|
||||
}
|
||||
25
src/server/checker/runner/icmp/schema.ts
Normal file
25
src/server/checker/runner/icmp/schema.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
import type { CheckerSchemas } from "../types";
|
||||
|
||||
export const icmpCheckerSchemas: CheckerSchemas = {
|
||||
config: Type.Object(
|
||||
{
|
||||
count: Type.Optional(Type.Integer({ maximum: 100, minimum: 1 })),
|
||||
host: Type.String({ minLength: 1 }),
|
||||
packetSize: Type.Optional(Type.Integer({ maximum: 65500, minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
defaults: Type.Object({}, { additionalProperties: false }),
|
||||
expect: Type.Object(
|
||||
{
|
||||
alive: Type.Optional(Type.Boolean()),
|
||||
maxAvgLatencyMs: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
maxDurationMs: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
maxMaxLatencyMs: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
maxPacketLoss: Type.Optional(Type.Number({ maximum: 100, minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
};
|
||||
41
src/server/checker/runner/icmp/types.ts
Normal file
41
src/server/checker/runner/icmp/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ResolvedTargetBase } from "../../types";
|
||||
|
||||
export interface PingExpectConfig {
|
||||
alive?: boolean;
|
||||
maxAvgLatencyMs?: number;
|
||||
maxDurationMs?: number;
|
||||
maxMaxLatencyMs?: number;
|
||||
maxPacketLoss?: number;
|
||||
}
|
||||
|
||||
export interface PingStats {
|
||||
alive: boolean;
|
||||
avgLatencyMs: null | number;
|
||||
maxLatencyMs: null | number;
|
||||
minLatencyMs: null | number;
|
||||
packetLoss: number;
|
||||
received: number;
|
||||
transmitted: number;
|
||||
}
|
||||
|
||||
export interface PingTargetConfig {
|
||||
count?: number;
|
||||
host: string;
|
||||
packetSize?: number;
|
||||
}
|
||||
|
||||
export interface ResolvedPingConfig {
|
||||
count: number;
|
||||
host: string;
|
||||
packetSize: number;
|
||||
}
|
||||
|
||||
export interface ResolvedPingTarget extends ResolvedTargetBase {
|
||||
expect?: PingExpectConfig;
|
||||
group: string;
|
||||
intervalMs: number;
|
||||
name: null | string;
|
||||
ping: ResolvedPingConfig;
|
||||
timeoutMs: number;
|
||||
type: "ping";
|
||||
}
|
||||
118
src/server/checker/runner/icmp/validate.ts
Normal file
118
src/server/checker/runner/icmp/validate.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { isNumber, isPlainObject, isString } from "es-toolkit";
|
||||
|
||||
import type { ConfigValidationIssue } from "../../schema/issues";
|
||||
import type { CheckerValidationInput } from "../types";
|
||||
|
||||
import { issue, joinPath } from "../../schema/issues";
|
||||
|
||||
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
|
||||
const defaults = input.defaults["ping"];
|
||||
if (defaults !== undefined && defaults !== null) {
|
||||
const targetName = "defaults.ping";
|
||||
if (!isPlainObject(defaults)) {
|
||||
issues.push(issue("invalid-type", "defaults.ping", "必须为对象", 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < input.targets.length; i++) {
|
||||
const target = input.targets[i] as unknown;
|
||||
if (!isPlainObject(target)) continue;
|
||||
const targetRecord = target as Record<string, unknown>;
|
||||
if (targetRecord["type"] !== "ping") continue;
|
||||
issues.push(...validatePingTarget(targetRecord, `targets[${i}]`));
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function getTargetName(target: Record<string, unknown>): string | undefined {
|
||||
if (isString(target["name"])) return target["name"];
|
||||
return isString(target["id"]) ? target["id"] : undefined;
|
||||
}
|
||||
|
||||
function isNonNegativeFiniteNumber(value: unknown): boolean {
|
||||
return isNumber(value) && Number.isFinite(value) && value >= 0;
|
||||
}
|
||||
|
||||
function validatePingExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
const rawExpect = target["expect"];
|
||||
if (rawExpect === undefined || rawExpect === null || !isPlainObject(rawExpect)) return [];
|
||||
const expect = rawExpect as Record<string, unknown>;
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const targetName = getTargetName(target);
|
||||
const expectPath = joinPath(path, "expect");
|
||||
|
||||
if (expect["alive"] !== undefined && typeof expect["alive"] !== "boolean") {
|
||||
issues.push(issue("invalid-type", joinPath(expectPath, "alive"), "必须为布尔值", targetName));
|
||||
}
|
||||
if (expect["maxPacketLoss"] !== undefined) {
|
||||
const value = expect["maxPacketLoss"];
|
||||
if (!isNumber(value) || !Number.isFinite(value) || value < 0 || value > 100) {
|
||||
issues.push(issue("invalid-value", joinPath(expectPath, "maxPacketLoss"), "必须为 0-100 的数字", targetName));
|
||||
}
|
||||
}
|
||||
for (const key of ["maxAvgLatencyMs", "maxMaxLatencyMs", "maxDurationMs"]) {
|
||||
if (expect[key] !== undefined && !isNonNegativeFiniteNumber(expect[key])) {
|
||||
issues.push(issue("invalid-type", joinPath(expectPath, key), "必须为非负有限数字", targetName));
|
||||
}
|
||||
}
|
||||
|
||||
const allowedKeys = new Set(["alive", "maxAvgLatencyMs", "maxDurationMs", "maxMaxLatencyMs", "maxPacketLoss"]);
|
||||
for (const key of Object.keys(expect)) {
|
||||
if (!allowedKeys.has(key)) {
|
||||
issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validatePingTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const targetName = getTargetName(target);
|
||||
const rawPing = target["ping"];
|
||||
|
||||
if (!isPlainObject(rawPing)) {
|
||||
issues.push(issue("required", joinPath(path, "ping"), "缺少 ping 配置分组", targetName));
|
||||
issues.push(...validatePingExpect(target, path));
|
||||
return issues;
|
||||
}
|
||||
const ping = rawPing as Record<string, unknown>;
|
||||
|
||||
if (!isString(ping["host"]) || ping["host"].trim() === "") {
|
||||
issues.push(issue("required", joinPath(joinPath(path, "ping"), "host"), "缺少 ping.host 字段", targetName));
|
||||
}
|
||||
if (ping["count"] !== undefined) {
|
||||
const count = ping["count"];
|
||||
if (!isNumber(count) || !Number.isInteger(count) || count < 1 || count > 100) {
|
||||
issues.push(
|
||||
issue("invalid-value", joinPath(joinPath(path, "ping"), "count"), "必须为 1-100 的正整数", targetName),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ping["packetSize"] !== undefined) {
|
||||
const packetSize = ping["packetSize"];
|
||||
if (!isNumber(packetSize) || !Number.isInteger(packetSize) || packetSize < 1 || packetSize > 65500) {
|
||||
issues.push(
|
||||
issue("invalid-value", joinPath(joinPath(path, "ping"), "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));
|
||||
}
|
||||
}
|
||||
|
||||
issues.push(...validatePingExpect(target, path));
|
||||
return issues;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { CommandChecker } from "./cmd";
|
||||
import { DbChecker } from "./db";
|
||||
import { HttpChecker } from "./http";
|
||||
import { IcmpChecker } from "./icmp";
|
||||
import { CheckerRegistry } from "./registry";
|
||||
import { TcpChecker } from "./tcp";
|
||||
|
||||
const checkers = [new HttpChecker(), new CommandChecker(), new DbChecker(), new TcpChecker()];
|
||||
const checkers = [new HttpChecker(), new CommandChecker(), new DbChecker(), new TcpChecker(), new IcmpChecker()];
|
||||
|
||||
export function createDefaultCheckerRegistry(): CheckerRegistry {
|
||||
const registry = new CheckerRegistry();
|
||||
|
||||
Reference in New Issue
Block a user