@checkstack/healthcheck-script-backend 0.0.3 → 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 +36 -0
- package/package.json +1 -1
- package/src/execute-collector.test.ts +196 -0
- package/src/execute-collector.ts +178 -0
- package/src/index.ts +7 -5
- package/src/strategy.test.ts +51 -83
- package/src/strategy.ts +127 -171
- package/src/transport-client.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @checkstack/healthcheck-script-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
|
+
|
|
3
39
|
## 0.0.3
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { ExecuteCollector, type ExecuteConfig } from "./execute-collector";
|
|
3
|
+
import type { ScriptTransportClient } from "./transport-client";
|
|
4
|
+
|
|
5
|
+
describe("ExecuteCollector", () => {
|
|
6
|
+
const createMockClient = (
|
|
7
|
+
response: {
|
|
8
|
+
exitCode?: number;
|
|
9
|
+
stdout?: string;
|
|
10
|
+
stderr?: string;
|
|
11
|
+
timedOut?: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
} = {}
|
|
14
|
+
): ScriptTransportClient => ({
|
|
15
|
+
exec: mock(() =>
|
|
16
|
+
Promise.resolve({
|
|
17
|
+
exitCode: response.exitCode ?? 0,
|
|
18
|
+
stdout: response.stdout ?? "",
|
|
19
|
+
stderr: response.stderr ?? "",
|
|
20
|
+
timedOut: response.timedOut ?? false,
|
|
21
|
+
error: response.error,
|
|
22
|
+
})
|
|
23
|
+
),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("execute", () => {
|
|
27
|
+
it("should execute script successfully", async () => {
|
|
28
|
+
const collector = new ExecuteCollector();
|
|
29
|
+
const client = createMockClient({ exitCode: 0, stdout: "Hello World" });
|
|
30
|
+
|
|
31
|
+
const result = await collector.execute({
|
|
32
|
+
config: { command: "echo", args: ["Hello", "World"], timeout: 5000 },
|
|
33
|
+
client,
|
|
34
|
+
pluginId: "test",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result.result.exitCode).toBe(0);
|
|
38
|
+
expect(result.result.stdout).toBe("Hello World");
|
|
39
|
+
expect(result.result.success).toBe(true);
|
|
40
|
+
expect(result.result.timedOut).toBe(false);
|
|
41
|
+
expect(result.result.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
42
|
+
expect(result.error).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return error for failed script", async () => {
|
|
46
|
+
const collector = new ExecuteCollector();
|
|
47
|
+
const client = createMockClient({
|
|
48
|
+
exitCode: 1,
|
|
49
|
+
stderr: "Command not found",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = await collector.execute({
|
|
53
|
+
config: { command: "nonexistent", args: [], timeout: 5000 },
|
|
54
|
+
client,
|
|
55
|
+
pluginId: "test",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.result.exitCode).toBe(1);
|
|
59
|
+
expect(result.result.success).toBe(false);
|
|
60
|
+
expect(result.error).toContain("Exit code: 1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should handle timeout", async () => {
|
|
64
|
+
const collector = new ExecuteCollector();
|
|
65
|
+
const client = createMockClient({ timedOut: true, exitCode: -1 });
|
|
66
|
+
|
|
67
|
+
const result = await collector.execute({
|
|
68
|
+
config: { command: "sleep", args: ["999"], timeout: 100 },
|
|
69
|
+
client,
|
|
70
|
+
pluginId: "test",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.result.timedOut).toBe(true);
|
|
74
|
+
expect(result.result.success).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should pass correct parameters to client", async () => {
|
|
78
|
+
const collector = new ExecuteCollector();
|
|
79
|
+
const client = createMockClient();
|
|
80
|
+
|
|
81
|
+
await collector.execute({
|
|
82
|
+
config: {
|
|
83
|
+
command: "/usr/bin/check",
|
|
84
|
+
args: ["--verbose"],
|
|
85
|
+
cwd: "/tmp",
|
|
86
|
+
env: { MY_VAR: "value" },
|
|
87
|
+
timeout: 3000,
|
|
88
|
+
},
|
|
89
|
+
client,
|
|
90
|
+
pluginId: "test",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(client.exec).toHaveBeenCalledWith({
|
|
94
|
+
command: "/usr/bin/check",
|
|
95
|
+
args: ["--verbose"],
|
|
96
|
+
cwd: "/tmp",
|
|
97
|
+
env: { MY_VAR: "value" },
|
|
98
|
+
timeout: 3000,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("aggregateResult", () => {
|
|
104
|
+
it("should calculate average execution time and success rate", () => {
|
|
105
|
+
const collector = new ExecuteCollector();
|
|
106
|
+
const runs = [
|
|
107
|
+
{
|
|
108
|
+
id: "1",
|
|
109
|
+
status: "healthy" as const,
|
|
110
|
+
latencyMs: 100,
|
|
111
|
+
checkId: "c1",
|
|
112
|
+
timestamp: new Date(),
|
|
113
|
+
metadata: {
|
|
114
|
+
exitCode: 0,
|
|
115
|
+
stdout: "",
|
|
116
|
+
stderr: "",
|
|
117
|
+
executionTimeMs: 50,
|
|
118
|
+
success: true,
|
|
119
|
+
timedOut: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "2",
|
|
124
|
+
status: "healthy" as const,
|
|
125
|
+
latencyMs: 150,
|
|
126
|
+
checkId: "c1",
|
|
127
|
+
timestamp: new Date(),
|
|
128
|
+
metadata: {
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
stdout: "",
|
|
131
|
+
stderr: "",
|
|
132
|
+
executionTimeMs: 100,
|
|
133
|
+
success: true,
|
|
134
|
+
timedOut: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const aggregated = collector.aggregateResult(runs);
|
|
140
|
+
|
|
141
|
+
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
142
|
+
expect(aggregated.successRate).toBe(100);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should calculate success rate correctly", () => {
|
|
146
|
+
const collector = new ExecuteCollector();
|
|
147
|
+
const runs = [
|
|
148
|
+
{
|
|
149
|
+
id: "1",
|
|
150
|
+
status: "healthy" as const,
|
|
151
|
+
latencyMs: 100,
|
|
152
|
+
checkId: "c1",
|
|
153
|
+
timestamp: new Date(),
|
|
154
|
+
metadata: {
|
|
155
|
+
exitCode: 0,
|
|
156
|
+
stdout: "",
|
|
157
|
+
stderr: "",
|
|
158
|
+
executionTimeMs: 50,
|
|
159
|
+
success: true,
|
|
160
|
+
timedOut: false,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "2",
|
|
165
|
+
status: "unhealthy" as const,
|
|
166
|
+
latencyMs: 150,
|
|
167
|
+
checkId: "c1",
|
|
168
|
+
timestamp: new Date(),
|
|
169
|
+
metadata: {
|
|
170
|
+
exitCode: 1,
|
|
171
|
+
stdout: "",
|
|
172
|
+
stderr: "",
|
|
173
|
+
executionTimeMs: 100,
|
|
174
|
+
success: false,
|
|
175
|
+
timedOut: false,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const aggregated = collector.aggregateResult(runs);
|
|
181
|
+
|
|
182
|
+
expect(aggregated.successRate).toBe(50);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("metadata", () => {
|
|
187
|
+
it("should have correct static properties", () => {
|
|
188
|
+
const collector = new ExecuteCollector();
|
|
189
|
+
|
|
190
|
+
expect(collector.id).toBe("execute");
|
|
191
|
+
expect(collector.displayName).toBe("Execute Script");
|
|
192
|
+
expect(collector.allowMultiple).toBe(true);
|
|
193
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
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 { ScriptTransportClient } from "./transport-client";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CONFIGURATION SCHEMA
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const executeConfigSchema = z.object({
|
|
21
|
+
command: z.string().min(1).describe("Command or script path to execute"),
|
|
22
|
+
args: z.array(z.string()).default([]).describe("Command arguments"),
|
|
23
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
24
|
+
env: z
|
|
25
|
+
.record(z.string(), z.string())
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Environment variables"),
|
|
28
|
+
timeout: z
|
|
29
|
+
.number()
|
|
30
|
+
.min(100)
|
|
31
|
+
.default(30_000)
|
|
32
|
+
.describe("Timeout in milliseconds"),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type ExecuteConfig = z.infer<typeof executeConfigSchema>;
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// RESULT SCHEMAS
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
const executeResultSchema = z.object({
|
|
42
|
+
exitCode: healthResultNumber({
|
|
43
|
+
"x-chart-type": "counter",
|
|
44
|
+
"x-chart-label": "Exit Code",
|
|
45
|
+
}),
|
|
46
|
+
stdout: healthResultString({
|
|
47
|
+
"x-chart-type": "text",
|
|
48
|
+
"x-chart-label": "Standard Output",
|
|
49
|
+
}),
|
|
50
|
+
stderr: healthResultString({
|
|
51
|
+
"x-chart-type": "text",
|
|
52
|
+
"x-chart-label": "Standard Error",
|
|
53
|
+
}),
|
|
54
|
+
executionTimeMs: healthResultNumber({
|
|
55
|
+
"x-chart-type": "line",
|
|
56
|
+
"x-chart-label": "Execution Time",
|
|
57
|
+
"x-chart-unit": "ms",
|
|
58
|
+
}),
|
|
59
|
+
success: healthResultBoolean({
|
|
60
|
+
"x-chart-type": "boolean",
|
|
61
|
+
"x-chart-label": "Success",
|
|
62
|
+
}),
|
|
63
|
+
timedOut: healthResultBoolean({
|
|
64
|
+
"x-chart-type": "boolean",
|
|
65
|
+
"x-chart-label": "Timed Out",
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type ExecuteResult = z.infer<typeof executeResultSchema>;
|
|
70
|
+
|
|
71
|
+
const executeAggregatedSchema = z.object({
|
|
72
|
+
avgExecutionTimeMs: healthResultNumber({
|
|
73
|
+
"x-chart-type": "line",
|
|
74
|
+
"x-chart-label": "Avg Execution Time",
|
|
75
|
+
"x-chart-unit": "ms",
|
|
76
|
+
}),
|
|
77
|
+
successRate: healthResultNumber({
|
|
78
|
+
"x-chart-type": "gauge",
|
|
79
|
+
"x-chart-label": "Success Rate",
|
|
80
|
+
"x-chart-unit": "%",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export type ExecuteAggregatedResult = z.infer<typeof executeAggregatedSchema>;
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// EXECUTE COLLECTOR
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Built-in Script execute collector.
|
|
92
|
+
* Runs commands and checks results.
|
|
93
|
+
*/
|
|
94
|
+
export class ExecuteCollector
|
|
95
|
+
implements
|
|
96
|
+
CollectorStrategy<
|
|
97
|
+
ScriptTransportClient,
|
|
98
|
+
ExecuteConfig,
|
|
99
|
+
ExecuteResult,
|
|
100
|
+
ExecuteAggregatedResult
|
|
101
|
+
>
|
|
102
|
+
{
|
|
103
|
+
id = "execute";
|
|
104
|
+
displayName = "Execute Script";
|
|
105
|
+
description = "Execute a command or script and check the result";
|
|
106
|
+
|
|
107
|
+
supportedPlugins = [pluginMetadata];
|
|
108
|
+
|
|
109
|
+
allowMultiple = true;
|
|
110
|
+
|
|
111
|
+
config = new Versioned({ version: 1, schema: executeConfigSchema });
|
|
112
|
+
result = new Versioned({ version: 1, schema: executeResultSchema });
|
|
113
|
+
aggregatedResult = new Versioned({
|
|
114
|
+
version: 1,
|
|
115
|
+
schema: executeAggregatedSchema,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async execute({
|
|
119
|
+
config,
|
|
120
|
+
client,
|
|
121
|
+
}: {
|
|
122
|
+
config: ExecuteConfig;
|
|
123
|
+
client: ScriptTransportClient;
|
|
124
|
+
pluginId: string;
|
|
125
|
+
}): Promise<CollectorResult<ExecuteResult>> {
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
128
|
+
const response = await client.exec({
|
|
129
|
+
command: config.command,
|
|
130
|
+
args: config.args,
|
|
131
|
+
cwd: config.cwd,
|
|
132
|
+
env: config.env,
|
|
133
|
+
timeout: config.timeout,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const executionTimeMs = Date.now() - startTime;
|
|
137
|
+
const success = response.exitCode === 0 && !response.timedOut;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
result: {
|
|
141
|
+
exitCode: response.exitCode,
|
|
142
|
+
stdout: response.stdout,
|
|
143
|
+
stderr: response.stderr,
|
|
144
|
+
executionTimeMs,
|
|
145
|
+
success,
|
|
146
|
+
timedOut: response.timedOut,
|
|
147
|
+
},
|
|
148
|
+
error:
|
|
149
|
+
response.error ??
|
|
150
|
+
(success ? undefined : `Exit code: ${response.exitCode}`),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
aggregateResult(
|
|
155
|
+
runs: HealthCheckRunForAggregation<ExecuteResult>[]
|
|
156
|
+
): ExecuteAggregatedResult {
|
|
157
|
+
const times = runs
|
|
158
|
+
.map((r) => r.metadata?.executionTimeMs)
|
|
159
|
+
.filter((v): v is number => typeof v === "number");
|
|
160
|
+
|
|
161
|
+
const successes = runs
|
|
162
|
+
.map((r) => r.metadata?.success)
|
|
163
|
+
.filter((v): v is boolean => typeof v === "boolean");
|
|
164
|
+
|
|
165
|
+
const successCount = successes.filter(Boolean).length;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
avgExecutionTimeMs:
|
|
169
|
+
times.length > 0
|
|
170
|
+
? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
|
|
171
|
+
: 0,
|
|
172
|
+
successRate:
|
|
173
|
+
successes.length > 0
|
|
174
|
+
? Math.round((successCount / successes.length) * 100)
|
|
175
|
+
: 0,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
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 { ScriptHealthCheckStrategy } from "./strategy";
|
|
6
3
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
|
+
import { ExecuteCollector } from "./execute-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 Script Health Check Strategy...");
|
|
18
17
|
const strategy = new ScriptHealthCheckStrategy();
|
|
19
18
|
healthCheckRegistry.register(strategy);
|
|
19
|
+
collectorRegistry.register(new ExecuteCollector());
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
export { pluginMetadata } from "./plugin-metadata";
|
package/src/strategy.test.ts
CHANGED
|
@@ -24,134 +24,100 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
24
24
|
),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
describe("
|
|
28
|
-
it("should return
|
|
27
|
+
describe("createClient", () => {
|
|
28
|
+
it("should return a connected client", async () => {
|
|
29
|
+
const strategy = new ScriptHealthCheckStrategy(createMockExecutor());
|
|
30
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
31
|
+
|
|
32
|
+
expect(connectedClient.client).toBeDefined();
|
|
33
|
+
expect(connectedClient.client.exec).toBeDefined();
|
|
34
|
+
expect(connectedClient.close).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should allow closing the client", async () => {
|
|
38
|
+
const strategy = new ScriptHealthCheckStrategy(createMockExecutor());
|
|
39
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
40
|
+
|
|
41
|
+
expect(() => connectedClient.close()).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("client.exec", () => {
|
|
46
|
+
it("should return successful result for successful script execution", async () => {
|
|
29
47
|
const strategy = new ScriptHealthCheckStrategy(
|
|
30
48
|
createMockExecutor({ exitCode: 0, stdout: "OK" })
|
|
31
49
|
);
|
|
50
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
32
51
|
|
|
33
|
-
const result = await
|
|
52
|
+
const result = await connectedClient.client.exec({
|
|
34
53
|
command: "/usr/bin/true",
|
|
54
|
+
args: [],
|
|
35
55
|
timeout: 5000,
|
|
36
56
|
});
|
|
37
57
|
|
|
38
|
-
expect(result.
|
|
39
|
-
expect(result.
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
expect(result.exitCode).toBe(0);
|
|
59
|
+
expect(result.timedOut).toBe(false);
|
|
60
|
+
|
|
61
|
+
connectedClient.close();
|
|
42
62
|
});
|
|
43
63
|
|
|
44
|
-
it("should return
|
|
64
|
+
it("should return non-zero exit code for failed script", async () => {
|
|
45
65
|
const strategy = new ScriptHealthCheckStrategy(
|
|
46
66
|
createMockExecutor({ exitCode: 1, stderr: "Error" })
|
|
47
67
|
);
|
|
68
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
48
69
|
|
|
49
|
-
const result = await
|
|
70
|
+
const result = await connectedClient.client.exec({
|
|
50
71
|
command: "/usr/bin/false",
|
|
72
|
+
args: [],
|
|
51
73
|
timeout: 5000,
|
|
52
74
|
});
|
|
53
75
|
|
|
54
|
-
expect(result.
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
expect(result.exitCode).toBe(1);
|
|
77
|
+
|
|
78
|
+
connectedClient.close();
|
|
57
79
|
});
|
|
58
80
|
|
|
59
|
-
it("should
|
|
81
|
+
it("should indicate timeout for timed out script", async () => {
|
|
60
82
|
const strategy = new ScriptHealthCheckStrategy(
|
|
61
83
|
createMockExecutor({ timedOut: true, exitCode: -1 })
|
|
62
84
|
);
|
|
85
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
63
86
|
|
|
64
|
-
const result = await
|
|
87
|
+
const result = await connectedClient.client.exec({
|
|
65
88
|
command: "sleep",
|
|
66
89
|
args: ["60"],
|
|
67
90
|
timeout: 1000,
|
|
68
91
|
});
|
|
69
92
|
|
|
70
|
-
expect(result.
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
expect(result.timedOut).toBe(true);
|
|
94
|
+
|
|
95
|
+
connectedClient.close();
|
|
73
96
|
});
|
|
74
97
|
|
|
75
|
-
it("should return
|
|
98
|
+
it("should return error for execution error", async () => {
|
|
76
99
|
const strategy = new ScriptHealthCheckStrategy(
|
|
77
100
|
createMockExecutor({ error: new Error("Command not found") })
|
|
78
101
|
);
|
|
102
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
79
103
|
|
|
80
|
-
const result = await
|
|
104
|
+
const result = await connectedClient.client.exec({
|
|
81
105
|
command: "nonexistent-command",
|
|
106
|
+
args: [],
|
|
82
107
|
timeout: 5000,
|
|
83
108
|
});
|
|
84
109
|
|
|
85
|
-
expect(result.
|
|
86
|
-
expect(result.message).toContain("Command not found");
|
|
87
|
-
expect(result.metadata?.executed).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should pass executionTime assertion when below threshold", async () => {
|
|
91
|
-
const strategy = new ScriptHealthCheckStrategy(createMockExecutor());
|
|
92
|
-
|
|
93
|
-
const result = await strategy.execute({
|
|
94
|
-
command: "/usr/bin/true",
|
|
95
|
-
timeout: 5000,
|
|
96
|
-
assertions: [
|
|
97
|
-
{ field: "executionTime", operator: "lessThan", value: 5000 },
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
expect(result.status).toBe("healthy");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("should pass exitCode assertion", async () => {
|
|
105
|
-
const strategy = new ScriptHealthCheckStrategy(
|
|
106
|
-
createMockExecutor({ exitCode: 0 })
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const result = await strategy.execute({
|
|
110
|
-
command: "/usr/bin/true",
|
|
111
|
-
timeout: 5000,
|
|
112
|
-
assertions: [{ field: "exitCode", operator: "equals", value: 0 }],
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(result.status).toBe("healthy");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("should fail exitCode assertion when non-zero", async () => {
|
|
119
|
-
const strategy = new ScriptHealthCheckStrategy(
|
|
120
|
-
createMockExecutor({ exitCode: 2 })
|
|
121
|
-
);
|
|
110
|
+
expect(result.error).toContain("Command not found");
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
command: "/usr/bin/false",
|
|
125
|
-
timeout: 5000,
|
|
126
|
-
assertions: [{ field: "exitCode", operator: "equals", value: 0 }],
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(result.status).toBe("unhealthy");
|
|
130
|
-
expect(result.message).toContain("Assertion failed");
|
|
112
|
+
connectedClient.close();
|
|
131
113
|
});
|
|
132
114
|
|
|
133
|
-
it("should pass
|
|
134
|
-
const strategy = new ScriptHealthCheckStrategy(
|
|
135
|
-
createMockExecutor({ stdout: "Service is running" })
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const result = await strategy.execute({
|
|
139
|
-
command: "/usr/bin/echo",
|
|
140
|
-
args: ["Service is running"],
|
|
141
|
-
timeout: 5000,
|
|
142
|
-
assertions: [
|
|
143
|
-
{ field: "stdout", operator: "contains", value: "running" },
|
|
144
|
-
],
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(result.status).toBe("healthy");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("should pass with arguments and env vars", async () => {
|
|
115
|
+
it("should pass arguments, cwd, and env to executor", async () => {
|
|
151
116
|
const mockExecutor = createMockExecutor({ exitCode: 0 });
|
|
152
117
|
const strategy = new ScriptHealthCheckStrategy(mockExecutor);
|
|
118
|
+
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
153
119
|
|
|
154
|
-
await
|
|
120
|
+
await connectedClient.client.exec({
|
|
155
121
|
command: "./check.sh",
|
|
156
122
|
args: ["--verbose", "--env=prod"],
|
|
157
123
|
cwd: "/opt/scripts",
|
|
@@ -167,6 +133,8 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
167
133
|
env: { API_KEY: "secret" },
|
|
168
134
|
})
|
|
169
135
|
);
|
|
136
|
+
|
|
137
|
+
connectedClient.close();
|
|
170
138
|
});
|
|
171
139
|
});
|
|
172
140
|
|
package/src/strategy.ts
CHANGED
|
@@ -1,66 +1,50 @@
|
|
|
1
1
|
import { spawn, type Subprocess } from "bun";
|
|
2
2
|
import {
|
|
3
3
|
HealthCheckStrategy,
|
|
4
|
-
HealthCheckResult,
|
|
5
4
|
HealthCheckRunForAggregation,
|
|
6
5
|
Versioned,
|
|
7
6
|
z,
|
|
8
|
-
|
|
9
|
-
numericField,
|
|
10
|
-
booleanField,
|
|
11
|
-
stringField,
|
|
12
|
-
evaluateAssertions,
|
|
7
|
+
type ConnectedClient,
|
|
13
8
|
} from "@checkstack/backend-api";
|
|
14
9
|
import {
|
|
15
10
|
healthResultBoolean,
|
|
16
11
|
healthResultNumber,
|
|
17
12
|
healthResultString,
|
|
18
13
|
} from "@checkstack/healthcheck-common";
|
|
14
|
+
import type {
|
|
15
|
+
ScriptTransportClient,
|
|
16
|
+
ScriptRequest,
|
|
17
|
+
ScriptResult as ScriptResultType,
|
|
18
|
+
} from "./transport-client";
|
|
19
19
|
|
|
20
20
|
// ============================================================================
|
|
21
21
|
// SCHEMAS
|
|
22
22
|
// ============================================================================
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Assertion schema for Script health checks using shared factories.
|
|
26
|
-
*/
|
|
27
|
-
const scriptAssertionSchema = z.discriminatedUnion("field", [
|
|
28
|
-
timeThresholdField("executionTime"),
|
|
29
|
-
numericField("exitCode", { min: 0 }),
|
|
30
|
-
booleanField("success"),
|
|
31
|
-
stringField("stdout"),
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
export type ScriptAssertion = z.infer<typeof scriptAssertionSchema>;
|
|
35
|
-
|
|
36
24
|
/**
|
|
37
25
|
* Configuration schema for Script health checks.
|
|
26
|
+
* Global defaults only - action params moved to ExecuteCollector.
|
|
38
27
|
*/
|
|
39
28
|
export const scriptConfigSchema = z.object({
|
|
40
|
-
command: z.string().describe("Command or script path to execute"),
|
|
41
|
-
args: z
|
|
42
|
-
.array(z.string())
|
|
43
|
-
.default([])
|
|
44
|
-
.describe("Arguments to pass to the command"),
|
|
45
|
-
cwd: z.string().optional().describe("Working directory for script execution"),
|
|
46
|
-
env: z
|
|
47
|
-
.record(z.string(), z.string())
|
|
48
|
-
.optional()
|
|
49
|
-
.describe("Environment variables to set"),
|
|
50
29
|
timeout: z
|
|
51
30
|
.number()
|
|
52
31
|
.min(100)
|
|
53
32
|
.default(30_000)
|
|
54
|
-
.describe("
|
|
55
|
-
assertions: z
|
|
56
|
-
.array(scriptAssertionSchema)
|
|
57
|
-
.optional()
|
|
58
|
-
.describe("Validation conditions"),
|
|
33
|
+
.describe("Default execution timeout in milliseconds"),
|
|
59
34
|
});
|
|
60
35
|
|
|
61
36
|
export type ScriptConfig = z.infer<typeof scriptConfigSchema>;
|
|
62
37
|
export type ScriptConfigInput = z.input<typeof scriptConfigSchema>;
|
|
63
38
|
|
|
39
|
+
// Legacy config type for migrations
|
|
40
|
+
interface ScriptConfigV1 {
|
|
41
|
+
command: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
cwd?: string;
|
|
44
|
+
env?: Record<string, string>;
|
|
45
|
+
timeout: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
64
48
|
/**
|
|
65
49
|
* Per-run result metadata.
|
|
66
50
|
*/
|
|
@@ -78,14 +62,6 @@ const scriptResultSchema = z.object({
|
|
|
78
62
|
"x-chart-type": "counter",
|
|
79
63
|
"x-chart-label": "Exit Code",
|
|
80
64
|
}).optional(),
|
|
81
|
-
stdout: healthResultString({
|
|
82
|
-
"x-chart-type": "text",
|
|
83
|
-
"x-chart-label": "Stdout",
|
|
84
|
-
}).optional(),
|
|
85
|
-
stderr: healthResultString({
|
|
86
|
-
"x-chart-type": "text",
|
|
87
|
-
"x-chart-label": "Stderr",
|
|
88
|
-
}).optional(),
|
|
89
65
|
success: healthResultBoolean({
|
|
90
66
|
"x-chart-type": "boolean",
|
|
91
67
|
"x-chart-label": "Success",
|
|
@@ -94,14 +70,13 @@ const scriptResultSchema = z.object({
|
|
|
94
70
|
"x-chart-type": "boolean",
|
|
95
71
|
"x-chart-label": "Timed Out",
|
|
96
72
|
}),
|
|
97
|
-
failedAssertion: scriptAssertionSchema.optional(),
|
|
98
73
|
error: healthResultString({
|
|
99
74
|
"x-chart-type": "status",
|
|
100
75
|
"x-chart-label": "Error",
|
|
101
76
|
}).optional(),
|
|
102
77
|
});
|
|
103
78
|
|
|
104
|
-
|
|
79
|
+
type ScriptResult = z.infer<typeof scriptResultSchema>;
|
|
105
80
|
|
|
106
81
|
/**
|
|
107
82
|
* Aggregated metadata for buckets.
|
|
@@ -127,13 +102,13 @@ const scriptAggregatedSchema = z.object({
|
|
|
127
102
|
}),
|
|
128
103
|
});
|
|
129
104
|
|
|
130
|
-
|
|
105
|
+
type ScriptAggregatedResult = z.infer<typeof scriptAggregatedSchema>;
|
|
131
106
|
|
|
132
107
|
// ============================================================================
|
|
133
108
|
// SCRIPT EXECUTOR INTERFACE (for testability)
|
|
134
109
|
// ============================================================================
|
|
135
110
|
|
|
136
|
-
|
|
111
|
+
interface ScriptExecutionResult {
|
|
137
112
|
exitCode: number;
|
|
138
113
|
stdout: string;
|
|
139
114
|
stderr: string;
|
|
@@ -164,7 +139,7 @@ const defaultScriptExecutor: ScriptExecutor = {
|
|
|
164
139
|
}, config.timeout);
|
|
165
140
|
});
|
|
166
141
|
|
|
167
|
-
|
|
142
|
+
try {
|
|
168
143
|
proc = spawn({
|
|
169
144
|
cmd: [config.command, ...config.args],
|
|
170
145
|
cwd: config.cwd,
|
|
@@ -173,15 +148,14 @@ const defaultScriptExecutor: ScriptExecutor = {
|
|
|
173
148
|
stderr: "pipe",
|
|
174
149
|
});
|
|
175
150
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const exitCode = await proc.exited;
|
|
151
|
+
const [stdout, stderr, exitCode] = await Promise.race([
|
|
152
|
+
Promise.all([
|
|
153
|
+
new Response(proc.stdout as ReadableStream).text(),
|
|
154
|
+
new Response(proc.stderr as ReadableStream).text(),
|
|
155
|
+
proc.exited,
|
|
156
|
+
]),
|
|
157
|
+
timeoutPromise,
|
|
158
|
+
]);
|
|
185
159
|
|
|
186
160
|
return {
|
|
187
161
|
exitCode,
|
|
@@ -189,10 +163,6 @@ const defaultScriptExecutor: ScriptExecutor = {
|
|
|
189
163
|
stderr: stderr.trim(),
|
|
190
164
|
timedOut: false,
|
|
191
165
|
};
|
|
192
|
-
})();
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
return await Promise.race([execPromise, timeoutPromise]);
|
|
196
166
|
} catch (error) {
|
|
197
167
|
if (timedOut) {
|
|
198
168
|
return {
|
|
@@ -213,7 +183,12 @@ const defaultScriptExecutor: ScriptExecutor = {
|
|
|
213
183
|
|
|
214
184
|
export class ScriptHealthCheckStrategy
|
|
215
185
|
implements
|
|
216
|
-
HealthCheckStrategy<
|
|
186
|
+
HealthCheckStrategy<
|
|
187
|
+
ScriptConfig,
|
|
188
|
+
ScriptTransportClient,
|
|
189
|
+
ScriptResult,
|
|
190
|
+
ScriptAggregatedResult
|
|
191
|
+
>
|
|
217
192
|
{
|
|
218
193
|
id = "script";
|
|
219
194
|
displayName = "Script Health Check";
|
|
@@ -226,13 +201,31 @@ export class ScriptHealthCheckStrategy
|
|
|
226
201
|
}
|
|
227
202
|
|
|
228
203
|
config: Versioned<ScriptConfig> = new Versioned({
|
|
229
|
-
version:
|
|
204
|
+
version: 2,
|
|
230
205
|
schema: scriptConfigSchema,
|
|
206
|
+
migrations: [
|
|
207
|
+
{
|
|
208
|
+
fromVersion: 1,
|
|
209
|
+
toVersion: 2,
|
|
210
|
+
description: "Remove command/args/cwd/env (moved to ExecuteCollector)",
|
|
211
|
+
migrate: (data: ScriptConfigV1): ScriptConfig => ({
|
|
212
|
+
timeout: data.timeout,
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
231
216
|
});
|
|
232
217
|
|
|
233
218
|
result: Versioned<ScriptResult> = new Versioned({
|
|
234
|
-
version:
|
|
219
|
+
version: 2,
|
|
235
220
|
schema: scriptResultSchema,
|
|
221
|
+
migrations: [
|
|
222
|
+
{
|
|
223
|
+
fromVersion: 1,
|
|
224
|
+
toVersion: 2,
|
|
225
|
+
description: "Migrate to createClient pattern (no result changes)",
|
|
226
|
+
migrate: (data: unknown) => data,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
236
229
|
});
|
|
237
230
|
|
|
238
231
|
aggregatedResult: Versioned<ScriptAggregatedResult> = new Versioned({
|
|
@@ -243,123 +236,86 @@ export class ScriptHealthCheckStrategy
|
|
|
243
236
|
aggregateResult(
|
|
244
237
|
runs: HealthCheckRunForAggregation<ScriptResult>[]
|
|
245
238
|
): ScriptAggregatedResult {
|
|
246
|
-
|
|
247
|
-
let successCount = 0;
|
|
248
|
-
let errorCount = 0;
|
|
249
|
-
let timeoutCount = 0;
|
|
250
|
-
let validRuns = 0;
|
|
251
|
-
|
|
252
|
-
for (const run of runs) {
|
|
253
|
-
if (run.metadata?.error) {
|
|
254
|
-
errorCount++;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (run.metadata?.timedOut) {
|
|
258
|
-
timeoutCount++;
|
|
259
|
-
}
|
|
260
|
-
if (run.status === "healthy") {
|
|
261
|
-
successCount++;
|
|
262
|
-
}
|
|
263
|
-
if (run.metadata) {
|
|
264
|
-
totalExecutionTime += run.metadata.executionTimeMs;
|
|
265
|
-
validRuns++;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
239
|
+
const validRuns = runs.filter((r) => r.metadata);
|
|
268
240
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
241
|
+
if (validRuns.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
avgExecutionTime: 0,
|
|
244
|
+
successRate: 0,
|
|
245
|
+
errorCount: 0,
|
|
246
|
+
timeoutCount: 0,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
276
249
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const validatedConfig = this.config.validate(config);
|
|
281
|
-
const start = performance.now();
|
|
250
|
+
const executionTimes = validRuns
|
|
251
|
+
.map((r) => r.metadata?.executionTimeMs)
|
|
252
|
+
.filter((t): t is number => typeof t === "number");
|
|
282
253
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
timeout: validatedConfig.timeout,
|
|
290
|
-
});
|
|
254
|
+
const avgExecutionTime =
|
|
255
|
+
executionTimes.length > 0
|
|
256
|
+
? Math.round(
|
|
257
|
+
executionTimes.reduce((a, b) => a + b, 0) / executionTimes.length
|
|
258
|
+
)
|
|
259
|
+
: 0;
|
|
291
260
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
executed: true,
|
|
297
|
-
executionTimeMs,
|
|
298
|
-
exitCode: execResult.exitCode,
|
|
299
|
-
stdout: execResult.stdout,
|
|
300
|
-
stderr: execResult.stderr,
|
|
301
|
-
success,
|
|
302
|
-
timedOut: execResult.timedOut,
|
|
303
|
-
};
|
|
261
|
+
const successCount = validRuns.filter(
|
|
262
|
+
(r) => r.metadata?.success === true
|
|
263
|
+
).length;
|
|
264
|
+
const successRate = Math.round((successCount / validRuns.length) * 100);
|
|
304
265
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
exitCode: execResult.exitCode,
|
|
309
|
-
success,
|
|
310
|
-
stdout: execResult.stdout,
|
|
311
|
-
});
|
|
266
|
+
const errorCount = validRuns.filter(
|
|
267
|
+
(r) => r.metadata?.error !== undefined
|
|
268
|
+
).length;
|
|
312
269
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
latencyMs: executionTimeMs,
|
|
317
|
-
message: `Assertion failed: ${failedAssertion.field} ${
|
|
318
|
-
failedAssertion.operator
|
|
319
|
-
}${"value" in failedAssertion ? ` ${failedAssertion.value}` : ""}`,
|
|
320
|
-
metadata: { ...result, failedAssertion },
|
|
321
|
-
};
|
|
322
|
-
}
|
|
270
|
+
const timeoutCount = validRuns.filter(
|
|
271
|
+
(r) => r.metadata?.timedOut === true
|
|
272
|
+
).length;
|
|
323
273
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
274
|
+
return {
|
|
275
|
+
avgExecutionTime,
|
|
276
|
+
successRate,
|
|
277
|
+
errorCount,
|
|
278
|
+
timeoutCount,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
332
281
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
282
|
+
async createClient(
|
|
283
|
+
_config: ScriptConfigInput
|
|
284
|
+
): Promise<ConnectedClient<ScriptTransportClient>> {
|
|
285
|
+
const client: ScriptTransportClient = {
|
|
286
|
+
exec: async (request: ScriptRequest): Promise<ScriptResultType> => {
|
|
287
|
+
try {
|
|
288
|
+
const result = await this.executor.execute({
|
|
289
|
+
command: request.command,
|
|
290
|
+
args: request.args,
|
|
291
|
+
cwd: request.cwd,
|
|
292
|
+
env: request.env,
|
|
293
|
+
timeout: request.timeout,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
exitCode: result.exitCode,
|
|
298
|
+
stdout: result.stdout,
|
|
299
|
+
stderr: result.stderr,
|
|
300
|
+
timedOut: result.timedOut,
|
|
301
|
+
};
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return {
|
|
304
|
+
exitCode: -1,
|
|
305
|
+
stdout: "",
|
|
306
|
+
stderr: "",
|
|
307
|
+
timedOut: false,
|
|
308
|
+
error: error instanceof Error ? error.message : String(error),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
};
|
|
341
313
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
} catch (error: unknown) {
|
|
349
|
-
const end = performance.now();
|
|
350
|
-
const isError = error instanceof Error;
|
|
351
|
-
return {
|
|
352
|
-
status: "unhealthy",
|
|
353
|
-
latencyMs: Math.round(end - start),
|
|
354
|
-
message: isError ? error.message : "Script execution failed",
|
|
355
|
-
metadata: {
|
|
356
|
-
executed: false,
|
|
357
|
-
executionTimeMs: Math.round(end - start),
|
|
358
|
-
success: false,
|
|
359
|
-
timedOut: false,
|
|
360
|
-
error: isError ? error.name : "UnknownError",
|
|
361
|
-
},
|
|
362
|
-
};
|
|
363
|
-
}
|
|
314
|
+
return {
|
|
315
|
+
client,
|
|
316
|
+
close: () => {
|
|
317
|
+
// Script executor is stateless, nothing to close
|
|
318
|
+
},
|
|
319
|
+
};
|
|
364
320
|
}
|
|
365
321
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TransportClient } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script execution request.
|
|
5
|
+
*/
|
|
6
|
+
export interface ScriptRequest {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
cwd?: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
timeout: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Script execution result.
|
|
16
|
+
*/
|
|
17
|
+
export interface ScriptResult {
|
|
18
|
+
exitCode: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
timedOut: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Script transport client for command execution.
|
|
27
|
+
*/
|
|
28
|
+
export type ScriptTransportClient = TransportClient<
|
|
29
|
+
ScriptRequest,
|
|
30
|
+
ScriptResult
|
|
31
|
+
>;
|