@empire-builder-kit/nx 1.0.0-rc.43 → 1.0.0-rc.45

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 (88) hide show
  1. package/package.json +3 -3
  2. package/src/blueprints/external-sandbox.d.ts +1 -1
  3. package/src/cli/main.d.ts +11 -5
  4. package/src/cli/main.js +88 -29
  5. package/src/container-images.d.ts +1 -1
  6. package/src/container-images.js +1 -1
  7. package/src/executors/deployment-plan/deployment-plan.js +5 -1
  8. package/src/executors/deployment-plan/schema.json +1 -1
  9. package/src/executors/fleet/fleet.js +4 -0
  10. package/src/executors/fleet/schema.json +1 -1
  11. package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
  12. package/src/executors/local-deploy/federated-credential-transport.js +15 -5
  13. package/src/foundation/cost-estimate.d.ts +8 -3
  14. package/src/foundation/cost-estimate.js +42 -19
  15. package/src/foundation/index.d.ts +1 -0
  16. package/src/foundation/index.js +1 -0
  17. package/src/foundation/local-private-smoke-access.js +6 -0
  18. package/src/foundation/logging.d.ts +3 -1
  19. package/src/foundation/logging.js +8 -1
  20. package/src/foundation/observability.d.ts +42 -0
  21. package/src/foundation/observability.js +181 -0
  22. package/src/foundation/preflight.js +1 -1
  23. package/src/generators/function/function.js +5 -2
  24. package/src/generators/observability-infrastructure.d.ts +2 -0
  25. package/src/generators/observability-infrastructure.js +31 -0
  26. package/src/generators/observability-policy.d.ts +4 -0
  27. package/src/generators/observability-policy.js +23 -0
  28. package/src/generators/preset/dependency-overrides.d.ts +34 -0
  29. package/src/generators/preset/dependency-overrides.js +35 -1
  30. package/src/generators/preset/tool-versions.d.ts +2 -2
  31. package/src/generators/preset/tool-versions.js +2 -2
  32. package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
  33. package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
  34. package/src/generators/preset/workspace-files-agent-development.js +42 -0
  35. package/src/generators/preset/workspace-files-thin-deploy.js +12 -3
  36. package/src/generators/preset/workspace-files-thin-docs.js +10 -22
  37. package/src/generators/preset/workspace-files.d.ts +3 -0
  38. package/src/generators/preset/workspace-files.js +93 -8
  39. package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
  40. package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
  41. package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
  42. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
  43. package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
  44. package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
  45. package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
  46. package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
  47. package/src/generators/slice/built-in-plan-metadata-files.js +33 -1
  48. package/src/generators/slice/built-in-plan-operations-files.js +106 -6
  49. package/src/generators/slice/built-in-plan-project-files.js +1 -0
  50. package/src/generators/slice/built-in-plan-release-files.js +2 -43
  51. package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
  52. package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
  53. package/src/generators/slice/built-in-plan-shared.js +29 -2
  54. package/src/generators/slice/built-in-plan-web-files.js +18 -1
  55. package/src/generators/slice/built-in-plan.js +1 -0
  56. package/src/generators/slice/slice.js +2 -0
  57. package/src/generators/upgrade/upgrade.d.ts +4 -3
  58. package/src/generators/upgrade/upgrade.js +50 -26
  59. package/src/generators/workload/frozen-container.d.ts +1 -1
  60. package/src/generators/workload/service-performance-files.js +22 -5
  61. package/src/generators/workload/workload.js +5 -1
  62. package/src/local-dev/dev-command-core.d.ts +24 -3
  63. package/src/local-dev/dev-command-core.js +124 -12
  64. package/src/local-dev/errors.d.ts +19 -1
  65. package/src/local-dev/errors.js +18 -1
  66. package/src/local-dev/fleet-client.d.ts +64 -6
  67. package/src/local-dev/fleet-client.js +189 -15
  68. package/src/local-dev/fleet-process.d.ts +47 -2
  69. package/src/local-dev/fleet-process.js +248 -31
  70. package/src/local-dev/fleet-runtime.d.ts +37 -0
  71. package/src/local-dev/fleet-runtime.js +127 -69
  72. package/src/local-dev/fleet-session.d.ts +1 -0
  73. package/src/local-dev/fleet-session.js +42 -3
  74. package/src/local-dev/fleet-state.d.ts +5 -0
  75. package/src/local-dev/fleet-state.js +11 -2
  76. package/src/local-dev/fleet-supervisor.d.ts +6 -0
  77. package/src/local-dev/fleet-supervisor.js +49 -1
  78. package/src/local-dev/ports.d.ts +11 -1
  79. package/src/local-dev/ports.js +78 -38
  80. package/src/local-dev/slice-runner.d.ts +19 -1
  81. package/src/local-dev/slice-runner.js +290 -91
  82. package/src/repository-policy/deployment-plan.d.ts +1 -1
  83. package/src/repository-policy/deployment-plan.js +16 -1
  84. package/src/secrets/generated-aws-control-plane.d.ts +11 -1
  85. package/src/secrets/generated-aws-control-plane.js +92 -29
  86. package/src/secrets/operator.d.ts +3 -2
  87. package/src/secrets/operator.js +27 -5
  88. package/src/supply-chain/generated-files.js +9 -0
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderProductInfrastructureFiles = renderProductInfrastructureFiles;
4
+ const built_in_plan_alarm_identity_js_1 = require("./built-in-plan-alarm-identity.js");
4
5
  const built_in_plan_shared_js_1 = require("./built-in-plan-shared.js");
5
6
  const built_in_plan_key_value_retention_infrastructure_js_1 = require("./built-in-plan-key-value-retention-infrastructure.js");
6
7
  /** @internal */
