fix(health): не показывать Down при выключенном health-check
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 4s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m58s
CD / publish (push) Successful in 1m25s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 4s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m58s
CD / publish (push) Successful in 1m25s
Маскируем устаревший down, если HC выкл (binding/группа), IP или сервис отключены. Бейдж Disabled — нейтральный, не красный. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -546,6 +546,34 @@ function bestAliveDisplayStatus(statuses: readonly string[]): IpHealthState {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/** Health badge applies only when the service, IP and HC (binding or group) are active. */
|
||||
function isServiceHealthCheckActive(db: Db, view: ServiceView): boolean {
|
||||
if ((view.domains ?? []).some((domain) => domain.health_check_enabled)) {
|
||||
return true;
|
||||
}
|
||||
if (!view.service_group_id) return false;
|
||||
const group = repos.getServiceGroup(db, view.service_group_id);
|
||||
return Boolean(group.health_check_enabled);
|
||||
}
|
||||
|
||||
function isIpHealthMonitored(db: Db, view: ServiceView, ip: string): boolean {
|
||||
if (!view.enabled) return false;
|
||||
if (view.ip_enabled[ip] === false) return false;
|
||||
return isServiceHealthCheckActive(db, view);
|
||||
}
|
||||
|
||||
function inactiveIpHealthRow(ip: string): ServiceHealthRow {
|
||||
return {
|
||||
ip,
|
||||
status: "unknown",
|
||||
latency_ms: null,
|
||||
last_checked_at: null,
|
||||
last_error: null,
|
||||
provider: "local",
|
||||
colo: null,
|
||||
};
|
||||
}
|
||||
|
||||
function attachServiceHealth(
|
||||
db: Db,
|
||||
views: ServiceView[],
|
||||
@@ -567,6 +595,9 @@ function attachServiceHealth(
|
||||
),
|
||||
);
|
||||
const ip_health = (view.ips ?? []).map((ip) => {
|
||||
if (!isIpHealthMonitored(db, view, ip)) {
|
||||
return inactiveIpHealthRow(ip);
|
||||
}
|
||||
const row = byIp.get(ip) ?? (aRecordIps.has(ip) ? undefined : cnameFallback);
|
||||
const live = liveByIp.get(ip);
|
||||
const status = overlayLiveHealth(row?.status, live?.status);
|
||||
@@ -584,17 +615,26 @@ function attachServiceHealth(
|
||||
colo: extras?.colo ?? null,
|
||||
};
|
||||
});
|
||||
const displayStatus = bestAliveDisplayStatus(ip_health.map((row) => row.status));
|
||||
const monitoredStatuses = ip_health
|
||||
.filter((row) => isIpHealthMonitored(db, view, row.ip))
|
||||
.map((row) => row.status);
|
||||
const displayStatus =
|
||||
monitoredStatuses.length > 0
|
||||
? bestAliveDisplayStatus(monitoredStatuses)
|
||||
: ("unknown" as const);
|
||||
const latencyRow =
|
||||
ip_health.find((row) => row.status === displayStatus && row.latency_ms != null) ??
|
||||
ip_health.find((row) => row.latency_ms != null);
|
||||
return {
|
||||
...view,
|
||||
health_status: overlayLiveHealth(health?.health_status, displayStatus),
|
||||
health_status:
|
||||
monitoredStatuses.length > 0
|
||||
? overlayLiveHealth(health?.health_status, displayStatus)
|
||||
: "unknown",
|
||||
health_latency_ms:
|
||||
displayStatus !== "unknown"
|
||||
monitoredStatuses.length > 0 && displayStatus !== "unknown"
|
||||
? (latencyRow?.latency_ms ?? null)
|
||||
: (health?.health_latency_ms ?? null),
|
||||
: null,
|
||||
ip_health,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -376,6 +376,7 @@ describe("CNAME health mapped onto service IPs", () => {
|
||||
repos.replaceServiceIps(db, service.id, ["2.59.161.102"]);
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "s", null);
|
||||
repos.setBindingCnameTarget(db, binding.id, "ihome.rkns.top");
|
||||
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
@@ -412,6 +413,7 @@ describe("CNAME health mapped onto service IPs", () => {
|
||||
{ ip: "10.0.0.1", weight: 1, priority: 1 },
|
||||
{ ip: "10.0.0.2", weight: 1, priority: 1 },
|
||||
]);
|
||||
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
@@ -450,6 +452,7 @@ describe("CNAME health mapped onto service IPs", () => {
|
||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||
{ ip: "2.59.161.102", weight: 1, priority: 1 },
|
||||
]);
|
||||
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
@@ -478,4 +481,74 @@ describe("CNAME health mapped onto service IPs", () => {
|
||||
expect(view.ip_health[0]?.status).toBe("up");
|
||||
expect(view.ip_health[0]?.latency_ms).toBe(63);
|
||||
});
|
||||
|
||||
it("getView masks stale down when health-check is disabled", async () => {
|
||||
const { createMemoryDb, repos, runMigrations } = await import("@cfdm/db");
|
||||
const { getView } = await import("../src/services/service-config-service.js");
|
||||
const { db, sqlite } = createMemoryDb();
|
||||
runMigrations(sqlite);
|
||||
|
||||
const domain = repos.createDomain(db, null, "rkns.top", "zone-id");
|
||||
const service = repos.createService(db, "Main TG", "main-tg");
|
||||
repos.setServiceEnabled(db, service.id, true);
|
||||
repos.replaceServiceIps(db, service.id, ["130.49.213.176"]);
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
|
||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||
{ ip: "130.49.213.176", weight: 1, priority: 1 },
|
||||
]);
|
||||
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: false });
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
binding.id,
|
||||
"130.49.213.176",
|
||||
"down",
|
||||
null,
|
||||
5,
|
||||
"timeout",
|
||||
);
|
||||
|
||||
const view = await getView(db, service.id);
|
||||
expect(view.health_status).toBe("unknown");
|
||||
expect(view.ip_health).toEqual([
|
||||
expect.objectContaining({
|
||||
ip: "130.49.213.176",
|
||||
status: "unknown",
|
||||
latency_ms: null,
|
||||
last_error: null,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("getView masks stale down when IP is disabled in pool", async () => {
|
||||
const { createMemoryDb, repos, runMigrations } = await import("@cfdm/db");
|
||||
const { getView } = await import("../src/services/service-config-service.js");
|
||||
const { db, sqlite } = createMemoryDb();
|
||||
runMigrations(sqlite);
|
||||
|
||||
const domain = repos.createDomain(db, null, "rkns.top", "zone-id");
|
||||
const service = repos.createService(db, "Main TG", "main-tg");
|
||||
repos.setServiceEnabled(db, service.id, true);
|
||||
repos.replaceServiceIps(db, service.id, ["130.49.213.176"]);
|
||||
repos.setServiceIpEnabled(db, service.id, "130.49.213.176", false);
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
|
||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||
{ ip: "130.49.213.176", weight: 1, priority: 1 },
|
||||
]);
|
||||
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
binding.id,
|
||||
"130.49.213.176",
|
||||
"down",
|
||||
null,
|
||||
5,
|
||||
"timeout",
|
||||
);
|
||||
|
||||
const view = await getView(db, service.id);
|
||||
expect(view.health_status).toBe("unknown");
|
||||
expect(view.ip_health[0]?.status).toBe("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@ type HealthStatus =
|
||||
|
||||
function normalizeHealth(status: HealthStatus): IpHealthStatus['status'] {
|
||||
if (status === 'healthy') return 'up'
|
||||
if (status === 'unhealthy' || status === 'disabled') return 'down'
|
||||
if (status === 'checking') return 'unknown'
|
||||
if (status === 'unhealthy') return 'down'
|
||||
if (status === 'disabled' || status === 'checking') return 'unknown'
|
||||
return status
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ const VISIBLE_IP_LIMIT = 6
|
||||
interface ServiceIpListProps {
|
||||
ips: string[]
|
||||
ipHealth?: ServiceView['ip_health']
|
||||
healthCheckEnabled?: boolean
|
||||
ipEnabled?: Record<string, boolean>
|
||||
togglingIp?: string | null
|
||||
ipToggleDisabled?: boolean
|
||||
@@ -151,6 +152,7 @@ interface ServiceIpListProps {
|
||||
export function ServiceIpList({
|
||||
ips,
|
||||
ipHealth = [],
|
||||
healthCheckEnabled = true,
|
||||
ipEnabled = {},
|
||||
togglingIp = null,
|
||||
ipToggleDisabled = false,
|
||||
@@ -184,6 +186,10 @@ export function ServiceIpList({
|
||||
{visible.map((ip) => {
|
||||
const health = healthByIp.get(ip)
|
||||
const enabled = ipEnabled[ip] !== false
|
||||
const monitored = healthCheckEnabled && enabled
|
||||
const badgeStatus = monitored
|
||||
? (health?.status ?? 'unknown')
|
||||
: 'disabled'
|
||||
return (
|
||||
<Item
|
||||
key={ip}
|
||||
@@ -192,7 +198,7 @@ export function ServiceIpList({
|
||||
>
|
||||
<ItemMedia>
|
||||
<HealthCheckBadge
|
||||
status={health?.status ?? 'unknown'}
|
||||
status={badgeStatus}
|
||||
latencyMs={health?.latency_ms}
|
||||
lastCheckedAt={health?.last_checked_at}
|
||||
lastError={health?.last_error}
|
||||
|
||||
@@ -273,6 +273,10 @@ export function ServiceUnitCard({
|
||||
alignWithMenu
|
||||
ips={service.ips ?? []}
|
||||
ipHealth={service.ip_health ?? []}
|
||||
healthCheckEnabled={
|
||||
service.enabled &&
|
||||
(service.domains ?? []).some((domain) => domain.health_check_enabled)
|
||||
}
|
||||
ipEnabled={service.ip_enabled ?? {}}
|
||||
ipToggleDisabled={togglingId === service.id}
|
||||
togglingIp={togglingIp}
|
||||
|
||||
Reference in New Issue
Block a user