Files
cloudflare-domain-manager/apps/api/src/services/node-service.ts
T
Denozordec 3f6f402872
quality / commitlint (push) Skipped
CD / update-wiki (push) Failing after 8s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 50s
quality / api (push) Successful in 41s
CD / quality (push) Successful in 1m40s
CD / publish (push) Successful in 1m33s
feat(health-checks): enhance health check functionality and add new routes
- Introduced origin health check routes and integrated them into the application.
- Updated health check configuration to include success recovery thresholds.
- Expanded error handling with new error codes for health check failures.
- Added new service routes for managing health checks, including creation and listing.
- Improved health check service logic to track consecutive successes and failures.

This commit enhances the health check capabilities, providing better monitoring and management of service health.
2026-08-19 12:26:12 +07:00

151 lines
4.2 KiB
TypeScript

import type { Db } from "@cfdm/db";
import { repos } from "@cfdm/db";
import type {
CreateServiceNodeInput,
ServiceNode,
ServiceOverview,
UpdateServiceNodeInput,
} from "@cfdm/shared";
import { AppError } from "../errors.js";
import { isValidIpv4 } from "../lib/validators.js";
import { getView } from "./service-config-service.js";
import { selectActiveIpsByMode } from "./routing/index.js";
function assertAddress(address: string): void {
if (!isValidIpv4(address)) {
throw AppError.invalidIp(`Некорректный IP-адрес: ${address}`);
}
}
export function listNodes(db: Db, serviceId: number): ServiceNode[] {
repos.getService(db, serviceId);
return repos.listNodes(db, serviceId);
}
export function createNode(
db: Db,
serviceId: number,
input: CreateServiceNodeInput,
): ServiceNode {
repos.getService(db, serviceId);
assertAddress(input.address);
try {
return repos.createNode(db, serviceId, {
address: input.address,
protocol: input.protocol,
port: input.port,
enabled: input.enabled,
priority: input.priority,
weight: input.weight,
health_check_id: input.health_check_id,
});
} catch (err) {
if (err instanceof Error && err.name === "ConflictError") {
throw AppError.conflict(err.message);
}
throw err;
}
}
export function updateNode(
db: Db,
serviceId: number,
nodeId: number,
patch: UpdateServiceNodeInput,
): ServiceNode {
const node = repos.getNode(db, nodeId);
if (node.service_id !== serviceId) {
throw AppError.notFound(`node ${nodeId}`);
}
if (patch.address) assertAddress(patch.address);
return repos.updateNode(db, nodeId, patch);
}
export function deleteNode(db: Db, serviceId: number, nodeId: number): void {
const node = repos.getNode(db, nodeId);
if (node.service_id !== serviceId) {
throw AppError.notFound(`node ${nodeId}`);
}
repos.deleteNode(db, nodeId);
}
export async function getOverview(
db: Db,
serviceId: number,
): Promise<ServiceOverview> {
const service = await getView(db, serviceId);
const nodes = repos.listNodes(db, serviceId);
const bindings = repos.listBindingsByService(db, serviceId);
const first = bindings[0];
const routing = first?.routing_strategy ?? first?.lb_mode ?? "round_robin";
const healthCheck =
nodes
.map((n) => n.health_check_id)
.find((id): id is number => id != null) != null
? repos.getHealthCheck(
db,
nodes.find((n) => n.health_check_id != null)!.health_check_id!,
)
: null;
const active = new Set<string>();
for (const binding of bindings) {
const metas = repos.listBindingIpsWithMeta(db, binding.id);
const rows = metas.map((entry) => {
const status = repos.getIpHealthStatusRow(db, "binding", binding.id, entry.ip);
return {
ip: entry.ip,
weight: entry.weight,
priority: entry.priority,
health: status ? status.status : ("unknown" as const),
};
});
for (const ip of selectActiveIpsByMode(
{
lb_mode: binding.lb_mode,
health_check_enabled: binding.health_check_enabled,
},
rows,
)) {
active.add(ip);
}
}
return {
service,
nodes,
health_check: healthCheck,
routing_strategy: routing,
active_addresses: [...active],
};
}
export function opsSummary(db: Db) {
const allNodes = repos.listAllNodes(db);
const services = repos.listServices(db);
const domains = repos.listDomains(db);
const healthy = allNodes.filter(
(n) => n.health_status === "healthy" || n.health_status === "up",
).length;
const unhealthy = allNodes.filter(
(n) => n.health_status === "unhealthy" || n.health_status === "down",
).length;
const failoverActive = repos.listAllBindings(db).filter((binding) => {
if (binding.lb_mode !== "failover" || !binding.health_check_enabled) {
return false;
}
return binding.target_ips.some((ip) => {
const row = repos.getIpHealthStatusRow(db, "binding", binding.id, ip);
return row?.status === "down";
});
}).length;
return {
domains: domains.length,
services: services.length,
nodes: allNodes.length,
healthy,
unhealthy,
active_failovers: failoverActive,
};
}