@checkstack/healthcheck-backend 1.21.3 → 1.22.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 +219 -0
- package/drizzle/0021_amazing_wolf_cub.sql +8 -0
- package/drizzle/meta/0021_snapshot.json +717 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +21 -20
- package/src/ai/system-signals-contributor.test.ts +1 -0
- package/src/cache.test.ts +3 -0
- package/src/effective-environments.test.ts +63 -2
- package/src/effective-environments.ts +34 -0
- package/src/health-entity.ts +8 -2
- package/src/health-state.ts +15 -6
- package/src/index.ts +23 -0
- package/src/queue-executor.ts +545 -565
- package/src/router-satellite-ingest.test.ts +136 -0
- package/src/router.ts +65 -4
- package/src/schema.ts +21 -0
- package/src/service-batching.test.ts +3 -1
- package/src/service-ingest-assertions.test.ts +33 -60
- package/src/service-paused-filter.test.ts +9 -4
- package/src/service-rollup-worst-wins.test.ts +147 -55
- package/src/service.ts +355 -148
- package/src/state-evaluator.test.ts +49 -0
- package/src/system-health-override.ts +6 -1
- package/tsconfig.json +3 -0
|
@@ -48,18 +48,46 @@ type Run = { status: "healthy" | "degraded" | "unhealthy"; timestamp: Date };
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Build a mock db for the rollup path. `runsByEnv` maps each environment key
|
|
51
|
-
* (`null` = env-less) to that env's runs (DESC)
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* (`null` = env-less) to that env's runs (DESC), all attributed to the LOCAL
|
|
52
|
+
* core; `satelliteRuns` adds slices probed from a satellite. `environmentIds` is
|
|
53
|
+
* the assignment's selector under test.
|
|
54
|
+
*
|
|
55
|
+
* The per-slice runs query resolves against the env id AND source id bound in
|
|
56
|
+
* its predicate. Known env ids and satellite ids are disjoint in these fixtures,
|
|
57
|
+
* so each is recovered by set membership; an absent value means the `isNull`
|
|
58
|
+
* clause, i.e. the env-less / local slice.
|
|
55
59
|
*/
|
|
56
60
|
function createRollupMockDb(props: {
|
|
57
61
|
runsByEnv: Map<string | null, Run[]>;
|
|
58
62
|
environmentIds: string[] | null;
|
|
63
|
+
satelliteRuns?: { sourceId: string; environmentId: string | null; runs: Run[] }[];
|
|
64
|
+
satelliteIds?: string[] | null;
|
|
65
|
+
satelliteEnvironmentIds?: Record<string, string[] | null> | null;
|
|
66
|
+
includeLocal?: boolean;
|
|
59
67
|
}) {
|
|
60
|
-
const {
|
|
68
|
+
const {
|
|
69
|
+
runsByEnv,
|
|
70
|
+
environmentIds,
|
|
71
|
+
satelliteRuns = [],
|
|
72
|
+
satelliteIds = null,
|
|
73
|
+
satelliteEnvironmentIds = null,
|
|
74
|
+
includeLocal = true,
|
|
75
|
+
} = props;
|
|
76
|
+
const slices = [
|
|
77
|
+
...[...runsByEnv].map(([environmentId, runs]) => ({
|
|
78
|
+
environmentId,
|
|
79
|
+
sourceId: null as string | null,
|
|
80
|
+
runs,
|
|
81
|
+
})),
|
|
82
|
+
...satelliteRuns,
|
|
83
|
+
];
|
|
61
84
|
const knownEnvIds = new Set(
|
|
62
|
-
|
|
85
|
+
slices
|
|
86
|
+
.map((s) => s.environmentId)
|
|
87
|
+
.filter((k): k is string => k !== null),
|
|
88
|
+
);
|
|
89
|
+
const knownSourceIds = new Set(
|
|
90
|
+
slices.map((s) => s.sourceId).filter((k): k is string => k !== null),
|
|
63
91
|
);
|
|
64
92
|
|
|
65
93
|
const assocWhere = mock(() =>
|
|
@@ -71,6 +99,9 @@ function createRollupMockDb(props: {
|
|
|
71
99
|
paused: false,
|
|
72
100
|
stateThresholds: null,
|
|
73
101
|
environmentIds,
|
|
102
|
+
satelliteIds,
|
|
103
|
+
satelliteEnvironmentIds,
|
|
104
|
+
includeLocal,
|
|
74
105
|
},
|
|
75
106
|
]),
|
|
76
107
|
);
|
|
@@ -81,25 +112,35 @@ function createRollupMockDb(props: {
|
|
|
81
112
|
innerJoin: mock(() => assocInnerJoin),
|
|
82
113
|
});
|
|
83
114
|
|
|
84
|
-
// Per-
|
|
85
|
-
const
|
|
115
|
+
// Per-slice runs query: pick the slice named by the predicate's env + source.
|
|
116
|
+
const resolveSlice = (predicate: unknown): Run[] => {
|
|
86
117
|
const values = collectPredicateValues(predicate);
|
|
87
|
-
const
|
|
88
|
-
|
|
118
|
+
const environmentId = values.find((v) => knownEnvIds.has(v)) ?? null;
|
|
119
|
+
const sourceId = values.find((v) => knownSourceIds.has(v)) ?? null;
|
|
120
|
+
return (
|
|
121
|
+
slices.find(
|
|
122
|
+
(s) => s.environmentId === environmentId && s.sourceId === sourceId,
|
|
123
|
+
)?.runs ?? []
|
|
124
|
+
);
|
|
89
125
|
};
|
|
90
126
|
const runsFromFor = () => {
|
|
91
127
|
const runsWhere = mock((predicate: unknown) => {
|
|
92
|
-
const rows =
|
|
128
|
+
const rows = resolveSlice(predicate);
|
|
93
129
|
const limit = mock(() => Promise.resolve(rows));
|
|
94
130
|
return { orderBy: mock(() => ({ limit })), limit };
|
|
95
131
|
});
|
|
96
132
|
return Object.assign(Promise.resolve([]), { where: runsWhere });
|
|
97
133
|
};
|
|
98
134
|
|
|
99
|
-
// Distinct
|
|
135
|
+
// Distinct slice keys query: selectDistinct({environmentId, sourceId}).
|
|
100
136
|
const distinctFrom = Object.assign(Promise.resolve([]), {
|
|
101
137
|
where: mock(() =>
|
|
102
|
-
Promise.resolve(
|
|
138
|
+
Promise.resolve(
|
|
139
|
+
slices.map(({ environmentId, sourceId }) => ({
|
|
140
|
+
environmentId,
|
|
141
|
+
sourceId,
|
|
142
|
+
})),
|
|
143
|
+
),
|
|
103
144
|
),
|
|
104
145
|
});
|
|
105
146
|
|
|
@@ -245,58 +286,109 @@ describe("HealthCheckService - system rollup worst-wins across environments", ()
|
|
|
245
286
|
expect(result.checkStatuses[0].runsConsidered).toBe(3);
|
|
246
287
|
});
|
|
247
288
|
|
|
248
|
-
it("
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
]
|
|
263
|
-
);
|
|
264
|
-
const assocInnerJoin = Object.assign(Promise.resolve([]), { where: assocWhere });
|
|
265
|
-
const assocFrom = Object.assign(Promise.resolve([]), {
|
|
266
|
-
innerJoin: mock(() => assocInnerJoin),
|
|
289
|
+
it("reports unhealthy when the LOCAL check passes but a SATELLITE check fails", async () => {
|
|
290
|
+
// The reported bug (@stuajnht): a system read HEALTHY while one of its
|
|
291
|
+
// probe locations failed every single time. Both sources' runs landed in
|
|
292
|
+
// one slice, so `evaluateConsecutive` saw healthy/unhealthy alternating,
|
|
293
|
+
// broke its streak on every run, met no threshold, and fell through to its
|
|
294
|
+
// healthy default. Sliced per source, the satellite is evaluated on its own
|
|
295
|
+
// and worst-wins carries it to the system.
|
|
296
|
+
const runsByEnv = new Map<string | null, Run[]>([[null, runs("healthy", 5)]]);
|
|
297
|
+
const mockDb = createRollupMockDb({
|
|
298
|
+
runsByEnv,
|
|
299
|
+
environmentIds: null,
|
|
300
|
+
satelliteIds: ["sat-eu"],
|
|
301
|
+
satelliteRuns: [
|
|
302
|
+
{ sourceId: "sat-eu", environmentId: null, runs: runs("unhealthy", 5) },
|
|
303
|
+
],
|
|
267
304
|
});
|
|
305
|
+
const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
|
|
306
|
+
|
|
307
|
+
const result = await service.getSystemHealthStatus("system-1");
|
|
268
308
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
309
|
+
expect(result.status).toBe("unhealthy");
|
|
310
|
+
expect(result.checkStatuses[0].status).toBe("unhealthy");
|
|
311
|
+
// Two locations probing one environment = two slices, one failing.
|
|
312
|
+
expect(result.checkStatuses[0].sliceCount).toBe(2);
|
|
313
|
+
expect(result.checkStatuses[0].failingSliceCount).toBe(1);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("names the failing location in the per-slice breakdown", async () => {
|
|
317
|
+
const runsByEnv = new Map<string | null, Run[]>([["prod", runs("healthy", 5)]]);
|
|
318
|
+
const mockDb = createRollupMockDb({
|
|
319
|
+
runsByEnv,
|
|
320
|
+
environmentIds: ["prod"],
|
|
321
|
+
satelliteIds: ["sat-eu"],
|
|
322
|
+
satelliteRuns: [
|
|
323
|
+
{ sourceId: "sat-eu", environmentId: "prod", runs: runs("unhealthy", 5) },
|
|
324
|
+
],
|
|
275
325
|
});
|
|
326
|
+
const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
|
|
276
327
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
328
|
+
const { slices } = (await service.getSystemHealthStatus("system-1")).checkStatuses[0];
|
|
329
|
+
|
|
330
|
+
expect(slices).toHaveLength(2);
|
|
331
|
+
expect(slices.find((s) => s.sourceId === null)?.status).toBe("healthy");
|
|
332
|
+
expect(slices.find((s) => s.sourceId === "sat-eu")?.status).toBe("unhealthy");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("drops a de-assigned satellite's stale failing runs", async () => {
|
|
336
|
+
// No health-change event fires for a slice that merely stopped producing
|
|
337
|
+
// runs, so without the effective-source filter an unassigned satellite's
|
|
338
|
+
// last failures would drag the rollup until they aged out of the window.
|
|
339
|
+
const runsByEnv = new Map<string | null, Run[]>([[null, runs("healthy", 5)]]);
|
|
340
|
+
const mockDb = createRollupMockDb({
|
|
341
|
+
runsByEnv,
|
|
342
|
+
environmentIds: null,
|
|
343
|
+
satelliteIds: [],
|
|
344
|
+
satelliteRuns: [
|
|
345
|
+
{ sourceId: "sat-gone", environmentId: null, runs: runs("unhealthy", 5) },
|
|
346
|
+
],
|
|
294
347
|
});
|
|
348
|
+
const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
|
|
349
|
+
|
|
350
|
+
const result = await service.getSystemHealthStatus("system-1");
|
|
351
|
+
|
|
352
|
+
expect(result.status).toBe("healthy");
|
|
353
|
+
expect(result.checkStatuses[0].sliceCount).toBe(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("keeps evaluating satellites when the core stopped running the check", async () => {
|
|
357
|
+
// `includeLocal: false` means the core's old runs no longer contribute, so
|
|
358
|
+
// the satellite alone decides.
|
|
359
|
+
const runsByEnv = new Map<string | null, Run[]>([[null, runs("healthy", 5)]]);
|
|
360
|
+
const mockDb = createRollupMockDb({
|
|
361
|
+
runsByEnv,
|
|
362
|
+
environmentIds: null,
|
|
363
|
+
includeLocal: false,
|
|
364
|
+
satelliteIds: ["sat-eu"],
|
|
365
|
+
satelliteRuns: [
|
|
366
|
+
{ sourceId: "sat-eu", environmentId: null, runs: runs("unhealthy", 5) },
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
|
|
370
|
+
|
|
371
|
+
const result = await service.getSystemHealthStatus("system-1");
|
|
295
372
|
|
|
373
|
+
expect(result.status).toBe("unhealthy");
|
|
374
|
+
expect(result.checkStatuses[0].sliceCount).toBe(1);
|
|
375
|
+
expect(result.checkStatuses[0].failingSliceCount).toBe(1);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("a pinned environment evaluates only that environment's slices", async () => {
|
|
379
|
+
// Passing an explicit environmentId narrows to that env - the other env's
|
|
380
|
+
// runs must not contribute - while the source dimension is still sliced.
|
|
381
|
+
const runsByEnv = new Map<string | null, Run[]>([
|
|
382
|
+
["prod", runs("unhealthy", 5)],
|
|
383
|
+
["staging", runs("healthy", 5)],
|
|
384
|
+
]);
|
|
385
|
+
const mockDb = createRollupMockDb({ runsByEnv, environmentIds: null });
|
|
296
386
|
const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
|
|
387
|
+
|
|
297
388
|
const result = await service.getSystemHealthStatus("system-1", "prod");
|
|
298
389
|
expect(result.status).toBe("unhealthy");
|
|
299
390
|
expect(result.checkStatuses[0].sliceCount).toBe(1);
|
|
300
391
|
expect(result.checkStatuses[0].failingSliceCount).toBe(1);
|
|
392
|
+
expect(result.checkStatuses[0].runsConsidered).toBe(5);
|
|
301
393
|
});
|
|
302
394
|
});
|