@checkstack/healthcheck-backend 0.4.0 → 0.4.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 +40 -0
- package/package.json +1 -1
- package/src/aggregation.test.ts +45 -82
- package/src/index.ts +2 -2
- package/src/queue-executor.ts +2 -2
- package/src/retention-job.ts +21 -18
- package/src/router.ts +15 -15
- package/src/service.ts +48 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 66a3963: Fix 500 error on `getDetailedAggregatedHistory` and update to SafeDatabase type
|
|
8
|
+
|
|
9
|
+
- Fixed runtime error caused by usage of Drizzle relational query API (`db.query`) in `getAggregatedHistory`
|
|
10
|
+
- Replaced `db.query.healthCheckConfigurations.findFirst()` with standard `db.select()` query
|
|
11
|
+
- Updated all database type declarations from `NodePgDatabase` to `SafeDatabase`
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [2c0822d]
|
|
14
|
+
- Updated dependencies [66a3963]
|
|
15
|
+
- Updated dependencies [66a3963]
|
|
16
|
+
- Updated dependencies [66a3963]
|
|
17
|
+
- @checkstack/queue-api@0.2.0
|
|
18
|
+
- @checkstack/catalog-backend@0.2.7
|
|
19
|
+
- @checkstack/integration-backend@0.1.6
|
|
20
|
+
- @checkstack/backend-api@0.5.0
|
|
21
|
+
- @checkstack/command-backend@0.1.6
|
|
22
|
+
|
|
23
|
+
## 0.4.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [8a87cd4]
|
|
28
|
+
- Updated dependencies [8a87cd4]
|
|
29
|
+
- Updated dependencies [8a87cd4]
|
|
30
|
+
- Updated dependencies [8a87cd4]
|
|
31
|
+
- Updated dependencies [8a87cd4]
|
|
32
|
+
- @checkstack/backend-api@0.4.1
|
|
33
|
+
- @checkstack/catalog-common@1.2.3
|
|
34
|
+
- @checkstack/common@0.5.0
|
|
35
|
+
- @checkstack/healthcheck-common@0.4.2
|
|
36
|
+
- @checkstack/maintenance-common@0.4.1
|
|
37
|
+
- @checkstack/catalog-backend@0.2.6
|
|
38
|
+
- @checkstack/command-backend@0.1.5
|
|
39
|
+
- @checkstack/integration-backend@0.1.5
|
|
40
|
+
- @checkstack/queue-api@0.1.3
|
|
41
|
+
- @checkstack/signal-common@0.1.3
|
|
42
|
+
|
|
3
43
|
## 0.4.0
|
|
4
44
|
|
|
5
45
|
### Minor Changes
|
package/package.json
CHANGED
package/src/aggregation.test.ts
CHANGED
|
@@ -6,25 +6,27 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
6
6
|
let mockDb: ReturnType<typeof createMockDb>;
|
|
7
7
|
let mockRegistry: ReturnType<typeof createMockRegistry>;
|
|
8
8
|
let service: HealthCheckService;
|
|
9
|
+
// Store mock data for different queries
|
|
10
|
+
let mockConfigResult: { id: string; strategyId: string } | null = null;
|
|
11
|
+
let mockRunsResult: unknown[] = [];
|
|
9
12
|
|
|
10
13
|
function createMockDb() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Create a mock that handles both config queries (with limit) and runs queries (with orderBy)
|
|
15
|
+
const createSelectChain = () => ({
|
|
16
|
+
from: mock(() => ({
|
|
17
|
+
where: mock(() => ({
|
|
18
|
+
// For config query: uses .limit(1)
|
|
19
|
+
limit: mock(() =>
|
|
20
|
+
Promise.resolve(mockConfigResult ? [mockConfigResult] : []),
|
|
21
|
+
),
|
|
22
|
+
// For runs query: uses .orderBy()
|
|
23
|
+
orderBy: mock(() => Promise.resolve(mockRunsResult)),
|
|
17
24
|
})),
|
|
18
25
|
})),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
() => Promise<{ id: string; strategyId: string } | null>
|
|
24
|
-
>
|
|
25
|
-
>,
|
|
26
|
-
},
|
|
27
|
-
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
select: mock(createSelectChain),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -47,6 +49,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
beforeEach(() => {
|
|
52
|
+
// Reset mock data
|
|
53
|
+
mockConfigResult = null;
|
|
54
|
+
mockRunsResult = [];
|
|
50
55
|
mockDb = createMockDb();
|
|
51
56
|
mockRegistry = createMockRegistry();
|
|
52
57
|
service = new HealthCheckService(mockDb as never, mockRegistry as never);
|
|
@@ -65,7 +70,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
65
70
|
endDate,
|
|
66
71
|
bucketSize: "auto",
|
|
67
72
|
},
|
|
68
|
-
{ includeAggregatedResult: true }
|
|
73
|
+
{ includeAggregatedResult: true },
|
|
69
74
|
);
|
|
70
75
|
|
|
71
76
|
expect(result.buckets).toEqual([]);
|
|
@@ -83,7 +88,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
83
88
|
endDate,
|
|
84
89
|
bucketSize: "auto",
|
|
85
90
|
},
|
|
86
|
-
{ includeAggregatedResult: true }
|
|
91
|
+
{ includeAggregatedResult: true },
|
|
87
92
|
);
|
|
88
93
|
|
|
89
94
|
expect(result.buckets).toEqual([]);
|
|
@@ -122,18 +127,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
122
127
|
},
|
|
123
128
|
];
|
|
124
129
|
|
|
125
|
-
// Setup mock
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
where: mock(() => ({
|
|
129
|
-
orderBy: mock(() => Promise.resolve(runs)),
|
|
130
|
-
})),
|
|
131
|
-
})),
|
|
132
|
-
}));
|
|
133
|
-
|
|
134
|
-
mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
|
|
135
|
-
Promise.resolve({ id: "config-1", strategyId: "http" })
|
|
136
|
-
);
|
|
130
|
+
// Setup mock data
|
|
131
|
+
mockRunsResult = runs;
|
|
132
|
+
mockConfigResult = { id: "config-1", strategyId: "http" };
|
|
137
133
|
|
|
138
134
|
const result = await service.getAggregatedHistory(
|
|
139
135
|
{
|
|
@@ -143,14 +139,14 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
143
139
|
endDate: new Date("2024-01-01T23:59:59Z"),
|
|
144
140
|
bucketSize: "hourly",
|
|
145
141
|
},
|
|
146
|
-
{ includeAggregatedResult: true }
|
|
142
|
+
{ includeAggregatedResult: true },
|
|
147
143
|
);
|
|
148
144
|
|
|
149
145
|
expect(result.buckets).toHaveLength(2);
|
|
150
146
|
|
|
151
147
|
// First bucket (10:00)
|
|
152
148
|
const bucket10 = result.buckets.find(
|
|
153
|
-
(b) => b.bucketStart.getHours() === 10
|
|
149
|
+
(b) => b.bucketStart.getHours() === 10,
|
|
154
150
|
);
|
|
155
151
|
expect(bucket10).toBeDefined();
|
|
156
152
|
expect(bucket10!.runCount).toBe(2);
|
|
@@ -161,7 +157,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
161
157
|
|
|
162
158
|
// Second bucket (11:00)
|
|
163
159
|
const bucket11 = result.buckets.find(
|
|
164
|
-
(b) => b.bucketStart.getHours() === 11
|
|
160
|
+
(b) => b.bucketStart.getHours() === 11,
|
|
165
161
|
);
|
|
166
162
|
expect(bucket11).toBeDefined();
|
|
167
163
|
expect(bucket11!.runCount).toBe(1);
|
|
@@ -182,17 +178,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
182
178
|
timestamp: new Date("2024-01-01T10:00:00Z"),
|
|
183
179
|
}));
|
|
184
180
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
orderBy: mock(() => Promise.resolve(runs)),
|
|
189
|
-
})),
|
|
190
|
-
})),
|
|
191
|
-
}));
|
|
192
|
-
|
|
193
|
-
mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
|
|
194
|
-
Promise.resolve({ id: "config-1", strategyId: "http" })
|
|
195
|
-
);
|
|
181
|
+
// Setup mock data
|
|
182
|
+
mockRunsResult = runs;
|
|
183
|
+
mockConfigResult = { id: "config-1", strategyId: "http" };
|
|
196
184
|
|
|
197
185
|
const result = await service.getAggregatedHistory(
|
|
198
186
|
{
|
|
@@ -202,7 +190,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
202
190
|
endDate: new Date("2024-01-01T23:59:59Z"),
|
|
203
191
|
bucketSize: "hourly",
|
|
204
192
|
},
|
|
205
|
-
{ includeAggregatedResult: true }
|
|
193
|
+
{ includeAggregatedResult: true },
|
|
206
194
|
);
|
|
207
195
|
|
|
208
196
|
expect(result.buckets).toHaveLength(1);
|
|
@@ -224,17 +212,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
224
212
|
},
|
|
225
213
|
];
|
|
226
214
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
orderBy: mock(() => Promise.resolve(runs)),
|
|
231
|
-
})),
|
|
232
|
-
})),
|
|
233
|
-
}));
|
|
234
|
-
|
|
235
|
-
mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
|
|
236
|
-
Promise.resolve({ id: "config-1", strategyId: "http" })
|
|
237
|
-
);
|
|
215
|
+
// Setup mock data
|
|
216
|
+
mockRunsResult = runs;
|
|
217
|
+
mockConfigResult = { id: "config-1", strategyId: "http" };
|
|
238
218
|
|
|
239
219
|
const result = await service.getAggregatedHistory(
|
|
240
220
|
{
|
|
@@ -244,7 +224,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
244
224
|
endDate: new Date("2024-01-01T23:59:59Z"),
|
|
245
225
|
bucketSize: "hourly",
|
|
246
226
|
},
|
|
247
|
-
{ includeAggregatedResult: true }
|
|
227
|
+
{ includeAggregatedResult: true },
|
|
248
228
|
);
|
|
249
229
|
|
|
250
230
|
const bucket = result.buckets[0];
|
|
@@ -270,18 +250,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
270
250
|
},
|
|
271
251
|
];
|
|
272
252
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
orderBy: mock(() => Promise.resolve(runs)),
|
|
277
|
-
})),
|
|
278
|
-
})),
|
|
279
|
-
}));
|
|
280
|
-
|
|
281
|
-
// No config found means no strategy
|
|
282
|
-
mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
|
|
283
|
-
Promise.resolve(null)
|
|
284
|
-
);
|
|
253
|
+
// Setup mock data - no config found means no strategy
|
|
254
|
+
mockRunsResult = runs;
|
|
255
|
+
mockConfigResult = null;
|
|
285
256
|
|
|
286
257
|
const result = await service.getAggregatedHistory(
|
|
287
258
|
{
|
|
@@ -291,12 +262,12 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
291
262
|
endDate: new Date("2024-01-01T23:59:59Z"),
|
|
292
263
|
bucketSize: "hourly",
|
|
293
264
|
},
|
|
294
|
-
{ includeAggregatedResult: true }
|
|
265
|
+
{ includeAggregatedResult: true },
|
|
295
266
|
);
|
|
296
267
|
|
|
297
268
|
const bucket = result.buckets[0];
|
|
298
269
|
expect(
|
|
299
|
-
"aggregatedResult" in bucket ? bucket.aggregatedResult : undefined
|
|
270
|
+
"aggregatedResult" in bucket ? bucket.aggregatedResult : undefined,
|
|
300
271
|
).toBeUndefined();
|
|
301
272
|
});
|
|
302
273
|
});
|
|
@@ -333,17 +304,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
333
304
|
},
|
|
334
305
|
];
|
|
335
306
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
orderBy: mock(() => Promise.resolve(runs)),
|
|
340
|
-
})),
|
|
341
|
-
})),
|
|
342
|
-
}));
|
|
343
|
-
|
|
344
|
-
mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
|
|
345
|
-
Promise.resolve({ id: "config-1", strategyId: "http" })
|
|
346
|
-
);
|
|
307
|
+
// Setup mock data
|
|
308
|
+
mockRunsResult = runs;
|
|
309
|
+
mockConfigResult = { id: "config-1", strategyId: "http" };
|
|
347
310
|
|
|
348
311
|
const result = await service.getAggregatedHistory(
|
|
349
312
|
{
|
|
@@ -353,7 +316,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
|
|
|
353
316
|
endDate: new Date("2024-01-03T00:00:00Z"),
|
|
354
317
|
bucketSize: "daily",
|
|
355
318
|
},
|
|
356
|
-
{ includeAggregatedResult: true }
|
|
319
|
+
{ includeAggregatedResult: true },
|
|
357
320
|
);
|
|
358
321
|
|
|
359
322
|
expect(result.buckets).toHaveLength(2);
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
bootstrapHealthChecks,
|
|
4
4
|
} from "./queue-executor";
|
|
5
5
|
import * as schema from "./schema";
|
|
6
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
6
|
import {
|
|
8
7
|
healthCheckAccessRules,
|
|
9
8
|
healthCheckAccess,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
createBackendPlugin,
|
|
16
15
|
coreServices,
|
|
17
16
|
type EmitHookFn,
|
|
17
|
+
type SafeDatabase,
|
|
18
18
|
} from "@checkstack/backend-api";
|
|
19
19
|
import { integrationEventExtensionPoint } from "@checkstack/integration-backend";
|
|
20
20
|
import { z } from "zod";
|
|
@@ -130,7 +130,7 @@ export default createBackendPlugin({
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
const healthCheckRouter = createHealthCheckRouter(
|
|
133
|
-
database as
|
|
133
|
+
database as SafeDatabase<typeof schema>,
|
|
134
134
|
healthCheckRegistry,
|
|
135
135
|
);
|
|
136
136
|
rpc.registerRouter(healthCheckRouter, healthCheckContract);
|
package/src/queue-executor.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type EmitHookFn,
|
|
5
5
|
type CollectorRegistry,
|
|
6
6
|
evaluateAssertions,
|
|
7
|
+
type SafeDatabase,
|
|
7
8
|
} from "@checkstack/backend-api";
|
|
8
9
|
import { QueueManager } from "@checkstack/queue-api";
|
|
9
10
|
import {
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
} from "./schema";
|
|
14
15
|
import * as schema from "./schema";
|
|
15
16
|
import { eq, and, max } from "drizzle-orm";
|
|
16
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
17
17
|
import { type SignalService } from "@checkstack/signal-common";
|
|
18
18
|
import {
|
|
19
19
|
HEALTH_CHECK_RUN_COMPLETED,
|
|
@@ -25,7 +25,7 @@ import { resolveRoute, type InferClient } from "@checkstack/common";
|
|
|
25
25
|
import { HealthCheckService } from "./service";
|
|
26
26
|
import { healthCheckHooks } from "./hooks";
|
|
27
27
|
|
|
28
|
-
type Db =
|
|
28
|
+
type Db = SafeDatabase<typeof schema>;
|
|
29
29
|
type CatalogClient = InferClient<typeof CatalogApi>;
|
|
30
30
|
type MaintenanceClient = InferClient<typeof MaintenanceApi>;
|
|
31
31
|
|
package/src/retention-job.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
|
+
HealthCheckRegistry,
|
|
3
|
+
Logger,
|
|
4
|
+
SafeDatabase,
|
|
5
|
+
} from "@checkstack/backend-api";
|
|
2
6
|
import * as schema from "./schema";
|
|
3
7
|
import {
|
|
4
8
|
healthCheckRuns,
|
|
5
9
|
systemHealthChecks,
|
|
6
10
|
healthCheckAggregates,
|
|
11
|
+
healthCheckConfigurations,
|
|
7
12
|
DEFAULT_RETENTION_CONFIG,
|
|
8
13
|
} from "./schema";
|
|
9
14
|
import { eq, and, lt, sql } from "drizzle-orm";
|
|
10
|
-
import type {
|
|
11
|
-
HealthCheckRegistry,
|
|
12
|
-
Logger,
|
|
13
|
-
} from "@checkstack/backend-api";
|
|
14
15
|
import type { QueueManager } from "@checkstack/queue-api";
|
|
15
16
|
|
|
16
|
-
type Db =
|
|
17
|
+
type Db = SafeDatabase<typeof schema>;
|
|
17
18
|
|
|
18
19
|
interface RetentionJobDeps {
|
|
19
20
|
db: Db;
|
|
@@ -46,7 +47,7 @@ export async function setupRetentionJob(deps: RetentionJobDeps) {
|
|
|
46
47
|
await runRetentionJob({ db, registry, logger, queueManager });
|
|
47
48
|
logger.info("Completed health check retention job");
|
|
48
49
|
},
|
|
49
|
-
{ consumerGroup: "retention-worker" }
|
|
50
|
+
{ consumerGroup: "retention-worker" },
|
|
50
51
|
);
|
|
51
52
|
|
|
52
53
|
// Schedule daily retention run (86400 seconds = 24 hours)
|
|
@@ -55,7 +56,7 @@ export async function setupRetentionJob(deps: RetentionJobDeps) {
|
|
|
55
56
|
{
|
|
56
57
|
jobId: "health-check-retention-daily",
|
|
57
58
|
intervalSeconds: 24 * 60 * 60, // Daily (24 hours)
|
|
58
|
-
}
|
|
59
|
+
},
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
logger.info("Health check retention job scheduled (runs daily)");
|
|
@@ -102,7 +103,7 @@ export async function runRetentionJob(deps: RetentionJobDeps) {
|
|
|
102
103
|
} catch (error) {
|
|
103
104
|
logger.error(
|
|
104
105
|
`Retention job failed for ${assignment.systemId}/${assignment.configurationId}`,
|
|
105
|
-
{ error }
|
|
106
|
+
{ error },
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
109
|
}
|
|
@@ -127,9 +128,11 @@ async function aggregateRawRuns(params: AggregateRawRunsParams) {
|
|
|
127
128
|
cutoffDate.setHours(cutoffDate.getHours(), 0, 0, 0); // Round to hour
|
|
128
129
|
|
|
129
130
|
// Get strategy for metadata aggregation
|
|
130
|
-
const config = await db
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
const [config] = await db
|
|
132
|
+
.select()
|
|
133
|
+
.from(healthCheckConfigurations)
|
|
134
|
+
.where(eq(healthCheckConfigurations.id, configurationId))
|
|
135
|
+
.limit(1);
|
|
133
136
|
const strategy = config ? registry.getStrategy(config.strategyId) : undefined;
|
|
134
137
|
|
|
135
138
|
// Query raw runs older than cutoff, grouped by hour
|
|
@@ -140,8 +143,8 @@ async function aggregateRawRuns(params: AggregateRawRunsParams) {
|
|
|
140
143
|
and(
|
|
141
144
|
eq(healthCheckRuns.systemId, systemId),
|
|
142
145
|
eq(healthCheckRuns.configurationId, configurationId),
|
|
143
|
-
lt(healthCheckRuns.timestamp, cutoffDate)
|
|
144
|
-
)
|
|
146
|
+
lt(healthCheckRuns.timestamp, cutoffDate),
|
|
147
|
+
),
|
|
145
148
|
)
|
|
146
149
|
.orderBy(healthCheckRuns.timestamp);
|
|
147
150
|
|
|
@@ -284,8 +287,8 @@ async function rollupHourlyAggregates(params: RollupParams) {
|
|
|
284
287
|
eq(healthCheckAggregates.systemId, systemId),
|
|
285
288
|
eq(healthCheckAggregates.configurationId, configurationId),
|
|
286
289
|
eq(healthCheckAggregates.bucketSize, "hourly"),
|
|
287
|
-
lt(healthCheckAggregates.bucketStart, cutoffDate)
|
|
288
|
-
)
|
|
290
|
+
lt(healthCheckAggregates.bucketStart, cutoffDate),
|
|
291
|
+
),
|
|
289
292
|
);
|
|
290
293
|
|
|
291
294
|
if (oldHourly.length === 0) return;
|
|
@@ -392,8 +395,8 @@ async function deleteExpiredAggregates(params: DeleteExpiredParams) {
|
|
|
392
395
|
eq(healthCheckAggregates.systemId, systemId),
|
|
393
396
|
eq(healthCheckAggregates.configurationId, configurationId),
|
|
394
397
|
eq(healthCheckAggregates.bucketSize, "daily"),
|
|
395
|
-
lt(healthCheckAggregates.bucketStart, cutoffDate)
|
|
396
|
-
)
|
|
398
|
+
lt(healthCheckAggregates.bucketStart, cutoffDate),
|
|
399
|
+
),
|
|
397
400
|
);
|
|
398
401
|
}
|
|
399
402
|
|
package/src/router.ts
CHANGED
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
toJsonSchema,
|
|
5
5
|
type RpcContext,
|
|
6
6
|
type HealthCheckRegistry,
|
|
7
|
+
type SafeDatabase,
|
|
7
8
|
} from "@checkstack/backend-api";
|
|
8
9
|
import { healthCheckContract } from "@checkstack/healthcheck-common";
|
|
9
10
|
import { HealthCheckService } from "./service";
|
|
10
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
11
11
|
import * as schema from "./schema";
|
|
12
12
|
import { toJsonSchemaWithChartMeta } from "./schema-utils";
|
|
13
13
|
|
|
@@ -18,8 +18,8 @@ import { toJsonSchemaWithChartMeta } from "./schema-utils";
|
|
|
18
18
|
* based on the contract's meta.userType and meta.access.
|
|
19
19
|
*/
|
|
20
20
|
export const createHealthCheckRouter = (
|
|
21
|
-
database:
|
|
22
|
-
registry: HealthCheckRegistry
|
|
21
|
+
database: SafeDatabase<typeof schema>,
|
|
22
|
+
registry: HealthCheckRegistry,
|
|
23
23
|
) => {
|
|
24
24
|
// Create service instance once - shared across all handlers
|
|
25
25
|
const service = new HealthCheckService(database, registry);
|
|
@@ -40,7 +40,7 @@ export const createHealthCheckRouter = (
|
|
|
40
40
|
? toJsonSchemaWithChartMeta(r.strategy.result.schema)
|
|
41
41
|
: undefined,
|
|
42
42
|
aggregatedResultSchema: toJsonSchemaWithChartMeta(
|
|
43
|
-
r.strategy.aggregatedResult.schema
|
|
43
|
+
r.strategy.aggregatedResult.schema,
|
|
44
44
|
),
|
|
45
45
|
}));
|
|
46
46
|
}),
|
|
@@ -48,7 +48,7 @@ export const createHealthCheckRouter = (
|
|
|
48
48
|
getCollectors: os.getCollectors.handler(async ({ input, context }) => {
|
|
49
49
|
// Get strategy to verify it exists
|
|
50
50
|
const strategy = context.healthCheckRegistry.getStrategy(
|
|
51
|
-
input.strategyId
|
|
51
|
+
input.strategyId,
|
|
52
52
|
);
|
|
53
53
|
if (!strategy) {
|
|
54
54
|
return [];
|
|
@@ -103,13 +103,13 @@ export const createHealthCheckRouter = (
|
|
|
103
103
|
getSystemConfigurations: os.getSystemConfigurations.handler(
|
|
104
104
|
async ({ input }) => {
|
|
105
105
|
return service.getSystemConfigurations(input);
|
|
106
|
-
}
|
|
106
|
+
},
|
|
107
107
|
),
|
|
108
108
|
|
|
109
109
|
getSystemAssociations: os.getSystemAssociations.handler(
|
|
110
110
|
async ({ input }) => {
|
|
111
111
|
return service.getSystemAssociations(input.systemId);
|
|
112
|
-
}
|
|
112
|
+
},
|
|
113
113
|
),
|
|
114
114
|
|
|
115
115
|
associateSystem: os.associateSystem.handler(async ({ input, context }) => {
|
|
@@ -123,7 +123,7 @@ export const createHealthCheckRouter = (
|
|
|
123
123
|
// If enabling the health check, schedule it immediately
|
|
124
124
|
if (input.body.enabled) {
|
|
125
125
|
const config = await service.getConfiguration(
|
|
126
|
-
input.body.configurationId
|
|
126
|
+
input.body.configurationId,
|
|
127
127
|
);
|
|
128
128
|
if (config) {
|
|
129
129
|
const { scheduleHealthCheck } = await import("./queue-executor");
|
|
@@ -152,9 +152,9 @@ export const createHealthCheckRouter = (
|
|
|
152
152
|
await service.updateRetentionConfig(
|
|
153
153
|
input.systemId,
|
|
154
154
|
input.configurationId,
|
|
155
|
-
input.retentionConfig
|
|
155
|
+
input.retentionConfig,
|
|
156
156
|
);
|
|
157
|
-
}
|
|
157
|
+
},
|
|
158
158
|
),
|
|
159
159
|
|
|
160
160
|
getHistory: os.getHistory.handler(async ({ input }) => {
|
|
@@ -176,12 +176,12 @@ export const createHealthCheckRouter = (
|
|
|
176
176
|
return service.getAggregatedHistory(input, {
|
|
177
177
|
includeAggregatedResult: true,
|
|
178
178
|
});
|
|
179
|
-
}
|
|
179
|
+
},
|
|
180
180
|
),
|
|
181
181
|
getSystemHealthStatus: os.getSystemHealthStatus.handler(
|
|
182
182
|
async ({ input }) => {
|
|
183
183
|
return service.getSystemHealthStatus(input.systemId);
|
|
184
|
-
}
|
|
184
|
+
},
|
|
185
185
|
),
|
|
186
186
|
|
|
187
187
|
getBulkSystemHealthStatus: os.getBulkSystemHealthStatus.handler(
|
|
@@ -195,17 +195,17 @@ export const createHealthCheckRouter = (
|
|
|
195
195
|
await Promise.all(
|
|
196
196
|
input.systemIds.map(async (systemId) => {
|
|
197
197
|
statuses[systemId] = await service.getSystemHealthStatus(systemId);
|
|
198
|
-
})
|
|
198
|
+
}),
|
|
199
199
|
);
|
|
200
200
|
|
|
201
201
|
return { statuses };
|
|
202
|
-
}
|
|
202
|
+
},
|
|
203
203
|
),
|
|
204
204
|
|
|
205
205
|
getSystemHealthOverview: os.getSystemHealthOverview.handler(
|
|
206
206
|
async ({ input }) => {
|
|
207
207
|
return service.getSystemHealthOverview(input.systemId);
|
|
208
|
-
}
|
|
208
|
+
},
|
|
209
209
|
),
|
|
210
210
|
});
|
|
211
211
|
};
|
package/src/service.ts
CHANGED
|
@@ -14,14 +14,16 @@ import {
|
|
|
14
14
|
} from "./schema";
|
|
15
15
|
import * as schema from "./schema";
|
|
16
16
|
import { eq, and, InferSelectModel, desc, gte, lte } from "drizzle-orm";
|
|
17
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
18
17
|
import { ORPCError } from "@orpc/server";
|
|
19
18
|
import { evaluateHealthStatus } from "./state-evaluator";
|
|
20
19
|
import { stateThresholds } from "./state-thresholds-migrations";
|
|
21
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
HealthCheckRegistry,
|
|
22
|
+
SafeDatabase,
|
|
23
|
+
} from "@checkstack/backend-api";
|
|
22
24
|
|
|
23
|
-
// Drizzle type helper
|
|
24
|
-
type Db =
|
|
25
|
+
// Drizzle type helper - uses SafeDatabase to prevent relational query API usage
|
|
26
|
+
type Db = SafeDatabase<typeof schema>;
|
|
25
27
|
|
|
26
28
|
interface SystemCheckStatus {
|
|
27
29
|
configurationId: string;
|
|
@@ -38,10 +40,13 @@ interface SystemHealthStatusResponse {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export class HealthCheckService {
|
|
41
|
-
constructor(
|
|
43
|
+
constructor(
|
|
44
|
+
private db: Db,
|
|
45
|
+
private registry?: HealthCheckRegistry,
|
|
46
|
+
) {}
|
|
42
47
|
|
|
43
48
|
async createConfiguration(
|
|
44
|
-
data: CreateHealthCheckConfiguration
|
|
49
|
+
data: CreateHealthCheckConfiguration,
|
|
45
50
|
): Promise<HealthCheckConfiguration> {
|
|
46
51
|
const [config] = await this.db
|
|
47
52
|
.insert(healthCheckConfigurations)
|
|
@@ -58,7 +63,7 @@ export class HealthCheckService {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
async getConfiguration(
|
|
61
|
-
id: string
|
|
66
|
+
id: string,
|
|
62
67
|
): Promise<HealthCheckConfiguration | undefined> {
|
|
63
68
|
const [config] = await this.db
|
|
64
69
|
.select()
|
|
@@ -69,7 +74,7 @@ export class HealthCheckService {
|
|
|
69
74
|
|
|
70
75
|
async updateConfiguration(
|
|
71
76
|
id: string,
|
|
72
|
-
data: UpdateHealthCheckConfiguration
|
|
77
|
+
data: UpdateHealthCheckConfiguration,
|
|
73
78
|
): Promise<HealthCheckConfiguration | undefined> {
|
|
74
79
|
const [config] = await this.db
|
|
75
80
|
.update(healthCheckConfigurations)
|
|
@@ -137,8 +142,8 @@ export class HealthCheckService {
|
|
|
137
142
|
.where(
|
|
138
143
|
and(
|
|
139
144
|
eq(systemHealthChecks.systemId, systemId),
|
|
140
|
-
eq(systemHealthChecks.configurationId, configurationId)
|
|
141
|
-
)
|
|
145
|
+
eq(systemHealthChecks.configurationId, configurationId),
|
|
146
|
+
),
|
|
142
147
|
);
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -147,7 +152,7 @@ export class HealthCheckService {
|
|
|
147
152
|
*/
|
|
148
153
|
async getRetentionConfig(
|
|
149
154
|
systemId: string,
|
|
150
|
-
configurationId: string
|
|
155
|
+
configurationId: string,
|
|
151
156
|
): Promise<{ retentionConfig: RetentionConfig | null }> {
|
|
152
157
|
const row = await this.db
|
|
153
158
|
.select({ retentionConfig: systemHealthChecks.retentionConfig })
|
|
@@ -155,8 +160,8 @@ export class HealthCheckService {
|
|
|
155
160
|
.where(
|
|
156
161
|
and(
|
|
157
162
|
eq(systemHealthChecks.systemId, systemId),
|
|
158
|
-
eq(systemHealthChecks.configurationId, configurationId)
|
|
159
|
-
)
|
|
163
|
+
eq(systemHealthChecks.configurationId, configurationId),
|
|
164
|
+
),
|
|
160
165
|
)
|
|
161
166
|
.then((rows) => rows[0]);
|
|
162
167
|
|
|
@@ -170,7 +175,7 @@ export class HealthCheckService {
|
|
|
170
175
|
async updateRetentionConfig(
|
|
171
176
|
systemId: string,
|
|
172
177
|
configurationId: string,
|
|
173
|
-
retentionConfig: RetentionConfig | null
|
|
178
|
+
retentionConfig: RetentionConfig | null,
|
|
174
179
|
): Promise<void> {
|
|
175
180
|
// Validate retention hierarchy: raw < hourly < daily
|
|
176
181
|
if (retentionConfig) {
|
|
@@ -197,8 +202,8 @@ export class HealthCheckService {
|
|
|
197
202
|
.where(
|
|
198
203
|
and(
|
|
199
204
|
eq(systemHealthChecks.systemId, systemId),
|
|
200
|
-
eq(systemHealthChecks.configurationId, configurationId)
|
|
201
|
-
)
|
|
205
|
+
eq(systemHealthChecks.configurationId, configurationId),
|
|
206
|
+
),
|
|
202
207
|
);
|
|
203
208
|
}
|
|
204
209
|
|
|
@@ -213,7 +218,7 @@ export class HealthCheckService {
|
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
async getSystemConfigurations(
|
|
216
|
-
systemId: string
|
|
221
|
+
systemId: string,
|
|
217
222
|
): Promise<HealthCheckConfiguration[]> {
|
|
218
223
|
const rows = await this.db
|
|
219
224
|
.select({
|
|
@@ -222,7 +227,7 @@ export class HealthCheckService {
|
|
|
222
227
|
.from(systemHealthChecks)
|
|
223
228
|
.innerJoin(
|
|
224
229
|
healthCheckConfigurations,
|
|
225
|
-
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
|
|
230
|
+
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
|
|
226
231
|
)
|
|
227
232
|
.where(eq(systemHealthChecks.systemId, systemId));
|
|
228
233
|
|
|
@@ -243,7 +248,7 @@ export class HealthCheckService {
|
|
|
243
248
|
.from(systemHealthChecks)
|
|
244
249
|
.innerJoin(
|
|
245
250
|
healthCheckConfigurations,
|
|
246
|
-
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
|
|
251
|
+
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
|
|
247
252
|
)
|
|
248
253
|
.where(eq(systemHealthChecks.systemId, systemId));
|
|
249
254
|
|
|
@@ -269,7 +274,7 @@ export class HealthCheckService {
|
|
|
269
274
|
* Aggregates status from all health check configurations for this system.
|
|
270
275
|
*/
|
|
271
276
|
async getSystemHealthStatus(
|
|
272
|
-
systemId: string
|
|
277
|
+
systemId: string,
|
|
273
278
|
): Promise<SystemHealthStatusResponse> {
|
|
274
279
|
// Get all associations for this system with their thresholds and config names
|
|
275
280
|
const associations = await this.db
|
|
@@ -282,13 +287,13 @@ export class HealthCheckService {
|
|
|
282
287
|
.from(systemHealthChecks)
|
|
283
288
|
.innerJoin(
|
|
284
289
|
healthCheckConfigurations,
|
|
285
|
-
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
|
|
290
|
+
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
|
|
286
291
|
)
|
|
287
292
|
.where(
|
|
288
293
|
and(
|
|
289
294
|
eq(systemHealthChecks.systemId, systemId),
|
|
290
|
-
eq(systemHealthChecks.enabled, true)
|
|
291
|
-
)
|
|
295
|
+
eq(systemHealthChecks.enabled, true),
|
|
296
|
+
),
|
|
292
297
|
);
|
|
293
298
|
|
|
294
299
|
if (associations.length === 0) {
|
|
@@ -314,8 +319,8 @@ export class HealthCheckService {
|
|
|
314
319
|
.where(
|
|
315
320
|
and(
|
|
316
321
|
eq(healthCheckRuns.systemId, systemId),
|
|
317
|
-
eq(healthCheckRuns.configurationId, assoc.configurationId)
|
|
318
|
-
)
|
|
322
|
+
eq(healthCheckRuns.configurationId, assoc.configurationId),
|
|
323
|
+
),
|
|
319
324
|
)
|
|
320
325
|
.orderBy(desc(healthCheckRuns.timestamp))
|
|
321
326
|
.limit(maxWindowSize);
|
|
@@ -375,7 +380,7 @@ export class HealthCheckService {
|
|
|
375
380
|
.from(systemHealthChecks)
|
|
376
381
|
.innerJoin(
|
|
377
382
|
healthCheckConfigurations,
|
|
378
|
-
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
|
|
383
|
+
eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
|
|
379
384
|
)
|
|
380
385
|
.where(eq(systemHealthChecks.systemId, systemId));
|
|
381
386
|
|
|
@@ -394,8 +399,8 @@ export class HealthCheckService {
|
|
|
394
399
|
.where(
|
|
395
400
|
and(
|
|
396
401
|
eq(healthCheckRuns.systemId, systemId),
|
|
397
|
-
eq(healthCheckRuns.configurationId, assoc.configurationId)
|
|
398
|
-
)
|
|
402
|
+
eq(healthCheckRuns.configurationId, assoc.configurationId),
|
|
403
|
+
),
|
|
399
404
|
)
|
|
400
405
|
.orderBy(desc(healthCheckRuns.timestamp))
|
|
401
406
|
.limit(sparklineLimit);
|
|
@@ -558,7 +563,7 @@ export class HealthCheckService {
|
|
|
558
563
|
endDate: Date;
|
|
559
564
|
bucketSize: "hourly" | "daily" | "auto";
|
|
560
565
|
},
|
|
561
|
-
options: { includeAggregatedResult: boolean }
|
|
566
|
+
options: { includeAggregatedResult: boolean },
|
|
562
567
|
) {
|
|
563
568
|
const { systemId, configurationId, startDate, endDate } = props;
|
|
564
569
|
let bucketSize = props.bucketSize;
|
|
@@ -571,9 +576,13 @@ export class HealthCheckService {
|
|
|
571
576
|
}
|
|
572
577
|
|
|
573
578
|
// Get the configuration to find the strategy
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
579
|
+
// Note: Using standard select instead of relational query API
|
|
580
|
+
// as the relational API is blocked by the scoped database proxy
|
|
581
|
+
const [config] = await this.db
|
|
582
|
+
.select()
|
|
583
|
+
.from(healthCheckConfigurations)
|
|
584
|
+
.where(eq(healthCheckConfigurations.id, configurationId))
|
|
585
|
+
.limit(1);
|
|
577
586
|
|
|
578
587
|
// Look up strategy for aggregateResult function (only if needed)
|
|
579
588
|
const strategy =
|
|
@@ -590,8 +599,8 @@ export class HealthCheckService {
|
|
|
590
599
|
eq(healthCheckRuns.systemId, systemId),
|
|
591
600
|
eq(healthCheckRuns.configurationId, configurationId),
|
|
592
601
|
gte(healthCheckRuns.timestamp, startDate),
|
|
593
|
-
lte(healthCheckRuns.timestamp, endDate)
|
|
594
|
-
)
|
|
602
|
+
lte(healthCheckRuns.timestamp, endDate),
|
|
603
|
+
),
|
|
595
604
|
)
|
|
596
605
|
.orderBy(healthCheckRuns.timestamp);
|
|
597
606
|
|
|
@@ -631,13 +640,13 @@ export class HealthCheckService {
|
|
|
631
640
|
const buckets = [...bucketMap.values()].map((bucket) => {
|
|
632
641
|
const runCount = bucket.runs.length;
|
|
633
642
|
const healthyCount = bucket.runs.filter(
|
|
634
|
-
(r) => r.status === "healthy"
|
|
643
|
+
(r) => r.status === "healthy",
|
|
635
644
|
).length;
|
|
636
645
|
const degradedCount = bucket.runs.filter(
|
|
637
|
-
(r) => r.status === "degraded"
|
|
646
|
+
(r) => r.status === "degraded",
|
|
638
647
|
).length;
|
|
639
648
|
const unhealthyCount = bucket.runs.filter(
|
|
640
|
-
(r) => r.status === "unhealthy"
|
|
649
|
+
(r) => r.status === "unhealthy",
|
|
641
650
|
).length;
|
|
642
651
|
const successRate = runCount > 0 ? healthyCount / runCount : 0;
|
|
643
652
|
|
|
@@ -691,7 +700,7 @@ export class HealthCheckService {
|
|
|
691
700
|
|
|
692
701
|
private getBucketStart(
|
|
693
702
|
timestamp: Date,
|
|
694
|
-
bucketSize: "hourly" | "daily"
|
|
703
|
+
bucketSize: "hourly" | "daily",
|
|
695
704
|
): Date {
|
|
696
705
|
const date = new Date(timestamp);
|
|
697
706
|
if (bucketSize === "daily") {
|
|
@@ -709,7 +718,7 @@ export class HealthCheckService {
|
|
|
709
718
|
}
|
|
710
719
|
|
|
711
720
|
private mapConfig(
|
|
712
|
-
row: InferSelectModel<typeof healthCheckConfigurations
|
|
721
|
+
row: InferSelectModel<typeof healthCheckConfigurations>,
|
|
713
722
|
): HealthCheckConfiguration {
|
|
714
723
|
return {
|
|
715
724
|
id: row.id,
|