@checkstack/healthcheck-redis-backend 0.0.2 → 0.1.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 +43 -0
- package/package.json +1 -1
- package/src/command-collector.test.ts +147 -0
- package/src/command-collector.ts +153 -0
- package/src/index.ts +7 -5
- package/src/strategy.test.ts +68 -81
- package/src/strategy.ts +154 -202
- package/src/transport-client.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @checkstack/healthcheck-redis-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f5b1f49: Refactored health check strategies to use `createClient()` pattern with built-in collectors.
|
|
8
|
+
|
|
9
|
+
**Strategy Changes:**
|
|
10
|
+
|
|
11
|
+
- Replaced `execute()` with `createClient()` that returns a transport client
|
|
12
|
+
- Strategy configs now only contain connection parameters
|
|
13
|
+
- Collector configs handle what to do with the connection
|
|
14
|
+
|
|
15
|
+
**Built-in Collectors Added:**
|
|
16
|
+
|
|
17
|
+
- DNS: `LookupCollector` for hostname resolution
|
|
18
|
+
- gRPC: `HealthCollector` for gRPC health protocol
|
|
19
|
+
- HTTP: `RequestCollector` for HTTP requests
|
|
20
|
+
- MySQL: `QueryCollector` for database queries
|
|
21
|
+
- Ping: `PingCollector` for ICMP ping
|
|
22
|
+
- Postgres: `QueryCollector` for database queries
|
|
23
|
+
- Redis: `CommandCollector` for Redis commands
|
|
24
|
+
- Script: `ExecuteCollector` for script execution
|
|
25
|
+
- SSH: `CommandCollector` for SSH commands
|
|
26
|
+
- TCP: `BannerCollector` for TCP banner grabbing
|
|
27
|
+
- TLS: `CertificateCollector` for certificate inspection
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [f5b1f49]
|
|
32
|
+
- Updated dependencies [f5b1f49]
|
|
33
|
+
- Updated dependencies [f5b1f49]
|
|
34
|
+
- Updated dependencies [f5b1f49]
|
|
35
|
+
- @checkstack/backend-api@0.1.0
|
|
36
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
37
|
+
- @checkstack/common@0.0.3
|
|
38
|
+
|
|
39
|
+
## 0.0.3
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Updated dependencies [cb82e4d]
|
|
44
|
+
- @checkstack/healthcheck-common@0.0.3
|
|
45
|
+
|
|
3
46
|
## 0.0.2
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { CommandCollector, type CommandConfig } from "./command-collector";
|
|
3
|
+
import type { RedisTransportClient } from "./transport-client";
|
|
4
|
+
|
|
5
|
+
describe("CommandCollector", () => {
|
|
6
|
+
const createMockClient = (
|
|
7
|
+
response: {
|
|
8
|
+
value?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
} = {}
|
|
11
|
+
): RedisTransportClient => ({
|
|
12
|
+
exec: mock(() =>
|
|
13
|
+
Promise.resolve({
|
|
14
|
+
value: response.value ?? "PONG",
|
|
15
|
+
error: response.error,
|
|
16
|
+
})
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("execute", () => {
|
|
21
|
+
it("should execute PING successfully", async () => {
|
|
22
|
+
const collector = new CommandCollector();
|
|
23
|
+
const client = createMockClient({ value: "PONG" });
|
|
24
|
+
|
|
25
|
+
const result = await collector.execute({
|
|
26
|
+
config: { command: "PING" },
|
|
27
|
+
client,
|
|
28
|
+
pluginId: "test",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result.result.response).toBe("PONG");
|
|
32
|
+
expect(result.result.success).toBe(true);
|
|
33
|
+
expect(result.result.responseTimeMs).toBeGreaterThanOrEqual(0);
|
|
34
|
+
expect(result.error).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should execute INFO with section", async () => {
|
|
38
|
+
const collector = new CommandCollector();
|
|
39
|
+
const client = createMockClient({ value: "redis_version:7.0.0" });
|
|
40
|
+
|
|
41
|
+
const result = await collector.execute({
|
|
42
|
+
config: { command: "INFO", args: "server" },
|
|
43
|
+
client,
|
|
44
|
+
pluginId: "test",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.result.response).toContain("redis_version");
|
|
48
|
+
expect(result.result.success).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return error for failed command", async () => {
|
|
52
|
+
const collector = new CommandCollector();
|
|
53
|
+
const client = createMockClient({ error: "NOAUTH" });
|
|
54
|
+
|
|
55
|
+
const result = await collector.execute({
|
|
56
|
+
config: { command: "PING" },
|
|
57
|
+
client,
|
|
58
|
+
pluginId: "test",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.result.success).toBe(false);
|
|
62
|
+
expect(result.error).toBe("NOAUTH");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should pass correct parameters to client", async () => {
|
|
66
|
+
const collector = new CommandCollector();
|
|
67
|
+
const client = createMockClient();
|
|
68
|
+
|
|
69
|
+
await collector.execute({
|
|
70
|
+
config: { command: "GET", args: "mykey" },
|
|
71
|
+
client,
|
|
72
|
+
pluginId: "test",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(client.exec).toHaveBeenCalledWith({
|
|
76
|
+
cmd: "GET",
|
|
77
|
+
args: ["mykey"],
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("aggregateResult", () => {
|
|
83
|
+
it("should calculate average response time and success rate", () => {
|
|
84
|
+
const collector = new CommandCollector();
|
|
85
|
+
const runs = [
|
|
86
|
+
{
|
|
87
|
+
id: "1",
|
|
88
|
+
status: "healthy" as const,
|
|
89
|
+
latencyMs: 10,
|
|
90
|
+
checkId: "c1",
|
|
91
|
+
timestamp: new Date(),
|
|
92
|
+
metadata: { responseTimeMs: 5, success: true },
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "2",
|
|
96
|
+
status: "healthy" as const,
|
|
97
|
+
latencyMs: 15,
|
|
98
|
+
checkId: "c1",
|
|
99
|
+
timestamp: new Date(),
|
|
100
|
+
metadata: { responseTimeMs: 15, success: true },
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const aggregated = collector.aggregateResult(runs);
|
|
105
|
+
|
|
106
|
+
expect(aggregated.avgResponseTimeMs).toBe(10);
|
|
107
|
+
expect(aggregated.successRate).toBe(100);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should calculate success rate correctly", () => {
|
|
111
|
+
const collector = new CommandCollector();
|
|
112
|
+
const runs = [
|
|
113
|
+
{
|
|
114
|
+
id: "1",
|
|
115
|
+
status: "healthy" as const,
|
|
116
|
+
latencyMs: 10,
|
|
117
|
+
checkId: "c1",
|
|
118
|
+
timestamp: new Date(),
|
|
119
|
+
metadata: { responseTimeMs: 5, success: true },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "2",
|
|
123
|
+
status: "unhealthy" as const,
|
|
124
|
+
latencyMs: 15,
|
|
125
|
+
checkId: "c1",
|
|
126
|
+
timestamp: new Date(),
|
|
127
|
+
metadata: { responseTimeMs: 15, success: false },
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const aggregated = collector.aggregateResult(runs);
|
|
132
|
+
|
|
133
|
+
expect(aggregated.successRate).toBe(50);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("metadata", () => {
|
|
138
|
+
it("should have correct static properties", () => {
|
|
139
|
+
const collector = new CommandCollector();
|
|
140
|
+
|
|
141
|
+
expect(collector.id).toBe("command");
|
|
142
|
+
expect(collector.displayName).toBe("Redis Command");
|
|
143
|
+
expect(collector.allowMultiple).toBe(true);
|
|
144
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import {
|
|
9
|
+
healthResultNumber,
|
|
10
|
+
healthResultString,
|
|
11
|
+
healthResultBoolean,
|
|
12
|
+
} from "@checkstack/healthcheck-common";
|
|
13
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
14
|
+
import type { RedisTransportClient } from "./transport-client";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CONFIGURATION SCHEMA
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const commandConfigSchema = z.object({
|
|
21
|
+
command: z
|
|
22
|
+
.enum(["PING", "INFO", "GET"])
|
|
23
|
+
.default("PING")
|
|
24
|
+
.describe("Redis command to execute"),
|
|
25
|
+
args: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Command argument (section for INFO, key for GET)"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type CommandConfig = z.infer<typeof commandConfigSchema>;
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// RESULT SCHEMAS
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
const commandResultSchema = z.object({
|
|
38
|
+
response: healthResultString({
|
|
39
|
+
"x-chart-type": "text",
|
|
40
|
+
"x-chart-label": "Response",
|
|
41
|
+
}).optional(),
|
|
42
|
+
responseTimeMs: healthResultNumber({
|
|
43
|
+
"x-chart-type": "line",
|
|
44
|
+
"x-chart-label": "Response Time",
|
|
45
|
+
"x-chart-unit": "ms",
|
|
46
|
+
}),
|
|
47
|
+
success: healthResultBoolean({
|
|
48
|
+
"x-chart-type": "boolean",
|
|
49
|
+
"x-chart-label": "Success",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type CommandResult = z.infer<typeof commandResultSchema>;
|
|
54
|
+
|
|
55
|
+
const commandAggregatedSchema = z.object({
|
|
56
|
+
avgResponseTimeMs: healthResultNumber({
|
|
57
|
+
"x-chart-type": "line",
|
|
58
|
+
"x-chart-label": "Avg Response Time",
|
|
59
|
+
"x-chart-unit": "ms",
|
|
60
|
+
}),
|
|
61
|
+
successRate: healthResultNumber({
|
|
62
|
+
"x-chart-type": "gauge",
|
|
63
|
+
"x-chart-label": "Success Rate",
|
|
64
|
+
"x-chart-unit": "%",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// COMMAND COLLECTOR
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Built-in Redis command collector.
|
|
76
|
+
* Executes Redis commands and checks results.
|
|
77
|
+
*/
|
|
78
|
+
export class CommandCollector
|
|
79
|
+
implements
|
|
80
|
+
CollectorStrategy<
|
|
81
|
+
RedisTransportClient,
|
|
82
|
+
CommandConfig,
|
|
83
|
+
CommandResult,
|
|
84
|
+
CommandAggregatedResult
|
|
85
|
+
>
|
|
86
|
+
{
|
|
87
|
+
id = "command";
|
|
88
|
+
displayName = "Redis Command";
|
|
89
|
+
description = "Execute a Redis command and check the result";
|
|
90
|
+
|
|
91
|
+
supportedPlugins = [pluginMetadata];
|
|
92
|
+
|
|
93
|
+
allowMultiple = true;
|
|
94
|
+
|
|
95
|
+
config = new Versioned({ version: 1, schema: commandConfigSchema });
|
|
96
|
+
result = new Versioned({ version: 1, schema: commandResultSchema });
|
|
97
|
+
aggregatedResult = new Versioned({
|
|
98
|
+
version: 1,
|
|
99
|
+
schema: commandAggregatedSchema,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async execute({
|
|
103
|
+
config,
|
|
104
|
+
client,
|
|
105
|
+
}: {
|
|
106
|
+
config: CommandConfig;
|
|
107
|
+
client: RedisTransportClient;
|
|
108
|
+
pluginId: string;
|
|
109
|
+
}): Promise<CollectorResult<CommandResult>> {
|
|
110
|
+
const startTime = Date.now();
|
|
111
|
+
|
|
112
|
+
const response = await client.exec({
|
|
113
|
+
cmd: config.command,
|
|
114
|
+
args: config.args ? [config.args] : undefined,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const responseTimeMs = Date.now() - startTime;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
result: {
|
|
121
|
+
response: response.value,
|
|
122
|
+
responseTimeMs,
|
|
123
|
+
success: !response.error,
|
|
124
|
+
},
|
|
125
|
+
error: response.error,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
aggregateResult(
|
|
130
|
+
runs: HealthCheckRunForAggregation<CommandResult>[]
|
|
131
|
+
): CommandAggregatedResult {
|
|
132
|
+
const times = runs
|
|
133
|
+
.map((r) => r.metadata?.responseTimeMs)
|
|
134
|
+
.filter((v): v is number => typeof v === "number");
|
|
135
|
+
|
|
136
|
+
const successes = runs
|
|
137
|
+
.map((r) => r.metadata?.success)
|
|
138
|
+
.filter((v): v is boolean => typeof v === "boolean");
|
|
139
|
+
|
|
140
|
+
const successCount = successes.filter(Boolean).length;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
avgResponseTimeMs:
|
|
144
|
+
times.length > 0
|
|
145
|
+
? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
|
|
146
|
+
: 0,
|
|
147
|
+
successRate:
|
|
148
|
+
successes.length > 0
|
|
149
|
+
? Math.round((successCount / successes.length) * 100)
|
|
150
|
+
: 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createBackendPlugin,
|
|
3
|
-
coreServices,
|
|
4
|
-
} from "@checkstack/backend-api";
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
5
2
|
import { RedisHealthCheckStrategy } from "./strategy";
|
|
6
3
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
|
+
import { CommandCollector } from "./command-collector";
|
|
7
5
|
|
|
8
6
|
export default createBackendPlugin({
|
|
9
7
|
metadata: pluginMetadata,
|
|
@@ -11,13 +9,17 @@ export default createBackendPlugin({
|
|
|
11
9
|
env.registerInit({
|
|
12
10
|
deps: {
|
|
13
11
|
healthCheckRegistry: coreServices.healthCheckRegistry,
|
|
12
|
+
collectorRegistry: coreServices.collectorRegistry,
|
|
14
13
|
logger: coreServices.logger,
|
|
15
14
|
},
|
|
16
|
-
init: async ({ healthCheckRegistry, logger }) => {
|
|
15
|
+
init: async ({ healthCheckRegistry, collectorRegistry, logger }) => {
|
|
17
16
|
logger.debug("🔌 Registering Redis Health Check Strategy...");
|
|
18
17
|
const strategy = new RedisHealthCheckStrategy();
|
|
19
18
|
healthCheckRegistry.register(strategy);
|
|
19
|
+
collectorRegistry.register(new CommandCollector());
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
export { pluginMetadata } from "./plugin-metadata";
|
package/src/strategy.test.ts
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
RedisHealthCheckStrategy,
|
|
4
|
+
RedisClient,
|
|
5
|
+
RedisConnection,
|
|
6
|
+
} from "./strategy";
|
|
3
7
|
|
|
4
8
|
describe("RedisHealthCheckStrategy", () => {
|
|
9
|
+
// Helper to create mock Redis connection
|
|
10
|
+
const createMockConnection = (
|
|
11
|
+
config: {
|
|
12
|
+
pingResponse?: string;
|
|
13
|
+
infoResponse?: string;
|
|
14
|
+
pingError?: Error;
|
|
15
|
+
} = {}
|
|
16
|
+
): RedisConnection => ({
|
|
17
|
+
ping: mock(() =>
|
|
18
|
+
config.pingError
|
|
19
|
+
? Promise.reject(config.pingError)
|
|
20
|
+
: Promise.resolve(config.pingResponse ?? "PONG")
|
|
21
|
+
),
|
|
22
|
+
info: mock(() =>
|
|
23
|
+
Promise.resolve(
|
|
24
|
+
config.infoResponse ?? "redis_version:7.0.0\r\nrole:master\r\n"
|
|
25
|
+
)
|
|
26
|
+
),
|
|
27
|
+
get: mock(() => Promise.resolve(undefined)),
|
|
28
|
+
quit: mock(() => Promise.resolve("OK")),
|
|
29
|
+
});
|
|
30
|
+
|
|
5
31
|
// Helper to create mock Redis client
|
|
6
32
|
const createMockClient = (
|
|
7
33
|
config: {
|
|
@@ -14,127 +40,91 @@ describe("RedisHealthCheckStrategy", () => {
|
|
|
14
40
|
connect: mock(() =>
|
|
15
41
|
config.connectError
|
|
16
42
|
? Promise.reject(config.connectError)
|
|
17
|
-
: Promise.resolve(
|
|
18
|
-
ping: mock(() =>
|
|
19
|
-
config.pingError
|
|
20
|
-
? Promise.reject(config.pingError)
|
|
21
|
-
: Promise.resolve(config.pingResponse ?? "PONG")
|
|
22
|
-
),
|
|
23
|
-
info: mock(() =>
|
|
24
|
-
Promise.resolve(
|
|
25
|
-
config.infoResponse ?? "redis_version:7.0.0\r\nrole:master\r\n"
|
|
26
|
-
)
|
|
27
|
-
),
|
|
28
|
-
quit: mock(() => Promise.resolve("OK")),
|
|
29
|
-
})
|
|
43
|
+
: Promise.resolve(createMockConnection(config))
|
|
30
44
|
),
|
|
31
45
|
});
|
|
32
46
|
|
|
33
|
-
describe("
|
|
34
|
-
it("should return
|
|
47
|
+
describe("createClient", () => {
|
|
48
|
+
it("should return a connected client for successful connection", async () => {
|
|
35
49
|
const strategy = new RedisHealthCheckStrategy(createMockClient());
|
|
36
50
|
|
|
37
|
-
const
|
|
51
|
+
const connectedClient = await strategy.createClient({
|
|
38
52
|
host: "localhost",
|
|
39
53
|
port: 6379,
|
|
40
54
|
timeout: 5000,
|
|
41
55
|
});
|
|
42
56
|
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
45
|
-
expect(
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
expect(connectedClient.client).toBeDefined();
|
|
58
|
+
expect(connectedClient.client.exec).toBeDefined();
|
|
59
|
+
expect(connectedClient.close).toBeDefined();
|
|
60
|
+
|
|
61
|
+
connectedClient.close();
|
|
48
62
|
});
|
|
49
63
|
|
|
50
|
-
it("should
|
|
64
|
+
it("should throw for connection error", async () => {
|
|
51
65
|
const strategy = new RedisHealthCheckStrategy(
|
|
52
66
|
createMockClient({ connectError: new Error("Connection refused") })
|
|
53
67
|
);
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
await expect(
|
|
70
|
+
strategy.createClient({
|
|
71
|
+
host: "localhost",
|
|
72
|
+
port: 6379,
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
})
|
|
75
|
+
).rejects.toThrow("Connection refused");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("client.exec", () => {
|
|
80
|
+
it("should execute PING successfully", async () => {
|
|
81
|
+
const strategy = new RedisHealthCheckStrategy(createMockClient());
|
|
82
|
+
const connectedClient = await strategy.createClient({
|
|
56
83
|
host: "localhost",
|
|
57
84
|
port: 6379,
|
|
58
85
|
timeout: 5000,
|
|
59
86
|
});
|
|
60
87
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expect(result.
|
|
88
|
+
const result = await connectedClient.client.exec({ cmd: "PING" });
|
|
89
|
+
|
|
90
|
+
expect(result.value).toBe("PONG");
|
|
91
|
+
|
|
92
|
+
connectedClient.close();
|
|
64
93
|
});
|
|
65
94
|
|
|
66
|
-
it("should return
|
|
95
|
+
it("should return error for ping failure", async () => {
|
|
67
96
|
const strategy = new RedisHealthCheckStrategy(
|
|
68
97
|
createMockClient({ pingError: new Error("NOAUTH") })
|
|
69
98
|
);
|
|
70
|
-
|
|
71
|
-
const result = await strategy.execute({
|
|
99
|
+
const connectedClient = await strategy.createClient({
|
|
72
100
|
host: "localhost",
|
|
73
101
|
port: 6379,
|
|
74
102
|
timeout: 5000,
|
|
75
103
|
});
|
|
76
104
|
|
|
77
|
-
|
|
78
|
-
expect(result.metadata?.pingSuccess).toBe(false);
|
|
79
|
-
});
|
|
105
|
+
const result = await connectedClient.client.exec({ cmd: "PING" });
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
const strategy = new RedisHealthCheckStrategy(createMockClient());
|
|
107
|
+
expect(result.error).toContain("NOAUTH");
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
host: "localhost",
|
|
86
|
-
port: 6379,
|
|
87
|
-
timeout: 5000,
|
|
88
|
-
assertions: [
|
|
89
|
-
{ field: "connectionTime", operator: "lessThan", value: 5000 },
|
|
90
|
-
],
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(result.status).toBe("healthy");
|
|
109
|
+
connectedClient.close();
|
|
94
110
|
});
|
|
95
111
|
|
|
96
|
-
it("should
|
|
112
|
+
it("should return server info", async () => {
|
|
97
113
|
const strategy = new RedisHealthCheckStrategy(createMockClient());
|
|
98
|
-
|
|
99
|
-
const result = await strategy.execute({
|
|
114
|
+
const connectedClient = await strategy.createClient({
|
|
100
115
|
host: "localhost",
|
|
101
116
|
port: 6379,
|
|
102
117
|
timeout: 5000,
|
|
103
|
-
assertions: [{ field: "role", operator: "equals", value: "master" }],
|
|
104
118
|
});
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
it("should fail role assertion when replica", async () => {
|
|
110
|
-
const strategy = new RedisHealthCheckStrategy(
|
|
111
|
-
createMockClient({
|
|
112
|
-
infoResponse: "redis_version:7.0.0\r\nrole:slave\r\n",
|
|
113
|
-
})
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const result = await strategy.execute({
|
|
117
|
-
host: "localhost",
|
|
118
|
-
port: 6379,
|
|
119
|
-
timeout: 5000,
|
|
120
|
-
assertions: [{ field: "role", operator: "equals", value: "master" }],
|
|
120
|
+
const result = await connectedClient.client.exec({
|
|
121
|
+
cmd: "INFO",
|
|
122
|
+
args: ["server"],
|
|
121
123
|
});
|
|
122
124
|
|
|
123
|
-
expect(result.
|
|
124
|
-
expect(result.message).toContain("Assertion failed");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("should pass pingSuccess assertion", async () => {
|
|
128
|
-
const strategy = new RedisHealthCheckStrategy(createMockClient());
|
|
129
|
-
|
|
130
|
-
const result = await strategy.execute({
|
|
131
|
-
host: "localhost",
|
|
132
|
-
port: 6379,
|
|
133
|
-
timeout: 5000,
|
|
134
|
-
assertions: [{ field: "pingSuccess", operator: "isTrue" }],
|
|
135
|
-
});
|
|
125
|
+
expect(result.value).toContain("redis_version");
|
|
136
126
|
|
|
137
|
-
|
|
127
|
+
connectedClient.close();
|
|
138
128
|
});
|
|
139
129
|
});
|
|
140
130
|
|
|
@@ -151,7 +141,6 @@ describe("RedisHealthCheckStrategy", () => {
|
|
|
151
141
|
metadata: {
|
|
152
142
|
connected: true,
|
|
153
143
|
connectionTimeMs: 5,
|
|
154
|
-
pingTimeMs: 1,
|
|
155
144
|
pingSuccess: true,
|
|
156
145
|
role: "master",
|
|
157
146
|
},
|
|
@@ -165,7 +154,6 @@ describe("RedisHealthCheckStrategy", () => {
|
|
|
165
154
|
metadata: {
|
|
166
155
|
connected: true,
|
|
167
156
|
connectionTimeMs: 15,
|
|
168
|
-
pingTimeMs: 3,
|
|
169
157
|
pingSuccess: true,
|
|
170
158
|
role: "master",
|
|
171
159
|
},
|
|
@@ -175,7 +163,6 @@ describe("RedisHealthCheckStrategy", () => {
|
|
|
175
163
|
const aggregated = strategy.aggregateResult(runs);
|
|
176
164
|
|
|
177
165
|
expect(aggregated.avgConnectionTime).toBe(10);
|
|
178
|
-
expect(aggregated.avgPingTime).toBe(2);
|
|
179
166
|
expect(aggregated.successRate).toBe(100);
|
|
180
167
|
expect(aggregated.errorCount).toBe(0);
|
|
181
168
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -1,46 +1,29 @@
|
|
|
1
1
|
import Redis from "ioredis";
|
|
2
2
|
import {
|
|
3
3
|
HealthCheckStrategy,
|
|
4
|
-
HealthCheckResult,
|
|
5
4
|
HealthCheckRunForAggregation,
|
|
6
5
|
Versioned,
|
|
7
6
|
z,
|
|
8
|
-
timeThresholdField,
|
|
9
|
-
booleanField,
|
|
10
|
-
enumField,
|
|
11
7
|
configString,
|
|
12
8
|
configNumber,
|
|
13
9
|
configBoolean,
|
|
14
|
-
|
|
10
|
+
type ConnectedClient,
|
|
15
11
|
} from "@checkstack/backend-api";
|
|
16
12
|
import {
|
|
17
13
|
healthResultBoolean,
|
|
18
14
|
healthResultNumber,
|
|
19
15
|
healthResultString,
|
|
20
16
|
} from "@checkstack/healthcheck-common";
|
|
17
|
+
import type {
|
|
18
|
+
RedisTransportClient,
|
|
19
|
+
RedisCommand,
|
|
20
|
+
RedisCommandResult,
|
|
21
|
+
} from "./transport-client";
|
|
21
22
|
|
|
22
23
|
// ============================================================================
|
|
23
24
|
// SCHEMAS
|
|
24
25
|
// ============================================================================
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Valid Redis server roles from the INFO replication command.
|
|
28
|
-
*/
|
|
29
|
-
const RedisRole = z.enum(["master", "slave", "sentinel"]);
|
|
30
|
-
export type RedisRole = z.infer<typeof RedisRole>;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Assertion schema for Redis health checks using shared factories.
|
|
34
|
-
*/
|
|
35
|
-
const redisAssertionSchema = z.discriminatedUnion("field", [
|
|
36
|
-
timeThresholdField("connectionTime"),
|
|
37
|
-
timeThresholdField("pingTime"),
|
|
38
|
-
booleanField("pingSuccess"),
|
|
39
|
-
enumField("role", RedisRole.options),
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
export type RedisAssertion = z.infer<typeof redisAssertionSchema>;
|
|
43
|
-
|
|
44
27
|
/**
|
|
45
28
|
* Configuration schema for Redis health checks.
|
|
46
29
|
*/
|
|
@@ -53,18 +36,18 @@ export const redisConfigSchema = z.object({
|
|
|
53
36
|
.default(6379)
|
|
54
37
|
.describe("Redis port"),
|
|
55
38
|
password: configString({ "x-secret": true })
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
database: configNumber({})
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Redis password"),
|
|
41
|
+
database: configNumber({})
|
|
42
|
+
.int()
|
|
43
|
+
.min(0)
|
|
44
|
+
.default(0)
|
|
45
|
+
.describe("Redis database number"),
|
|
59
46
|
tls: configBoolean({}).default(false).describe("Use TLS connection"),
|
|
60
47
|
timeout: configNumber({})
|
|
61
48
|
.min(100)
|
|
62
49
|
.default(5000)
|
|
63
50
|
.describe("Connection timeout in milliseconds"),
|
|
64
|
-
assertions: z
|
|
65
|
-
.array(redisAssertionSchema)
|
|
66
|
-
.optional()
|
|
67
|
-
.describe("Validation conditions"),
|
|
68
51
|
});
|
|
69
52
|
|
|
70
53
|
export type RedisConfig = z.infer<typeof redisConfigSchema>;
|
|
@@ -83,31 +66,13 @@ const redisResultSchema = z.object({
|
|
|
83
66
|
"x-chart-label": "Connection Time",
|
|
84
67
|
"x-chart-unit": "ms",
|
|
85
68
|
}),
|
|
86
|
-
pingTimeMs: healthResultNumber({
|
|
87
|
-
"x-chart-type": "line",
|
|
88
|
-
"x-chart-label": "Ping Time",
|
|
89
|
-
"x-chart-unit": "ms",
|
|
90
|
-
}).optional(),
|
|
91
|
-
pingSuccess: healthResultBoolean({
|
|
92
|
-
"x-chart-type": "boolean",
|
|
93
|
-
"x-chart-label": "Ping Success",
|
|
94
|
-
}),
|
|
95
|
-
role: healthResultString({
|
|
96
|
-
"x-chart-type": "text",
|
|
97
|
-
"x-chart-label": "Role",
|
|
98
|
-
}).optional(),
|
|
99
|
-
redisVersion: healthResultString({
|
|
100
|
-
"x-chart-type": "text",
|
|
101
|
-
"x-chart-label": "Redis Version",
|
|
102
|
-
}).optional(),
|
|
103
|
-
failedAssertion: redisAssertionSchema.optional(),
|
|
104
69
|
error: healthResultString({
|
|
105
70
|
"x-chart-type": "status",
|
|
106
71
|
"x-chart-label": "Error",
|
|
107
72
|
}).optional(),
|
|
108
73
|
});
|
|
109
74
|
|
|
110
|
-
|
|
75
|
+
type RedisResult = z.infer<typeof redisResultSchema>;
|
|
111
76
|
|
|
112
77
|
/**
|
|
113
78
|
* Aggregated metadata for buckets.
|
|
@@ -118,9 +83,9 @@ const redisAggregatedSchema = z.object({
|
|
|
118
83
|
"x-chart-label": "Avg Connection Time",
|
|
119
84
|
"x-chart-unit": "ms",
|
|
120
85
|
}),
|
|
121
|
-
|
|
86
|
+
maxConnectionTime: healthResultNumber({
|
|
122
87
|
"x-chart-type": "line",
|
|
123
|
-
"x-chart-label": "
|
|
88
|
+
"x-chart-label": "Max Connection Time",
|
|
124
89
|
"x-chart-unit": "ms",
|
|
125
90
|
}),
|
|
126
91
|
successRate: healthResultNumber({
|
|
@@ -134,7 +99,7 @@ const redisAggregatedSchema = z.object({
|
|
|
134
99
|
}),
|
|
135
100
|
});
|
|
136
101
|
|
|
137
|
-
|
|
102
|
+
type RedisAggregatedResult = z.infer<typeof redisAggregatedSchema>;
|
|
138
103
|
|
|
139
104
|
// ============================================================================
|
|
140
105
|
// REDIS CLIENT INTERFACE (for testability)
|
|
@@ -143,6 +108,7 @@ export type RedisAggregatedResult = z.infer<typeof redisAggregatedSchema>;
|
|
|
143
108
|
export interface RedisConnection {
|
|
144
109
|
ping(): Promise<string>;
|
|
145
110
|
info(section: string): Promise<string>;
|
|
111
|
+
get(key: string): Promise<string | undefined>;
|
|
146
112
|
quit(): Promise<string>;
|
|
147
113
|
}
|
|
148
114
|
|
|
@@ -159,24 +125,33 @@ export interface RedisClient {
|
|
|
159
125
|
|
|
160
126
|
// Default client using ioredis
|
|
161
127
|
const defaultRedisClient: RedisClient = {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
128
|
+
connect(config) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const redis = new Redis({
|
|
131
|
+
host: config.host,
|
|
132
|
+
port: config.port,
|
|
133
|
+
password: config.password,
|
|
134
|
+
db: config.db,
|
|
135
|
+
tls: config.tls ? {} : undefined,
|
|
136
|
+
connectTimeout: config.connectTimeout,
|
|
137
|
+
lazyConnect: true,
|
|
138
|
+
maxRetriesPerRequest: 0,
|
|
139
|
+
});
|
|
174
140
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
141
|
+
redis.on("error", reject);
|
|
142
|
+
|
|
143
|
+
redis
|
|
144
|
+
.connect()
|
|
145
|
+
.then(() => {
|
|
146
|
+
resolve({
|
|
147
|
+
ping: () => redis.ping(),
|
|
148
|
+
info: (section: string) => redis.info(section),
|
|
149
|
+
get: (key: string) => redis.get(key).then((v) => v ?? undefined),
|
|
150
|
+
quit: () => redis.quit(),
|
|
151
|
+
});
|
|
152
|
+
})
|
|
153
|
+
.catch(reject);
|
|
154
|
+
});
|
|
180
155
|
},
|
|
181
156
|
};
|
|
182
157
|
|
|
@@ -186,7 +161,12 @@ const defaultRedisClient: RedisClient = {
|
|
|
186
161
|
|
|
187
162
|
export class RedisHealthCheckStrategy
|
|
188
163
|
implements
|
|
189
|
-
HealthCheckStrategy<
|
|
164
|
+
HealthCheckStrategy<
|
|
165
|
+
RedisConfig,
|
|
166
|
+
RedisTransportClient,
|
|
167
|
+
RedisResult,
|
|
168
|
+
RedisAggregatedResult
|
|
169
|
+
>
|
|
190
170
|
{
|
|
191
171
|
id = "redis";
|
|
192
172
|
displayName = "Redis Health Check";
|
|
@@ -199,13 +179,29 @@ export class RedisHealthCheckStrategy
|
|
|
199
179
|
}
|
|
200
180
|
|
|
201
181
|
config: Versioned<RedisConfig> = new Versioned({
|
|
202
|
-
version:
|
|
182
|
+
version: 2, // Bumped for createClient pattern
|
|
203
183
|
schema: redisConfigSchema,
|
|
184
|
+
migrations: [
|
|
185
|
+
{
|
|
186
|
+
fromVersion: 1,
|
|
187
|
+
toVersion: 2,
|
|
188
|
+
description: "Migrate to createClient pattern (no config changes)",
|
|
189
|
+
migrate: (data: unknown) => data,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
204
192
|
});
|
|
205
193
|
|
|
206
194
|
result: Versioned<RedisResult> = new Versioned({
|
|
207
|
-
version:
|
|
195
|
+
version: 2,
|
|
208
196
|
schema: redisResultSchema,
|
|
197
|
+
migrations: [
|
|
198
|
+
{
|
|
199
|
+
fromVersion: 1,
|
|
200
|
+
toVersion: 2,
|
|
201
|
+
description: "Migrate to createClient pattern (no result changes)",
|
|
202
|
+
migrate: (data: unknown) => data,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
209
205
|
});
|
|
210
206
|
|
|
211
207
|
aggregatedResult: Versioned<RedisAggregatedResult> = new Versioned({
|
|
@@ -216,147 +212,103 @@ export class RedisHealthCheckStrategy
|
|
|
216
212
|
aggregateResult(
|
|
217
213
|
runs: HealthCheckRunForAggregation<RedisResult>[]
|
|
218
214
|
): RedisAggregatedResult {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
errorCount++;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (run.status === "healthy") {
|
|
232
|
-
successCount++;
|
|
233
|
-
}
|
|
234
|
-
if (run.metadata) {
|
|
235
|
-
totalConnectionTime += run.metadata.connectionTimeMs;
|
|
236
|
-
if (run.metadata.pingTimeMs !== undefined) {
|
|
237
|
-
totalPingTime += run.metadata.pingTimeMs;
|
|
238
|
-
pingRuns++;
|
|
239
|
-
}
|
|
240
|
-
validRuns++;
|
|
241
|
-
}
|
|
215
|
+
const validRuns = runs.filter((r) => r.metadata);
|
|
216
|
+
|
|
217
|
+
if (validRuns.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
avgConnectionTime: 0,
|
|
220
|
+
maxConnectionTime: 0,
|
|
221
|
+
successRate: 0,
|
|
222
|
+
errorCount: 0,
|
|
223
|
+
};
|
|
242
224
|
}
|
|
243
225
|
|
|
226
|
+
const connectionTimes = validRuns
|
|
227
|
+
.map((r) => r.metadata?.connectionTimeMs)
|
|
228
|
+
.filter((t): t is number => typeof t === "number");
|
|
229
|
+
|
|
230
|
+
const avgConnectionTime =
|
|
231
|
+
connectionTimes.length > 0
|
|
232
|
+
? Math.round(
|
|
233
|
+
connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
|
|
234
|
+
)
|
|
235
|
+
: 0;
|
|
236
|
+
|
|
237
|
+
const maxConnectionTime =
|
|
238
|
+
connectionTimes.length > 0 ? Math.max(...connectionTimes) : 0;
|
|
239
|
+
|
|
240
|
+
const successCount = validRuns.filter(
|
|
241
|
+
(r) => r.metadata?.connected === true
|
|
242
|
+
).length;
|
|
243
|
+
const successRate = Math.round((successCount / validRuns.length) * 100);
|
|
244
|
+
|
|
245
|
+
const errorCount = validRuns.filter(
|
|
246
|
+
(r) => r.metadata?.error !== undefined
|
|
247
|
+
).length;
|
|
248
|
+
|
|
244
249
|
return {
|
|
245
|
-
avgConnectionTime
|
|
246
|
-
|
|
247
|
-
successRate
|
|
250
|
+
avgConnectionTime,
|
|
251
|
+
maxConnectionTime,
|
|
252
|
+
successRate,
|
|
248
253
|
errorCount,
|
|
249
254
|
};
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
async
|
|
257
|
+
async createClient(
|
|
253
258
|
config: RedisConfigInput
|
|
254
|
-
): Promise<
|
|
259
|
+
): Promise<ConnectedClient<RedisTransportClient>> {
|
|
255
260
|
const validatedConfig = this.config.validate(config);
|
|
256
|
-
const start = performance.now();
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
// Connect to Redis
|
|
260
|
-
const connection = await this.redisClient.connect({
|
|
261
|
-
host: validatedConfig.host,
|
|
262
|
-
port: validatedConfig.port,
|
|
263
|
-
password: validatedConfig.password,
|
|
264
|
-
db: validatedConfig.database,
|
|
265
|
-
tls: validatedConfig.tls,
|
|
266
|
-
connectTimeout: validatedConfig.timeout,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const connectionTimeMs = Math.round(performance.now() - start);
|
|
270
|
-
|
|
271
|
-
// Execute PING command
|
|
272
|
-
const pingStart = performance.now();
|
|
273
|
-
let pingSuccess = false;
|
|
274
|
-
let pingTimeMs: number | undefined;
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const pong = await connection.ping();
|
|
278
|
-
pingSuccess = pong === "PONG";
|
|
279
|
-
pingTimeMs = Math.round(performance.now() - pingStart);
|
|
280
|
-
} catch {
|
|
281
|
-
pingSuccess = false;
|
|
282
|
-
pingTimeMs = Math.round(performance.now() - pingStart);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Get server info
|
|
286
|
-
let role: string | undefined;
|
|
287
|
-
let redisVersion: string | undefined;
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const info = await connection.info("server");
|
|
291
|
-
const roleMatch = /role:(\w+)/i.exec(info);
|
|
292
|
-
const versionMatch = /redis_version:([^\r\n]+)/i.exec(info);
|
|
293
|
-
role = roleMatch?.[1];
|
|
294
|
-
redisVersion = versionMatch?.[1];
|
|
295
|
-
} catch {
|
|
296
|
-
// Info command failed, continue without it
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
await connection.quit();
|
|
300
|
-
|
|
301
|
-
const result: Omit<RedisResult, "failedAssertion" | "error"> = {
|
|
302
|
-
connected: true,
|
|
303
|
-
connectionTimeMs,
|
|
304
|
-
pingTimeMs,
|
|
305
|
-
pingSuccess,
|
|
306
|
-
role,
|
|
307
|
-
redisVersion,
|
|
308
|
-
};
|
|
309
261
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
262
|
+
const connection = await this.redisClient.connect({
|
|
263
|
+
host: validatedConfig.host,
|
|
264
|
+
port: validatedConfig.port,
|
|
265
|
+
password: validatedConfig.password,
|
|
266
|
+
db: validatedConfig.database,
|
|
267
|
+
tls: validatedConfig.tls,
|
|
268
|
+
connectTimeout: validatedConfig.timeout,
|
|
269
|
+
});
|
|
317
270
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
271
|
+
const client: RedisTransportClient = {
|
|
272
|
+
async exec(command: RedisCommand): Promise<RedisCommandResult> {
|
|
273
|
+
try {
|
|
274
|
+
let value: string | undefined;
|
|
275
|
+
switch (command.cmd) {
|
|
276
|
+
case "PING": {
|
|
277
|
+
value = await connection.ping();
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "INFO": {
|
|
281
|
+
value = await connection.info(command.args?.[0] ?? "server");
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "GET": {
|
|
285
|
+
value = await connection.get(command.args?.[0] ?? "");
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
default: {
|
|
289
|
+
return {
|
|
290
|
+
value: undefined,
|
|
291
|
+
error: `Unsupported command: ${command.cmd}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { value };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return {
|
|
298
|
+
value: undefined,
|
|
299
|
+
error: error instanceof Error ? error.message : String(error),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
337
304
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
} catch (error: unknown) {
|
|
347
|
-
const end = performance.now();
|
|
348
|
-
const isError = error instanceof Error;
|
|
349
|
-
return {
|
|
350
|
-
status: "unhealthy",
|
|
351
|
-
latencyMs: Math.round(end - start),
|
|
352
|
-
message: isError ? error.message : "Redis connection failed",
|
|
353
|
-
metadata: {
|
|
354
|
-
connected: false,
|
|
355
|
-
connectionTimeMs: Math.round(end - start),
|
|
356
|
-
pingSuccess: false,
|
|
357
|
-
error: isError ? error.name : "UnknownError",
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
}
|
|
305
|
+
return {
|
|
306
|
+
client,
|
|
307
|
+
close: () => {
|
|
308
|
+
connection.quit().catch(() => {
|
|
309
|
+
// Ignore quit errors
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
};
|
|
361
313
|
}
|
|
362
314
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TransportClient } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Redis command for transport client.
|
|
5
|
+
*/
|
|
6
|
+
export interface RedisCommand {
|
|
7
|
+
cmd: "PING" | "INFO" | "GET" | "SET" | "KEYS" | "CUSTOM";
|
|
8
|
+
args?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Redis command result.
|
|
13
|
+
*/
|
|
14
|
+
export interface RedisCommandResult {
|
|
15
|
+
value: string | undefined;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Redis transport client for collector execution.
|
|
21
|
+
*/
|
|
22
|
+
export type RedisTransportClient = TransportClient<
|
|
23
|
+
RedisCommand,
|
|
24
|
+
RedisCommandResult
|
|
25
|
+
>;
|