@reventlessdev/reventless-aws 3.0.0-alpha.271 → 3.0.0-alpha.273

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +9 -8
  3. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +20 -16
  4. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +7 -7
  5. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +14 -5
  6. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +10 -3
  7. package/src/adapter/Api/Platform_UIFragments_Lambda.res +15 -13
  8. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +3 -2
  9. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +8 -1
  10. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +3 -2
  11. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +15 -13
  12. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +3 -2
  13. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +6 -12
  14. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +4 -8
  15. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res +155 -0
  16. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res.mjs +92 -0
  17. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +4 -9
  18. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +8 -2
  19. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +32 -62
  20. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +7 -20
  21. package/src/adapter/StateTopic/StateTopic_AppSync.res +15 -13
  22. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +3 -2
  23. package/src/adapter/Upload/Upload_Claim_S3.res +15 -13
  24. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +3 -2
  25. package/src/adapter/Upload/Upload_Presign_S3.res +15 -13
  26. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +3 -2
  27. package/src/components/Api/AppSync_Adapter.res +13 -9
  28. package/src/components/Api/AppSync_Adapter.res.mjs +4 -3
  29. package/src/util/Util_LambdaLogging.res +69 -21
  30. package/src/util/Util_LambdaLogging.res.mjs +21 -8
  31. package/src/util/Util_LogGroup_Adopting.res +480 -0
  32. package/src/util/Util_LogGroup_Adopting.res.mjs +365 -0
  33. package/src/util/Util_LogGroup_Adopting_Js.mjs +20 -0
  34. package/src/util/Util_LogRetention.res +11 -12
  35. package/tests/Util_LambdaLoggingTest.res +36 -0
  36. package/tests/Util_LambdaLoggingTest.res.mjs +22 -0
  37. package/tests/Util_LogGroup_AdoptingTest.res +169 -0
  38. package/tests/Util_LogGroup_AdoptingTest.res.mjs +208 -0
@@ -0,0 +1,92 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Aws from "@pulumi/aws";
4
+ import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
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$ReventlessAws from "../AWS.res.mjs";
9
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
10
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
11
+ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
12
+ import * as QueryInterception$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/QueryInterception/QueryInterception.res.mjs";
13
+ import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
14
+ import * as RuntimeEnvironment_Lambda$ReventlessAws from "../Runtime/RuntimeEnvironment_Lambda.res.mjs";
15
+
16
+ let byApi = new Map();
17
+
18
+ let namesUsed = {};
19
+
20
+ function nameFor() {
21
+ let owner = Stdlib_Option.getOr(ResourceAttribution$ReventlessCore.current.contents.plugin, "Platform");
22
+ let used = Stdlib_Option.getOr(namesUsed[owner], 0);
23
+ namesUsed[owner] = used + 1 | 0;
24
+ if (used === 0) {
25
+ return owner + "QueryInterceptor";
26
+ } else {
27
+ return owner + "QueryInterceptor" + (used + 1 | 0).toString();
28
+ }
29
+ }
30
+
31
+ function buildInterceptor(api, opts) {
32
+ let name = nameFor();
33
+ let componentOpts_parent = opts.parent;
34
+ let componentOpts = {
35
+ parent: componentOpts_parent
36
+ };
37
+ let packageDirs = Object.fromEntries([[
38
+ "@reventlessdev/reventless-aws",
39
+ Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
40
+ ]]);
41
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryInterceptor_Lambda.res.mjs", packageDirs, undefined, undefined);
42
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, {
43
+ TAG: "Other",
44
+ _0: "QueryInterceptor"
45
+ }, "Plugin", match.code, match.sourceCodeHash, undefined, 256, 10, undefined, undefined, undefined, undefined, undefined, componentOpts);
46
+ let lambdaArn = Output$Pulumi.flatMap(runtime.parts.lambda, lambda => lambda.arn);
47
+ let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Plugin", "Identity", "Plugin", undefined, undefined, undefined, undefined), opts);
48
+ Pulumi.all([
49
+ lambdaArn,
50
+ dataSourceRole.id
51
+ ]).apply(param => {
52
+ new (Aws.iam.RolePolicy)(name + "DataSource", {
53
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "DataSourcePolicy", [{
54
+ Sid: "AllowDataSourceInvokeInterceptor",
55
+ Effect: "Allow",
56
+ Action: "lambda:InvokeFunction",
57
+ Resource: param[0]
58
+ }])),
59
+ role: param[1]
60
+ }, opts);
61
+ });
62
+ return new (Aws.appsync.DataSource)(name + "DataSource", {
63
+ type: "AWS_LAMBDA",
64
+ apiId: Output$Pulumi.flatMap(api, api => api.id),
65
+ lambdaConfig: {
66
+ functionArn: lambdaArn
67
+ },
68
+ serviceRoleArn: dataSourceRole.arn
69
+ }, opts).name;
70
+ }
71
+
72
+ function dataSourceName(api, opts) {
73
+ if (!QueryInterception$ReventlessCore.isEnabled()) {
74
+ return;
75
+ }
76
+ let existing = byApi.get(api);
77
+ if (existing !== undefined) {
78
+ return existing;
79
+ }
80
+ let created = buildInterceptor(api, opts);
81
+ byApi.set(api, created);
82
+ return created;
83
+ }
84
+
85
+ export {
86
+ byApi,
87
+ namesUsed,
88
+ nameFor,
89
+ buildInterceptor,
90
+ dataSourceName,
91
+ }
92
+ /* byApi Not a pure module */
@@ -659,12 +659,9 @@ let warnSkippedExtension = (ext: extensionConfig) =>
659
659
  // ── Asset files (bundled alongside index.mjs in the code archive) ───────────
