@checkstack/healthcheck-ssh-backend 0.1.13 → 0.2.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 +55 -0
- package/package.json +7 -7
- package/src/command-collector.test.ts +8 -6
- package/src/command-collector.ts +33 -35
- package/src/strategy.test.ts +27 -28
- package/src/strategy.ts +48 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @checkstack/healthcheck-ssh-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.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.1.14
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
32
|
+
|
|
33
|
+
### Breaking Changes (Internal)
|
|
34
|
+
|
|
35
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
36
|
+
|
|
37
|
+
### New Features
|
|
38
|
+
|
|
39
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
40
|
+
- `mergeCounter()` - track occurrences
|
|
41
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
42
|
+
- `mergeRate()` - track success/total, compute %
|
|
43
|
+
- `mergeMinMax()` - track min/max values
|
|
44
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
45
|
+
|
|
46
|
+
### Improvements
|
|
47
|
+
|
|
48
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
49
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [f676e11]
|
|
52
|
+
- Updated dependencies [48c2080]
|
|
53
|
+
- @checkstack/common@0.6.2
|
|
54
|
+
- @checkstack/backend-api@0.6.0
|
|
55
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
56
|
+
- @checkstack/healthcheck-ssh-common@0.1.8
|
|
57
|
+
|
|
3
58
|
## 0.1.13
|
|
4
59
|
|
|
5
60
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-ssh-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,17 +9,17 @@
|
|
|
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.
|
|
15
|
-
"@checkstack/healthcheck-ssh-common": "0.1.
|
|
12
|
+
"@checkstack/backend-api": "0.5.2",
|
|
13
|
+
"@checkstack/common": "0.6.1",
|
|
14
|
+
"@checkstack/healthcheck-common": "0.8.1",
|
|
15
|
+
"@checkstack/healthcheck-ssh-common": "0.1.7",
|
|
16
16
|
"ssh2": "^1.15.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/bun": "^1.0.0",
|
|
20
20
|
"@types/ssh2": "^1.15.0",
|
|
21
21
|
"typescript": "^5.0.0",
|
|
22
|
-
"@checkstack/tsconfig": "0.0.
|
|
23
|
-
"@checkstack/scripts": "0.1.
|
|
22
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
23
|
+
"@checkstack/scripts": "0.1.1"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -69,7 +69,7 @@ describe("CommandCollector", () => {
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
describe("
|
|
72
|
+
describe("mergeResult", () => {
|
|
73
73
|
it("should calculate average execution time and success rate", () => {
|
|
74
74
|
const collector = new CommandCollector();
|
|
75
75
|
const runs = [
|
|
@@ -101,10 +101,11 @@ describe("CommandCollector", () => {
|
|
|
101
101
|
},
|
|
102
102
|
];
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
105
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
105
106
|
|
|
106
|
-
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
107
|
-
expect(aggregated.successRate).toBe(100);
|
|
107
|
+
expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
|
|
108
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
108
109
|
});
|
|
109
110
|
|
|
110
111
|
it("should calculate success rate based on exit codes", () => {
|
|
@@ -138,9 +139,10 @@ describe("CommandCollector", () => {
|
|
|
138
139
|
},
|
|
139
140
|
];
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
143
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
142
144
|
|
|
143
|
-
expect(aggregated.successRate).toBe(50);
|
|
145
|
+
expect(aggregated.successRate.rate).toBe(50);
|
|
144
146
|
});
|
|
145
147
|
});
|
|
146
148
|
|
package/src/command-collector.ts
CHANGED
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
mergeRate,
|
|
9
|
+
VersionedAggregated,
|
|
10
|
+
aggregatedAverage,
|
|
11
|
+
aggregatedRate,
|
|
12
|
+
type InferAggregatedResult,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import {
|
|
9
15
|
healthResultNumber,
|
|
@@ -49,20 +55,24 @@ const commandResultSchema = healthResultSchema({
|
|
|
49
55
|
|
|
50
56
|
export type CommandResult = z.infer<typeof commandResultSchema>;
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
// Aggregated result fields definition
|
|
59
|
+
const commandAggregatedFields = {
|
|
60
|
+
avgExecutionTimeMs: aggregatedAverage({
|
|
54
61
|
"x-chart-type": "line",
|
|
55
62
|
"x-chart-label": "Avg Execution Time",
|
|
56
63
|
"x-chart-unit": "ms",
|
|
57
64
|
}),
|
|
58
|
-
successRate:
|
|
65
|
+
successRate: aggregatedRate({
|
|
59
66
|
"x-chart-type": "gauge",
|
|
60
67
|
"x-chart-label": "Success Rate",
|
|
61
68
|
"x-chart-unit": "%",
|
|
62
69
|
}),
|
|
63
|
-
}
|
|
70
|
+
};
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
// Type inferred from field definitions
|
|
73
|
+
export type CommandAggregatedResult = InferAggregatedResult<
|
|
74
|
+
typeof commandAggregatedFields
|
|
75
|
+
>;
|
|
66
76
|
|
|
67
77
|
// ============================================================================
|
|
68
78
|
// COMMAND COLLECTOR (PSEUDO-COLLECTOR)
|
|
@@ -73,15 +83,12 @@ export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
|
73
83
|
* Allows users to run arbitrary shell commands as check items.
|
|
74
84
|
* This is the "basic mode" functionality exposed as a collector.
|
|
75
85
|
*/
|
|
76
|
-
export class CommandCollector
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
CommandAggregatedResult
|
|
83
|
-
>
|
|
84
|
-
{
|
|
86
|
+
export class CommandCollector implements CollectorStrategy<
|
|
87
|
+
SshTransportClient,
|
|
88
|
+
CommandConfig,
|
|
89
|
+
CommandResult,
|
|
90
|
+
CommandAggregatedResult
|
|
91
|
+
> {
|
|
85
92
|
/**
|
|
86
93
|
* ID for this collector.
|
|
87
94
|
* Built-in collectors are identified by ownerPlugin matching the strategy's plugin.
|
|
@@ -98,9 +105,9 @@ export class CommandCollector
|
|
|
98
105
|
|
|
99
106
|
config = new Versioned({ version: 1, schema: commandConfigSchema });
|
|
100
107
|
result = new Versioned({ version: 1, schema: commandResultSchema });
|
|
101
|
-
aggregatedResult = new
|
|
108
|
+
aggregatedResult = new VersionedAggregated({
|
|
102
109
|
version: 1,
|
|
103
|
-
|
|
110
|
+
fields: commandAggregatedFields,
|
|
104
111
|
});
|
|
105
112
|
|
|
106
113
|
async execute({
|
|
@@ -125,28 +132,19 @@ export class CommandCollector
|
|
|
125
132
|
};
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
mergeResult(
|
|
136
|
+
existing: CommandAggregatedResult | undefined,
|
|
137
|
+
run: HealthCheckRunForAggregation<CommandResult>,
|
|
130
138
|
): CommandAggregatedResult {
|
|
131
|
-
const
|
|
132
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
133
|
-
.filter((v): v is number => typeof v === "number");
|
|
134
|
-
|
|
135
|
-
const exitCodes = runs
|
|
136
|
-
.map((r) => r.metadata?.exitCode)
|
|
137
|
-
.filter((v): v is number => typeof v === "number");
|
|
138
|
-
|
|
139
|
-
const successCount = exitCodes.filter((code) => code === 0).length;
|
|
139
|
+
const metadata = run.metadata;
|
|
140
140
|
|
|
141
|
+
// Success is exit code 0
|
|
141
142
|
return {
|
|
142
|
-
avgExecutionTimeMs:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
successRate:
|
|
147
|
-
exitCodes.length > 0
|
|
148
|
-
? Math.round((successCount / exitCodes.length) * 100)
|
|
149
|
-
: 0,
|
|
143
|
+
avgExecutionTimeMs: mergeAverage(
|
|
144
|
+
existing?.avgExecutionTimeMs,
|
|
145
|
+
metadata?.executionTimeMs,
|
|
146
|
+
),
|
|
147
|
+
successRate: mergeRate(existing?.successRate, metadata?.exitCode === 0),
|
|
150
148
|
};
|
|
151
149
|
}
|
|
152
150
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
10
10
|
stderr?: string;
|
|
11
11
|
execError?: Error;
|
|
12
12
|
connectError?: Error;
|
|
13
|
-
} = {}
|
|
13
|
+
} = {},
|
|
14
14
|
): SshClient => ({
|
|
15
15
|
connect: mock(() =>
|
|
16
16
|
config.connectError
|
|
@@ -23,10 +23,10 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
23
23
|
exitCode: config.exitCode ?? 0,
|
|
24
24
|
stdout: config.stdout ?? "",
|
|
25
25
|
stderr: config.stderr ?? "",
|
|
26
|
-
})
|
|
26
|
+
}),
|
|
27
27
|
),
|
|
28
28
|
end: mock(() => {}),
|
|
29
|
-
})
|
|
29
|
+
}),
|
|
30
30
|
),
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -51,7 +51,7 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
51
51
|
|
|
52
52
|
it("should throw for connection error", async () => {
|
|
53
53
|
const strategy = new SshHealthCheckStrategy(
|
|
54
|
-
createMockClient({ connectError: new Error("Connection refused") })
|
|
54
|
+
createMockClient({ connectError: new Error("Connection refused") }),
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
await expect(
|
|
@@ -61,7 +61,7 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
61
61
|
username: "user",
|
|
62
62
|
password: "secret",
|
|
63
63
|
timeout: 5000,
|
|
64
|
-
})
|
|
64
|
+
}),
|
|
65
65
|
).rejects.toThrow("Connection refused");
|
|
66
66
|
});
|
|
67
67
|
});
|
|
@@ -69,7 +69,7 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
69
69
|
describe("client.exec", () => {
|
|
70
70
|
it("should execute command successfully", async () => {
|
|
71
71
|
const strategy = new SshHealthCheckStrategy(
|
|
72
|
-
createMockClient({ exitCode: 0, stdout: "OK" })
|
|
72
|
+
createMockClient({ exitCode: 0, stdout: "OK" }),
|
|
73
73
|
);
|
|
74
74
|
const connectedClient = await strategy.createClient({
|
|
75
75
|
host: "localhost",
|
|
@@ -90,7 +90,7 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
90
90
|
|
|
91
91
|
it("should return non-zero exit code for failed command", async () => {
|
|
92
92
|
const strategy = new SshHealthCheckStrategy(
|
|
93
|
-
createMockClient({ exitCode: 1, stderr: "Error" })
|
|
93
|
+
createMockClient({ exitCode: 1, stderr: "Error" }),
|
|
94
94
|
);
|
|
95
95
|
const connectedClient = await strategy.createClient({
|
|
96
96
|
host: "localhost",
|
|
@@ -109,7 +109,7 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
describe("
|
|
112
|
+
describe("mergeResult", () => {
|
|
113
113
|
it("should calculate averages correctly", () => {
|
|
114
114
|
const strategy = new SshHealthCheckStrategy();
|
|
115
115
|
const runs = [
|
|
@@ -139,34 +139,33 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
139
139
|
},
|
|
140
140
|
];
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
143
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
143
144
|
|
|
144
|
-
expect(aggregated.avgConnectionTime).toBe(75);
|
|
145
|
-
expect(aggregated.successRate).toBe(100);
|
|
146
|
-
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);
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
it("should count errors", () => {
|
|
150
151
|
const strategy = new SshHealthCheckStrategy();
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
error: "Connection refused",
|
|
162
|
-
},
|
|
152
|
+
const run = {
|
|
153
|
+
id: "1",
|
|
154
|
+
status: "unhealthy" as const,
|
|
155
|
+
latencyMs: 100,
|
|
156
|
+
checkId: "c1",
|
|
157
|
+
timestamp: new Date(),
|
|
158
|
+
metadata: {
|
|
159
|
+
connected: false,
|
|
160
|
+
connectionTimeMs: 100,
|
|
161
|
+
error: "Connection refused",
|
|
163
162
|
},
|
|
164
|
-
|
|
163
|
+
};
|
|
165
164
|
|
|
166
|
-
const aggregated = strategy.
|
|
165
|
+
const aggregated = strategy.mergeResult(undefined, run);
|
|
167
166
|
|
|
168
|
-
expect(aggregated.errorCount).toBe(1);
|
|
169
|
-
expect(aggregated.successRate).toBe(0);
|
|
167
|
+
expect(aggregated.errorCount.count).toBe(1);
|
|
168
|
+
expect(aggregated.successRate.rate).toBe(0);
|
|
170
169
|
});
|
|
171
170
|
});
|
|
172
171
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -3,10 +3,20 @@ import {
|
|
|
3
3
|
HealthCheckStrategy,
|
|
4
4
|
HealthCheckRunForAggregation,
|
|
5
5
|
Versioned,
|
|
6
|
+
VersionedAggregated,
|
|
7
|
+
aggregatedAverage,
|
|
8
|
+
aggregatedMinMax,
|
|
9
|
+
aggregatedRate,
|
|
10
|
+
aggregatedCounter,
|
|
11
|
+
mergeAverage,
|
|
12
|
+
mergeRate,
|
|
13
|
+
mergeCounter,
|
|
14
|
+
mergeMinMax,
|
|
6
15
|
z,
|
|
7
16
|
configString,
|
|
8
17
|
configNumber,
|
|
9
18
|
type ConnectedClient,
|
|
19
|
+
type InferAggregatedResult,
|
|
10
20
|
} from "@checkstack/backend-api";
|
|
11
21
|
import {
|
|
12
22
|
healthResultBoolean,
|
|
@@ -66,32 +76,30 @@ const sshResultSchema = healthResultSchema({
|
|
|
66
76
|
|
|
67
77
|
type SshResult = z.infer<typeof sshResultSchema>;
|
|
68
78
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const sshAggregatedSchema = healthResultSchema({
|
|
73
|
-
avgConnectionTime: healthResultNumber({
|
|
79
|
+
/** Aggregated field definitions for bucket merging */
|
|
80
|
+
const sshAggregatedFields = {
|
|
81
|
+
avgConnectionTime: aggregatedAverage({
|
|
74
82
|
"x-chart-type": "line",
|
|
75
83
|
"x-chart-label": "Avg Connection Time",
|
|
76
84
|
"x-chart-unit": "ms",
|
|
77
85
|
}),
|
|
78
|
-
maxConnectionTime:
|
|
86
|
+
maxConnectionTime: aggregatedMinMax({
|
|
79
87
|
"x-chart-type": "line",
|
|
80
88
|
"x-chart-label": "Max Connection Time",
|
|
81
89
|
"x-chart-unit": "ms",
|
|
82
90
|
}),
|
|
83
|
-
successRate:
|
|
91
|
+
successRate: aggregatedRate({
|
|
84
92
|
"x-chart-type": "gauge",
|
|
85
93
|
"x-chart-label": "Success Rate",
|
|
86
94
|
"x-chart-unit": "%",
|
|
87
95
|
}),
|
|
88
|
-
errorCount:
|
|
96
|
+
errorCount: aggregatedCounter({
|
|
89
97
|
"x-chart-type": "counter",
|
|
90
98
|
"x-chart-label": "Errors",
|
|
91
99
|
}),
|
|
92
|
-
}
|
|
100
|
+
};
|
|
93
101
|
|
|
94
|
-
type SshAggregatedResult =
|
|
102
|
+
type SshAggregatedResult = InferAggregatedResult<typeof sshAggregatedFields>;
|
|
95
103
|
|
|
96
104
|
// ============================================================================
|
|
97
105
|
// SSH CLIENT INTERFACE (for testability)
|
|
@@ -178,15 +186,12 @@ const defaultSshClient: SshClient = {
|
|
|
178
186
|
// STRATEGY
|
|
179
187
|
// ============================================================================
|
|
180
188
|
|
|
181
|
-
export class SshHealthCheckStrategy
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
SshAggregatedResult
|
|
188
|
-
>
|
|
189
|
-
{
|
|
189
|
+
export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
190
|
+
SshConfig,
|
|
191
|
+
SshTransportClient,
|
|
192
|
+
SshResult,
|
|
193
|
+
typeof sshAggregatedFields
|
|
194
|
+
> {
|
|
190
195
|
id = "ssh";
|
|
191
196
|
displayName = "SSH Health Check";
|
|
192
197
|
description = "SSH server connectivity and command execution health check";
|
|
@@ -207,61 +212,41 @@ export class SshHealthCheckStrategy
|
|
|
207
212
|
schema: sshResultSchema,
|
|
208
213
|
});
|
|
209
214
|
|
|
210
|
-
aggregatedResult
|
|
215
|
+
aggregatedResult = new VersionedAggregated({
|
|
211
216
|
version: 1,
|
|
212
|
-
|
|
217
|
+
fields: sshAggregatedFields,
|
|
213
218
|
});
|
|
214
219
|
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
mergeResult(
|
|
221
|
+
existing: SshAggregatedResult | undefined,
|
|
222
|
+
run: HealthCheckRunForAggregation<SshResult>,
|
|
217
223
|
): SshAggregatedResult {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (validRuns.length === 0) {
|
|
221
|
-
return {
|
|
222
|
-
avgConnectionTime: 0,
|
|
223
|
-
maxConnectionTime: 0,
|
|
224
|
-
successRate: 0,
|
|
225
|
-
errorCount: 0,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const connectionTimes = validRuns
|
|
230
|
-
.map((r) => r.metadata?.connectionTimeMs)
|
|
231
|
-
.filter((t): t is number => typeof t === "number");
|
|
232
|
-
|
|
233
|
-
const avgConnectionTime =
|
|
234
|
-
connectionTimes.length > 0
|
|
235
|
-
? Math.round(
|
|
236
|
-
connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
|
|
237
|
-
)
|
|
238
|
-
: 0;
|
|
239
|
-
|
|
240
|
-
const maxConnectionTime =
|
|
241
|
-
connectionTimes.length > 0 ? Math.max(...connectionTimes) : 0;
|
|
242
|
-
|
|
243
|
-
const successCount = validRuns.filter(
|
|
244
|
-
(r) => r.metadata?.connected === true
|
|
245
|
-
).length;
|
|
246
|
-
const successRate = Math.round((successCount / validRuns.length) * 100);
|
|
247
|
-
|
|
248
|
-
const errorCount = validRuns.filter(
|
|
249
|
-
(r) => r.metadata?.error !== undefined
|
|
250
|
-
).length;
|
|
224
|
+
const metadata = run.metadata;
|
|
251
225
|
|
|
252
|
-
|
|
253
|
-
avgConnectionTime,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
226
|
+
const avgConnectionTime = mergeAverage(
|
|
227
|
+
existing?.avgConnectionTime,
|
|
228
|
+
metadata?.connectionTimeMs,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const maxConnectionTime = mergeMinMax(
|
|
232
|
+
existing?.maxConnectionTime,
|
|
233
|
+
metadata?.connectionTimeMs,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const isSuccess = metadata?.connected ?? false;
|
|
237
|
+
const successRate = mergeRate(existing?.successRate, isSuccess);
|
|
238
|
+
|
|
239
|
+
const hasError = metadata?.error !== undefined;
|
|
240
|
+
const errorCount = mergeCounter(existing?.errorCount, hasError);
|
|
241
|
+
|
|
242
|
+
return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
|
|
258
243
|
}
|
|
259
244
|
|
|
260
245
|
/**
|
|
261
246
|
* Create a connected SSH transport client.
|
|
262
247
|
*/
|
|
263
248
|
async createClient(
|
|
264
|
-
config: SshConfigInput
|
|
249
|
+
config: SshConfigInput,
|
|
265
250
|
): Promise<ConnectedClient<SshTransportClient>> {
|
|
266
251
|
const validatedConfig = this.config.validate(config);
|
|
267
252
|
|