@checkstack/healthcheck-backend 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +211 -0
- package/package.json +29 -29
- package/src/aggregation-utils.test.ts +132 -0
- package/src/aggregation-utils.ts +70 -6
- package/src/collector-assertions.test.ts +97 -1
- package/src/collector-assertions.ts +141 -34
- package/src/index.ts +29 -0
- package/src/queue-executor.test.ts +182 -0
- package/src/queue-executor.ts +12 -3
- package/src/realtime-aggregation.test.ts +136 -0
- package/src/realtime-aggregation.ts +39 -2
- package/src/retention-job.ts +64 -1
- package/src/retention-rollup.test.ts +69 -0
- package/src/router.ts +92 -5
- package/src/service-ingest-assertions.test.ts +213 -0
- package/src/service.ts +179 -9
- package/src/status-page/widgets.ts +11 -1
- package/src/system-health-override.test.ts +94 -0
- package/src/system-health-override.ts +93 -0
package/src/service.ts
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
type CollectorConfigEntry,
|
|
13
13
|
type HealthcheckSignalStatuses,
|
|
14
14
|
type RunStats,
|
|
15
|
+
stripEphemeralFields,
|
|
15
16
|
} from "@checkstack/healthcheck-common";
|
|
17
|
+
import { evaluateCollectorAssertionOutcomes } from "./collector-assertions";
|
|
16
18
|
import { summarizeRuns, type StatRun } from "./run-stats.logic";
|
|
17
19
|
import type { ConfigService } from "@checkstack/backend-api";
|
|
18
20
|
import type { InferClient } from "@checkstack/common";
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
gte,
|
|
40
42
|
lte,
|
|
41
43
|
isNull,
|
|
44
|
+
isNotNull,
|
|
42
45
|
inArray,
|
|
43
46
|
} from "drizzle-orm";
|
|
44
47
|
import { ORPCError } from "@orpc/server";
|
|
@@ -57,12 +60,14 @@ import type {
|
|
|
57
60
|
import {
|
|
58
61
|
aggregateCollectorData,
|
|
59
62
|
extractLatencies,
|
|
63
|
+
foldRunAssertionStats,
|
|
60
64
|
mergeTieredBuckets,
|
|
61
65
|
reaggregateBuckets,
|
|
62
66
|
countStatuses,
|
|
63
67
|
calculateLatencyStats,
|
|
64
68
|
type NormalizedBucket,
|
|
65
69
|
} from "./aggregation-utils";
|
|
70
|
+
import { ASSERTIONS_AGG_KEY } from "@checkstack/healthcheck-common";
|
|
66
71
|
import {
|
|
67
72
|
extractConfigurationSecrets,
|
|
68
73
|
mergeConfigurationSecrets,
|
|
@@ -1337,6 +1342,98 @@ export class HealthCheckService {
|
|
|
1337
1342
|
return Object.fromEntries(entries);
|
|
1338
1343
|
}
|
|
1339
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* Bulk per-(system, check, environment) health for the given systems.
|
|
1347
|
+
*
|
|
1348
|
+
* For each system returns the cross-environment rollup (status +
|
|
1349
|
+
* checkStatuses, same as {@link getSystemHealthStatus}) PLUS a slice per
|
|
1350
|
+
* environment the system has runs for. Consumers that scope by environment
|
|
1351
|
+
* (the dependency map) must read the per-environment slice, because the
|
|
1352
|
+
* rollup deliberately hides a single failing environment.
|
|
1353
|
+
*
|
|
1354
|
+
* Cost scales with the number of environments each system actually fans out
|
|
1355
|
+
* to (`1 + #envs` status evaluations per system); systems with only env-less
|
|
1356
|
+
* runs cost the same as a plain rollup read. Not on any per-run hot path.
|
|
1357
|
+
*/
|
|
1358
|
+
async getBulkSystemHealthMatrix(systemIds: string[]): Promise<
|
|
1359
|
+
Record<
|
|
1360
|
+
string,
|
|
1361
|
+
{
|
|
1362
|
+
status: HealthCheckStatus;
|
|
1363
|
+
checkStatuses: SystemHealthStatusResponse["checkStatuses"];
|
|
1364
|
+
environments: Record<
|
|
1365
|
+
string,
|
|
1366
|
+
{
|
|
1367
|
+
status: HealthCheckStatus;
|
|
1368
|
+
checkStatuses: SystemHealthStatusResponse["checkStatuses"];
|
|
1369
|
+
}
|
|
1370
|
+
>;
|
|
1371
|
+
}
|
|
1372
|
+
>
|
|
1373
|
+
> {
|
|
1374
|
+
const result: Record<
|
|
1375
|
+
string,
|
|
1376
|
+
{
|
|
1377
|
+
status: HealthCheckStatus;
|
|
1378
|
+
checkStatuses: SystemHealthStatusResponse["checkStatuses"];
|
|
1379
|
+
environments: Record<
|
|
1380
|
+
string,
|
|
1381
|
+
{
|
|
1382
|
+
status: HealthCheckStatus;
|
|
1383
|
+
checkStatuses: SystemHealthStatusResponse["checkStatuses"];
|
|
1384
|
+
}
|
|
1385
|
+
>;
|
|
1386
|
+
}
|
|
1387
|
+
> = {};
|
|
1388
|
+
|
|
1389
|
+
await Promise.all(
|
|
1390
|
+
systemIds.map(async (systemId) => {
|
|
1391
|
+
const overall = await this.getSystemHealthStatus(systemId);
|
|
1392
|
+
|
|
1393
|
+
// Environments this system actually has runs for (env-less excluded -
|
|
1394
|
+
// it is folded into the rollup and never a real environment id).
|
|
1395
|
+
const envRows = await this.db
|
|
1396
|
+
.selectDistinct({ environmentId: healthCheckRuns.environmentId })
|
|
1397
|
+
.from(healthCheckRuns)
|
|
1398
|
+
.where(
|
|
1399
|
+
and(
|
|
1400
|
+
eq(healthCheckRuns.systemId, systemId),
|
|
1401
|
+
isNotNull(healthCheckRuns.environmentId),
|
|
1402
|
+
),
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
const environments: Record<
|
|
1406
|
+
string,
|
|
1407
|
+
{
|
|
1408
|
+
status: HealthCheckStatus;
|
|
1409
|
+
checkStatuses: SystemHealthStatusResponse["checkStatuses"];
|
|
1410
|
+
}
|
|
1411
|
+
> = {};
|
|
1412
|
+
await Promise.all(
|
|
1413
|
+
envRows.map(async ({ environmentId }) => {
|
|
1414
|
+
if (!environmentId) return;
|
|
1415
|
+
const envStatus = await this.getSystemHealthStatus(
|
|
1416
|
+
systemId,
|
|
1417
|
+
environmentId,
|
|
1418
|
+
);
|
|
1419
|
+
environments[environmentId] = {
|
|
1420
|
+
status: envStatus.status,
|
|
1421
|
+
checkStatuses: envStatus.checkStatuses,
|
|
1422
|
+
};
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
result[systemId] = {
|
|
1427
|
+
status: overall.status,
|
|
1428
|
+
checkStatuses: overall.checkStatuses,
|
|
1429
|
+
environments,
|
|
1430
|
+
};
|
|
1431
|
+
}),
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
return result;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1340
1437
|
/**
|
|
1341
1438
|
* Get comprehensive health overview for a system.
|
|
1342
1439
|
* Returns all health checks with their last 25 runs for sparkline visualization.
|
|
@@ -2151,9 +2248,16 @@ export class HealthCheckService {
|
|
|
2151
2248
|
);
|
|
2152
2249
|
}
|
|
2153
2250
|
|
|
2251
|
+
// Per-assertion pass/fail counts (platform-owned, sibling of
|
|
2252
|
+
// `collectors` — see assertion-analytics in healthcheck-common).
|
|
2253
|
+
const assertionStats = foldRunAssertionStats(bucket.runs);
|
|
2254
|
+
|
|
2154
2255
|
aggregatedResult = {
|
|
2155
2256
|
...strategyResult,
|
|
2156
2257
|
...(collectorsAggregated ? { collectors: collectorsAggregated } : {}),
|
|
2258
|
+
...(assertionStats === undefined
|
|
2259
|
+
? {}
|
|
2260
|
+
: { [ASSERTIONS_AGG_KEY]: assertionStats }),
|
|
2157
2261
|
};
|
|
2158
2262
|
}
|
|
2159
2263
|
|
|
@@ -2421,6 +2525,15 @@ export class HealthCheckService {
|
|
|
2421
2525
|
* Ingest a health check result from a satellite.
|
|
2422
2526
|
* Stores the run with source attribution (sourceId + sourceLabel)
|
|
2423
2527
|
* and triggers incremental aggregation to keep charts/availability current.
|
|
2528
|
+
*
|
|
2529
|
+
* Assertions are evaluated HERE, on the core, not on the satellite: the
|
|
2530
|
+
* satellite never held the assertion semantics, so historically
|
|
2531
|
+
* satellite-executed checks silently skipped assertions entirely.
|
|
2532
|
+
* Evaluating at ingest fixes that for every satellite version with no
|
|
2533
|
+
* wire-protocol change. Caveat: buffered results are evaluated against the
|
|
2534
|
+
* configuration CURRENT at ingest time. Ephemeral result fields (e.g. raw
|
|
2535
|
+
* HTTP bodies) are needed for JSONPath assertions and are stripped right
|
|
2536
|
+
* after evaluation, matching what the local executor stores.
|
|
2424
2537
|
*/
|
|
2425
2538
|
async ingestSatelliteResult(props: {
|
|
2426
2539
|
configId: string;
|
|
@@ -2432,20 +2545,77 @@ export class HealthCheckService {
|
|
|
2432
2545
|
sourceId: string;
|
|
2433
2546
|
sourceLabel: string;
|
|
2434
2547
|
}) {
|
|
2435
|
-
const {
|
|
2436
|
-
|
|
2437
|
-
systemId,
|
|
2438
|
-
status,
|
|
2439
|
-
latencyMs,
|
|
2440
|
-
result,
|
|
2441
|
-
sourceId,
|
|
2442
|
-
sourceLabel,
|
|
2443
|
-
} = props;
|
|
2548
|
+
const { configId, systemId, latencyMs, result, sourceId, sourceLabel } =
|
|
2549
|
+
props;
|
|
2444
2550
|
|
|
2445
2551
|
const resultRecord = result
|
|
2446
2552
|
? ({ ...result } as Record<string, unknown>)
|
|
2447
2553
|
: {};
|
|
2448
2554
|
|
|
2555
|
+
let status = props.status;
|
|
2556
|
+
const metadata = resultRecord.metadata as
|
|
2557
|
+
| Record<string, unknown>
|
|
2558
|
+
| undefined;
|
|
2559
|
+
const collectorsMeta = metadata?.collectors as
|
|
2560
|
+
| Record<string, Record<string, unknown>>
|
|
2561
|
+
| undefined;
|
|
2562
|
+
if (collectorsMeta && Object.keys(collectorsMeta).length > 0) {
|
|
2563
|
+
const [configRow] = await this.db
|
|
2564
|
+
.select({ collectors: healthCheckConfigurations.collectors })
|
|
2565
|
+
.from(healthCheckConfigurations)
|
|
2566
|
+
.where(eq(healthCheckConfigurations.id, configId));
|
|
2567
|
+
const entries: CollectorConfigEntry[] = configRow?.collectors ?? [];
|
|
2568
|
+
|
|
2569
|
+
let firstFailure: string | undefined;
|
|
2570
|
+
const nextCollectorsMeta: Record<string, Record<string, unknown>> = {
|
|
2571
|
+
...collectorsMeta,
|
|
2572
|
+
};
|
|
2573
|
+
for (const entry of entries) {
|
|
2574
|
+
const entryResult = nextCollectorsMeta[entry.id];
|
|
2575
|
+
if (!entryResult || typeof entryResult !== "object") continue;
|
|
2576
|
+
|
|
2577
|
+
let evaluated: Record<string, unknown> = { ...entryResult };
|
|
2578
|
+
if (entry.assertions?.length) {
|
|
2579
|
+
const evaluation = evaluateCollectorAssertionOutcomes({
|
|
2580
|
+
assertions: entry.assertions,
|
|
2581
|
+
result: evaluated,
|
|
2582
|
+
});
|
|
2583
|
+
evaluated._assertions = evaluation.outcomes;
|
|
2584
|
+
evaluated._assertionFailed = evaluation.firstFailureMessage;
|
|
2585
|
+
if (
|
|
2586
|
+
evaluation.firstFailureMessage !== undefined &&
|
|
2587
|
+
firstFailure === undefined
|
|
2588
|
+
) {
|
|
2589
|
+
firstFailure = evaluation.firstFailureMessage;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// Parity with the local executor: satellites send raw results, so
|
|
2594
|
+
// ephemeral fields (assertable but never persisted) get stripped
|
|
2595
|
+
// here, AFTER assertions ran against them.
|
|
2596
|
+
const registered = this.collectorRegistry.getCollector(
|
|
2597
|
+
entry.collectorId,
|
|
2598
|
+
);
|
|
2599
|
+
if (registered) {
|
|
2600
|
+
evaluated = stripEphemeralFields(
|
|
2601
|
+
evaluated,
|
|
2602
|
+
registered.collector.result.schema,
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
nextCollectorsMeta[entry.id] = evaluated;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
resultRecord.metadata = { ...metadata, collectors: nextCollectorsMeta };
|
|
2609
|
+
|
|
2610
|
+
// Mirror the local executor: a failed assertion downgrades a run the
|
|
2611
|
+
// satellite reported healthy.
|
|
2612
|
+
if (firstFailure !== undefined && status === "healthy") {
|
|
2613
|
+
status = "unhealthy";
|
|
2614
|
+
resultRecord.status = status;
|
|
2615
|
+
resultRecord.message = `Check failed: Assertion failed: ${firstFailure}`;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2449
2619
|
// Atomic: the run row and the hourly-aggregate increment it feeds must
|
|
2450
2620
|
// commit together. Without the transaction a failure on the (non-idempotent
|
|
2451
2621
|
// `runCount + 1`) aggregate left a committed run that the aggregate never
|
|
@@ -91,7 +91,17 @@ async function healthStatuses(
|
|
|
91
91
|
const { statuses } = await ctx.rpcClient
|
|
92
92
|
.forPlugin(HealthCheckApi)
|
|
93
93
|
.getBulkSystemHealthStatus({ systemIds: ids });
|
|
94
|
-
|
|
94
|
+
// Project to ONLY the derived status. `getBulkSystemHealthStatus` folds active
|
|
95
|
+
// incident overrides into `status` (so the public page shows the forced
|
|
96
|
+
// status), but the response also carries `override.reason` = the incident
|
|
97
|
+
// TITLE. Status pages are public and incidents may be hidden, so we drop
|
|
98
|
+
// everything but the status here - the incident name must never reach a public
|
|
99
|
+
// widget DTO.
|
|
100
|
+
const projected: Record<string, { status: string } | undefined> = {};
|
|
101
|
+
for (const [systemId, value] of Object.entries(statuses)) {
|
|
102
|
+
projected[systemId] = value ? { status: value.status } : undefined;
|
|
103
|
+
}
|
|
104
|
+
return projected;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
function publicStatus({
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
applySystemHealthOverrides,
|
|
4
|
+
worstHealthStatus,
|
|
5
|
+
type SystemHealthOverrideInput,
|
|
6
|
+
} from "./system-health-override";
|
|
7
|
+
import type { SystemHealthStatusResponse } from "@checkstack/healthcheck-common";
|
|
8
|
+
|
|
9
|
+
const at = new Date("2026-07-05T00:00:00.000Z");
|
|
10
|
+
|
|
11
|
+
function base(
|
|
12
|
+
status: SystemHealthStatusResponse["status"],
|
|
13
|
+
): SystemHealthStatusResponse {
|
|
14
|
+
return { status, evaluatedAt: at, checkStatuses: [] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function override(
|
|
18
|
+
status: "degraded" | "unhealthy",
|
|
19
|
+
sourceId: string,
|
|
20
|
+
): SystemHealthOverrideInput {
|
|
21
|
+
return {
|
|
22
|
+
status,
|
|
23
|
+
source: "incident",
|
|
24
|
+
reason: `Incident ${sourceId}`,
|
|
25
|
+
sourceId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("worstHealthStatus", () => {
|
|
30
|
+
it("orders unhealthy > degraded > healthy", () => {
|
|
31
|
+
expect(worstHealthStatus("healthy", "degraded")).toBe("degraded");
|
|
32
|
+
expect(worstHealthStatus("degraded", "unhealthy")).toBe("unhealthy");
|
|
33
|
+
expect(worstHealthStatus("unhealthy", "degraded")).toBe("unhealthy");
|
|
34
|
+
expect(worstHealthStatus("healthy", "healthy")).toBe("healthy");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("applySystemHealthOverrides", () => {
|
|
39
|
+
it("returns the base unchanged (no override field) when there are no overrides", () => {
|
|
40
|
+
const result = applySystemHealthOverrides({
|
|
41
|
+
base: base("healthy"),
|
|
42
|
+
overrides: [],
|
|
43
|
+
});
|
|
44
|
+
expect(result.status).toBe("healthy");
|
|
45
|
+
expect(result.override).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("raises a healthy system to the override status and records the source", () => {
|
|
49
|
+
const result = applySystemHealthOverrides({
|
|
50
|
+
base: base("healthy"),
|
|
51
|
+
overrides: [override("unhealthy", "inc-1")],
|
|
52
|
+
});
|
|
53
|
+
expect(result.status).toBe("unhealthy");
|
|
54
|
+
expect(result.override).toEqual({
|
|
55
|
+
status: "unhealthy",
|
|
56
|
+
source: "incident",
|
|
57
|
+
reason: "Incident inc-1",
|
|
58
|
+
sourceId: "inc-1",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("applies an override even when the system has no health checks", () => {
|
|
63
|
+
const result = applySystemHealthOverrides({
|
|
64
|
+
base: base("healthy"), // no-checks default is healthy
|
|
65
|
+
overrides: [override("degraded", "inc-2")],
|
|
66
|
+
});
|
|
67
|
+
expect(result.status).toBe("degraded");
|
|
68
|
+
expect(result.override?.status).toBe("degraded");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps the worse HEALTH CHECK status when a check is worse than the override", () => {
|
|
72
|
+
// Override says degraded, but a check reports unhealthy -> unhealthy wins.
|
|
73
|
+
const result = applySystemHealthOverrides({
|
|
74
|
+
base: base("unhealthy"),
|
|
75
|
+
overrides: [override("degraded", "inc-3")],
|
|
76
|
+
});
|
|
77
|
+
expect(result.status).toBe("unhealthy");
|
|
78
|
+
// The override is still surfaced (it contributed), at its own status.
|
|
79
|
+
expect(result.override?.status).toBe("degraded");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("picks the worst override when several incidents override the same system", () => {
|
|
83
|
+
const result = applySystemHealthOverrides({
|
|
84
|
+
base: base("healthy"),
|
|
85
|
+
overrides: [
|
|
86
|
+
override("degraded", "inc-a"),
|
|
87
|
+
override("unhealthy", "inc-b"),
|
|
88
|
+
override("degraded", "inc-c"),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
expect(result.status).toBe("unhealthy");
|
|
92
|
+
expect(result.override?.sourceId).toBe("inc-b");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HealthCheckStatus,
|
|
3
|
+
SystemHealthStatusResponse,
|
|
4
|
+
SystemHealthOverride,
|
|
5
|
+
} from "@checkstack/healthcheck-common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Worst-wins ordering for the derived health vocabulary: a higher rank is a
|
|
9
|
+
* worse status. Mirrors the inline `unhealthy > degraded > healthy` ordering the
|
|
10
|
+
* per-check rollup uses in `service.ts`, extracted so the incident-override fold
|
|
11
|
+
* shares the exact same comparison.
|
|
12
|
+
*/
|
|
13
|
+
const HEALTH_RANK: Record<HealthCheckStatus, number> = {
|
|
14
|
+
healthy: 0,
|
|
15
|
+
degraded: 1,
|
|
16
|
+
unhealthy: 2,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Returns whichever status is worse (ties return `a`). */
|
|
20
|
+
export function worstHealthStatus(
|
|
21
|
+
a: HealthCheckStatus,
|
|
22
|
+
b: HealthCheckStatus,
|
|
23
|
+
): HealthCheckStatus {
|
|
24
|
+
return HEALTH_RANK[b] > HEALTH_RANK[a] ? b : a;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A non-health-check contribution to a system's health, as consumed by the
|
|
29
|
+
* fold. Kept source-agnostic (`source`/`sourceId`) so the health plugin does not
|
|
30
|
+
* hard-code incident semantics; the incident-backed reader maps its rows into
|
|
31
|
+
* this shape.
|
|
32
|
+
*/
|
|
33
|
+
export interface SystemHealthOverrideInput {
|
|
34
|
+
/** The status this contributor forces. Never `healthy` in practice. */
|
|
35
|
+
status: HealthCheckStatus;
|
|
36
|
+
/** Contributor kind, e.g. "incident". */
|
|
37
|
+
source: string;
|
|
38
|
+
/** Human-readable reason, e.g. the incident title. */
|
|
39
|
+
reason: string;
|
|
40
|
+
/** Opaque id of the contributing record, e.g. the incident id. */
|
|
41
|
+
sourceId?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads active health overrides for a set of systems. Implemented in the plugin
|
|
46
|
+
* wiring over the incident RPC; injected into the health service so the service
|
|
47
|
+
* stays free of a direct incident dependency and tests can stub it.
|
|
48
|
+
*/
|
|
49
|
+
export interface SystemHealthOverrideReader {
|
|
50
|
+
getActiveOverrides(
|
|
51
|
+
systemIds: string[],
|
|
52
|
+
): Promise<Record<string, SystemHealthOverrideInput[]>>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fold a system's active overrides into its health-check-derived status via
|
|
57
|
+
* worst-wins. The overall status becomes the worst of the checks-derived status
|
|
58
|
+
* and every override, so an override that raises a system to `degraded` never
|
|
59
|
+
* masks a health check reporting `unhealthy` (the worse status always wins), and
|
|
60
|
+
* an override applies even when the system has no health checks at all.
|
|
61
|
+
*
|
|
62
|
+
* The worst contributing override (if any) is surfaced on `override` so a UI can
|
|
63
|
+
* explain why a system reads worse than its checks alone. When the overrides are
|
|
64
|
+
* empty the base response is returned unchanged (no `override`).
|
|
65
|
+
*/
|
|
66
|
+
export function applySystemHealthOverrides({
|
|
67
|
+
base,
|
|
68
|
+
overrides,
|
|
69
|
+
}: {
|
|
70
|
+
base: SystemHealthStatusResponse;
|
|
71
|
+
overrides: SystemHealthOverrideInput[];
|
|
72
|
+
}): SystemHealthStatusResponse {
|
|
73
|
+
if (overrides.length === 0) return base;
|
|
74
|
+
|
|
75
|
+
// The override that reads worst wins the `override` slot; a tie keeps the
|
|
76
|
+
// first (query order), which for incidents is a stable, arbitrary pick.
|
|
77
|
+
let worst = overrides[0]!;
|
|
78
|
+
for (const candidate of overrides) {
|
|
79
|
+
if (HEALTH_RANK[candidate.status] > HEALTH_RANK[worst.status]) {
|
|
80
|
+
worst = candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const status = worstHealthStatus(base.status, worst.status);
|
|
85
|
+
const override: SystemHealthOverride = {
|
|
86
|
+
status: worst.status,
|
|
87
|
+
source: worst.source,
|
|
88
|
+
reason: worst.reason,
|
|
89
|
+
sourceId: worst.sourceId,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return { ...base, status, override };
|
|
93
|
+
}
|