@checkstack/healthcheck-mysql-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-mysql-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-mysql-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
  "mysql2": "^3.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
  }
@@ -64,7 +64,7 @@ describe("QueryCollector", () => {
64
64
  });
65
65
  });
66
66
 
67
- describe("aggregateResult", () => {
67
+ describe("mergeResult", () => {
68
68
  it("should calculate average execution time and success rate", () => {
69
69
  const collector = new QueryCollector();
70
70
  const runs = [
@@ -86,10 +86,11 @@ describe("QueryCollector", () => {
86
86
  },
87
87
  ];
88
88
 
89
- const aggregated = collector.aggregateResult(runs);
89
+ let aggregated = collector.mergeResult(undefined, runs[0]);
90
+ aggregated = collector.mergeResult(aggregated, runs[1]);
90
91
 
91
- expect(aggregated.avgExecutionTimeMs).toBe(75);
92
- expect(aggregated.successRate).toBe(100);
92
+ expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
93
+ expect(aggregated.successRate.rate).toBe(100);
93
94
  });
94
95
 
95
96
  it("should calculate success rate correctly", () => {
@@ -113,9 +114,10 @@ describe("QueryCollector", () => {
113
114
  },
114
115
  ];
115
116
 
116
- const aggregated = collector.aggregateResult(runs);
117
+ let aggregated = collector.mergeResult(undefined, runs[0]);
118
+ aggregated = collector.mergeResult(aggregated, runs[1]);
117
119
 
118
- expect(aggregated.successRate).toBe(50);
120
+ expect(aggregated.successRate.rate).toBe(50);
119
121
  });
120
122
  });
121
123
 
@@ -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,
@@ -45,20 +51,24 @@ const queryResultSchema = healthResultSchema({
45
51
 
46
52
  export type QueryResult = z.infer<typeof queryResultSchema>;
47
53
 
48
- const queryAggregatedSchema = healthResultSchema({
49
- avgExecutionTimeMs: healthResultNumber({
54
+ // Aggregated result fields definition
55
+ const queryAggregatedFields = {
56
+ avgExecutionTimeMs: aggregatedAverage({
50
57
  "x-chart-type": "line",
51
58
  "x-chart-label": "Avg Execution Time",
52
59
  "x-chart-unit": "ms",
53
60
  }),
54
- successRate: healthResultNumber({
61
+ successRate: aggregatedRate({
55
62
  "x-chart-type": "gauge",
56
63
  "x-chart-label": "Success Rate",
57
64
  "x-chart-unit": "%",
58
65
  }),
59
- });
66
+ };
60
67
 
61
- export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
68
+ // Type inferred from field definitions
69
+ export type QueryAggregatedResult = InferAggregatedResult<
70
+ typeof queryAggregatedFields
71
+ >;
62
72
 
63
73
  // ============================================================================
64
74
  // QUERY COLLECTOR
@@ -68,15 +78,12 @@ export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
68
78
  * Built-in MySQL query collector.
69
79
  * Executes SQL queries and checks results.
70
80
  */
71
- export class QueryCollector
72
- implements
73
- CollectorStrategy<
74
- MysqlTransportClient,
75
- QueryConfig,
76
- QueryResult,
77
- QueryAggregatedResult
78
- >
79
- {
81
+ export class QueryCollector implements CollectorStrategy<
82
+ MysqlTransportClient,
83
+ QueryConfig,
84
+ QueryResult,
85
+ QueryAggregatedResult
86
+ > {
80
87
  id = "query";
81
88
  displayName = "SQL Query";
82
89
  description = "Execute a SQL query and check the result";
@@ -87,9 +94,9 @@ export class QueryCollector
87
94
 
88
95
  config = new Versioned({ version: 1, schema: queryConfigSchema });
89
96
  result = new Versioned({ version: 1, schema: queryResultSchema });
90
- aggregatedResult = new Versioned({
97
+ aggregatedResult = new VersionedAggregated({
91
98
  version: 1,
92
- schema: queryAggregatedSchema,
99
+ fields: queryAggregatedFields,
93
100
  });
94
101
 
95
102
  async execute({
@@ -115,28 +122,18 @@ export class QueryCollector
115
122
  };
116
123
  }
117
124
 
118
- aggregateResult(
119
- runs: HealthCheckRunForAggregation<QueryResult>[]
125
+ mergeResult(
126
+ existing: QueryAggregatedResult | undefined,
127
+ run: HealthCheckRunForAggregation<QueryResult>,
120
128
  ): QueryAggregatedResult {
121
- const times = runs
122
- .map((r) => r.metadata?.executionTimeMs)
123
- .filter((v): v is number => typeof v === "number");
124
-
125
- const successes = runs
126
- .map((r) => r.metadata?.success)
127
- .filter((v): v is boolean => typeof v === "boolean");
128
-
129
- const successCount = successes.filter(Boolean).length;
129
+ const metadata = run.metadata;
130
130
 
131
131
  return {
132
- avgExecutionTimeMs:
133
- times.length > 0
134
- ? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
135
- : 0,
136
- successRate:
137
- successes.length > 0
138
- ? Math.round((successCount / successes.length) * 100)
139
- : 0,
132
+ avgExecutionTimeMs: mergeAverage(
133
+ existing?.avgExecutionTimeMs,
134
+ metadata?.executionTimeMs,
135
+ ),
136
+ successRate: mergeRate(existing?.successRate, metadata?.success),
140
137
  };
141
138
  }
142
139
  }
@@ -8,7 +8,7 @@ describe("MysqlHealthCheckStrategy", () => {
8
8
  rowCount?: number;
9
9
  queryError?: Error;
10
10
  connectError?: Error;
11
- } = {}
11
+ } = {},
12
12
  ): DbClient => ({
13
13
  connect: mock(() =>
14
14
  config.connectError
@@ -17,10 +17,10 @@ describe("MysqlHealthCheckStrategy", () => {
17
17
  query: mock(() =>
18
18
  config.queryError
19
19
  ? Promise.reject(config.queryError)
20
- : Promise.resolve({ rowCount: config.rowCount ?? 1 })
20
+ : Promise.resolve({ rowCount: config.rowCount ?? 1 }),
21
21
  ),
22
22
  end: mock(() => Promise.resolve()),
23
- })
23
+ }),
24
24
  ),
25
25
  });
26
26
 
@@ -46,7 +46,7 @@ describe("MysqlHealthCheckStrategy", () => {
46
46
 
47
47
  it("should throw for connection error", async () => {
48
48
  const strategy = new MysqlHealthCheckStrategy(
49
- createMockClient({ connectError: new Error("Connection refused") })
49
+ createMockClient({ connectError: new Error("Connection refused") }),
50
50
  );
51
51
 
52
52
  await expect(
@@ -57,7 +57,7 @@ describe("MysqlHealthCheckStrategy", () => {
57
57
  user: "root",
58
58
  password: "secret",
59
59
  timeout: 5000,
60
- })
60
+ }),
61
61
  ).rejects.toThrow("Connection refused");
62
62
  });
63
63
  });
@@ -85,7 +85,7 @@ describe("MysqlHealthCheckStrategy", () => {
85
85
 
86
86
  it("should return error for query error", async () => {
87
87
  const strategy = new MysqlHealthCheckStrategy(
88
- createMockClient({ queryError: new Error("Syntax error") })
88
+ createMockClient({ queryError: new Error("Syntax error") }),
89
89
  );
90
90
  const connectedClient = await strategy.createClient({
91
91
  host: "localhost",
@@ -107,7 +107,7 @@ describe("MysqlHealthCheckStrategy", () => {
107
107
 
108
108
  it("should return custom row count", async () => {
109
109
  const strategy = new MysqlHealthCheckStrategy(
110
- createMockClient({ rowCount: 5 })
110
+ createMockClient({ rowCount: 5 }),
111
111
  );
112
112
  const connectedClient = await strategy.createClient({
113
113
  host: "localhost",
@@ -128,7 +128,7 @@ describe("MysqlHealthCheckStrategy", () => {
128
128
  });
129
129
  });
130
130
 
131
- describe("aggregateResult", () => {
131
+ describe("mergeResult", () => {
132
132
  it("should calculate averages correctly", () => {
133
133
  const strategy = new MysqlHealthCheckStrategy();
134
134
  const runs = [
@@ -158,34 +158,33 @@ describe("MysqlHealthCheckStrategy", () => {
158
158
  },
159
159
  ];
160
160
 
161
- const aggregated = strategy.aggregateResult(runs);
161
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
162
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
162
163
 
163
- expect(aggregated.avgConnectionTime).toBe(75);
164
- expect(aggregated.successRate).toBe(100);
165
- expect(aggregated.errorCount).toBe(0);
164
+ expect(aggregated.avgConnectionTime.avg).toBe(75);
165
+ expect(aggregated.successRate.rate).toBe(100);
166
+ expect(aggregated.errorCount.count).toBe(0);
166
167
  });
167
168
 
168
169
  it("should count errors", () => {
169
170
  const strategy = new MysqlHealthCheckStrategy();
170
- const runs = [
171
- {
172
- id: "1",
173
- status: "unhealthy" as const,
174
- latencyMs: 100,
175
- checkId: "c1",
176
- timestamp: new Date(),
177
- metadata: {
178
- connected: false,
179
- connectionTimeMs: 100,
180
- error: "Connection refused",
181
- },
171
+ const run = {
172
+ id: "1",
173
+ status: "unhealthy" as const,
174
+ latencyMs: 100,
175
+ checkId: "c1",
176
+ timestamp: new Date(),
177
+ metadata: {
178
+ connected: false,
179
+ connectionTimeMs: 100,
180
+ error: "Connection refused",
182
181
  },
183
- ];
182
+ };
184
183
 
185
- const aggregated = strategy.aggregateResult(runs);
184
+ const aggregated = strategy.mergeResult(undefined, run);
186
185
 
187
- expect(aggregated.errorCount).toBe(1);
188
- expect(aggregated.successRate).toBe(0);
186
+ expect(aggregated.errorCount.count).toBe(1);
187
+ expect(aggregated.successRate.rate).toBe(0);
189
188
  });
190
189
  });
191
190
  });
package/src/strategy.ts CHANGED
@@ -3,10 +3,20 @@ 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
  type ConnectedClient,
19
+ type InferAggregatedResult,
10
20
  } from "@checkstack/backend-api";
11
21
  import {
12
22
  healthResultBoolean,
@@ -68,32 +78,32 @@ const mysqlResultSchema = healthResultSchema({
68
78
 
69
79
  type MysqlResult = z.infer<typeof mysqlResultSchema>;
70
80
 
71
- /**
72
- * Aggregated metadata for buckets.
73
- */
74
- const mysqlAggregatedSchema = healthResultSchema({
75
- avgConnectionTime: healthResultNumber({
81
+ /** Aggregated field definitions for bucket merging */
82
+ const mysqlAggregatedFields = {
83
+ avgConnectionTime: aggregatedAverage({
76
84
  "x-chart-type": "line",
77
85
  "x-chart-label": "Avg Connection Time",
78
86
  "x-chart-unit": "ms",
79
87
  }),
80
- maxConnectionTime: healthResultNumber({
88
+ maxConnectionTime: aggregatedMinMax({
81
89
  "x-chart-type": "line",
82
90
  "x-chart-label": "Max Connection Time",
83
91
  "x-chart-unit": "ms",
84
92
  }),
85
- successRate: healthResultNumber({
93
+ successRate: aggregatedRate({
86
94
  "x-chart-type": "gauge",
87
95
  "x-chart-label": "Success Rate",
88
96
  "x-chart-unit": "%",
89
97
  }),
90
- errorCount: healthResultNumber({
98
+ errorCount: aggregatedCounter({
91
99
  "x-chart-type": "counter",
92
100
  "x-chart-label": "Errors",
93
101
  }),
94
- });
102
+ };
95
103
 
96
- type MysqlAggregatedResult = z.infer<typeof mysqlAggregatedSchema>;
104
+ type MysqlAggregatedResult = InferAggregatedResult<
105
+ typeof mysqlAggregatedFields
106
+ >;
97
107
 
98
108
  // ============================================================================
99
109
  // DATABASE CLIENT INTERFACE (for testability)
@@ -147,15 +157,12 @@ const defaultDbClient: DbClient = {
147
157
  // STRATEGY
148
158
  // ============================================================================
149
159
 
150
- export class MysqlHealthCheckStrategy
151
- implements
152
- HealthCheckStrategy<
153
- MysqlConfig,
154
- MysqlTransportClient,
155
- MysqlResult,
156
- MysqlAggregatedResult
157
- >
158
- {
160
+ export class MysqlHealthCheckStrategy implements HealthCheckStrategy<
161
+ MysqlConfig,
162
+ MysqlTransportClient,
163
+ MysqlResult,
164
+ typeof mysqlAggregatedFields
165
+ > {
159
166
  id = "mysql";
160
167
  displayName = "MySQL Health Check";
161
168
  description = "MySQL database connectivity and query health check";
@@ -192,58 +199,38 @@ export class MysqlHealthCheckStrategy
192
199
  ],
193
200
  });
194
201
 
195
- aggregatedResult: Versioned<MysqlAggregatedResult> = new Versioned({
202
+ aggregatedResult = new VersionedAggregated({
196
203
  version: 1,
197
- schema: mysqlAggregatedSchema,
204
+ fields: mysqlAggregatedFields,
198
205
  });
199
206
 
200
- aggregateResult(
201
- runs: HealthCheckRunForAggregation<MysqlResult>[]
207
+ mergeResult(
208
+ existing: MysqlAggregatedResult | undefined,
209
+ run: HealthCheckRunForAggregation<MysqlResult>,
202
210
  ): MysqlAggregatedResult {
203
- const validRuns = runs.filter((r) => r.metadata);
211
+ const metadata = run.metadata;
204
212
 
205
- if (validRuns.length === 0) {
206
- return {
207
- avgConnectionTime: 0,
208
- maxConnectionTime: 0,
209
- successRate: 0,
210
- errorCount: 0,
211
- };
212
- }
213
+ const avgConnectionTime = mergeAverage(
214
+ existing?.avgConnectionTime,
215
+ metadata?.connectionTimeMs,
216
+ );
213
217
 
214
- const connectionTimes = validRuns
215
- .map((r) => r.metadata?.connectionTimeMs)
216
- .filter((t): t is number => typeof t === "number");
218
+ const maxConnectionTime = mergeMinMax(
219
+ existing?.maxConnectionTime,
220
+ metadata?.connectionTimeMs,
221
+ );
217
222
 
218
- const avgConnectionTime =
219
- connectionTimes.length > 0
220
- ? Math.round(
221
- connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
222
- )
223
- : 0;
223
+ const isSuccess = metadata?.connected ?? false;
224
+ const successRate = mergeRate(existing?.successRate, isSuccess);
224
225
 
225
- const maxConnectionTime =
226
- connectionTimes.length > 0 ? Math.max(...connectionTimes) : 0;
226
+ const hasError = metadata?.error !== undefined;
227
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
227
228
 
228
- const successCount = validRuns.filter(
229
- (r) => r.metadata?.connected === true
230
- ).length;
231
- const successRate = Math.round((successCount / validRuns.length) * 100);
232
-
233
- const errorCount = validRuns.filter(
234
- (r) => r.metadata?.error !== undefined
235
- ).length;
236
-
237
- return {
238
- avgConnectionTime,
239
- maxConnectionTime,
240
- successRate,
241
- errorCount,
242
- };
229
+ return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
243
230
  }
244
231
 
245
232
  async createClient(
246
- config: MysqlConfigInput
233
+ config: MysqlConfigInput,
247
234
  ): Promise<ConnectedClient<MysqlTransportClient>> {
248
235
  const validatedConfig = this.config.validate(config);
249
236