@checkstack/healthcheck-backend 1.21.3 → 1.22.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,224 @@
1
1
  # @checkstack/healthcheck-backend
2
2
 
3
+ ## 1.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - be74b01: Evaluate health per probe location, so a failing satellite can no longer read as healthy
8
+
9
+ Thanks to @stuajnht for reporting: a system whose local check succeeded and
10
+ whose satellite check failed was shown as **healthy**, and the report correctly
11
+ guessed the cause - one combined verdict where there should have been one per
12
+ location.
13
+
14
+ A check's runs were grouped into slices by environment alone, so both locations'
15
+ runs landed in the same slice and were handed to the threshold evaluator as one
16
+ interleaved stream. In the default `consecutive` mode the streak breaks on every
17
+ alternation, no threshold is ever reached, and evaluation falls through to its
18
+ healthy default. A satellite failing 100% of the time was therefore invisible
19
+ for as long as a local check succeeded between its runs.
20
+
21
+ A slice is now an **(environment, source)** pair - one environment as probed
22
+ from one location - and each is evaluated on its own window, with the worst
23
+ result deciding the check. This is the same rule environments already followed;
24
+ the source dimension was simply never considered. Both the system rollup and the
25
+ system overview were affected, and both are fixed.
26
+
27
+ Related correctness fixes that fall out of keying slices by source:
28
+
29
+ - A **de-assigned satellite** (or the core after **Include local** is turned
30
+ off) stops counting immediately instead of dragging the rollup with its last
31
+ failures until they age out of the window. Its history moves under **Old
32
+ checks**.
33
+ - **Per-satellite environment scoping** is honoured when resolving slices, so a
34
+ satellite narrowed to production no longer keeps a stale staging slice alive.
35
+ - A satellite scoped to run env-less while the core fans out keeps its slice
36
+ live; the "has a live environment slice" question is now answered per
37
+ location, as the backend already did.
38
+
39
+ The system overview shows one row per slice and names the location (for example
40
+ **EU West**) as soon as a check runs from more than one place. A check that only
41
+ ever runs on the core shows no location label - there is nothing to
42
+ disambiguate.
43
+
44
+ `checkStatuses[].slices` and the overview's per-slice entries carry the
45
+ breakdown (`sourceId`, `sourceLabel`, `sourceOrphaned`) on the wire, and
46
+ `sliceCount` / `failingSliceCount` now count locations as well as environments -
47
+ so a check probing one environment from the core and one satellite contributes
48
+ 2 to the dashboard's "X of Y checks failing" denominator, not 1.
49
+
50
+ - be74b01: Satellites run per environment, and can be scoped to specific ones
51
+
52
+ Satellites were handed no environment information at all, so every result they
53
+ reported was stored env-less. On a system with environments that meant satellite
54
+ checks contributed nothing to per-environment health - and, until the preceding
55
+ fix, were labelled "Old checks" for it.
56
+
57
+ A satellite now fans out exactly as the local executor does:
58
+
59
+ - `getAssignmentsForSatellite` resolves each assignment's effective environments
60
+ and sends them with the assignment.
61
+ - The agent schedules ONE run per environment and reports each result with its
62
+ `environmentId`, so per-environment history, charts and rollups include
63
+ satellite results.
64
+ - Collectors on a satellite now receive the `environment` run-context block, so
65
+ `{{ environment.<key> }}` templating resolves there exactly as it does locally.
66
+
67
+ **A satellite can also be scoped to specific environments.** Without that, every
68
+ satellite would probe every environment - a staging-network satellite would start
69
+ failing prod checks it has no route to, and one per-environment slice would merge
70
+ results from satellites in different networks. A new `satelliteEnvironmentIds`
71
+ map on the assignment scopes each satellite: an absent key means "all
72
+ environments" (so every existing assignment behaves exactly as before), `[]` means
73
+ one env-less run, and a list narrows to those ids. A satellite can only ever
74
+ narrow the assignment's own selector, never widen it.
75
+
76
+ Both protocol additions are optional, for version skew in either direction: an
77
+ older satellite sends no `environmentId` and its runs are stored env-less as they
78
+ always were, while an older core sends no environments and the agent falls back to
79
+ a single env-less run.
80
+
81
+ The assignment's Execution panel gains a per-satellite environment picker,
82
+ shown for each assigned satellite once the system has environments.
83
+
84
+ Thanks to [@stuajnht](https://github.com/stuajnht) for the valuable feedback.
85
+
86
+ - be74b01: Drive satellite health results through the same reactive/notify path as local runs
87
+
88
+ A satellite-detected health change previously did almost nothing on the core:
89
+ `ingestSatelliteResult` inserted the run row and invalidated the cache, and
90
+ stopped there. A LOCAL run additionally drives the whole reactive layer - the
91
+ `health` entity write (which fires the ENTITY_CHANGED that automations and
92
+ triggers key on), the state-transition record, the subscriber notification, the
93
+ checkCompleted/checkFailed automation hooks, and the realtime signals. So a
94
+ satellite that detected an outage fired **no notifications, no automations, no
95
+ transition record, and no realtime signal** - satellite monitoring was
96
+ effectively silent.
97
+
98
+ Both paths now run through ONE shared function, `persistRunAndReact`, so a
99
+ satellite result reacts exactly like a local one. The host binds the service
100
+ dependencies once and hands the router a narrowed reactor, so the local and
101
+ satellite callers cannot pass different dependencies and drift apart again
102
+ (`ingestSatelliteResult` was itself a duplicated-and-drifted copy of the local
103
+ persistence path - this removes the duplication that caused it). Ingest now
104
+ splits into `processSatelliteResult` (evaluate assertions, strip ephemeral
105
+ fields, resolve the check name) plus the shared reactive path.
106
+
107
+ Also fixed: a satellite collector's transport error is now annotated as
108
+ `_collectorError` on the stored result, matching a local run - the satellite
109
+ previously dropped that annotation.
110
+
111
+ Coverage: added tests that a satellite result is routed through the shared
112
+ reactor with its processed payload (guarding against a silent regression back
113
+ to insert-only), and extracted the satellite's `executeAssignment` into a
114
+ testable module with tests for custom-field template expansion, probe-measured
115
+ timings, the `_collectorError` annotation, and the strategy-not-loaded path.
116
+
117
+ - be74b01: Expand system/environment custom fields in satellite health checks, via one shared execution engine
118
+
119
+ Thanks to @stuajnht for reporting: a system or environment custom field
120
+ referenced with `{{ system.metadata.<key> }}` / `{{ environment.<key> }}` in a
121
+ health check was NOT expanded when the check ran on a satellite - the raw
122
+ template reached the probe. The core queue executor grew a per-run templating
123
+ pass, but the satellite's execution loop was a hand-maintained COPY that never
124
+ did, so the two drifted.
125
+
126
+ The fix removes the copy. A new lean package `@checkstack/healthcheck-execution`
127
+ owns the shared execution engine - render the strategy + collector
128
+ `x-templatable` fields against the run's environment/system context, build the
129
+ transport client, run the collectors, close the client - and BOTH the core
130
+ queue executor and the satellite now run through it. Templating, the
131
+ secret-then-template ordering, and the per-collector fan-out therefore cannot
132
+ drift between core and satellite again. Each side keeps only its genuine edges
133
+ as injected hooks: the core resolves secrets from its database and does
134
+ migrate-on-read; the satellite resolves them just-in-time over its socket.
135
+
136
+ Also fixed: transport sub-phase timings (DNS / connect / TLS / wait / transfer)
137
+ are now measured AT THE PROBE and reported by satellites, so a satellite run's
138
+ `metadata.timings` matches a local run's. The core cannot derive the timing of a
139
+ probe it did not run - and may have no route to a target a satellite can reach -
140
+ so the satellite must produce these; the core persists them as-is.
141
+
142
+ - be74b01: Stop reporting systems as healthy when nothing has measured them
143
+
144
+ A system whose health check had never produced a run reported `healthy` - so it
145
+ showed green in the catalog, kept its group green, and read "operational" on the
146
+ public status page. A system with no checks at all did the same. For a
147
+ monitoring product that is the worst possible default: the one state you must
148
+ never invent is the reassuring one.
149
+
150
+ `getSystemHealthStatus` began each check at `healthy` and each system's
151
+ aggregate at `healthy`, then only ever downgraded. With no runs to examine,
152
+ nothing downgraded them. `HealthCheckStatus` had no way to say "not measured".
153
+
154
+ A new `SystemHealthStatus` adds `unknown` for systems and their checks. It is
155
+ deliberately NOT a run status - a run that happened is always healthy, degraded
156
+ or unhealthy, and the database enum stays three-valued. Now:
157
+
158
+ - A check with no runs is `unknown`, not `healthy`.
159
+ - A system reports `unknown` when no check contributed a signal. A system with
160
+ one healthy check and one never-run check still reads `healthy`: it has
161
+ positive evidence, and the unmeasured check is visible on its own page.
162
+ - The catalog reports `unknown` by OMISSION, which its group rollup already
163
+ treats as "no signal" - so a group with an unmeasured member stops claiming to
164
+ be healthy. That is the reported bug.
165
+ - The public status page maps it to its existing `unknown`, which is ignored for
166
+ the overall banner unless everything is unknown. One unmeasured system no
167
+ longer claims "operational" for itself, and does not panic the whole page.
168
+ - A first measurement records a transition with a NULL `fromStatus` - the column
169
+ was already nullable for exactly this case - instead of pretending the system
170
+ was healthy beforehand.
171
+ - Automations matching on `unhealthy` do not fire for a merely unmeasured
172
+ system, which is correct: an unmeasured system is not a detected outage.
173
+
174
+ Dependency warnings deliberately keep their current behaviour: an unmeasured
175
+ upstream raises no warning, and a never-run check is dropped from the evaluation
176
+ rather than counted as passing.
177
+
178
+ Note that pausing a system's only check now leaves it `unknown` rather than
179
+ `healthy`. Paused failures still do not keep a system degraded - that behaviour
180
+ is unchanged - but with nothing running, the system is genuinely unmeasured.
181
+
182
+ Thanks to [@stuajnht](https://github.com/stuajnht) for the valuable feedback.
183
+
184
+ ### Patch Changes
185
+
186
+ - Updated dependencies [be74b01]
187
+ - Updated dependencies [be74b01]
188
+ - Updated dependencies [be74b01]
189
+ - Updated dependencies [be74b01]
190
+ - Updated dependencies [be74b01]
191
+ - Updated dependencies [be74b01]
192
+ - Updated dependencies [be74b01]
193
+ - Updated dependencies [be74b01]
194
+ - Updated dependencies [be74b01]
195
+ - Updated dependencies [be74b01]
196
+ - Updated dependencies [be74b01]
197
+ - Updated dependencies [be74b01]
198
+ - Updated dependencies [be74b01]
199
+ - Updated dependencies [be74b01]
200
+ - Updated dependencies [be74b01]
201
+ - Updated dependencies [be74b01]
202
+ - @checkstack/ai-backend@0.11.4
203
+ - @checkstack/notification-common@1.8.0
204
+ - @checkstack/incident-backend@1.13.6
205
+ - @checkstack/healthcheck-common@1.19.0
206
+ - @checkstack/satellite-backend@0.9.4
207
+ - @checkstack/healthcheck-execution@0.35.0
208
+ - @checkstack/status-page-backend@0.6.6
209
+ - @checkstack/status-page-common@0.6.5
210
+ - @checkstack/automation-backend@0.11.8
211
+ - @checkstack/secrets-backend@0.3.9
212
+ - @checkstack/catalog-backend@1.10.1
213
+ - @checkstack/catalog-common@2.8.1
214
+ - @checkstack/incident-common@1.10.5
215
+ - @checkstack/maintenance-common@1.10.5
216
+ - @checkstack/script-packages-backend@0.4.6
217
+ - @checkstack/sdk@0.135.1
218
+ - @checkstack/backend-api@0.34.1
219
+ - @checkstack/command-backend@0.2.27
220
+ - @checkstack/gitops-backend@0.5.27
221
+
3
222
  ## 1.21.3
4
223
 
5
224
  ### 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;