660
660
  // pluginDefinition is shipped as a file rather than packed into HANDLER_CONFIG
661
661
  // (UpdateFunctionConfiguration has a 5120-byte limit; pluginStructure alone blew
662
- // past it). process.cwd() is /var/task in Lambda, so a relative URL resolves
663
- // against the extracted zip.
662
+ // past it). process.cwd() is /var/task in Lambda, where the archive is extracted.
664
663
 
665
- type url
666
- @new external makeUrl: (string, string) => url = "URL"
667
- @module("node:fs") external readFileSyncUrl: (url, string) => string = "readFileSync"
664
+ let assetPath = (fileName: string): string => NodePath.join([NodeProcess.cwd(), fileName])
668
665
 
669
666
  // pluginDefinition.json is written at deploy time as a stable JSON literal
670
667
  // matching Reventless.Plugin.pluginDefinitionSchema (PluginRuntime_Builder), so
@@ -675,9 +672,7 @@ external asPluginDefinition: JSON.t => Reventless.Plugin.pluginDefinition = "%id
675
672
 
676
673
  let loadPluginDefinition = (): Reventless.Plugin.pluginDefinition =>
677
674
  try {
678
- readFileSyncUrl(makeUrl("./pluginDefinition.json", `file://${NodeProcess.cwd()}/`), "utf-8")
679
- ->JSON.parseOrThrow
680
- ->asPluginDefinition
675
+ assetPath("pluginDefinition.json")->NodeFs.readFileSync->JSON.parseOrThrow->asPluginDefinition
681
676
  } catch {
682
677
  | exn =>
683
678
  JsError.throwWithMessage(
@@ -691,7 +686,7 @@ let loadPluginDefinition = (): Reventless.Plugin.pluginDefinition =>
691
686
  // both map to None.
692
687
  let loadUiFragments = (): option<JSON.t> =>
693
688
  try {
694
- switch readFileSyncUrl(makeUrl("./uiFragments.json", `file://${NodeProcess.cwd()}/`), "utf-8")->JSON.parseOrThrow {
689
+ switch assetPath("uiFragments.json")->NodeFs.readFileSync->JSON.parseOrThrow {
695
690
  | JSON.Null => None
696
691
  | json => Some(json)
697
692
  }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
4
5
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
7
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
@@ -515,9 +516,13 @@ function warnSkippedExtension(ext) {
515
516
  });
516
517
  }
517
518
 
519
+ function assetPath(fileName) {
520
+ return Nodepath.join(process.cwd(), fileName);
521
+ }
522
+
518
523
  function loadPluginDefinition() {
519
524
  try {
520
- return JSON.parse(Nodefs.readFileSync(new URL("./pluginDefinition.json", `file://` + process.cwd() + `/`), "utf-8"));
525
+ return JSON.parse(Nodefs.readFileSync(assetPath("pluginDefinition.json"), "utf8"));
521
526
  } catch (raw_exn) {
522
527
  let exn = Primitive_exceptions.internalToException(raw_exn);
523
528
  return Stdlib_JsError.throwWithMessage("Failed to load pluginDefinition.json from asset zip: " + exnMessage(exn));
@@ -526,7 +531,7 @@ function loadPluginDefinition() {
526
531
 
527
532
  function loadUiFragments() {
528
533
  try {
529
- let json = JSON.parse(Nodefs.readFileSync(new URL("./uiFragments.json", `file://` + process.cwd() + `/`), "utf-8"));
534
+ let json = JSON.parse(Nodefs.readFileSync(assetPath("uiFragments.json"), "utf8"));
530
535
  if (json === null) {
531
536
  return;
532
537
  } else {
@@ -642,6 +647,7 @@ export {
642
647
  buildPublishToAggregates,
643
648
  extensionPublishDicts,
644
649
  warnSkippedExtension,
650
+ assetPath,
645
651
  loadPluginDefinition,
646
652
  loadUiFragments,
647
653
  pushHandler,
@@ -145,17 +145,7 @@ let makeFromCodeAsset: (
145
145
  let opts =
146
146
  opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
147
147
 
148
- // The environment tier drives retention, default log level and whether the log
149
- // group is Pulumi-managed. Resolved once from the stack name and the shared
150
- // prod allow-list (the same notion of "production" that names the host-shell
151
- // domain and the store layout).
152
148
  let stack = Pulumi.Pulumi.getStackName()
153
- let prodStacks = Util_HostUiDomain.resolveProdStacks()
154
- // Stacks explicitly held on auto-created log groups pending a `pulumi import`
155
- // (none today — the escape hatch for adopting a pre-existing stack later).
156
- let unmanagedStacks = Util_LogRetention.parseUnmanagedStacks(
157
- Util_LocalConfig.get("unmanagedLogGroupStacks")->Option.getOr(""),
158
- )
159
149
 
160
150
  // One attribution identity for every resource provisioned here: same owning
161
151
  // component and kind throughout, distinct piece roles — the function itself,
@@ -285,6 +275,26 @@ let makeFromCodeAsset: (
285
275
  variables->Dict.set("RUNTIME_EXTENSIONS", config->JSON.stringify->Pulumi.Input.make)
286
276
  }
287
277
 
278
+ // Managed CloudWatch log group with explicit retention, created **before** the
279
+ // function so the function can be told to write to it. That ordering is what
280
+ // keeps the deploy the owner: a group created after its function races the
281
+ // auto-create that the function's first invocation triggers, and losing that
282
+ // race is permanent — the group persists, so every retry fails identically.
283
+ // Runtimes with a heartbeat, a stream subscription or a schedule are invoked
284
+ // within seconds of existing, so they lost it most reliably.
285
+ //
286
+ // Created when the caller pins `~logRetentionDays` (the app-developer opt-in)
287
+ // OR the stack manages its groups — every stack by default, bar any named in
288
+ // `unmanagedLogGroupStacks`. When neither holds, Lambda auto-creates an
289
+ // unmanaged group with no retention, as before.
290
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
291
+ ~name,
292
+ ~retentionDaysOverride=?logRetentionDays,
293
+ ~tags=tagsFor(~resourceName=`${name}LogGroup`, ~role=Logs),
294
+ ~opts?,
295
+ (),
296
+ )
297
+
288
298
  let tags = tagsFor(~resourceName=name, ~role=Runtime)
289
299
  let lambda = Lambda.Function.make(
290
300
  ~name,
@@ -305,60 +315,20 @@ let makeFromCodeAsset: (
305
315
  vpcConfig: ?vpcConfig,
306
316
  environment: ({Lambda.Function.variables: variables}: Lambda.Function.functionEnvironment)
307
317
  ->Pulumi.Input.make,
318
+ // Points the function at the group above and, by reading its name output,
319
+ // orders the function after it.
320
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
308
321
  },
309
322
  ~opts?,
310
323
  )
311
324
 
312
- // Managed CloudWatch log-group with explicit retention. Created when the caller
313
- // pins `~logRetentionDays` (the app-developer opt-in) OR the stack manages its
314
- // groups every stack by default, bar any named in `unmanagedLogGroupStacks`
315
- // (see `Util_LogRetention.managesLogGroup`). When neither holds, Lambda
316
- // auto-creates an unmanaged group with no retention.
317
- //
318
- // Precedence for the retention value: explicit `~logRetentionDays` → the
319
- // `logRetentionDays` config key → the environment tier default.
320
- //
321
- // The group name derives from the function's *physical* name output, not the
322
- // logical `name`. `Lambda.Function` has no `name` arg, so its physical name
323
- // always carries Pulumi's `-<7hex>` suffix; a literal `/aws/lambda/${name}`
324
- // would create an empty group beside the real one, leave orphan accumulation
325
- // unchanged, and collide across stacks sharing an account.
326
- let managedRetention = switch (
327
- logRetentionDays,
328
- Util_LogRetention.managesLogGroup(~stack, ~unmanagedStacks),
329
- ) {
330
- | (Some(days), _) => Some(days)
331
- | (None, true) =>
332
- Some(
333
- Util_LogRetention.retentionDaysFor(
334
- ~stack,
335
- ~prodStacks,
336
- ~configOverride=?Util_LocalConfig.get("logRetentionDays")->Option.flatMap(s =>
337
- Int.fromString(s)
338
- ),
339
- ),
340
- )
341
- | (None, false) => None
342
- }
343
- managedRetention->Option.forEach(days => {
344
- let logGroup = Cloudwatch.LogGroup.make(
345
- ~name=`${name}LogGroup`,
346
- ~args={
347
- name: lambda.name
348
- ->Pulumi.Output.apply(n => `/aws/lambda/${n}`)
349
- ->Pulumi.Output.asInput,
350
- retentionInDays: days->Pulumi.Input.make,
351
- tags: tagsFor(~resourceName=`${name}LogGroup`, ~role=Logs),
352
- },
353
- ~opts?,
354
- )
355
-
356
- // DCB command-handler Lambdas: extract the provider-neutral metric lines
357
- // emitted by StateChangeSlice_Callback (`{reventlessMetric, slice, value}`)
358
- // into CloudWatch metrics. Attached to the managed log group above so there
359
- // is no race against Lambda's lazy auto-created group. Namespace/dimension
360
- // are CloudWatch-specific and live here, not in core (provider-neutral).
361
- if dcbMetrics {
325
+ // DCB command-handler Lambdas: extract the provider-neutral metric lines
326
+ // emitted by StateChangeSlice_Callback (`{reventlessMetric, slice, value}`)
327
+ // into CloudWatch metrics. Attached to the managed log group, which is the only
328
+ // group the function ever writes to. Namespace/dimension are CloudWatch-specific
329
+ // and live here, not in core (provider-neutral).
330
+ if dcbMetrics {
331
+ logGroup->Option.forEach(logGroup =>
362
332
  Util_DcbMetrics.metricNames->Array.forEach(metricName => {
363
333
  let _ = Cloudwatch.LogMetricFilter.make(
364
334
  ~name=`${name}${metricName}Filter`,
@@ -370,8 +340,8 @@ let makeFromCodeAsset: (
370
340
  ~opts?,
371
341
  )
372
342
  })
373
- }
374
- })
343
+ )
344
+ }
375
345
 
376
346
  let lambdaResource = lambda->Util.Lambda.functionToResource(~tags=tags->Pulumi.Output.fromInput)
377
347
 
@@ -22,10 +22,7 @@ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/
22
22
  import * as Util_IAM_Role$ReventlessAws from "../../util/Util_IAM_Role.res.mjs";
23
23
  import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
24
24
  import * as Util_DcbMetrics$ReventlessAws from "../../util/Util_DcbMetrics.res.mjs";
25
- import * as Util_LocalConfig$ReventlessAws from "../../util/Util_LocalConfig.res.mjs";
26
25
  import * as RuntimeExtension$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
27
- import * as Util_HostUiDomain$ReventlessAws from "../../util/Util_HostUiDomain.res.mjs";
28
- import * as Util_LogRetention$ReventlessAws from "../../util/Util_LogRetention.res.mjs";
29
26
  import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
30
27
  import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
31
28
 
@@ -59,8 +56,6 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
59
56
  let dcbMetrics = dcbMetricsOpt !== undefined ? dcbMetricsOpt : false;
60
57
  let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
61
58
  let stack = Pulumi.getStack();
62
- let prodStacks = Util_HostUiDomain$ReventlessAws.resolveProdStacks();
63
- let unmanagedStacks = Util_LogRetention$ReventlessAws.parseUnmanagedStacks(Stdlib_Option.getOr(Util_LocalConfig$ReventlessAws.get("unmanagedLogGroupStacks"), ""));
64
59
  let tagsFor = (resourceName, role) => AWS_Tags$ReventlessAws.make(resourceName, componentKind, role, undefined, name, undefined, undefined, undefined);
65
60
  let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), tagsFor(name, "Identity"), opts$1);
66
61
  Stdlib_Option.forEach(vpcConfig, param => {
@@ -136,6 +131,7 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
136
131
  ]);
137
132
  variables["RUNTIME_EXTENSIONS"] = JSON.stringify(config);
138
133
  }
134
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, logRetentionDays, tagsFor(name + `LogGroup`, "Logs"), opts$1, undefined);
139
135
  let tags = tagsFor(name, "Runtime");
140
136
  let lambda = new (Aws.lambda.Function)(name, {
141
137
  handler: "index.handler",
@@ -154,19 +150,11 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
154
150
  ephemeralStorage: Stdlib_Option.map(ephemeralStorageMb, mb => ({
155
151
  size: mb
156
152
  })),
157
- vpcConfig: vpcConfig
153
+ vpcConfig: vpcConfig,
154
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
158
155
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
159
- let match$1 = Util_LogRetention$ReventlessAws.managesLogGroup(stack, unmanagedStacks);
160
- let managedRetention = logRetentionDays !== undefined ? logRetentionDays : (
161
- match$1 ? Util_LogRetention$ReventlessAws.retentionDaysFor(stack, prodStacks, Stdlib_Option.flatMap(Util_LocalConfig$ReventlessAws.get("logRetentionDays"), s => Stdlib_Int.fromString(s, undefined))) : undefined
162
- );
163
- Stdlib_Option.forEach(managedRetention, days => {
164
- let logGroup = new (Aws.cloudwatch.LogGroup)(name + `LogGroup`, {
165
- name: lambda.name.apply(n => `/aws/lambda/` + n),
166
- retentionInDays: days,
167
- tags: tagsFor(name + `LogGroup`, "Logs")
168
- }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
169
- if (dcbMetrics) {
156
+ if (dcbMetrics) {
157
+ Stdlib_Option.forEach(logGroup, logGroup => {
170
158
  Util_DcbMetrics$ReventlessAws.metricNames.forEach(metricName => {
171
159
  new (Aws.cloudwatch.LogMetricFilter)(name + metricName + `Filter`, {
172
160
  pattern: Util_DcbMetrics$ReventlessAws.patternFor(metricName),
@@ -174,9 +162,8 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
174
162
  metricTransformation: Util_DcbMetrics$ReventlessAws.transformationFor(metricName)
175
163
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
176
164
  });
177
- return;
178
- }
179
- });
165
+ });
166
+ }
180
167
  let lambdaResource = Util_Lambda$ReventlessAws.functionToResource(tags, lambda);
181
168
  Monitoring$ReventlessCore.notify(unitKind, name, lambdaResource);
182
169
  return {
@@ -218,6 +218,20 @@ let finish = (
218
218
 
219
219
  let appsyncEndpoint = AppSync_EventsApi.httpEndpoint(eventsApi)
220
220
 
221
+ // Created before the function that writes to it — see
222
+ // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
223
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
224
+ ~name=name ++ "StateTopicPublisher",
225
+ ~tags=AWS.Tags.make(
226
+ ~name=name ++ "StateTopicPublisherLogGroup",
227
+ ~kind=ReventlessCore.QueryDb.componentType,
228
+ ~role=Logs,
229
+ ~component=name,
230
+ ),
231
+ ~opts,
232
+ (),
233
+ )
234
+
221
235
  let lambda = Lambda.Function.make(
222
236
  ~name=name ++ "StateTopicPublisher",
223
237
  ~args={
@@ -242,23 +256,11 @@ let finish = (
242
256
  ]),
243
257
  }: Lambda.Function.functionEnvironment
244
258
  )->Pulumi.Input.make,
259
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
245
260
  },
246
261
  ~opts,
247
262
  )
248
263
 
249
- Util_LambdaLogging.makeManagedLogGroup(
250
- ~name=name ++ "StateTopicPublisher",
251
- ~lambdaName=lambda.name,
252
- ~tags=AWS.Tags.make(
253
- ~name=name ++ "StateTopicPublisherLogGroup",
254
- ~kind=ReventlessCore.QueryDb.componentType,
255
- ~role=Logs,
256
- ~component=name,
257
- ),
258
- ~opts,
259
- (),
260
- )
261
-
262
264
  // One EventSourceMapping per stream, all targeting the shared Lambda.
263
265
  // Retry posture: the handler throws on 5xx / network errors and logs-and-
264
266
  // continues on 4xx. `maximumRetryAttempts: 3` bounds the per-record
@@ -90,6 +90,7 @@ function finish(eventsApi, opts) {
90
90
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs", packageDirs, undefined, false);
91
91
  let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
92
92
  let appsyncEndpoint = AppSync_EventsApi$ReventlessAws.httpEndpoint(eventsApi);
93
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "StateTopicPublisher", undefined, AWS_Tags$ReventlessAws.make(name + "StateTopicPublisherLogGroup", QueryDb$ReventlessCore.componentType, "Logs", undefined, name, undefined, undefined, undefined), opts, undefined);
93
94
  let lambda = new (Aws.lambda.Function)(name + "StateTopicPublisher", {
94
95
  handler: "index.handler",
95
96
  runtime: "nodejs22.x",
@@ -124,9 +125,9 @@ function finish(eventsApi, opts) {
124
125
  Util_LambdaLogging$ReventlessAws.logLevelEntry()
125
126
  ])
126
127
  },
127
- sourceCodeHash: match.sourceCodeHash
128
+ sourceCodeHash: match.sourceCodeHash,
129
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
128
130
  }, opts);
129
- Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "StateTopicPublisher", lambda.name, AWS_Tags$ReventlessAws.make(name + "StateTopicPublisherLogGroup", QueryDb$ReventlessCore.componentType, "Logs", undefined, name, undefined, undefined, undefined), opts, undefined);
130
131
  entries.forEach(entry => {
131
132
  let esmName = entry.topicName + "Stream2" + name + "StateTopic";
132
133
  new (Aws.lambda.EventSourceMapping)(esmName, {
@@ -266,6 +266,20 @@ let finish = (
266
266
  ->Option.getOr([])
267
267
  ->Pulumi.Input.make
268
268
 
269
+ // Created before the function that writes to it — see
270
+ // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
271
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
272
+ ~name,
273
+ ~tags=AWS.Tags.make(
274
+ ~name=name ++ "LogGroup",
275
+ ~kind=ReventlessCore.ComponentType.Plugin,
276
+ ~role=Logs,
277
+ ~component=name,
278
+ ),
279
+ ~opts,
280
+ (),
281
+ )
282
+
269
283
  let lambda = Lambda.Function.make(
270
284
  ~name,
271
285
  ~args={
@@ -295,23 +309,11 @@ let finish = (
295
309
  ]),
296
310
  }: Lambda.Function.functionEnvironment
297
311
  )->Pulumi.Input.make,
312
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
298
313
  },
299
314
  ~opts,
300
315
  )
301
316
 
302
- Util_LambdaLogging.makeManagedLogGroup(
303
- ~name,
304
- ~lambdaName=lambda.name,
305
- ~tags=AWS.Tags.make(
306
- ~name=name ++ "LogGroup",
307
- ~kind=ReventlessCore.ComponentType.Plugin,
308
- ~role=Logs,
309
- ~component=name,
310
- ),
311
- ~opts,
312
- (),
313
- )
314
-
315
317
  // One mapping per event log stream, all targeting the one Lambda.
316
318
  // `maximumRetryAttempts` bounds a permanently failing record so it cannot
317
319
  // wedge the shard — the default of -1 retries forever, which would turn one
@@ -125,6 +125,7 @@ function finish(plugin, stores, iteratorAgeAlarmMsOpt, opts) {
125
125
  ]]);
126
126
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Upload/Upload_Claim_S3_Ops.res.mjs", packageDirs, undefined, false);
127
127
  let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
128
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, undefined, AWS_Tags$ReventlessAws.make(name + "LogGroup", "Plugin", "Logs", undefined, name, undefined, undefined, undefined), opts, undefined);
128
129
  let lambda = new (Aws.lambda.Function)(name, {
129
130
  handler: "index.handler",
130
131
  runtime: "nodejs22.x",
@@ -159,9 +160,9 @@ function finish(plugin, stores, iteratorAgeAlarmMsOpt, opts) {
159
160
  Util_LambdaLogging$ReventlessAws.logLevelEntry()
160
161
  ])
161
162
  },
162
- sourceCodeHash: match.sourceCodeHash
163
+ sourceCodeHash: match.sourceCodeHash,
164
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
163
165
  }, opts);
164
- Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, lambda.name, AWS_Tags$ReventlessAws.make(name + "LogGroup", "Plugin", "Logs", undefined, name, undefined, undefined, undefined), opts, undefined);
165
166
  entries.forEach((entry, i) => {
166
167
  let esmName = name + `Stream` + i.toString();
167
168
  new (Aws.lambda.EventSourceMapping)(esmName, {
@@ -184,6 +184,20 @@ let make = (
184
184
  ->Option.getOr([])
185
185
  ->Pulumi.Input.make
186
186
 
187
+ // Created before the function that writes to it — see
188
+ // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
189
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
190
+ ~name=name ++ "Lambda",
191
+ ~tags=AWS.Tags.make(
192
+ ~name=name ++ "LambdaLogGroup",
193
+ ~kind=ReventlessCore.ComponentType.Platform,
194
+ ~role=Logs,
195
+ ~scope=Platform,
196
+ ),
197
+ ~opts,
198
+ (),
199
+ )
200
+
187
201
  let lambda = Lambda.Function.make(
188
202
  ~name=name ++ "Lambda",
189
203
  ~args={
@@ -213,23 +227,11 @@ let make = (
213
227
  ]),
214
228
  }: Lambda.Function.functionEnvironment
215
229
  )->Pulumi.Input.make,
230
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
216
231
  },
217
232
  ~opts,
218
233
  )
219
234
 
220
- Util_LambdaLogging.makeManagedLogGroup(
221
- ~name=name ++ "Lambda",
222
- ~lambdaName=lambda.name,
223
- ~tags=AWS.Tags.make(
224
- ~name=name ++ "LambdaLogGroup",
225
- ~kind=ReventlessCore.ComponentType.Platform,
226
- ~role=Logs,
227
- ~scope=Platform,
228
- ),
229
- ~opts,
230
- (),
231
- )
232
-
233
235
  let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
234
236
  ~name=name ++ "DataSource",
235
237
  ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
@@ -94,6 +94,7 @@ function make(api, stores, releaseWindowSecondsOpt, nameOpt, opts) {
94
94
  ]]);
