@checkstack/healthcheck-redis-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 CHANGED
@@ -1,5 +1,59 @@
1
1
  # @checkstack/healthcheck-redis-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-redis-backend",
3
- "version": "0.1.13",
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.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
  "ioredis": "^5.3.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
  }
@@ -79,7 +79,7 @@ describe("CommandCollector", () => {
79
79
  });
80
80
  });
81
81
 
82
- describe("aggregateResult", () => {
82
+ describe("mergeResult", () => {
83
83
  it("should calculate average response time and success rate", () => {
84
84
  const collector = new CommandCollector();
85
85
  const runs = [
@@ -101,10 +101,11 @@ describe("CommandCollector", () => {
101
101
  },
102
102
  ];
103
103
 
104
- const aggregated = collector.aggregateResult(runs);
104
+ let aggregated = collector.mergeResult(undefined, runs[0]);
105
+ aggregated = collector.mergeResult(aggregated, runs[1]);
105
106
 
106
- expect(aggregated.avgResponseTimeMs).toBe(10);
107
- expect(aggregated.successRate).toBe(100);
107
+ expect(aggregated.avgResponseTimeMs.avg).toBe(10);
108
+ expect(aggregated.successRate.rate).toBe(100);
108
109
  });
109
110
 
110
111
  it("should calculate success rate correctly", () => {
@@ -128,9 +129,10 @@ describe("CommandCollector", () => {
128
129
  },
129
130
  ];
130
131
 
131
- const aggregated = collector.aggregateResult(runs);
132
+ let aggregated = collector.mergeResult(undefined, runs[0]);
133
+ aggregated = collector.mergeResult(aggregated, runs[1]);
132
134
 
133
- expect(aggregated.successRate).toBe(50);
135
+ expect(aggregated.successRate.rate).toBe(50);
134
136
  });
135
137
  });
136
138
 
@@ -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,
@@ -53,20 +59,24 @@ const commandResultSchema = healthResultSchema({
53
59
 
54
60
  export type CommandResult = z.infer<typeof commandResultSchema>;
55
61
 
56
- const commandAggregatedSchema = healthResultSchema({
57
- avgResponseTimeMs: healthResultNumber({
62
+ // Aggregated result fields definition
63
+ const commandAggregatedFields = {
64
+ avgResponseTimeMs: aggregatedAverage({
58
65
  "x-chart-type": "line",
59
66
  "x-chart-label": "Avg Response Time",
60
67
  "x-chart-unit": "ms",
61
68
  }),
62
- successRate: healthResultNumber({
69
+ successRate: aggregatedRate({
63
70
  "x-chart-type": "gauge",
64
71
  "x-chart-label": "Success Rate",
65
72
  "x-chart-unit": "%",
66
73
  }),
67
- });
74
+ };
68
75
 
69
- export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
76
+ // Type inferred from field definitions
77
+ export type CommandAggregatedResult = InferAggregatedResult<
78
+ typeof commandAggregatedFields
79
+ >;
70
80
 
71
81
  // ============================================================================
72
82
  // COMMAND COLLECTOR
@@ -76,15 +86,12 @@ export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
76
86
  * Built-in Redis command collector.
77
87
  * Executes Redis commands and checks results.
78
88
  */
79
- export class CommandCollector
80
- implements
81
- CollectorStrategy<
82
- RedisTransportClient,
83
- CommandConfig,
84
- CommandResult,
85
- CommandAggregatedResult
86
- >
87
- {
89
+ export class CommandCollector implements CollectorStrategy<
90
+ RedisTransportClient,
91
+ CommandConfig,
92
+ CommandResult,
93
+ CommandAggregatedResult
94
+ > {
88
95
  id = "command";
89
96
  displayName = "Redis Command";
90
97
  description = "Execute a Redis command and check the result";
@@ -95,9 +102,9 @@ export class CommandCollector
95
102
 
96
103
  config = new Versioned({ version: 1, schema: commandConfigSchema });
97
104
  result = new Versioned({ version: 1, schema: commandResultSchema });
98
- aggregatedResult = new Versioned({
105
+ aggregatedResult = new VersionedAggregated({
99
106
  version: 1,
100
- schema: commandAggregatedSchema,
107
+ fields: commandAggregatedFields,
101
108
  });
102
109
 
103
110
  async execute({
@@ -127,28 +134,18 @@ export class CommandCollector
127
134
  };
128
135
  }
129
136
 
130
- aggregateResult(
131
- runs: HealthCheckRunForAggregation<CommandResult>[]
137
+ mergeResult(
138
+ existing: CommandAggregatedResult | undefined,
139
+ run: HealthCheckRunForAggregation<CommandResult>,
132
140
  ): CommandAggregatedResult {
133
- const times = runs
134
- .map((r) => r.metadata?.responseTimeMs)
135
- .filter((v): v is number => typeof v === "number");
136
-
137
- const successes = runs
138
- .map((r) => r.metadata?.success)
139
- .filter((v): v is boolean => typeof v === "boolean");
140
-
141
- const successCount = successes.filter(Boolean).length;
141
+ const metadata = run.metadata;
142
142
 
143
143
  return {
144
- avgResponseTimeMs:
145
- times.length > 0
146
- ? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
147
- : 0,
148
- successRate:
149
- successes.length > 0
150
- ? Math.round((successCount / successes.length) * 100)
151
- : 0,
144
+ avgResponseTimeMs: mergeAverage(
145
+ existing?.avgResponseTimeMs,
146
+ metadata?.responseTimeMs,
147
+ ),
148
+ successRate: mergeRate(existing?.successRate, metadata?.success),
152
149
  };
153
150
  }
154
151
  }
@@ -12,17 +12,17 @@ describe("RedisHealthCheckStrategy", () => {
12
12
  pingResponse?: string;
13
13
  infoResponse?: string;
14
14
  pingError?: Error;
15
- } = {}
15
+ } = {},
16
16
  ): RedisConnection => ({
17
17
  ping: mock(() =>
18
18
  config.pingError
19
19
  ? Promise.reject(config.pingError)
20
- : Promise.resolve(config.pingResponse ?? "PONG")
20
+ : Promise.resolve(config.pingResponse ?? "PONG"),
21
21
  ),
22
22
  info: mock(() =>
23
23
  Promise.resolve(
24
- config.infoResponse ?? "redis_version:7.0.0\r\nrole:master\r\n"
25
- )
24
+ config.infoResponse ?? "redis_version:7.0.0\r\nrole:master\r\n",
25
+ ),
26
26
  ),
27
27
  get: mock(() => Promise.resolve(undefined)),
28
28
  quit: mock(() => Promise.resolve("OK")),
@@ -35,12 +35,12 @@ describe("RedisHealthCheckStrategy", () => {
35
35
  infoResponse?: string;
36
36
  pingError?: Error;
37
37
  connectError?: Error;
38
- } = {}
38
+ } = {},
39
39
  ): RedisClient => ({
40
40
  connect: mock(() =>
41
41
  config.connectError
42
42
  ? Promise.reject(config.connectError)
43
- : Promise.resolve(createMockConnection(config))
43
+ : Promise.resolve(createMockConnection(config)),
44
44
  ),
45
45
  });
46
46
 
@@ -63,7 +63,7 @@ describe("RedisHealthCheckStrategy", () => {
63
63
 
64
64
  it("should throw for connection error", async () => {
65
65
  const strategy = new RedisHealthCheckStrategy(
66
- createMockClient({ connectError: new Error("Connection refused") })
66
+ createMockClient({ connectError: new Error("Connection refused") }),
67
67
  );
68
68
 
69
69
  await expect(
@@ -71,7 +71,7 @@ describe("RedisHealthCheckStrategy", () => {
71
71
  host: "localhost",
72
72
  port: 6379,
73
73
  timeout: 5000,
74
- })
74
+ }),
75
75
  ).rejects.toThrow("Connection refused");
76
76
  });
77
77
  });
@@ -94,7 +94,7 @@ describe("RedisHealthCheckStrategy", () => {
94
94
 
95
95
  it("should return error for ping failure", async () => {
96
96
  const strategy = new RedisHealthCheckStrategy(
97
- createMockClient({ pingError: new Error("NOAUTH") })
97
+ createMockClient({ pingError: new Error("NOAUTH") }),
98
98
  );
99
99
  const connectedClient = await strategy.createClient({
100
100
  host: "localhost",
@@ -128,7 +128,7 @@ describe("RedisHealthCheckStrategy", () => {
128
128
  });
129
129
  });
130
130
 
131
- describe("aggregateResult", () => {
131
+ describe("mergeResult", () => {
132
132
  it("should calculate averages correctly", () => {
133
133
  const strategy = new RedisHealthCheckStrategy();
134
134
  const runs = [
@@ -160,35 +160,34 @@ describe("RedisHealthCheckStrategy", () => {
160
160
  },
161
161
  ];
162
162
 
163
- const aggregated = strategy.aggregateResult(runs);
163
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
164
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
164
165
 
165
- expect(aggregated.avgConnectionTime).toBe(10);
166
- expect(aggregated.successRate).toBe(100);
167
- expect(aggregated.errorCount).toBe(0);
166
+ expect(aggregated.avgConnectionTime.avg).toBe(10);
167
+ expect(aggregated.successRate.rate).toBe(100);
168
+ expect(aggregated.errorCount.count).toBe(0);
168
169
  });
169
170
 
170
171
  it("should count errors", () => {
171
172
  const strategy = new RedisHealthCheckStrategy();
172
- const runs = [
173
- {
174
- id: "1",
175
- status: "unhealthy" as const,
176
- latencyMs: 100,
177
- checkId: "c1",
178
- timestamp: new Date(),
179
- metadata: {
180
- connected: false,
181
- connectionTimeMs: 100,
182
- pingSuccess: false,
183
- error: "Connection refused",
184
- },
173
+ const run = {
174
+ id: "1",
175
+ status: "unhealthy" as const,
176
+ latencyMs: 100,
177
+ checkId: "c1",
178
+ timestamp: new Date(),
179
+ metadata: {
180
+ connected: false,
181
+ connectionTimeMs: 100,
182
+ pingSuccess: false,
183
+ error: "Connection refused",
185
184
  },
186
- ];
185
+ };
187
186
 
188
- const aggregated = strategy.aggregateResult(runs);
187
+ const aggregated = strategy.mergeResult(undefined, run);
189
188
 
190
- expect(aggregated.errorCount).toBe(1);
191
- expect(aggregated.successRate).toBe(0);
189
+ expect(aggregated.errorCount.count).toBe(1);
190
+ expect(aggregated.successRate.rate).toBe(0);
192
191
  });
193
192
  });
194
193
  });
package/src/strategy.ts CHANGED
@@ -3,11 +3,21 @@ import {
3
3
  HealthCheckStrategy,
4
4
  HealthCheckRunForAggregation,
5
5
  Versioned,
6
+ VersionedAggregated,
7
+ aggregatedAverage,
8
+ aggregatedMinMax,
9
+ aggregatedRate,
10
+ aggregatedCounter,
11
+ mergeAverage,
12
+ mergeRate,
13
+ mergeCounter,
14
+ mergeMinMax,
6
15
  z,
7
16
  configString,
8
17
  configNumber,
9
18
  configBoolean,
10
19
  type ConnectedClient,
20
+ type InferAggregatedResult,
11
21
  } from "@checkstack/backend-api";
12
22
  import {
13
23
  healthResultBoolean,
@@ -75,32 +85,32 @@ const redisResultSchema = healthResultSchema({
75
85
 
76
86
  type RedisResult = z.infer<typeof redisResultSchema>;
77
87
 
78
- /**
79
- * Aggregated metadata for buckets.
80
- */
81
- const redisAggregatedSchema = healthResultSchema({
82
- avgConnectionTime: healthResultNumber({
88
+ /** Aggregated field definitions for bucket merging */
89
+ const redisAggregatedFields = {
90
+ avgConnectionTime: aggregatedAverage({
83
91
  "x-chart-type": "line",
84
92
  "x-chart-label": "Avg Connection Time",
85
93
  "x-chart-unit": "ms",
86
94
  }),
87
- maxConnectionTime: healthResultNumber({
95
+ maxConnectionTime: aggregatedMinMax({
88
96
  "x-chart-type": "line",
89
97
  "x-chart-label": "Max Connection Time",
90
98
  "x-chart-unit": "ms",
91
99
  }),
92
- successRate: healthResultNumber({
100
+ successRate: aggregatedRate({
93
101
  "x-chart-type": "gauge",
94
102
  "x-chart-label": "Success Rate",
95
103
  "x-chart-unit": "%",
96
104
  }),
97
- errorCount: healthResultNumber({
105
+ errorCount: aggregatedCounter({
98
106
  "x-chart-type": "counter",
99
107
  "x-chart-label": "Errors",
100
108
  }),
101
- });
109
+ };
102
110
 
103
- type RedisAggregatedResult = z.infer<typeof redisAggregatedSchema>;
111
+ type RedisAggregatedResult = InferAggregatedResult<
112
+ typeof redisAggregatedFields
113
+ >;
104
114
 
105
115
  // ============================================================================
106
116
  // REDIS CLIENT INTERFACE (for testability)
@@ -160,15 +170,12 @@ const defaultRedisClient: RedisClient = {
160
170
  // STRATEGY
161
171
  // ============================================================================
162
172
 
163
- export class RedisHealthCheckStrategy
164
- implements
165
- HealthCheckStrategy<
166
- RedisConfig,
167
- RedisTransportClient,
168
- RedisResult,
169
- RedisAggregatedResult
170
- >
171
- {
173
+ export class RedisHealthCheckStrategy implements HealthCheckStrategy<
174
+ RedisConfig,
175
+ RedisTransportClient,
176
+ RedisResult,
177
+ typeof redisAggregatedFields
178
+ > {
172
179
  id = "redis";
173
180
  displayName = "Redis Health Check";
174
181
  description = "Redis server connectivity and health monitoring";
@@ -205,58 +212,38 @@ export class RedisHealthCheckStrategy
205
212
  ],
206
213
  });
207
214
 
208
- aggregatedResult: Versioned<RedisAggregatedResult> = new Versioned({
215
+ aggregatedResult = new VersionedAggregated({
209
216
  version: 1,
210
- schema: redisAggregatedSchema,
217
+ fields: redisAggregatedFields,
211
218
  });
212
219
 
213
- aggregateResult(
214
- runs: HealthCheckRunForAggregation<RedisResult>[]
220
+ mergeResult(
221
+ existing: RedisAggregatedResult | undefined,
222
+ run: HealthCheckRunForAggregation<RedisResult>,
215
223
  ): RedisAggregatedResult {
216
- const validRuns = runs.filter((r) => r.metadata);
224
+ const metadata = run.metadata;
217
225
 
218
- if (validRuns.length === 0) {
219
- return {
220
- avgConnectionTime: 0,
221
- maxConnectionTime: 0,
222
- successRate: 0,
223
- errorCount: 0,
224
- };
225
- }
226
+ const avgConnectionTime = mergeAverage(
227
+ existing?.avgConnectionTime,
228
+ metadata?.connectionTimeMs,
229
+ );
226
230
 
227
- const connectionTimes = validRuns
228
- .map((r) => r.metadata?.connectionTimeMs)
229
- .filter((t): t is number => typeof t === "number");
231
+ const maxConnectionTime = mergeMinMax(
232
+ existing?.maxConnectionTime,
233
+ metadata?.connectionTimeMs,
234
+ );
230
235
 
231
- const avgConnectionTime =
232
- connectionTimes.length > 0
233
- ? Math.round(
234
- connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
235
- )
236
- : 0;
236
+ const isSuccess = metadata?.connected ?? false;
237
+ const successRate = mergeRate(existing?.successRate, isSuccess);
237
238
 
238
- const maxConnectionTime =
239
- connectionTimes.length > 0 ? Math.max(...connectionTimes) : 0;
239
+ const hasError = metadata?.error !== undefined;
240
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
240
241
 
241
- const successCount = validRuns.filter(
242
- (r) => r.metadata?.connected === true
243
- ).length;
244
- const successRate = Math.round((successCount / validRuns.length) * 100);
245
-
246
- const errorCount = validRuns.filter(
247
- (r) => r.metadata?.error !== undefined
248
- ).length;
249
-
250
- return {
251
- avgConnectionTime,
252
- maxConnectionTime,
253
- successRate,
254
- errorCount,
255
- };
242
+ return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
256
243
  }
257
244
 
258
245
  async createClient(
259
- config: RedisConfigInput
246
+ config: RedisConfigInput,
260
247
  ): Promise<ConnectedClient<RedisTransportClient>> {
261
248
  const validatedConfig = this.config.validate(config);
262
249