@checkstack/healthcheck-ssh-backend 0.1.13 → 0.1.14
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 +31 -0
- package/package.json +7 -7
- package/src/command-collector.test.ts +5 -3
- package/src/command-collector.ts +41 -27
- package/src/strategy.test.ts +22 -23
- package/src/strategy.ts +65 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @checkstack/healthcheck-ssh-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
8
|
+
|
|
9
|
+
### Breaking Changes (Internal)
|
|
10
|
+
|
|
11
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
12
|
+
|
|
13
|
+
### New Features
|
|
14
|
+
|
|
15
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
16
|
+
- `mergeCounter()` - track occurrences
|
|
17
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
18
|
+
- `mergeRate()` - track success/total, compute %
|
|
19
|
+
- `mergeMinMax()` - track min/max values
|
|
20
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
21
|
+
|
|
22
|
+
### Improvements
|
|
23
|
+
|
|
24
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
25
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [f676e11]
|
|
28
|
+
- Updated dependencies [48c2080]
|
|
29
|
+
- @checkstack/common@0.6.2
|
|
30
|
+
- @checkstack/backend-api@0.6.0
|
|
31
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
32
|
+
- @checkstack/healthcheck-ssh-common@0.1.8
|
|
33
|
+
|
|
3
34
|
## 0.1.13
|
|
4
35
|
|
|
5
36
|
### 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.1.14",
|
|
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,7 +101,8 @@ 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
107
|
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
107
108
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -138,7 +139,8 @@ 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
145
|
expect(aggregated.successRate).toBe(50);
|
|
144
146
|
});
|
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
|
+
averageStateSchema,
|
|
9
|
+
mergeRate,
|
|
10
|
+
rateStateSchema,
|
|
11
|
+
type AverageState,
|
|
12
|
+
type RateState,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import {
|
|
9
15
|
healthResultNumber,
|
|
@@ -49,7 +55,7 @@ const commandResultSchema = healthResultSchema({
|
|
|
49
55
|
|
|
50
56
|
export type CommandResult = z.infer<typeof commandResultSchema>;
|
|
51
57
|
|
|
52
|
-
const
|
|
58
|
+
const commandAggregatedDisplaySchema = healthResultSchema({
|
|
53
59
|
avgExecutionTimeMs: healthResultNumber({
|
|
54
60
|
"x-chart-type": "line",
|
|
55
61
|
"x-chart-label": "Avg Execution Time",
|
|
@@ -62,6 +68,17 @@ const commandAggregatedSchema = healthResultSchema({
|
|
|
62
68
|
}),
|
|
63
69
|
});
|
|
64
70
|
|
|
71
|
+
const commandAggregatedInternalSchema = z.object({
|
|
72
|
+
_executionTime: averageStateSchema
|
|
73
|
+
.optional(),
|
|
74
|
+
_success: rateStateSchema
|
|
75
|
+
.optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const commandAggregatedSchema = commandAggregatedDisplaySchema.merge(
|
|
79
|
+
commandAggregatedInternalSchema,
|
|
80
|
+
);
|
|
81
|
+
|
|
65
82
|
export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
66
83
|
|
|
67
84
|
// ============================================================================
|
|
@@ -73,15 +90,12 @@ export type CommandAggregatedResult = z.infer<typeof commandAggregatedSchema>;
|
|
|
73
90
|
* Allows users to run arbitrary shell commands as check items.
|
|
74
91
|
* This is the "basic mode" functionality exposed as a collector.
|
|
75
92
|
*/
|
|
76
|
-
export class CommandCollector
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
CommandAggregatedResult
|
|
83
|
-
>
|
|
84
|
-
{
|
|
93
|
+
export class CommandCollector implements CollectorStrategy<
|
|
94
|
+
SshTransportClient,
|
|
95
|
+
CommandConfig,
|
|
96
|
+
CommandResult,
|
|
97
|
+
CommandAggregatedResult
|
|
98
|
+
> {
|
|
85
99
|
/**
|
|
86
100
|
* ID for this collector.
|
|
87
101
|
* Built-in collectors are identified by ownerPlugin matching the strategy's plugin.
|
|
@@ -125,28 +139,28 @@ export class CommandCollector
|
|
|
125
139
|
};
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
mergeResult(
|
|
143
|
+
existing: CommandAggregatedResult | undefined,
|
|
144
|
+
run: HealthCheckRunForAggregation<CommandResult>,
|
|
130
145
|
): CommandAggregatedResult {
|
|
131
|
-
const
|
|
132
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
133
|
-
.filter((v): v is number => typeof v === "number");
|
|
146
|
+
const metadata = run.metadata;
|
|
134
147
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
const executionTimeState = mergeAverage(
|
|
149
|
+
existing?._executionTime as AverageState | undefined,
|
|
150
|
+
metadata?.executionTimeMs,
|
|
151
|
+
);
|
|
138
152
|
|
|
139
|
-
|
|
153
|
+
// Success is exit code 0
|
|
154
|
+
const successState = mergeRate(
|
|
155
|
+
existing?._success as RateState | undefined,
|
|
156
|
+
metadata?.exitCode === 0,
|
|
157
|
+
);
|
|
140
158
|
|
|
141
159
|
return {
|
|
142
|
-
avgExecutionTimeMs:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
successRate:
|
|
147
|
-
exitCodes.length > 0
|
|
148
|
-
? Math.round((successCount / exitCodes.length) * 100)
|
|
149
|
-
: 0,
|
|
160
|
+
avgExecutionTimeMs: executionTimeState.avg,
|
|
161
|
+
successRate: successState.rate,
|
|
162
|
+
_executionTime: executionTimeState,
|
|
163
|
+
_success: successState,
|
|
150
164
|
};
|
|
151
165
|
}
|
|
152
166
|
}
|
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,7 +139,8 @@ 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
145
|
expect(aggregated.avgConnectionTime).toBe(75);
|
|
145
146
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -148,22 +149,20 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
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
167
|
expect(aggregated.errorCount).toBe(1);
|
|
169
168
|
expect(aggregated.successRate).toBe(0);
|
package/src/strategy.ts
CHANGED
|
@@ -7,6 +7,18 @@ import {
|
|
|
7
7
|
configString,
|
|
8
8
|
configNumber,
|
|
9
9
|
type ConnectedClient,
|
|
10
|
+
mergeAverage,
|
|
11
|
+
averageStateSchema,
|
|
12
|
+
mergeRate,
|
|
13
|
+
rateStateSchema,
|
|
14
|
+
mergeCounter,
|
|
15
|
+
counterStateSchema,
|
|
16
|
+
mergeMinMax,
|
|
17
|
+
minMaxStateSchema,
|
|
18
|
+
type AverageState,
|
|
19
|
+
type RateState,
|
|
20
|
+
type CounterState,
|
|
21
|
+
type MinMaxState,
|
|
10
22
|
} from "@checkstack/backend-api";
|
|
11
23
|
import {
|
|
12
24
|
healthResultBoolean,
|
|
@@ -69,7 +81,7 @@ type SshResult = z.infer<typeof sshResultSchema>;
|
|
|
69
81
|
/**
|
|
70
82
|
* Aggregated metadata for buckets.
|
|
71
83
|
*/
|
|
72
|
-
const
|
|
84
|
+
const sshAggregatedDisplaySchema = healthResultSchema({
|
|
73
85
|
avgConnectionTime: healthResultNumber({
|
|
74
86
|
"x-chart-type": "line",
|
|
75
87
|
"x-chart-label": "Avg Connection Time",
|
|
@@ -91,6 +103,19 @@ const sshAggregatedSchema = healthResultSchema({
|
|
|
91
103
|
}),
|
|
92
104
|
});
|
|
93
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
|
+
);
|
|
118
|
+
|
|
94
119
|
type SshAggregatedResult = z.infer<typeof sshAggregatedSchema>;
|
|
95
120
|
|
|
96
121
|
// ============================================================================
|
|
@@ -178,15 +203,12 @@ const defaultSshClient: SshClient = {
|
|
|
178
203
|
// STRATEGY
|
|
179
204
|
// ============================================================================
|
|
180
205
|
|
|
181
|
-
export class SshHealthCheckStrategy
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
SshAggregatedResult
|
|
188
|
-
>
|
|
189
|
-
{
|
|
206
|
+
export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
207
|
+
SshConfig,
|
|
208
|
+
SshTransportClient,
|
|
209
|
+
SshResult,
|
|
210
|
+
SshAggregatedResult
|
|
211
|
+
> {
|
|
190
212
|
id = "ssh";
|
|
191
213
|
displayName = "SSH Health Check";
|
|
192
214
|
description = "SSH server connectivity and command execution health check";
|
|
@@ -212,48 +234,41 @@ export class SshHealthCheckStrategy
|
|
|
212
234
|
schema: sshAggregatedSchema,
|
|
213
235
|
});
|
|
214
236
|
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
mergeResult(
|
|
238
|
+
existing: SshAggregatedResult | undefined,
|
|
239
|
+
run: HealthCheckRunForAggregation<SshResult>,
|
|
217
240
|
): SshAggregatedResult {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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;
|
|
241
|
+
const metadata = run.metadata;
|
|
242
|
+
|
|
243
|
+
const connectionTimeState = mergeAverage(
|
|
244
|
+
existing?._connectionTime as AverageState | undefined,
|
|
245
|
+
metadata?.connectionTimeMs,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const maxConnectionTimeState = mergeMinMax(
|
|
249
|
+
existing?._maxConnectionTime as MinMaxState | undefined,
|
|
250
|
+
metadata?.connectionTimeMs,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const successState = mergeRate(
|
|
254
|
+
existing?._success as RateState | undefined,
|
|
255
|
+
metadata?.connected,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const errorState = mergeCounter(
|
|
259
|
+
existing?._errors as CounterState | undefined,
|
|
260
|
+
metadata?.error !== undefined,
|
|
261
|
+
);
|
|
251
262
|
|
|
252
263
|
return {
|
|
253
|
-
avgConnectionTime,
|
|
254
|
-
maxConnectionTime,
|
|
255
|
-
successRate,
|
|
256
|
-
errorCount,
|
|
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,
|
|
257
272
|
};
|
|
258
273
|
}
|
|
259
274
|
|
|
@@ -261,7 +276,7 @@ export class SshHealthCheckStrategy
|
|
|
261
276
|
* Create a connected SSH transport client.
|
|
262
277
|
*/
|
|
263
278
|
async createClient(
|
|
264
|
-
config: SshConfigInput
|
|
279
|
+
config: SshConfigInput,
|
|
265
280
|
): Promise<ConnectedClient<SshTransportClient>> {
|
|
266
281
|
const validatedConfig = this.config.validate(config);
|
|
267
282
|
|