@checkstack/healthcheck-backend 0.0.3 → 0.2.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 +149 -0
- package/drizzle/0006_reflective_power_pack.sql +1 -0
- package/drizzle/meta/0006_snapshot.json +407 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/queue-executor.test.ts +23 -6
- package/src/queue-executor.ts +167 -4
- package/src/router.test.ts +78 -13
- package/src/router.ts +42 -9
- package/src/schema-utils.ts +2 -0
- package/src/schema.ts +6 -1
- package/src/service.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,154 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8e43507: # Teams and Resource-Level Access Control
|
|
8
|
+
|
|
9
|
+
This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Team Management
|
|
14
|
+
|
|
15
|
+
- Create, update, and delete teams with name and description
|
|
16
|
+
- Add/remove users from teams
|
|
17
|
+
- Designate team managers with elevated privileges
|
|
18
|
+
- View team membership and manager status
|
|
19
|
+
|
|
20
|
+
### Resource-Level Access Control
|
|
21
|
+
|
|
22
|
+
- Grant teams access to specific resources (systems, health checks, incidents, maintenances)
|
|
23
|
+
- Configure read-only or manage permissions per team
|
|
24
|
+
- Resource-level "Team Only" mode that restricts access exclusively to team members
|
|
25
|
+
- Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
|
|
26
|
+
- Automatic cleanup of grants when teams are deleted (database cascade)
|
|
27
|
+
|
|
28
|
+
### Middleware Integration
|
|
29
|
+
|
|
30
|
+
- Extended `autoAuthMiddleware` to support resource access checks
|
|
31
|
+
- Single-resource pre-handler validation for detail endpoints
|
|
32
|
+
- Automatic list filtering for collection endpoints
|
|
33
|
+
- S2S endpoints for access verification
|
|
34
|
+
|
|
35
|
+
### Frontend Components
|
|
36
|
+
|
|
37
|
+
- `TeamsTab` component for managing teams in Auth Settings
|
|
38
|
+
- `TeamAccessEditor` component for assigning team access to resources
|
|
39
|
+
- Resource-level "Team Only" toggle in `TeamAccessEditor`
|
|
40
|
+
- Integration into System, Health Check, Incident, and Maintenance editors
|
|
41
|
+
|
|
42
|
+
## Breaking Changes
|
|
43
|
+
|
|
44
|
+
### API Response Format Changes
|
|
45
|
+
|
|
46
|
+
List endpoints now return objects with named keys instead of arrays directly:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Before
|
|
50
|
+
const systems = await catalogApi.getSystems();
|
|
51
|
+
|
|
52
|
+
// After
|
|
53
|
+
const { systems } = await catalogApi.getSystems();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Affected endpoints:
|
|
57
|
+
|
|
58
|
+
- `catalog.getSystems` → `{ systems: [...] }`
|
|
59
|
+
- `healthcheck.getConfigurations` → `{ configurations: [...] }`
|
|
60
|
+
- `incident.listIncidents` → `{ incidents: [...] }`
|
|
61
|
+
- `maintenance.listMaintenances` → `{ maintenances: [...] }`
|
|
62
|
+
|
|
63
|
+
### User Identity Enrichment
|
|
64
|
+
|
|
65
|
+
`RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
See `docs/backend/teams.md` for complete API reference and integration guide.
|
|
70
|
+
|
|
71
|
+
- 97c5a6b: Add UUID-based collector identification for better multiple collector support
|
|
72
|
+
|
|
73
|
+
**Breaking Change**: Existing health check configurations with collectors need to be recreated.
|
|
74
|
+
|
|
75
|
+
- Each collector instance now has a unique UUID assigned on creation
|
|
76
|
+
- Collector results are stored under the UUID key with `_collectorId` and `_assertionFailed` metadata
|
|
77
|
+
- Auto-charts correctly display separate charts for each collector instance
|
|
78
|
+
- Charts are now grouped by collector instance with clear headings
|
|
79
|
+
- Assertion status card shows pass/fail for each collector
|
|
80
|
+
- Renamed "Success" to "HTTP Success" to clarify it's about HTTP request success
|
|
81
|
+
- Fixed deletion of collectors not persisting to database
|
|
82
|
+
- Fixed duplicate React key warnings in auto-chart grid
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- 97c5a6b: Fix collector lookup when health check is assigned to a system
|
|
87
|
+
|
|
88
|
+
Collectors are now stored in the registry with their fully-qualified ID format (ownerPluginId.collectorId) to match how they are referenced in health check configurations. Added `qualifiedId` field to `RegisteredCollector` interface to avoid re-constructing the ID at query time. This fixes the "Collector not found" warning that occurred when executing health checks with assigned systems.
|
|
89
|
+
|
|
90
|
+
- Updated dependencies [97c5a6b]
|
|
91
|
+
- Updated dependencies [8e43507]
|
|
92
|
+
- Updated dependencies [8e43507]
|
|
93
|
+
- Updated dependencies [97c5a6b]
|
|
94
|
+
- @checkstack/backend-api@0.2.0
|
|
95
|
+
- @checkstack/catalog-common@1.0.0
|
|
96
|
+
- @checkstack/catalog-backend@0.1.0
|
|
97
|
+
- @checkstack/common@0.1.0
|
|
98
|
+
- @checkstack/healthcheck-common@0.2.0
|
|
99
|
+
- @checkstack/command-backend@0.0.4
|
|
100
|
+
- @checkstack/integration-backend@0.0.4
|
|
101
|
+
- @checkstack/queue-api@0.0.4
|
|
102
|
+
- @checkstack/signal-common@0.0.4
|
|
103
|
+
|
|
104
|
+
## 0.1.0
|
|
105
|
+
|
|
106
|
+
### Minor Changes
|
|
107
|
+
|
|
108
|
+
- f5b1f49: Extended health check system with per-collector assertion support.
|
|
109
|
+
|
|
110
|
+
- Added `collectors` column to `healthCheckConfigurations` schema for storing collector configs
|
|
111
|
+
- Updated queue-executor to run configured collectors and evaluate per-collector assertions
|
|
112
|
+
- Added `CollectorAssertionSchema` to healthcheck-common for assertion validation
|
|
113
|
+
- Results now stored with `metadata.collectors` containing per-collector result data
|
|
114
|
+
|
|
115
|
+
### Patch Changes
|
|
116
|
+
|
|
117
|
+
- f5b1f49: Added JSONPath assertions for response body validation and fully qualified strategy IDs.
|
|
118
|
+
|
|
119
|
+
**JSONPath Assertions:**
|
|
120
|
+
|
|
121
|
+
- Added `healthResultJSONPath()` factory in healthcheck-common for fields supporting JSONPath queries
|
|
122
|
+
- Extended AssertionBuilder with jsonpath field type showing path input (e.g., `$.data.status`)
|
|
123
|
+
- Added `jsonPath` field to `CollectorAssertionSchema` for persistence
|
|
124
|
+
- HTTP Request collector body field now supports JSONPath assertions
|
|
125
|
+
|
|
126
|
+
**Fully Qualified Strategy IDs:**
|
|
127
|
+
|
|
128
|
+
- HealthCheckRegistry now uses scoped factories like CollectorRegistry
|
|
129
|
+
- Strategies are stored with `pluginId.strategyId` format
|
|
130
|
+
- Added `getStrategiesWithMeta()` method to HealthCheckRegistry interface
|
|
131
|
+
- Router returns qualified IDs so frontend can correctly fetch collectors
|
|
132
|
+
|
|
133
|
+
**UI Improvements:**
|
|
134
|
+
|
|
135
|
+
- Save button disabled when collector configs have invalid required fields
|
|
136
|
+
- Fixed nested button warning in CollectorList accordion
|
|
137
|
+
|
|
138
|
+
- Updated dependencies [f5b1f49]
|
|
139
|
+
- Updated dependencies [f5b1f49]
|
|
140
|
+
- Updated dependencies [f5b1f49]
|
|
141
|
+
- Updated dependencies [f5b1f49]
|
|
142
|
+
- @checkstack/backend-api@0.1.0
|
|
143
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
144
|
+
- @checkstack/common@0.0.3
|
|
145
|
+
- @checkstack/catalog-backend@0.0.3
|
|
146
|
+
- @checkstack/command-backend@0.0.3
|
|
147
|
+
- @checkstack/integration-backend@0.0.3
|
|
148
|
+
- @checkstack/queue-api@0.0.3
|
|
149
|
+
- @checkstack/catalog-common@0.0.3
|
|
150
|
+
- @checkstack/signal-common@0.0.3
|
|
151
|
+
|
|
3
152
|
## 0.0.3
|
|
4
153
|
|
|
5
154
|
### Patch Changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "health_check_configurations" ADD COLUMN "collectors" jsonb;
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "1ae9bf74-594e-4801-b49a-e1b7073d8572",
|
|
3
|
+
"prevId": "18e326cb-700a-4c6e-bc15-43335c6bb035",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.health_check_aggregates": {
|
|
8
|
+
"name": "health_check_aggregates",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true,
|
|
16
|
+
"default": "gen_random_uuid()"
|
|
17
|
+
},
|
|
18
|
+
"configuration_id": {
|
|
19
|
+
"name": "configuration_id",
|
|
20
|
+
"type": "uuid",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"system_id": {
|
|
25
|
+
"name": "system_id",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true
|
|
29
|
+
},
|
|
30
|
+
"bucket_start": {
|
|
31
|
+
"name": "bucket_start",
|
|
32
|
+
"type": "timestamp",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true
|
|
35
|
+
},
|
|
36
|
+
"bucket_size": {
|
|
37
|
+
"name": "bucket_size",
|
|
38
|
+
"type": "bucket_size",
|
|
39
|
+
"typeSchema": "public",
|
|
40
|
+
"primaryKey": false,
|
|
41
|
+
"notNull": true
|
|
42
|
+
},
|
|
43
|
+
"run_count": {
|
|
44
|
+
"name": "run_count",
|
|
45
|
+
"type": "integer",
|
|
46
|
+
"primaryKey": false,
|
|
47
|
+
"notNull": true
|
|
48
|
+
},
|
|
49
|
+
"healthy_count": {
|
|
50
|
+
"name": "healthy_count",
|
|
51
|
+
"type": "integer",
|
|
52
|
+
"primaryKey": false,
|
|
53
|
+
"notNull": true
|
|
54
|
+
},
|
|
55
|
+
"degraded_count": {
|
|
56
|
+
"name": "degraded_count",
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"primaryKey": false,
|
|
59
|
+
"notNull": true
|
|
60
|
+
},
|
|
61
|
+
"unhealthy_count": {
|
|
62
|
+
"name": "unhealthy_count",
|
|
63
|
+
"type": "integer",
|
|
64
|
+
"primaryKey": false,
|
|
65
|
+
"notNull": true
|
|
66
|
+
},
|
|
67
|
+
"avg_latency_ms": {
|
|
68
|
+
"name": "avg_latency_ms",
|
|
69
|
+
"type": "integer",
|
|
70
|
+
"primaryKey": false,
|
|
71
|
+
"notNull": false
|
|
72
|
+
},
|
|
73
|
+
"min_latency_ms": {
|
|
74
|
+
"name": "min_latency_ms",
|
|
75
|
+
"type": "integer",
|
|
76
|
+
"primaryKey": false,
|
|
77
|
+
"notNull": false
|
|
78
|
+
},
|
|
79
|
+
"max_latency_ms": {
|
|
80
|
+
"name": "max_latency_ms",
|
|
81
|
+
"type": "integer",
|
|
82
|
+
"primaryKey": false,
|
|
83
|
+
"notNull": false
|
|
84
|
+
},
|
|
85
|
+
"p95_latency_ms": {
|
|
86
|
+
"name": "p95_latency_ms",
|
|
87
|
+
"type": "integer",
|
|
88
|
+
"primaryKey": false,
|
|
89
|
+
"notNull": false
|
|
90
|
+
},
|
|
91
|
+
"aggregated_result": {
|
|
92
|
+
"name": "aggregated_result",
|
|
93
|
+
"type": "jsonb",
|
|
94
|
+
"primaryKey": false,
|
|
95
|
+
"notNull": false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"indexes": {
|
|
99
|
+
"health_check_aggregates_bucket_unique": {
|
|
100
|
+
"name": "health_check_aggregates_bucket_unique",
|
|
101
|
+
"columns": [
|
|
102
|
+
{
|
|
103
|
+
"expression": "configuration_id",
|
|
104
|
+
"isExpression": false,
|
|
105
|
+
"asc": true,
|
|
106
|
+
"nulls": "last"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"expression": "system_id",
|
|
110
|
+
"isExpression": false,
|
|
111
|
+
"asc": true,
|
|
112
|
+
"nulls": "last"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"expression": "bucket_start",
|
|
116
|
+
"isExpression": false,
|
|
117
|
+
"asc": true,
|
|
118
|
+
"nulls": "last"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"expression": "bucket_size",
|
|
122
|
+
"isExpression": false,
|
|
123
|
+
"asc": true,
|
|
124
|
+
"nulls": "last"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"isUnique": true,
|
|
128
|
+
"concurrently": false,
|
|
129
|
+
"method": "btree",
|
|
130
|
+
"with": {}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"foreignKeys": {
|
|
134
|
+
"health_check_aggregates_configuration_id_health_check_configurations_id_fk": {
|
|
135
|
+
"name": "health_check_aggregates_configuration_id_health_check_configurations_id_fk",
|
|
136
|
+
"tableFrom": "health_check_aggregates",
|
|
137
|
+
"tableTo": "health_check_configurations",
|
|
138
|
+
"columnsFrom": [
|
|
139
|
+
"configuration_id"
|
|
140
|
+
],
|
|
141
|
+
"columnsTo": [
|
|
142
|
+
"id"
|
|
143
|
+
],
|
|
144
|
+
"onDelete": "cascade",
|
|
145
|
+
"onUpdate": "no action"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"compositePrimaryKeys": {},
|
|
149
|
+
"uniqueConstraints": {},
|
|
150
|
+
"policies": {},
|
|
151
|
+
"checkConstraints": {},
|
|
152
|
+
"isRLSEnabled": false
|
|
153
|
+
},
|
|
154
|
+
"public.health_check_configurations": {
|
|
155
|
+
"name": "health_check_configurations",
|
|
156
|
+
"schema": "",
|
|
157
|
+
"columns": {
|
|
158
|
+
"id": {
|
|
159
|
+
"name": "id",
|
|
160
|
+
"type": "uuid",
|
|
161
|
+
"primaryKey": true,
|
|
162
|
+
"notNull": true,
|
|
163
|
+
"default": "gen_random_uuid()"
|
|
164
|
+
},
|
|
165
|
+
"name": {
|
|
166
|
+
"name": "name",
|
|
167
|
+
"type": "text",
|
|
168
|
+
"primaryKey": false,
|
|
169
|
+
"notNull": true
|
|
170
|
+
},
|
|
171
|
+
"strategy_id": {
|
|
172
|
+
"name": "strategy_id",
|
|
173
|
+
"type": "text",
|
|
174
|
+
"primaryKey": false,
|
|
175
|
+
"notNull": true
|
|
176
|
+
},
|
|
177
|
+
"config": {
|
|
178
|
+
"name": "config",
|
|
179
|
+
"type": "jsonb",
|
|
180
|
+
"primaryKey": false,
|
|
181
|
+
"notNull": true
|
|
182
|
+
},
|
|
183
|
+
"collectors": {
|
|
184
|
+
"name": "collectors",
|
|
185
|
+
"type": "jsonb",
|
|
186
|
+
"primaryKey": false,
|
|
187
|
+
"notNull": false
|
|
188
|
+
},
|
|
189
|
+
"interval_seconds": {
|
|
190
|
+
"name": "interval_seconds",
|
|
191
|
+
"type": "integer",
|
|
192
|
+
"primaryKey": false,
|
|
193
|
+
"notNull": true
|
|
194
|
+
},
|
|
195
|
+
"is_template": {
|
|
196
|
+
"name": "is_template",
|
|
197
|
+
"type": "boolean",
|
|
198
|
+
"primaryKey": false,
|
|
199
|
+
"notNull": false,
|
|
200
|
+
"default": false
|
|
201
|
+
},
|
|
202
|
+
"created_at": {
|
|
203
|
+
"name": "created_at",
|
|
204
|
+
"type": "timestamp",
|
|
205
|
+
"primaryKey": false,
|
|
206
|
+
"notNull": true,
|
|
207
|
+
"default": "now()"
|
|
208
|
+
},
|
|
209
|
+
"updated_at": {
|
|
210
|
+
"name": "updated_at",
|
|
211
|
+
"type": "timestamp",
|
|
212
|
+
"primaryKey": false,
|
|
213
|
+
"notNull": true,
|
|
214
|
+
"default": "now()"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"indexes": {},
|
|
218
|
+
"foreignKeys": {},
|
|
219
|
+
"compositePrimaryKeys": {},
|
|
220
|
+
"uniqueConstraints": {},
|
|
221
|
+
"policies": {},
|
|
222
|
+
"checkConstraints": {},
|
|
223
|
+
"isRLSEnabled": false
|
|
224
|
+
},
|
|
225
|
+
"public.health_check_runs": {
|
|
226
|
+
"name": "health_check_runs",
|
|
227
|
+
"schema": "",
|
|
228
|
+
"columns": {
|
|
229
|
+
"id": {
|
|
230
|
+
"name": "id",
|
|
231
|
+
"type": "uuid",
|
|
232
|
+
"primaryKey": true,
|
|
233
|
+
"notNull": true,
|
|
234
|
+
"default": "gen_random_uuid()"
|
|
235
|
+
},
|
|
236
|
+
"configuration_id": {
|
|
237
|
+
"name": "configuration_id",
|
|
238
|
+
"type": "uuid",
|
|
239
|
+
"primaryKey": false,
|
|
240
|
+
"notNull": true
|
|
241
|
+
},
|
|
242
|
+
"system_id": {
|
|
243
|
+
"name": "system_id",
|
|
244
|
+
"type": "text",
|
|
245
|
+
"primaryKey": false,
|
|
246
|
+
"notNull": true
|
|
247
|
+
},
|
|
248
|
+
"status": {
|
|
249
|
+
"name": "status",
|
|
250
|
+
"type": "health_check_status",
|
|
251
|
+
"typeSchema": "public",
|
|
252
|
+
"primaryKey": false,
|
|
253
|
+
"notNull": true
|
|
254
|
+
},
|
|
255
|
+
"latency_ms": {
|
|
256
|
+
"name": "latency_ms",
|
|
257
|
+
"type": "integer",
|
|
258
|
+
"primaryKey": false,
|
|
259
|
+
"notNull": false
|
|
260
|
+
},
|
|
261
|
+
"result": {
|
|
262
|
+
"name": "result",
|
|
263
|
+
"type": "jsonb",
|
|
264
|
+
"primaryKey": false,
|
|
265
|
+
"notNull": false
|
|
266
|
+
},
|
|
267
|
+
"timestamp": {
|
|
268
|
+
"name": "timestamp",
|
|
269
|
+
"type": "timestamp",
|
|
270
|
+
"primaryKey": false,
|
|
271
|
+
"notNull": true,
|
|
272
|
+
"default": "now()"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"indexes": {},
|
|
276
|
+
"foreignKeys": {
|
|
277
|
+
"health_check_runs_configuration_id_health_check_configurations_id_fk": {
|
|
278
|
+
"name": "health_check_runs_configuration_id_health_check_configurations_id_fk",
|
|
279
|
+
"tableFrom": "health_check_runs",
|
|
280
|
+
"tableTo": "health_check_configurations",
|
|
281
|
+
"columnsFrom": [
|
|
282
|
+
"configuration_id"
|
|
283
|
+
],
|
|
284
|
+
"columnsTo": [
|
|
285
|
+
"id"
|
|
286
|
+
],
|
|
287
|
+
"onDelete": "cascade",
|
|
288
|
+
"onUpdate": "no action"
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
"compositePrimaryKeys": {},
|
|
292
|
+
"uniqueConstraints": {},
|
|
293
|
+
"policies": {},
|
|
294
|
+
"checkConstraints": {},
|
|
295
|
+
"isRLSEnabled": false
|
|
296
|
+
},
|
|
297
|
+
"public.system_health_checks": {
|
|
298
|
+
"name": "system_health_checks",
|
|
299
|
+
"schema": "",
|
|
300
|
+
"columns": {
|
|
301
|
+
"system_id": {
|
|
302
|
+
"name": "system_id",
|
|
303
|
+
"type": "text",
|
|
304
|
+
"primaryKey": false,
|
|
305
|
+
"notNull": true
|
|
306
|
+
},
|
|
307
|
+
"configuration_id": {
|
|
308
|
+
"name": "configuration_id",
|
|
309
|
+
"type": "uuid",
|
|
310
|
+
"primaryKey": false,
|
|
311
|
+
"notNull": true
|
|
312
|
+
},
|
|
313
|
+
"enabled": {
|
|
314
|
+
"name": "enabled",
|
|
315
|
+
"type": "boolean",
|
|
316
|
+
"primaryKey": false,
|
|
317
|
+
"notNull": true,
|
|
318
|
+
"default": true
|
|
319
|
+
},
|
|
320
|
+
"state_thresholds": {
|
|
321
|
+
"name": "state_thresholds",
|
|
322
|
+
"type": "jsonb",
|
|
323
|
+
"primaryKey": false,
|
|
324
|
+
"notNull": false
|
|
325
|
+
},
|
|
326
|
+
"retention_config": {
|
|
327
|
+
"name": "retention_config",
|
|
328
|
+
"type": "jsonb",
|
|
329
|
+
"primaryKey": false,
|
|
330
|
+
"notNull": false
|
|
331
|
+
},
|
|
332
|
+
"created_at": {
|
|
333
|
+
"name": "created_at",
|
|
334
|
+
"type": "timestamp",
|
|
335
|
+
"primaryKey": false,
|
|
336
|
+
"notNull": true,
|
|
337
|
+
"default": "now()"
|
|
338
|
+
},
|
|
339
|
+
"updated_at": {
|
|
340
|
+
"name": "updated_at",
|
|
341
|
+
"type": "timestamp",
|
|
342
|
+
"primaryKey": false,
|
|
343
|
+
"notNull": true,
|
|
344
|
+
"default": "now()"
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
"indexes": {},
|
|
348
|
+
"foreignKeys": {
|
|
349
|
+
"system_health_checks_configuration_id_health_check_configurations_id_fk": {
|
|
350
|
+
"name": "system_health_checks_configuration_id_health_check_configurations_id_fk",
|
|
351
|
+
"tableFrom": "system_health_checks",
|
|
352
|
+
"tableTo": "health_check_configurations",
|
|
353
|
+
"columnsFrom": [
|
|
354
|
+
"configuration_id"
|
|
355
|
+
],
|
|
356
|
+
"columnsTo": [
|
|
357
|
+
"id"
|
|
358
|
+
],
|
|
359
|
+
"onDelete": "cascade",
|
|
360
|
+
"onUpdate": "no action"
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
"compositePrimaryKeys": {
|
|
364
|
+
"system_health_checks_system_id_configuration_id_pk": {
|
|
365
|
+
"name": "system_health_checks_system_id_configuration_id_pk",
|
|
366
|
+
"columns": [
|
|
367
|
+
"system_id",
|
|
368
|
+
"configuration_id"
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"uniqueConstraints": {},
|
|
373
|
+
"policies": {},
|
|
374
|
+
"checkConstraints": {},
|
|
375
|
+
"isRLSEnabled": false
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
"enums": {
|
|
379
|
+
"public.bucket_size": {
|
|
380
|
+
"name": "bucket_size",
|
|
381
|
+
"schema": "public",
|
|
382
|
+
"values": [
|
|
383
|
+
"hourly",
|
|
384
|
+
"daily"
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
"public.health_check_status": {
|
|
388
|
+
"name": "health_check_status",
|
|
389
|
+
"schema": "public",
|
|
390
|
+
"values": [
|
|
391
|
+
"healthy",
|
|
392
|
+
"unhealthy",
|
|
393
|
+
"degraded"
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
"schemas": {},
|
|
398
|
+
"sequences": {},
|
|
399
|
+
"roles": {},
|
|
400
|
+
"policies": {},
|
|
401
|
+
"views": {},
|
|
402
|
+
"_meta": {
|
|
403
|
+
"columns": {},
|
|
404
|
+
"schemas": {},
|
|
405
|
+
"tables": {}
|
|
406
|
+
}
|
|
407
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -90,6 +90,7 @@ export default createBackendPlugin({
|
|
|
90
90
|
deps: {
|
|
91
91
|
logger: coreServices.logger,
|
|
92
92
|
healthCheckRegistry: coreServices.healthCheckRegistry,
|
|
93
|
+
collectorRegistry: coreServices.collectorRegistry,
|
|
93
94
|
rpc: coreServices.rpc,
|
|
94
95
|
rpcClient: coreServices.rpcClient,
|
|
95
96
|
queueManager: coreServices.queueManager,
|
|
@@ -100,6 +101,7 @@ export default createBackendPlugin({
|
|
|
100
101
|
logger,
|
|
101
102
|
database,
|
|
102
103
|
healthCheckRegistry,
|
|
104
|
+
collectorRegistry,
|
|
103
105
|
rpc,
|
|
104
106
|
rpcClient,
|
|
105
107
|
queueManager,
|
|
@@ -114,6 +116,7 @@ export default createBackendPlugin({
|
|
|
114
116
|
await setupHealthCheckWorker({
|
|
115
117
|
db: database,
|
|
116
118
|
registry: healthCheckRegistry,
|
|
119
|
+
collectorRegistry,
|
|
117
120
|
logger,
|
|
118
121
|
queueManager,
|
|
119
122
|
signalService,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "@checkstack/backend-api";
|
|
19
19
|
import { mock } from "bun:test";
|
|
20
20
|
|
|
21
|
-
// Helper to create mock health check registry
|
|
21
|
+
// Helper to create mock health check registry with createClient pattern
|
|
22
22
|
const createMockRegistry = (): HealthCheckRegistry => ({
|
|
23
23
|
getStrategy: mock((id: string) => ({
|
|
24
24
|
id,
|
|
@@ -28,19 +28,32 @@ const createMockRegistry = (): HealthCheckRegistry => ({
|
|
|
28
28
|
version: 1,
|
|
29
29
|
schema: z.object({}),
|
|
30
30
|
}),
|
|
31
|
+
result: new Versioned({
|
|
32
|
+
version: 1,
|
|
33
|
+
schema: z.object({}),
|
|
34
|
+
}),
|
|
31
35
|
aggregatedResult: new Versioned({
|
|
32
36
|
version: 1,
|
|
33
37
|
schema: z.object({}),
|
|
34
38
|
}),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
createClient: mock(async () => ({
|
|
40
|
+
client: {
|
|
41
|
+
exec: mock(async () => ({})),
|
|
42
|
+
},
|
|
43
|
+
close: mock(() => {}),
|
|
39
44
|
})),
|
|
40
45
|
aggregateResult: mock(() => ({})),
|
|
41
46
|
})),
|
|
42
47
|
register: mock(() => {}),
|
|
43
48
|
getStrategies: mock(() => []),
|
|
49
|
+
getStrategiesWithMeta: mock(() => []),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Helper to create mock collector registry
|
|
53
|
+
const createMockCollectorRegistry = () => ({
|
|
54
|
+
register: mock(() => {}),
|
|
55
|
+
getCollector: mock(() => undefined),
|
|
56
|
+
getCollectors: mock(() => []),
|
|
44
57
|
});
|
|
45
58
|
|
|
46
59
|
// Helper to create mock catalog client for notification delegation
|
|
@@ -48,7 +61,7 @@ const createMockCatalogClient = () => ({
|
|
|
48
61
|
notifySystemSubscribers: mock(async () => ({ notifiedCount: 0 })),
|
|
49
62
|
// Other methods not used in queue-executor
|
|
50
63
|
getEntities: mock(async () => ({ systems: [], groups: [] })),
|
|
51
|
-
getSystems: mock(async () => []),
|
|
64
|
+
getSystems: mock(async () => ({ systems: [] })),
|
|
52
65
|
getGroups: mock(async () => []),
|
|
53
66
|
createSystem: mock(async () => ({})),
|
|
54
67
|
updateSystem: mock(async () => ({})),
|
|
@@ -120,6 +133,10 @@ describe("Queue-Based Health Check Executor", () => {
|
|
|
120
133
|
typeof setupHealthCheckWorker
|
|
121
134
|
>[0]["db"],
|
|
122
135
|
registry: mockRegistry,
|
|
136
|
+
collectorRegistry:
|
|
137
|
+
createMockCollectorRegistry() as unknown as Parameters<
|
|
138
|
+
typeof setupHealthCheckWorker
|
|
139
|
+
>[0]["collectorRegistry"],
|
|
123
140
|
logger: mockLogger,
|
|
124
141
|
queueManager: mockQueueManager,
|
|
125
142
|
signalService: createMockSignalService(),
|
package/src/queue-executor.ts
CHANGED
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
HealthCheckRegistry,
|
|
3
3
|
Logger,
|
|
4
4
|
type EmitHookFn,
|
|
5
|
+
type CollectorRegistry,
|
|
6
|
+
evaluateAssertions,
|
|
5
7
|
} from "@checkstack/backend-api";
|
|
6
8
|
import { QueueManager } from "@checkstack/queue-api";
|
|
7
9
|
import {
|
|
@@ -159,6 +161,7 @@ async function executeHealthCheckJob(props: {
|
|
|
159
161
|
payload: HealthCheckJobPayload;
|
|
160
162
|
db: Db;
|
|
161
163
|
registry: HealthCheckRegistry;
|
|
164
|
+
collectorRegistry: CollectorRegistry;
|
|
162
165
|
logger: Logger;
|
|
163
166
|
signalService: SignalService;
|
|
164
167
|
catalogClient: CatalogClient;
|
|
@@ -168,6 +171,7 @@ async function executeHealthCheckJob(props: {
|
|
|
168
171
|
payload,
|
|
169
172
|
db,
|
|
170
173
|
registry,
|
|
174
|
+
collectorRegistry,
|
|
171
175
|
logger,
|
|
172
176
|
signalService,
|
|
173
177
|
catalogClient,
|
|
@@ -190,6 +194,7 @@ async function executeHealthCheckJob(props: {
|
|
|
190
194
|
configName: healthCheckConfigurations.name,
|
|
191
195
|
strategyId: healthCheckConfigurations.strategyId,
|
|
192
196
|
config: healthCheckConfigurations.config,
|
|
197
|
+
collectors: healthCheckConfigurations.collectors,
|
|
193
198
|
interval: healthCheckConfigurations.intervalSeconds,
|
|
194
199
|
enabled: systemHealthChecks.enabled,
|
|
195
200
|
})
|
|
@@ -234,10 +239,165 @@ async function executeHealthCheckJob(props: {
|
|
|
234
239
|
return;
|
|
235
240
|
}
|
|
236
241
|
|
|
237
|
-
// Execute health check
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
// Execute health check using createClient pattern
|
|
243
|
+
const start = performance.now();
|
|
244
|
+
let connectedClient;
|
|
245
|
+
try {
|
|
246
|
+
connectedClient = await strategy.createClient(
|
|
247
|
+
configRow.config as Record<string, unknown>
|
|
248
|
+
);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// Connection failed
|
|
251
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
252
|
+
const errorMessage =
|
|
253
|
+
error instanceof Error ? error.message : "Connection failed";
|
|
254
|
+
|
|
255
|
+
const result = {
|
|
256
|
+
status: "unhealthy" as const,
|
|
257
|
+
latencyMs,
|
|
258
|
+
message: errorMessage,
|
|
259
|
+
metadata: { connected: false, error: errorMessage },
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
await db.insert(healthCheckRuns).values({
|
|
263
|
+
configurationId: configId,
|
|
264
|
+
systemId,
|
|
265
|
+
status: result.status,
|
|
266
|
+
latencyMs: result.latencyMs,
|
|
267
|
+
result: { ...result } as Record<string, unknown>,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
logger.debug(
|
|
271
|
+
`Health check ${configId} for system ${systemId} failed: ${errorMessage}`
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Broadcast failure signal
|
|
275
|
+
await signalService.broadcast(HEALTH_CHECK_RUN_COMPLETED, {
|
|
276
|
+
systemId,
|
|
277
|
+
systemName,
|
|
278
|
+
configurationId: configId,
|
|
279
|
+
configurationName: configRow.configName,
|
|
280
|
+
status: result.status,
|
|
281
|
+
latencyMs: result.latencyMs,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Check and notify state change
|
|
285
|
+
const newState = await service.getSystemHealthStatus(systemId);
|
|
286
|
+
if (newState.status !== previousStatus) {
|
|
287
|
+
await notifyStateChange({
|
|
288
|
+
systemId,
|
|
289
|
+
previousStatus,
|
|
290
|
+
newStatus: newState.status,
|
|
291
|
+
catalogClient,
|
|
292
|
+
logger,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const connectionTimeMs = Math.round(performance.now() - start);
|
|
300
|
+
|
|
301
|
+
// Execute collectors
|
|
302
|
+
const collectors = configRow.collectors ?? [];
|
|
303
|
+
const collectorResults: Record<string, unknown> = {};
|
|
304
|
+
let hasCollectorError = false;
|
|
305
|
+
let errorMessage: string | undefined;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
for (const collectorEntry of collectors) {
|
|
309
|
+
const registered = collectorRegistry.getCollector(
|
|
310
|
+
collectorEntry.collectorId
|
|
311
|
+
);
|
|
312
|
+
if (!registered) {
|
|
313
|
+
logger.warn(
|
|
314
|
+
`Collector ${collectorEntry.collectorId} not found, skipping`
|
|
315
|
+
);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Use the collector's UUID as the storage key
|
|
320
|
+
const storageKey = collectorEntry.id;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const collectorResult = await registered.collector.execute({
|
|
324
|
+
config: collectorEntry.config,
|
|
325
|
+
client: connectedClient.client,
|
|
326
|
+
pluginId: configRow.strategyId,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Check for collector-level error
|
|
330
|
+
if (collectorResult.error) {
|
|
331
|
+
hasCollectorError = true;
|
|
332
|
+
errorMessage = collectorResult.error;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Evaluate per-collector assertions
|
|
336
|
+
let assertionFailed: string | undefined;
|
|
337
|
+
if (
|
|
338
|
+
collectorEntry.assertions &&
|
|
339
|
+
collectorEntry.assertions.length > 0 &&
|
|
340
|
+
collectorResult.result
|
|
341
|
+
) {
|
|
342
|
+
const assertions = collectorEntry.assertions;
|
|
343
|
+
const failedAssertion = evaluateAssertions(
|
|
344
|
+
assertions,
|
|
345
|
+
collectorResult.result as Record<string, unknown>
|
|
346
|
+
);
|
|
347
|
+
if (failedAssertion) {
|
|
348
|
+
hasCollectorError = true;
|
|
349
|
+
assertionFailed = `${failedAssertion.field} ${
|
|
350
|
+
failedAssertion.operator
|
|
351
|
+
} ${failedAssertion.value ?? ""}`;
|
|
352
|
+
errorMessage = `Assertion failed: ${assertionFailed}`;
|
|
353
|
+
logger.debug(
|
|
354
|
+
`Collector ${storageKey} assertion failed: ${errorMessage}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Store result under the collector's UUID, with collector type and assertion metadata
|
|
360
|
+
collectorResults[storageKey] = {
|
|
361
|
+
_collectorId: collectorEntry.collectorId, // Store the type for frontend schema linking
|
|
362
|
+
_assertionFailed: assertionFailed, // null if no assertion failed
|
|
363
|
+
...collectorResult.result,
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
hasCollectorError = true;
|
|
367
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
368
|
+
collectorResults[storageKey] = {
|
|
369
|
+
_collectorId: collectorEntry.collectorId,
|
|
370
|
+
_assertionFailed: undefined,
|
|
371
|
+
error: errorMessage,
|
|
372
|
+
};
|
|
373
|
+
logger.debug(`Collector ${storageKey} failed: ${errorMessage}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
// Clean up connection
|
|
378
|
+
try {
|
|
379
|
+
connectedClient.close();
|
|
380
|
+
} catch {
|
|
381
|
+
// Ignore close errors
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Determine health status based on collector results
|
|
386
|
+
const status = hasCollectorError ? "unhealthy" : "healthy";
|
|
387
|
+
const totalLatencyMs = Math.round(performance.now() - start);
|
|
388
|
+
|
|
389
|
+
const result = {
|
|
390
|
+
status: status as "healthy" | "unhealthy",
|
|
391
|
+
latencyMs: totalLatencyMs,
|
|
392
|
+
message: hasCollectorError
|
|
393
|
+
? `Check failed: ${errorMessage}`
|
|
394
|
+
: `Completed in ${totalLatencyMs}ms`,
|
|
395
|
+
metadata: {
|
|
396
|
+
connected: true,
|
|
397
|
+
connectionTimeMs,
|
|
398
|
+
collectors: collectorResults,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
241
401
|
|
|
242
402
|
// Store result (spread to convert structured type to plain record for jsonb)
|
|
243
403
|
await db.insert(healthCheckRuns).values({
|
|
@@ -412,6 +572,7 @@ async function executeHealthCheckJob(props: {
|
|
|
412
572
|
export async function setupHealthCheckWorker(props: {
|
|
413
573
|
db: Db;
|
|
414
574
|
registry: HealthCheckRegistry;
|
|
575
|
+
collectorRegistry: CollectorRegistry;
|
|
415
576
|
logger: Logger;
|
|
416
577
|
queueManager: QueueManager;
|
|
417
578
|
signalService: SignalService;
|
|
@@ -421,6 +582,7 @@ export async function setupHealthCheckWorker(props: {
|
|
|
421
582
|
const {
|
|
422
583
|
db,
|
|
423
584
|
registry,
|
|
585
|
+
collectorRegistry,
|
|
424
586
|
logger,
|
|
425
587
|
queueManager,
|
|
426
588
|
signalService,
|
|
@@ -438,6 +600,7 @@ export async function setupHealthCheckWorker(props: {
|
|
|
438
600
|
payload: job.data,
|
|
439
601
|
db,
|
|
440
602
|
registry,
|
|
603
|
+
collectorRegistry,
|
|
441
604
|
logger,
|
|
442
605
|
signalService,
|
|
443
606
|
catalogClient,
|
package/src/router.test.ts
CHANGED
|
@@ -40,6 +40,7 @@ describe("HealthCheck Router", () => {
|
|
|
40
40
|
register: mock(),
|
|
41
41
|
getStrategy: mock(),
|
|
42
42
|
getStrategies: mock(() => []),
|
|
43
|
+
getStrategiesWithMeta: mock(() => []),
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
const router = createHealthCheckRouter(mockDb as never, mockRegistry);
|
|
@@ -48,26 +49,31 @@ describe("HealthCheck Router", () => {
|
|
|
48
49
|
const context = createMockRpcContext({
|
|
49
50
|
user: mockUser,
|
|
50
51
|
healthCheckRegistry: {
|
|
51
|
-
|
|
52
|
+
getStrategiesWithMeta: mock().mockReturnValue([
|
|
52
53
|
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
strategy: {
|
|
55
|
+
id: "http",
|
|
56
|
+
displayName: "HTTP",
|
|
57
|
+
description: "Check HTTP",
|
|
58
|
+
config: {
|
|
59
|
+
version: 1,
|
|
60
|
+
schema: z.object({}),
|
|
61
|
+
},
|
|
62
|
+
aggregatedResult: {
|
|
63
|
+
schema: z.object({}),
|
|
64
|
+
},
|
|
62
65
|
},
|
|
66
|
+
qualifiedId: "healthcheck-http.http",
|
|
67
|
+
ownerPluginId: "healthcheck-http",
|
|
63
68
|
},
|
|
64
69
|
]),
|
|
65
|
-
|
|
70
|
+
getStrategies: mock().mockReturnValue([]),
|
|
71
|
+
} as never,
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
const result = await call(router.getStrategies, undefined, { context });
|
|
69
75
|
expect(result).toHaveLength(1);
|
|
70
|
-
expect(result[0].id).toBe("http");
|
|
76
|
+
expect(result[0].id).toBe("healthcheck-http.http");
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
it("getConfigurations calls service", async () => {
|
|
@@ -76,6 +82,65 @@ describe("HealthCheck Router", () => {
|
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
const result = await call(router.getConfigurations, undefined, { context });
|
|
79
|
-
expect(
|
|
85
|
+
expect(result).toHaveProperty("configurations");
|
|
86
|
+
expect(Array.isArray(result.configurations)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("getCollectors returns collectors for strategy", async () => {
|
|
90
|
+
const mockCollector = {
|
|
91
|
+
qualifiedId: "collector-hardware.cpu",
|
|
92
|
+
collector: {
|
|
93
|
+
id: "cpu",
|
|
94
|
+
displayName: "CPU Metrics",
|
|
95
|
+
description: "Collect CPU stats",
|
|
96
|
+
supportedPlugins: [{ pluginId: "healthcheck-ssh" }],
|
|
97
|
+
allowMultiple: false,
|
|
98
|
+
config: { version: 1, schema: z.object({}) },
|
|
99
|
+
result: { version: 1, schema: z.object({}) },
|
|
100
|
+
aggregatedResult: { version: 1, schema: z.object({}) },
|
|
101
|
+
},
|
|
102
|
+
ownerPlugin: { pluginId: "collector-hardware" },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const context = createMockRpcContext({
|
|
106
|
+
user: mockUser,
|
|
107
|
+
healthCheckRegistry: {
|
|
108
|
+
getStrategy: mock().mockReturnValue({ id: "healthcheck-ssh" }),
|
|
109
|
+
getStrategies: mock().mockReturnValue([]),
|
|
110
|
+
} as never,
|
|
111
|
+
collectorRegistry: {
|
|
112
|
+
getCollectorsForPlugin: mock().mockReturnValue([mockCollector]),
|
|
113
|
+
getCollector: mock(),
|
|
114
|
+
getCollectors: mock().mockReturnValue([]),
|
|
115
|
+
register: mock(),
|
|
116
|
+
} as never,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await call(
|
|
120
|
+
router.getCollectors,
|
|
121
|
+
{ strategyId: "healthcheck-ssh" },
|
|
122
|
+
{ context }
|
|
123
|
+
);
|
|
124
|
+
expect(result).toHaveLength(1);
|
|
125
|
+
expect(result[0].id).toBe("collector-hardware.cpu");
|
|
126
|
+
expect(result[0].displayName).toBe("CPU Metrics");
|
|
127
|
+
expect(result[0].allowMultiple).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("getCollectors returns empty for unknown strategy", async () => {
|
|
131
|
+
const context = createMockRpcContext({
|
|
132
|
+
user: mockUser,
|
|
133
|
+
healthCheckRegistry: {
|
|
134
|
+
getStrategy: mock().mockReturnValue(undefined),
|
|
135
|
+
getStrategies: mock().mockReturnValue([]),
|
|
136
|
+
} as never,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = await call(
|
|
140
|
+
router.getCollectors,
|
|
141
|
+
{ strategyId: "unknown" },
|
|
142
|
+
{ context }
|
|
143
|
+
);
|
|
144
|
+
expect(result).toHaveLength(0);
|
|
80
145
|
});
|
|
81
146
|
});
|
package/src/router.ts
CHANGED
|
@@ -31,22 +31,55 @@ export const createHealthCheckRouter = (
|
|
|
31
31
|
|
|
32
32
|
return os.router({
|
|
33
33
|
getStrategies: os.getStrategies.handler(async ({ context }) => {
|
|
34
|
-
return context.healthCheckRegistry.
|
|
35
|
-
id:
|
|
36
|
-
displayName:
|
|
37
|
-
description:
|
|
38
|
-
configSchema: toJsonSchema(
|
|
39
|
-
resultSchema:
|
|
40
|
-
? toJsonSchemaWithChartMeta(
|
|
34
|
+
return context.healthCheckRegistry.getStrategiesWithMeta().map((r) => ({
|
|
35
|
+
id: r.qualifiedId, // Return fully qualified ID
|
|
36
|
+
displayName: r.strategy.displayName,
|
|
37
|
+
description: r.strategy.description,
|
|
38
|
+
configSchema: toJsonSchema(r.strategy.config.schema),
|
|
39
|
+
resultSchema: r.strategy.result
|
|
40
|
+
? toJsonSchemaWithChartMeta(r.strategy.result.schema)
|
|
41
41
|
: undefined,
|
|
42
42
|
aggregatedResultSchema: toJsonSchemaWithChartMeta(
|
|
43
|
-
|
|
43
|
+
r.strategy.aggregatedResult.schema
|
|
44
44
|
),
|
|
45
45
|
}));
|
|
46
46
|
}),
|
|
47
47
|
|
|
48
|
+
getCollectors: os.getCollectors.handler(async ({ input, context }) => {
|
|
49
|
+
// Get strategy to verify it exists
|
|
50
|
+
const strategy = context.healthCheckRegistry.getStrategy(
|
|
51
|
+
input.strategyId
|
|
52
|
+
);
|
|
53
|
+
if (!strategy) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Strategy ID is fully qualified: pluginId.strategyId
|
|
58
|
+
// Extract the plugin ID (everything before the last dot)
|
|
59
|
+
const lastDotIndex = input.strategyId.lastIndexOf(".");
|
|
60
|
+
const pluginId =
|
|
61
|
+
lastDotIndex > 0
|
|
62
|
+
? input.strategyId.slice(0, lastDotIndex)
|
|
63
|
+
: input.strategyId;
|
|
64
|
+
|
|
65
|
+
// Get collectors that support this strategy's plugin
|
|
66
|
+
const registeredCollectors =
|
|
67
|
+
context.collectorRegistry.getCollectorsForPlugin({
|
|
68
|
+
pluginId,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return registeredCollectors.map(({ qualifiedId, collector }) => ({
|
|
72
|
+
id: qualifiedId,
|
|
73
|
+
displayName: collector.displayName,
|
|
74
|
+
description: collector.description,
|
|
75
|
+
configSchema: toJsonSchema(collector.config.schema),
|
|
76
|
+
resultSchema: toJsonSchemaWithChartMeta(collector.result.schema),
|
|
77
|
+
allowMultiple: collector.allowMultiple ?? false,
|
|
78
|
+
}));
|
|
79
|
+
}),
|
|
80
|
+
|
|
48
81
|
getConfigurations: os.getConfigurations.handler(async () => {
|
|
49
|
-
return service.getConfigurations();
|
|
82
|
+
return { configurations: await service.getConfigurations() };
|
|
50
83
|
}),
|
|
51
84
|
|
|
52
85
|
createConfiguration: os.createConfiguration.handler(async ({ input }) => {
|
package/src/schema-utils.ts
CHANGED
|
@@ -58,6 +58,8 @@ function addHealthResultMeta(
|
|
|
58
58
|
jsonField["x-chart-label"] = healthMeta["x-chart-label"];
|
|
59
59
|
if (healthMeta["x-chart-unit"])
|
|
60
60
|
jsonField["x-chart-unit"] = healthMeta["x-chart-unit"];
|
|
61
|
+
if (healthMeta["x-jsonpath"])
|
|
62
|
+
jsonField["x-jsonpath"] = healthMeta["x-jsonpath"];
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
// Recurse into nested objects and arrays
|
package/src/schema.ts
CHANGED
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
primaryKey,
|
|
11
11
|
uniqueIndex,
|
|
12
12
|
} from "drizzle-orm/pg-core";
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
StateThresholds,
|
|
15
|
+
CollectorConfigEntry,
|
|
16
|
+
} from "@checkstack/healthcheck-common";
|
|
14
17
|
import type { VersionedRecord } from "@checkstack/backend-api";
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -38,6 +41,8 @@ export const healthCheckConfigurations = pgTable(
|
|
|
38
41
|
name: text("name").notNull(),
|
|
39
42
|
strategyId: text("strategy_id").notNull(),
|
|
40
43
|
config: jsonb("config").$type<Record<string, unknown>>().notNull(),
|
|
44
|
+
/** Collector configurations for this health check */
|
|
45
|
+
collectors: jsonb("collectors").$type<CollectorConfigEntry[]>(),
|
|
41
46
|
intervalSeconds: integer("interval_seconds").notNull(),
|
|
42
47
|
isTemplate: boolean("is_template").default(false),
|
|
43
48
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
package/src/service.ts
CHANGED
|
@@ -49,6 +49,7 @@ export class HealthCheckService {
|
|
|
49
49
|
name: data.name,
|
|
50
50
|
strategyId: data.strategyId,
|
|
51
51
|
config: data.config,
|
|
52
|
+
collectors: data.collectors ?? undefined,
|
|
52
53
|
intervalSeconds: data.intervalSeconds,
|
|
53
54
|
isTemplate: false, // Defaulting for now
|
|
54
55
|
})
|
|
@@ -715,6 +716,7 @@ export class HealthCheckService {
|
|
|
715
716
|
name: row.name,
|
|
716
717
|
strategyId: row.strategyId,
|
|
717
718
|
config: row.config,
|
|
719
|
+
collectors: row.collectors ?? undefined,
|
|
718
720
|
intervalSeconds: row.intervalSeconds,
|
|
719
721
|
createdAt: row.createdAt,
|
|
720
722
|
updatedAt: row.updatedAt,
|