feat(api, web): integrate health status management for services and domains
Build, Test, and Push CFDM Docker Image / test (push) Successful in 10m17s
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 10m17s
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
- Added health status and latency fields to service and domain schemas, enhancing monitoring capabilities. - Implemented health status aggregation for services in the API, allowing for improved health checks and reporting. - Updated web components to display health status using HealthCheckBadge, improving user visibility of service health. - Refactored service and domain management components to incorporate health status in various views, enhancing overall functionality. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Vendored
+15
-2
File diff suppressed because one or more lines are too long
Vendored
+106
@@ -415,6 +415,8 @@ function getAppSwitcher(db) {
|
||||
var repos_exports = {};
|
||||
__export(repos_exports, {
|
||||
addDomainTags: () => addDomainTags,
|
||||
aggregateIpHealthByRefs: () => aggregateIpHealthByRefs,
|
||||
aggregateIpHealthByServiceIds: () => aggregateIpHealthByServiceIds,
|
||||
bindingsToRemove: () => bindingsToRemove,
|
||||
countCertificatesByStatus: () => countCertificatesByStatus,
|
||||
createDomain: () => createDomain,
|
||||
@@ -489,6 +491,7 @@ __export(repos_exports, {
|
||||
listSubdomainsByDomain: () => listSubdomainsByDomain,
|
||||
listUngroupedServices: () => listUngroupedServices,
|
||||
markDnsPendingDelete: () => markDnsPendingDelete,
|
||||
mergeHealthAggregates: () => mergeHealthAggregates,
|
||||
reorderServices: () => reorderServices,
|
||||
replaceBindingIps: () => replaceBindingIps,
|
||||
replaceBindingIpsWithMeta: () => replaceBindingIpsWithMeta,
|
||||
@@ -1294,6 +1297,109 @@ function listIpHealthStatus(db, scope, refId) {
|
||||
WHERE scope = ${scope} AND ref_id = ${refId}
|
||||
`);
|
||||
}
|
||||
var UNKNOWN_HEALTH = {
|
||||
health_status: "unknown",
|
||||
health_latency_ms: null
|
||||
};
|
||||
function parseHealthAggregateRow(row) {
|
||||
const status = row.health_status;
|
||||
if (status === "up" || status === "down" || status === "degraded" || status === "unknown") {
|
||||
return {
|
||||
health_status: status,
|
||||
health_latency_ms: row.health_latency_ms ?? null
|
||||
};
|
||||
}
|
||||
return UNKNOWN_HEALTH;
|
||||
}
|
||||
var WORST_HEALTH_SQL = sql2.raw(`CASE
|
||||
WHEN MAX(CASE
|
||||
WHEN status = 'down' THEN 3
|
||||
WHEN status = 'degraded' THEN 2
|
||||
WHEN status = 'up' THEN 1
|
||||
ELSE 0
|
||||
END) = 3 THEN 'down'
|
||||
WHEN MAX(CASE
|
||||
WHEN status = 'down' THEN 3
|
||||
WHEN status = 'degraded' THEN 2
|
||||
WHEN status = 'up' THEN 1
|
||||
ELSE 0
|
||||
END) = 2 THEN 'degraded'
|
||||
WHEN MAX(CASE
|
||||
WHEN status = 'down' THEN 3
|
||||
WHEN status = 'degraded' THEN 2
|
||||
WHEN status = 'up' THEN 1
|
||||
ELSE 0
|
||||
END) = 1 THEN 'up'
|
||||
ELSE 'unknown'
|
||||
END`);
|
||||
function aggregateIpHealthByRefs(db, scope, refIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (refIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
refIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT ref_id,
|
||||
${WORST_HEALTH_SQL} AS health_status,
|
||||
MAX(latency_ms) AS health_latency_ms
|
||||
FROM ip_health_status
|
||||
WHERE scope = ${scope} AND ref_id IN (${idList})
|
||||
GROUP BY ref_id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
result.set(row.ref_id, parseHealthAggregateRow(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function aggregateIpHealthByServiceIds(db, serviceIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
serviceIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT sb.service_id AS service_id,
|
||||
${WORST_HEALTH_SQL} AS health_status,
|
||||
MAX(ihs.latency_ms) AS health_latency_ms
|
||||
FROM ip_health_status ihs
|
||||
INNER JOIN service_bindings sb
|
||||
ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
|
||||
WHERE sb.service_id IN (${idList})
|
||||
GROUP BY sb.service_id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
result.set(row.service_id, parseHealthAggregateRow(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function mergeHealthAggregates(parts) {
|
||||
const rank = {
|
||||
unknown: 0,
|
||||
up: 1,
|
||||
degraded: 2,
|
||||
down: 3
|
||||
};
|
||||
let worst = UNKNOWN_HEALTH;
|
||||
let hasAny = false;
|
||||
for (const part of parts) {
|
||||
if (!part) continue;
|
||||
hasAny = true;
|
||||
if (rank[part.health_status] > rank[worst.health_status]) {
|
||||
worst = {
|
||||
health_status: part.health_status,
|
||||
health_latency_ms: part.health_latency_ms
|
||||
};
|
||||
} else if (part.health_status === worst.health_status && part.health_latency_ms != null && (worst.health_latency_ms == null || part.health_latency_ms > worst.health_latency_ms)) {
|
||||
worst = {
|
||||
health_status: worst.health_status,
|
||||
health_latency_ms: part.health_latency_ms
|
||||
};
|
||||
}
|
||||
}
|
||||
return hasAny ? worst : UNKNOWN_HEALTH;
|
||||
}
|
||||
function getIpHealthStatusRow(db, scope, refId, ip) {
|
||||
const rows = db.all(sql2`
|
||||
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
HealthCheckScope,
|
||||
HealthCheckTarget,
|
||||
HealthCheckType,
|
||||
IpHealthState,
|
||||
IpHealthStatus,
|
||||
LbMode,
|
||||
Service,
|
||||
@@ -1466,6 +1467,152 @@ export function listIpHealthStatus(
|
||||
`);
|
||||
}
|
||||
|
||||
export type HealthAggregate = {
|
||||
health_status: IpHealthState;
|
||||
health_latency_ms: number | null;
|
||||
};
|
||||
|
||||
const UNKNOWN_HEALTH: HealthAggregate = {
|
||||
health_status: "unknown",
|
||||
health_latency_ms: null,
|
||||
};
|
||||
|
||||
function parseHealthAggregateRow(row: {
|
||||
health_status: string | null;
|
||||
health_latency_ms: number | null;
|
||||
}): HealthAggregate {
|
||||
const status = row.health_status;
|
||||
if (
|
||||
status === "up" ||
|
||||
status === "down" ||
|
||||
status === "degraded" ||
|
||||
status === "unknown"
|
||||
) {
|
||||
return {
|
||||
health_status: status,
|
||||
health_latency_ms: row.health_latency_ms ?? null,
|
||||
};
|
||||
}
|
||||
return UNKNOWN_HEALTH;
|
||||
}
|
||||
|
||||
const WORST_HEALTH_SQL = sql.raw(`CASE
|
||||
WHEN MAX(CASE
|
||||
WHEN status = 'down' THEN 3
|
||||
WHEN status = 'degraded' THEN 2
|
||||
WHEN status = 'up' THEN 1
|
||||
ELSE 0
|
||||
END) = 3 THEN 'down'
|
||||
WHEN MAX(CASE
|
||||
WHEN status = 'down' THEN 3
|
||||
WHEN status = 'degraded' THEN 2
|
||||
WHEN status = 'up' THEN 1
|
||||
ELSE 0
|
||||
END) = 2 THEN 'degraded'
|
||||
WHEN MAX(CASE
|
||||
WHEN status = 'down' THEN 3
|
||||
WHEN status = 'degraded' THEN 2
|
||||
WHEN status = 'up' THEN 1
|
||||
ELSE 0
|
||||
END) = 1 THEN 'up'
|
||||
ELSE 'unknown'
|
||||
END`);
|
||||
|
||||
/** Worst status across ip_health_status rows for each ref_id in a scope. */
|
||||
export function aggregateIpHealthByRefs(
|
||||
db: Db,
|
||||
scope: HealthCheckScope,
|
||||
refIds: number[],
|
||||
): Map<number, HealthAggregate> {
|
||||
const result = new Map<number, HealthAggregate>();
|
||||
if (refIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
refIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<{
|
||||
ref_id: number;
|
||||
health_status: string | null;
|
||||
health_latency_ms: number | null;
|
||||
}>(sql`
|
||||
SELECT ref_id,
|
||||
${WORST_HEALTH_SQL} AS health_status,
|
||||
MAX(latency_ms) AS health_latency_ms
|
||||
FROM ip_health_status
|
||||
WHERE scope = ${scope} AND ref_id IN (${idList})
|
||||
GROUP BY ref_id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
result.set(row.ref_id, parseHealthAggregateRow(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Worst binding-scope health rolled up per service_id. */
|
||||
export function aggregateIpHealthByServiceIds(
|
||||
db: Db,
|
||||
serviceIds: number[],
|
||||
): Map<number, HealthAggregate> {
|
||||
const result = new Map<number, HealthAggregate>();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
serviceIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<{
|
||||
service_id: number;
|
||||
health_status: string | null;
|
||||
health_latency_ms: number | null;
|
||||
}>(sql`
|
||||
SELECT sb.service_id AS service_id,
|
||||
${WORST_HEALTH_SQL} AS health_status,
|
||||
MAX(ihs.latency_ms) AS health_latency_ms
|
||||
FROM ip_health_status ihs
|
||||
INNER JOIN service_bindings sb
|
||||
ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
|
||||
WHERE sb.service_id IN (${idList})
|
||||
GROUP BY sb.service_id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
result.set(row.service_id, parseHealthAggregateRow(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mergeHealthAggregates(
|
||||
parts: Array<HealthAggregate | undefined | null>,
|
||||
): HealthAggregate {
|
||||
const rank: Record<IpHealthState, number> = {
|
||||
unknown: 0,
|
||||
up: 1,
|
||||
degraded: 2,
|
||||
down: 3,
|
||||
};
|
||||
let worst: HealthAggregate = UNKNOWN_HEALTH;
|
||||
let hasAny = false;
|
||||
for (const part of parts) {
|
||||
if (!part) continue;
|
||||
hasAny = true;
|
||||
if (rank[part.health_status] > rank[worst.health_status]) {
|
||||
worst = {
|
||||
health_status: part.health_status,
|
||||
health_latency_ms: part.health_latency_ms,
|
||||
};
|
||||
} else if (
|
||||
part.health_status === worst.health_status &&
|
||||
part.health_latency_ms != null &&
|
||||
(worst.health_latency_ms == null ||
|
||||
part.health_latency_ms > worst.health_latency_ms)
|
||||
) {
|
||||
worst = {
|
||||
health_status: worst.health_status,
|
||||
health_latency_ms: part.health_latency_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
return hasAny ? worst : UNKNOWN_HEALTH;
|
||||
}
|
||||
|
||||
export function getIpHealthStatusRow(
|
||||
db: Db,
|
||||
scope: HealthCheckScope,
|
||||
|
||||
Reference in New Issue
Block a user