@checkstack/healthcheck-grpc-backend 0.1.13 → 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 +54 -0
- package/package.json +6 -6
- package/src/health-collector.test.ts +8 -6
- package/src/health-collector.ts +32 -35
- package/src/strategy.test.ts +12 -10
- package/src/strategy.ts +40 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @checkstack/healthcheck-grpc-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
8
|
+
|
|
9
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
10
|
+
|
|
11
|
+
**Key changes:**
|
|
12
|
+
|
|
13
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
14
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
15
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
16
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
17
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
18
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
19
|
+
|
|
20
|
+
**Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [3dd1914]
|
|
25
|
+
- @checkstack/backend-api@0.7.0
|
|
26
|
+
|
|
27
|
+
## 0.1.14
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
32
|
+
|
|
33
|
+
### Breaking Changes (Internal)
|
|
34
|
+
|
|
35
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
36
|
+
|
|
37
|
+
### New Features
|
|
38
|
+
|
|
39
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
40
|
+
- `mergeCounter()` - track occurrences
|
|
41
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
42
|
+
- `mergeRate()` - track success/total, compute %
|
|
43
|
+
- `mergeMinMax()` - track min/max values
|
|
44
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
45
|
+
|
|
46
|
+
### Improvements
|
|
47
|
+
|
|
48
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
49
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [f676e11]
|
|
52
|
+
- Updated dependencies [48c2080]
|
|
53
|
+
- @checkstack/common@0.6.2
|
|
54
|
+
- @checkstack/backend-api@0.6.0
|
|
55
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
56
|
+
|
|
3
57
|
## 0.1.13
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-grpc-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/backend-api": "0.5.
|
|
13
|
-
"@checkstack/common": "0.6.
|
|
14
|
-
"@checkstack/healthcheck-common": "0.
|
|
12
|
+
"@checkstack/backend-api": "0.5.2",
|
|
13
|
+
"@checkstack/common": "0.6.1",
|
|
14
|
+
"@checkstack/healthcheck-common": "0.8.1",
|
|
15
15
|
"@grpc/grpc-js": "^1.9.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/bun": "^1.0.0",
|
|
19
19
|
"typescript": "^5.0.0",
|
|
20
|
-
"@checkstack/tsconfig": "0.0.
|
|
21
|
-
"@checkstack/scripts": "0.1.
|
|
20
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
21
|
+
"@checkstack/scripts": "0.1.1"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -65,7 +65,7 @@ describe("HealthCollector", () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
describe("
|
|
68
|
+
describe("mergeResult", () => {
|
|
69
69
|
it("should calculate average response time and serving rate", () => {
|
|
70
70
|
const collector = new HealthCollector();
|
|
71
71
|
const runs = [
|
|
@@ -87,10 +87,11 @@ describe("HealthCollector", () => {
|
|
|
87
87
|
},
|
|
88
88
|
];
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
91
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
91
92
|
|
|
92
|
-
expect(aggregated.avgResponseTimeMs).toBe(75);
|
|
93
|
-
expect(aggregated.servingRate).toBe(100);
|
|
93
|
+
expect(aggregated.avgResponseTimeMs.avg).toBe(75);
|
|
94
|
+
expect(aggregated.servingRate.rate).toBe(100);
|
|
94
95
|
});
|
|
95
96
|
|
|
96
97
|
it("should calculate serving rate correctly", () => {
|
|
@@ -118,9 +119,10 @@ describe("HealthCollector", () => {
|
|
|
118
119
|
},
|
|
119
120
|
];
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
123
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
122
124
|
|
|
123
|
-
expect(aggregated.servingRate).toBe(50);
|
|
125
|
+
expect(aggregated.servingRate.rate).toBe(50);
|
|
124
126
|
});
|
|
125
127
|
});
|
|
126
128
|
|
package/src/health-collector.ts
CHANGED
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
mergeRate,
|
|
9
|
+
VersionedAggregated,
|
|
10
|
+
aggregatedAverage,
|
|
11
|
+
aggregatedRate,
|
|
12
|
+
type InferAggregatedResult,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import {
|
|
9
15
|
healthResultNumber,
|
|
@@ -49,20 +55,24 @@ const grpcHealthResultSchema = healthResultSchema({
|
|
|
49
55
|
|
|
50
56
|
export type HealthResult = z.infer<typeof grpcHealthResultSchema>;
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
// Aggregated result fields definition
|
|
59
|
+
const healthAggregatedFields = {
|
|
60
|
+
avgResponseTimeMs: aggregatedAverage({
|
|
54
61
|
"x-chart-type": "line",
|
|
55
62
|
"x-chart-label": "Avg Response Time",
|
|
56
63
|
"x-chart-unit": "ms",
|
|
57
64
|
}),
|
|
58
|
-
servingRate:
|
|
65
|
+
servingRate: aggregatedRate({
|
|
59
66
|
"x-chart-type": "gauge",
|
|
60
67
|
"x-chart-label": "Serving Rate",
|
|
61
68
|
"x-chart-unit": "%",
|
|
62
69
|
}),
|
|
63
|
-
}
|
|
70
|
+
};
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
// Type inferred from field definitions
|
|
73
|
+
export type HealthAggregatedResult = InferAggregatedResult<
|
|
74
|
+
typeof healthAggregatedFields
|
|
75
|
+
>;
|
|
66
76
|
|
|
67
77
|
// ============================================================================
|
|
68
78
|
// HEALTH COLLECTOR
|
|
@@ -72,15 +82,12 @@ export type HealthAggregatedResult = z.infer<typeof healthAggregatedSchema>;
|
|
|
72
82
|
* Built-in gRPC health collector.
|
|
73
83
|
* Checks gRPC health status using the standard Health Checking Protocol.
|
|
74
84
|
*/
|
|
75
|
-
export class HealthCollector
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
HealthAggregatedResult
|
|
82
|
-
>
|
|
83
|
-
{
|
|
85
|
+
export class HealthCollector implements CollectorStrategy<
|
|
86
|
+
GrpcTransportClient,
|
|
87
|
+
HealthConfig,
|
|
88
|
+
HealthResult,
|
|
89
|
+
HealthAggregatedResult
|
|
90
|
+
> {
|
|
84
91
|
id = "health";
|
|
85
92
|
displayName = "gRPC Health Check";
|
|
86
93
|
description = "Check gRPC service health status";
|
|
@@ -91,9 +98,9 @@ export class HealthCollector
|
|
|
91
98
|
|
|
92
99
|
config = new Versioned({ version: 1, schema: healthConfigSchema });
|
|
93
100
|
result = new Versioned({ version: 1, schema: grpcHealthResultSchema });
|
|
94
|
-
aggregatedResult = new
|
|
101
|
+
aggregatedResult = new VersionedAggregated({
|
|
95
102
|
version: 1,
|
|
96
|
-
|
|
103
|
+
fields: healthAggregatedFields,
|
|
97
104
|
});
|
|
98
105
|
|
|
99
106
|
async execute({
|
|
@@ -124,28 +131,18 @@ export class HealthCollector
|
|
|
124
131
|
};
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
mergeResult(
|
|
135
|
+
existing: HealthAggregatedResult | undefined,
|
|
136
|
+
run: HealthCheckRunForAggregation<HealthResult>,
|
|
129
137
|
): HealthAggregatedResult {
|
|
130
|
-
const
|
|
131
|
-
.map((r) => r.metadata?.responseTimeMs)
|
|
132
|
-
.filter((v): v is number => typeof v === "number");
|
|
133
|
-
|
|
134
|
-
const servingResults = runs
|
|
135
|
-
.map((r) => r.metadata?.serving)
|
|
136
|
-
.filter((v): v is boolean => typeof v === "boolean");
|
|
137
|
-
|
|
138
|
-
const servingCount = servingResults.filter(Boolean).length;
|
|
138
|
+
const metadata = run.metadata;
|
|
139
139
|
|
|
140
140
|
return {
|
|
141
|
-
avgResponseTimeMs:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
servingRate:
|
|
146
|
-
servingResults.length > 0
|
|
147
|
-
? Math.round((servingCount / servingResults.length) * 100)
|
|
148
|
-
: 0,
|
|
141
|
+
avgResponseTimeMs: mergeAverage(
|
|
142
|
+
existing?.avgResponseTimeMs,
|
|
143
|
+
metadata?.responseTimeMs,
|
|
144
|
+
),
|
|
145
|
+
servingRate: mergeRate(existing?.servingRate, metadata?.serving),
|
|
149
146
|
};
|
|
150
147
|
}
|
|
151
148
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -111,7 +111,7 @@ describe("GrpcHealthCheckStrategy", () => {
|
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
describe("
|
|
114
|
+
describe("mergeResult", () => {
|
|
115
115
|
it("should calculate averages correctly", () => {
|
|
116
116
|
const strategy = new GrpcHealthCheckStrategy();
|
|
117
117
|
const runs = [
|
|
@@ -141,12 +141,13 @@ describe("GrpcHealthCheckStrategy", () => {
|
|
|
141
141
|
},
|
|
142
142
|
];
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
145
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
145
146
|
|
|
146
|
-
expect(aggregated.avgResponseTime).toBe(10);
|
|
147
|
-
expect(aggregated.successRate).toBe(100);
|
|
148
|
-
expect(aggregated.servingCount).toBe(2);
|
|
149
|
-
expect(aggregated.errorCount).toBe(0);
|
|
147
|
+
expect(aggregated.avgResponseTime.avg).toBe(10);
|
|
148
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
149
|
+
expect(aggregated.servingCount.count).toBe(2);
|
|
150
|
+
expect(aggregated.errorCount.count).toBe(0);
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
it("should count errors and non-serving", () => {
|
|
@@ -179,11 +180,12 @@ describe("GrpcHealthCheckStrategy", () => {
|
|
|
179
180
|
},
|
|
180
181
|
];
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
184
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
183
185
|
|
|
184
|
-
expect(aggregated.errorCount).toBe(1);
|
|
185
|
-
expect(aggregated.servingCount).toBe(0);
|
|
186
|
-
expect(aggregated.successRate).toBe(0);
|
|
186
|
+
expect(aggregated.errorCount.count).toBe(1);
|
|
187
|
+
expect(aggregated.servingCount.count).toBe(0);
|
|
188
|
+
expect(aggregated.successRate.rate).toBe(0);
|
|
187
189
|
});
|
|
188
190
|
});
|
|
189
191
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -3,8 +3,16 @@ import {
|
|
|
3
3
|
HealthCheckStrategy,
|
|
4
4
|
HealthCheckRunForAggregation,
|
|
5
5
|
Versioned,
|
|
6
|
+
VersionedAggregated,
|
|
7
|
+
aggregatedAverage,
|
|
8
|
+
aggregatedRate,
|
|
9
|
+
aggregatedCounter,
|
|
10
|
+
mergeAverage,
|
|
11
|
+
mergeRate,
|
|
12
|
+
mergeCounter,
|
|
6
13
|
z,
|
|
7
14
|
type ConnectedClient,
|
|
15
|
+
type InferAggregatedResult,
|
|
8
16
|
} from "@checkstack/backend-api";
|
|
9
17
|
import {
|
|
10
18
|
healthResultBoolean,
|
|
@@ -80,31 +88,29 @@ const grpcResultSchema = healthResultSchema({
|
|
|
80
88
|
|
|
81
89
|
type GrpcResult = z.infer<typeof grpcResultSchema>;
|
|
82
90
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const grpcAggregatedSchema = healthResultSchema({
|
|
87
|
-
avgResponseTime: healthResultNumber({
|
|
91
|
+
/** Aggregated field definitions for bucket merging */
|
|
92
|
+
const grpcAggregatedFields = {
|
|
93
|
+
avgResponseTime: aggregatedAverage({
|
|
88
94
|
"x-chart-type": "line",
|
|
89
95
|
"x-chart-label": "Avg Response Time",
|
|
90
96
|
"x-chart-unit": "ms",
|
|
91
97
|
}),
|
|
92
|
-
successRate:
|
|
98
|
+
successRate: aggregatedRate({
|
|
93
99
|
"x-chart-type": "gauge",
|
|
94
100
|
"x-chart-label": "Success Rate",
|
|
95
101
|
"x-chart-unit": "%",
|
|
96
102
|
}),
|
|
97
|
-
errorCount:
|
|
103
|
+
errorCount: aggregatedCounter({
|
|
98
104
|
"x-chart-type": "counter",
|
|
99
105
|
"x-chart-label": "Errors",
|
|
100
106
|
}),
|
|
101
|
-
servingCount:
|
|
107
|
+
servingCount: aggregatedCounter({
|
|
102
108
|
"x-chart-type": "counter",
|
|
103
109
|
"x-chart-label": "Serving",
|
|
104
110
|
}),
|
|
105
|
-
}
|
|
111
|
+
};
|
|
106
112
|
|
|
107
|
-
type GrpcAggregatedResult =
|
|
113
|
+
type GrpcAggregatedResult = InferAggregatedResult<typeof grpcAggregatedFields>;
|
|
108
114
|
|
|
109
115
|
// ============================================================================
|
|
110
116
|
// GRPC CLIENT INTERFACE (for testability)
|
|
@@ -176,7 +182,7 @@ const defaultGrpcClient: GrpcHealthClient = {
|
|
|
176
182
|
resolve({
|
|
177
183
|
status: statusMap[response?.status ?? 0] ?? "UNKNOWN",
|
|
178
184
|
});
|
|
179
|
-
}
|
|
185
|
+
},
|
|
180
186
|
);
|
|
181
187
|
});
|
|
182
188
|
},
|
|
@@ -186,15 +192,12 @@ const defaultGrpcClient: GrpcHealthClient = {
|
|
|
186
192
|
// STRATEGY
|
|
187
193
|
// ============================================================================
|
|
188
194
|
|
|
189
|
-
export class GrpcHealthCheckStrategy
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
GrpcAggregatedResult
|
|
196
|
-
>
|
|
197
|
-
{
|
|
195
|
+
export class GrpcHealthCheckStrategy implements HealthCheckStrategy<
|
|
196
|
+
GrpcConfig,
|
|
197
|
+
GrpcTransportClient,
|
|
198
|
+
GrpcResult,
|
|
199
|
+
typeof grpcAggregatedFields
|
|
200
|
+
> {
|
|
198
201
|
id = "grpc";
|
|
199
202
|
displayName = "gRPC Health Check";
|
|
200
203
|
description =
|
|
@@ -232,55 +235,35 @@ export class GrpcHealthCheckStrategy
|
|
|
232
235
|
],
|
|
233
236
|
});
|
|
234
237
|
|
|
235
|
-
aggregatedResult
|
|
238
|
+
aggregatedResult = new VersionedAggregated({
|
|
236
239
|
version: 1,
|
|
237
|
-
|
|
240
|
+
fields: grpcAggregatedFields,
|
|
238
241
|
});
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
mergeResult(
|
|
244
|
+
existing: GrpcAggregatedResult | undefined,
|
|
245
|
+
run: HealthCheckRunForAggregation<GrpcResult>,
|
|
242
246
|
): GrpcAggregatedResult {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
if (validRuns.length === 0) {
|
|
246
|
-
return {
|
|
247
|
-
avgResponseTime: 0,
|
|
248
|
-
successRate: 0,
|
|
249
|
-
errorCount: 0,
|
|
250
|
-
servingCount: 0,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
247
|
+
const metadata = run.metadata;
|
|
253
248
|
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
const avgResponseTime = mergeAverage(
|
|
250
|
+
existing?.avgResponseTime,
|
|
251
|
+
metadata?.responseTimeMs,
|
|
252
|
+
);
|
|
257
253
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
? Math.round(
|
|
261
|
-
responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
|
262
|
-
)
|
|
263
|
-
: 0;
|
|
254
|
+
const isSuccess = metadata?.status === "SERVING";
|
|
255
|
+
const successRate = mergeRate(existing?.successRate, isSuccess);
|
|
264
256
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
).length;
|
|
268
|
-
const successRate = Math.round((servingCount / validRuns.length) * 100);
|
|
257
|
+
const hasError = metadata?.error !== undefined;
|
|
258
|
+
const errorCount = mergeCounter(existing?.errorCount, hasError);
|
|
269
259
|
|
|
270
|
-
const
|
|
271
|
-
(r) => r.metadata?.error !== undefined
|
|
272
|
-
).length;
|
|
260
|
+
const servingCount = mergeCounter(existing?.servingCount, isSuccess);
|
|
273
261
|
|
|
274
|
-
return {
|
|
275
|
-
avgResponseTime,
|
|
276
|
-
successRate,
|
|
277
|
-
errorCount,
|
|
278
|
-
servingCount,
|
|
279
|
-
};
|
|
262
|
+
return { avgResponseTime, successRate, errorCount, servingCount };
|
|
280
263
|
}
|
|
281
264
|
|
|
282
265
|
async createClient(
|
|
283
|
-
config: GrpcConfigInput
|
|
266
|
+
config: GrpcConfigInput,
|
|
284
267
|
): Promise<ConnectedClient<GrpcTransportClient>> {
|
|
285
268
|
const validatedConfig = this.config.validate(config);
|
|
286
269
|
|