@checkstack/healthcheck-mysql-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 +30 -0
- package/package.json +6 -6
- package/src/query-collector.test.ts +5 -3
- package/src/query-collector.ts +38 -27
- package/src/strategy.test.ts +22 -23
- package/src/strategy.ts +69 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @checkstack/healthcheck-mysql-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-mysql-backend",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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
|
"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.
|
|
21
|
-
"@checkstack/scripts": "0.1.
|
|
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("
|
|
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,7 +86,8 @@ describe("QueryCollector", () => {
|
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
90
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
90
91
|
|
|
91
92
|
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
92
93
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -113,7 +114,8 @@ describe("QueryCollector", () => {
|
|
|
113
114
|
},
|
|
114
115
|
];
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
118
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
117
119
|
|
|
118
120
|
expect(aggregated.successRate).toBe(50);
|
|
119
121
|
});
|
package/src/query-collector.ts
CHANGED
|
@@ -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,
|
|
@@ -45,7 +51,7 @@ const queryResultSchema = healthResultSchema({
|
|
|
45
51
|
|
|
46
52
|
export type QueryResult = z.infer<typeof queryResultSchema>;
|
|
47
53
|
|
|
48
|
-
const
|
|
54
|
+
const queryAggregatedDisplaySchema = healthResultSchema({
|
|
49
55
|
avgExecutionTimeMs: healthResultNumber({
|
|
50
56
|
"x-chart-type": "line",
|
|
51
57
|
"x-chart-label": "Avg Execution Time",
|
|
@@ -58,6 +64,15 @@ const queryAggregatedSchema = healthResultSchema({
|
|
|
58
64
|
}),
|
|
59
65
|
});
|
|
60
66
|
|
|
67
|
+
const queryAggregatedInternalSchema = z.object({
|
|
68
|
+
_executionTime: averageStateSchema.optional(),
|
|
69
|
+
_success: rateStateSchema.optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const queryAggregatedSchema = queryAggregatedDisplaySchema.merge(
|
|
73
|
+
queryAggregatedInternalSchema,
|
|
74
|
+
);
|
|
75
|
+
|
|
61
76
|
export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
|
|
62
77
|
|
|
63
78
|
// ============================================================================
|
|
@@ -68,15 +83,12 @@ export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
|
|
|
68
83
|
* Built-in MySQL query collector.
|
|
69
84
|
* Executes SQL queries and checks results.
|
|
70
85
|
*/
|
|
71
|
-
export class QueryCollector
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
QueryAggregatedResult
|
|
78
|
-
>
|
|
79
|
-
{
|
|
86
|
+
export class QueryCollector implements CollectorStrategy<
|
|
87
|
+
MysqlTransportClient,
|
|
88
|
+
QueryConfig,
|
|
89
|
+
QueryResult,
|
|
90
|
+
QueryAggregatedResult
|
|
91
|
+
> {
|
|
80
92
|
id = "query";
|
|
81
93
|
displayName = "SQL Query";
|
|
82
94
|
description = "Execute a SQL query and check the result";
|
|
@@ -115,28 +127,27 @@ export class QueryCollector
|
|
|
115
127
|
};
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
mergeResult(
|
|
131
|
+
existing: QueryAggregatedResult | undefined,
|
|
132
|
+
run: HealthCheckRunForAggregation<QueryResult>,
|
|
120
133
|
): QueryAggregatedResult {
|
|
121
|
-
const
|
|
122
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
123
|
-
.filter((v): v is number => typeof v === "number");
|
|
134
|
+
const metadata = run.metadata;
|
|
124
135
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
const executionTimeState = mergeAverage(
|
|
137
|
+
existing?._executionTime as AverageState | undefined,
|
|
138
|
+
metadata?.executionTimeMs,
|
|
139
|
+
);
|
|
128
140
|
|
|
129
|
-
const
|
|
141
|
+
const successState = mergeRate(
|
|
142
|
+
existing?._success as RateState | undefined,
|
|
143
|
+
metadata?.success,
|
|
144
|
+
);
|
|
130
145
|
|
|
131
146
|
return {
|
|
132
|
-
avgExecutionTimeMs:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
successRate:
|
|
137
|
-
successes.length > 0
|
|
138
|
-
? Math.round((successCount / successes.length) * 100)
|
|
139
|
-
: 0,
|
|
147
|
+
avgExecutionTimeMs: executionTimeState.avg,
|
|
148
|
+
successRate: successState.rate,
|
|
149
|
+
_executionTime: executionTimeState,
|
|
150
|
+
_success: successState,
|
|
140
151
|
};
|
|
141
152
|
}
|
|
142
153
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -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("
|
|
131
|
+
describe("mergeResult", () => {
|
|
132
132
|
it("should calculate averages correctly", () => {
|
|
133
133
|
const strategy = new MysqlHealthCheckStrategy();
|
|
134
134
|
const runs = [
|
|
@@ -158,7 +158,8 @@ describe("MysqlHealthCheckStrategy", () => {
|
|
|
158
158
|
},
|
|
159
159
|
];
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
162
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
162
163
|
|
|
163
164
|
expect(aggregated.avgConnectionTime).toBe(75);
|
|
164
165
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -167,22 +168,20 @@ describe("MysqlHealthCheckStrategy", () => {
|
|
|
167
168
|
|
|
168
169
|
it("should count errors", () => {
|
|
169
170
|
const strategy = new MysqlHealthCheckStrategy();
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.
|
|
184
|
+
const aggregated = strategy.mergeResult(undefined, run);
|
|
186
185
|
|
|
187
186
|
expect(aggregated.errorCount).toBe(1);
|
|
188
187
|
expect(aggregated.successRate).toBe(0);
|
package/src/strategy.ts
CHANGED
|
@@ -7,6 +7,18 @@ import {
|
|
|
7
7
|
configString,
|
|
8
8
|
configNumber,
|
|
9
9
|
type ConnectedClient,
|
|
10
|
+
mergeAverage,
|
|
11
|
+
averageStateSchema,
|
|
12
|
+
mergeRate,
|
|
13
|
+
rateStateSchema,
|
|
14
|
+
mergeCounter,
|
|
15
|
+
counterStateSchema,
|
|
16
|
+
mergeMinMax,
|
|
17
|
+
minMaxStateSchema,
|
|
18
|
+
type AverageState,
|
|
19
|
+
type RateState,
|
|
20
|
+
type CounterState,
|
|
21
|
+
type MinMaxState,
|
|
10
22
|
} from "@checkstack/backend-api";
|
|
11
23
|
import {
|
|
12
24
|
healthResultBoolean,
|
|
@@ -71,7 +83,8 @@ type MysqlResult = z.infer<typeof mysqlResultSchema>;
|
|
|
71
83
|
/**
|
|
72
84
|
* Aggregated metadata for buckets.
|
|
73
85
|
*/
|
|
74
|
-
|
|
86
|
+
// UI-visible aggregated fields (for charts)
|
|
87
|
+
const mysqlAggregatedDisplaySchema = healthResultSchema({
|
|
75
88
|
avgConnectionTime: healthResultNumber({
|
|
76
89
|
"x-chart-type": "line",
|
|
77
90
|
"x-chart-label": "Avg Connection Time",
|
|
@@ -93,6 +106,18 @@ const mysqlAggregatedSchema = healthResultSchema({
|
|
|
93
106
|
}),
|
|
94
107
|
});
|
|
95
108
|
|
|
109
|
+
// Internal state for incremental aggregation
|
|
110
|
+
const mysqlAggregatedInternalSchema = z.object({
|
|
111
|
+
_connectionTime: averageStateSchema.optional(),
|
|
112
|
+
_maxConnectionTime: minMaxStateSchema.optional(),
|
|
113
|
+
_success: rateStateSchema.optional(),
|
|
114
|
+
_errors: counterStateSchema.optional(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const mysqlAggregatedSchema = mysqlAggregatedDisplaySchema.merge(
|
|
118
|
+
mysqlAggregatedInternalSchema,
|
|
119
|
+
);
|
|
120
|
+
|
|
96
121
|
type MysqlAggregatedResult = z.infer<typeof mysqlAggregatedSchema>;
|
|
97
122
|
|
|
98
123
|
// ============================================================================
|
|
@@ -147,15 +172,12 @@ const defaultDbClient: DbClient = {
|
|
|
147
172
|
// STRATEGY
|
|
148
173
|
// ============================================================================
|
|
149
174
|
|
|
150
|
-
export class MysqlHealthCheckStrategy
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
MysqlAggregatedResult
|
|
157
|
-
>
|
|
158
|
-
{
|
|
175
|
+
export class MysqlHealthCheckStrategy implements HealthCheckStrategy<
|
|
176
|
+
MysqlConfig,
|
|
177
|
+
MysqlTransportClient,
|
|
178
|
+
MysqlResult,
|
|
179
|
+
MysqlAggregatedResult
|
|
180
|
+
> {
|
|
159
181
|
id = "mysql";
|
|
160
182
|
displayName = "MySQL Health Check";
|
|
161
183
|
description = "MySQL database connectivity and query health check";
|
|
@@ -197,53 +219,50 @@ export class MysqlHealthCheckStrategy
|
|
|
197
219
|
schema: mysqlAggregatedSchema,
|
|
198
220
|
});
|
|
199
221
|
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
mergeResult(
|
|
223
|
+
existing: MysqlAggregatedResult | undefined,
|
|
224
|
+
run: HealthCheckRunForAggregation<MysqlResult>,
|
|
202
225
|
): MysqlAggregatedResult {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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;
|
|
226
|
+
const metadata = run.metadata;
|
|
227
|
+
|
|
228
|
+
// Merge connection time average
|
|
229
|
+
const connectionTimeState = mergeAverage(
|
|
230
|
+
existing?._connectionTime as AverageState | undefined,
|
|
231
|
+
metadata?.connectionTimeMs,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Merge max connection time
|
|
235
|
+
const maxConnectionTimeState = mergeMinMax(
|
|
236
|
+
existing?._maxConnectionTime as MinMaxState | undefined,
|
|
237
|
+
metadata?.connectionTimeMs,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Merge success rate
|
|
241
|
+
const successState = mergeRate(
|
|
242
|
+
existing?._success as RateState | undefined,
|
|
243
|
+
metadata?.connected,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Merge error count
|
|
247
|
+
const errorState = mergeCounter(
|
|
248
|
+
existing?._errors as CounterState | undefined,
|
|
249
|
+
metadata?.error !== undefined,
|
|
250
|
+
);
|
|
236
251
|
|
|
237
252
|
return {
|
|
238
|
-
avgConnectionTime,
|
|
239
|
-
maxConnectionTime,
|
|
240
|
-
successRate,
|
|
241
|
-
errorCount,
|
|
253
|
+
avgConnectionTime: connectionTimeState.avg,
|
|
254
|
+
maxConnectionTime: maxConnectionTimeState.max,
|
|
255
|
+
successRate: successState.rate,
|
|
256
|
+
errorCount: errorState.count,
|
|
257
|
+
_connectionTime: connectionTimeState,
|
|
258
|
+
_maxConnectionTime: maxConnectionTimeState,
|
|
259
|
+
_success: successState,
|
|
260
|
+
_errors: errorState,
|
|
242
261
|
};
|
|
243
262
|
}
|
|
244
263
|
|
|
245
264
|
async createClient(
|
|
246
|
-
config: MysqlConfigInput
|
|
265
|
+
config: MysqlConfigInput,
|
|
247
266
|
): Promise<ConnectedClient<MysqlTransportClient>> {
|
|
248
267
|
const validatedConfig = this.config.validate(config);
|
|
249
268
|
|