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 <cursoragent@cursor.com>
This commit is contained in:
@@ -546,6 +546,34 @@ function bestAliveDisplayStatus(statuses: readonly string[]): IpHealthState {
|
|||||||
return "unknown";
|
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(
|
function attachServiceHealth(
|
||||||
db: Db,
|
db: Db,
|
||||||
views: ServiceView[],
|
views: ServiceView[],
|
||||||
@@ -567,6 +595,9 @@ function attachServiceHealth(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const ip_health = (view.ips ?? []).map((ip) => {
|
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 row = byIp.get(ip) ?? (aRecordIps.has(ip) ? undefined : cnameFallback);
|
||||||
const live = liveByIp.get(ip);
|
const live = liveByIp.get(ip);
|
||||||
const status = overlayLiveHealth(row?.status, live?.status);
|
const status = overlayLiveHealth(row?.status, live?.status);
|
||||||
@@ -584,17 +615,26 @@ function attachServiceHealth(
|
|||||||
colo: extras?.colo ?? null,
|
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 =
|
const latencyRow =
|
||||||
ip_health.find((row) => row.status === displayStatus && row.latency_ms != null) ??
|
ip_health.find((row) => row.status === displayStatus && row.latency_ms != null) ??
|
||||||
ip_health.find((row) => row.latency_ms != null);
|
ip_health.find((row) => row.latency_ms != null);
|
||||||
return {
|
return {
|
||||||
...view,
|
...view,
|
||||||
health_status: overlayLiveHealth(health?.health_status, displayStatus),
|
health_status:
|
||||||
|
monitoredStatuses.length > 0
|
||||||
|
? overlayLiveHealth(health?.health_status, displayStatus)
|
||||||
|
: "unknown",
|
||||||
health_latency_ms:
|
health_latency_ms:
|
||||||
displayStatus !== "unknown"
|
monitoredStatuses.length > 0 && displayStatus !== "unknown"
|
||||||
? (latencyRow?.latency_ms ?? null)
|
? (latencyRow?.latency_ms ?? null)
|
||||||
: (health?.health_latency_ms ?? null),
|
: null,
|
||||||
ip_health,
|
ip_health,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ describe("CNAME health mapped onto service IPs", () => {
|
|||||||
repos.replaceServiceIps(db, service.id, ["2.59.161.102"]);
|
repos.replaceServiceIps(db, service.id, ["2.59.161.102"]);
|
||||||
const binding = repos.insertBinding(db, domain.id, service.id, "s", null);
|
const binding = repos.insertBinding(db, domain.id, service.id, "s", null);
|
||||||
repos.setBindingCnameTarget(db, binding.id, "ihome.rkns.top");
|
repos.setBindingCnameTarget(db, binding.id, "ihome.rkns.top");
|
||||||
|
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||||
repos.upsertIpHealthStatus(
|
repos.upsertIpHealthStatus(
|
||||||
db,
|
db,
|
||||||
"binding",
|
"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.1", weight: 1, priority: 1 },
|
||||||
{ ip: "10.0.0.2", weight: 1, priority: 1 },
|
{ ip: "10.0.0.2", weight: 1, priority: 1 },
|
||||||
]);
|
]);
|
||||||
|
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||||
repos.upsertIpHealthStatus(
|
repos.upsertIpHealthStatus(
|
||||||
db,
|
db,
|
||||||
"binding",
|
"binding",
|
||||||
@@ -450,6 +452,7 @@ describe("CNAME health mapped onto service IPs", () => {
|
|||||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||||
{ ip: "2.59.161.102", weight: 1, priority: 1 },
|
{ ip: "2.59.161.102", weight: 1, priority: 1 },
|
||||||
]);
|
]);
|
||||||
|
repos.updateBindingLbConfig(db, binding.id, { health_check_enabled: true });
|
||||||
repos.upsertIpHealthStatus(
|
repos.upsertIpHealthStatus(
|
||||||
db,
|
db,
|
||||||
"binding",
|
"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]?.status).toBe("up");
|
||||||
expect(view.ip_health[0]?.latency_ms).toBe(63);
|
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'] {
|
function normalizeHealth(status: HealthStatus): IpHealthStatus['status'] {
|
||||||
if (status === 'healthy') return 'up'
|
if (status === 'healthy') return 'up'
|
||||||
if (status === 'unhealthy' || status === 'disabled') return 'down'
|
if (status === 'unhealthy') return 'down'
|
||||||
if (status === 'checking') return 'unknown'
|
if (status === 'disabled' || status === 'checking') return 'unknown'
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const VISIBLE_IP_LIMIT = 6
|
|||||||
interface ServiceIpListProps {
|
interface ServiceIpListProps {
|
||||||
ips: string[]
|
ips: string[]
|
||||||
ipHealth?: ServiceView['ip_health']
|
ipHealth?: ServiceView['ip_health']
|
||||||
|
healthCheckEnabled?: boolean
|
||||||
ipEnabled?: Record<string, boolean>
|
ipEnabled?: Record<string, boolean>
|
||||||
togglingIp?: string | null
|
togglingIp?: string | null
|
||||||
ipToggleDisabled?: boolean
|
ipToggleDisabled?: boolean
|
||||||
@@ -151,6 +152,7 @@ interface ServiceIpListProps {
|
|||||||
export function ServiceIpList({
|
export function ServiceIpList({
|
||||||
ips,
|
ips,
|
||||||
ipHealth = [],
|
ipHealth = [],
|
||||||
|
healthCheckEnabled = true,
|
||||||
ipEnabled = {},
|
ipEnabled = {},
|
||||||
togglingIp = null,
|
togglingIp = null,
|
||||||
ipToggleDisabled = false,
|
ipToggleDisabled = false,
|
||||||
@@ -184,6 +186,10 @@ export function ServiceIpList({
|
|||||||
{visible.map((ip) => {
|
{visible.map((ip) => {
|
||||||
const health = healthByIp.get(ip)
|
const health = healthByIp.get(ip)
|
||||||
const enabled = ipEnabled[ip] !== false
|
const enabled = ipEnabled[ip] !== false
|
||||||
|
const monitored = healthCheckEnabled && enabled
|
||||||
|
const badgeStatus = monitored
|
||||||
|
? (health?.status ?? 'unknown')
|
||||||
|
: 'disabled'
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
key={ip}
|
key={ip}
|
||||||
@@ -192,7 +198,7 @@ export function ServiceIpList({
|
|||||||
>
|
>
|
||||||
<ItemMedia>
|
<ItemMedia>
|
||||||
<HealthCheckBadge
|
<HealthCheckBadge
|
||||||
status={health?.status ?? 'unknown'}
|
status={badgeStatus}
|
||||||
latencyMs={health?.latency_ms}
|
latencyMs={health?.latency_ms}
|
||||||
lastCheckedAt={health?.last_checked_at}
|
lastCheckedAt={health?.last_checked_at}
|
||||||
lastError={health?.last_error}
|
lastError={health?.last_error}
|
||||||
|
|||||||
@@ -273,6 +273,10 @@ export function ServiceUnitCard({
|
|||||||
alignWithMenu
|
alignWithMenu
|
||||||
ips={service.ips ?? []}
|
ips={service.ips ?? []}
|
||||||
ipHealth={service.ip_health ?? []}
|
ipHealth={service.ip_health ?? []}
|
||||||
|
healthCheckEnabled={
|
||||||
|
service.enabled &&
|
||||||
|
(service.domains ?? []).some((domain) => domain.health_check_enabled)
|
||||||
|
}
|
||||||
ipEnabled={service.ip_enabled ?? {}}
|
ipEnabled={service.ip_enabled ?? {}}
|
||||||
ipToggleDisabled={togglingId === service.id}
|
ipToggleDisabled={togglingId === service.id}
|
||||||
togglingIp={togglingIp}
|
togglingIp={togglingIp}
|
||||||
|
|||||||
Reference in New Issue
Block a user