@checkstack/healthcheck-backend 0.0.2
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 +181 -0
- package/drizzle/0000_stormy_slayback.sql +33 -0
- package/drizzle/0001_thin_shotgun.sql +1 -0
- package/drizzle/0002_closed_lucky_pierre.sql +19 -0
- package/drizzle/0003_powerful_rage.sql +1 -0
- package/drizzle/0004_short_ezekiel.sql +1 -0
- package/drizzle/0005_glossy_longshot.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +234 -0
- package/drizzle/meta/0001_snapshot.json +240 -0
- package/drizzle/meta/0002_snapshot.json +361 -0
- package/drizzle/meta/0003_snapshot.json +367 -0
- package/drizzle/meta/0004_snapshot.json +401 -0
- package/drizzle/meta/0005_snapshot.json +401 -0
- package/drizzle/meta/_journal.json +48 -0
- package/drizzle.config.ts +7 -0
- package/package.json +37 -0
- package/src/aggregation.test.ts +373 -0
- package/src/hooks.test.ts +16 -0
- package/src/hooks.ts +35 -0
- package/src/index.ts +195 -0
- package/src/queue-executor.test.ts +229 -0
- package/src/queue-executor.ts +569 -0
- package/src/retention-job.ts +404 -0
- package/src/router.test.ts +81 -0
- package/src/router.ts +157 -0
- package/src/schema.ts +153 -0
- package/src/service.ts +718 -0
- package/src/state-evaluator.test.ts +237 -0
- package/src/state-evaluator.ts +105 -0
- package/src/state-thresholds-migrations.ts +15 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { evaluateHealthStatus } from "./state-evaluator";
|
|
3
|
+
import type {
|
|
4
|
+
HealthCheckStatus,
|
|
5
|
+
ConsecutiveThresholds,
|
|
6
|
+
WindowThresholds,
|
|
7
|
+
} from "@checkstack/healthcheck-common";
|
|
8
|
+
|
|
9
|
+
// Helper to create runs with timestamps
|
|
10
|
+
function createRuns(
|
|
11
|
+
statuses: HealthCheckStatus[]
|
|
12
|
+
): { status: HealthCheckStatus; timestamp: Date }[] {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
return statuses.map((status, i) => ({
|
|
15
|
+
status,
|
|
16
|
+
timestamp: new Date(now - i * 60000), // 1 minute apart, newest first
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("evaluateHealthStatus", () => {
|
|
21
|
+
describe("with no runs", () => {
|
|
22
|
+
test("returns healthy when no runs exist", () => {
|
|
23
|
+
const result = evaluateHealthStatus({ runs: [] });
|
|
24
|
+
expect(result).toBe("healthy");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("consecutive mode", () => {
|
|
29
|
+
const thresholds: ConsecutiveThresholds = {
|
|
30
|
+
mode: "consecutive",
|
|
31
|
+
healthy: { minSuccessCount: 2 },
|
|
32
|
+
degraded: { minFailureCount: 2 },
|
|
33
|
+
unhealthy: { minFailureCount: 4 },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
test("returns healthy after minSuccessCount consecutive successes", () => {
|
|
37
|
+
const runs = createRuns(["healthy", "healthy", "unhealthy"]);
|
|
38
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns healthy with exactly minSuccessCount successes", () => {
|
|
42
|
+
const runs = createRuns(["healthy", "healthy"]);
|
|
43
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns degraded after minFailureCount consecutive failures", () => {
|
|
47
|
+
const runs = createRuns(["unhealthy", "degraded", "healthy"]);
|
|
48
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("degraded");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("returns unhealthy after higher minFailureCount", () => {
|
|
52
|
+
const runs = createRuns([
|
|
53
|
+
"unhealthy",
|
|
54
|
+
"unhealthy",
|
|
55
|
+
"degraded",
|
|
56
|
+
"unhealthy",
|
|
57
|
+
"healthy",
|
|
58
|
+
]);
|
|
59
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("unhealthy");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("returns latest status when not enough history", () => {
|
|
63
|
+
const runs = createRuns(["healthy"]); // Only 1 run, needs 2 for healthy
|
|
64
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("handles mix of degraded and unhealthy as failures", () => {
|
|
68
|
+
const runs = createRuns(["degraded", "unhealthy"]);
|
|
69
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("degraded");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("resets count when streak breaks", () => {
|
|
73
|
+
// Latest: 1 healthy, then failures - should use latest status
|
|
74
|
+
const runs = createRuns(["healthy", "unhealthy", "unhealthy"]);
|
|
75
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("window mode", () => {
|
|
80
|
+
const thresholds: WindowThresholds = {
|
|
81
|
+
mode: "window",
|
|
82
|
+
windowSize: 5,
|
|
83
|
+
degraded: { minFailureCount: 2 },
|
|
84
|
+
unhealthy: { minFailureCount: 4 },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
test("returns healthy when failures below threshold", () => {
|
|
88
|
+
const runs = createRuns([
|
|
89
|
+
"healthy",
|
|
90
|
+
"unhealthy",
|
|
91
|
+
"healthy",
|
|
92
|
+
"healthy",
|
|
93
|
+
"healthy",
|
|
94
|
+
]);
|
|
95
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns degraded when failures at degraded threshold", () => {
|
|
99
|
+
const runs = createRuns([
|
|
100
|
+
"unhealthy",
|
|
101
|
+
"unhealthy",
|
|
102
|
+
"healthy",
|
|
103
|
+
"healthy",
|
|
104
|
+
"healthy",
|
|
105
|
+
]);
|
|
106
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("degraded");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("returns unhealthy when failures at unhealthy threshold", () => {
|
|
110
|
+
const runs = createRuns([
|
|
111
|
+
"unhealthy",
|
|
112
|
+
"degraded",
|
|
113
|
+
"unhealthy",
|
|
114
|
+
"unhealthy",
|
|
115
|
+
"healthy",
|
|
116
|
+
]);
|
|
117
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("unhealthy");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("only considers runs within window size", () => {
|
|
121
|
+
// Window is 5, so old failures outside window don't count
|
|
122
|
+
const runs = createRuns([
|
|
123
|
+
"healthy",
|
|
124
|
+
"healthy",
|
|
125
|
+
"healthy",
|
|
126
|
+
"healthy",
|
|
127
|
+
"healthy",
|
|
128
|
+
"unhealthy", // Outside window
|
|
129
|
+
"unhealthy",
|
|
130
|
+
"unhealthy",
|
|
131
|
+
"unhealthy",
|
|
132
|
+
]);
|
|
133
|
+
expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("handles window smaller than run count", () => {
|
|
137
|
+
const smallWindowThresholds: WindowThresholds = {
|
|
138
|
+
mode: "window",
|
|
139
|
+
windowSize: 3,
|
|
140
|
+
degraded: { minFailureCount: 2 },
|
|
141
|
+
unhealthy: { minFailureCount: 3 },
|
|
142
|
+
};
|
|
143
|
+
const runs = createRuns([
|
|
144
|
+
"unhealthy",
|
|
145
|
+
"unhealthy",
|
|
146
|
+
"healthy",
|
|
147
|
+
"unhealthy",
|
|
148
|
+
]);
|
|
149
|
+
expect(
|
|
150
|
+
evaluateHealthStatus({ runs, thresholds: smallWindowThresholds })
|
|
151
|
+
).toBe("degraded");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("default thresholds", () => {
|
|
156
|
+
test("uses default consecutive mode when thresholds not provided", () => {
|
|
157
|
+
// Default: healthy after 1 success, degraded after 2 failures, unhealthy after 5
|
|
158
|
+
const runs = createRuns(["healthy"]);
|
|
159
|
+
expect(evaluateHealthStatus({ runs })).toBe("healthy");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("default degraded after 2 consecutive failures", () => {
|
|
163
|
+
const runs = createRuns(["unhealthy", "unhealthy"]);
|
|
164
|
+
expect(evaluateHealthStatus({ runs })).toBe("degraded");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("default unhealthy after 5 consecutive failures", () => {
|
|
168
|
+
const runs = createRuns([
|
|
169
|
+
"unhealthy",
|
|
170
|
+
"unhealthy",
|
|
171
|
+
"unhealthy",
|
|
172
|
+
"unhealthy",
|
|
173
|
+
"unhealthy",
|
|
174
|
+
]);
|
|
175
|
+
expect(evaluateHealthStatus({ runs })).toBe("unhealthy");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("flickering scenarios", () => {
|
|
180
|
+
test("window mode handles flickering better than consecutive", () => {
|
|
181
|
+
// System that is mostly failing but occasionally succeeds
|
|
182
|
+
const runs = createRuns([
|
|
183
|
+
"unhealthy",
|
|
184
|
+
"healthy", // Flicker
|
|
185
|
+
"unhealthy",
|
|
186
|
+
"unhealthy",
|
|
187
|
+
"unhealthy",
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const consecutiveThresholds: ConsecutiveThresholds = {
|
|
191
|
+
mode: "consecutive",
|
|
192
|
+
healthy: { minSuccessCount: 1 },
|
|
193
|
+
degraded: { minFailureCount: 2 },
|
|
194
|
+
unhealthy: { minFailureCount: 3 },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const windowThresholds: WindowThresholds = {
|
|
198
|
+
mode: "window",
|
|
199
|
+
windowSize: 5,
|
|
200
|
+
degraded: { minFailureCount: 2 },
|
|
201
|
+
unhealthy: { minFailureCount: 4 },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Consecutive: sees only 1 failure at start, returns unhealthy (just the first)
|
|
205
|
+
expect(
|
|
206
|
+
evaluateHealthStatus({ runs, thresholds: consecutiveThresholds })
|
|
207
|
+
).toBe("unhealthy");
|
|
208
|
+
|
|
209
|
+
// Window: sees 4 failures in window of 5, returns unhealthy
|
|
210
|
+
expect(evaluateHealthStatus({ runs, thresholds: windowThresholds })).toBe(
|
|
211
|
+
"unhealthy"
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("window mode shows recovery when mostly healthy", () => {
|
|
216
|
+
const runs = createRuns([
|
|
217
|
+
"healthy",
|
|
218
|
+
"unhealthy", // Flicker
|
|
219
|
+
"healthy",
|
|
220
|
+
"healthy",
|
|
221
|
+
"healthy",
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
const windowThresholds: WindowThresholds = {
|
|
225
|
+
mode: "window",
|
|
226
|
+
windowSize: 5,
|
|
227
|
+
degraded: { minFailureCount: 2 },
|
|
228
|
+
unhealthy: { minFailureCount: 4 },
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Only 1 failure in window - still healthy
|
|
232
|
+
expect(evaluateHealthStatus({ runs, thresholds: windowThresholds })).toBe(
|
|
233
|
+
"healthy"
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StateThresholds,
|
|
3
|
+
ConsecutiveThresholds,
|
|
4
|
+
WindowThresholds,
|
|
5
|
+
HealthCheckStatus,
|
|
6
|
+
} from "@checkstack/healthcheck-common";
|
|
7
|
+
import { DEFAULT_STATE_THRESHOLDS } from "@checkstack/healthcheck-common";
|
|
8
|
+
|
|
9
|
+
interface RunForEvaluation {
|
|
10
|
+
status: HealthCheckStatus;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluates the current health status based on recent runs and configured thresholds.
|
|
16
|
+
* Returns the evaluated status based on the threshold mode.
|
|
17
|
+
*
|
|
18
|
+
* @param runs - Recent health check runs, sorted by timestamp descending (newest first)
|
|
19
|
+
* @param thresholds - State threshold configuration (uses defaults if not provided)
|
|
20
|
+
*/
|
|
21
|
+
export function evaluateHealthStatus(props: {
|
|
22
|
+
runs: RunForEvaluation[];
|
|
23
|
+
thresholds?: StateThresholds;
|
|
24
|
+
}): HealthCheckStatus {
|
|
25
|
+
const { runs, thresholds = DEFAULT_STATE_THRESHOLDS } = props;
|
|
26
|
+
|
|
27
|
+
if (runs.length === 0) {
|
|
28
|
+
// No runs yet - assume healthy (matches current behavior)
|
|
29
|
+
return "healthy";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return thresholds.mode === "consecutive"
|
|
33
|
+
? evaluateConsecutive({ runs, thresholds })
|
|
34
|
+
: evaluateWindow({ runs, thresholds });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Consecutive mode: evaluates based on sequential identical results from most recent.
|
|
39
|
+
*/
|
|
40
|
+
function evaluateConsecutive(props: {
|
|
41
|
+
runs: RunForEvaluation[];
|
|
42
|
+
thresholds: ConsecutiveThresholds;
|
|
43
|
+
}): HealthCheckStatus {
|
|
44
|
+
const { runs, thresholds } = props;
|
|
45
|
+
|
|
46
|
+
// Count consecutive identical results from most recent
|
|
47
|
+
let consecutiveFailures = 0;
|
|
48
|
+
let consecutiveSuccesses = 0;
|
|
49
|
+
|
|
50
|
+
for (const run of runs) {
|
|
51
|
+
if (run.status === "healthy") {
|
|
52
|
+
if (consecutiveFailures === 0) {
|
|
53
|
+
consecutiveSuccesses++;
|
|
54
|
+
} else {
|
|
55
|
+
break; // Streak ended
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// degraded or unhealthy both count as failures for threshold purposes
|
|
59
|
+
if (consecutiveSuccesses === 0) {
|
|
60
|
+
consecutiveFailures++;
|
|
61
|
+
} else {
|
|
62
|
+
break; // Streak ended
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Evaluate thresholds (unhealthy > degraded > healthy)
|
|
68
|
+
if (consecutiveFailures >= thresholds.unhealthy.minFailureCount) {
|
|
69
|
+
return "unhealthy";
|
|
70
|
+
}
|
|
71
|
+
if (consecutiveFailures >= thresholds.degraded.minFailureCount) {
|
|
72
|
+
return "degraded";
|
|
73
|
+
}
|
|
74
|
+
if (consecutiveSuccesses >= thresholds.healthy.minSuccessCount) {
|
|
75
|
+
return "healthy";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Edge case: not enough history to determine - use latest individual status
|
|
79
|
+
return runs[0].status;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Window mode: evaluates based on failure count within a sliding window.
|
|
84
|
+
* Better for flickering systems where failures are intermittent.
|
|
85
|
+
*/
|
|
86
|
+
function evaluateWindow(props: {
|
|
87
|
+
runs: RunForEvaluation[];
|
|
88
|
+
thresholds: WindowThresholds;
|
|
89
|
+
}): HealthCheckStatus {
|
|
90
|
+
const { runs, thresholds } = props;
|
|
91
|
+
|
|
92
|
+
// Take only the window size worth of runs
|
|
93
|
+
const windowRuns = runs.slice(0, thresholds.windowSize);
|
|
94
|
+
const failureCount = windowRuns.filter((r) => r.status !== "healthy").length;
|
|
95
|
+
|
|
96
|
+
// Evaluate thresholds (unhealthy > degraded > healthy)
|
|
97
|
+
if (failureCount >= thresholds.unhealthy.minFailureCount) {
|
|
98
|
+
return "unhealthy";
|
|
99
|
+
}
|
|
100
|
+
if (failureCount >= thresholds.degraded.minFailureCount) {
|
|
101
|
+
return "degraded";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return "healthy";
|
|
105
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Versioned } from "@checkstack/backend-api";
|
|
2
|
+
import {
|
|
3
|
+
StateThresholdsSchema,
|
|
4
|
+
type StateThresholds,
|
|
5
|
+
} from "@checkstack/healthcheck-common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Versioned handler for state thresholds.
|
|
9
|
+
* Provides parsing, validation, and migration capabilities.
|
|
10
|
+
*/
|
|
11
|
+
export const stateThresholds = new Versioned<StateThresholds>({
|
|
12
|
+
version: 1,
|
|
13
|
+
schema: StateThresholdsSchema,
|
|
14
|
+
migrations: [],
|
|
15
|
+
});
|