@checkstack/healthcheck-ssh-backend 0.2.34 → 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 CHANGED
@@ -1,5 +1,90 @@
1
1
  # @checkstack/healthcheck-ssh-backend
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8cad340: Add a finer per-run transport timing breakdown to health checks.
8
+
9
+ Each run now records an optional structured `metadata.timings` (DNS, connect,
10
+ TLS, wait/time-to-first-byte, transfer, and a `processing` catch-all for
11
+ non-HTTP operation time). The run-detail view renders the phases it has, in
12
+ transport order, and falls back to the previous Connection + Processing split
13
+ for older runs that lack the finer data.
14
+
15
+ For HTTP the request is issued verbatim through `fetch` (original URL, headers,
16
+ and body), so request behavior is identical to a plain `fetch`. The timing is
17
+ measured around it: `fetch` resolves at the response headers, so wait
18
+ (time-to-first-byte) and transfer (body) are measured exactly on the request,
19
+ DNS is timed at the resolve step, and connect/TLS come from a short-lived,
20
+ best-effort raw `net`/`tls` probe to the same already-validated IP (the request
21
+ socket exposes no connect/handshake events on the Bun runtime). The probe is
22
+ timing-only and never fails the check. The probe validates the TLS certificate
23
+ (against the original hostname via SNI) like the real request does - it does not
24
+ disable certificate validation; an unverifiable cert simply yields no TLS-phase
25
+ timing rather than aborting. Other transports surface the connect and operation
26
+ times they already measure.
27
+
28
+ The SSRF guard now validates the resolved host (rejecting cloud-metadata /
29
+ link-local and operator-denied ranges) as a pre-flight check and no longer pins
30
+ the request to the resolved IP. Pinning rewrote the URL to the IP literal and
31
+ moved the host to the `Host` header, which breaks HTTP/2 origins (their
32
+ authority comes from the URL's `:authority`, not `Host`) - that is why real
33
+ hosts such as `google.com` started answering 404/429 instead of 200. The
34
+ pre-flight validation keeps blocking static metadata/link-local targets and
35
+ direct denied IP literals; the only thing dropped is DNS-rebind TOCTOU
36
+ protection (a narrow window that pinning closed at the cost of breaking
37
+ legitimate HTTP/2 requests).
38
+
39
+ The run-detail "slowest" badge no longer collides with the timing bar, and a
40
+ genuinely sub-millisecond phase reads as "<1 ms" instead of a bare "0 ms".
41
+
42
+ ### Patch Changes
43
+
44
+ - 8cad340: Retune anomaly-detection defaults across every health-check strategy and the
45
+ hardware collector for a low-noise, problem-focused out-of-the-box experience.
46
+
47
+ The detection engine already learns a per-metric baseline, debounces with a
48
+ confirmation window, and applies practical-significance floors. This pass tunes
49
+ the per-metric **defaults** so a fresh install alerts only on genuine,
50
+ statistically-significant, problem-mapping deviations instead of flooding on
51
+ every metric that wiggles. 264 metrics were reviewed:
52
+
53
+ - **Default-disabled** the high-noise and un-baselineable classes that were
54
+ alerting for no good reason: raw identifiers and counts (status codes, error
55
+ and row counts, build counts, player and executor counts), config echoes and
56
+ near-constants (probe packet counts, CPU core count, total/swap memory),
57
+ payload-size and other run-to-run-volatile values, and deterministic values
58
+ like certificate days-remaining (governed by the check's own static-threshold
59
+ health logic, not statistics). These stay chartable and can be re-enabled per
60
+ field.
61
+ - **Hardened** the signals that should alert - latency/response/execution time
62
+ and availability/success/saturation percentages - with confirmation windows
63
+ and absolute + relative floors so brief spikes and sub-threshold jitter no
64
+ longer flap, and prefer percentage metrics over their absolute twins.
65
+
66
+ No detection-engine or schema changes; only per-metric `x-anomaly-*` defaults.
67
+ Users who had opted into any now-disabled metric keep their explicit override.
68
+
69
+ - Updated dependencies [8cad340]
70
+ - Updated dependencies [8cad340]
71
+ - Updated dependencies [8cad340]
72
+ - Updated dependencies [8cad340]
73
+ - Updated dependencies [8cad340]
74
+ - Updated dependencies [8cad340]
75
+ - Updated dependencies [8cad340]
76
+ - @checkstack/backend-api@0.25.0
77
+ - @checkstack/healthcheck-common@1.8.0
78
+ - @checkstack/common@0.17.0
79
+ - @checkstack/healthcheck-ssh-common@0.1.23
80
+
81
+ ## 0.2.35
82
+
83
+ ### Patch Changes
84
+
85
+ - Updated dependencies [2ec8f64]
86
+ - @checkstack/backend-api@0.24.1
87
+
3
88
  ## 0.2.34
4
89
 
5
90
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-ssh-backend",
3
- "version": "0.2.34",
3
+ "version": "0.3.0",
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.24.0",
21
- "@checkstack/common": "0.16.0",
22
- "@checkstack/healthcheck-common": "1.7.1",
23
- "@checkstack/healthcheck-ssh-common": "0.1.22",
20
+ "@checkstack/backend-api": "0.25.0",
21
+ "@checkstack/common": "0.17.0",
22
+ "@checkstack/healthcheck-common": "1.8.0",
23
+ "@checkstack/healthcheck-ssh-common": "0.1.23",
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.2"
31
+ "@checkstack/scripts": "0.6.3"
32
32
  },
33
33
  "description": "Checkstack healthcheck-ssh-backend plugin",
34
34
  "author": {
@@ -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
- "x-anomaly-enabled": true,
41
- "x-anomaly-direction": "dominance",
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
 
@@ -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
- "x-anomaly-enabled": true,
99
- "x-anomaly-direction": "lower-is-better",
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
- "x-anomaly-enabled": true,
112
- "x-anomaly-direction": "lower-is-better",
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) => connection.exec(command),
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
  }