feat(api, web): enhance health check and domain management features
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m3s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m48s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m3s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m48s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
- Integrated domain monitoring routes and bulk update functionality for domains in the API. - Improved health check service to include domain monitoring and logging of health status changes. - Updated web components to reflect health status with new HealthCheckBadge and enhanced domain filtering options. - Refactored domain service to support bulk updates and improved domain management capabilities. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -43,11 +43,51 @@ export async function createDomain(
|
||||
export function updateDomain(
|
||||
db: Db,
|
||||
id: number,
|
||||
groupId: number | null,
|
||||
status: string,
|
||||
certMonitoring?: string,
|
||||
patch: {
|
||||
group_id?: number | null;
|
||||
status?: string;
|
||||
cert_monitoring?: string;
|
||||
environment?: string | null;
|
||||
tags?: string[];
|
||||
},
|
||||
): Domain {
|
||||
return repos.updateDomain(db, id, groupId, status, certMonitoring);
|
||||
const domain = repos.updateDomain(db, id, {
|
||||
group_id: patch.group_id,
|
||||
status: patch.status,
|
||||
cert_monitoring: patch.cert_monitoring,
|
||||
environment: patch.environment,
|
||||
});
|
||||
if (patch.tags !== undefined) {
|
||||
repos.setDomainTags(db, id, patch.tags);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
export function bulkUpdateDomains(
|
||||
db: Db,
|
||||
ids: number[],
|
||||
patch: {
|
||||
group_id?: number | null;
|
||||
environment?: string | null;
|
||||
tags_add?: string[];
|
||||
},
|
||||
): number {
|
||||
let updated = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
repos.updateDomain(db, id, {
|
||||
group_id: patch.group_id,
|
||||
environment: patch.environment,
|
||||
});
|
||||
if (patch.tags_add?.length) {
|
||||
repos.addDomainTags(db, id, patch.tags_add);
|
||||
}
|
||||
updated += 1;
|
||||
} catch {
|
||||
// skip missing
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function deleteDomain(db: Db, id: number): void {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { connect } from "node:net";
|
||||
import { resolve4, resolve6 } from "node:dns/promises";
|
||||
import { Agent, fetch as undiciFetch } from "undici";
|
||||
import type { Db } from "@cfdm/db";
|
||||
import { repos } from "@cfdm/db";
|
||||
@@ -68,10 +69,6 @@ async function httpProbe(
|
||||
const pathWithSlash = path.startsWith("/") ? path : `/${path}`;
|
||||
const port = target.port ?? 80;
|
||||
const useTls = port === 443;
|
||||
// For HTTPS on 443, probe via the hostname (URL host = hostname) so TLS SNI, Host header
|
||||
// and any edge/vhost protection (e.g. Cloudflare origin 421 on direct-IP) all line up.
|
||||
// For multi-A records this loses strict per-IP HTTPS granularity — use TCP probe for that.
|
||||
// For plain HTTP we still hit the literal IP (per-record target).
|
||||
const urlHost = useTls ? target.hostname || ip : ip;
|
||||
const url = `${useTls ? "https" : "http"}://${urlHost}${pathWithSlash}`;
|
||||
const dispatcher =
|
||||
@@ -119,6 +116,47 @@ async function httpProbe(
|
||||
}
|
||||
}
|
||||
|
||||
/** Host reachability via TCP :443 then :80 (ICMP often unavailable in Node). */
|
||||
async function pingProbe(hostname: string, timeoutMs: number): Promise<ProbeResult> {
|
||||
const ports = [443, 80];
|
||||
let last: ProbeResult = {
|
||||
ok: false,
|
||||
latencyMs: 0,
|
||||
error: "unreachable",
|
||||
};
|
||||
for (const port of ports) {
|
||||
last = await tcpProbe(hostname, port, timeoutMs);
|
||||
if (last.ok) return last;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
async function dnsProbe(hostname: string): Promise<ProbeResult> {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const [v4, v6] = await Promise.allSettled([
|
||||
resolve4(hostname),
|
||||
resolve6(hostname),
|
||||
]);
|
||||
const hasV4 = v4.status === "fulfilled" && v4.value.length > 0;
|
||||
const hasV6 = v6.status === "fulfilled" && v6.value.length > 0;
|
||||
if (!hasV4 && !hasV6) {
|
||||
return {
|
||||
ok: false,
|
||||
latencyMs: Date.now() - started,
|
||||
error: "no A/AAAA records",
|
||||
};
|
||||
}
|
||||
return { ok: true, latencyMs: Date.now() - started, error: null };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
latencyMs: Date.now() - started,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function probeTarget(
|
||||
target: HealthCheckTarget,
|
||||
): Promise<ProbeResult> {
|
||||
@@ -127,6 +165,12 @@ export async function probeTarget(
|
||||
if (target.type === "http") {
|
||||
return httpProbe(target.ip, target, timeoutMs);
|
||||
}
|
||||
if (target.type === "ping") {
|
||||
return pingProbe(target.hostname || target.ip, timeoutMs);
|
||||
}
|
||||
if (target.type === "dns") {
|
||||
return dnsProbe(target.hostname || target.ip);
|
||||
}
|
||||
return tcpProbe(target.ip, port, timeoutMs);
|
||||
}
|
||||
|
||||
@@ -205,6 +249,79 @@ export async function runAllChecks(
|
||||
return targets.length;
|
||||
}
|
||||
|
||||
export async function runDomainMonitors(
|
||||
db: Db,
|
||||
thresholds: HealthCheckThresholds,
|
||||
): Promise<number> {
|
||||
const monitors = repos.listEnabledDomainMonitors(db);
|
||||
let checked = 0;
|
||||
for (const monitor of monitors) {
|
||||
const target: HealthCheckTarget = {
|
||||
scope: "binding",
|
||||
ref_id: monitor.id,
|
||||
ip: monitor.hostname,
|
||||
hostname: monitor.hostname,
|
||||
type: monitor.type as HealthCheckTarget["type"],
|
||||
port: monitor.type === "http" ? (monitor.path?.includes("443") ? 443 : 80) : null,
|
||||
path: monitor.path,
|
||||
expected_status: monitor.expected_status,
|
||||
timeout_ms: monitor.timeout_ms,
|
||||
};
|
||||
let result: ProbeResult;
|
||||
if (monitor.type === "http") {
|
||||
result = await httpProbe(monitor.hostname, {
|
||||
...target,
|
||||
port: 443,
|
||||
path: monitor.path ?? "/",
|
||||
}, monitor.timeout_ms);
|
||||
if (!result.ok) {
|
||||
result = await httpProbe(monitor.hostname, {
|
||||
...target,
|
||||
port: 80,
|
||||
path: monitor.path ?? "/",
|
||||
}, monitor.timeout_ms);
|
||||
}
|
||||
} else if (monitor.type === "ping") {
|
||||
result = await pingProbe(monitor.hostname, monitor.timeout_ms);
|
||||
} else {
|
||||
result = await dnsProbe(monitor.hostname);
|
||||
}
|
||||
const prevStatus = monitor.last_status as IpHealthState;
|
||||
const { state } = deriveState(
|
||||
result.ok,
|
||||
result.latencyMs,
|
||||
{
|
||||
consecutive_failures: result.ok ? 0 : 1,
|
||||
status: prevStatus,
|
||||
},
|
||||
thresholds,
|
||||
);
|
||||
repos.updateDomainMonitorResult(
|
||||
db,
|
||||
monitor.id,
|
||||
state,
|
||||
result.latencyMs,
|
||||
result.error,
|
||||
);
|
||||
if (prevStatus !== state && prevStatus !== "unknown") {
|
||||
const label =
|
||||
state === "up" ? "OK" : state === "degraded" ? "Slow" : state === "down" ? "Down" : "—";
|
||||
repos.insertNotificationLog(
|
||||
db,
|
||||
"domain_monitor",
|
||||
"domain_monitor",
|
||||
monitor.id,
|
||||
`${monitor.hostname}: ${label}`,
|
||||
result.error
|
||||
? `${monitor.type.toUpperCase()} → ${label}. ${result.error}`
|
||||
: `${monitor.type.toUpperCase()} → ${label}${result.latencyMs != null ? ` (${result.latencyMs} мс)` : ""}`,
|
||||
);
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
|
||||
export function listStatus(
|
||||
db: Db,
|
||||
scope: "binding" | "group",
|
||||
|
||||
Reference in New Issue
Block a user