@smthrs/notifications 0.0.0-stage → 1.0.0-rc.13
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 +127 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/dist/cjs/AlertPolicy.d.ts +284 -0
- package/dist/cjs/AlertPolicy.d.ts.map +1 -0
- package/dist/cjs/AlertPolicy.js +124 -0
- package/dist/cjs/AlertPolicy.js.map +7 -0
- package/dist/cjs/AlertRuntime.d.ts +90 -0
- package/dist/cjs/AlertRuntime.d.ts.map +1 -0
- package/dist/cjs/AlertRuntime.js +166 -0
- package/dist/cjs/AlertRuntime.js.map +7 -0
- package/dist/cjs/AlertSink.d.ts +101 -0
- package/dist/cjs/AlertSink.d.ts.map +1 -0
- package/dist/cjs/AlertSink.js +44 -0
- package/dist/cjs/AlertSink.js.map +7 -0
- package/dist/cjs/Alerts.d.ts +57 -0
- package/dist/cjs/Alerts.d.ts.map +1 -0
- package/dist/cjs/Alerts.js +46 -0
- package/dist/cjs/Alerts.js.map +7 -0
- package/dist/cjs/Notification.d.ts +214 -0
- package/dist/cjs/Notification.d.ts.map +1 -0
- package/dist/cjs/Notification.js +62 -0
- package/dist/cjs/Notification.js.map +7 -0
- package/dist/cjs/NotificationEvent.d.ts +214 -0
- package/dist/cjs/NotificationEvent.d.ts.map +1 -0
- package/dist/cjs/NotificationEvent.js +84 -0
- package/dist/cjs/NotificationEvent.js.map +7 -0
- package/dist/cjs/NotificationQueue.d.ts +194 -0
- package/dist/cjs/NotificationQueue.d.ts.map +1 -0
- package/dist/cjs/NotificationQueue.js +477 -0
- package/dist/cjs/NotificationQueue.js.map +7 -0
- package/dist/cjs/NotificationState.d.ts +169 -0
- package/dist/cjs/NotificationState.d.ts.map +1 -0
- package/dist/cjs/NotificationState.js +110 -0
- package/dist/cjs/NotificationState.js.map +7 -0
- package/dist/cjs/Projection.d.ts +30 -0
- package/dist/cjs/Projection.d.ts.map +1 -0
- package/dist/cjs/Projection.js +53 -0
- package/dist/cjs/Projection.js.map +7 -0
- package/dist/cjs/SteerPayload.d.ts +177 -0
- package/dist/cjs/SteerPayload.d.ts.map +1 -0
- package/dist/cjs/SteerPayload.js +61 -0
- package/dist/cjs/SteerPayload.js.map +7 -0
- package/dist/cjs/index.d.ts +41 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/foldCache.d.ts +36 -0
- package/dist/cjs/internal/foldCache.d.ts.map +1 -0
- package/dist/cjs/internal/foldCache.js +54 -0
- package/dist/cjs/internal/foldCache.js.map +7 -0
- package/dist/cjs/internal/replay.d.ts +22 -0
- package/dist/cjs/internal/replay.d.ts.map +1 -0
- package/dist/cjs/internal/replay.js +46 -0
- package/dist/cjs/internal/replay.js.map +7 -0
- package/dist/cjs/layerWebhook.d.ts +60 -0
- package/dist/cjs/layerWebhook.d.ts.map +1 -0
- package/dist/cjs/layerWebhook.js +145 -0
- package/dist/cjs/layerWebhook.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/AlertPolicy.d.ts +284 -0
- package/dist/esm/AlertPolicy.d.ts.map +1 -0
- package/dist/esm/AlertPolicy.js +284 -0
- package/dist/esm/AlertPolicy.js.map +1 -0
- package/dist/esm/AlertRuntime.d.ts +90 -0
- package/dist/esm/AlertRuntime.d.ts.map +1 -0
- package/dist/esm/AlertRuntime.js +191 -0
- package/dist/esm/AlertRuntime.js.map +1 -0
- package/dist/esm/AlertSink.d.ts +101 -0
- package/dist/esm/AlertSink.d.ts.map +1 -0
- package/dist/esm/AlertSink.js +63 -0
- package/dist/esm/AlertSink.js.map +1 -0
- package/dist/esm/Alerts.d.ts +57 -0
- package/dist/esm/Alerts.d.ts.map +1 -0
- package/dist/esm/Alerts.js +57 -0
- package/dist/esm/Alerts.js.map +1 -0
- package/dist/esm/Notification.d.ts +214 -0
- package/dist/esm/Notification.d.ts.map +1 -0
- package/dist/esm/Notification.js +121 -0
- package/dist/esm/Notification.js.map +1 -0
- package/dist/esm/NotificationEvent.d.ts +214 -0
- package/dist/esm/NotificationEvent.d.ts.map +1 -0
- package/dist/esm/NotificationEvent.js +158 -0
- package/dist/esm/NotificationEvent.js.map +1 -0
- package/dist/esm/NotificationQueue.d.ts +194 -0
- package/dist/esm/NotificationQueue.d.ts.map +1 -0
- package/dist/esm/NotificationQueue.js +625 -0
- package/dist/esm/NotificationQueue.js.map +1 -0
- package/dist/esm/NotificationState.d.ts +169 -0
- package/dist/esm/NotificationState.d.ts.map +1 -0
- package/dist/esm/NotificationState.js +191 -0
- package/dist/esm/NotificationState.js.map +1 -0
- package/dist/esm/Projection.d.ts +30 -0
- package/dist/esm/Projection.d.ts.map +1 -0
- package/dist/esm/Projection.js +47 -0
- package/dist/esm/Projection.js.map +1 -0
- package/dist/esm/SteerPayload.d.ts +177 -0
- package/dist/esm/SteerPayload.d.ts.map +1 -0
- package/dist/esm/SteerPayload.js +139 -0
- package/dist/esm/SteerPayload.js.map +1 -0
- package/dist/esm/index.d.ts +41 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +41 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/foldCache.d.ts +36 -0
- package/dist/esm/internal/foldCache.d.ts.map +1 -0
- package/dist/esm/internal/foldCache.js +58 -0
- package/dist/esm/internal/foldCache.js.map +1 -0
- package/dist/esm/internal/replay.d.ts +22 -0
- package/dist/esm/internal/replay.d.ts.map +1 -0
- package/dist/esm/internal/replay.js +31 -0
- package/dist/esm/internal/replay.js.map +1 -0
- package/dist/esm/layerWebhook.d.ts +60 -0
- package/dist/esm/layerWebhook.d.ts.map +1 -0
- package/dist/esm/layerWebhook.js +186 -0
- package/dist/esm/layerWebhook.js.map +1 -0
- package/docs/README.md +146 -0
- package/docs/api.md +443 -0
- package/docs/concepts/admission-and-promotion.md +144 -0
- package/docs/concepts/alerting.md +138 -0
- package/docs/concepts/journal-records.md +132 -0
- package/docs/guides/alert-on-a-stuck-run.md +140 -0
- package/docs/guides/drain-at-a-turn-boundary.md +95 -0
- package/docs/guides/handle-a-full-queue.md +102 -0
- package/docs/guides/report-pending-notifications.md +92 -0
- package/docs/guides/send-alerts-to-a-webhook.md +155 -0
- package/docs/guides/steer-a-run.md +127 -0
- package/docs/guides/testing.md +161 -0
- package/docs/installation.md +123 -0
- package/docs/quickstart.md +159 -0
- package/docs/troubleshooting.md +254 -0
- package/package.json +182 -3
- package/src/AlertPolicy.ts +377 -0
- package/src/AlertRuntime.ts +280 -0
- package/src/AlertSink.ts +105 -0
- package/src/Alerts.ts +71 -0
- package/src/Notification.ts +171 -0
- package/src/NotificationEvent.ts +213 -0
- package/src/NotificationQueue.ts +856 -0
- package/src/NotificationState.ts +295 -0
- package/src/Projection.ts +48 -0
- package/src/SteerPayload.ts +198 -0
- package/src/index.ts +47 -0
- package/src/internal/foldCache.ts +72 -0
- package/src/internal/replay.ts +39 -0
- package/src/layerWebhook.ts +239 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @smthrs/notifications
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- `SteerPayload.decode` takes the notification instead of its payload and
|
|
8
|
+
answers `undefined` for every `system-event`. A webhook or system-event
|
|
9
|
+
producer whose payload carried a `body` string or a `kind` could otherwise be
|
|
10
|
+
read as an operator message, seat, thinking, or tools steer.
|
|
11
|
+
- `NotificationEvent.fromEntry` reads an `Admitted` entry only under
|
|
12
|
+
`admissionSourceId` of the notification it carries, and a `Promoted` entry
|
|
13
|
+
only under a `/notifications/drain/` source, so another producer's record
|
|
14
|
+
with the same event type stays out of the projection. The source scopes
|
|
15
|
+
records; it does not authenticate the writer.
|
|
16
|
+
- `NotificationQueue.admit` refuses a notification larger than 1,048,576 UTF-16
|
|
17
|
+
code units with `notification_invalid`, before copying or fingerprinting it.
|
|
18
|
+
- `Alerts.defaultDetectors` consult only `control.*` event types, and a
|
|
19
|
+
`Detector.eventTypes` item ending in `.*` names a family. A
|
|
20
|
+
`flows.alerts.delivered` entry suppresses a page only under the source the
|
|
21
|
+
alerter writes.
|
|
22
|
+
|
|
23
|
+
## [1.0.0-rc.0] - 2026-09-01
|
|
24
|
+
|
|
25
|
+
### Breaking Changes
|
|
26
|
+
|
|
27
|
+
- `NotificationQueue.admit` decodes its argument against
|
|
28
|
+
`Notification.Notification` before it writes anything. A structurally invalid
|
|
29
|
+
value used to be acknowledged at a real journal sequence and then skipped by
|
|
30
|
+
every replay, which is acknowledged data loss. It now fails with
|
|
31
|
+
`notification_invalid` and the dotted path of the field that failed.
|
|
32
|
+
- `NotificationQueue.NotificationError.code` gained
|
|
33
|
+
`notification_id_reused` and `notification_invalid`, and the error carries
|
|
34
|
+
`notificationId` and `path`. Reusing a stable id with different content used
|
|
35
|
+
to be raised as a `Journal.JournalError` with `idempotency_conflict`,
|
|
36
|
+
borrowing the storage layer's error type for a rule this package decides.
|
|
37
|
+
- A full queue no longer journals its refusal. `admit` returns
|
|
38
|
+
`rejected-full` in the receipt alone, so the notification id stays admissible
|
|
39
|
+
once a boundary drains. A recorded refusal matched on every later attempt and
|
|
40
|
+
burned the id permanently, and no drain could ever free it.
|
|
41
|
+
- `NotificationState.defaultCapacity` replaces `Projection.defaultCapacity`.
|
|
42
|
+
The bound is the queue's, not the read model's, and the queue no longer
|
|
43
|
+
depends on the projection module for it.
|
|
44
|
+
- Admission and promotion records are written with `dedupe: "identity"`. Both
|
|
45
|
+
used to compare payload bytes, so two processes that loaded at different fill
|
|
46
|
+
levels failed the second writer over `decision`, a derived field neither
|
|
47
|
+
caller supplied.
|
|
48
|
+
- `Alerts.AlertError` carries `code`, `status`, and `reason` instead of
|
|
49
|
+
`cause`. The cause was the `HttpClientError`, which holds the request, which
|
|
50
|
+
holds the credential a deployment handed `layerWebhook`.
|
|
51
|
+
- `Alerts.Rule.afterMs` is a whole, non-negative number of milliseconds, and
|
|
52
|
+
`Alerts.layer` decodes the whole policy when the layer is built. `NaN` fired
|
|
53
|
+
on the first tick and stamped a `firedAt` that JSON wrote as `null`; a
|
|
54
|
+
negative delay fired with a `firedAt` earlier than the condition it described.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- A drain's journal identity is now the lineage and the boundary, each
|
|
59
|
+
percent-encoded. It was keyed on the boundary alone while the duplicate read
|
|
60
|
+
required both, so a second lineage draining the same boundary name on one run
|
|
61
|
+
failed with `idempotency_conflict` and its notifications stayed pending.
|
|
62
|
+
- `admit` and `drain` return the decision the journal committed, read back
|
|
63
|
+
after the write, rather than the value this process computed before it.
|
|
64
|
+
- `admit`, `drain`, `pending`, and `Alerts.tick` fold a run once and then page
|
|
65
|
+
only the entries committed since. Every call used to re-page and re-decode
|
|
66
|
+
the run's entire journal, twice in `admit` and `drain`, making the cost
|
|
67
|
+
quadratic in journal length. Each layer keeps the 64 most recently read runs.
|
|
68
|
+
- `Alerts` records one journal entry per alert per outcome. Failures were
|
|
69
|
+
written with no `sourceSeq`, so the journal allocated a new sequence on every
|
|
70
|
+
attempt and a webhook that stayed down appended a row per tick forever.
|
|
71
|
+
- `Alerts.tick` reads the admission receipt. An alert the queue refused is
|
|
72
|
+
reported in the new `Alerts.Tick.refused`, the sink is not called, and no
|
|
73
|
+
delivery is recorded, so nobody is paged about an alert the run cannot read.
|
|
74
|
+
- `Alerts.conditions` tests a detector's field with `Object.hasOwn`. `in` walks
|
|
75
|
+
`Object.prototype`, so a detector named `toString` or `constructor` read
|
|
76
|
+
every record-shaped entry in the run as evidence and closed the condition on
|
|
77
|
+
all of them, silently preventing the alert from ever firing.
|
|
78
|
+
- `Alerts.layerWebhook` bounds its request with `defaultWebhookTimeout`, ten
|
|
79
|
+
seconds by default. An endpoint that accepted the connection and never
|
|
80
|
+
answered stalled a run's whole alert runtime.
|
|
81
|
+
- `Alerts.coalescingKey` percent-encodes each component, so a run id or a
|
|
82
|
+
condition name containing the separator can no longer forge another pair's
|
|
83
|
+
key and suppress an unrelated page.
|
|
84
|
+
- `SteerPayload.encode` copies a `Tools` item's `toolNames`. The returned
|
|
85
|
+
record shared the caller's array with an admission that serializes it later.
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- `NotificationQueue.layerWith({ capacity })` serves the same layer at a bound
|
|
90
|
+
the composition chooses. The capacity used to be a constant no deployment
|
|
91
|
+
could raise.
|
|
92
|
+
- `NotificationQueue.DrainInput.cutoffSeq` carries the journal sequence that
|
|
93
|
+
opened a turn. `NotificationState.promoteSteers` documented that a steer
|
|
94
|
+
admitted after the cutoff is held for the next boundary, and the layer could
|
|
95
|
+
never express it: the cutoff was the last sequence it had just read, so every
|
|
96
|
+
pending steer always satisfied it.
|
|
97
|
+
- `NotificationEvent.AdmissionDecision` is the one declaration of the admission
|
|
98
|
+
vocabulary, exported as both a schema and a type, and `NotificationState`
|
|
99
|
+
re-exports it. The two used to be independent declarations that a fourth
|
|
100
|
+
decision could silently desynchronize.
|
|
101
|
+
- `NotificationEvent.isAdmitted` and `NotificationEvent.isPromoted` give the
|
|
102
|
+
untagged owned-event union one named discriminator, replacing three separate
|
|
103
|
+
property-presence checks.
|
|
104
|
+
- `Alerts.FailureCode`, `Alerts.coalescingKey`, and
|
|
105
|
+
`Alerts.defaultWebhookTimeout` are public.
|
|
106
|
+
- The webhook sink sends `alertId` in the body and as an `Idempotency-Key`
|
|
107
|
+
header. The package requires the receiver to dedupe on that id, and the body
|
|
108
|
+
did not carry it.
|
|
109
|
+
- Package-owned documentation under `docs/`, generated into
|
|
110
|
+
`docs/pages/api/notifications.md` by the `docsPages` target, plus this
|
|
111
|
+
changelog, which `package.json` already declared in `files`.
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
115
|
+
- The durable event types stay spelled `flows/notifications/Admitted` and
|
|
116
|
+
`flows/notifications/Promoted`, which is slashes and a PascalCase leaf where
|
|
117
|
+
every other event type in the repository is dot-separated and lowercase. The
|
|
118
|
+
values are already durable in every engine database and a rename would
|
|
119
|
+
silently stop matching consumer projections, so both spellings are frozen by
|
|
120
|
+
`test/WireFormat.test.ts` and the divergence is stated in their JSDoc.
|
|
121
|
+
- `SteerPayload.decode` still reads a record carrying a `body` string and no
|
|
122
|
+
`kind` as a message, but the reason is now stated correctly: it is the shape
|
|
123
|
+
a minimal caller writes, and the steer RPC accepts the same one. The previous
|
|
124
|
+
rationale claimed the branch existed for payloads written before the
|
|
125
|
+
vocabulary, which rc.0 cannot hold because it never loads a 0.x database.
|
|
126
|
+
- Retired the 45 internal `@slop` review markers that shipped in `src` and in
|
|
127
|
+
the published `.d.ts`, writing the sentence each one stood in for.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Cory and the Smithers Flows contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @smthrs/notifications
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
|
|
4
|
+
|
|
5
|
+
This package declares `effect` as an exact `4.0.5` peer dependency. Install one
|
|
6
|
+
`effect` release at that version so all Smithers packages share one Effect
|
|
7
|
+
runtime.
|
|
8
|
+
|
|
9
|
+
**Documentation:** https://github.com/smithersai/smithers/tree/main/packages/smithers/notifications/docs
|
|
10
|
+
|
|
11
|
+
Durable notification queue, admission policy, and journal projection for flows. It models human and system notifications, derives queue state from journal events, and drains eligible work at harness boundaries.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @smthrs/notifications@next effect
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node.js 26.4.0 or later. `@smthrs/journal` holds the durable records, and the
|
|
18
|
+
example below imports it directly, so declare it in your own package too.
|
|
19
|
+
|
|
20
|
+
## Public API
|
|
21
|
+
|
|
22
|
+
The root entry point exports these namespaces; each is also importable from `@smthrs/notifications/<Module>`.
|
|
23
|
+
|
|
24
|
+
| Module | Description |
|
|
25
|
+
| ------------------- | ----------------------------------------------------------------------------------------- |
|
|
26
|
+
| `Alerts` | Turns run conditions that outlive a delay into coalesced, at-least-once alerts. |
|
|
27
|
+
| `Notification` | Defines notification schemas, admission classes, and coalescing keys. |
|
|
28
|
+
| `NotificationEvent` | Defines journal notification events and decodes them from journal entries. |
|
|
29
|
+
| `NotificationQueue` | Defines the durable admit/drain/pending service and its journal-backed layer. |
|
|
30
|
+
| `NotificationState` | Implements the pure notification admission and promotion state machine. |
|
|
31
|
+
| `Projection` | Derives notification state from journal entries. |
|
|
32
|
+
| `SteerPayload` | Defines the steering vocabulary a control plane and a harness exchange through a payload. |
|
|
33
|
+
|
|
34
|
+
Every export of every module, with its signature and the meaning of each
|
|
35
|
+
parameter, is on
|
|
36
|
+
[the API reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/notifications/docs/api.md). This file
|
|
37
|
+
deliberately does not repeat it.
|
|
38
|
+
|
|
39
|
+
## What a caller has to know
|
|
40
|
+
|
|
41
|
+
- A caller MUST read `AdmissionReceipt.decision`. `rejected-full` means the run
|
|
42
|
+
already holds `NotificationState.defaultCapacity` pending notifications, so
|
|
43
|
+
the queue retained nothing and wrote no journal entry. The id stays
|
|
44
|
+
admissible, and the caller admits it again once a boundary has drained.
|
|
45
|
+
`admit` does not fail on a full queue. `NotificationQueue.layerWith` raises
|
|
46
|
+
or lowers the bound for one composition.
|
|
47
|
+
- `targetLineageId` is the address of whatever will read the notification: the
|
|
48
|
+
run, or one branch of it that closes turns of its own. The queue treats it as
|
|
49
|
+
an opaque string and only compares it for equality, so pass the run id when a
|
|
50
|
+
run has one reader and the branch's id when it has several.
|
|
51
|
+
- The unit of drain is the triple `(runId, targetLineageId, boundary)`. Two
|
|
52
|
+
addresses closing a turn under the same boundary name are two drains, recorded
|
|
53
|
+
separately. `DrainInput.cutoffSeq` holds a steer admitted mid-turn until the
|
|
54
|
+
next boundary.
|
|
55
|
+
- `NotificationError.code` is the stable half to branch on:
|
|
56
|
+
`notification_unavailable`, `notification_id_reused`, and
|
|
57
|
+
`notification_invalid`. Storage failures arrive as `Journal.JournalError`.
|
|
58
|
+
- `admit` decodes and snapshots its argument, so nothing the caller mutates
|
|
59
|
+
afterwards changes what was journaled.
|
|
60
|
+
- An alert sink must be idempotent on `Alerts.alertId`, which the webhook sink
|
|
61
|
+
sends in the body and as an `Idempotency-Key` header. That sink bounds every
|
|
62
|
+
request with `Alerts.defaultWebhookTimeout`, and `Alerts.AlertError` reports
|
|
63
|
+
`sink_rejected`, `sink_unreachable`, or `sink_timeout` and never carries the
|
|
64
|
+
request or its credentials.
|
|
65
|
+
- A layer folds each run's journal once and then pages only what has been
|
|
66
|
+
committed since, keeping the 64 most recently read runs.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import * as TestJournal from "@smthrs/journal/test/TestJournal"
|
|
70
|
+
import { NotificationQueue } from "@smthrs/notifications"
|
|
71
|
+
import { Effect } from "effect"
|
|
72
|
+
|
|
73
|
+
const program = Effect.gen(function*() {
|
|
74
|
+
const queue = yield* NotificationQueue.NotificationQueue
|
|
75
|
+
return yield* queue.drain({
|
|
76
|
+
runId: "run-1",
|
|
77
|
+
targetLineageId: "run-1/root",
|
|
78
|
+
boundary: "turn-1",
|
|
79
|
+
wouldIdle: true
|
|
80
|
+
})
|
|
81
|
+
}).pipe(
|
|
82
|
+
Effect.provide(NotificationQueue.layer),
|
|
83
|
+
// A real SQLite journal over an in-memory database. Swap in `SqlJournal.layer`
|
|
84
|
+
// over a file for a deployment.
|
|
85
|
+
Effect.provide(TestJournal.layer()),
|
|
86
|
+
Effect.scoped
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`NotificationQueue.layerNoop()` provides the same seam with every method failing as
|
|
91
|
+
`notification_unavailable`, for a composition that means to serve nothing.
|
|
92
|
+
`@smthrs/notifications/package.json` is also exported; `internal/*` and nested `*/index` subpaths are blocked.
|
|
93
|
+
|
|
94
|
+
A replayed drain fails with `notification_unavailable` if a committed notification or its payload can no longer be read. It never reports a successful partial delivery.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert policy: what a condition is, when a journal leaves one open, and
|
|
3
|
+
* which open conditions have outlived their delay.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is pure and a function of journal time, so the same entries
|
|
6
|
+
* decide the same alerts in any process at any instant past the delay. The
|
|
7
|
+
* transport that pages about them is `./layerWebhook.ts` and the journal-backed
|
|
8
|
+
* tick that drives them is `./AlertRuntime.ts`; `./Alerts.ts` is the module a
|
|
9
|
+
* consumer imports.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
import type { JournalEvent } from "@smthrs/journal";
|
|
14
|
+
import { Schema } from "effect";
|
|
15
|
+
/**
|
|
16
|
+
* The journal event type one delivered alert is recorded under.
|
|
17
|
+
*
|
|
18
|
+
* @category constants
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
*/
|
|
21
|
+
export declare const deliveredEventType = "flows.alerts.delivered";
|
|
22
|
+
/**
|
|
23
|
+
* The journal event type one failed delivery attempt is recorded under.
|
|
24
|
+
*
|
|
25
|
+
* One entry is written per alert per failure code, never one per tick: a
|
|
26
|
+
* webhook that stays down for an hour leaves one record, not a hundred and
|
|
27
|
+
* twenty that every later tick has to read past.
|
|
28
|
+
*
|
|
29
|
+
* @category constants
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
export declare const failedEventType = "flows.alerts.failed";
|
|
33
|
+
/**
|
|
34
|
+
* How loud an alert is.
|
|
35
|
+
*
|
|
36
|
+
* @category models
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
export declare const Severity: Schema.Literals<readonly ["info", "warning", "critical"]>;
|
|
40
|
+
/**
|
|
41
|
+
* How loud an alert is.
|
|
42
|
+
*
|
|
43
|
+
* @category models
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
export type Severity = typeof Severity.Type;
|
|
47
|
+
/**
|
|
48
|
+
* How a condition is recognized in the journal.
|
|
49
|
+
*
|
|
50
|
+
* `field` names a payload key, `value` the value that means the condition
|
|
51
|
+
* holds, and `eventTypes` narrows which entries are consulted at all. An
|
|
52
|
+
* `eventTypes` item ending in `.*` names a family: `control.*` consults
|
|
53
|
+
* `control.run.parked` and `control.monitor.beat` alike. An entry that carries
|
|
54
|
+
* the field with a different value CLOSES the condition, which is what makes a
|
|
55
|
+
* resume clear an approval alert without a second vocabulary for "cleared".
|
|
56
|
+
*
|
|
57
|
+
* @category models
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
export declare const Detector: Schema.Struct<{
|
|
61
|
+
readonly field: Schema.NonEmptyString;
|
|
62
|
+
readonly value: Schema.NonEmptyString;
|
|
63
|
+
readonly eventTypes: Schema.optional<Schema.$Array<Schema.NonEmptyString>>;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* How a condition is recognized in the journal.
|
|
67
|
+
*
|
|
68
|
+
* @category models
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
*/
|
|
71
|
+
export type Detector = typeof Detector.Type;
|
|
72
|
+
/**
|
|
73
|
+
* What to do about one condition, and after how long.
|
|
74
|
+
*
|
|
75
|
+
* `afterMs` is a whole, non-negative number of milliseconds. The bound is the
|
|
76
|
+
* schema's job because the delay is also arithmetic on journal time: a `NaN`
|
|
77
|
+
* delay fires on the first tick and stamps a `firedAt` that JSON writes as
|
|
78
|
+
* `null`, and a negative one fires with a `firedAt` earlier than the condition
|
|
79
|
+
* it describes.
|
|
80
|
+
*
|
|
81
|
+
* @category models
|
|
82
|
+
* @since 0.1.0
|
|
83
|
+
*/
|
|
84
|
+
export declare const Rule: Schema.Struct<{
|
|
85
|
+
readonly afterMs: Schema.Int;
|
|
86
|
+
readonly severity: Schema.optional<Schema.Literals<readonly ["info", "warning", "critical"]>>;
|
|
87
|
+
readonly owner: Schema.optional<Schema.String>;
|
|
88
|
+
readonly runbook: Schema.optional<Schema.String>;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* What to do about one condition, and after how long.
|
|
92
|
+
*
|
|
93
|
+
* @category models
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
export type Rule = typeof Rule.Type;
|
|
97
|
+
/**
|
|
98
|
+
* The conditions a policy alerts on and the delays it alerts after.
|
|
99
|
+
*
|
|
100
|
+
* `defaults` fills in what a rule leaves out, so a policy states the delay per
|
|
101
|
+
* condition and the ownership once.
|
|
102
|
+
*
|
|
103
|
+
* @category models
|
|
104
|
+
* @since 0.1.0
|
|
105
|
+
*/
|
|
106
|
+
export declare const Policy: Schema.Struct<{
|
|
107
|
+
readonly defaults: Schema.optional<Schema.Struct<{
|
|
108
|
+
readonly severity: Schema.optional<Schema.Literals<readonly ["info", "warning", "critical"]>>;
|
|
109
|
+
readonly owner: Schema.optional<Schema.String>;
|
|
110
|
+
readonly runbook: Schema.optional<Schema.String>;
|
|
111
|
+
}>>;
|
|
112
|
+
readonly rules: Schema.$Record<Schema.String, Schema.Struct<{
|
|
113
|
+
readonly afterMs: Schema.Int;
|
|
114
|
+
readonly severity: Schema.optional<Schema.Literals<readonly ["info", "warning", "critical"]>>;
|
|
115
|
+
readonly owner: Schema.optional<Schema.String>;
|
|
116
|
+
readonly runbook: Schema.optional<Schema.String>;
|
|
117
|
+
}>>;
|
|
118
|
+
/** Detectors for conditions this deployment names itself. */
|
|
119
|
+
readonly detectors: Schema.optional<Schema.$Record<Schema.String, Schema.Struct<{
|
|
120
|
+
readonly field: Schema.NonEmptyString;
|
|
121
|
+
readonly value: Schema.NonEmptyString;
|
|
122
|
+
readonly eventTypes: Schema.optional<Schema.$Array<Schema.NonEmptyString>>;
|
|
123
|
+
}>>>;
|
|
124
|
+
}>;
|
|
125
|
+
/**
|
|
126
|
+
* The conditions a policy alerts on and the delays it alerts after.
|
|
127
|
+
*
|
|
128
|
+
* @category models
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
export type Policy = typeof Policy.Type;
|
|
132
|
+
/**
|
|
133
|
+
* The four conditions a control plane journals out of the box.
|
|
134
|
+
*
|
|
135
|
+
* `status` rides on every `control.run.*` entry, and `health` on every
|
|
136
|
+
* `control.monitor.beat`. A deployment whose supervisor journals a park reason
|
|
137
|
+
* gets `quota-parked` from the same field the run summary reports it under.
|
|
138
|
+
*
|
|
139
|
+
* Each consults the `control.*` family only. A run's journal is shared with
|
|
140
|
+
* flow steps, agent traces, and anything else that writes to the run, and a
|
|
141
|
+
* `status` or `health` field on one of those entries would otherwise open or
|
|
142
|
+
* close a condition the control plane never reported.
|
|
143
|
+
*
|
|
144
|
+
* @category constants
|
|
145
|
+
* @since 0.1.0
|
|
146
|
+
*/
|
|
147
|
+
export declare const defaultDetectors: Readonly<Record<string, Detector>>;
|
|
148
|
+
/**
|
|
149
|
+
* A condition that is open on a run, and when it opened.
|
|
150
|
+
*
|
|
151
|
+
* @category models
|
|
152
|
+
* @since 0.1.0
|
|
153
|
+
*/
|
|
154
|
+
export interface Open {
|
|
155
|
+
readonly runId: string;
|
|
156
|
+
readonly condition: string;
|
|
157
|
+
/** The journal time of the entry that opened the condition. */
|
|
158
|
+
readonly since: number;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* One alert a policy decided to raise.
|
|
162
|
+
*
|
|
163
|
+
* @category models
|
|
164
|
+
* @since 0.1.0
|
|
165
|
+
*/
|
|
166
|
+
export interface Alert {
|
|
167
|
+
readonly runId: string;
|
|
168
|
+
readonly condition: string;
|
|
169
|
+
readonly since: number;
|
|
170
|
+
/**
|
|
171
|
+
* The journal instant the condition outlived its delay: `since` plus the
|
|
172
|
+
* rule's `afterMs`, never the wall clock of whichever tick noticed.
|
|
173
|
+
*/
|
|
174
|
+
readonly firedAt: number;
|
|
175
|
+
readonly severity: Severity;
|
|
176
|
+
readonly coalescingKey: string;
|
|
177
|
+
readonly owner?: string | undefined;
|
|
178
|
+
readonly runbook?: string | undefined;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The journal source the alerter writes one outcome of one alert under.
|
|
182
|
+
*
|
|
183
|
+
* A delivery record counts as evidence that an alert went out only under this
|
|
184
|
+
* source, so another producer's `flows.alerts.delivered` record with a
|
|
185
|
+
* matching `alertId` does not suppress a page. This scopes records; it does
|
|
186
|
+
* not authenticate the writer, because the journal does not bind a source to
|
|
187
|
+
* a writer.
|
|
188
|
+
*
|
|
189
|
+
* @param id the alert's {@link alertId}
|
|
190
|
+
* @param outcome `delivered`, `refused`, or `failed/<code>`
|
|
191
|
+
* @private
|
|
192
|
+
* @since 1.0.0
|
|
193
|
+
*/
|
|
194
|
+
export declare const recordSourceId: (id: string, outcome: string) => string;
|
|
195
|
+
/**
|
|
196
|
+
* The key an alert coalesces on: one open condition on one run.
|
|
197
|
+
*
|
|
198
|
+
* Each component is percent-encoded before it is joined, so a run id or a
|
|
199
|
+
* condition name containing the separator cannot forge another pair's key and
|
|
200
|
+
* suppress a page that belongs to somebody else. Both components are values a
|
|
201
|
+
* deployment chooses, which is why the encoding is not optional.
|
|
202
|
+
*
|
|
203
|
+
* @param runId the run the condition is open on
|
|
204
|
+
* @param condition the condition name the policy uses
|
|
205
|
+
* @category getters
|
|
206
|
+
* @since 1.0.0
|
|
207
|
+
*/
|
|
208
|
+
export declare const coalescingKey: (runId: string, condition: string) => string;
|
|
209
|
+
/**
|
|
210
|
+
* The identity a delivery is recorded under.
|
|
211
|
+
*
|
|
212
|
+
* The opening time is part of it on purpose. A condition that clears and
|
|
213
|
+
* re-opens is a NEW alert, because the second approval wait is not the first
|
|
214
|
+
* one, and a key without the time would suppress it forever.
|
|
215
|
+
*
|
|
216
|
+
* @param alert the alert
|
|
217
|
+
* @category getters
|
|
218
|
+
* @since 0.1.0
|
|
219
|
+
*/
|
|
220
|
+
export declare const alertId: (alert: Pick<Alert, "coalescingKey" | "since">) => string;
|
|
221
|
+
/**
|
|
222
|
+
* The detectors one policy decides with: the four this package ships, with
|
|
223
|
+
* the deployment's own overriding them by name.
|
|
224
|
+
*
|
|
225
|
+
* @private
|
|
226
|
+
* @since 1.0.0
|
|
227
|
+
*/
|
|
228
|
+
export declare const detectorsOf: (policy: Policy) => Readonly<Record<string, Detector>>;
|
|
229
|
+
/**
|
|
230
|
+
* One entry's payload when it is a record a detector can read a field off,
|
|
231
|
+
* and `undefined` for the arrays and primitives that carry no fields at all.
|
|
232
|
+
*
|
|
233
|
+
* @private
|
|
234
|
+
* @since 1.0.0
|
|
235
|
+
*/
|
|
236
|
+
export declare const payloadRecord: (payload: unknown) => Readonly<Record<string, unknown>> | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Folds one entry into the open-condition map. Shared by {@link conditions}
|
|
239
|
+
* and the runtime's incremental read, so both decide a condition the same
|
|
240
|
+
* way.
|
|
241
|
+
*
|
|
242
|
+
* The alerter's own records are never evidence about a condition. They are
|
|
243
|
+
* written into the same journal they are read from, and they carry the alert's
|
|
244
|
+
* own vocabulary: a refusal reports the answering HTTP `status`, which a
|
|
245
|
+
* detector watching the run's `status` would read as the condition clearing.
|
|
246
|
+
* A page would then close the condition it paged about and the next tick would
|
|
247
|
+
* re-open it, so a webhook that answered 503 once would alert forever.
|
|
248
|
+
*
|
|
249
|
+
* @private
|
|
250
|
+
* @since 1.0.0
|
|
251
|
+
*/
|
|
252
|
+
export declare const observe: (policy: Policy, detectors: Readonly<Record<string, Detector>>, entry: JournalEvent.Entry, payload: Readonly<Record<string, unknown>>, open: Map<string, number>) => void;
|
|
253
|
+
/**
|
|
254
|
+
* Every condition a run's journal leaves open, with the time each opened.
|
|
255
|
+
*
|
|
256
|
+
* An entry that carries a detector's field opens the condition when the value
|
|
257
|
+
* matches and closes it when it does not. Entries the detector does not name
|
|
258
|
+
* are ignored entirely, so a monitor beat cannot close an approval wait and an
|
|
259
|
+
* approval cannot close a stall.
|
|
260
|
+
*
|
|
261
|
+
* @param policy the policy whose detectors decide what counts
|
|
262
|
+
* @param runId the run the entries belong to
|
|
263
|
+
* @param entries the run's journal, oldest first
|
|
264
|
+
* @category projections
|
|
265
|
+
* @since 0.1.0
|
|
266
|
+
*/
|
|
267
|
+
export declare const conditions: (policy: Policy, runId: string, entries: ReadonlyArray<JournalEvent.Entry>) => ReadonlyArray<Open>;
|
|
268
|
+
/**
|
|
269
|
+
* The alerts a policy raises for the conditions open at `now`.
|
|
270
|
+
*
|
|
271
|
+
* Pure, and a function of journal time: the same journal raises the same
|
|
272
|
+
* alerts in any process, at any instant past the delay. `now` decides WHETHER
|
|
273
|
+
* an alert is raised; it never appears in one. A condition that has been open
|
|
274
|
+
* for less than its rule's delay raises nothing, which is the whole point of
|
|
275
|
+
* the delay, because most stalls clear themselves.
|
|
276
|
+
*
|
|
277
|
+
* @param policy the policy
|
|
278
|
+
* @param open the conditions the journal left open
|
|
279
|
+
* @param now the instant to judge them at
|
|
280
|
+
* @category projections
|
|
281
|
+
* @since 0.1.0
|
|
282
|
+
*/
|
|
283
|
+
export declare const decide: (policy: Policy, open: ReadonlyArray<Open>, now: number) => ReadonlyArray<Alert>;
|
|
284
|
+
//# sourceMappingURL=AlertPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AlertPolicy.d.ts","sourceRoot":"","sources":["../../src/AlertPolicy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var AlertPolicy_exports = {};
|
|
20
|
+
__export(AlertPolicy_exports, {
|
|
21
|
+
Detector: () => Detector,
|
|
22
|
+
Policy: () => Policy,
|
|
23
|
+
Rule: () => Rule,
|
|
24
|
+
Severity: () => Severity,
|
|
25
|
+
alertId: () => alertId,
|
|
26
|
+
coalescingKey: () => coalescingKey,
|
|
27
|
+
conditions: () => conditions,
|
|
28
|
+
decide: () => decide,
|
|
29
|
+
defaultDetectors: () => defaultDetectors,
|
|
30
|
+
deliveredEventType: () => deliveredEventType,
|
|
31
|
+
detectorsOf: () => detectorsOf,
|
|
32
|
+
failedEventType: () => failedEventType,
|
|
33
|
+
observe: () => observe,
|
|
34
|
+
payloadRecord: () => payloadRecord,
|
|
35
|
+
recordSourceId: () => recordSourceId
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(AlertPolicy_exports);
|
|
38
|
+
var import_effect = require("effect");
|
|
39
|
+
const deliveredEventType = "flows.alerts.delivered";
|
|
40
|
+
const failedEventType = "flows.alerts.failed";
|
|
41
|
+
const Severity = import_effect.Schema.Literals(["info", "warning", "critical"]);
|
|
42
|
+
const Detector = import_effect.Schema.Struct({
|
|
43
|
+
field: import_effect.Schema.NonEmptyString,
|
|
44
|
+
value: import_effect.Schema.NonEmptyString,
|
|
45
|
+
eventTypes: import_effect.Schema.optional(import_effect.Schema.Array(import_effect.Schema.NonEmptyString))
|
|
46
|
+
});
|
|
47
|
+
const Rule = import_effect.Schema.Struct({
|
|
48
|
+
afterMs: import_effect.Schema.Int.pipe(import_effect.Schema.check(import_effect.Schema.isGreaterThanOrEqualTo(0))),
|
|
49
|
+
severity: import_effect.Schema.optional(Severity),
|
|
50
|
+
owner: import_effect.Schema.optional(import_effect.Schema.String),
|
|
51
|
+
runbook: import_effect.Schema.optional(import_effect.Schema.String)
|
|
52
|
+
});
|
|
53
|
+
const Policy = import_effect.Schema.Struct({
|
|
54
|
+
defaults: import_effect.Schema.optional(import_effect.Schema.Struct({
|
|
55
|
+
severity: import_effect.Schema.optional(Severity),
|
|
56
|
+
owner: import_effect.Schema.optional(import_effect.Schema.String),
|
|
57
|
+
runbook: import_effect.Schema.optional(import_effect.Schema.String)
|
|
58
|
+
})),
|
|
59
|
+
rules: import_effect.Schema.Record(import_effect.Schema.String, Rule),
|
|
60
|
+
/** Detectors for conditions this deployment names itself. */
|
|
61
|
+
detectors: import_effect.Schema.optional(import_effect.Schema.Record(import_effect.Schema.String, Detector))
|
|
62
|
+
});
|
|
63
|
+
const defaultDetectors = {
|
|
64
|
+
"waiting-approval": { field: "status", value: "waiting-approval", eventTypes: ["control.*"] },
|
|
65
|
+
failed: { field: "status", value: "failed", eventTypes: ["control.*"] },
|
|
66
|
+
stalled: { field: "health", value: "stalled", eventTypes: ["control.*"] },
|
|
67
|
+
"quota-parked": { field: "waitingReason", value: "quota", eventTypes: ["control.*"] }
|
|
68
|
+
};
|
|
69
|
+
const names = (eventTypes, eventType) => eventTypes.some((named) => named.endsWith(".*") ? eventType.startsWith(named.slice(0, -1)) : named === eventType);
|
|
70
|
+
const recordSourceId = (id, outcome) => `/notifications/alerts/${id}/${outcome}`;
|
|
71
|
+
const coalescingKey = (runId, condition) => `${encodeURIComponent(runId)}:${encodeURIComponent(condition)}`;
|
|
72
|
+
const alertId = (alert) => `alert:${alert.coalescingKey}:${alert.since}`;
|
|
73
|
+
const detectorsOf = (policy) => ({
|
|
74
|
+
...defaultDetectors,
|
|
75
|
+
...policy.detectors
|
|
76
|
+
});
|
|
77
|
+
const payloadRecord = (payload) => typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : void 0;
|
|
78
|
+
const observe = (policy, detectors, entry, payload, open) => {
|
|
79
|
+
if (entry.eventType === deliveredEventType || entry.eventType === failedEventType) return;
|
|
80
|
+
for (const condition of Object.keys(policy.rules)) {
|
|
81
|
+
const detector = detectors[condition];
|
|
82
|
+
if (detector === void 0) continue;
|
|
83
|
+
if (detector.eventTypes !== void 0 && !names(detector.eventTypes, entry.eventType)) continue;
|
|
84
|
+
if (!Object.hasOwn(payload, detector.field)) continue;
|
|
85
|
+
if (payload[detector.field] === detector.value) {
|
|
86
|
+
if (!open.has(condition)) open.set(condition, entry.emittedAtMs);
|
|
87
|
+
} else {
|
|
88
|
+
open.delete(condition);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const conditions = (policy, runId, entries) => {
|
|
93
|
+
const detectors = detectorsOf(policy);
|
|
94
|
+
const open = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const payload = payloadRecord(entry.payload);
|
|
97
|
+
if (payload === void 0) continue;
|
|
98
|
+
observe(policy, detectors, entry, payload, open);
|
|
99
|
+
}
|
|
100
|
+
return Array.from(open, ([condition, since]) => ({ runId, condition, since }));
|
|
101
|
+
};
|
|
102
|
+
const decide = (policy, open, now) => open.flatMap((condition) => {
|
|
103
|
+
const rule = policy.rules[condition.condition];
|
|
104
|
+
if (rule === void 0) return [];
|
|
105
|
+
if (now - condition.since < rule.afterMs) return [];
|
|
106
|
+
const severity = rule.severity ?? policy.defaults?.severity ?? "warning";
|
|
107
|
+
const owner = rule.owner ?? policy.defaults?.owner;
|
|
108
|
+
const runbook = rule.runbook ?? policy.defaults?.runbook;
|
|
109
|
+
return [{
|
|
110
|
+
runId: condition.runId,
|
|
111
|
+
condition: condition.condition,
|
|
112
|
+
since: condition.since,
|
|
113
|
+
// Derived from the journal, not read off the clock. The alert id is
|
|
114
|
+
// stable across ticks, and the queue refuses a reused id whose content
|
|
115
|
+
// changed, so an alert stamped with the reading time would become
|
|
116
|
+
// permanently undeliverable the moment a tick refused and time moved on.
|
|
117
|
+
firedAt: condition.since + rule.afterMs,
|
|
118
|
+
severity,
|
|
119
|
+
coalescingKey: coalescingKey(condition.runId, condition.condition),
|
|
120
|
+
...owner === void 0 ? {} : { owner },
|
|
121
|
+
...runbook === void 0 ? {} : { runbook }
|
|
122
|
+
}];
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=AlertPolicy.js.map
|