@checkstack/healthcheck-ssh-backend 0.2.35 → 0.3.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 +89 -0
- package/package.json +6 -6
- package/src/command-collector.ts +16 -2
- package/src/strategy.test.ts +4 -0
- package/src/strategy.ts +38 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# @checkstack/healthcheck-ssh-backend
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [2e20792]
|
|
8
|
+
- Updated dependencies [2e20792]
|
|
9
|
+
- @checkstack/backend-api@0.26.0
|
|
10
|
+
- @checkstack/healthcheck-common@1.8.1
|
|
11
|
+
- @checkstack/healthcheck-ssh-common@0.1.24
|
|
12
|
+
- @checkstack/common@0.17.0
|
|
13
|
+
|
|
14
|
+
## 0.3.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 8cad340: Add a finer per-run transport timing breakdown to health checks.
|
|
19
|
+
|
|
20
|
+
Each run now records an optional structured `metadata.timings` (DNS, connect,
|
|
21
|
+
TLS, wait/time-to-first-byte, transfer, and a `processing` catch-all for
|
|
22
|
+
non-HTTP operation time). The run-detail view renders the phases it has, in
|
|
23
|
+
transport order, and falls back to the previous Connection + Processing split
|
|
24
|
+
for older runs that lack the finer data.
|
|
25
|
+
|
|
26
|
+
For HTTP the request is issued verbatim through `fetch` (original URL, headers,
|
|
27
|
+
and body), so request behavior is identical to a plain `fetch`. The timing is
|
|
28
|
+
measured around it: `fetch` resolves at the response headers, so wait
|
|
29
|
+
(time-to-first-byte) and transfer (body) are measured exactly on the request,
|
|
30
|
+
DNS is timed at the resolve step, and connect/TLS come from a short-lived,
|
|
31
|
+
best-effort raw `net`/`tls` probe to the same already-validated IP (the request
|
|
32
|
+
socket exposes no connect/handshake events on the Bun runtime). The probe is
|
|
33
|
+
timing-only and never fails the check. The probe validates the TLS certificate
|
|
34
|
+
(against the original hostname via SNI) like the real request does - it does not
|
|
35
|
+
disable certificate validation; an unverifiable cert simply yields no TLS-phase
|
|
36
|
+
timing rather than aborting. Other transports surface the connect and operation
|
|
37
|
+
times they already measure.
|
|
38
|
+
|
|
39
|
+
The SSRF guard now validates the resolved host (rejecting cloud-metadata /
|
|
40
|
+
link-local and operator-denied ranges) as a pre-flight check and no longer pins
|
|
41
|
+
the request to the resolved IP. Pinning rewrote the URL to the IP literal and
|
|
42
|
+
moved the host to the `Host` header, which breaks HTTP/2 origins (their
|
|
43
|
+
authority comes from the URL's `:authority`, not `Host`) - that is why real
|
|
44
|
+
hosts such as `google.com` started answering 404/429 instead of 200. The
|
|
45
|
+
pre-flight validation keeps blocking static metadata/link-local targets and
|
|
46
|
+
direct denied IP literals; the only thing dropped is DNS-rebind TOCTOU
|
|
47
|
+
protection (a narrow window that pinning closed at the cost of breaking
|
|
48
|
+
legitimate HTTP/2 requests).
|
|
49
|
+
|
|
50
|
+
The run-detail "slowest" badge no longer collides with the timing bar, and a
|
|
51
|
+
genuinely sub-millisecond phase reads as "<1 ms" instead of a bare "0 ms".
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- 8cad340: Retune anomaly-detection defaults across every health-check strategy and the
|
|
56
|
+
hardware collector for a low-noise, problem-focused out-of-the-box experience.
|
|
57
|
+
|
|
58
|
+
The detection engine already learns a per-metric baseline, debounces with a
|
|
59
|
+
confirmation window, and applies practical-significance floors. This pass tunes
|
|
60
|
+
the per-metric **defaults** so a fresh install alerts only on genuine,
|
|
61
|
+
statistically-significant, problem-mapping deviations instead of flooding on
|
|
62
|
+
every metric that wiggles. 264 metrics were reviewed:
|
|
63
|
+
|
|
64
|
+
- **Default-disabled** the high-noise and un-baselineable classes that were
|
|
65
|
+
alerting for no good reason: raw identifiers and counts (status codes, error
|
|
66
|
+
and row counts, build counts, player and executor counts), config echoes and
|
|
67
|
+
near-constants (probe packet counts, CPU core count, total/swap memory),
|
|
68
|
+
payload-size and other run-to-run-volatile values, and deterministic values
|
|
69
|
+
like certificate days-remaining (governed by the check's own static-threshold
|
|
70
|
+
health logic, not statistics). These stay chartable and can be re-enabled per
|
|
71
|
+
field.
|
|
72
|
+
- **Hardened** the signals that should alert - latency/response/execution time
|
|
73
|
+
and availability/success/saturation percentages - with confirmation windows
|
|
74
|
+
and absolute + relative floors so brief spikes and sub-threshold jitter no
|
|
75
|
+
longer flap, and prefer percentage metrics over their absolute twins.
|
|
76
|
+
|
|
77
|
+
No detection-engine or schema changes; only per-metric `x-anomaly-*` defaults.
|
|
78
|
+
Users who had opted into any now-disabled metric keep their explicit override.
|
|
79
|
+
|
|
80
|
+
- Updated dependencies [8cad340]
|
|
81
|
+
- Updated dependencies [8cad340]
|
|
82
|
+
- Updated dependencies [8cad340]
|
|
83
|
+
- Updated dependencies [8cad340]
|
|
84
|
+
- Updated dependencies [8cad340]
|
|
85
|
+
- Updated dependencies [8cad340]
|
|
86
|
+
- Updated dependencies [8cad340]
|
|
87
|
+
- @checkstack/backend-api@0.25.0
|
|
88
|
+
- @checkstack/healthcheck-common@1.8.0
|
|
89
|
+
- @checkstack/common@0.17.0
|
|
90
|
+
- @checkstack/healthcheck-ssh-common@0.1.23
|
|
91
|
+
|
|
3
92
|
## 0.2.35
|
|
4
93
|
|
|
5
94
|
### 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.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"checkstack": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"pack": "bunx @checkstack/scripts plugin-pack"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@checkstack/backend-api": "0.
|
|
21
|
-
"@checkstack/common": "0.
|
|
22
|
-
"@checkstack/healthcheck-common": "1.
|
|
23
|
-
"@checkstack/healthcheck-ssh-common": "0.1.
|
|
20
|
+
"@checkstack/backend-api": "0.26.0",
|
|
21
|
+
"@checkstack/common": "0.17.0",
|
|
22
|
+
"@checkstack/healthcheck-common": "1.8.1",
|
|
23
|
+
"@checkstack/healthcheck-ssh-common": "0.1.24",
|
|
24
24
|
"ssh2": "1.16.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/ssh2": "1.15.0",
|
|
29
29
|
"typescript": "^5.0.0",
|
|
30
30
|
"@checkstack/tsconfig": "0.0.7",
|
|
31
|
-
"@checkstack/scripts": "0.6.
|
|
31
|
+
"@checkstack/scripts": "0.6.4"
|
|
32
32
|
},
|
|
33
33
|
"description": "Checkstack healthcheck-ssh-backend plugin",
|
|
34
34
|
"author": {
|
package/src/command-collector.ts
CHANGED
|
@@ -37,8 +37,12 @@ const commandResultSchema = healthResultSchema({
|
|
|
37
37
|
exitCode: healthResultNumber({
|
|
38
38
|
"x-chart-type": "counter",
|
|
39
39
|
"x-chart-label": "Exit Code",
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Disabled by default: the command is arbitrary and user-supplied, so the
|
|
41
|
+
// exit code has no learnable baseline (many commands legitimately return
|
|
42
|
+
// non-zero, and the meaningful "did it succeed" signal is already carried
|
|
43
|
+
// by the aggregated successRate). Treating exit-code changes as anomalies
|
|
44
|
+
// is a classic alert-fatigue source. Still chartable; users can opt in.
|
|
45
|
+
"x-anomaly-enabled": false,
|
|
42
46
|
}),
|
|
43
47
|
stdout: healthResultString({
|
|
44
48
|
"x-chart-type": "text",
|
|
@@ -73,6 +77,12 @@ const commandAggregatedFields = {
|
|
|
73
77
|
"x-chart-unit": "ms",
|
|
74
78
|
"x-anomaly-enabled": true,
|
|
75
79
|
"x-anomaly-direction": "lower-is-better",
|
|
80
|
+
// Latency: err wider and require sustained slowdown plus practical floors
|
|
81
|
+
// so small jitter on fast commands never alerts.
|
|
82
|
+
"x-anomaly-sensitivity": 2,
|
|
83
|
+
"x-anomaly-confirmation-window": 3,
|
|
84
|
+
"x-anomaly-min-absolute-delta": 50,
|
|
85
|
+
"x-anomaly-min-relative-delta": 0.5,
|
|
76
86
|
}),
|
|
77
87
|
successRate: aggregatedRate({
|
|
78
88
|
"x-chart-type": "gauge",
|
|
@@ -80,6 +90,10 @@ const commandAggregatedFields = {
|
|
|
80
90
|
"x-chart-unit": "%",
|
|
81
91
|
"x-anomaly-enabled": true,
|
|
82
92
|
"x-anomaly-direction": "higher-is-better",
|
|
93
|
+
// Availability percent: confirm a sustained drop and require a few percent
|
|
94
|
+
// of real movement before alerting.
|
|
95
|
+
"x-anomaly-confirmation-window": 3,
|
|
96
|
+
"x-anomaly-min-absolute-delta": 5,
|
|
83
97
|
}),
|
|
84
98
|
};
|
|
85
99
|
|
package/src/strategy.test.ts
CHANGED
|
@@ -45,6 +45,8 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
45
45
|
expect(connectedClient.client).toBeDefined();
|
|
46
46
|
expect(connectedClient.client.exec).toBeDefined();
|
|
47
47
|
expect(connectedClient.close).toBeDefined();
|
|
48
|
+
// The SSH handshake phase is measured up front in createClient.
|
|
49
|
+
expect(connectedClient.timings?.connectMs).toBeGreaterThanOrEqual(0);
|
|
48
50
|
|
|
49
51
|
connectedClient.close();
|
|
50
52
|
});
|
|
@@ -84,6 +86,8 @@ describe("SshHealthCheckStrategy", () => {
|
|
|
84
86
|
|
|
85
87
|
expect(result.exitCode).toBe(0);
|
|
86
88
|
expect(result.stdout).toBe("OK");
|
|
89
|
+
// exec records the command duration as processingMs (last-command-wins).
|
|
90
|
+
expect(connectedClient.timings?.processingMs).toBeGreaterThanOrEqual(0);
|
|
87
91
|
|
|
88
92
|
connectedClient.close();
|
|
89
93
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
z,
|
|
16
16
|
configString,
|
|
17
17
|
type ConnectedClient,
|
|
18
|
+
type TransportTimings,
|
|
18
19
|
type InferAggregatedResult,
|
|
19
20
|
baseStrategyConfigSchema,
|
|
20
21
|
} from "@checkstack/backend-api";
|
|
@@ -90,13 +91,22 @@ const sshAggregatedFields = {
|
|
|
90
91
|
"x-chart-unit": "ms",
|
|
91
92
|
"x-anomaly-enabled": true,
|
|
92
93
|
"x-anomaly-direction": "lower-is-better",
|
|
94
|
+
// Latency: err wider and require a sustained, practically-significant
|
|
95
|
+
// slowdown so small jitter never alerts.
|
|
96
|
+
"x-anomaly-sensitivity": 2,
|
|
97
|
+
"x-anomaly-confirmation-window": 3,
|
|
98
|
+
"x-anomaly-min-absolute-delta": 50,
|
|
99
|
+
"x-anomaly-min-relative-delta": 0.5,
|
|
93
100
|
}),
|
|
94
101
|
maxConnectionTime: aggregatedMinMax({
|
|
95
102
|
"x-chart-type": "line",
|
|
96
103
|
"x-chart-label": "Max Connection Time",
|
|
97
104
|
"x-chart-unit": "ms",
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
// A per-bucket max is the most outlier-sensitive aggregate: a single slow
|
|
106
|
+
// run drives it, so baselining it produces noisy alerts. The average twin
|
|
107
|
+
// already covers sustained latency regressions, so keep this off by default
|
|
108
|
+
// (still chartable for tail-latency inspection).
|
|
109
|
+
"x-anomaly-enabled": false,
|
|
100
110
|
}),
|
|
101
111
|
successRate: aggregatedRate({
|
|
102
112
|
"x-chart-type": "gauge",
|
|
@@ -104,12 +114,19 @@ const sshAggregatedFields = {
|
|
|
104
114
|
"x-chart-unit": "%",
|
|
105
115
|
"x-anomaly-enabled": true,
|
|
106
116
|
"x-anomaly-direction": "higher-is-better",
|
|
117
|
+
// Availability percent: confirm a sustained drop and require a few percent
|
|
118
|
+
// of real movement before alerting.
|
|
119
|
+
"x-anomaly-confirmation-window": 3,
|
|
120
|
+
"x-anomaly-min-absolute-delta": 5,
|
|
107
121
|
}),
|
|
108
122
|
errorCount: aggregatedCounter({
|
|
109
123
|
"x-chart-type": "counter",
|
|
110
124
|
"x-chart-label": "Errors",
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
// Disabled by default: an absolute error count per bucket scales with the
|
|
126
|
+
// number of samples in the bucket, so its baseline drifts with traffic
|
|
127
|
+
// volume rather than with a real problem. Availability is already covered
|
|
128
|
+
// by successRate (the percent form), so prefer that and keep this off.
|
|
129
|
+
"x-anomaly-enabled": false,
|
|
113
130
|
}),
|
|
114
131
|
};
|
|
115
132
|
|
|
@@ -265,6 +282,10 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
265
282
|
): Promise<ConnectedClient<SshTransportClient>> {
|
|
266
283
|
const validatedConfig = this.config.validate(config);
|
|
267
284
|
|
|
285
|
+
// The SSH handshake (TCP connect + auth + channel ready) is a single
|
|
286
|
+
// measurable phase up front; record it as connectMs. The per-command exec
|
|
287
|
+
// time is filled in below as processingMs (last-command-wins).
|
|
288
|
+
const connectStart = performance.now();
|
|
268
289
|
const connection = await this.sshClient.connect({
|
|
269
290
|
host: validatedConfig.host,
|
|
270
291
|
port: validatedConfig.port,
|
|
@@ -274,11 +295,23 @@ export class SshHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
274
295
|
passphrase: validatedConfig.passphrase,
|
|
275
296
|
readyTimeout: validatedConfig.timeout,
|
|
276
297
|
});
|
|
298
|
+
const timings: TransportTimings = {
|
|
299
|
+
connectMs: Math.max(0, Math.round(performance.now() - connectStart)),
|
|
300
|
+
};
|
|
277
301
|
|
|
278
302
|
return {
|
|
279
303
|
client: {
|
|
280
|
-
exec: (command: string) =>
|
|
304
|
+
exec: async (command: string): Promise<SshCommandResult> => {
|
|
305
|
+
const start = performance.now();
|
|
306
|
+
const result = await connection.exec(command);
|
|
307
|
+
timings.processingMs = Math.max(
|
|
308
|
+
0,
|
|
309
|
+
Math.round(performance.now() - start),
|
|
310
|
+
);
|
|
311
|
+
return result;
|
|
312
|
+
},
|
|
281
313
|
},
|
|
314
|
+
timings,
|
|
282
315
|
close: () => connection.end(),
|
|
283
316
|
};
|
|
284
317
|
}
|