@checkstack/healthcheck-grpc-backend 0.1.14 → 0.2.1

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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # @checkstack/healthcheck-grpc-backend
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 869b4ab: ## Health Check Execution Improvements
8
+
9
+ ### Breaking Changes (backend-api)
10
+
11
+ - `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
12
+
13
+ ### Features
14
+
15
+ - **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
16
+ - **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
17
+ - **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
18
+
19
+ ### Fixes
20
+
21
+ - Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
22
+ - Simplified registry type signatures by using default type parameters.
23
+
24
+ - Updated dependencies [869b4ab]
25
+ - @checkstack/backend-api@0.8.0
26
+
27
+ ## 0.2.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
32
+
33
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
34
+
35
+ **Key changes:**
36
+
37
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
38
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
39
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
40
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
41
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
42
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
43
+
44
+ **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.
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [3dd1914]
49
+ - @checkstack/backend-api@0.7.0
50
+
3
51
  ## 0.1.14
4
52
 
5
53
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-grpc-backend",
3
- "version": "0.1.14",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,9 +9,9 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.2",
13
- "@checkstack/common": "0.6.1",
14
- "@checkstack/healthcheck-common": "0.8.1",
12
+ "@checkstack/backend-api": "0.7.0",
13
+ "@checkstack/common": "0.6.2",
14
+ "@checkstack/healthcheck-common": "0.8.2",
15
15
  "@grpc/grpc-js": "^1.9.0"
16
16
  },
17
17
  "devDependencies": {
@@ -90,8 +90,8 @@ describe("HealthCollector", () => {
90
90
  let aggregated = collector.mergeResult(undefined, runs[0]);
91
91
  aggregated = collector.mergeResult(aggregated, runs[1]);
92
92
 
93
- expect(aggregated.avgResponseTimeMs).toBe(75);
94
- expect(aggregated.servingRate).toBe(100);
93
+ expect(aggregated.avgResponseTimeMs.avg).toBe(75);
94
+ expect(aggregated.servingRate.rate).toBe(100);
95
95
  });
96
96
 
97
97
  it("should calculate serving rate correctly", () => {
@@ -122,7 +122,7 @@ describe("HealthCollector", () => {
122
122
  let aggregated = collector.mergeResult(undefined, runs[0]);
123
123
  aggregated = collector.mergeResult(aggregated, runs[1]);
124
124
 
125
- expect(aggregated.servingRate).toBe(50);
125
+ expect(aggregated.servingRate.rate).toBe(50);
126
126
  });
127
127
  });
128
128
 
@@ -5,11 +5,11 @@ import {
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
7
  mergeAverage,
8
- averageStateSchema,
9
8
  mergeRate,
10
- rateStateSchema,
11
- type AverageState,
12
- type RateState,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
13
13
  } from "@checkstack/backend-api";
14
14
  import {
15
15
  healthResultNumber,
@@ -55,31 +55,24 @@ const grpcHealthResultSchema = healthResultSchema({
55
55
 
56
56
  export type HealthResult = z.infer<typeof grpcHealthResultSchema>;
57
57
 
58
- const healthAggregatedDisplaySchema = healthResultSchema({
59
- avgResponseTimeMs: healthResultNumber({
58
+ // Aggregated result fields definition
59
+ const healthAggregatedFields = {
60
+ avgResponseTimeMs: aggregatedAverage({
60
61
  "x-chart-type": "line",
61
62
  "x-chart-label": "Avg Response Time",
62
63
  "x-chart-unit": "ms",
63
64
  }),
64
- servingRate: healthResultNumber({
65
+ servingRate: aggregatedRate({
65
66
  "x-chart-type": "gauge",
66
67
  "x-chart-label": "Serving Rate",
67
68
  "x-chart-unit": "%",
68
69
  }),
69
- });
70
-
71
- const healthAggregatedInternalSchema = z.object({
72
- _responseTime: averageStateSchema
73
- .optional(),
74
- _serving: rateStateSchema
75
- .optional(),
76
- });
70
+ };
77
71
 
78
- const healthAggregatedSchema = healthAggregatedDisplaySchema.merge(
79
- healthAggregatedInternalSchema,
80
- );
81
-
82
- export type HealthAggregatedResult = z.infer<typeof healthAggregatedSchema>;
72
+ // Type inferred from field definitions
73
+ export type HealthAggregatedResult = InferAggregatedResult<
74
+ typeof healthAggregatedFields
75
+ >;
83
76
 
84
77
  // ============================================================================
85
78
  // HEALTH COLLECTOR
@@ -105,9 +98,9 @@ export class HealthCollector implements CollectorStrategy<
105
98
 
106
99
  config = new Versioned({ version: 1, schema: healthConfigSchema });
107
100
  result = new Versioned({ version: 1, schema: grpcHealthResultSchema });
108
- aggregatedResult = new Versioned({
101
+ aggregatedResult = new VersionedAggregated({
109
102
  version: 1,
110
- schema: healthAggregatedSchema,
103
+ fields: healthAggregatedFields,
111
104
  });
112
105
 
113
106
  async execute({
@@ -144,21 +137,12 @@ export class HealthCollector implements CollectorStrategy<
144
137
  ): HealthAggregatedResult {
145
138
  const metadata = run.metadata;
146
139
 
147
- const responseTimeState = mergeAverage(
148
- existing?._responseTime as AverageState | undefined,
149
- metadata?.responseTimeMs,
150
- );
151
-
152
- const servingState = mergeRate(
153
- existing?._serving as RateState | undefined,
154
- metadata?.serving,
155
- );
156
-
157
140
  return {
158
- avgResponseTimeMs: responseTimeState.avg,
159
- servingRate: servingState.rate,
160
- _responseTime: responseTimeState,
161
- _serving: servingState,
141
+ avgResponseTimeMs: mergeAverage(
142
+ existing?.avgResponseTimeMs,
143
+ metadata?.responseTimeMs,
144
+ ),
145
+ servingRate: mergeRate(existing?.servingRate, metadata?.serving),
162
146
  };
163
147
  }
164
148
  }
@@ -144,10 +144,10 @@ describe("GrpcHealthCheckStrategy", () => {
144
144
  let aggregated = strategy.mergeResult(undefined, runs[0]);
145
145
  aggregated = strategy.mergeResult(aggregated, runs[1]);
146
146
 
147
- expect(aggregated.avgResponseTime).toBe(10);
148
- expect(aggregated.successRate).toBe(100);
149
- expect(aggregated.servingCount).toBe(2);
150
- 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);
151
151
  });
152
152
 
153
153
  it("should count errors and non-serving", () => {
@@ -183,9 +183,9 @@ describe("GrpcHealthCheckStrategy", () => {
183
183
  let aggregated = strategy.mergeResult(undefined, runs[0]);
184
184
  aggregated = strategy.mergeResult(aggregated, runs[1]);
185
185
 
186
- expect(aggregated.errorCount).toBe(1);
187
- expect(aggregated.servingCount).toBe(0);
188
- 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);
189
189
  });
190
190
  });
191
191
  });
package/src/strategy.ts CHANGED
@@ -3,17 +3,17 @@ import {
3
3
  HealthCheckStrategy,
4
4
  HealthCheckRunForAggregation,
5
5
  Versioned,
6
- z,
7
- type ConnectedClient,
6
+ VersionedAggregated,
7
+ aggregatedAverage,
8
+ aggregatedRate,
9
+ aggregatedCounter,
8
10
  mergeAverage,
9
- averageStateSchema,
10
11
  mergeRate,
11
- rateStateSchema,
12
12
  mergeCounter,
13
- counterStateSchema,
14
- type AverageState,
15
- type RateState,
16
- type CounterState,
13
+ z,
14
+ type ConnectedClient,
15
+ type InferAggregatedResult,
16
+ baseStrategyConfigSchema,
17
17
  } from "@checkstack/backend-api";
18
18
  import {
19
19
  healthResultBoolean,
@@ -46,7 +46,7 @@ export type GrpcHealthStatusType = z.infer<typeof GrpcHealthStatus>;
46
46
  /**
47
47
  * Configuration schema for gRPC health checks.
48
48
  */
49
- export const grpcConfigSchema = z.object({
49
+ export const grpcConfigSchema = baseStrategyConfigSchema.extend({
50
50
  host: z.string().describe("gRPC server hostname"),
51
51
  port: z.number().int().min(1).max(65_535).describe("gRPC port"),
52
52
  service: z
@@ -54,11 +54,6 @@ export const grpcConfigSchema = z.object({
54
54
  .default("")
55
55
  .describe("Service name to check (empty for server health)"),
56
56
  useTls: z.boolean().default(false).describe("Use TLS connection"),
57
- timeout: z
58
- .number()
59
- .min(100)
60
- .default(5000)
61
- .describe("Request timeout in milliseconds"),
62
57
  });
63
58
 
64
59
  export type GrpcConfig = z.infer<typeof grpcConfigSchema>;
@@ -89,44 +84,29 @@ const grpcResultSchema = healthResultSchema({
89
84
 
90
85
  type GrpcResult = z.infer<typeof grpcResultSchema>;
91
86
 
92
- /**
93
- * Aggregated metadata for buckets.
94
- */
95
- const grpcAggregatedDisplaySchema = healthResultSchema({
96
- avgResponseTime: healthResultNumber({
87
+ /** Aggregated field definitions for bucket merging */
88
+ const grpcAggregatedFields = {
89
+ avgResponseTime: aggregatedAverage({
97
90
  "x-chart-type": "line",
98
91
  "x-chart-label": "Avg Response Time",
99
92
  "x-chart-unit": "ms",
100
93
  }),
101
- successRate: healthResultNumber({
94
+ successRate: aggregatedRate({
102
95
  "x-chart-type": "gauge",
103
96
  "x-chart-label": "Success Rate",
104
97
  "x-chart-unit": "%",
105
98
  }),
106
- errorCount: healthResultNumber({
99
+ errorCount: aggregatedCounter({
107
100
  "x-chart-type": "counter",
108
101
  "x-chart-label": "Errors",
109
102
  }),
110
- servingCount: healthResultNumber({
103
+ servingCount: aggregatedCounter({
111
104
  "x-chart-type": "counter",
112
105
  "x-chart-label": "Serving",
113
106
  }),
114
- });
115
-
116
- const grpcAggregatedInternalSchema = z.object({
117
- _responseTime: averageStateSchema
118
- .optional(),
119
- _success: rateStateSchema
120
- .optional(),
121
- _errors: counterStateSchema.optional(),
122
- _serving: counterStateSchema.optional(),
123
- });
124
-
125
- const grpcAggregatedSchema = grpcAggregatedDisplaySchema.merge(
126
- grpcAggregatedInternalSchema,
127
- );
107
+ };
128
108
 
129
- type GrpcAggregatedResult = z.infer<typeof grpcAggregatedSchema>;
109
+ type GrpcAggregatedResult = InferAggregatedResult<typeof grpcAggregatedFields>;
130
110
 
131
111
  // ============================================================================
132
112
  // GRPC CLIENT INTERFACE (for testability)
@@ -212,7 +192,7 @@ export class GrpcHealthCheckStrategy implements HealthCheckStrategy<
212
192
  GrpcConfig,
213
193
  GrpcTransportClient,
214
194
  GrpcResult,
215
- GrpcAggregatedResult
195
+ typeof grpcAggregatedFields
216
196
  > {
217
197
  id = "grpc";
218
198
  displayName = "gRPC Health Check";
@@ -251,9 +231,9 @@ export class GrpcHealthCheckStrategy implements HealthCheckStrategy<
251
231
  ],
252
232
  });
253
233
 
254
- aggregatedResult: Versioned<GrpcAggregatedResult> = new Versioned({
234
+ aggregatedResult = new VersionedAggregated({
255
235
  version: 1,
256
- schema: grpcAggregatedSchema,
236
+ fields: grpcAggregatedFields,
257
237
  });
258
238
 
259
239
  mergeResult(
@@ -262,36 +242,20 @@ export class GrpcHealthCheckStrategy implements HealthCheckStrategy<
262
242
  ): GrpcAggregatedResult {
263
243
  const metadata = run.metadata;
264
244
 
265
- const responseTimeState = mergeAverage(
266
- existing?._responseTime as AverageState | undefined,
245
+ const avgResponseTime = mergeAverage(
246
+ existing?.avgResponseTime,
267
247
  metadata?.responseTimeMs,
268
248
  );
269
249
 
270
- const successState = mergeRate(
271
- existing?._success as RateState | undefined,
272
- metadata?.status === "SERVING",
273
- );
250
+ const isSuccess = metadata?.status === "SERVING";
251
+ const successRate = mergeRate(existing?.successRate, isSuccess);
274
252
 
275
- const errorState = mergeCounter(
276
- existing?._errors as CounterState | undefined,
277
- metadata?.error !== undefined,
278
- );
253
+ const hasError = metadata?.error !== undefined;
254
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
279
255
 
280
- const servingState = mergeCounter(
281
- existing?._serving as CounterState | undefined,
282
- metadata?.status === "SERVING",
283
- );
256
+ const servingCount = mergeCounter(existing?.servingCount, isSuccess);
284
257
 
285
- return {
286
- avgResponseTime: responseTimeState.avg,
287
- successRate: successState.rate,
288
- errorCount: errorState.count,
289
- servingCount: servingState.count,
290
- _responseTime: responseTimeState,
291
- _success: successState,
292
- _errors: errorState,
293
- _serving: servingState,
294
- };
258
+ return { avgResponseTime, successRate, errorCount, servingCount };
295
259
  }
296
260
 
297
261
  async createClient(