@checkstack/healthcheck-redis-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/command-collector.test.ts +5 -3
- package/src/command-collector.ts +40 -27
- package/src/strategy.test.ts +25 -26
- package/src/strategy.ts +67 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @checkstack/healthcheck-redis-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-redis-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
|
"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.
|
|
21
|
-
"@checkstack/scripts": "0.1.
|
|
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("
|
|
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,7 +101,8 @@ describe("CommandCollector", () => {
|
|
|
101
101
|
},
|
|
102
102
|
];
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
105
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
105
106
|
|
|
106
107
|
expect(aggregated.avgResponseTimeMs).toBe(10);
|
|
107
108
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -128,7 +129,8 @@ describe("CommandCollector", () => {
|
|
|
128
129
|
},
|
|
129
130
|
];
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
133
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
132
134
|
|
|
133
135
|
expect(aggregated.successRate).toBe(50);
|
|
134
136
|
});
|
package/src/command-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,
|
|
@@ -53,7 +59,7 @@ const commandResultSchema = healthResultSchema({
|
|
|
53
59
|
|
|
54
60
|
export type CommandResult = z.infer<typeof commandResultSchema>;
|
|
55
61
|
|
|
56
|
-
const
|
|
62
|
+
const commandAggregatedDisplaySchema = healthResultSchema({
|
|
57
63
|
avgResponseTimeMs: healthResultNumber({
|
|
58
64
|
"x-chart-type": "line",
|
|
59
65
|
"x-chart-label": "Avg Response Time",
|
|
@@ -66,6 +72,17 @@ const commandAggregatedSchema = healthResultSchema({
|
|
|
66
72
|
}),
|
|
67
73
|
});
|
|
68
74
|
|
|
75
|
+
const commandAggregatedInternalSchema = z.object({
|
|
76
|
+
_responseTime: averageStateSchema
|
|
77
|
+
.optional(),
|
|
78
|
+
_success: rateStateSchema
|
|
79
|
+
.optional(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const commandAggregatedSchema = commandAggregatedDisplaySchema.merge(
|
|
83
|
+
commandAggregatedInternalSchema,
|
|
84
|
+
);
|
|
85
|
+
|
|
69
86
|
export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
70
87
|
|
|
71
88
|
// ============================================================================
|
|
@@ -76,15 +93,12 @@ export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
|
76
93
|
* Built-in Redis command collector.
|
|
77
94
|
* Executes Redis commands and checks results.
|
|
78
95
|
*/
|
|
79
|
-
export class CommandCollector
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
CommandAggregatedResult
|
|
86
|
-
>
|
|
87
|
-
{
|
|
96
|
+
export class CommandCollector implements CollectorStrategy<
|
|
97
|
+
RedisTransportClient,
|
|
98
|
+
CommandConfig,
|
|
99
|
+
CommandResult,
|
|
100
|
+
CommandAggregatedResult
|
|
101
|
+
> {
|
|
88
102
|
id = "command";
|
|
89
103
|
displayName = "Redis Command";
|
|
90
104
|
description = "Execute a Redis command and check the result";
|
|
@@ -127,28 +141,27 @@ export class CommandCollector
|
|
|
127
141
|
};
|
|
128
142
|
}
|
|
129
143
|
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
mergeResult(
|
|
145
|
+
existing: CommandAggregatedResult | undefined,
|
|
146
|
+
run: HealthCheckRunForAggregation<CommandResult>,
|
|
132
147
|
): CommandAggregatedResult {
|
|
133
|
-
const
|
|
134
|
-
.map((r) => r.metadata?.responseTimeMs)
|
|
135
|
-
.filter((v): v is number => typeof v === "number");
|
|
148
|
+
const metadata = run.metadata;
|
|
136
149
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
const responseTimeState = mergeAverage(
|
|
151
|
+
existing?._responseTime as AverageState | undefined,
|
|
152
|
+
metadata?.responseTimeMs,
|
|
153
|
+
);
|
|
140
154
|
|
|
141
|
-
const
|
|
155
|
+
const successState = mergeRate(
|
|
156
|
+
existing?._success as RateState | undefined,
|
|
157
|
+
metadata?.success,
|
|
158
|
+
);
|
|
142
159
|
|
|
143
160
|
return {
|
|
144
|
-
avgResponseTimeMs:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
successRate:
|
|
149
|
-
successes.length > 0
|
|
150
|
-
? Math.round((successCount / successes.length) * 100)
|
|
151
|
-
: 0,
|
|
161
|
+
avgResponseTimeMs: responseTimeState.avg,
|
|
162
|
+
successRate: successState.rate,
|
|
163
|
+
_responseTime: responseTimeState,
|
|
164
|
+
_success: successState,
|
|
152
165
|
};
|
|
153
166
|
}
|
|
154
167
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -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("
|
|
131
|
+
describe("mergeResult", () => {
|
|
132
132
|
it("should calculate averages correctly", () => {
|
|
133
133
|
const strategy = new RedisHealthCheckStrategy();
|
|
134
134
|
const runs = [
|
|
@@ -160,7 +160,8 @@ describe("RedisHealthCheckStrategy", () => {
|
|
|
160
160
|
},
|
|
161
161
|
];
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
164
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
164
165
|
|
|
165
166
|
expect(aggregated.avgConnectionTime).toBe(10);
|
|
166
167
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -169,23 +170,21 @@ describe("RedisHealthCheckStrategy", () => {
|
|
|
169
170
|
|
|
170
171
|
it("should count errors", () => {
|
|
171
172
|
const strategy = new RedisHealthCheckStrategy();
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
187
|
+
const aggregated = strategy.mergeResult(undefined, run);
|
|
189
188
|
|
|
190
189
|
expect(aggregated.errorCount).toBe(1);
|
|
191
190
|
expect(aggregated.successRate).toBe(0);
|
package/src/strategy.ts
CHANGED
|
@@ -8,6 +8,18 @@ import {
|
|
|
8
8
|
configNumber,
|
|
9
9
|
configBoolean,
|
|
10
10
|
type ConnectedClient,
|
|
11
|
+
mergeAverage,
|
|
12
|
+
averageStateSchema,
|
|
13
|
+
mergeRate,
|
|
14
|
+
rateStateSchema,
|
|
15
|
+
mergeCounter,
|
|
16
|
+
counterStateSchema,
|
|
17
|
+
mergeMinMax,
|
|
18
|
+
minMaxStateSchema,
|
|
19
|
+
type AverageState,
|
|
20
|
+
type RateState,
|
|
21
|
+
type CounterState,
|
|
22
|
+
type MinMaxState,
|
|
11
23
|
} from "@checkstack/backend-api";
|
|
12
24
|
import {
|
|
13
25
|
healthResultBoolean,
|
|
@@ -78,7 +90,8 @@ type RedisResult = z.infer<typeof redisResultSchema>;
|
|
|
78
90
|
/**
|
|
79
91
|
* Aggregated metadata for buckets.
|
|
80
92
|
*/
|
|
81
|
-
|
|
93
|
+
// UI-visible aggregated fields
|
|
94
|
+
const redisAggregatedDisplaySchema = healthResultSchema({
|
|
82
95
|
avgConnectionTime: healthResultNumber({
|
|
83
96
|
"x-chart-type": "line",
|
|
84
97
|
"x-chart-label": "Avg Connection Time",
|
|
@@ -100,6 +113,20 @@ const redisAggregatedSchema = healthResultSchema({
|
|
|
100
113
|
}),
|
|
101
114
|
});
|
|
102
115
|
|
|
116
|
+
// Internal state for incremental aggregation
|
|
117
|
+
const redisAggregatedInternalSchema = z.object({
|
|
118
|
+
_connectionTime: averageStateSchema
|
|
119
|
+
.optional(),
|
|
120
|
+
_maxConnectionTime: minMaxStateSchema.optional(),
|
|
121
|
+
_success: rateStateSchema
|
|
122
|
+
.optional(),
|
|
123
|
+
_errors: counterStateSchema.optional(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const redisAggregatedSchema = redisAggregatedDisplaySchema.merge(
|
|
127
|
+
redisAggregatedInternalSchema,
|
|
128
|
+
);
|
|
129
|
+
|
|
103
130
|
type RedisAggregatedResult = z.infer<typeof redisAggregatedSchema>;
|
|
104
131
|
|
|
105
132
|
// ============================================================================
|
|
@@ -160,15 +187,12 @@ const defaultRedisClient: RedisClient = {
|
|
|
160
187
|
// STRATEGY
|
|
161
188
|
// ============================================================================
|
|
162
189
|
|
|
163
|
-
export class RedisHealthCheckStrategy
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
RedisAggregatedResult
|
|
170
|
-
>
|
|
171
|
-
{
|
|
190
|
+
export class RedisHealthCheckStrategy implements HealthCheckStrategy<
|
|
191
|
+
RedisConfig,
|
|
192
|
+
RedisTransportClient,
|
|
193
|
+
RedisResult,
|
|
194
|
+
RedisAggregatedResult
|
|
195
|
+
> {
|
|
172
196
|
id = "redis";
|
|
173
197
|
displayName = "Redis Health Check";
|
|
174
198
|
description = "Redis server connectivity and health monitoring";
|
|
@@ -210,53 +234,46 @@ export class RedisHealthCheckStrategy
|
|
|
210
234
|
schema: redisAggregatedSchema,
|
|
211
235
|
});
|
|
212
236
|
|
|
213
|
-
|
|
214
|
-
|
|
237
|
+
mergeResult(
|
|
238
|
+
existing: RedisAggregatedResult | undefined,
|
|
239
|
+
run: HealthCheckRunForAggregation<RedisResult>,
|
|
215
240
|
): RedisAggregatedResult {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const maxConnectionTime =
|
|
239
|
-
connectionTimes.length > 0 ? Math.max(...connectionTimes) : 0;
|
|
240
|
-
|
|
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;
|
|
241
|
+
const metadata = run.metadata;
|
|
242
|
+
|
|
243
|
+
const connectionTimeState = mergeAverage(
|
|
244
|
+
existing?._connectionTime as AverageState | undefined,
|
|
245
|
+
metadata?.connectionTimeMs,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const maxConnectionTimeState = mergeMinMax(
|
|
249
|
+
existing?._maxConnectionTime as MinMaxState | undefined,
|
|
250
|
+
metadata?.connectionTimeMs,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const successState = mergeRate(
|
|
254
|
+
existing?._success as RateState | undefined,
|
|
255
|
+
metadata?.connected,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const errorState = mergeCounter(
|
|
259
|
+
existing?._errors as CounterState | undefined,
|
|
260
|
+
metadata?.error !== undefined,
|
|
261
|
+
);
|
|
249
262
|
|
|
250
263
|
return {
|
|
251
|
-
avgConnectionTime,
|
|
252
|
-
maxConnectionTime,
|
|
253
|
-
successRate,
|
|
254
|
-
errorCount,
|
|
264
|
+
avgConnectionTime: connectionTimeState.avg,
|
|
265
|
+
maxConnectionTime: maxConnectionTimeState.max,
|
|
266
|
+
successRate: successState.rate,
|
|
267
|
+
errorCount: errorState.count,
|
|
268
|
+
_connectionTime: connectionTimeState,
|
|
269
|
+
_maxConnectionTime: maxConnectionTimeState,
|
|
270
|
+
_success: successState,
|
|
271
|
+
_errors: errorState,
|
|
255
272
|
};
|
|
256
273
|
}
|
|
257
274
|
|
|
258
275
|
async createClient(
|
|
259
|
-
config: RedisConfigInput
|
|
276
|
+
config: RedisConfigInput,
|
|
260
277
|
): Promise<ConnectedClient<RedisTransportClient>> {
|
|
261
278
|
const validatedConfig = this.config.validate(config);
|
|
262
279
|
|