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
- 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.
151 lines
4.2 KiB
TypeScript
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,
|
|
};
|
|
}
|