@checkstack/healthcheck-script-backend 0.1.13 → 0.3.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 +79 -0
- package/package.json +6 -6
- package/src/execute-collector.test.ts +8 -6
- package/src/execute-collector.ts +36 -36
- package/src/index.ts +2 -0
- package/src/inline-script-collector.test.ts +256 -0
- package/src/inline-script-collector.ts +348 -0
- package/src/strategy.test.ts +12 -10
- package/src/strategy.ts +42 -60
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @checkstack/healthcheck-script-backend
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
8
|
+
|
|
9
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
10
|
+
|
|
11
|
+
**Key changes:**
|
|
12
|
+
|
|
13
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
14
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
15
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
16
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
17
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
18
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
19
|
+
|
|
20
|
+
**Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [3dd1914]
|
|
25
|
+
- @checkstack/backend-api@0.7.0
|
|
26
|
+
|
|
27
|
+
## 0.2.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- f676e11: Add script execution support and migrate CodeEditor to Monaco
|
|
32
|
+
|
|
33
|
+
**Integration providers** (`@checkstack/integration-script-backend`):
|
|
34
|
+
|
|
35
|
+
- **Script** - Execute TypeScript/JavaScript with context object
|
|
36
|
+
- **Bash** - Execute shell scripts with environment variables ($EVENT*ID, $PAYLOAD*\*)
|
|
37
|
+
|
|
38
|
+
**Health check collectors** (`@checkstack/healthcheck-script-backend`):
|
|
39
|
+
|
|
40
|
+
- **InlineScriptCollector** - Run TypeScript directly for health checks
|
|
41
|
+
- **ExecuteCollector** - Bash syntax highlighting for command field
|
|
42
|
+
|
|
43
|
+
**CodeEditor migration to Monaco** (`@checkstack/ui`):
|
|
44
|
+
|
|
45
|
+
- Replaced CodeMirror with Monaco Editor (VS Code's editor)
|
|
46
|
+
- Full TypeScript/JavaScript IntelliSense with custom type definitions
|
|
47
|
+
- Added `generateTypeDefinitions()` for JSON Schema → TypeScript conversion
|
|
48
|
+
- Removed all CodeMirror dependencies
|
|
49
|
+
|
|
50
|
+
**Type updates** (`@checkstack/common`):
|
|
51
|
+
|
|
52
|
+
- Added `javascript`, `typescript`, and `bash` to `EditorType` union
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
57
|
+
|
|
58
|
+
### Breaking Changes (Internal)
|
|
59
|
+
|
|
60
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
61
|
+
|
|
62
|
+
### New Features
|
|
63
|
+
|
|
64
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
65
|
+
- `mergeCounter()` - track occurrences
|
|
66
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
67
|
+
- `mergeRate()` - track success/total, compute %
|
|
68
|
+
- `mergeMinMax()` - track min/max values
|
|
69
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
70
|
+
|
|
71
|
+
### Improvements
|
|
72
|
+
|
|
73
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
74
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
75
|
+
|
|
76
|
+
- Updated dependencies [f676e11]
|
|
77
|
+
- Updated dependencies [48c2080]
|
|
78
|
+
- @checkstack/common@0.6.2
|
|
79
|
+
- @checkstack/backend-api@0.6.0
|
|
80
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
81
|
+
|
|
3
82
|
## 0.1.13
|
|
4
83
|
|
|
5
84
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-script-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,14 +9,14 @@
|
|
|
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
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/bun": "^1.0.0",
|
|
18
18
|
"typescript": "^5.0.0",
|
|
19
|
-
"@checkstack/tsconfig": "0.0.
|
|
20
|
-
"@checkstack/scripts": "0.1.
|
|
19
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
20
|
+
"@checkstack/scripts": "0.1.1"
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -100,7 +100,7 @@ describe("ExecuteCollector", () => {
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
describe("
|
|
103
|
+
describe("mergeResult", () => {
|
|
104
104
|
it("should calculate average execution time and success rate", () => {
|
|
105
105
|
const collector = new ExecuteCollector();
|
|
106
106
|
const runs = [
|
|
@@ -136,10 +136,11 @@ describe("ExecuteCollector", () => {
|
|
|
136
136
|
},
|
|
137
137
|
];
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
140
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
140
141
|
|
|
141
|
-
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
142
|
-
expect(aggregated.successRate).toBe(100);
|
|
142
|
+
expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
|
|
143
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
143
144
|
});
|
|
144
145
|
|
|
145
146
|
it("should calculate success rate correctly", () => {
|
|
@@ -177,9 +178,10 @@ describe("ExecuteCollector", () => {
|
|
|
177
178
|
},
|
|
178
179
|
];
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
182
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
181
183
|
|
|
182
|
-
expect(aggregated.successRate).toBe(50);
|
|
184
|
+
expect(aggregated.successRate.rate).toBe(50);
|
|
183
185
|
});
|
|
184
186
|
});
|
|
185
187
|
|
package/src/execute-collector.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Versioned,
|
|
3
3
|
z,
|
|
4
|
+
configString,
|
|
4
5
|
type HealthCheckRunForAggregation,
|
|
5
6
|
type CollectorResult,
|
|
6
7
|
type CollectorStrategy,
|
|
8
|
+
mergeAverage,
|
|
9
|
+
mergeRate,
|
|
10
|
+
VersionedAggregated,
|
|
11
|
+
aggregatedAverage,
|
|
12
|
+
aggregatedRate,
|
|
13
|
+
type InferAggregatedResult,
|
|
7
14
|
} from "@checkstack/backend-api";
|
|
8
15
|
import {
|
|
9
16
|
healthResultNumber,
|
|
@@ -19,7 +26,9 @@ import type { ScriptTransportClient } from "./transport-client";
|
|
|
19
26
|
// ============================================================================
|
|
20
27
|
|
|
21
28
|
const executeConfigSchema = z.object({
|
|
22
|
-
command:
|
|
29
|
+
command: configString({
|
|
30
|
+
"x-editor-types": ["shell"],
|
|
31
|
+
}).describe("Shell command or script to execute"),
|
|
23
32
|
args: z.array(z.string()).default([]).describe("Command arguments"),
|
|
24
33
|
cwd: z.string().optional().describe("Working directory"),
|
|
25
34
|
env: z
|
|
@@ -69,20 +78,24 @@ const executeResultSchema = healthResultSchema({
|
|
|
69
78
|
|
|
70
79
|
export type ExecuteResult = z.infer<typeof executeResultSchema>;
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
// Aggregated result fields definition
|
|
82
|
+
const executeAggregatedFields = {
|
|
83
|
+
avgExecutionTimeMs: aggregatedAverage({
|
|
74
84
|
"x-chart-type": "line",
|
|
75
85
|
"x-chart-label": "Avg Execution Time",
|
|
76
86
|
"x-chart-unit": "ms",
|
|
77
87
|
}),
|
|
78
|
-
successRate:
|
|
88
|
+
successRate: aggregatedRate({
|
|
79
89
|
"x-chart-type": "gauge",
|
|
80
90
|
"x-chart-label": "Success Rate",
|
|
81
91
|
"x-chart-unit": "%",
|
|
82
92
|
}),
|
|
83
|
-
}
|
|
93
|
+
};
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
// Type inferred from field definitions
|
|
96
|
+
export type ExecuteAggregatedResult = InferAggregatedResult<
|
|
97
|
+
typeof executeAggregatedFields
|
|
98
|
+
>;
|
|
86
99
|
|
|
87
100
|
// ============================================================================
|
|
88
101
|
// EXECUTE COLLECTOR
|
|
@@ -92,15 +105,12 @@ export type ExecuteAggregatedResult = z.infer<typeof executeAggregatedSchema>;
|
|
|
92
105
|
* Built-in Script execute collector.
|
|
93
106
|
* Runs commands and checks results.
|
|
94
107
|
*/
|
|
95
|
-
export class ExecuteCollector
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
ExecuteAggregatedResult
|
|
102
|
-
>
|
|
103
|
-
{
|
|
108
|
+
export class ExecuteCollector implements CollectorStrategy<
|
|
109
|
+
ScriptTransportClient,
|
|
110
|
+
ExecuteConfig,
|
|
111
|
+
ExecuteResult,
|
|
112
|
+
ExecuteAggregatedResult
|
|
113
|
+
> {
|
|
104
114
|
id = "execute";
|
|
105
115
|
displayName = "Execute Script";
|
|
106
116
|
description = "Execute a command or script and check the result";
|
|
@@ -111,9 +121,9 @@ export class ExecuteCollector
|
|
|
111
121
|
|
|
112
122
|
config = new Versioned({ version: 1, schema: executeConfigSchema });
|
|
113
123
|
result = new Versioned({ version: 1, schema: executeResultSchema });
|
|
114
|
-
aggregatedResult = new
|
|
124
|
+
aggregatedResult = new VersionedAggregated({
|
|
115
125
|
version: 1,
|
|
116
|
-
|
|
126
|
+
fields: executeAggregatedFields,
|
|
117
127
|
});
|
|
118
128
|
|
|
119
129
|
async execute({
|
|
@@ -152,28 +162,18 @@ export class ExecuteCollector
|
|
|
152
162
|
};
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
mergeResult(
|
|
166
|
+
existing: ExecuteAggregatedResult | undefined,
|
|
167
|
+
run: HealthCheckRunForAggregation<ExecuteResult>,
|
|
157
168
|
): ExecuteAggregatedResult {
|
|
158
|
-
const
|
|
159
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
160
|
-
.filter((v): v is number => typeof v === "number");
|
|
161
|
-
|
|
162
|
-
const successes = runs
|
|
163
|
-
.map((r) => r.metadata?.success)
|
|
164
|
-
.filter((v): v is boolean => typeof v === "boolean");
|
|
165
|
-
|
|
166
|
-
const successCount = successes.filter(Boolean).length;
|
|
169
|
+
const metadata = run.metadata;
|
|
167
170
|
|
|
168
171
|
return {
|
|
169
|
-
avgExecutionTimeMs:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
successRate:
|
|
174
|
-
successes.length > 0
|
|
175
|
-
? Math.round((successCount / successes.length) * 100)
|
|
176
|
-
: 0,
|
|
172
|
+
avgExecutionTimeMs: mergeAverage(
|
|
173
|
+
existing?.avgExecutionTimeMs,
|
|
174
|
+
metadata?.executionTimeMs,
|
|
175
|
+
),
|
|
176
|
+
successRate: mergeRate(existing?.successRate, metadata?.success),
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
|
2
2
|
import { ScriptHealthCheckStrategy } from "./strategy";
|
|
3
3
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
4
|
import { ExecuteCollector } from "./execute-collector";
|
|
5
|
+
import { InlineScriptCollector } from "./inline-script-collector";
|
|
5
6
|
|
|
6
7
|
export default createBackendPlugin({
|
|
7
8
|
metadata: pluginMetadata,
|
|
@@ -17,6 +18,7 @@ export default createBackendPlugin({
|
|
|
17
18
|
const strategy = new ScriptHealthCheckStrategy();
|
|
18
19
|
healthCheckRegistry.register(strategy);
|
|
19
20
|
collectorRegistry.register(new ExecuteCollector());
|
|
21
|
+
collectorRegistry.register(new InlineScriptCollector());
|
|
20
22
|
},
|
|
21
23
|
});
|
|
22
24
|
},
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
InlineScriptCollector,
|
|
4
|
+
type InlineScriptConfig,
|
|
5
|
+
} from "./inline-script-collector";
|
|
6
|
+
import type { ScriptTransportClient } from "./transport-client";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unit tests for the Inline Script Collector.
|
|
10
|
+
*
|
|
11
|
+
* Tests cover:
|
|
12
|
+
* - Basic script execution
|
|
13
|
+
* - Return value handling
|
|
14
|
+
* - Error handling
|
|
15
|
+
* - Timeout protection
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Mock client (not actually used by InlineScriptCollector, but required by interface)
|
|
19
|
+
const mockClient: ScriptTransportClient = {
|
|
20
|
+
exec: async () => ({
|
|
21
|
+
exitCode: 0,
|
|
22
|
+
stdout: "",
|
|
23
|
+
stderr: "",
|
|
24
|
+
timedOut: false,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function createConfig(
|
|
29
|
+
overrides: Partial<InlineScriptConfig> = {},
|
|
30
|
+
): InlineScriptConfig {
|
|
31
|
+
return {
|
|
32
|
+
script: "return { success: true };",
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("InlineScriptCollector", () => {
|
|
39
|
+
const collector = new InlineScriptCollector();
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────
|
|
42
|
+
// Metadata
|
|
43
|
+
// ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("metadata", () => {
|
|
46
|
+
it("has correct basic metadata", () => {
|
|
47
|
+
expect(collector.id).toBe("inline-script");
|
|
48
|
+
expect(collector.displayName).toBe("Inline Script");
|
|
49
|
+
expect(collector.description).toContain("TypeScript");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("allows multiple instances", () => {
|
|
53
|
+
expect(collector.allowMultiple).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────
|
|
58
|
+
// Basic Execution
|
|
59
|
+
// ─────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
describe("execute - basic", () => {
|
|
62
|
+
it("executes script returning success object", async () => {
|
|
63
|
+
const config = createConfig({
|
|
64
|
+
script: 'return { success: true, message: "All good" };',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await collector.execute({
|
|
68
|
+
config,
|
|
69
|
+
client: mockClient,
|
|
70
|
+
pluginId: "script",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.result.success).toBe(true);
|
|
74
|
+
expect(result.result.message).toBe("All good");
|
|
75
|
+
expect(result.error).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("executes script returning boolean", async () => {
|
|
79
|
+
const config = createConfig({
|
|
80
|
+
script: "return true;",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = await collector.execute({
|
|
84
|
+
config,
|
|
85
|
+
client: mockClient,
|
|
86
|
+
pluginId: "script",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.result.success).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("executes script with no return (defaults to success)", async () => {
|
|
93
|
+
const config = createConfig({
|
|
94
|
+
script: "const x = 1 + 1;",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await collector.execute({
|
|
98
|
+
config,
|
|
99
|
+
client: mockClient,
|
|
100
|
+
pluginId: "script",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.result.success).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("executes async script", async () => {
|
|
107
|
+
const config = createConfig({
|
|
108
|
+
script: `
|
|
109
|
+
await Promise.resolve();
|
|
110
|
+
return { success: true, message: "async works" };
|
|
111
|
+
`,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await collector.execute({
|
|
115
|
+
config,
|
|
116
|
+
client: mockClient,
|
|
117
|
+
pluginId: "script",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.result.success).toBe(true);
|
|
121
|
+
expect(result.result.message).toBe("async works");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("captures numeric value from script", async () => {
|
|
125
|
+
const config = createConfig({
|
|
126
|
+
script: "return { success: true, value: 42 };",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await collector.execute({
|
|
130
|
+
config,
|
|
131
|
+
client: mockClient,
|
|
132
|
+
pluginId: "script",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.result.value).toBe(42);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─────────────────────────────────────────────────────────────
|
|
140
|
+
// Failure Handling
|
|
141
|
+
// ─────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
describe("execute - failures", () => {
|
|
144
|
+
it("handles script returning false", async () => {
|
|
145
|
+
const config = createConfig({
|
|
146
|
+
script: "return false;",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = await collector.execute({
|
|
150
|
+
config,
|
|
151
|
+
client: mockClient,
|
|
152
|
+
pluginId: "script",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.result.success).toBe(false);
|
|
156
|
+
expect(result.error).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("handles script returning success: false", async () => {
|
|
160
|
+
const config = createConfig({
|
|
161
|
+
script: 'return { success: false, message: "Check failed" };',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = await collector.execute({
|
|
165
|
+
config,
|
|
166
|
+
client: mockClient,
|
|
167
|
+
pluginId: "script",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(result.result.success).toBe(false);
|
|
171
|
+
expect(result.result.message).toBe("Check failed");
|
|
172
|
+
expect(result.error).toBe("Check failed");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("handles script errors", async () => {
|
|
176
|
+
const config = createConfig({
|
|
177
|
+
script: 'throw new Error("Something went wrong");',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await collector.execute({
|
|
181
|
+
config,
|
|
182
|
+
client: mockClient,
|
|
183
|
+
pluginId: "script",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.result.success).toBe(false);
|
|
187
|
+
expect(result.error).toContain("Something went wrong");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("handles syntax errors", async () => {
|
|
191
|
+
const config = createConfig({
|
|
192
|
+
script: "const x = {",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await collector.execute({
|
|
196
|
+
config,
|
|
197
|
+
client: mockClient,
|
|
198
|
+
pluginId: "script",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.result.success).toBe(false);
|
|
202
|
+
expect(result.error).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─────────────────────────────────────────────────────────────
|
|
207
|
+
// Timeout
|
|
208
|
+
// ─────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
describe("execute - timeout", () => {
|
|
211
|
+
it("times out long-running scripts", async () => {
|
|
212
|
+
const config = createConfig({
|
|
213
|
+
script: `
|
|
214
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
215
|
+
return { success: true };
|
|
216
|
+
`,
|
|
217
|
+
timeout: 1000,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await collector.execute({
|
|
221
|
+
config,
|
|
222
|
+
client: mockClient,
|
|
223
|
+
pluginId: "script",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.result.success).toBe(false);
|
|
227
|
+
expect(result.result.timedOut).toBe(true);
|
|
228
|
+
expect(result.error).toContain("timed out");
|
|
229
|
+
}, 10000);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────
|
|
233
|
+
// Aggregation
|
|
234
|
+
// ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe("mergeResult", () => {
|
|
237
|
+
it("aggregates execution time and success rate", () => {
|
|
238
|
+
const run1 = {
|
|
239
|
+
metadata: { success: true, executionTimeMs: 100, timedOut: false },
|
|
240
|
+
};
|
|
241
|
+
const run2 = {
|
|
242
|
+
metadata: { success: true, executionTimeMs: 200, timedOut: false },
|
|
243
|
+
};
|
|
244
|
+
const run3 = {
|
|
245
|
+
metadata: { success: false, executionTimeMs: 150, timedOut: false },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
let aggregated = collector.mergeResult(undefined, run1 as never);
|
|
249
|
+
aggregated = collector.mergeResult(aggregated, run2 as never);
|
|
250
|
+
aggregated = collector.mergeResult(aggregated, run3 as never);
|
|
251
|
+
|
|
252
|
+
expect(aggregated.avgExecutionTimeMs.avg).toBe(150); // (100+200+150)/3
|
|
253
|
+
expect(aggregated.successRate.rate).toBeCloseTo(67, 0); // 2/3 * 100 = ~67
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
configString,
|
|
5
|
+
configNumber,
|
|
6
|
+
type HealthCheckRunForAggregation,
|
|
7
|
+
type CollectorResult,
|
|
8
|
+
type CollectorStrategy,
|
|
9
|
+
mergeAverage,
|
|
10
|
+
mergeRate,
|
|
11
|
+
VersionedAggregated,
|
|
12
|
+
aggregatedAverage,
|
|
13
|
+
aggregatedRate,
|
|
14
|
+
type InferAggregatedResult,
|
|
15
|
+
} from "@checkstack/backend-api";
|
|
16
|
+
import {
|
|
17
|
+
healthResultNumber,
|
|
18
|
+
healthResultString,
|
|
19
|
+
healthResultBoolean,
|
|
20
|
+
healthResultSchema,
|
|
21
|
+
} from "@checkstack/healthcheck-common";
|
|
22
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
23
|
+
import type { ScriptTransportClient } from "./transport-client";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// SCRIPT EXECUTION UTILITIES (shared with integration-script-backend pattern)
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context available to inline scripts.
|
|
31
|
+
*/
|
|
32
|
+
interface ScriptContext {
|
|
33
|
+
/** Health check configuration */
|
|
34
|
+
config: Record<string, unknown>;
|
|
35
|
+
/** Fetch API for HTTP requests */
|
|
36
|
+
fetch: typeof fetch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Safe console interface for scripts.
|
|
41
|
+
*/
|
|
42
|
+
interface SafeConsole {
|
|
43
|
+
log: (...args: unknown[]) => void;
|
|
44
|
+
warn: (...args: unknown[]) => void;
|
|
45
|
+
error: (...args: unknown[]) => void;
|
|
46
|
+
info: (...args: unknown[]) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Expected return type from health check scripts.
|
|
51
|
+
*/
|
|
52
|
+
interface ScriptHealthResult {
|
|
53
|
+
/** Whether the health check passed */
|
|
54
|
+
success: boolean;
|
|
55
|
+
/** Optional message describing the result */
|
|
56
|
+
message?: string;
|
|
57
|
+
/** Optional numeric value for metrics */
|
|
58
|
+
value?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute an inline script with the given context.
|
|
63
|
+
*/
|
|
64
|
+
async function executeInlineScript({
|
|
65
|
+
script,
|
|
66
|
+
context,
|
|
67
|
+
safeConsole,
|
|
68
|
+
timeoutMs,
|
|
69
|
+
}: {
|
|
70
|
+
script: string;
|
|
71
|
+
context: ScriptContext;
|
|
72
|
+
safeConsole: SafeConsole;
|
|
73
|
+
timeoutMs: number;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
result: ScriptHealthResult | undefined;
|
|
76
|
+
error?: string;
|
|
77
|
+
timedOut: boolean;
|
|
78
|
+
}> {
|
|
79
|
+
try {
|
|
80
|
+
// Create an async function from the script
|
|
81
|
+
const asyncFn = new Function(
|
|
82
|
+
"context",
|
|
83
|
+
"console",
|
|
84
|
+
"fetch",
|
|
85
|
+
`return (async () => { ${script} })();`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Execute with timeout
|
|
89
|
+
const result = await Promise.race([
|
|
90
|
+
asyncFn(context, safeConsole, fetch),
|
|
91
|
+
new Promise<never>((_, reject) =>
|
|
92
|
+
setTimeout(() => reject(new Error("__TIMEOUT__")), timeoutMs),
|
|
93
|
+
),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// Normalize result
|
|
97
|
+
if (result === undefined || result === null) {
|
|
98
|
+
return { result: { success: true }, timedOut: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof result === "boolean") {
|
|
102
|
+
return { result: { success: result }, timedOut: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof result === "object") {
|
|
106
|
+
return {
|
|
107
|
+
result: {
|
|
108
|
+
success: Boolean(result.success ?? true),
|
|
109
|
+
message: result.message,
|
|
110
|
+
value: result.value,
|
|
111
|
+
},
|
|
112
|
+
timedOut: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
result: { success: true, message: String(result) },
|
|
118
|
+
timedOut: false,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
if (message === "__TIMEOUT__") {
|
|
123
|
+
return {
|
|
124
|
+
result: undefined,
|
|
125
|
+
error: "Script execution timed out",
|
|
126
|
+
timedOut: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { result: undefined, error: message, timedOut: false };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// CONFIGURATION SCHEMA
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
const inlineScriptConfigSchema = z.object({
|
|
138
|
+
script: configString({
|
|
139
|
+
"x-editor-types": ["typescript"],
|
|
140
|
+
}).describe(
|
|
141
|
+
"TypeScript/JavaScript code to execute. Return { success: boolean, message?: string, value?: number }",
|
|
142
|
+
),
|
|
143
|
+
timeout: configNumber({})
|
|
144
|
+
.min(1000)
|
|
145
|
+
.max(60_000)
|
|
146
|
+
.default(10_000)
|
|
147
|
+
.describe("Maximum execution time in milliseconds"),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export type InlineScriptConfig = z.infer<typeof inlineScriptConfigSchema>;
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// RESULT SCHEMAS
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
const inlineScriptResultSchema = healthResultSchema({
|
|
157
|
+
success: healthResultBoolean({
|
|
158
|
+
"x-chart-type": "boolean",
|
|
159
|
+
"x-chart-label": "Success",
|
|
160
|
+
}),
|
|
161
|
+
message: healthResultString({
|
|
162
|
+
"x-chart-type": "text",
|
|
163
|
+
"x-chart-label": "Message",
|
|
164
|
+
}).optional(),
|
|
165
|
+
value: healthResultNumber({
|
|
166
|
+
"x-chart-type": "line",
|
|
167
|
+
"x-chart-label": "Value",
|
|
168
|
+
}).optional(),
|
|
169
|
+
executionTimeMs: healthResultNumber({
|
|
170
|
+
"x-chart-type": "line",
|
|
171
|
+
"x-chart-label": "Execution Time",
|
|
172
|
+
"x-chart-unit": "ms",
|
|
173
|
+
}),
|
|
174
|
+
timedOut: healthResultBoolean({
|
|
175
|
+
"x-chart-type": "boolean",
|
|
176
|
+
"x-chart-label": "Timed Out",
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export type InlineScriptResult = z.infer<typeof inlineScriptResultSchema>;
|
|
181
|
+
|
|
182
|
+
// Aggregated result fields definition
|
|
183
|
+
const inlineScriptAggregatedFields = {
|
|
184
|
+
avgExecutionTimeMs: aggregatedAverage({
|
|
185
|
+
"x-chart-type": "line",
|
|
186
|
+
"x-chart-label": "Avg Execution Time",
|
|
187
|
+
"x-chart-unit": "ms",
|
|
188
|
+
}),
|
|
189
|
+
successRate: aggregatedRate({
|
|
190
|
+
"x-chart-type": "gauge",
|
|
191
|
+
"x-chart-label": "Success Rate",
|
|
192
|
+
"x-chart-unit": "%",
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Type inferred from field definitions
|
|
197
|
+
export type InlineScriptAggregatedResult = InferAggregatedResult<
|
|
198
|
+
typeof inlineScriptAggregatedFields
|
|
199
|
+
>;
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// INLINE SCRIPT COLLECTOR
|
|
203
|
+
// ============================================================================
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Inline Script collector for health checks.
|
|
207
|
+
* Executes TypeScript/JavaScript code directly and checks the result.
|
|
208
|
+
*
|
|
209
|
+
* Scripts should return an object with:
|
|
210
|
+
* - success: boolean - Whether the check passed
|
|
211
|
+
* - message?: string - Optional status message
|
|
212
|
+
* - value?: number - Optional numeric value for metrics
|
|
213
|
+
*
|
|
214
|
+
* Scripts have access to:
|
|
215
|
+
* - context.config - The collector configuration
|
|
216
|
+
* - console.log/warn/error - Logging functions
|
|
217
|
+
* - fetch - HTTP client for making requests
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* // Simple check
|
|
222
|
+
* return { success: true, message: "All good!" };
|
|
223
|
+
*
|
|
224
|
+
* // HTTP health check
|
|
225
|
+
* const response = await fetch("https://api.example.com/health");
|
|
226
|
+
* return {
|
|
227
|
+
* success: response.ok,
|
|
228
|
+
* message: `Status: ${response.status}`,
|
|
229
|
+
* value: response.status
|
|
230
|
+
* };
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export class InlineScriptCollector implements CollectorStrategy<
|
|
234
|
+
ScriptTransportClient,
|
|
235
|
+
InlineScriptConfig,
|
|
236
|
+
InlineScriptResult,
|
|
237
|
+
InlineScriptAggregatedResult
|
|
238
|
+
> {
|
|
239
|
+
id = "inline-script";
|
|
240
|
+
displayName = "Inline Script";
|
|
241
|
+
description = "Execute TypeScript/JavaScript code for health checking";
|
|
242
|
+
|
|
243
|
+
supportedPlugins = [pluginMetadata];
|
|
244
|
+
|
|
245
|
+
allowMultiple = true;
|
|
246
|
+
|
|
247
|
+
config = new Versioned({ version: 1, schema: inlineScriptConfigSchema });
|
|
248
|
+
result = new Versioned({ version: 1, schema: inlineScriptResultSchema });
|
|
249
|
+
aggregatedResult = new VersionedAggregated({
|
|
250
|
+
version: 1,
|
|
251
|
+
fields: inlineScriptAggregatedFields,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
async execute({
|
|
255
|
+
config,
|
|
256
|
+
}: {
|
|
257
|
+
config: InlineScriptConfig;
|
|
258
|
+
client: ScriptTransportClient;
|
|
259
|
+
pluginId: string;
|
|
260
|
+
}): Promise<CollectorResult<InlineScriptResult>> {
|
|
261
|
+
const startTime = Date.now();
|
|
262
|
+
|
|
263
|
+
// Build context for the script
|
|
264
|
+
const scriptContext: ScriptContext = {
|
|
265
|
+
config: config as unknown as Record<string, unknown>,
|
|
266
|
+
fetch,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Create a safe console that captures logs
|
|
270
|
+
const logs: string[] = [];
|
|
271
|
+
const safeConsole: SafeConsole = {
|
|
272
|
+
log: (...args) => {
|
|
273
|
+
logs.push(
|
|
274
|
+
args
|
|
275
|
+
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
|
|
276
|
+
.join(" "),
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
warn: (...args) => {
|
|
280
|
+
logs.push(
|
|
281
|
+
`[WARN] ${args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ")}`,
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
error: (...args) => {
|
|
285
|
+
logs.push(
|
|
286
|
+
`[ERROR] ${args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ")}`,
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
info: (...args) => {
|
|
290
|
+
logs.push(
|
|
291
|
+
`[INFO] ${args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ")}`,
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Execute the script
|
|
297
|
+
const { result, error, timedOut } = await executeInlineScript({
|
|
298
|
+
script: config.script,
|
|
299
|
+
context: scriptContext,
|
|
300
|
+
safeConsole,
|
|
301
|
+
timeoutMs: config.timeout,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const executionTimeMs = Date.now() - startTime;
|
|
305
|
+
|
|
306
|
+
if (error) {
|
|
307
|
+
return {
|
|
308
|
+
result: {
|
|
309
|
+
success: false,
|
|
310
|
+
message: error,
|
|
311
|
+
executionTimeMs,
|
|
312
|
+
timedOut,
|
|
313
|
+
},
|
|
314
|
+
error,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
result: {
|
|
320
|
+
success: result?.success ?? true,
|
|
321
|
+
message:
|
|
322
|
+
result?.message ?? (logs.length > 0 ? logs.join("\n") : undefined),
|
|
323
|
+
value: result?.value,
|
|
324
|
+
executionTimeMs,
|
|
325
|
+
timedOut: false,
|
|
326
|
+
},
|
|
327
|
+
error:
|
|
328
|
+
result?.success === false
|
|
329
|
+
? (result.message ?? "Check failed")
|
|
330
|
+
: undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
mergeResult(
|
|
335
|
+
existing: InlineScriptAggregatedResult | undefined,
|
|
336
|
+
run: HealthCheckRunForAggregation<InlineScriptResult>,
|
|
337
|
+
): InlineScriptAggregatedResult {
|
|
338
|
+
const metadata = run.metadata;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
avgExecutionTimeMs: mergeAverage(
|
|
342
|
+
existing?.avgExecutionTimeMs,
|
|
343
|
+
metadata?.executionTimeMs,
|
|
344
|
+
),
|
|
345
|
+
successRate: mergeRate(existing?.successRate, metadata?.success),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -138,7 +138,7 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
describe("
|
|
141
|
+
describe("mergeResult", () => {
|
|
142
142
|
it("should calculate averages correctly", () => {
|
|
143
143
|
const strategy = new ScriptHealthCheckStrategy();
|
|
144
144
|
const runs = [
|
|
@@ -172,12 +172,13 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
172
172
|
},
|
|
173
173
|
];
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
176
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
176
177
|
|
|
177
|
-
expect(aggregated.avgExecutionTime).toBe(75);
|
|
178
|
-
expect(aggregated.successRate).toBe(100);
|
|
179
|
-
expect(aggregated.errorCount).toBe(0);
|
|
180
|
-
expect(aggregated.timeoutCount).toBe(0);
|
|
178
|
+
expect(aggregated.avgExecutionTime.avg).toBe(75);
|
|
179
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
180
|
+
expect(aggregated.errorCount.count).toBe(0);
|
|
181
|
+
expect(aggregated.timeoutCount.count).toBe(0);
|
|
181
182
|
});
|
|
182
183
|
|
|
183
184
|
it("should count errors and timeouts", () => {
|
|
@@ -213,11 +214,12 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
213
214
|
},
|
|
214
215
|
];
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
218
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
217
219
|
|
|
218
|
-
expect(aggregated.errorCount).toBe(1);
|
|
219
|
-
expect(aggregated.timeoutCount).toBe(1);
|
|
220
|
-
expect(aggregated.successRate).toBe(0);
|
|
220
|
+
expect(aggregated.errorCount.count).toBe(1);
|
|
221
|
+
expect(aggregated.timeoutCount.count).toBe(1);
|
|
222
|
+
expect(aggregated.successRate.rate).toBe(0);
|
|
221
223
|
});
|
|
222
224
|
});
|
|
223
225
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -3,8 +3,16 @@ import {
|
|
|
3
3
|
HealthCheckStrategy,
|
|
4
4
|
HealthCheckRunForAggregation,
|
|
5
5
|
Versioned,
|
|
6
|
+
VersionedAggregated,
|
|
7
|
+
aggregatedAverage,
|
|
8
|
+
aggregatedRate,
|
|
9
|
+
aggregatedCounter,
|
|
10
|
+
mergeAverage,
|
|
11
|
+
mergeRate,
|
|
12
|
+
mergeCounter,
|
|
6
13
|
z,
|
|
7
14
|
type ConnectedClient,
|
|
15
|
+
type InferAggregatedResult,
|
|
8
16
|
} from "@checkstack/backend-api";
|
|
9
17
|
import {
|
|
10
18
|
healthResultBoolean,
|
|
@@ -79,31 +87,31 @@ const scriptResultSchema = healthResultSchema({
|
|
|
79
87
|
|
|
80
88
|
type ScriptResult = z.infer<typeof scriptResultSchema>;
|
|
81
89
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const scriptAggregatedSchema = healthResultSchema({
|
|
86
|
-
avgExecutionTime: healthResultNumber({
|
|
90
|
+
/** Aggregated field definitions for bucket merging */
|
|
91
|
+
const scriptAggregatedFields = {
|
|
92
|
+
avgExecutionTime: aggregatedAverage({
|
|
87
93
|
"x-chart-type": "line",
|
|
88
94
|
"x-chart-label": "Avg Execution Time",
|
|
89
95
|
"x-chart-unit": "ms",
|
|
90
96
|
}),
|
|
91
|
-
successRate:
|
|
97
|
+
successRate: aggregatedRate({
|
|
92
98
|
"x-chart-type": "gauge",
|
|
93
99
|
"x-chart-label": "Success Rate",
|
|
94
100
|
"x-chart-unit": "%",
|
|
95
101
|
}),
|
|
96
|
-
errorCount:
|
|
102
|
+
errorCount: aggregatedCounter({
|
|
97
103
|
"x-chart-type": "counter",
|
|
98
104
|
"x-chart-label": "Errors",
|
|
99
105
|
}),
|
|
100
|
-
timeoutCount:
|
|
106
|
+
timeoutCount: aggregatedCounter({
|
|
101
107
|
"x-chart-type": "counter",
|
|
102
108
|
"x-chart-label": "Timeouts",
|
|
103
109
|
}),
|
|
104
|
-
}
|
|
110
|
+
};
|
|
105
111
|
|
|
106
|
-
type ScriptAggregatedResult =
|
|
112
|
+
type ScriptAggregatedResult = InferAggregatedResult<
|
|
113
|
+
typeof scriptAggregatedFields
|
|
114
|
+
>;
|
|
107
115
|
|
|
108
116
|
// ============================================================================
|
|
109
117
|
// SCRIPT EXECUTOR INTERFACE (for testability)
|
|
@@ -182,15 +190,12 @@ const defaultScriptExecutor: ScriptExecutor = {
|
|
|
182
190
|
// STRATEGY
|
|
183
191
|
// ============================================================================
|
|
184
192
|
|
|
185
|
-
export class ScriptHealthCheckStrategy
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
ScriptAggregatedResult
|
|
192
|
-
>
|
|
193
|
-
{
|
|
193
|
+
export class ScriptHealthCheckStrategy implements HealthCheckStrategy<
|
|
194
|
+
ScriptConfig,
|
|
195
|
+
ScriptTransportClient,
|
|
196
|
+
ScriptResult,
|
|
197
|
+
typeof scriptAggregatedFields
|
|
198
|
+
> {
|
|
194
199
|
id = "script";
|
|
195
200
|
displayName = "Script Health Check";
|
|
196
201
|
description = "Execute local scripts or commands for health checking";
|
|
@@ -229,59 +234,36 @@ export class ScriptHealthCheckStrategy
|
|
|
229
234
|
],
|
|
230
235
|
});
|
|
231
236
|
|
|
232
|
-
aggregatedResult
|
|
237
|
+
aggregatedResult = new VersionedAggregated({
|
|
233
238
|
version: 1,
|
|
234
|
-
|
|
239
|
+
fields: scriptAggregatedFields,
|
|
235
240
|
});
|
|
236
241
|
|
|
237
|
-
|
|
238
|
-
|
|
242
|
+
mergeResult(
|
|
243
|
+
existing: ScriptAggregatedResult | undefined,
|
|
244
|
+
run: HealthCheckRunForAggregation<ScriptResult>,
|
|
239
245
|
): ScriptAggregatedResult {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
if (validRuns.length === 0) {
|
|
243
|
-
return {
|
|
244
|
-
avgExecutionTime: 0,
|
|
245
|
-
successRate: 0,
|
|
246
|
-
errorCount: 0,
|
|
247
|
-
timeoutCount: 0,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
246
|
+
const metadata = run.metadata;
|
|
250
247
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
const avgExecutionTime = mergeAverage(
|
|
249
|
+
existing?.avgExecutionTime,
|
|
250
|
+
metadata?.executionTimeMs,
|
|
251
|
+
);
|
|
254
252
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
? Math.round(
|
|
258
|
-
executionTimes.reduce((a, b) => a + b, 0) / executionTimes.length
|
|
259
|
-
)
|
|
260
|
-
: 0;
|
|
253
|
+
const isSuccess = metadata?.success ?? false;
|
|
254
|
+
const successRate = mergeRate(existing?.successRate, isSuccess);
|
|
261
255
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
).length;
|
|
265
|
-
const successRate = Math.round((successCount / validRuns.length) * 100);
|
|
256
|
+
const hasError = metadata?.error !== undefined;
|
|
257
|
+
const errorCount = mergeCounter(existing?.errorCount, hasError);
|
|
266
258
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
).length;
|
|
259
|
+
const hasTimeout = metadata?.timedOut === true;
|
|
260
|
+
const timeoutCount = mergeCounter(existing?.timeoutCount, hasTimeout);
|
|
270
261
|
|
|
271
|
-
|
|
272
|
-
(r) => r.metadata?.timedOut === true
|
|
273
|
-
).length;
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
avgExecutionTime,
|
|
277
|
-
successRate,
|
|
278
|
-
errorCount,
|
|
279
|
-
timeoutCount,
|
|
280
|
-
};
|
|
262
|
+
return { avgExecutionTime, successRate, errorCount, timeoutCount };
|
|
281
263
|
}
|
|
282
264
|
|
|
283
265
|
async createClient(
|
|
284
|
-
_config: ScriptConfigInput
|
|
266
|
+
_config: ScriptConfigInput,
|
|
285
267
|
): Promise<ConnectedClient<ScriptTransportClient>> {
|
|
286
268
|
const client: ScriptTransportClient = {
|
|
287
269
|
exec: async (request: ScriptRequest): Promise<ScriptResultType> => {
|