@checkstack/healthcheck-ssh-backend 0.1.14 → 0.2.1
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 +48 -0
- package/package.json +5 -5
- package/src/command-collector.test.ts +3 -3
- package/src/command-collector.ts +20 -36
- package/src/strategy.test.ts +5 -5
- package/src/strategy.ts +31 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @checkstack/healthcheck-ssh-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 869b4ab: ## Health Check Execution Improvements
|
|
8
|
+
|
|
9
|
+
### Breaking Changes (backend-api)
|
|
10
|
+
|
|
11
|
+
- `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
|
|
16
|
+
- **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
|
|
17
|
+
- **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
|
|
21
|
+
- Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
|
|
22
|
+
- Simplified registry type signatures by using default type parameters.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [869b4ab]
|
|
25
|
+
- @checkstack/backend-api@0.8.0
|
|
26
|
+
|
|
27
|
+
## 0.2.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
32
|
+
|
|
33
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
34
|
+
|
|
35
|
+
**Key changes:**
|
|
36
|
+
|
|
37
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
38
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
39
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
40
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
41
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
42
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
43
|
+
|
|
44
|
+
**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.
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- Updated dependencies [3dd1914]
|
|
49
|
+
- @checkstack/backend-api@0.7.0
|
|
50
|
+
|
|
3
51
|
## 0.1.14
|
|
4
52
|
|
|
5
53
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-ssh-backend",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/backend-api": "0.
|
|
13
|
-
"@checkstack/common": "0.6.
|
|
14
|
-
"@checkstack/healthcheck-common": "0.8.
|
|
15
|
-
"@checkstack/healthcheck-ssh-common": "0.1.
|
|
12
|
+
"@checkstack/backend-api": "0.7.0",
|
|
13
|
+
"@checkstack/common": "0.6.2",
|
|
14
|
+
"@checkstack/healthcheck-common": "0.8.2",
|
|
15
|
+
"@checkstack/healthcheck-ssh-common": "0.1.8",
|
|
16
16
|
"ssh2": "^1.15.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
@@ -104,8 +104,8 @@ describe("CommandCollector", () => {
|
|
|
104
104
|
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
105
105
|
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
106
106
|
|
|
107
|
-
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
108
|
-
expect(aggregated.successRate).toBe(100);
|
|
107
|
+
expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
|
|
108
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
it("should calculate success rate based on exit codes", () => {
|
|
@@ -142,7 +142,7 @@ describe("CommandCollector", () => {
|
|
|
142
142
|
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
143
143
|
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
144
144
|
|
|
145
|
-
expect(aggregated.successRate).toBe(50);
|
|
145
|
+
expect(aggregated.successRate.rate).toBe(50);
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
148
|
|
package/src/command-collector.ts
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
7
|
mergeAverage,
|
|
8
|
-
averageStateSchema,
|
|
9
8
|
mergeRate,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
VersionedAggregated,
|
|
10
|
+
aggregatedAverage,
|
|
11
|
+
aggregatedRate,
|
|
12
|
+
type InferAggregatedResult,
|
|
13
13
|
} from "@checkstack/backend-api";
|
|
14
14
|
import {
|
|
15
15
|
healthResultNumber,
|
|
@@ -55,31 +55,24 @@ const commandResultSchema = healthResultSchema({
|
|
|
55
55
|
|
|
56
56
|
export type CommandResult = z.infer<typeof commandResultSchema>;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Aggregated result fields definition
|
|
59
|
+
const commandAggregatedFields = {
|
|
60
|
+
avgExecutionTimeMs: aggregatedAverage({
|
|
60
61
|
"x-chart-type": "line",
|
|
61
62
|
"x-chart-label": "Avg Execution Time",
|
|
62
63
|
"x-chart-unit": "ms",
|
|
63
64
|
}),
|
|
64
|
-
successRate:
|
|
65
|
+
successRate: aggregatedRate({
|
|
65
66
|
"x-chart-type": "gauge",
|
|
66
67
|
"x-chart-label": "Success Rate",
|
|
67
68
|
"x-chart-unit": "%",
|
|
68
69
|
}),
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const commandAggregatedInternalSchema = z.object({
|
|
72
|
-
_executionTime: averageStateSchema
|
|
73
|
-
.optional(),
|
|
74
|
-
_success: rateStateSchema
|
|
75
|
-
.optional(),
|
|
76
|
-
});
|
|
70
|
+
};
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
72
|
+
// Type inferred from field definitions
|
|
73
|
+
export type CommandAggregatedResult = InferAggregatedResult<
|
|
74
|
+
typeof commandAggregatedFields
|
|
75
|
+
>;
|
|
83
76
|
|
|
84
77
|
// ============================================================================
|
|
85
78
|
// COMMAND COLLECTOR (PSEUDO-COLLECTOR)
|
|
@@ -112,9 +105,9 @@ export class CommandCollector implements CollectorStrategy<
|
|
|
112
105
|
|
|
113
106
|
config = new Versioned({ version: 1, schema: commandConfigSchema });
|
|
114
107
|
result = new Versioned({ version: 1, schema: commandResultSchema });
|
|
115
|
-
aggregatedResult = new
|
|
108
|
+
aggregatedResult = new VersionedAggregated({
|
|
116
109
|
version: 1,
|
|
117
|
-
|
|
110
|
+
fields: commandAggregatedFields,
|
|
118
111
|
});
|
|
119
112
|
|
|
120
113
|
async execute({
|
|
@@ -145,22 +138,13 @@ export class CommandCollector implements CollectorStrategy<
|
|
|
145
138
|
): CommandAggregatedResult {
|
|
146
139
|
const metadata = run.metadata;
|
|
147
140
|
|
|
148
|
-
const executionTimeState = mergeAverage(
|
|
149
|
-
existing?._executionTime as AverageState | undefined,
|
|
150
|
-
metadata?.executionTimeMs,
|
|
151
|
-
);
|
|
152
|
-
|
|
153
141
|
// Success is exit code 0
|
|
154
|
-
const successState = mergeRate(
|
|
155
|
-
existing?._success as RateState | undefined,
|
|
156
|
-
metadata?.exitCode === 0,
|
|
157
|
-
);
|
|
158
|
-
|
|
159
142
|
return {
|
|
160
|
-
avgExecutionTimeMs:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
143
|
+
avgExecutionTimeMs: mergeAverage(
|
|
144
|
+
existing?.avgExecutionTimeMs,
|
|
145
|
+
metadata?.executionTimeMs,
|
|
146
|
+
),
|
|
147
|
+
successRate: mergeRate(existing?.successRate, metadata?.exitCode === 0),
|
|
164
148
|
};
|
|
165
149
|
}
|
|
166
150
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -142,9 +142,9 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
142
142
|
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
143
143
|
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
144
144
|
|
|
145
|
-
expect(aggregated.avgConnectionTime).toBe(75);
|
|
146
|
-
expect(aggregated.successRate).toBe(100);
|
|
147
|
-
expect(aggregated.errorCount).toBe(0);
|
|
145
|
+
expect(aggregated.avgConnectionTime.avg).toBe(75);
|
|
146
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
147
|
+
expect(aggregated.errorCount.count).toBe(0);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
it("should count errors", () => {
|
|
@@ -164,8 +164,8 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
164
164
|
|
|
165
165
|
const aggregated = strategy.mergeResult(undefined, run);
|
|
166
166
|
|
|
167
|
-
expect(aggregated.errorCount).toBe(1);
|
|
168
|
-
expect(aggregated.successRate).toBe(0);
|
|
167
|
+
expect(aggregated.errorCount.count).toBe(1);
|
|
168
|
+
expect(aggregated.successRate.rate).toBe(0);
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
171
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -3,22 +3,20 @@ import {
|
|
|
3
3
|
HealthCheckStrategy,
|
|
4
4
|
HealthCheckRunForAggregation,
|
|
5
5
|
Versioned,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
VersionedAggregated,
|
|
7
|
+
aggregatedAverage,
|
|
8
|
+
aggregatedMinMax,
|
|
9
|
+
aggregatedRate,
|
|
10
|
+
aggregatedCounter,
|
|
10
11
|
mergeAverage,
|
|
11
|
-
averageStateSchema,
|
|
12
12
|
mergeRate,
|
|
13
|
-
rateStateSchema,
|
|
14
13
|
mergeCounter,
|
|
15
|
-
counterStateSchema,
|
|
16
14
|
mergeMinMax,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
|
|
15
|
+
z,
|
|
16
|
+
configString,
|
|
17
|
+
type ConnectedClient,
|
|
18
|
+
type InferAggregatedResult,
|
|
19
|
+
baseStrategyConfigSchema,
|
|
22
20
|
} from "@checkstack/backend-api";
|
|
23
21
|
import {
|
|
24
22
|
healthResultBoolean,
|
|
@@ -35,7 +33,7 @@ import type { SshTransportClient, SshCommandResult } from "./transport-client";
|
|
|
35
33
|
/**
|
|
36
34
|
* Configuration schema for SSH health checks.
|
|
37
35
|
*/
|
|
38
|
-
export const sshConfigSchema =
|
|
36
|
+
export const sshConfigSchema = baseStrategyConfigSchema.extend({
|
|
39
37
|
host: z.string().describe("SSH server hostname"),
|
|
40
38
|
port: z.number().int().min(1).max(65_535).default(22).describe("SSH port"),
|
|
41
39
|
username: z.string().describe("SSH username"),
|
|
@@ -48,10 +46,6 @@ export const sshConfigSchema = z.object({
|
|
|
48
46
|
passphrase: configString({ "x-secret": true })
|
|
49
47
|
.describe("Passphrase for private key")
|
|
50
48
|
.optional(),
|
|
51
|
-
timeout: configNumber({})
|
|
52
|
-
.min(100)
|
|
53
|
-
.default(10_000)
|
|
54
|
-
.describe("Connection timeout in milliseconds"),
|
|
55
49
|
});
|
|
56
50
|
|
|
57
51
|
export type SshConfig = z.infer<typeof sshConfigSchema>;
|
|
@@ -78,45 +72,30 @@ const sshResultSchema = healthResultSchema({
|
|
|
78
72
|
|
|
79
73
|
type SshResult = z.infer<typeof sshResultSchema>;
|
|
80
74
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const sshAggregatedDisplaySchema = healthResultSchema({
|
|
85
|
-
avgConnectionTime: healthResultNumber({
|
|
75
|
+
/** Aggregated field definitions for bucket merging */
|
|
76
|
+
const sshAggregatedFields = {
|
|
77
|
+
avgConnectionTime: aggregatedAverage({
|
|
86
78
|
"x-chart-type": "line",
|
|
87
79
|
"x-chart-label": "Avg Connection Time",
|
|
88
80
|
"x-chart-unit": "ms",
|
|
89
81
|
}),
|
|
90
|
-
maxConnectionTime:
|
|
82
|
+
maxConnectionTime: aggregatedMinMax({
|
|
91
83
|
"x-chart-type": "line",
|
|
92
84
|
"x-chart-label": "Max Connection Time",
|
|
93
85
|
"x-chart-unit": "ms",
|
|
94
86
|
}),
|
|
95
|
-
successRate:
|
|
87
|
+
successRate: aggregatedRate({
|
|
96
88
|
"x-chart-type": "gauge",
|
|
97
89
|
"x-chart-label": "Success Rate",
|
|
98
90
|
"x-chart-unit": "%",
|
|
99
91
|
}),
|
|
100
|
-
errorCount:
|
|
92
|
+
errorCount: aggregatedCounter({
|
|
101
93
|
"x-chart-type": "counter",
|
|
102
94
|
"x-chart-label": "Errors",
|
|
103
95
|
}),
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const sshAggregatedInternalSchema = z.object({
|
|
107
|
-
_connectionTime: averageStateSchema
|
|
108
|
-
.optional(),
|
|
109
|
-
_maxConnectionTime: minMaxStateSchema.optional(),
|
|
110
|
-
_success: rateStateSchema
|
|
111
|
-
.optional(),
|
|
112
|
-
_errors: counterStateSchema.optional(),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const sshAggregatedSchema = sshAggregatedDisplaySchema.merge(
|
|
116
|
-
sshAggregatedInternalSchema,
|
|
117
|
-
);
|
|
96
|
+
};
|
|
118
97
|
|
|
119
|
-
type SshAggregatedResult =
|
|
98
|
+
type SshAggregatedResult = InferAggregatedResult<typeof sshAggregatedFields>;
|
|
120
99
|
|
|
121
100
|
// ============================================================================
|
|
122
101
|
// SSH CLIENT INTERFACE (for testability)
|
|
@@ -207,7 +186,7 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
207
186
|
SshConfig,
|
|
208
187
|
SshTransportClient,
|
|
209
188
|
SshResult,
|
|
210
|
-
|
|
189
|
+
typeof sshAggregatedFields
|
|
211
190
|
> {
|
|
212
191
|
id = "ssh";
|
|
213
192
|
displayName = "SSH Health Check";
|
|
@@ -229,9 +208,9 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
229
208
|
schema: sshResultSchema,
|
|
230
209
|
});
|
|
231
210
|
|
|
232
|
-
aggregatedResult
|
|
211
|
+
aggregatedResult = new VersionedAggregated({
|
|
233
212
|
version: 1,
|
|
234
|
-
|
|
213
|
+
fields: sshAggregatedFields,
|
|
235
214
|
});
|
|
236
215
|
|
|
237
216
|
mergeResult(
|
|
@@ -240,36 +219,23 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
240
219
|
): SshAggregatedResult {
|
|
241
220
|
const metadata = run.metadata;
|
|
242
221
|
|
|
243
|
-
const
|
|
244
|
-
existing?.
|
|
222
|
+
const avgConnectionTime = mergeAverage(
|
|
223
|
+
existing?.avgConnectionTime,
|
|
245
224
|
metadata?.connectionTimeMs,
|
|
246
225
|
);
|
|
247
226
|
|
|
248
|
-
const
|
|
249
|
-
existing?.
|
|
227
|
+
const maxConnectionTime = mergeMinMax(
|
|
228
|
+
existing?.maxConnectionTime,
|
|
250
229
|
metadata?.connectionTimeMs,
|
|
251
230
|
);
|
|
252
231
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
metadata?.connected,
|
|
256
|
-
);
|
|
232
|
+
const isSuccess = metadata?.connected ?? false;
|
|
233
|
+
const successRate = mergeRate(existing?.successRate, isSuccess);
|
|
257
234
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
metadata?.error !== undefined,
|
|
261
|
-
);
|
|
235
|
+
const hasError = metadata?.error !== undefined;
|
|
236
|
+
const errorCount = mergeCounter(existing?.errorCount, hasError);
|
|
262
237
|
|
|
263
|
-
return {
|
|
264
|
-
avgConnectionTime: connectionTimeState.avg,
|
|
265
|
-
maxConnectionTime: maxConnectionTimeState.max,
|
|
266
|
-
successRate: successState.rate,
|
|
267
|
-
errorCount: errorState.count,
|
|
268
|
-
_connectionTime: connectionTimeState,
|
|
269
|
-
_maxConnectionTime: maxConnectionTimeState,
|
|
270
|
-
_success: successState,
|
|
271
|
-
_errors: errorState,
|
|
272
|
-
};
|
|
238
|
+
return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
|
|
273
239
|
}
|
|
274
240
|
|
|
275
241
|
/**
|