@checkstack/healthcheck-backend 1.21.3 → 1.23.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,319 @@
1
1
  # @checkstack/healthcheck-backend
2
2
 
3
+ ## 1.23.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 88f4333: Per-satellite offline threshold, connectivity notifications, and stop satellite-only checks going silent
8
+
9
+ **A satellite going offline was invisible, and so were its checks.** Three
10
+ related changes:
11
+
12
+ **Per-satellite offline threshold.** The 45-second global constant is now a
13
+ per-satellite override (**Offline after**, 2 minutes to 24 hours), because
14
+ tolerance is a property of the link, not of the platform: a satellite on a flaky
15
+ uplink needs grace that should not be forced on every other satellite. The
16
+ threshold is carried on every row read by `computeStatus`, so the entity read,
17
+ the admin list and the heartbeat monitor cannot disagree about the same
18
+ satellite. Additive, nullable column - existing satellites keep the default.
19
+
20
+ **Connectivity notifications.** Satellites are now a notification target with a
21
+ **Satellite connectivity** subscription: a warning when a satellite stops
22
+ heartbeating, informational when it returns. A reconnect only notifies if the
23
+ satellite was actually offline, so a redeploy is not an event. (The same
24
+ transitions remain available as `satellite.heartbeat_lost` / `.connected`
25
+ automation triggers for anyone wanting different routing.)
26
+
27
+ **Satellite-only checks no longer go silent.** BUG FIX: a check with
28
+ `includeLocal: false` whose satellites were all offline recorded NOTHING, so it
29
+ displayed its last known status indefinitely - a dead probe was indistinguishable
30
+ from a passing one. The core now records a `degraded` run with a clear message.
31
+ Degraded rather than unhealthy because the target may be fine; what failed is our
32
+ ability to observe it. Liveness that cannot be resolved is treated as "executing"
33
+ so a transient lookup failure cannot mark the whole fleet degraded at once.
34
+
35
+ Checks also surface staleness: a last run older than five intervals (minimum ten
36
+ minutes) is highlighted, so an ageing status is visible even with no run to
37
+ explain it. Paused checks are never stale, and neither is a RETIRED slice - one
38
+ whose environment was removed or whose satellite was unassigned - because
39
+ warning about something you retired on purpose trains operators to ignore the
40
+ badge.
41
+
42
+ The unobservable run does NOT notify subscribers. One offline satellite degrades
43
+ every check assigned to it in the same tick, and `healthy -> degraded` is an
44
+ escalation, so notifying per check would turn a single root cause into one alert
45
+ per check. The satellite's own connectivity subscription reports the cause once;
46
+ the runs are still recorded, so health and the UI stay honest.
47
+
48
+ Satellite liveness is cached on the shared platform cache with a 5s TTL. The
49
+ executor asks per tick of every satellite-only check and the read is a full
50
+ scan, so the uncached version scaled with the number of such checks. The TTL is
51
+ well below the smallest offline threshold the schema allows, so a cached answer
52
+ can lag a transition by one tick but never span one.
53
+
54
+ Corrects the user guide, which claimed offline satellites produced failed runs -
55
+ they produced nothing at all.
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies [88f4333]
60
+ - Updated dependencies [1deaac5]
61
+ - Updated dependencies [88f4333]
62
+ - Updated dependencies [88f4333]
63
+ - Updated dependencies [88f4333]
64
+ - Updated dependencies [88f4333]
65
+ - Updated dependencies [88f4333]
66
+ - Updated dependencies [1deaac5]
67
+ - Updated dependencies [56e5375]
68
+ - Updated dependencies [88f4333]
69
+ - @checkstack/common@0.24.0
70
+ - @checkstack/healthcheck-common@1.19.1
71
+ - @checkstack/status-page-common@0.7.0
72
+ - @checkstack/incident-common@1.11.0
73
+ - @checkstack/maintenance-common@1.11.0
74
+ - @checkstack/command-backend@0.3.0
75
+ - @checkstack/incident-backend@1.14.0
76
+ - @checkstack/satellite-backend@0.10.0
77
+ - @checkstack/status-page-backend@0.7.0
78
+ - @checkstack/notification-common@1.9.0
79
+ - @checkstack/ai-backend@0.11.5
80
+ - @checkstack/backend-api@0.35.0
81
+ - @checkstack/satellite-common@0.12.0
82
+ - @checkstack/automation-backend@0.11.9
83
+ - @checkstack/secrets-backend@0.3.10
84
+ - @checkstack/ai-common@0.6.8
85
+ - @checkstack/cache-api@0.3.21
86
+ - @checkstack/catalog-backend@1.10.2
87
+ - @checkstack/catalog-common@2.8.2
88
+ - @checkstack/gitops-backend@0.5.28
89
+ - @checkstack/gitops-common@0.7.5
90
+ - @checkstack/healthcheck-execution@0.35.1
91
+ - @checkstack/queue-api@0.4.1
92
+ - @checkstack/script-packages-backend@0.4.7
93
+ - @checkstack/sdk@0.136.1
94
+ - @checkstack/secrets-common@0.3.4
95
+ - @checkstack/signal-common@0.3.2
96
+ - @checkstack/cache-utils@0.3.2
97
+
98
+ ## 1.22.0
99
+
100
+ ### Minor Changes
101
+
102
+ - be74b01: Evaluate health per probe location, so a failing satellite can no longer read as healthy
103
+
104
+ Thanks to @stuajnht for reporting: a system whose local check succeeded and
105
+ whose satellite check failed was shown as **healthy**, and the report correctly
106
+ guessed the cause - one combined verdict where there should have been one per
107
+ location.
108
+
109
+ A check's runs were grouped into slices by environment alone, so both locations'
110
+ runs landed in the same slice and were handed to the threshold evaluator as one
111
+ interleaved stream. In the default `consecutive` mode the streak breaks on every
112
+ alternation, no threshold is ever reached, and evaluation falls through to its
113
+ healthy default. A satellite failing 100% of the time was therefore invisible
114
+ for as long as a local check succeeded between its runs.
115
+
116
+ A slice is now an **(environment, source)** pair - one environment as probed
117
+ from one location - and each is evaluated on its own window, with the worst
118
+ result deciding the check. This is the same rule environments already followed;
119
+ the source dimension was simply never considered. Both the system rollup and the
120
+ system overview were affected, and both are fixed.
121
+
122
+ Related correctness fixes that fall out of keying slices by source:
123
+
124
+ - A **de-assigned satellite** (or the core after **Include local** is turned
125
+ off) stops counting immediately instead of dragging the rollup with its last
126
+ failures until they age out of the window. Its history moves under **Old
127
+ checks**.
128
+ - **Per-satellite environment scoping** is honoured when resolving slices, so a
129
+ satellite narrowed to production no longer keeps a stale staging slice alive.
130
+ - A satellite scoped to run env-less while the core fans out keeps its slice
131
+ live; the "has a live environment slice" question is now answered per
132
+ location, as the backend already did.
133
+
134
+ The system overview shows one row per slice and names the location (for example
135
+ **EU West**) as soon as a check runs from more than one place. A check that only
136
+ ever runs on the core shows no location label - there is nothing to
137
+ disambiguate.
138
+
139
+ `checkStatuses[].slices` and the overview's per-slice entries carry the
140
+ breakdown (`sourceId`, `sourceLabel`, `sourceOrphaned`) on the wire, and
141
+ `sliceCount` / `failingSliceCount` now count locations as well as environments -
142
+ so a check probing one environment from the core and one satellite contributes
143
+ 2 to the dashboard's "X of Y checks failing" denominator, not 1.
144
+
145
+ - be74b01: Satellites run per environment, and can be scoped to specific ones
146
+
147
+ Satellites were handed no environment information at all, so every result they
148
+ reported was stored env-less. On a system with environments that meant satellite
149
+ checks contributed nothing to per-environment health - and, until the preceding
150
+ fix, were labelled "Old checks" for it.
151
+
152
+ A satellite now fans out exactly as the local executor does:
153
+
154
+ - `getAssignmentsForSatellite` resolves each assignment's effective environments
155
+ and sends them with the assignment.
156
+ - The agent schedules ONE run per environment and reports each result with its
157
+ `environmentId`, so per-environment history, charts and rollups include
158
+ satellite results.
159
+ - Collectors on a satellite now receive the `environment` run-context block, so
160
+ `{{ environment.<key> }}` templating resolves there exactly as it does locally.
161
+
162
+ **A satellite can also be scoped to specific environments.** Without that, every
163
+ satellite would probe every environment - a staging-network satellite would start
164
+ failing prod checks it has no route to, and one per-environment slice would merge
165
+ results from satellites in different networks. A new `satelliteEnvironmentIds`
166
+ map on the assignment scopes each satellite: an absent key means "all
167
+ environments" (so every existing assignment behaves exactly as before), `[]` means
168
+ one env-less run, and a list narrows to those ids. A satellite can only ever
169
+ narrow the assignment's own selector, never widen it.
170
+
171
+ Both protocol additions are optional, for version skew in either direction: an
172
+ older satellite sends no `environmentId` and its runs are stored env-less as they
173
+ always were, while an older core sends no environments and the agent falls back to
174
+ a single env-less run.
175
+
176
+ The assignment's Execution panel gains a per-satellite environment picker,
177
+ shown for each assigned satellite once the system has environments.
178
+
179
+ Thanks to [@stuajnht](https://github.com/stuajnht) for the valuable feedback.
180
+
181
+ - be74b01: Drive satellite health results through the same reactive/notify path as local runs
182
+
183
+ A satellite-detected health change previously did almost nothing on the core:
184
+ `ingestSatelliteResult` inserted the run row and invalidated the cache, and
185
+ stopped there. A LOCAL run additionally drives the whole reactive layer - the
186
+ `health` entity write (which fires the ENTITY_CHANGED that automations and
187
+ triggers key on), the state-transition record, the subscriber notification, the
188
+ checkCompleted/checkFailed automation hooks, and the realtime signals. So a
189
+ satellite that detected an outage fired **no notifications, no automations, no
190
+ transition record, and no realtime signal** - satellite monitoring was
191
+ effectively silent.
192
+
193
+ Both paths now run through ONE shared function, `persistRunAndReact`, so a
194
+ satellite result reacts exactly like a local one. The host binds the service
195
+ dependencies once and hands the router a narrowed reactor, so the local and
196
+ satellite callers cannot pass different dependencies and drift apart again
197
+ (`ingestSatelliteResult` was itself a duplicated-and-drifted copy of the local
198
+ persistence path - this removes the duplication that caused it). Ingest now
199
+ splits into `processSatelliteResult` (evaluate assertions, strip ephemeral
200
+ fields, resolve the check name) plus the shared reactive path.
201
+
202
+ Also fixed: a satellite collector's transport error is now annotated as
203
+ `_collectorError` on the stored result, matching a local run - the satellite
204
+ previously dropped that annotation.
205
+
206
+ Coverage: added tests that a satellite result is routed through the shared
207
+ reactor with its processed payload (guarding against a silent regression back
208
+ to insert-only), and extracted the satellite's `executeAssignment` into a
209
+ testable module with tests for custom-field template expansion, probe-measured
210
+ timings, the `_collectorError` annotation, and the strategy-not-loaded path.
211
+
212
+ - be74b01: Expand system/environment custom fields in satellite health checks, via one shared execution engine
213
+
214
+ Thanks to @stuajnht for reporting: a system or environment custom field
215
+ referenced with `{{ system.metadata.<key> }}` / `{{ environment.<key> }}` in a
216
+ health check was NOT expanded when the check ran on a satellite - the raw
217
+ template reached the probe. The core queue executor grew a per-run templating
218
+ pass, but the satellite's execution loop was a hand-maintained COPY that never
219
+ did, so the two drifted.
220
+
221
+ The fix removes the copy. A new lean package `@checkstack/healthcheck-execution`
222
+ owns the shared execution engine - render the strategy + collector
223
+ `x-templatable` fields against the run's environment/system context, build the
224
+ transport client, run the collectors, close the client - and BOTH the core
225
+ queue executor and the satellite now run through it. Templating, the
226
+ secret-then-template ordering, and the per-collector fan-out therefore cannot
227
+ drift between core and satellite again. Each side keeps only its genuine edges
228
+ as injected hooks: the core resolves secrets from its database and does
229
+ migrate-on-read; the satellite resolves them just-in-time over its socket.
230
+
231
+ Also fixed: transport sub-phase timings (DNS / connect / TLS / wait / transfer)
232
+ are now measured AT THE PROBE and reported by satellites, so a satellite run's
233
+ `metadata.timings` matches a local run's. The core cannot derive the timing of a
234
+ probe it did not run - and may have no route to a target a satellite can reach -
235
+ so the satellite must produce these; the core persists them as-is.
236
+
237
+ - be74b01: Stop reporting systems as healthy when nothing has measured them
238
+
239
+ A system whose health check had never produced a run reported `healthy` - so it
240
+ showed green in the catalog, kept its group green, and read "operational" on the
241
+ public status page. A system with no checks at all did the same. For a
242
+ monitoring product that is the worst possible default: the one state you must
243
+ never invent is the reassuring one.
244
+
245
+ `getSystemHealthStatus` began each check at `healthy` and each system's
246
+ aggregate at `healthy`, then only ever downgraded. With no runs to examine,
247
+ nothing downgraded them. `HealthCheckStatus` had no way to say "not measured".
248
+
249
+ A new `SystemHealthStatus` adds `unknown` for systems and their checks. It is
250
+ deliberately NOT a run status - a run that happened is always healthy, degraded
251
+ or unhealthy, and the database enum stays three-valued. Now:
252
+
253
+ - A check with no runs is `unknown`, not `healthy`.
254
+ - A system reports `unknown` when no check contributed a signal. A system with
255
+ one healthy check and one never-run check still reads `healthy`: it has
256
+ positive evidence, and the unmeasured check is visible on its own page.
257
+ - The catalog reports `unknown` by OMISSION, which its group rollup already
258
+ treats as "no signal" - so a group with an unmeasured member stops claiming to
259
+ be healthy. That is the reported bug.
260
+ - The public status page maps it to its existing `unknown`, which is ignored for
261
+ the overall banner unless everything is unknown. One unmeasured system no
262
+ longer claims "operational" for itself, and does not panic the whole page.
263
+ - A first measurement records a transition with a NULL `fromStatus` - the column
264
+ was already nullable for exactly this case - instead of pretending the system
265
+ was healthy beforehand.
266
+ - Automations matching on `unhealthy` do not fire for a merely unmeasured
267
+ system, which is correct: an unmeasured system is not a detected outage.
268
+
269
+ Dependency warnings deliberately keep their current behaviour: an unmeasured
270
+ upstream raises no warning, and a never-run check is dropped from the evaluation
271
+ rather than counted as passing.
272
+
273
+ Note that pausing a system's only check now leaves it `unknown` rather than
274
+ `healthy`. Paused failures still do not keep a system degraded - that behaviour
275
+ is unchanged - but with nothing running, the system is genuinely unmeasured.
276
+
277
+ Thanks to [@stuajnht](https://github.com/stuajnht) for the valuable feedback.
278
+
279
+ ### Patch Changes
280
+
281
+ - Updated dependencies [be74b01]
282
+ - Updated dependencies [be74b01]
283
+ - Updated dependencies [be74b01]
284
+ - Updated dependencies [be74b01]
285
+ - Updated dependencies [be74b01]
286
+ - Updated dependencies [be74b01]
287
+ - Updated dependencies [be74b01]
288
+ - Updated dependencies [be74b01]
289
+ - Updated dependencies [be74b01]
290
+ - Updated dependencies [be74b01]
291
+ - Updated dependencies [be74b01]
292
+ - Updated dependencies [be74b01]
293
+ - Updated dependencies [be74b01]
294
+ - Updated dependencies [be74b01]
295
+ - Updated dependencies [be74b01]
296
+ - Updated dependencies [be74b01]
297
+ - @checkstack/ai-backend@0.11.4
298
+ - @checkstack/notification-common@1.8.0
299
+ - @checkstack/incident-backend@1.13.6
300
+ - @checkstack/healthcheck-common@1.19.0
301
+ - @checkstack/satellite-backend@0.9.4
302
+ - @checkstack/healthcheck-execution@0.35.0
303
+ - @checkstack/status-page-backend@0.6.6
304
+ - @checkstack/status-page-common@0.6.5
305
+ - @checkstack/automation-backend@0.11.8
306
+ - @checkstack/secrets-backend@0.3.9
307
+ - @checkstack/catalog-backend@1.10.1
308
+ - @checkstack/catalog-common@2.8.1
309
+ - @checkstack/incident-common@1.10.5
310
+ - @checkstack/maintenance-common@1.10.5
311
+ - @checkstack/script-packages-backend@0.4.6
312
+ - @checkstack/sdk@0.135.1
313
+ - @checkstack/backend-api@0.34.1
314
+ - @checkstack/command-backend@0.2.27
315
+ - @checkstack/gitops-backend@0.5.27
316
+
3
317
  ## 1.21.3
4
318
 
5
319
  ### Patch Changes
@@ -0,0 +1,8 @@
1
+ -- Per-satellite environment scoping for a health-check assignment.
2
+ --
3
+ -- Deliberately NO backfill: the column's "key absent" semantics already mean
4
+ -- "this satellite runs every environment the assignment resolves to", so a NULL
5
+ -- column reproduces exactly the behaviour every existing row has today. Writing
6
+ -- an explicit map for every row would only freeze today's satellite list into
7
+ -- data that has to be maintained.
8
+ ALTER TABLE "system_health_checks" ADD COLUMN "satellite_environment_ids" jsonb;