@reventlessdev/reventless-aws 3.0.0-alpha.337 → 3.0.0-alpha.338
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 +15 -0
- package/package.json +8 -8
- package/src/Platform.res +59 -5
- package/src/Platform.res.mjs +27 -4
- package/src/adapter/Monitoring/Monitoring_CloudWatch.res +197 -0
- package/src/adapter/Monitoring/Monitoring_CloudWatch.res.mjs +144 -0
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +10 -2
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +3 -2
- package/src/util/Util_AlarmSpec.res +155 -0
- package/src/util/Util_AlarmSpec.res.mjs +108 -0
- package/src/util/Util_DeadLetterQueue.res +70 -13
- package/src/util/Util_DeadLetterQueue.res.mjs +27 -6
- package/src/util/Util_ShellConfig.res +14 -0
- package/src/util/Util_ShellConfig.res.mjs +5 -1
- package/src/util/Util_StaticBundle.res +21 -0
- package/src/util/Util_StaticBundle.res.mjs +8 -0
- package/tests/Util_ShellConfigTest.res +42 -0
- package/tests/Util_ShellConfigTest.res.mjs +53 -15
- package/tests/util/Util_AlarmSpecTest.res +109 -0
- package/tests/util/Util_AlarmSpecTest.res.mjs +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.338 (2026-09-07)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** give the dead-letter handler a managed log group ([8631bd4](https://github.com/ReventlessDev/reventless-core/commit/8631bd457d168a5996c42e48db3c66d353bdda34))
|
|
11
|
+
* **aws:** stop a dead letter from being redelivered every three minutes ([1ae3a06](https://github.com/ReventlessDev/reventless-core/commit/1ae3a0686003c05079214a4eeeb60a38fcb775af))
|
|
12
|
+
* **aws:** the alarm backend stops wrapping Outputs in options ([476d974](https://github.com/ReventlessDev/reventless-core/commit/476d974223748024ae637b6cd5a6ebee95c21354))
|
|
13
|
+
* **logging:** log a message's identity, not its serialisation ([cd7467a](https://github.com/ReventlessDev/reventless-core/commit/cd7467abdc59609279f16f358cdd8884cc49f063))
|
|
14
|
+
* **ui:** a declared slot module is named, not only written ([de000ba](https://github.com/ReventlessDev/reventless-core/commit/de000baf428249f64cfc719ed52df3760198b82a))
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **aws:** alarm every execution unit a stack provisions ([46614af](https://github.com/ReventlessDev/reventless-core/commit/46614af92efb9596a09d3e7a15699712707b7ca6))
|
|
18
|
+
* **ui:** a deployment can name the module that draws its own surfaces ([b43828c](https://github.com/ReventlessDev/reventless-core/commit/b43828ca50cddd31f7aefae016669fcb7c059838))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.337 (2026-09-04)
|
|
7
22
|
|
|
8
23
|
* feat(spec)!: one optional encoding on the wire, with no annotation ([320f91d](https://github.com/ReventlessDev/reventless-core/commit/320f91daa8bd90812a6e82069e7a1cb473041930))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.338",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
"sury": "11.0.0-rc.2",
|
|
17
17
|
"uuid": "^13.0.0",
|
|
18
18
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.15",
|
|
19
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.9",
|
|
20
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
21
19
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
20
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
21
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.9",
|
|
22
22
|
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.8",
|
|
23
23
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
24
|
-
"@reventlessdev/
|
|
25
|
-
"@reventlessdev/reventless-
|
|
26
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.157",
|
|
24
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.257",
|
|
25
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.158",
|
|
27
26
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.35",
|
|
28
|
-
"@reventlessdev/
|
|
29
|
-
"@reventlessdev/reventless-
|
|
27
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
28
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.121",
|
|
29
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.130"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -1172,6 +1172,18 @@ module MakeWithConfig = (
|
|
|
1172
1172
|
// written; the shell treats the resulting 404 as "no hints" and boots
|
|
1173
1173
|
// unchanged. Single-mode shape: one origin-relative file per deployment.
|
|
1174
1174
|
uiHintsFile?: string,
|
|
1175
|
+
// Optional ES module registering AutoUI slot renderers — the part of a
|
|
1176
|
+
// surface that has to be drawn rather than said (a tile with the name over
|
|
1177
|
+
// the picture, a card face in a different type scale). When set, the deploy
|
|
1178
|
+
// reads it and writes it verbatim as a `ui-slots.js` BucketObject beside
|
|
1179
|
+
// `config.json`. Unset ⇒ no file written; the shell treats the resulting 404
|
|
1180
|
+
// as "no slots" and every mode draws its own regions.
|
|
1181
|
+
//
|
|
1182
|
+
// A module in the bundle's own origin rather than a fragment served from the
|
|
1183
|
+
// admin API, because the registry has to be populated for *every* caller —
|
|
1184
|
+
// including one served from a baked manifest, who issues no admin query and
|
|
1185
|
+
// for whom federation overrides therefore never apply.
|
|
1186
|
+
uiSlotsFile?: string,
|
|
1175
1187
|
// Optional baked component manifest. Declared here so a deployment can state
|
|
1176
1188
|
// it once; the emission that writes the file is not wired on this platform
|
|
1177
1189
|
// yet, and a declaration without it writes nothing.
|
|
@@ -2090,11 +2102,16 @@ module MakeWithConfig = (
|
|
|
2090
2102
|
~stableName=true,
|
|
2091
2103
|
// The explicit BucketObjects below write the production config.json
|
|
2092
2104
|
// (resolved API endpoints + Cognito IDs) and, when configured, a
|
|
2093
|
-
// ui-hints.json. The host-shell bundle's
|
|
2094
|
-
// public/ui-hints.json are dev-mode fallbacks —
|
|
2095
|
-
// bundle upload so the explicit BucketObjects
|
|
2096
|
-
// the same S3 keys.
|
|
2097
|
-
|
|
2105
|
+
// ui-hints.json and a ui-slots.js. The host-shell bundle's
|
|
2106
|
+
// public/config.json and public/ui-hints.json are dev-mode fallbacks —
|
|
2107
|
+
// exclude them from the bundle upload so the explicit BucketObjects
|
|
2108
|
+
// don't race the bundle on the same S3 keys.
|
|
2109
|
+
//
|
|
2110
|
+
// `ui-slots.js` is excluded on the same rule though the shell ships no
|
|
2111
|
+
// such file today, and that is the point: were one ever added, it would
|
|
2112
|
+
// be a fallback for *appearance*, and a deployment inheriting a
|
|
2113
|
+
// stranger's appearance is exactly the failure that is hard to notice.
|
|
2114
|
+
~excludeFiles=["config.json", "ui-hints.json", ReventlessCore.Platform_UiSlots.fileName],
|
|
2098
2115
|
~customDomain?,
|
|
2099
2116
|
// Served buckets front the host-shell distribution with `{prefix}/*`
|
|
2100
2117
|
// read paths (private, OAC-read) — same origin as the SPA, so served
|
|
@@ -2297,6 +2314,7 @@ module MakeWithConfig = (
|
|
|
2297
2314
|
~computed=withEvents,
|
|
2298
2315
|
~viewModes=?cfg.viewModes,
|
|
2299
2316
|
~bakedManifest=?cfg.bakedManifest,
|
|
2317
|
+
~uiSlotsFile=?cfg.uiSlotsFile,
|
|
2300
2318
|
~shellConfig=?cfg.shellConfig,
|
|
2301
2319
|
)
|
|
2302
2320
|
->JSON.Encode.object
|
|
@@ -2336,6 +2354,42 @@ module MakeWithConfig = (
|
|
|
2336
2354
|
)
|
|
2337
2355
|
}
|
|
2338
2356
|
|
|
2357
|
+
// Optional AutoUI slot renderers. Read verbatim and written beside the
|
|
2358
|
+
// hints file above — same seam, one file over, and the same rule that an
|
|
2359
|
+
// undeclared deployment writes nothing.
|
|
2360
|
+
//
|
|
2361
|
+
// Unlike the hints there is no content check at deploy time, because there
|
|
2362
|
+
// is no cheap one (see `readFileVerbatim`): a module is known to be good
|
|
2363
|
+
// once a browser has evaluated it. What the deploy owes instead is a
|
|
2364
|
+
// correct content type, which is the difference between the shell
|
|
2365
|
+
// importing this file and the shell refusing it — a module served as
|
|
2366
|
+
// `application/octet-stream` is blocked before it is read.
|
|
2367
|
+
//
|
|
2368
|
+
// Same origin as the bundle, deliberately: it rides this distribution, so
|
|
2369
|
+
// no CSP relaxation is involved and none is configured (this deploy sets
|
|
2370
|
+
// no response-headers policy at all). A slots file that loads locally and
|
|
2371
|
+
// silently fails on AWS is the worst shape this can take, and cross-origin
|
|
2372
|
+
// is how it would take it.
|
|
2373
|
+
switch cfg.uiSlotsFile {
|
|
2374
|
+
| None => ()
|
|
2375
|
+
| Some(slotsPath) =>
|
|
2376
|
+
let slotsContent = Util_StaticBundle.readFileVerbatim(
|
|
2377
|
+
~path=slotsPath,
|
|
2378
|
+
~label="host-ui ui-slots",
|
|
2379
|
+
)
|
|
2380
|
+
let _ = PulumiAws.S3.BucketObject.make(
|
|
2381
|
+
~name="host-ui-ui-slots-js",
|
|
2382
|
+
~args={
|
|
2383
|
+
bucket: bucketName->Pulumi.Output.asInput,
|
|
2384
|
+
// The same string the `uiSlotsUrl` above points at — see
|
|
2385
|
+
// `Platform_UiSlots` for why it is stated once.
|
|
2386
|
+
key: Pulumi.Input.make(ReventlessCore.Platform_UiSlots.fileName),
|
|
2387
|
+
content: Pulumi.Input.make(slotsContent),
|
|
2388
|
+
contentType: Pulumi.Input.make("application/javascript; charset=utf-8"),
|
|
2389
|
+
},
|
|
2390
|
+
)
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2339
2393
|
// The bake's write, granted here because here is where both facts are
|
|
2340
2394
|
// known: which bucket serves the shell, and that this deployment declared a
|
|
2341
2395
|
// manifest at all. Scoped to the one key the deploy told the shell to fetch
|
package/src/Platform.res.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import * as AppSync_EventsApi$ReventlessAws from "./adapter/Api/AppSync_EventsAp
|
|
|
51
51
|
import * as AppSync_MergedApi$ReventlessAws from "./components/Api/AppSync_MergedApi.res.mjs";
|
|
52
52
|
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
53
53
|
import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/NoEventMappings.res.mjs";
|
|
54
|
+
import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
|
|
54
55
|
import * as Upload_Presign_S3$ReventlessAws from "./adapter/Upload/Upload_Presign_S3.res.mjs";
|
|
55
56
|
import * as Util_HostUiDomain$ReventlessAws from "./util/Util_HostUiDomain.res.mjs";
|
|
56
57
|
import * as Util_StaticBundle$ReventlessAws from "./util/Util_StaticBundle.res.mjs";
|
|
@@ -1145,7 +1146,8 @@ function MakeWithConfig(Config) {
|
|
|
1145
1146
|
).concat(declaredServedBuckets);
|
|
1146
1147
|
let match$4 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", Stdlib_Option.getOr(hostUiBundle.bundleVersion, version), Stdlib_Option.getOr(hostUiBundle.assetsDir, Util_Bundle$ReventlessAws.resolvePackageRoot(true, "@reventlessdev/reventless-host-shell") + "/dist"), true, undefined, true, [
|
|
1147
1148
|
"config.json",
|
|
1148
|
-
"ui-hints.json"
|
|
1149
|
+
"ui-hints.json",
|
|
1150
|
+
Platform_UiSlots$ReventlessCore.fileName
|
|
1149
1151
|
], customDomain, servedBuckets);
|
|
1150
1152
|
let bucketName = match$4.bucketName;
|
|
1151
1153
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
@@ -1247,7 +1249,7 @@ function MakeWithConfig(Config) {
|
|
|
1247
1249
|
AppSync_EventsApi$ReventlessAws.clientNamespaceName
|
|
1248
1250
|
]
|
|
1249
1251
|
]) : computed;
|
|
1250
|
-
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
|
|
1252
|
+
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
|
|
1251
1253
|
});
|
|
1252
1254
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
1253
1255
|
bucket: bucketName,
|
|
@@ -1265,6 +1267,16 @@ function MakeWithConfig(Config) {
|
|
|
1265
1267
|
contentType: "application/json"
|
|
1266
1268
|
});
|
|
1267
1269
|
}
|
|
1270
|
+
let slotsPath = hostUiBundle.uiSlotsFile;
|
|
1271
|
+
if (slotsPath !== undefined) {
|
|
1272
|
+
let slotsContent = Util_StaticBundle$ReventlessAws.readFileVerbatim(slotsPath, "host-ui ui-slots");
|
|
1273
|
+
new (Aws.s3.BucketObject)("host-ui-ui-slots-js", {
|
|
1274
|
+
bucket: bucketName,
|
|
1275
|
+
key: Platform_UiSlots$ReventlessCore.fileName,
|
|
1276
|
+
content: slotsContent,
|
|
1277
|
+
contentType: "application/javascript; charset=utf-8"
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1268
1280
|
let match$9 = hostUiBundle.bakedManifest;
|
|
1269
1281
|
if (componentDefinitions !== undefined && match$9 !== undefined) {
|
|
1270
1282
|
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$9).map(param => param[0]);
|
|
@@ -2506,7 +2518,8 @@ function Make($star) {
|
|
|
2506
2518
|
).concat(declaredServedBuckets);
|
|
2507
2519
|
let match$4 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", Stdlib_Option.getOr(hostUiBundle.bundleVersion, version), Stdlib_Option.getOr(hostUiBundle.assetsDir, Util_Bundle$ReventlessAws.resolvePackageRoot(true, "@reventlessdev/reventless-host-shell") + "/dist"), true, undefined, true, [
|
|
2508
2520
|
"config.json",
|
|
2509
|
-
"ui-hints.json"
|
|
2521
|
+
"ui-hints.json",
|
|
2522
|
+
Platform_UiSlots$ReventlessCore.fileName
|
|
2510
2523
|
], customDomain, servedBuckets);
|
|
2511
2524
|
let bucketName = match$4.bucketName;
|
|
2512
2525
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
@@ -2608,7 +2621,7 @@ function Make($star) {
|
|
|
2608
2621
|
AppSync_EventsApi$ReventlessAws.clientNamespaceName
|
|
2609
2622
|
]
|
|
2610
2623
|
]) : computed;
|
|
2611
|
-
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
|
|
2624
|
+
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
|
|
2612
2625
|
});
|
|
2613
2626
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
2614
2627
|
bucket: bucketName,
|
|
@@ -2626,6 +2639,16 @@ function Make($star) {
|
|
|
2626
2639
|
contentType: "application/json"
|
|
2627
2640
|
});
|
|
2628
2641
|
}
|
|
2642
|
+
let slotsPath = hostUiBundle.uiSlotsFile;
|
|
2643
|
+
if (slotsPath !== undefined) {
|
|
2644
|
+
let slotsContent = Util_StaticBundle$ReventlessAws.readFileVerbatim(slotsPath, "host-ui ui-slots");
|
|
2645
|
+
new (Aws.s3.BucketObject)("host-ui-ui-slots-js", {
|
|
2646
|
+
bucket: bucketName,
|
|
2647
|
+
key: Platform_UiSlots$ReventlessCore.fileName,
|
|
2648
|
+
content: slotsContent,
|
|
2649
|
+
contentType: "application/javascript; charset=utf-8"
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2629
2652
|
let match$9 = hostUiBundle.bakedManifest;
|
|
2630
2653
|
if (componentDefinitions !== undefined && match$9 !== undefined) {
|
|
2631
2654
|
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$9).map(param => param[0]);
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// A CloudWatch alarm on every execution unit the framework provisions.
|
|
2
|
+
//
|
|
3
|
+
// Monitoring is deliberately not a framework concern — core announces a unit
|
|
4
|
+
// through the `Monitoring` seam and says nothing about what to do with it (see
|
|
5
|
+
// docs/plans/done/monitoring-hook-seam.md). This is one backend for that seam,
|
|
6
|
+
// living here rather than in core because it is provider-native to the bone:
|
|
7
|
+
// CloudWatch metrics, an SNS topic, an AWS notion of what "failing" means.
|
|
8
|
+
//
|
|
9
|
+
// It exists because the stacks this repo deploys itself had nothing attached, and
|
|
10
|
+
// those are the stacks where framework defects surface first — a wedged aggregate
|
|
11
|
+
// went unnoticed for twelve hours and was found by reading a bill. See
|
|
12
|
+
// docs/plans/no-monitoring-backend-on-deployed-stacks.md.
|
|
13
|
+
//
|
|
14
|
+
// A deploy program opts in with one line, before it builds its platform:
|
|
15
|
+
//
|
|
16
|
+
// ReventlessAws.Monitoring_CloudWatch.use()
|
|
17
|
+
//
|
|
18
|
+
// and the deployment decides whether that line does anything:
|
|
19
|
+
//
|
|
20
|
+
// alarmEmail ops@example.com creates a topic + email subscription
|
|
21
|
+
// alarmTopicArn arn:aws:sns:... publishes to a topic that exists
|
|
22
|
+
// alarmSilenceWindowSeconds 3600 how long a scheduler may be quiet
|
|
23
|
+
//
|
|
24
|
+
// With neither address configured the backend provisions nothing at all, so a
|
|
25
|
+
// stack that has not opted in deploys byte-identical to one built without this
|
|
26
|
+
// module. That is what makes `use()` safe to commit unconditionally.
|
|
27
|
+
//
|
|
28
|
+
// Each key is read through `Util_LocalConfig` first — `REVENTLESS_ALARM_EMAIL` in
|
|
29
|
+
// CI, a gitignored `Pulumi.local.yaml` on a workstation — before the stack's own
|
|
30
|
+
// `platform:` config. Where alerts go is per-deployment and often personal, and a
|
|
31
|
+
// tracked `Pulumi.<stack>.yaml` in a public repository is the wrong place for
|
|
32
|
+
// somebody's inbox.
|
|
33
|
+
open PulumiAws
|
|
34
|
+
|
|
35
|
+
module M = ReventlessCore.Monitoring
|
|
36
|
+
|
|
37
|
+
// Local override first (env var, then the gitignored sidecar), then stack config.
|
|
38
|
+
let configured = (key: string): option<string> =>
|
|
39
|
+
switch Util_LocalConfig.get(key) {
|
|
40
|
+
| Some(_) as local => local
|
|
41
|
+
| None => Pulumi.Config.make(Some("platform"))->Pulumi.Config.get(key)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let configuredEmail = () => configured("alarmEmail")
|
|
45
|
+
let configuredTopicArn = () => configured("alarmTopicArn")
|
|
46
|
+
|
|
47
|
+
let silenceWindowSeconds = () =>
|
|
48
|
+
configured("alarmSilenceWindowSeconds")
|
|
49
|
+
->Option.flatMap(Int.fromString(_))
|
|
50
|
+
->Option.getOr(Util_AlarmSpec.defaultSilenceWindowSeconds)
|
|
51
|
+
|
|
52
|
+
let topicName = "ReventlessAlarms"
|
|
53
|
+
|
|
54
|
+
// The memo's three states, spelled as a variant rather than an option: an
|
|
55
|
+
// `option<Pulumi.Output.t<_>>` is banned repo-wide because an Output is a Proxy
|
|
56
|
+
// that answers ReScript's nested-option probe, so one generic combinator on it
|
|
57
|
+
// silently yields None. "Not asked yet" and "asked, nothing configured" are
|
|
58
|
+
// distinct here anyway, which an option cannot say.
|
|
59
|
+
type topic =
|
|
60
|
+
| Unresolved
|
|
61
|
+
| NoTopic
|
|
62
|
+
| Topic(Pulumi.Output.t<string>)
|
|
63
|
+
|
|
64
|
+
// Resolved once, on the first alarm — so a stack that provisions no units, or one
|
|
65
|
+
// that has not configured an address, creates no topic either.
|
|
66
|
+
let resolvedTopic: ref<topic> = ref(Unresolved)
|
|
67
|
+
|
|
68
|
+
// An ARN the stack already owns wins over creating one: an estate with somewhere
|
|
69
|
+
// for alerts to go (a chat relay, a pager, one topic shared by several stacks)
|
|
70
|
+
// should point at it rather than accumulate a topic per stack that nobody reads.
|
|
71
|
+
let ensureTopicArn = (): topic =>
|
|
72
|
+
switch resolvedTopic.contents {
|
|
73
|
+
| Topic(_) as resolved => resolved
|
|
74
|
+
| NoTopic => NoTopic
|
|
75
|
+
| Unresolved =>
|
|
76
|
+
let resolved = switch (configuredTopicArn(), configuredEmail()) {
|
|
77
|
+
| (Some(arn), _) => Topic(arn->Pulumi.Output.make)
|
|
78
|
+
| (None, Some(email)) =>
|
|
79
|
+
let topic = SNS.Topic.make(
|
|
80
|
+
~name=topicName,
|
|
81
|
+
~args={
|
|
82
|
+
SNS.Topic.tags: AWS.Tags.make(
|
|
83
|
+
~name=topicName,
|
|
84
|
+
~kind=ReventlessCore.ComponentType.Plugin,
|
|
85
|
+
~role=Other("Alarm"),
|
|
86
|
+
~scope=Plugin,
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
let _subscription = SNS.TopicSubscription.make(
|
|
91
|
+
~name=topicName ++ "Email",
|
|
92
|
+
~args={
|
|
93
|
+
endpoint: email->Pulumi.Input.make,
|
|
94
|
+
topic: topic.arn->Pulumi.Output.asInput,
|
|
95
|
+
protocol: Email,
|
|
96
|
+
},
|
|
97
|
+
~opts=None,
|
|
98
|
+
)
|
|
99
|
+
Topic(topic.arn)
|
|
100
|
+
| (None, None) => NoTopic
|
|
101
|
+
}
|
|
102
|
+
resolvedTopic := resolved
|
|
103
|
+
resolved
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let alarmFor = (
|
|
107
|
+
~spec: Util_AlarmSpec.t,
|
|
108
|
+
~kind: M.unitKind,
|
|
109
|
+
~name: string,
|
|
110
|
+
~component: ReventlessInfra.Adapter.resource,
|
|
111
|
+
~plugin: option<string>,
|
|
112
|
+
~platform: option<string>,
|
|
113
|
+
~logLocator: Pulumi.Output.t<string>,
|
|
114
|
+
~topicArn: Pulumi.Output.t<string>,
|
|
115
|
+
) => {
|
|
116
|
+
let resourceName = Util_AlarmSpec.resourceName(~kind, ~name, ~plugin, ~suffix=spec.suffix)
|
|
117
|
+
let actions = [topicArn->Pulumi.Output.asInput]->Pulumi.Input.make
|
|
118
|
+
|
|
119
|
+
// The description is the only field a state-change message carries, so it is
|
|
120
|
+
// also where the log group goes — an alert names the unit and the metric and
|
|
121
|
+
// never where to read what happened. `~logLocator` is an Output, so the whole
|
|
122
|
+
// description resolves late; `""` is a unit with no logs of its own.
|
|
123
|
+
let describe = logs =>
|
|
124
|
+
Util_AlarmSpec.description(~kind, ~name, ~plugin, ~platform, ~spec, ~logs)
|
|
125
|
+
let alarmDescription =
|
|
126
|
+
logLocator
|
|
127
|
+
->Pulumi.Output.map(g => describe(g == "" ? None : Some(g)))
|
|
128
|
+
->Pulumi.Output.asInput
|
|
129
|
+
|
|
130
|
+
Cloudwatch.MetricAlarm.make(
|
|
131
|
+
~name=resourceName,
|
|
132
|
+
~args={
|
|
133
|
+
comparisonOperator: spec.comparisonOperator->Pulumi.Input.make,
|
|
134
|
+
evaluationPeriods: spec.evaluationPeriods->Pulumi.Input.make,
|
|
135
|
+
metricName: spec.metricName->Pulumi.Input.make,
|
|
136
|
+
namespace: spec.namespace->Pulumi.Input.make,
|
|
137
|
+
period: spec.period->Pulumi.Input.make,
|
|
138
|
+
statistic: spec.statistic->Pulumi.Input.make,
|
|
139
|
+
threshold: spec.threshold->Pulumi.Input.make,
|
|
140
|
+
// Every unit this seam announces on AWS is a Lambda function, which is what
|
|
141
|
+
// makes one dimension enough. A future non-Lambda execution unit needs the
|
|
142
|
+
// namespace and dimension to come from the announcement rather than from here.
|
|
143
|
+
dimensions: component.name
|
|
144
|
+
->Pulumi.Output.apply(fn => Dict.fromArray([("FunctionName", fn)]))
|
|
145
|
+
->Pulumi.Output.asInput,
|
|
146
|
+
treatMissingData: spec.treatMissingData->Pulumi.Input.make,
|
|
147
|
+
alarmActions: actions,
|
|
148
|
+
// Without these the channel reports every incident and no recovery, and a
|
|
149
|
+
// reader cannot tell an ongoing outage from one that cleared minutes later.
|
|
150
|
+
okActions: actions,
|
|
151
|
+
alarmDescription,
|
|
152
|
+
tags: AWS.Tags.make(
|
|
153
|
+
~name=resourceName,
|
|
154
|
+
~kind=ReventlessCore.ComponentType.Plugin,
|
|
155
|
+
~role=Other("Alarm"),
|
|
156
|
+
~component=name,
|
|
157
|
+
~plugin?,
|
|
158
|
+
~platform?,
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module Backend: M.Backend = {
|
|
165
|
+
let onProvisioned = (~kind, ~name, ~component, ~plugin, ~platform, ~logLocator) => {
|
|
166
|
+
// The seam hands the log address as an option; collapse it here, at the one
|
|
167
|
+
// boundary that has to, so no `option<Pulumi.Output.t<_>>` travels further.
|
|
168
|
+
let logLocator = switch logLocator {
|
|
169
|
+
| Some(locator) => locator
|
|
170
|
+
| None => ""->Pulumi.Output.make
|
|
171
|
+
}
|
|
172
|
+
switch ensureTopicArn() {
|
|
173
|
+
| Unresolved | NoTopic => ()
|
|
174
|
+
| Topic(topicArn) =>
|
|
175
|
+
Util_AlarmSpec.forKind(~kind, ~silenceWindowSeconds=silenceWindowSeconds())->Array.forEach(
|
|
176
|
+
spec => {
|
|
177
|
+
let _alarm = alarmFor(
|
|
178
|
+
~spec,
|
|
179
|
+
~kind,
|
|
180
|
+
~name,
|
|
181
|
+
~component,
|
|
182
|
+
~plugin,
|
|
183
|
+
~platform,
|
|
184
|
+
~logLocator,
|
|
185
|
+
~topicArn,
|
|
186
|
+
)
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Register this backend. Call it before building the platform — announcements
|
|
194
|
+
made earlier still arrive, because the seam holds them until someone
|
|
195
|
+
registers, but resources this creates must belong to the deploy program's own
|
|
196
|
+
run. Safe on a stack that configures no address: it provisions nothing. */
|
|
197
|
+
let use = () => M.use(module(Backend: M.Backend))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Aws from "@pulumi/aws";
|
|
5
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
8
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
9
|
+
import * as Monitoring$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Monitoring/Monitoring.res.mjs";
|
|
10
|
+
import * as Util_AlarmSpec$ReventlessAws from "../../util/Util_AlarmSpec.res.mjs";
|
|
11
|
+
import * as Util_LocalConfig$ReventlessAws from "../../util/Util_LocalConfig.res.mjs";
|
|
12
|
+
|
|
13
|
+
function configured(key) {
|
|
14
|
+
let local = Util_LocalConfig$ReventlessAws.get(key);
|
|
15
|
+
if (local !== undefined) {
|
|
16
|
+
return local;
|
|
17
|
+
} else {
|
|
18
|
+
return new Pulumi.Config("platform").get(key);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configuredEmail() {
|
|
23
|
+
return configured("alarmEmail");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function configuredTopicArn() {
|
|
27
|
+
return configured("alarmTopicArn");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function silenceWindowSeconds() {
|
|
31
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(configured("alarmSilenceWindowSeconds"), __x => Stdlib_Int.fromString(__x, undefined)), Util_AlarmSpec$ReventlessAws.defaultSilenceWindowSeconds);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let topicName = "ReventlessAlarms";
|
|
35
|
+
|
|
36
|
+
let resolvedTopic = {
|
|
37
|
+
contents: "Unresolved"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function ensureTopicArn() {
|
|
41
|
+
let resolved = resolvedTopic.contents;
|
|
42
|
+
if (typeof resolved === "object") {
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
if (resolved !== "Unresolved") {
|
|
46
|
+
return "NoTopic";
|
|
47
|
+
}
|
|
48
|
+
let match = configured("alarmTopicArn");
|
|
49
|
+
let match$1 = configured("alarmEmail");
|
|
50
|
+
let resolved$1;
|
|
51
|
+
if (match !== undefined) {
|
|
52
|
+
resolved$1 = {
|
|
53
|
+
TAG: "Topic",
|
|
54
|
+
_0: Pulumi.output(match)
|
|
55
|
+
};
|
|
56
|
+
} else if (match$1 !== undefined) {
|
|
57
|
+
let topic = new (Aws.sns.Topic)(topicName, {
|
|
58
|
+
tags: AWS_Tags$ReventlessAws.make(topicName, "Plugin", {
|
|
59
|
+
TAG: "Other",
|
|
60
|
+
_0: "Alarm"
|
|
61
|
+
}, "Plugin", undefined, undefined, undefined, undefined)
|
|
62
|
+
});
|
|
63
|
+
new (Aws.sns.TopicSubscription)(topicName + "Email", {
|
|
64
|
+
endpoint: match$1,
|
|
65
|
+
topic: topic.arn,
|
|
66
|
+
protocol: "email"
|
|
67
|
+
}, undefined);
|
|
68
|
+
resolved$1 = {
|
|
69
|
+
TAG: "Topic",
|
|
70
|
+
_0: topic.arn
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
resolved$1 = "NoTopic";
|
|
74
|
+
}
|
|
75
|
+
resolvedTopic.contents = resolved$1;
|
|
76
|
+
return resolved$1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function alarmFor(spec, kind, name, component, plugin, platform, logLocator, topicArn) {
|
|
80
|
+
let resourceName = Util_AlarmSpec$ReventlessAws.resourceName(kind, name, plugin, spec.suffix);
|
|
81
|
+
let actions = [topicArn];
|
|
82
|
+
let alarmDescription = Output$Pulumi.map(logLocator, g => {
|
|
83
|
+
let logs = g === "" ? undefined : g;
|
|
84
|
+
return Util_AlarmSpec$ReventlessAws.description(kind, name, plugin, platform, spec, logs);
|
|
85
|
+
});
|
|
86
|
+
return new (Aws.cloudwatch.MetricAlarm)(resourceName, {
|
|
87
|
+
comparisonOperator: spec.comparisonOperator,
|
|
88
|
+
evaluationPeriods: spec.evaluationPeriods,
|
|
89
|
+
metricName: spec.metricName,
|
|
90
|
+
namespace: spec.namespace,
|
|
91
|
+
period: spec.period,
|
|
92
|
+
statistic: spec.statistic,
|
|
93
|
+
threshold: spec.threshold,
|
|
94
|
+
dimensions: component.name.apply(fn => Object.fromEntries([[
|
|
95
|
+
"FunctionName",
|
|
96
|
+
fn
|
|
97
|
+
]])),
|
|
98
|
+
treatMissingData: spec.treatMissingData,
|
|
99
|
+
alarmActions: actions,
|
|
100
|
+
okActions: actions,
|
|
101
|
+
alarmDescription: alarmDescription,
|
|
102
|
+
tags: AWS_Tags$ReventlessAws.make(resourceName, "Plugin", {
|
|
103
|
+
TAG: "Other",
|
|
104
|
+
_0: "Alarm"
|
|
105
|
+
}, undefined, name, undefined, plugin, platform)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onProvisioned(kind, name, component, plugin, platform, logLocator) {
|
|
110
|
+
let logLocator$1 = logLocator !== undefined ? logLocator : Pulumi.output("");
|
|
111
|
+
let topicArn = ensureTopicArn();
|
|
112
|
+
if (typeof topicArn !== "object") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let topicArn$1 = topicArn._0;
|
|
116
|
+
Util_AlarmSpec$ReventlessAws.forKind(kind, silenceWindowSeconds()).forEach(spec => {
|
|
117
|
+
alarmFor(spec, kind, name, component, plugin, platform, logLocator$1, topicArn$1);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let Backend = {
|
|
122
|
+
onProvisioned: onProvisioned
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function use() {
|
|
126
|
+
Monitoring$ReventlessCore.use(Backend);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let M;
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
M,
|
|
133
|
+
configured,
|
|
134
|
+
configuredEmail,
|
|
135
|
+
configuredTopicArn,
|
|
136
|
+
silenceWindowSeconds,
|
|
137
|
+
topicName,
|
|
138
|
+
resolvedTopic,
|
|
139
|
+
ensureTopicArn,
|
|
140
|
+
alarmFor,
|
|
141
|
+
Backend,
|
|
142
|
+
use,
|
|
143
|
+
}
|
|
144
|
+
/* @pulumi/aws Not a pure module */
|
|
@@ -98,7 +98,11 @@ let queryByTableName = (
|
|
|
98
98
|
limit,
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
ReventlessCore.EffectLogger.logDebug(
|
|
101
|
+
ReventlessCore.EffectLogger.logDebug(
|
|
102
|
+
~comp=__MODULE__,
|
|
103
|
+
"queryByTableName params: " ++
|
|
104
|
+
params->JSON.stringifyAny->Option.getOr("")->ReventlessCore.LogFormat.truncate,
|
|
105
|
+
)
|
|
102
106
|
->Effect.flatMap(_ =>
|
|
103
107
|
Util_DynamoDb_Runtime.queryStream(params)
|
|
104
108
|
->Stream.runCollect
|
|
@@ -130,7 +134,11 @@ let scanByTableName = (~tableName, ~filterConfigs, ~limit) => {
|
|
|
130
134
|
expressionAttributeValues: ?attributeValues,
|
|
131
135
|
limit,
|
|
132
136
|
}
|
|
133
|
-
ReventlessCore.EffectLogger.logDebug(
|
|
137
|
+
ReventlessCore.EffectLogger.logDebug(
|
|
138
|
+
~comp=__MODULE__,
|
|
139
|
+
"scanByTableName params: " ++
|
|
140
|
+
params->JSON.stringifyAny->Option.getOr("")->ReventlessCore.LogFormat.truncate,
|
|
141
|
+
)
|
|
134
142
|
->Effect.flatMap(_ =>
|
|
135
143
|
Util_DynamoDb_Runtime.scanStream(params)
|
|
136
144
|
->Stream.runCollect
|
|
@@ -8,6 +8,7 @@ import * as Effect from "effect/Effect";
|
|
|
8
8
|
import * as Pulumi from "@pulumi/pulumi";
|
|
9
9
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
10
|
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
11
|
+
import * as LogFormat$ReventlessCore from "@reventlessdev/reventless-core/src/util/LogFormat.res.mjs";
|
|
11
12
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
12
13
|
import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
|
|
13
14
|
import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
|
|
@@ -166,7 +167,7 @@ function queryByTableName(tableName, keyOpt, id, subIdConfig, filterConfigsOpt,
|
|
|
166
167
|
Limit: params_Limit,
|
|
167
168
|
ScanIndexForward: params_ScanIndexForward
|
|
168
169
|
};
|
|
169
|
-
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "queryByTableName params: " + Stdlib_Option.getOr(JSON.stringify(params), "")), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.queryStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
|
|
170
|
+
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "queryByTableName params: " + LogFormat$ReventlessCore.truncate(Stdlib_Option.getOr(JSON.stringify(params), ""))), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.queryStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
|
|
170
171
|
let msg = DynamoDb_Error$ReventlessAws.message(err);
|
|
171
172
|
return Effect.map(EffectLogger$ReventlessCore.logError("QueryEngine_DynamoDb-ReventlessAws", undefined, "queryByTableName: " + msg), () => []);
|
|
172
173
|
})));
|
|
@@ -196,7 +197,7 @@ function scanByTableName(tableName, filterConfigs, limit) {
|
|
|
196
197
|
FilterExpression: params_FilterExpression,
|
|
197
198
|
Limit: params_Limit
|
|
198
199
|
};
|
|
199
|
-
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "scanByTableName params: " + Stdlib_Option.getOr(JSON.stringify(params), "")), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
|
|
200
|
+
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "scanByTableName params: " + LogFormat$ReventlessCore.truncate(Stdlib_Option.getOr(JSON.stringify(params), ""))), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
|
|
200
201
|
let msg = DynamoDb_Error$ReventlessAws.message(err);
|
|
201
202
|
return Effect.map(EffectLogger$ReventlessCore.logError("QueryEngine_DynamoDb-ReventlessAws", undefined, "scanByTableName: " + msg), () => []);
|
|
202
203
|
})));
|