@@ -8,7 +9,11 @@ function renderProductInfrastructureFiles(name, pascalName, workspace, protected
8
9
  const identifierPrefix = (0, built_in_plan_shared_js_1.lowerCamelIdentifier)(pascalName);
9
10
  const recordExportWorkflowName = (0, built_in_plan_shared_js_1.stableGeneratedName)(40, name);
10
11
  const files = {
11
- 'infra/event-subscriptions.ts': `/** Read the existing seeded event contract; never broaden source or stage routing. */
12
+ 'infra/event-subscriptions.ts': `/**
13
+ * Same-Slice routing reads only the Slice's own seeded EventContractSchema and
14
+ * never broadens its source or stage filter. Foreign Slices are routed only
15
+ * through the seeded contracts/event-subscriptions.json declaration below.
16
+ */
12
17
  export function sameSliceEventDetailTypes(value: unknown): string[] {
13
18
  const contract = value as {
14
19
  kind?: unknown;
@@ -46,6 +51,104 @@ export function sameSliceEventDetailTypes(value: unknown): string[] {
46
51
  types.delete('DataLifecycleChanged');
47
52
  return [...types].sort();
48
53
  }
54
+
55
+ /** One exact queue rule for each foreign Slice that the seeded declaration names. */
56
+ export interface CrossSliceEventSubscription {
57
+ readonly detailTypes: readonly string[];
58
+ readonly ruleName: string;
59
+ readonly schemaVersions: readonly number[];
60
+ readonly source: string;
61
+ }
62
+
63
+ function exactKeys(
64
+ value: unknown,
65
+ keys: readonly string[],
66
+ ): value is Record<string, unknown> {
67
+ return !!value && typeof value === 'object' && !Array.isArray(value) &&
68
+ Object.keys(value).length === keys.length &&
69
+ keys.every((key) => Object.hasOwn(value, key));
70
+ }
71
+
72
+ function subscriptionError(detail: string): Error {
73
+ return new Error('contracts/event-subscriptions.json ' + detail);
74
+ }
75
+
76
+ /**
77
+ * Validate the seeded cross-Slice declaration before any rule registers.
78
+ * Source and detail type are delivery filters, not authentication: the
79
+ * consumer authorizes every effect from its own data and policy.
80
+ */
81
+ export function crossSliceEventSubscriptions(
82
+ declaration: unknown,
83
+ dependencies: unknown,
84
+ ): CrossSliceEventSubscription[] {
85
+ if (
86
+ !exactKeys(declaration, ['schemaVersion', 'subscriptions']) ||
87
+ declaration.schemaVersion !== 1 ||
88
+ !Array.isArray(declaration.subscriptions) ||
89
+ declaration.subscriptions.length > 8
90
+ ) throw subscriptionError('must be a v1 declaration with at most 8 source subscriptions.');
91
+ if (
92
+ !exactKeys(dependencies, ['consumes', 'schemaVersion']) ||
93
+ dependencies.schemaVersion !== 1 ||
94
+ !Array.isArray(dependencies.consumes)
95
+ ) throw subscriptionError('requires the v1 contracts/dependencies.json declaration.');
96
+ const consumes = new Set<unknown>(dependencies.consumes);
97
+ const sources = new Set<string>();
98
+ const ruleNames = new Set<string>();
99
+ const subscriptions: CrossSliceEventSubscription[] = [];
100
+ for (const raw of declaration.subscriptions as unknown[]) {
101
+ if (
102
+ !exactKeys(raw, ['events', 'source']) || typeof raw.source !== 'string' ||
103
+ raw.source.length > 63 || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(raw.source) ||
104
+ raw.source === '${name}' || sources.has(raw.source)
105
+ ) throw subscriptionError('requires unique foreign Slice sources other than ${name}.');
106
+ const source = raw.source;
107
+ sources.add(source);
108
+ const contracts = '@${workspace}/' + source + '/contracts';
109
+ if (!consumes.has(contracts))
110
+ throw subscriptionError('source ' + source + ' requires ' + contracts + ' in contracts/dependencies.json consumes.');
111
+ if (!Array.isArray(raw.events) || raw.events.length < 1 || raw.events.length > 32)
112
+ throw subscriptionError('source ' + source + ' must declare 1 to 32 events.');
113
+ const detailTypes = new Set<string>();
114
+ let schemaVersions: number[] | undefined;
115
+ for (const event of raw.events as unknown[]) {
116
+ if (
117
+ !exactKeys(event, ['detailType', 'schemaVersions']) ||
118
+ typeof event.detailType !== 'string' ||
119
+ !/^[A-Za-z][A-Za-z0-9._-]{0,127}$/.test(event.detailType) ||
120
+ event.detailType === 'DataLifecycleChanged' || detailTypes.has(event.detailType)
121
+ ) throw subscriptionError('source ' + source + ' requires unique literal detail types other than DataLifecycleChanged.');
122
+ const declared: unknown = event.schemaVersions;
123
+ if (
124
+ !Array.isArray(declared) || declared.length < 1 || declared.length > 8 ||
125
+ declared.some((version) => typeof version !== 'number' ||
126
+ !Number.isSafeInteger(version) || version < 1) ||
127
+ new Set(declared).size !== declared.length
128
+ ) throw subscriptionError('source ' + source + ' requires 1 to 8 unique schemaVersions of at least 1.');
129
+ const versions = (declared as number[]).slice().sort((left, right) => left - right);
130
+ // One rule filters detail type and schema version independently, so
131
+ // every event of one source shares one version set to stay exact.
132
+ if (schemaVersions && schemaVersions.join(',') !== versions.join(','))
133
+ throw subscriptionError('source ' + source + ' must declare the same schemaVersions for every event.');
134
+ schemaVersions = versions;
135
+ detailTypes.add(event.detailType);
136
+ }
137
+ const ruleName = '${pascalName}' + source.split('-').map((part) =>
138
+ part.charAt(0).toUpperCase() + part.slice(1)).join('') + 'SubscribedEventsRule';
139
+ if (!schemaVersions || ruleNames.has(ruleName))
140
+ throw subscriptionError('sources must derive unique rule identities.');
141
+ ruleNames.add(ruleName);
142
+ subscriptions.push({
143
+ detailTypes: [...detailTypes].sort(),
144
+ ruleName,
145
+ schemaVersions,
146
+ source,
147
+ });
148
+ }
149
+ return subscriptions.sort((left, right) =>
150
+ left.source < right.source ? -1 : left.source > right.source ? 1 : 0);
151
+ }
49
152
  `,
50
153
  'infra/route-function-materialization.ts': `type RouteFunctionArgs = {
51
154
  ebkRouteMaterialization?: unknown;
@@ -388,6 +491,7 @@ export function validateSuccessfulDeploymentReceipt(
388
491
  'infra/product.ts': `import {
389
492
  createManagedLogGroupContract,
390
493
  createRouterOriginProofInjection,
494
+ resolveObservabilityPolicy,
391
495
  ${sql ? ' provisionDatabaseBindings,\n' : ''}\
392
496
  } from '@empire-builder-kit/nx/foundation';
393
497
  import * as random from '@pulumi/random';
@@ -420,8 +524,14 @@ import {
420
524
  ${identifierPrefix}UseRecordDelegation,
421
525
  } from '../api/contracts/protected.js';
422
526
  import lifecycleConsumers from '../data/lifecycle-consumers.json';
527
+ import observabilityPolicy from '../../../ebk.observability.json';
528
+ import contractDependencies from '../contracts/dependencies.json';
529
+ import eventSubscriptions from '../contracts/event-subscriptions.json';
423
530
  import { EventContractSchema } from '../contracts/events.js';
424
- import { sameSliceEventDetailTypes } from './event-subscriptions.js';
531
+ import {
532
+ crossSliceEventSubscriptions,
533
+ sameSliceEventDetailTypes,
534
+ } from './event-subscriptions.js';
425
535
  ${sql ? "import databaseBindings from '../database/bindings.json';\n" : ''}\
426
536
  ${sql ? "import { createSqlOutboxInfrastructure } from './sql-outbox.js';\n" : ''}\
427
537
  ${"import { createKeyValueInfrastructure } from './key-value.js';\n"}\
@@ -464,6 +574,13 @@ import { validateSuccessfulDeploymentReceipt } from './deployment-receipt.js';
464
574
  // <ebk:composition-imports:start>
465
575
  // <ebk:composition-imports:end>
466
576
 
577
+ const observability = resolveObservabilityPolicy({
578
+ policy: observabilityPolicy,
579
+ stage: $app.stage,
580
+ productionProfile: process.env.EBK_PRODUCTION_FOUNDATION_PROFILE,
581
+ local: $dev,
582
+ });
583
+
467
584
  function protectedPerformanceDeploymentAuthority(input: {
468
585
  enabled: string;
469
586
  releaseSourceCommit: string;
@@ -578,6 +695,7 @@ function managedLogGroup(
578
695
  function managedLogContract(component: string, resource?: string) {
579
696
  return createManagedLogGroupContract({
580
697
  component,
698
+ retentionInDays: observability.protectedRetentionDays,
581
699
  ...(resource ? { resource } : {}),
582
700
  slice: '${name}',
583
701
  stage: $app.stage,
@@ -593,14 +711,22 @@ function operationalAlarmDescription(input: {
593
711
  return JSON.stringify({
594
712
  businessSymptom: input.businessSymptom,
595
713
  id: input.id,
596
- owner: '${name}-product',
597
- route: '${name}-on-call',
714
+ owner: '${(0, built_in_plan_alarm_identity_js_1.sliceAlarmOwnership)(name).owner}',
715
+ route: '${(0, built_in_plan_alarm_identity_js_1.sliceAlarmOwnership)(name).route}',
598
716
  runbook: input.runbook,
599
717
  schemaVersion: 1,
600
718
  slice: '${name}',
601
719
  });
602
720
  }
603
721
 
722
+ // sst.aws.Queue has no name member: QueueName dimensions read the physical
723
+ // aws.sqs.Queue name through nodes (nodes.queue.id is the queue URL).
724
+ function physicalQueueName<Q extends { nodes: { queue: { name: unknown } } }>(
725
+ queue: Q,
726
+ ): Q['nodes']['queue']['name'] {
727
+ return queue.nodes.queue.name;
728
+ }
729
+
604
730
  function managedLogDataProtection(
605
731
  logicalName: string,
606
732
  contract: ReturnType<typeof createManagedLogGroupContract>,
@@ -620,12 +746,7 @@ function managedLogDataProtection(
620
746
  logicalName + 'DataProtectionFindings',
621
747
  {
622
748
  alarmActions: [alertTopicArn],
623
- alarmDescription: operationalAlarmDescription({
624
- businessSymptom:
625
- 'A managed log group contains a protected-data finding.',
626
- id: 'data-protection-findings',
627
- runbook: 'docs/runbooks/log-investigation.md',
628
- }),
749
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'data-protection-findings', ' ')}),
629
750
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
630
751
  dimensions: { LogGroupName: logGroupName },
631
752
  evaluationPeriods: 1,
@@ -645,14 +766,26 @@ function lambdaLogging(logGroupName: any, functionName?: any) {
645
766
  return {
646
767
  logging: { format: 'json' as const, logGroup: logGroupName },
647
768
  transform: {
648
- function: {
649
- ...(functionName ? { name: functionName } : {}),
650
- loggingConfig: {
769
+ function(args: Record<string, any>): undefined {
770
+ if (functionName) args.name = functionName;
771
+ args.environment = $output(args.environment ?? {}).apply(
772
+ (environment: Record<string, any>) => ({
773
+ ...environment,
774
+ variables: $output(environment.variables ?? {}).apply(
775
+ (variables: Record<string, any>) => ({
776
+ ...variables,
777
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
778
+ String(observability.routineSuccessSampleRate),
779
+ }),
780
+ ),
781
+ }),
782
+ );
783
+ args.loggingConfig = {
651
784
  applicationLogLevel: 'TRACE',
652
785
  logFormat: 'JSON',
653
786
  logGroup: logGroupName,
654
- systemLogLevel: 'INFO',
655
- },
787
+ systemLogLevel: observability.systemLogLevel,
788
+ };
656
789
  },
657
790
  },
658
791
  };
@@ -664,6 +797,11 @@ export function product() {
664
797
  $dev ? 'development' : $app.stage,
665
798
  );
666
799
  const asyncAdaptersConfigured = assertAsyncAdapterDeploymentReady();
800
+ // Declared cross-Slice subscriptions fail closed before any resource registers.
801
+ const crossSliceSubscriptions = crossSliceEventSubscriptions(
802
+ eventSubscriptions,
803
+ contractDependencies,
804
+ );
667
805
  const secretLinks = createStandardSliceSecretLinks();
668
806
  const performanceHmacLogicalName =
669
807
  'GeneratedPerformanceLoadHmacKeys';
@@ -780,12 +918,7 @@ ${` const keyValue = createKeyValueInfrastructure({
780
918
  '${pascalName}KeyValueStreamDeliveryErrors',
781
919
  {
782
920
  alarmActions: [foundation.discovery.alertTopicArn],
783
- alarmDescription: operationalAlarmDescription({
784
- businessSymptom:
785
- 'The key-value stream delivery Function failed an invocation.',
786
- id: 'key-value-stream-delivery-errors',
787
- runbook: 'docs/runbooks/async-recovery.md',
788
- }),
921
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'key-value-stream-delivery-errors', ' ')}),
789
922
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
790
923
  dimensions: { FunctionName: keyValue.dispatcher.name },
791
924
  evaluationPeriods: 1,
@@ -802,12 +935,7 @@ ${` const keyValue = createKeyValueInfrastructure({
802
935
  '${pascalName}KeyValueStreamIteratorAge',
803
936
  {
804
937
  alarmActions: [foundation.discovery.alertTopicArn],
805
- alarmDescription: operationalAlarmDescription({
806
- businessSymptom:
807
- 'Prompt domain-event delivery from the key-value stream is falling behind.',
808
- id: 'key-value-stream-iterator-age',
809
- runbook: 'docs/runbooks/async-recovery.md',
810
- }),
938
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'key-value-stream-iterator-age', ' ')}),
811
939
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
812
940
  dimensions: { FunctionName: keyValue.dispatcher.name },
813
941
  evaluationPeriods: 1,
@@ -824,11 +952,7 @@ ${` const keyValue = createKeyValueInfrastructure({
824
952
  '${pascalName}KeyValueReconciliationErrors',
825
953
  {
826
954
  alarmActions: [foundation.discovery.alertTopicArn],
827
- alarmDescription: operationalAlarmDescription({
828
- businessSymptom: 'Durable domain-event recovery is failing.',
829
- id: 'key-value-reconciliation-errors',
830
- runbook: 'docs/runbooks/async-recovery.md',
831
- }),
955
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'key-value-reconciliation-errors', ' ')}),
832
956
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
833
957
  dimensions: { FunctionName: keyValue.reconciler.name },
834
958
  evaluationPeriods: 1,
@@ -849,11 +973,7 @@ ${` const keyValue = createKeyValueInfrastructure({
849
973
  '${pascalName}OutboxBacklog',
850
974
  {
851
975
  alarmActions: [foundation.discovery.alertTopicArn],
852
- alarmDescription: operationalAlarmDescription({
853
- businessSymptom: 'Durable domain-event intents remain undelivered.',
854
- id: 'outbox-backlog',
855
- runbook: 'docs/runbooks/async-recovery.md',
856
- }),
976
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'outbox-backlog', ' ')}),
857
977
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
858
978
  dimensions: { Slice: '${name}', Stage: $app.stage },
859
979
  evaluationPeriods: 2,
@@ -870,11 +990,7 @@ ${` const keyValue = createKeyValueInfrastructure({
870
990
  '${pascalName}EventIntentRejected',
871
991
  {
872
992
  alarmActions: [foundation.discovery.alertTopicArn],
873
- alarmDescription: operationalAlarmDescription({
874
- businessSymptom: 'A retained domain-event intent cannot be delivered.',
875
- id: 'event-intent-rejected',
876
- runbook: 'docs/runbooks/async-recovery.md',
877
- }),
993
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'event-intent-rejected', ' ')}),
878
994
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
879
995
  dimensions: { Slice: '${name}', Stage: $app.stage },
880
996
  evaluationPeriods: 1,
@@ -891,12 +1007,7 @@ ${` const keyValue = createKeyValueInfrastructure({
891
1007
  '${pascalName}EventDeliveryAttempts',
892
1008
  {
893
1009
  alarmActions: [foundation.discovery.alertTopicArn],
894
- alarmDescription: operationalAlarmDescription({
895
- businessSymptom:
896
- 'A domain-event intent is repeatedly failing delivery.',
897
- id: 'event-delivery-attempts',
898
- runbook: 'docs/runbooks/async-recovery.md',
899
- }),
1010
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'event-delivery-attempts', ' ')}),
900
1011
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
901
1012
  dimensions: { Slice: '${name}', Stage: $app.stage },
902
1013
  evaluationPeriods: 1,
@@ -970,6 +1081,8 @@ ${sql
970
1081
  userPoolArn: foundation.discovery.identityUserPoolArn,
971
1082
  }),
972
1083
  environment: {
1084
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1085
+ String(observability.routineSuccessSampleRate),
973
1086
  EBK_SLICE: '${name}',
974
1087
  EBK_STAGE: $app.stage,
975
1088
  EBK_WORKSPACE: '${workspace}',
@@ -1065,21 +1178,21 @@ ${sql
1065
1178
  'async-inbox-cleanup',
1066
1179
  foundation.discovery.alertTopicArn,
1067
1180
  );
1068
- const asyncProviderWorkerLogs = managedLogGroup(
1181
+ const asyncProviderWorkerLogs = asyncAdaptersConfigured || observability.retainDormantComponentLogs ? managedLogGroup(
1069
1182
  '${pascalName}AsyncProviderWorkerLogs',
1070
1183
  'async-provider-worker',
1071
1184
  foundation.discovery.alertTopicArn,
1072
- );
1185
+ ) : undefined;
1073
1186
  const asyncScheduleTargetLogs = managedLogGroup(
1074
1187
  '${pascalName}AsyncScheduleTargetLogs',
1075
1188
  'async-schedule-target',
1076
1189
  foundation.discovery.alertTopicArn,
1077
1190
  );
1078
- const asyncWebhookIngressLogs = managedLogGroup(
1191
+ const asyncWebhookIngressLogs = asyncAdaptersConfigured || observability.retainDormantComponentLogs ? managedLogGroup(
1079
1192
  '${pascalName}AsyncWebhookIngressLogs',
1080
1193
  'async-webhook-ingress',
1081
1194
  foundation.discovery.alertTopicArn,
1082
- );
1195
+ ) : undefined;
1083
1196
  const asyncWorkflowTaskLogs = managedLogGroup(
1084
1197
  '${pascalName}AsyncWorkflowTaskLogs',
1085
1198
  'async-workflow-task',
@@ -1174,6 +1287,8 @@ ${network ? ' ...(performanceDeploymentAuthority ? { vpc: foundation.vpc } :
1174
1287
  api.route(${identifierPrefix}Health.method + ' ' + ${identifierPrefix}Health.path, {
1175
1288
  permissions: nativeAuthority.permissions('api-health'),
1176
1289
  environment: {
1290
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1291
+ String(observability.routineSuccessSampleRate),
1177
1292
  EBK_SOURCE_COMMIT: releaseSourceCommit,
1178
1293
  EBK_STAGE: $app.stage,
1179
1294
  },
@@ -1188,6 +1303,8 @@ ${network ? ' ...(performanceDeploymentAuthority ? { vpc: foundation.vpc } :
1188
1303
  {
1189
1304
  permissions: nativeAuthority.permissions('api-machine-authorization-proof'),
1190
1305
  environment: {
1306
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1307
+ String(observability.routineSuccessSampleRate),
1191
1308
  ${' ...keyValue.environment,\n'}\
1192
1309
  AUTH_CLIENT_ID: authClient.id,
1193
1310
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1221,6 +1338,8 @@ ${' ...keyValue.environment,\n'}\
1221
1338
  routeInVpc(route.operation.method + ' ' + route.operation.path, {
1222
1339
  permissions: nativeAuthority.permissions('api-delegations'),
1223
1340
  environment: {
1341
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1342
+ String(observability.routineSuccessSampleRate),
1224
1343
  ${' ...keyValue.environment,\n'}\
1225
1344
  AUTH_CLIENT_ID: authClient.id,
1226
1345
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1275,6 +1394,8 @@ ${' ...keyValue.environment,\n'}\
1275
1394
  routeInVpc(route.operation.method + ' ' + route.operation.path, {
1276
1395
  permissions: nativeAuthority.permissions('api-data-rights'),
1277
1396
  environment: {
1397
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1398
+ String(observability.routineSuccessSampleRate),
1278
1399
  ${' ...keyValue.environment,\n'}\
1279
1400
  AUTH_CLIENT_ID: authClient.id,
1280
1401
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1308,6 +1429,8 @@ ${' ...keyValue.environment,\n'}\
1308
1429
  routeInVpc(route.operation.method + ' ' + route.operation.path, {
1309
1430
  permissions: nativeAuthority.permissions('api-database-records'),
1310
1431
  environment: {
1432
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1433
+ String(observability.routineSuccessSampleRate),
1311
1434
  ${' ...keyValue.environment,\n'}\
1312
1435
  AUTH_CLIENT_ID: authClient.id,
1313
1436
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1328,6 +1451,8 @@ ${' ...keyValue.environment,\n'}\
1328
1451
  {
1329
1452
  permissions: nativeAuthority.permissions('api-protected'),
1330
1453
  environment: {
1454
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1455
+ String(observability.routineSuccessSampleRate),
1331
1456
  ${' ...keyValue.environment,\n'}\
1332
1457
  AUTH_CLIENT_ID: authClient.id,
1333
1458
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1348,6 +1473,8 @@ ${' ...keyValue.environment,\n'}\
1348
1473
  {
1349
1474
  permissions: nativeAuthority.permissions('api-database-health'),
1350
1475
  environment: {
1476
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1477
+ String(observability.routineSuccessSampleRate),
1351
1478
  ${' ...keyValue.environment,\n'}\
1352
1479
  AUTH_CLIENT_ID: authClient.id,
1353
1480
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1378,6 +1505,8 @@ ${' ...keyValue.environment,\n'}\
1378
1505
  routeInVpc(route.operation.method + ' ' + route.operation.path, {
1379
1506
  permissions: nativeAuthority.permissions('api-database-records'),
1380
1507
  environment: {
1508
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1509
+ String(observability.routineSuccessSampleRate),
1381
1510
  ${' ...keyValue.environment,\n'}\
1382
1511
  AUTH_CLIENT_ID: authClient.id,
1383
1512
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1409,6 +1538,8 @@ ${' ...keyValue.environment,\n'}\
1409
1538
  routeInVpc(route.operation.method + ' ' + route.operation.path, {
1410
1539
  permissions: nativeAuthority.permissions('api-database-records'),
1411
1540
  environment: {
1541
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1542
+ String(observability.routineSuccessSampleRate),
1412
1543
  ${' ...keyValue.environment,\n'}\
1413
1544
  AUTH_CLIENT_ID: authClient.id,
1414
1545
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1429,6 +1560,8 @@ ${' ...keyValue.environment,\n'}\
1429
1560
  {
1430
1561
  permissions: nativeAuthority.permissions('api-publish-created'),
1431
1562
  environment: {
1563
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1564
+ String(observability.routineSuccessSampleRate),
1432
1565
  ${' ...keyValue.environment,\n'}\
1433
1566
  AUTH_CLIENT_ID: authClient.id,
1434
1567
  AUTH_MACHINE_ACCEPTED_CLIENT_IDS:
@@ -1452,6 +1585,8 @@ ${' ...keyValue.environment,\n'}\
1452
1585
  {
1453
1586
  permissions: machineAuthorization.proof.permissions,
1454
1587
  environment: {
1588
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1589
+ String(observability.routineSuccessSampleRate),
1455
1590
  ...machineAuthorization.proof.environment,
1456
1591
  AUTH_MACHINE_PROBE_URL:
1457
1592
  $interpolate\`\${api.url}\${${identifierPrefix}MachineAuthorizationProof.path}\`,
@@ -1467,6 +1602,8 @@ ${' ...keyValue.environment,\n'}\
1467
1602
  {
1468
1603
  permissions: dataExportStore.cleanupPermissions,
1469
1604
  environment: {
1605
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1606
+ String(observability.routineSuccessSampleRate),
1470
1607
  ${' ...keyValue.environment,\n'}\
1471
1608
  DATA_EXPORT_BUCKET_NAME: dataExportStore.bucket.bucket,
1472
1609
  DATA_EXPORT_KMS_KEY_ARN: dataExportStore.encryptionKey.arn,
@@ -1570,6 +1707,8 @@ ${''}\
1570
1707
  );
1571
1708
  eventQueue.subscribe({
1572
1709
  environment: {
1710
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
1711
+ String(observability.routineSuccessSampleRate),
1573
1712
  ${' ...keyValue.environment,\n'}\
1574
1713
  EBK_STAGE: $app.stage,
1575
1714
  EBK_WORKLOAD_IDENTITY: '${name}:event-consumer',
@@ -1605,6 +1744,26 @@ ${''}\
1605
1744
  },
1606
1745
  },
1607
1746
  );
1747
+ // One rule per declared foreign source: EventBridge ORs values within a
1748
+ // field and ANDs fields, so a merged rule would cross-match sources and types.
1749
+ for (const subscription of crossSliceSubscriptions) {
1750
+ foundation.eventBus.subscribeQueue(
1751
+ subscription.ruleName,
1752
+ eventQueue,
1753
+ {
1754
+ pattern: {
1755
+ detail: {
1756
+ meta: {
1757
+ schemaVersion: [...subscription.schemaVersions],
1758
+ stage: [$app.stage],
1759
+ },
1760
+ },
1761
+ detailType: [...subscription.detailTypes],
1762
+ source: [subscription.source],
1763
+ },
1764
+ },
1765
+ );
1766
+ }
1608
1767
 
1609
1768
  const eventConsumerAlarmDefinition = defineEventConsumerAlarms({
1610
1769
  consumer: '${pascalName}Created',
@@ -1618,18 +1777,14 @@ ${''}\
1618
1777
  }
1619
1778
  const dlqAlarm = new aws.cloudwatch.MetricAlarm('${pascalName}EventDlqDepth', {
1620
1779
  alarmActions: [foundation.discovery.alertTopicArn],
1621
- alarmDescription: operationalAlarmDescription({
1622
- businessSymptom: 'Domain-event processing has terminal failures.',
1623
- id: 'event-dlq-depth',
1624
- runbook: 'docs/runbooks/async-recovery.md',
1625
- }),
1780
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'event-dlq-depth', ' ')}),
1626
1781
  name: '${name}-' + $app.stage + '-event-dlq-depth',
1627
1782
  comparisonOperator: eventConsumerAlarmDefinition.comparisonOperator,
1628
- dimensions: { QueueName: eventDlq.name },
1783
+ dimensions: { QueueName: physicalQueueName(eventDlq) },
1629
1784
  evaluationPeriods: eventConsumerAlarmDefinition.evaluationPeriods,
1630
1785
  metricName: eventConsumerAlarmDefinition.metricName,
1631
1786
  namespace: eventConsumerAlarmDefinition.namespace,
1632
- period: eventConsumerAlarmDefinition.periodSeconds,
1787
+ period: 300,
1633
1788
  statistic: 'Maximum',
1634
1789
  threshold: eventConsumerAlarmDefinition.threshold,
1635
1790
  treatMissingData: 'notBreaching',
@@ -1683,22 +1838,18 @@ ${''}\
1683
1838
  alertTopicName: foundation.discovery.alertTopicArn.apply(
1684
1839
  (alertTopicArn: string) => alertTopicArn.split(':').at(-1) ?? '',
1685
1840
  ),
1686
- apiId: api.id,
1841
+ apiId: api.nodes.api.id,
1687
1842
  ${' databaseTableName: keyValue.table.name,\n'}\
1688
1843
  functionName: dataExportCleanup.name,
1689
- providerQueueName: providerQueue.name,
1844
+ providerQueueName: physicalQueueName(providerQueue),
1690
1845
  });
1691
1846
  const providerDlqAlarm = new aws.cloudwatch.MetricAlarm(
1692
1847
  '${pascalName}ProviderDlqDepth',
1693
1848
  {
1694
1849
  alarmActions: [foundation.discovery.alertTopicArn],
1695
- alarmDescription: operationalAlarmDescription({
1696
- businessSymptom: 'Provider deliveries have terminal failures.',
1697
- id: 'provider-dlq-depth',
1698
- runbook: 'docs/runbooks/async-recovery.md',
1699
- }),
1850
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'provider-dlq-depth', ' ')}),
1700
1851
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
1701
- dimensions: { QueueName: providerDlq.name },
1852
+ dimensions: { QueueName: physicalQueueName(providerDlq) },
1702
1853
  evaluationPeriods: 1,
1703
1854
  metricName: 'ApproximateNumberOfMessagesVisible',
1704
1855
  name: '${name}-' + $app.stage + '-provider-dlq-depth',
@@ -1713,13 +1864,9 @@ ${' databaseTableName: keyValue.table.name,\n'}\
1713
1864
  '${pascalName}AsyncTargetDlqDepth',
1714
1865
  {
1715
1866
  alarmActions: [foundation.discovery.alertTopicArn],
1716
- alarmDescription: operationalAlarmDescription({
1717
- businessSymptom: 'An asynchronous target has terminal failures.',
1718
- id: 'async-target-dlq-depth',
1719
- runbook: 'docs/runbooks/async-recovery.md',
1720
- }),
1867
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'async-target-dlq-depth', ' ')}),
1721
1868
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
1722
- dimensions: { QueueName: asyncTargetDlq.name },
1869
+ dimensions: { QueueName: physicalQueueName(asyncTargetDlq) },
1723
1870
  evaluationPeriods: 1,
1724
1871
  metricName: 'ApproximateNumberOfMessagesVisible',
1725
1872
  name: '${name}-' + $app.stage + '-async-target-dlq-depth',
@@ -1833,7 +1980,6 @@ ${' databaseTableName: keyValue.table.name,\n'}\
1833
1980
  contentType: 'application/json',
1834
1981
  description:
1835
1982
  '${name} immutable generated default configuration.',
1836
- versionLabel: releaseSourceCommit,
1837
1983
  },
1838
1984
  );
1839
1985
  const deploymentStrategy =
@@ -1898,9 +2044,14 @@ ${' databaseTableName: keyValue.table.name,\n'}\
1898
2044
  // <ebk:workload-registrations:end>
1899
2045
 
1900
2046
  if (asyncAdaptersConfigured) {
2047
+ if (!asyncProviderWorkerLogs || !asyncWebhookIngressLogs) {
2048
+ throw new Error('Configured async adapters require their protected log groups.');
2049
+ }
1901
2050
  providerQueue.subscribe(
1902
2051
  {
1903
2052
  environment: {
2053
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2054
+ String(observability.routineSuccessSampleRate),
1904
2055
  ${' ...keyValue.environment,\n'}\
1905
2056
  EBK_STAGE: $app.stage,
1906
2057
  EBK_WORKLOAD_IDENTITY: '${name}:async-provider-worker',
@@ -1928,6 +2079,8 @@ ${''}\
1928
2079
  api.route('POST /${name}/webhooks/provider', {
1929
2080
  permissions: nativeAuthority.permissions('async-webhook-ingress'),
1930
2081
  environment: {
2082
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2083
+ String(observability.routineSuccessSampleRate),
1931
2084
  EBK_ASYNC_LEDGER_TABLE: asyncRecoveryLedger.name,
1932
2085
  EBK_STAGE: $app.stage,
1933
2086
  EBK_WORKLOAD_IDENTITY: '${name}:async-webhook-ingress',
@@ -1947,6 +2100,8 @@ ${''}\
1947
2100
  {
1948
2101
  permissions: nativeAuthority.permissions('async-outbox-relay'),
1949
2102
  environment: {
2103
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2104
+ String(observability.routineSuccessSampleRate),
1950
2105
  ${' ...keyValue.environment,\n'}\
1951
2106
  ${''}\
1952
2107
  EBK_STAGE: $app.stage,
@@ -1979,11 +2134,7 @@ ${''}\
1979
2134
  '${pascalName}SqlOutboxRelayErrors',
1980
2135
  {
1981
2136
  alarmActions: [foundation.discovery.alertTopicArn],
1982
- alarmDescription: operationalAlarmDescription({
1983
- businessSymptom: 'Relational event delivery is failing.',
1984
- id: 'sql-outbox-relay-errors',
1985
- runbook: 'docs/runbooks/async-recovery.md',
1986
- }),
2137
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'sql-outbox-relay-errors', ' ')}),
1987
2138
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
1988
2139
  dimensions: { FunctionName: sqlOutbox.relay.name },
1989
2140
  evaluationPeriods: 1,
@@ -2002,11 +2153,7 @@ ${''}\
2002
2153
  '${pascalName}SqlOutboxBacklog',
2003
2154
  {
2004
2155
  alarmActions: [foundation.discovery.alertTopicArn],
2005
- alarmDescription: operationalAlarmDescription({
2006
- businessSymptom: 'Relational event intents remain undelivered.',
2007
- id: 'sql-outbox-backlog',
2008
- runbook: 'docs/runbooks/async-recovery.md',
2009
- }),
2156
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'sql-outbox-backlog', ' ')}),
2010
2157
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
2011
2158
  dimensions: { Slice: '${name}', Stage: $app.stage },
2012
2159
  evaluationPeriods: 2,
@@ -2023,12 +2170,7 @@ ${''}\
2023
2170
  '${pascalName}SqlOutboxAttempts',
2024
2171
  {
2025
2172
  alarmActions: [foundation.discovery.alertTopicArn],
2026
- alarmDescription: operationalAlarmDescription({
2027
- businessSymptom:
2028
- 'A relational event intent is repeatedly failing delivery.',
2029
- id: 'sql-outbox-attempts',
2030
- runbook: 'docs/runbooks/async-recovery.md',
2031
- }),
2173
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'sql-outbox-attempts', ' ')}),
2032
2174
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
2033
2175
  dimensions: { Slice: '${name}', Stage: $app.stage },
2034
2176
  evaluationPeriods: 1,
@@ -2048,6 +2190,8 @@ ${''}\
2048
2190
  {
2049
2191
  permissions: nativeAuthority.permissions('async-inbox-cleanup'),
2050
2192
  environment: {
2193
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2194
+ String(observability.routineSuccessSampleRate),
2051
2195
  ${' ...keyValue.environment,\n'}\
2052
2196
  EBK_STAGE: $app.stage,
2053
2197
  EBK_WORKLOAD_IDENTITY: '${name}:async-inbox-cleanup',
@@ -2068,6 +2212,8 @@ ${''}\
2068
2212
  {
2069
2213
  permissions: nativeAuthority.permissions('async-schedule-target'),
2070
2214
  environment: {
2215
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2216
+ String(observability.routineSuccessSampleRate),
2071
2217
  EBK_STAGE: $app.stage,
2072
2218
  EBK_WORKLOAD_IDENTITY: '${name}:async-schedule-target',
2073
2219
  EBK_WORKSPACE: '${workspace}',
@@ -2083,6 +2229,8 @@ ${''}\
2083
2229
  {
2084
2230
  permissions: nativeAuthority.permissions('async-workflow-task'),
2085
2231
  environment: {
2232
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2233
+ String(observability.routineSuccessSampleRate),
2086
2234
  EBK_ASYNC_LEDGER_TABLE: asyncRecoveryLedger.name,
2087
2235
  EBK_STAGE: $app.stage,
2088
2236
  EBK_WORKLOAD_IDENTITY: '${name}:async-workflow-task',
@@ -2404,6 +2552,8 @@ ${''}\
2404
2552
  url: \`http://127.0.0.1:\${devPort}/${name}\`,
2405
2553
  },
2406
2554
  environment: {
2555
+ EBK_OBSERVABILITY_SUCCESS_SAMPLE_RATE:
2556
+ String(observability.routineSuccessSampleRate),
2407
2557
  ${' ...keyValue.environment,\n'}\
2408
2558
  AUTH_CLIENT_ID: authClient.id,
2409
2559
  AUTH_USER_POOL_ID: foundation.userPool.id,
@@ -2671,6 +2821,7 @@ ${''}\
2671
2821
  transform: {
2672
2822
  logGroup: {
2673
2823
  logGroupClass: bootstrapLogContract.logGroupClass,
2824
+ retentionInDays: bootstrapLogContract.retentionInDays,
2674
2825
  tags: bootstrapLogContract.tags,
2675
2826
  },
2676
2827
  taskDefinition: {
@@ -2723,6 +2874,7 @@ ${''}\
2723
2874
  transform: {
2724
2875
  logGroup: {
2725
2876
  logGroupClass: migrationLogContract.logGroupClass,
2877
+ retentionInDays: migrationLogContract.retentionInDays,
2726
2878
  tags: migrationLogContract.tags,
2727
2879
  },
2728
2880
  taskDefinition: {
@@ -2858,6 +3010,7 @@ ${''}\
2858
3010
  transform: {
2859
3011
  logGroup: {
2860
3012
  logGroupClass: retirementLogContract.logGroupClass,
3013
+ retentionInDays: retirementLogContract.retentionInDays,
2861
3014
  tags: retirementLogContract.tags,
2862
3015
  },
2863
3016
  taskDefinition: {
@@ -2949,12 +3102,7 @@ ${''}\
2949
3102
  '${pascalName}OutboxRelayErrors',
2950
3103
  {
2951
3104
  alarmActions: [foundation.discovery.alertTopicArn],
2952
- alarmDescription: operationalAlarmDescription({
2953
- businessSymptom:
2954
- 'The isolated performance-sink outbox relay is failing.',
2955
- id: 'outbox-relay-errors',
2956
- runbook: 'docs/runbooks/async-recovery.md',
2957
- }),
3105
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'outbox-relay-errors', ' ')}),
2958
3106
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
2959
3107
  dimensions: { FunctionName: asyncOutboxRelay.name },
2960
3108
  evaluationPeriods: 1,
@@ -2971,11 +3119,7 @@ ${''}\
2971
3119
  '${pascalName}InboxCleanupErrors',
2972
3120
  {
2973
3121
  alarmActions: [foundation.discovery.alertTopicArn],
2974
- alarmDescription: operationalAlarmDescription({
2975
- businessSymptom: 'The asynchronous inbox cleanup job is failing.',
2976
- id: 'inbox-cleanup-errors',
2977
- runbook: 'docs/runbooks/async-recovery.md',
2978
- }),
3122
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'inbox-cleanup-errors', ' ')}),
2979
3123
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
2980
3124
  dimensions: { FunctionName: asyncInboxCleanup.name },
2981
3125
  evaluationPeriods: 1,
@@ -2992,12 +3136,7 @@ ${''}\
2992
3136
  '${pascalName}DataExportCleanupErrors',
2993
3137
  {
2994
3138
  alarmActions: [foundation.discovery.alertTopicArn],
2995
- alarmDescription: operationalAlarmDescription({
2996
- businessSymptom:
2997
- 'The confidential-export cleanup invocation is failing.',
2998
- id: 'data-export-cleanup-failure',
2999
- runbook: 'docs/runbooks/database-recovery.md',
3000
- }),
3139
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'data-export-cleanup-failure', ' ')}),
3001
3140
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
3002
3141
  dimensions: { FunctionName: dataExportCleanup.name },
3003
3142
  evaluationPeriods: 1,
@@ -3015,12 +3154,7 @@ ${''}\
3015
3154
  '${pascalName}DataExportCleanupInvocationMissing',
3016
3155
  {
3017
3156
  alarmActions: [foundation.discovery.alertTopicArn],
3018
- alarmDescription: operationalAlarmDescription({
3019
- businessSymptom:
3020
- 'The scheduled confidential-export cleanup is not running.',
3021
- id: 'data-export-cleanup-invocation-missing',
3022
- runbook: 'docs/runbooks/database-recovery.md',
3023
- }),
3157
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'data-export-cleanup-invocation-missing', ' ')}),
3024
3158
  comparisonOperator: 'LessThanThreshold',
3025
3159
  dimensions: { FunctionName: dataExportCleanup.name },
3026
3160
  evaluationPeriods: 2,
@@ -3040,11 +3174,7 @@ ${''}\
3040
3174
  '${pascalName}InboxCleanupBacklog',
3041
3175
  {
3042
3176
  alarmActions: [foundation.discovery.alertTopicArn],
3043
- alarmDescription: operationalAlarmDescription({
3044
- businessSymptom: 'Terminal asynchronous inbox records are accumulating.',
3045
- id: 'inbox-cleanup-backlog',
3046
- runbook: 'docs/runbooks/async-recovery.md',
3047
- }),
3177
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'inbox-cleanup-backlog', ' ')}),
3048
3178
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
3049
3179
  dimensions: {
3050
3180
  Component: 'inbox-cleanup',
@@ -3068,12 +3198,7 @@ ${''}\
3068
3198
  '${pascalName}DataExportCleanupDeadline',
3069
3199
  {
3070
3200
  alarmActions: [foundation.discovery.alertTopicArn],
3071
- alarmDescription: operationalAlarmDescription({
3072
- businessSymptom:
3073
- 'Confidential-export residue exceeded its deletion deadline.',
3074
- id: 'data-export-cleanup-deadline',
3075
- runbook: 'docs/runbooks/database-recovery.md',
3076
- }),
3201
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'data-export-cleanup-deadline', ' ')}),
3077
3202
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
3078
3203
  dimensions: {
3079
3204
  Component: 'data-export-cleanup',
@@ -3100,12 +3225,7 @@ ${''}\
3100
3225
  '${pascalName}PerformanceLoadCleanupBacklog',
3101
3226
  {
3102
3227
  alarmActions: [foundation.discovery.alertTopicArn],
3103
- alarmDescription: operationalAlarmDescription({
3104
- businessSymptom:
3105
- 'Expired protected performance-load evidence is accumulating.',
3106
- id: 'performance-load-cleanup-backlog',
3107
- runbook: 'docs/runbooks/performance-capacity-cost.md',
3108
- }),
3228
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'performance-load-cleanup-backlog', ' ')}),
3109
3229
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
3110
3230
  dimensions: {
3111
3231
  Component: 'performance-load-cleanup',
@@ -3132,12 +3252,7 @@ ${''}\
3132
3252
  '${pascalName}PerformanceLoadCleanupDeadline',
3133
3253
  {
3134
3254
  alarmActions: [foundation.discovery.alertTopicArn],
3135
- alarmDescription: operationalAlarmDescription({
3136
- businessSymptom:
3137
- 'Protected performance-load residue exceeded its cleanup deadline.',
3138
- id: 'performance-load-cleanup-deadline',
3139
- runbook: 'docs/runbooks/performance-capacity-cost.md',
3140
- }),
3255
+ alarmDescription: operationalAlarmDescription(${(0, built_in_plan_alarm_identity_js_1.renderSliceAlarmDescriptionInput)(name, 'performance-load-cleanup-deadline', ' ')}),
3141
3256
  comparisonOperator: 'GreaterThanOrEqualToThreshold',
3142
3257
  dimensions: {
3143
3258
  Component: 'performance-load-cleanup',
@@ -3159,15 +3274,60 @@ ${''}\
3159
3274
  treatMissingData: 'notBreaching',
3160
3275
  },
3161
3276
  );
3162
- const dashboard = new aws.cloudwatch.Dashboard('${pascalName}Dashboard', {
3277
+ const dashboard = observability.dashboards === 'slice'
3278
+ ? new aws.cloudwatch.Dashboard('${pascalName}Dashboard', {
3163
3279
  dashboardName: '${name}-' + $app.stage,
3164
- dashboardBody: JSON.stringify({
3165
- widgets: [{
3166
- type: 'text', x: 0, y: 0, width: 24, height: 3,
3167
- properties: { markdown: '# ${pascalName}\\nOwner: product' },
3168
- }],
3169
- }),
3170
- });
3280
+ dashboardBody: $resolve({
3281
+ table: keyValue.table.name,
3282
+ dispatcher: keyValue.dispatcher.name,
3283
+ reconciler: keyValue.reconciler.name,
3284
+ providerQueue: physicalQueueName(providerQueue),
3285
+ providerDlq: physicalQueueName(providerDlq),
3286
+ }).apply(({ table, dispatcher, reconciler, providerQueue, providerDlq }) => JSON.stringify({
3287
+ widgets: [
3288
+ {
3289
+ type: 'metric', x: 0, y: 0, width: 12, height: 6,
3290
+ properties: {
3291
+ title: '${pascalName} delivery errors and throttles',
3292
+ region: process.env.AWS_REGION ?? 'us-east-1',
3293
+ period: 300, stat: 'Sum',
3294
+ metrics: [
3295
+ ['AWS/Lambda', 'Errors', 'FunctionName', dispatcher],
3296
+ ['.', 'Throttles', '.', '.'],
3297
+ ['.', 'Errors', '.', reconciler],
3298
+ ['.', 'Throttles', '.', '.'],
3299
+ ],
3300
+ },
3301
+ },
3302
+ {
3303
+ type: 'metric', x: 12, y: 0, width: 12, height: 6,
3304
+ properties: {
3305
+ title: '${pascalName} durable storage capacity',
3306
+ region: process.env.AWS_REGION ?? 'us-east-1',
3307
+ period: 300, stat: 'Sum',
3308
+ metrics: [
3309
+ ['AWS/DynamoDB', 'ConsumedReadCapacityUnits', 'TableName', table],
3310
+ ['.', 'ConsumedWriteCapacityUnits', '.', '.'],
3311
+ ['.', 'ThrottledRequests', '.', '.', 'Operation', 'PutItem'],
3312
+ ],
3313
+ },
3314
+ },
3315
+ {
3316
+ type: 'metric', x: 0, y: 6, width: 24, height: 6,
3317
+ properties: {
3318
+ title: '${pascalName} provider backlog and dead letters',
3319
+ region: process.env.AWS_REGION ?? 'us-east-1',
3320
+ period: 300, stat: 'Maximum',
3321
+ metrics: [
3322
+ ['AWS/SQS', 'ApproximateNumberOfMessagesVisible', 'QueueName', providerQueue],
3323
+ ['.', '.', '.', providerDlq],
3324
+ ['.', 'ApproximateAgeOfOldestMessage', '.', providerQueue],
3325
+ ],
3326
+ },
3327
+ },
3328
+ ],
3329
+ })),
3330
+ }) : undefined;
3171
3331
  return {
3172
3332
  api,
3173
3333
  asyncInboxCleanup,