95
95
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs", packageDirs, undefined, false);
96
96
  let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
97
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", undefined, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
97
98
  let lambda = new (Aws.lambda.Function)(name + "Lambda", {
98
99
  handler: "index.handler",
99
100
  runtime: "nodejs22.x",
@@ -128,9 +129,9 @@ function make(api, stores, releaseWindowSecondsOpt, nameOpt, opts) {
128
129
  Util_LambdaLogging$ReventlessAws.logLevelEntry()
129
130
  ])
130
131
  },
131
- sourceCodeHash: match.sourceCodeHash
132
+ sourceCodeHash: match.sourceCodeHash,
133
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
132
134
  }, opts$1);
133
- Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
134
135
  let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
135
136
  Pulumi.all([
136
137
  lambda.arn,
@@ -552,19 +552,23 @@ let _makeApiResourceWith = (
552
552
  // Managed log group with tiered retention for AppSync's own field-resolver logs,
553
553
  // which land in `/aws/appsync/apis/<id>` and otherwise live forever with no
554
554
  // retention. Managed on every stack by default (bar any in
555
- // `unmanagedLogGroupStacks`), same as the Lambda groups. The name derives from
556
- // the API id output so it matches the group AppSync writes to, and Pulumi
557
- // therefore tears it down with the API.
555
+ // `unmanagedLogGroupStacks`), same as the Lambda groups.
556
+ //
557
+ // Unlike a Lambda's group this one cannot be created ahead of the resource that
558
+ // writes to it: the name comes from the API's server-assigned id, so the group
559
+ // is always second and any request the API serves in between creates it first.
560
+ // `Util_LogGroup_Adopting` is what makes losing that order survivable — it
561
+ // adopts an existing group rather than failing `ResourceAlreadyExists`.
558
562
  let stack = Pulumi.Pulumi.getStackName()
559
563
  let prodStacks = Util_HostUiDomain.resolveProdStacks()
560
564
  let unmanagedStacks = Util_LogRetention.parseUnmanagedStacks(
561
565
  Util_LocalConfig.get("unmanagedLogGroupStacks")->Option.getOr(""),
562
566
  )
563
567
  if Util_LogRetention.managesLogGroup(~stack, ~unmanagedStacks) {
564
- let _ = Cloudwatch.LogGroup.make(
568
+ let _ = Util_LogGroup_Adopting.make(
565
569
  ~name=`${name}AppSyncLogGroup`,
566
- ~args={
567
- name: graphQLApi.id
570
+ ~props={
571
+ logGroupName: graphQLApi.id
568
572
  ->Pulumi.Output.apply(id => `/aws/appsync/apis/${id}`)
569
573
  ->Pulumi.Output.asInput,
570
574
  retentionInDays: Util_LogRetention.retentionDaysFor(
@@ -574,14 +578,14 @@ let _makeApiResourceWith = (
574
578
  Int.fromString(s)
575
579
  ),
576
580
  )->Pulumi.Input.make,
577
- tags: AWS.Tags.make(
581
+ tags: AWS.Tags.makeDict(
578
582
  ~name=`${name}AppSyncLogGroup`,
579
583
  ~kind=ReventlessCore.ComponentType.Plugin,
580
584
  ~role=Logs,
581
585
  ~scope=Plugin,
582
- ),
586
+ )->Pulumi.Input.make,
583
587
  },
584
- ~opts=customOpts,
588
+ ~opts=Some(customOpts),
585
589
  )
586
590
  }
587
591
 
@@ -21,6 +21,7 @@ import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core
21
21
  import * as Util_HostUiDomain$ReventlessAws from "../../util/Util_HostUiDomain.res.mjs";
22
22
  import * as Util_LogRetention$ReventlessAws from "../../util/Util_LogRetention.res.mjs";
23
23
  import * as AppSync_SdlDecorate$ReventlessAws from "./AppSync_SdlDecorate.res.mjs";
24
+ import * as Util_LogGroup_Adopting$ReventlessAws from "../../util/Util_LogGroup_Adopting.res.mjs";
24
25
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
25
26
 
26
27
  let log = Logger$ReventlessCore.fromEnv();
@@ -316,10 +317,10 @@ function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
316
317
  let prodStacks = Util_HostUiDomain$ReventlessAws.resolveProdStacks();
317
318
  let unmanagedStacks = Util_LogRetention$ReventlessAws.parseUnmanagedStacks(Stdlib_Option.getOr(Util_LocalConfig$ReventlessAws.get("unmanagedLogGroupStacks"), ""));
318
319
  if (Util_LogRetention$ReventlessAws.managesLogGroup(stack, unmanagedStacks)) {
319
- new (Aws.cloudwatch.LogGroup)(name + `AppSyncLogGroup`, {
320
- name: graphQLApi.id.apply(id => `/aws/appsync/apis/` + id),
320
+ Util_LogGroup_Adopting$ReventlessAws.make(name + `AppSyncLogGroup`, {
321
+ logGroupName: graphQLApi.id.apply(id => `/aws/appsync/apis/` + id),
321
322
  retentionInDays: Util_LogRetention$ReventlessAws.retentionDaysFor(stack, prodStacks, Stdlib_Option.flatMap(Util_LocalConfig$ReventlessAws.get("logRetentionDays"), s => Stdlib_Int.fromString(s, undefined))),
322
- tags: AWS_Tags$ReventlessAws.make(name + `AppSyncLogGroup`, "Plugin", "Logs", "Plugin", undefined, undefined, undefined, undefined)
323
+ tags: AWS_Tags$ReventlessAws.makeDict(name + `AppSyncLogGroup`, "Plugin", "Logs", "Plugin", undefined, undefined, undefined, undefined)
323
324
  }, customOpts);
324
325
  }
325
326
  return [