@checkstack/healthcheck-grpc-backend 0.1.13 → 0.1.14

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,35 @@
1
1
  # @checkstack/healthcheck-grpc-backend
2
2
 
3
+ ## 0.1.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
8
+
9
+ ### Breaking Changes (Internal)
10
+
11
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
12
+
13
+ ### New Features
14
+
15
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
16
+ - `mergeCounter()` - track occurrences
17
+ - `mergeAverage()` - track sum/count, compute avg
18
+ - `mergeRate()` - track success/total, compute %
19
+ - `mergeMinMax()` - track min/max values
20
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
21
+
22
+ ### Improvements
23
+
24
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
25
+ - Prepares for real-time hourly aggregation without batch accumulation
26
+
27
+ - Updated dependencies [f676e11]
28
+ - Updated dependencies [48c2080]
29
+ - @checkstack/common@0.6.2
30
+ - @checkstack/backend-api@0.6.0
31
+ - @checkstack/healthcheck-common@0.8.2
32
+
3
33
  ## 0.1.13
4
34
 
5
35
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-grpc-backend",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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.1",
13
- "@checkstack/common": "0.6.0",
14
- "@checkstack/healthcheck-common": "0.7.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.2",
21
- "@checkstack/scripts": "0.1.0"
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("aggregateResult", () => {
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,7 +87,8 @@ describe("HealthCollector", () => {
87
87
  },
88
88
  ];
89
89
 
90
- const aggregated = collector.aggregateResult(runs);
90
+ let aggregated = collector.mergeResult(undefined, runs[0]);
91
+ aggregated = collector.mergeResult(aggregated, runs[1]);
91
92
 
92
93
  expect(aggregated.avgResponseTimeMs).toBe(75);
93
94
  expect(aggregated.servingRate).toBe(100);
@@ -118,7 +119,8 @@ describe("HealthCollector", () => {
118
119
  },
119
120
  ];
120
121
 
121
- const aggregated = collector.aggregateResult(runs);
122
+ let aggregated = collector.mergeResult(undefined, runs[0]);
123
+ aggregated = collector.mergeResult(aggregated, runs[1]);
122
124
 
123
125
  expect(aggregated.servingRate).toBe(50);
124
126
  });
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ averageStateSchema,
9
+ mergeRate,
10
+ rateStateSchema,
11
+ type AverageState,
12
+ type RateState,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -49,7 +55,7 @@ const grpcHealthResultSchema = healthResultSchema({
49
55
 
50
56
  export type HealthResult = z.infer<typeof grpcHealthResultSchema>;
51
57
 
52
- const healthAggregatedSchema = healthResultSchema({
58
+ const healthAggregatedDisplaySchema = healthResultSchema({
53
59
  avgResponseTimeMs: healthResultNumber({
54
60
  "x-chart-type": "line",
55
61
  "x-chart-label": "Avg Response Time",
@@ -62,6 +68,17 @@ const healthAggregatedSchema = healthResultSchema({
62
68
  }),
63
69
  });
64
70
 
71
+ const healthAggregatedInternalSchema = z.object({
72
+ _responseTime: averageStateSchema
73
+ .optional(),
74
+ _serving: rateStateSchema
75
+ .optional(),
76
+ });
77
+
78
+ const healthAggregatedSchema = healthAggregatedDisplaySchema.merge(
79
+ healthAggregatedInternalSchema,
80
+ );
81
+
65
82
  export type HealthAggregatedResult = z.infer<typeof healthAggregatedSchema>;
66
83
 
67
84
  // ============================================================================
@@ -72,15 +89,12 @@ export type HealthAggregatedResult = z.infer<typeof healthAggregatedSchema>;
72
89
  * Built-in gRPC health collector.
73
90
  * Checks gRPC health status using the standard Health Checking Protocol.
74
91
  */
75
- export class HealthCollector
76
- implements
77
- CollectorStrategy<
78
- GrpcTransportClient,
79
- HealthConfig,
80
- HealthResult,
81
- HealthAggregatedResult
82
- >
83
- {
92
+ export class HealthCollector implements CollectorStrategy<
93
+ GrpcTransportClient,
94
+ HealthConfig,
95
+ HealthResult,
96
+ HealthAggregatedResult
97
+ > {
84
98
  id = "health";
85
99
  displayName = "gRPC Health Check";
86
100
  description = "Check gRPC service health status";
@@ -124,28 +138,27 @@ export class HealthCollector
124
138
  };
125
139
  }
126
140
 
127
- aggregateResult(
128
- runs: HealthCheckRunForAggregation<HealthResult>[]
141
+ mergeResult(
142
+ existing: HealthAggregatedResult | undefined,
143
+ run: HealthCheckRunForAggregation<HealthResult>,
129
144
  ): HealthAggregatedResult {
130
- const times = runs
131
- .map((r) => r.metadata?.responseTimeMs)
132
- .filter((v): v is number => typeof v === "number");
145
+ const metadata = run.metadata;
133
146
 
134
- const servingResults = runs
135
- .map((r) => r.metadata?.serving)
136
- .filter((v): v is boolean => typeof v === "boolean");
147
+ const responseTimeState = mergeAverage(
148
+ existing?._responseTime as AverageState | undefined,
149
+ metadata?.responseTimeMs,
150
+ );
137
151
 
138
- const servingCount = servingResults.filter(Boolean).length;
152
+ const servingState = mergeRate(
153
+ existing?._serving as RateState | undefined,
154
+ metadata?.serving,
155
+ );
139
156
 
140
157
  return {
141
- avgResponseTimeMs:
142
- times.length > 0
143
- ? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
144
- : 0,
145
- servingRate:
146
- servingResults.length > 0
147
- ? Math.round((servingCount / servingResults.length) * 100)
148
- : 0,
158
+ avgResponseTimeMs: responseTimeState.avg,
159
+ servingRate: servingState.rate,
160
+ _responseTime: responseTimeState,
161
+ _serving: servingState,
149
162
  };
150
163
  }
151
164
  }
@@ -111,7 +111,7 @@ describe("GrpcHealthCheckStrategy", () => {
111
111
  });
112
112
  });
113
113
 
114
- describe("aggregateResult", () => {
114
+ describe("mergeResult", () => {
115
115
  it("should calculate averages correctly", () => {
116
116
  const strategy = new GrpcHealthCheckStrategy();
117
117
  const runs = [
@@ -141,7 +141,8 @@ describe("GrpcHealthCheckStrategy", () => {
141
141
  },
142
142
  ];
143
143
 
144
- const aggregated = strategy.aggregateResult(runs);
144
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
145
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
145
146
 
146
147
  expect(aggregated.avgResponseTime).toBe(10);
147
148
  expect(aggregated.successRate).toBe(100);
@@ -179,7 +180,8 @@ describe("GrpcHealthCheckStrategy", () => {
179
180
  },
180
181
  ];
181
182
 
182
- const aggregated = strategy.aggregateResult(runs);
183
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
184
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
183
185
 
184
186
  expect(aggregated.errorCount).toBe(1);
185
187
  expect(aggregated.servingCount).toBe(0);
package/src/strategy.ts CHANGED
@@ -5,6 +5,15 @@ import {
5
5
  Versioned,
6
6
  z,
7
7
  type ConnectedClient,
8
+ mergeAverage,
9
+ averageStateSchema,
10
+ mergeRate,
11
+ rateStateSchema,
12
+ mergeCounter,
13
+ counterStateSchema,
14
+ type AverageState,
15
+ type RateState,
16
+ type CounterState,
8
17
  } from "@checkstack/backend-api";
9
18
  import {
10
19
  healthResultBoolean,
@@ -83,7 +92,7 @@ type GrpcResult = z.infer<typeof grpcResultSchema>;
83
92
  /**
84
93
  * Aggregated metadata for buckets.
85
94
  */
86
- const grpcAggregatedSchema = healthResultSchema({
95
+ const grpcAggregatedDisplaySchema = healthResultSchema({
87
96
  avgResponseTime: healthResultNumber({
88
97
  "x-chart-type": "line",
89
98
  "x-chart-label": "Avg Response Time",
@@ -104,6 +113,19 @@ const grpcAggregatedSchema = healthResultSchema({
104
113
  }),
105
114
  });
106
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
+ );
128
+
107
129
  type GrpcAggregatedResult = z.infer<typeof grpcAggregatedSchema>;
108
130
 
109
131
  // ============================================================================
@@ -176,7 +198,7 @@ const defaultGrpcClient: GrpcHealthClient = {
176
198
  resolve({
177
199
  status: statusMap[response?.status ?? 0] ?? "UNKNOWN",
178
200
  });
179
- }
201
+ },
180
202
  );
181
203
  });
182
204
  },
@@ -186,15 +208,12 @@ const defaultGrpcClient: GrpcHealthClient = {
186
208
  // STRATEGY
187
209
  // ============================================================================
188
210
 
189
- export class GrpcHealthCheckStrategy
190
- implements
191
- HealthCheckStrategy<
192
- GrpcConfig,
193
- GrpcTransportClient,
194
- GrpcResult,
195
- GrpcAggregatedResult
196
- >
197
- {
211
+ export class GrpcHealthCheckStrategy implements HealthCheckStrategy<
212
+ GrpcConfig,
213
+ GrpcTransportClient,
214
+ GrpcResult,
215
+ GrpcAggregatedResult
216
+ > {
198
217
  id = "grpc";
199
218
  displayName = "gRPC Health Check";
200
219
  description =
@@ -237,50 +256,46 @@ export class GrpcHealthCheckStrategy
237
256
  schema: grpcAggregatedSchema,
238
257
  });
239
258
 
240
- aggregateResult(
241
- runs: HealthCheckRunForAggregation<GrpcResult>[]
259
+ mergeResult(
260
+ existing: GrpcAggregatedResult | undefined,
261
+ run: HealthCheckRunForAggregation<GrpcResult>,
242
262
  ): GrpcAggregatedResult {
243
- const validRuns = runs.filter((r) => r.metadata);
244
-
245
- if (validRuns.length === 0) {
246
- return {
247
- avgResponseTime: 0,
248
- successRate: 0,
249
- errorCount: 0,
250
- servingCount: 0,
251
- };
252
- }
263
+ const metadata = run.metadata;
253
264
 
254
- const responseTimes = validRuns
255
- .map((r) => r.metadata?.responseTimeMs)
256
- .filter((t): t is number => typeof t === "number");
265
+ const responseTimeState = mergeAverage(
266
+ existing?._responseTime as AverageState | undefined,
267
+ metadata?.responseTimeMs,
268
+ );
257
269
 
258
- const avgResponseTime =
259
- responseTimes.length > 0
260
- ? Math.round(
261
- responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
262
- )
263
- : 0;
270
+ const successState = mergeRate(
271
+ existing?._success as RateState | undefined,
272
+ metadata?.status === "SERVING",
273
+ );
264
274
 
265
- const servingCount = validRuns.filter(
266
- (r) => r.metadata?.status === "SERVING"
267
- ).length;
268
- const successRate = Math.round((servingCount / validRuns.length) * 100);
275
+ const errorState = mergeCounter(
276
+ existing?._errors as CounterState | undefined,
277
+ metadata?.error !== undefined,
278
+ );
269
279
 
270
- const errorCount = validRuns.filter(
271
- (r) => r.metadata?.error !== undefined
272
- ).length;
280
+ const servingState = mergeCounter(
281
+ existing?._serving as CounterState | undefined,
282
+ metadata?.status === "SERVING",
283
+ );
273
284
 
274
285
  return {
275
- avgResponseTime,
276
- successRate,
277
- errorCount,
278
- servingCount,
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,
279
294
  };
280
295
  }
281
296
 
282
297
  async createClient(
283
- config: GrpcConfigInput
298
+ config: GrpcConfigInput,
284
299
  ): Promise<ConnectedClient<GrpcTransportClient>> {
285
300
  const validatedConfig = this.config.validate(config);
286
301