From 130ea7de5d8f18fdd6062ce08f679a14e10a9773 Mon Sep 17 00:00:00 2001 From: Laxenade Date: Sun, 4 May 2025 20:16:20 -0700 Subject: [PATCH] feat(alarm): add support for nested composite alarms --- API.md | 162 ++- lib/common/alarm/AlarmFactory.ts | 50 +- lib/facade/MonitoringFacade.ts | 110 +- test/common/alarm/AlarmFactory.test.ts | 76 ++ .../__snapshots__/AlarmFactory.test.ts.snap | 128 ++ test/facade/MonitoringFacade.test.ts | 218 ++++ .../MonitoringFacade.test.ts.snap | 1044 +++++++++++++++++ 7 files changed, 1761 insertions(+), 27 deletions(-) diff --git a/API.md b/API.md index fabb8522..70d2f9f6 100644 --- a/API.md +++ b/API.md @@ -1062,6 +1062,8 @@ new MonitoringFacade(scope: Construct, id: string, props?: MonitoringFacadeProps | createdAlarmsWithDisambiguator | Returns a subset of created alarms that are marked by a specific disambiguator. | | createdAlarmsWithTag | Returns a subset of created alarms that are marked by a specific custom tag. | | createdCompositeAlarms | Returns the added composite alarms. | +| createdCompositeAlarmsWithDisambiguator | Returns a subset of created composite alarms that are marked by a specific disambiguator. | +| createdCompositeAlarmsWithTag | Returns a subset of created composite alarms that are marked by a specific custom tag. | | createdDashboard | *No description.* | | createdDashboardSegments | Returns all the added segments. | | createdMonitorings | Returns the added segments that subclass {@link Monitoring}. | @@ -1331,13 +1333,12 @@ A function that will accept a source alarm and determine whether and how a new a ##### `createCompositeAlarmUsingDisambiguator` ```typescript -public createCompositeAlarmUsingDisambiguator(alarmDisambiguator: string, props?: AddCompositeAlarmProps): CompositeAlarm +public createCompositeAlarmUsingDisambiguator(alarmDisambiguator: string, props?: AddCompositeAlarmProps, allowNestedCompositeAlarms?: boolean): CompositeAlarm ``` Finds a subset of created alarms that are marked by a specific disambiguator and creates a composite alarm. -This composite alarm is created with an 'OR' condition, so it triggers with any child alarm. -NOTE: This composite alarm is not added among other alarms, so it is not returned by createdAlarms() calls. +NOTE: This composite alarm is not returned in createdAlarms() calls, use createdCompositeAlarms() instead. ###### `alarmDisambiguator`Required @@ -1355,16 +1356,26 @@ customization options. --- +###### `allowNestedCompositeAlarms`Optional + +- *Type:* boolean + +whether to allow nested composite alarms. + +If set to true, previously created composite alarms with the matching disambiguator will be included in the composite alarm. + +--- + ##### `createCompositeAlarmUsingTag` ```typescript -public createCompositeAlarmUsingTag(customTag: string, props?: AddCompositeAlarmProps): CompositeAlarm +public createCompositeAlarmUsingTag(customTag: string, props?: AddCompositeAlarmProps, allowNestedCompositeAlarms?: boolean): CompositeAlarm ``` Finds a subset of created alarms that are marked by a specific custom tag and creates a composite alarm. -This composite alarm is created with an 'OR' condition, so it triggers with any child alarm. -NOTE: This composite alarm is not added among other alarms, so it is not returned by createdAlarms() calls. +This composite alarm is created with an 'OR' condition so it triggers with any child alarm. +NOTE: This composite alarm is not returned in createdAlarms() calls, use createdCompositeAlarms() instead. ###### `customTag`Required @@ -1382,6 +1393,16 @@ customization options. --- +###### `allowNestedCompositeAlarms`Optional + +- *Type:* boolean + +whether to allow nested composite alarms. + +If set to true, previously created composite alarms with the matching tag will be included in the composite alarm. + +--- + ##### ~~`createdAlarmDashboard`~~ ```typescript @@ -1436,6 +1457,38 @@ public createdCompositeAlarms(): CompositeAlarm[] Returns the added composite alarms. +##### `createdCompositeAlarmsWithDisambiguator` + +```typescript +public createdCompositeAlarmsWithDisambiguator(disambiguator: string): CompositeAlarm[] +``` + +Returns a subset of created composite alarms that are marked by a specific disambiguator. + +###### `disambiguator`Required + +- *Type:* string + +disambiguator to filter alarms by. + +--- + +##### `createdCompositeAlarmsWithTag` + +```typescript +public createdCompositeAlarmsWithTag(customTag: string): CompositeAlarm[] +``` + +Returns a subset of created composite alarms that are marked by a specific custom tag. + +###### `customTag`Required + +- *Type:* string + +tag to filter alarms by. + +--- + ##### ~~`createdDashboard`~~ ```typescript @@ -12239,6 +12292,58 @@ public readonly addFailedBuildRateAlarm: {[ key: string ]: ErrorRateThreshold}; --- +### CompositeAlarmWithMetadata + +Composite alarm with metadata. + +#### Initializer + +```typescript +import { CompositeAlarmWithMetadata } from 'cdk-monitoring-constructs' + +const compositeAlarmWithMetadata: CompositeAlarmWithMetadata = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| alarm | aws-cdk-lib.aws_cloudwatch.CompositeAlarm | *No description.* | +| customTags | string[] | *No description.* | +| disambiguator | string | *No description.* | + +--- + +##### `alarm`Required + +```typescript +public readonly alarm: CompositeAlarm; +``` + +- *Type:* aws-cdk-lib.aws_cloudwatch.CompositeAlarm + +--- + +##### `customTags`Optional + +```typescript +public readonly customTags: string[]; +``` + +- *Type:* string[] + +--- + +##### `disambiguator`Optional + +```typescript +public readonly disambiguator: string; +``` + +- *Type:* string + +--- + ### ConsumedCapacityThreshold #### Initializer @@ -54040,7 +54145,8 @@ new AlarmFactory(alarmScope: Construct, props: AlarmFactoryProps) | **Name** | **Description** | | --- | --- | | addAlarm | *No description.* | -| addCompositeAlarm | *No description.* | +| addCompositeAlarm | Creates a composite alarm from a collection of alarms and composite alarms. | +| addCompositeAlarmFromAlarmBases | Creates a composite alarm from a collection of alarm base objects. | --- @@ -54065,19 +54171,59 @@ public addAlarm(metric: Metric | MathExpression, props: AddAlarmProps): AlarmWit ##### `addCompositeAlarm` ```typescript -public addCompositeAlarm(alarms: AlarmWithAnnotation[], props: AddCompositeAlarmProps): CompositeAlarm +public addCompositeAlarm(alarms: AlarmWithAnnotation[], props: AddCompositeAlarmProps, compositeAlarms?: CompositeAlarm[]): CompositeAlarm ``` +Creates a composite alarm from a collection of alarms and composite alarms. + ###### `alarms`Required - *Type:* AlarmWithAnnotation[] +Array of individual alarms to be composed. + --- ###### `props`Required - *Type:* AddCompositeAlarmProps +Customization options for the composite alarm. + +--- + +###### `compositeAlarms`Optional + +- *Type:* aws-cdk-lib.aws_cloudwatch.CompositeAlarm[] + +Optional array of composite alarms to be composed together with regular alarms. + +--- + +##### `addCompositeAlarmFromAlarmBases` + +```typescript +public addCompositeAlarmFromAlarmBases(alarms: AlarmBase[], props: AddCompositeAlarmProps): CompositeAlarm +``` + +Creates a composite alarm from a collection of alarm base objects. + +This allows creation of composite alarms that include both metric alarms and other composite alarms. + +###### `alarms`Required + +- *Type:* aws-cdk-lib.aws_cloudwatch.AlarmBase[] + +Array of AlarmBase objects (can include both Alarm and CompositeAlarm). + +--- + +###### `props`Required + +- *Type:* AddCompositeAlarmProps + +Customization options for the composite alarm. + --- diff --git a/lib/common/alarm/AlarmFactory.ts b/lib/common/alarm/AlarmFactory.ts index 990f8d5e..b5f6489c 100644 --- a/lib/common/alarm/AlarmFactory.ts +++ b/lib/common/alarm/AlarmFactory.ts @@ -821,9 +821,37 @@ export class AlarmFactory { }; } + /** + * Creates a composite alarm from a collection of alarms and composite alarms. + * + * @param alarms Array of individual alarms to be composed + * @param props Customization options for the composite alarm + * @param compositeAlarms Optional array of composite alarms to be composed together with regular alarms + * @returns Newly created composite alarm + */ addCompositeAlarm( alarms: AlarmWithAnnotation[], props: AddCompositeAlarmProps, + compositeAlarms: CompositeAlarm[] = [], + ): CompositeAlarm { + const alarmBases = [ + ...alarms.map((alarm) => alarm.alarm), + ...compositeAlarms, + ] as AlarmBase[]; + return this.addCompositeAlarmFromAlarmBases(alarmBases, props); + } + + /** + * Creates a composite alarm from a collection of alarm base objects. + * This allows creation of composite alarms that include both metric alarms and other composite alarms. + * + * @param alarms Array of AlarmBase objects (can include both Alarm and CompositeAlarm) + * @param props Customization options for the composite alarm + * @returns Newly created composite alarm + */ + addCompositeAlarmFromAlarmBases( + alarms: AlarmBase[], + props: AddCompositeAlarmProps, ): CompositeAlarm { const actionsEnabled = this.determineActionsEnabled( props?.actionsEnabled, @@ -840,7 +868,10 @@ export class AlarmFactory { props?.documentationLink, ); const dedupeString = this.alarmNamingStrategy.getDedupeString(namingInput); - const alarmRule = this.determineCompositeAlarmRule(alarms, props); + const alarmRule = this.determineCompositeAlarmRuleFromAlarmBases( + alarms, + props, + ); const alarm = new CompositeAlarm(this.alarmScope, alarmName, { compositeAlarmName: alarmName, @@ -867,8 +898,23 @@ export class AlarmFactory { protected determineCompositeAlarmRule( alarms: AlarmWithAnnotation[], props: AddCompositeAlarmProps, + compositeAlarms: CompositeAlarm[] = [], ): IAlarmRule { - const alarmRules = alarms.map((alarm) => alarm.alarmRuleWhenAlarming); + const alarmBases = [ + ...alarms.map((alarm) => alarm.alarm), + ...compositeAlarms, + ] as AlarmBase[]; + return this.determineCompositeAlarmRuleFromAlarmBases(alarmBases, props); + } + + protected determineCompositeAlarmRuleFromAlarmBases( + alarms: AlarmBase[], + props: AddCompositeAlarmProps, + ): IAlarmRule { + const alarmRules = alarms.map((alarm) => + AlarmRule.fromAlarm(alarm, AlarmState.ALARM), + ); + const operator = props.compositeOperator ?? CompositeAlarmOperator.OR; switch (operator) { case CompositeAlarmOperator.AND: diff --git a/lib/facade/MonitoringFacade.ts b/lib/facade/MonitoringFacade.ts index 284456fb..bfa6cfff 100644 --- a/lib/facade/MonitoringFacade.ts +++ b/lib/facade/MonitoringFacade.ts @@ -1,5 +1,10 @@ import { Aspects, Stack } from "aws-cdk-lib"; -import { CompositeAlarm, Dashboard, IWidget } from "aws-cdk-lib/aws-cloudwatch"; +import { + AlarmBase, + CompositeAlarm, + Dashboard, + IWidget, +} from "aws-cdk-lib/aws-cloudwatch"; import { Construct } from "constructs"; import { MonitoringAspectProps } from "./IMonitoringAspect"; @@ -162,6 +167,15 @@ export interface MonitoringFacadeProps { readonly dashboardFactory?: IDynamicDashboardFactory; } +/** + * Composite alarm with metadata. + */ +export interface CompositeAlarmWithMetadata { + readonly alarm: CompositeAlarm; + readonly customTags?: string[]; + readonly disambiguator?: string; +} + /** * An implementation of a {@link MonitoringScope}. * @@ -178,7 +192,7 @@ export class MonitoringFacade extends MonitoringScope { | IDashboardSegment | IDynamicDashboardSegment )[]; - protected readonly createdComposites: CompositeAlarm[]; + protected readonly createdComposites: CompositeAlarmWithMetadata[]; protected readonly createdClones: AlarmWithAnnotation[]; constructor(scope: Construct, id: string, props?: MonitoringFacadeProps) { @@ -325,7 +339,31 @@ export class MonitoringFacade extends MonitoringScope { * Returns the added composite alarms. */ createdCompositeAlarms(): CompositeAlarm[] { - return this.createdComposites; + return this.createdComposites.map(({ alarm }) => alarm); + } + + /** + * Returns a subset of created composite alarms that are marked by a specific custom tag. + * + * @param customTag tag to filter alarms by + */ + createdCompositeAlarmsWithTag(customTag: string): CompositeAlarm[] { + return this.createdComposites + .filter(({ customTags }) => customTags?.includes(customTag)) + .map(({ alarm }) => alarm); + } + + /** + * Returns a subset of created composite alarms that are marked by a specific disambiguator. + * + * @param disambiguator disambiguator to filter alarms by + */ + createdCompositeAlarmsWithDisambiguator( + disambiguator: string, + ): CompositeAlarm[] { + return this.createdComposites + .filter(({ disambiguator: d }) => d === disambiguator) + .map(({ alarm }) => alarm); } /** @@ -349,53 +387,91 @@ export class MonitoringFacade extends MonitoringScope { /** * Finds a subset of created alarms that are marked by a specific custom tag and creates a composite alarm. - * This composite alarm is created with an 'OR' condition, so it triggers with any child alarm. - * NOTE: This composite alarm is not added among other alarms, so it is not returned by createdAlarms() calls. + * This composite alarm is created with an 'OR' condition so it triggers with any child alarm. + * NOTE: This composite alarm is not returned in createdAlarms() calls, use createdCompositeAlarms() instead. * * @param customTag tag to filter alarms by * @param props customization options + * @param allowNestedCompositeAlarms whether to allow nested composite alarms. If set to true, previously created composite alarms with the matching tag will be included in the composite alarm. */ createCompositeAlarmUsingTag( customTag: string, props?: AddCompositeAlarmProps, + allowNestedCompositeAlarms: boolean = false, ): CompositeAlarm | undefined { - const alarms = this.createdAlarmsWithTag(customTag); - if (alarms.length > 0) { + const metricAlarms = this.createdAlarmsWithTag(customTag).map( + (a) => a.alarm, + ); + const compositeAlarms = allowNestedCompositeAlarms + ? this.createdCompositeAlarmsWithTag(customTag) + : []; + const allAlarms = [...metricAlarms, ...compositeAlarms] as AlarmBase[]; + + if (allAlarms.length > 0) { const disambiguator = props?.disambiguator ?? customTag; const alarmFactory = this.createAlarmFactory("Composite"); - const composite = alarmFactory.addCompositeAlarm(alarms, { - ...(props ?? {}), + const composite = alarmFactory.addCompositeAlarmFromAlarmBases( + allAlarms, + { + ...(props ?? {}), + disambiguator, + }, + ); + + this.createdComposites.push({ + alarm: composite, + customTags: props?.customTags, disambiguator, }); - this.createdComposites.push(composite); + return composite; } + return undefined; } /** * Finds a subset of created alarms that are marked by a specific disambiguator and creates a composite alarm. - * This composite alarm is created with an 'OR' condition, so it triggers with any child alarm. - * NOTE: This composite alarm is not added among other alarms, so it is not returned by createdAlarms() calls. + * + * NOTE: This composite alarm is not returned in createdAlarms() calls, use createdCompositeAlarms() instead. * * @param alarmDisambiguator disambiguator to filter alarms by * @param props customization options + * @param allowNestedCompositeAlarms whether to allow nested composite alarms. If set to true, previously created composite alarms with the matching disambiguator will be included in the composite alarm. */ createCompositeAlarmUsingDisambiguator( alarmDisambiguator: string, props?: AddCompositeAlarmProps, + allowNestedCompositeAlarms: boolean = false, ): CompositeAlarm | undefined { - const alarms = this.createdAlarmsWithDisambiguator(alarmDisambiguator); - if (alarms.length > 0) { + const metricAlarms = this.createdAlarmsWithDisambiguator( + alarmDisambiguator, + ).map((a) => a.alarm); + const compositeAlarms = allowNestedCompositeAlarms + ? this.createdCompositeAlarmsWithDisambiguator(alarmDisambiguator) + : []; + const allAlarms = [...metricAlarms, ...compositeAlarms] as AlarmBase[]; + + if (allAlarms.length > 0) { const disambiguator = props?.disambiguator ?? alarmDisambiguator; const alarmFactory = this.createAlarmFactory("Composite"); - const composite = alarmFactory.addCompositeAlarm(alarms, { - ...(props ?? {}), + const composite = alarmFactory.addCompositeAlarmFromAlarmBases( + allAlarms, + { + ...(props ?? {}), + disambiguator, + }, + ); + + this.createdComposites.push({ + alarm: composite, + customTags: props?.customTags, disambiguator, }); - this.createdComposites.push(composite); + return composite; } + return undefined; } diff --git a/test/common/alarm/AlarmFactory.test.ts b/test/common/alarm/AlarmFactory.test.ts index 524cca09..c6ed6f7b 100644 --- a/test/common/alarm/AlarmFactory.test.ts +++ b/test/common/alarm/AlarmFactory.test.ts @@ -8,6 +8,7 @@ import { Metric, Shading, TreatMissingData, + AlarmBase, } from "aws-cdk-lib/aws-cloudwatch"; import { Topic } from "aws-cdk-lib/aws-sns"; import { Construct } from "constructs"; @@ -793,3 +794,78 @@ test("addAlarm: custom metric adjuster, applies it and DefaultMetricAdjuster aft )?.period, ).toEqual(Duration.minutes(10).toSeconds()); }); + +test("addCompositeAlarmFromAlarmBases: supports combining metric alarms and composite alarms", () => { + const stack = new Stack(); + const factory = new AlarmFactory(stack, { + globalMetricDefaults, + globalAlarmDefaults: globalAlarmDefaultsWithDisambiguator, + localAlarmNamePrefix: "prefix", + }); + const metric = new Metric({ + namespace: "DummyNamespace", + metricName: "DummyMetric", + }); + const alarm1 = factory.addAlarm(metric, { + alarmNameSuffix: "Alarm1", + alarmDescription: "Testing alarm 1", + threshold: 1, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: TreatMissingData.MISSING, + }); + const alarm2 = factory.addAlarm(metric, { + alarmNameSuffix: "Alarm2", + alarmDescription: "Testing alarm 2", + threshold: 2, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: TreatMissingData.MISSING, + }); + + // Extract the underlying AlarmBase objects + const alarm1Base: AlarmBase = alarm1.alarm; + + // Create first level composite alarm using the base method + const firstLevelComposite = factory.addCompositeAlarmFromAlarmBases( + [alarm1Base], + { + disambiguator: "FirstLevelBase", + alarmNameSuffix: "FirstLevelBase", + }, + ); + + // Extract the underlying AlarmBase for alarm2 + const alarm2Base: AlarmBase = alarm2.alarm; + + // Create second level composite alarm that includes a metric alarm and the first level composite + factory.addCompositeAlarmFromAlarmBases([alarm2Base, firstLevelComposite], { + disambiguator: "SecondLevelBase", + alarmNameSuffix: "SecondLevelBase", + compositeOperator: CompositeAlarmOperator.OR, + }); + + // Verify the template + const template = Template.fromStack(stack); + + // We should have two metric alarms and two composite alarms + template.resourceCountIs("AWS::CloudWatch::Alarm", 2); + template.resourceCountIs("AWS::CloudWatch::CompositeAlarm", 2); + + // The first level composite should reference only alarm1 + template.hasResourceProperties("AWS::CloudWatch::CompositeAlarm", { + AlarmName: Match.stringLikeRegexp("-FirstLevelBase$"), + AlarmRule: Match.objectLike({ + "Fn::Join": Match.anyValue(), + }), + }); + + // The second level composite should reference both alarm2 and the first level composite + template.hasResourceProperties("AWS::CloudWatch::CompositeAlarm", { + AlarmName: Match.stringLikeRegexp("-SecondLevelBase$"), + AlarmRule: Match.objectLike({ + "Fn::Join": Match.anyValue(), + }), + }); + + // Snapshot verification + expect(template).toMatchSnapshot(); +}); diff --git a/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap b/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap index 7dbd76be..f390e039 100644 --- a/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap +++ b/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap @@ -514,3 +514,131 @@ Object { }, } `; + +exports[`addCompositeAlarmFromAlarmBases: supports combining metric alarms and composite alarms 1`] = ` +Object { + "Parameters": Object { + "BootstrapVersion": Object { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": Object { + "DummyServiceAlarmsprefixAlarm1F4FCF957": Object { + "Properties": Object { + "ActionsEnabled": false, + "AlarmDescription": "Testing alarm 1", + "AlarmName": "DummyServiceAlarms-prefix-Alarm1", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 6, + "EvaluationPeriods": 6, + "MetricName": "DummyMetric", + "Namespace": "DummyNamespace", + "Period": 300, + "Statistic": "Average", + "Threshold": 1, + "TreatMissingData": "missing", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "DummyServiceAlarmsprefixAlarm25ADF8B37": Object { + "Properties": Object { + "ActionsEnabled": false, + "AlarmDescription": "Testing alarm 2", + "AlarmName": "DummyServiceAlarms-prefix-Alarm2", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 6, + "EvaluationPeriods": 6, + "MetricName": "DummyMetric", + "Namespace": "DummyNamespace", + "Period": 300, + "Statistic": "Average", + "Threshold": 2, + "TreatMissingData": "missing", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "DummyServiceAlarmsprefixFirstLevelBase6385B032": Object { + "Properties": Object { + "ActionsEnabled": false, + "AlarmDescription": "Composite alarm", + "AlarmName": "DummyServiceAlarms-prefix-FirstLevelBase", + "AlarmRule": Object { + "Fn::Join": Array [ + "", + Array [ + "(ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "DummyServiceAlarmsprefixAlarm1F4FCF957", + "Arn", + ], + }, + "\\"))", + ], + ], + }, + }, + "Type": "AWS::CloudWatch::CompositeAlarm", + }, + "DummyServiceAlarmsprefixSecondLevelBaseE5D5046F": Object { + "Properties": Object { + "ActionsEnabled": false, + "AlarmDescription": "Composite alarm", + "AlarmName": "DummyServiceAlarms-prefix-SecondLevelBase", + "AlarmRule": Object { + "Fn::Join": Array [ + "", + Array [ + "(ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "DummyServiceAlarmsprefixAlarm25ADF8B37", + "Arn", + ], + }, + "\\") OR ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "DummyServiceAlarmsprefixFirstLevelBase6385B032", + "Arn", + ], + }, + "\\"))", + ], + ], + }, + }, + "Type": "AWS::CloudWatch::CompositeAlarm", + }, + }, + "Rules": Object { + "CheckBootstrapVersion": Object { + "Assertions": Array [ + Object { + "Assert": Object { + "Fn::Not": Array [ + Object { + "Fn::Contains": Array [ + Array [ + "1", + "2", + "3", + "4", + "5", + ], + Object { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; diff --git a/test/facade/MonitoringFacade.test.ts b/test/facade/MonitoringFacade.test.ts index 8af20a8d..0c48e6ad 100644 --- a/test/facade/MonitoringFacade.test.ts +++ b/test/facade/MonitoringFacade.test.ts @@ -393,4 +393,222 @@ describe("test of defaults", () => { // Snapshot verification expect(template).toMatchSnapshot(); }); + + test("createCompositeAlarmUsingTag creates a composite alarm from matching alarms", () => { + const stack = new Stack(); + const onAlarmTopic = new Topic(stack, "OnAlarmTopicForComposites", { + topicName: "CompositeAlarmTopic", + }); + const facade = new MonitoringFacade(stack, "CompositeAlarmTestFacade", { + metricFactoryDefaults: { + namespace: "CompositeTest", + }, + alarmFactoryDefaults: { + alarmNamePrefix: "CompTest", + actionsEnabled: true, + datapointsToAlarm: 3, + action: notifySns(onAlarmTopic), + }, + }); + + // Create a set of metric alarms with the same tag + facade.monitorDynamoTable({ + table: Table.fromTableName(stack, "CompositeTestTable", "TestTable"), + addAverageSuccessfulGetItemLatencyAlarm: { + Critical: { + maxLatency: Duration.seconds(10), + customTags: ["comp-test-tag"], + }, + }, + addReadThrottledEventsCountAlarm: { + Critical: { + maxThrottledEventsThreshold: 100, + customTags: ["comp-test-tag"], + }, + }, + }); + + // Create first level composite alarm from metric alarms + const firstLevelComposite = facade.createCompositeAlarmUsingTag( + "comp-test-tag", + { + disambiguator: "FirstLevel", + alarmNameSuffix: "FirstLevel", + customTags: ["comp-test-nested"], + }, + true, // allow nested composite alarms + ); + + expect(firstLevelComposite).toBeDefined(); + + // Create more metric alarms with a different tag + facade.monitorLambdaFunction({ + lambdaFunction: Function.fromFunctionAttributes( + stack, + "CompositeTestFunction", + { + functionArn: `arn:aws:lambda:us-west-2:01234567890:function:TestFunction`, + sameEnvironment: true, + }, + ), + addFaultRateAlarm: { + Critical: { + maxErrorRate: 0.5, + customTags: ["comp-test-nested"], + }, + }, + }); + + // Create second level composite alarm that includes metric alarms and the first level composite + const secondLevelComposite = facade.createCompositeAlarmUsingTag( + "comp-test-nested", + { + disambiguator: "SecondLevel", + alarmNameSuffix: "SecondLevel", + }, + true, // allow nested composite alarms + ); + + expect(secondLevelComposite).toBeDefined(); + + // Verify the generated resources + const template = Template.fromStack(stack); + + // Find first level composite alarm + const firstLevelCompositeAlarm = template.findResources( + "AWS::CloudWatch::CompositeAlarm", + Match.objectLike({ + Properties: { + AlarmName: Match.stringLikeRegexp("-FirstLevel$"), + AlarmRule: Match.anyValue(), + }, + }), + ); + expect(Object.keys(firstLevelCompositeAlarm).length).toBe(1); + + // Find second level composite alarm + const secondLevelCompositeAlarm = template.findResources( + "AWS::CloudWatch::CompositeAlarm", + Match.objectLike({ + Properties: { + AlarmName: Match.stringLikeRegexp("-SecondLevel$"), + AlarmRule: Match.anyValue(), + }, + }), + ); + expect(Object.keys(secondLevelCompositeAlarm).length).toBe(1); + + // Verify total number of alarms (3 metric alarms + 2 composite alarms) + const allAlarms = [ + ...Object.keys(template.findResources("AWS::CloudWatch::Alarm")), + ...Object.keys(template.findResources("AWS::CloudWatch::CompositeAlarm")), + ]; + expect(allAlarms.length).toBe(5); + + // Snapshot verification + expect(template).toMatchSnapshot(); + }); + + test("composite alarms are not nested when allowNestedCompositeAlarms is false", () => { + const stack = new Stack(); + const onAlarmTopic = new Topic(stack, "AlarmTopic"); + const facade = new MonitoringFacade(stack, "NestingTestFacade", { + alarmFactoryDefaults: { + alarmNamePrefix: "NestTest", + actionsEnabled: true, + action: notifySns(onAlarmTopic), + }, + }); + + // Create first set of metric alarms with the "FirstGroup" tag + facade.monitorDynamoTable({ + table: Table.fromTableName(stack, "FirstGroupTable", "FirstGroupTable"), + addAverageSuccessfulGetItemLatencyAlarm: { + Critical: { + maxLatency: Duration.seconds(10), + customTags: ["FirstGroup"], + }, + }, + }); + + // Create first level composite alarm from the "FirstGroup" tag + const firstLevelComposite = facade.createCompositeAlarmUsingTag( + "FirstGroup", + { + disambiguator: "FirstLevel", + alarmNameSuffix: "FirstLevel", + customTags: ["FirstLevelTag"], + }, + ); + expect(firstLevelComposite).toBeDefined(); + + // Create second set of metric alarms + facade.monitorLambdaFunction({ + lambdaFunction: Function.fromFunctionName( + stack, + "SecondGroupFunction", + "SecondGroupFunction", + ), + addFaultRateAlarm: { + Critical: { + maxErrorRate: 0.1, + customTags: ["FirstLevelTag"], + }, + }, + }); + + // Create second level composite alarm with allowNestedCompositeAlarms = false + const secondLevelComposite = facade.createCompositeAlarmUsingTag( + "FirstLevelTag", + { + disambiguator: "SecondLevel", + alarmNameSuffix: "SecondLevel", + }, + false, // Explicitly set allowNestedCompositeAlarms to false + ); + expect(secondLevelComposite).toBeDefined(); + + // Verify the resources in the resulting template + const template = Template.fromStack(stack); + + // Check that both composite alarms exist + const compositeAlarms = template.findResources( + "AWS::CloudWatch::CompositeAlarm", + ); + expect(Object.keys(compositeAlarms).length).toBe(2); + + // Find all the alarm ARNs that are referenced in the second level composite's AlarmRule + let secondLevelCompositeLogicalId: string | undefined; + + // Find the logical ID of the second level composite alarm + for (const [logicalId, resource] of Object.entries(compositeAlarms)) { + if (resource.Properties?.AlarmName?.toString().includes("-SecondLevel")) { + secondLevelCompositeLogicalId = logicalId; + break; + } + } + + expect(secondLevelCompositeLogicalId).toBeDefined(); + + // Get the AlarmRule from the second level composite alarm + const secondLevelRule = + compositeAlarms[secondLevelCompositeLogicalId!].Properties.AlarmRule; + + // Convert the rule to string for inspection + const ruleString = JSON.stringify(secondLevelRule); + + // Verify the rule doesn't include a reference to the first level composite alarm + // by checking it doesn't contain FirstLevel in the alarm name + expect(ruleString).not.toContain("FirstLevel"); + + // Total resources should be 2 metric alarms + 2 composite alarms = 4 + const allAlarms = [ + ...Object.keys(template.findResources("AWS::CloudWatch::Alarm")), + ...Object.keys(template.findResources("AWS::CloudWatch::CompositeAlarm")), + ]; + expect(allAlarms.length).toBe(4); + + // Snapshot verification + expect(template).toMatchSnapshot(); + }); }); diff --git a/test/facade/__snapshots__/MonitoringFacade.test.ts.snap b/test/facade/__snapshots__/MonitoringFacade.test.ts.snap index 98987d7f..0ba4ee11 100644 --- a/test/facade/__snapshots__/MonitoringFacade.test.ts.snap +++ b/test/facade/__snapshots__/MonitoringFacade.test.ts.snap @@ -517,6 +517,1050 @@ Object { } `; +exports[`test of defaults composite alarms are not nested when allowNestedCompositeAlarms is false 1`] = ` +Object { + "Parameters": Object { + "BootstrapVersion": Object { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": Object { + "AlarmTopicD01E77F9": Object { + "Type": "AWS::SNS::Topic", + }, + "NestingTestFacadeNestTestCompositeFirstLevel5ED59FF5": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "AlarmTopicD01E77F9", + }, + ], + "AlarmDescription": "Composite alarm", + "AlarmName": "NestTest-Composite-FirstLevel", + "AlarmRule": Object { + "Fn::Join": Array [ + "", + Array [ + "(ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "NestingTestFacadeNestTestFirstGroupTableLatencyAverageGetItemCriticalF377C877", + "Arn", + ], + }, + "\\"))", + ], + ], + }, + }, + "Type": "AWS::CloudWatch::CompositeAlarm", + }, + "NestingTestFacadeNestTestCompositeSecondLevelEC1274EC": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "AlarmTopicD01E77F9", + }, + ], + "AlarmDescription": "Composite alarm", + "AlarmName": "NestTest-Composite-SecondLevel", + "AlarmRule": Object { + "Fn::Join": Array [ + "", + Array [ + "(ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "NestingTestFacadeNestTestSecondGroupFunctionFaultRateCritical0226378C", + "Arn", + ], + }, + "\\"))", + ], + ], + }, + }, + "Type": "AWS::CloudWatch::CompositeAlarm", + }, + "NestingTestFacadeNestTestFirstGroupTableLatencyAverageGetItemCriticalF377C877": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "AlarmTopicD01E77F9", + }, + ], + "AlarmDescription": "Average latency is too high.", + "AlarmName": "NestTest-FirstGroupTable-Latency-Average-GetItem-Critical", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "GetItem", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "Operation", + "Value": "GetItem", + }, + Object { + "Name": "TableName", + "Value": "FirstGroupTable", + }, + ], + "MetricName": "SuccessfulRequestLatency", + "Namespace": "AWS/DynamoDB", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 10000, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "NestingTestFacadeNestTestSecondGroupFunctionFaultRateCritical0226378C": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "AlarmTopicD01E77F9", + }, + ], + "AlarmDescription": "Fault rate is too high.", + "AlarmName": "NestTest-SecondGroupFunction-Fault-Rate-Critical", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "Faults (avg)", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + }, + ], + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 0.1, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "NestingTestFacadeNestingTestFacadeDashboardsDashboardA2475CC6": Object { + "Properties": Object { + "DashboardBody": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"start\\":\\"-PT8H\\",\\"periodOverride\\":\\"inherit\\",\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### Dynamo Table **[FirstGroupTable](https://", + Object { + "Ref": "AWS::Region", + }, + ".console.aws.amazon.com/dynamodb/home?region=", + Object { + "Ref": "AWS::Region", + }, + "#tables:selected=FirstGroupTable)**\\"}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":3,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Read Usage\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Consumed\\",\\"expression\\":\\"consumed_rcu_sum/PERIOD(consumed_rcu_sum)\\",\\"id\\":\\"consumed_read_cap\\"}],[\\"AWS/DynamoDB\\",\\"ConsumedReadCapacityUnits\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"consumed_rcu_sum\\"}],[\\"AWS/DynamoDB\\",\\"ProvisionedReadCapacityUnits\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"label\\":\\"Provisioned\\",\\"id\\":\\"provisioned_read_cap\\"}],[{\\"label\\":\\"Utilization\\",\\"expression\\":\\"100*(consumed_read_cap/provisioned_read_cap)\\",\\"yAxis\\":\\"right\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false},\\"right\\":{\\"min\\":0,\\"max\\":100,\\"label\\":\\"%\\",\\"showUnits\\":false}},\\"legend\\":{\\"position\\":\\"right\\"}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":3,\\"x\\":0,\\"y\\":4,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Write Usage\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Consumed\\",\\"expression\\":\\"consumed_wcu_sum/PERIOD(consumed_wcu_sum)\\",\\"id\\":\\"consumed_write_cap\\"}],[\\"AWS/DynamoDB\\",\\"ConsumedWriteCapacityUnits\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"consumed_wcu_sum\\"}],[\\"AWS/DynamoDB\\",\\"ProvisionedWriteCapacityUnits\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"label\\":\\"Provisioned\\",\\"id\\":\\"provisioned_write_cap\\"}],[{\\"label\\":\\"Utilization\\",\\"expression\\":\\"100*(consumed_write_cap/provisioned_write_cap)\\",\\"yAxis\\":\\"right\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false},\\"right\\":{\\"min\\":0,\\"max\\":100,\\"label\\":\\"%\\",\\"showUnits\\":false}},\\"legend\\":{\\"position\\":\\"right\\"}}},{\\"type\\":\\"metric\\",\\"width\\":9,\\"height\\":6,\\"x\\":6,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Latency (Average)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\" \\",\\"expression\\":\\"SEARCH('{\\\\\\"AWS/DynamoDB\\\\\\",\\\\\\"TableName\\\\\\",\\\\\\"Operation\\\\\\"} \\\\\\"TableName\\\\\\"=\\\\\\"FirstGroupTable\\\\\\" MetricName=\\\\\\"SuccessfulRequestLatency\\\\\\"', 'Average', 300)\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"GetItem > 10000 for 3 datapoints within 15 minutes\\",\\"value\\":10000,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}},\\"legend\\":{\\"position\\":\\"right\\"}}},{\\"type\\":\\"metric\\",\\"width\\":3,\\"height\\":6,\\"x\\":15,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Throttles\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Read\\",\\"expression\\":\\"FILL(readThrottles,0)\\"}],[\\"AWS/DynamoDB\\",\\"ReadThrottleEvents\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"label\\":\\"Read\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"readThrottles\\"}],[{\\"label\\":\\"Write\\",\\"expression\\":\\"FILL(writeThrottles,0)\\"}],[\\"AWS/DynamoDB\\",\\"WriteThrottleEvents\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"label\\":\\"Write\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"writeThrottles\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":6,\\"x\\":18,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"System Errors\\",\\"expression\\":\\"systemErrorGetItem+systemErrorBatchGetItem+systemErrorScan+systemErrorQuery+systemErrorGetRecords+systemErrorPutItem+systemErrorDeleteItem+systemErrorUpdateItem+systemErrorBatchWriteItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"GetItem\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorGetItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"BatchGetItem\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorBatchGetItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"Scan\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorScan\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"Query\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorQuery\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"GetRecords\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorGetRecords\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"PutItem\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorPutItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"DeleteItem\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorDeleteItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"UpdateItem\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorUpdateItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"BatchWriteItem\\",\\"TableName\\",\\"FirstGroupTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorBatchWriteItem\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":7,\\"properties\\":{\\"markdown\\":\\"### Lambda Function **[SecondGroupFunction](https://", + Object { + "Ref": "AWS::Region", + }, + ".console.aws.amazon.com/lambda/home?region=", + Object { + "Ref": "AWS::Region", + }, + "#/functions/", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + ")**\\"}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":0,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"TPS\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"TPS\\",\\"expression\\":\\"FILL(requests,0) / PERIOD(requests)\\"}],[\\"AWS/Lambda\\",\\"Invocations\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Invocations\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"requests\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":6,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Latency\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Duration\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"P50 (avg: \${AVG})\\",\\"stat\\":\\"p50\\"}],[\\"AWS/Lambda\\",\\"Duration\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"P90 (avg: \${AVG})\\",\\"stat\\":\\"p90\\"}],[\\"AWS/Lambda\\",\\"Duration\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"P99 (avg: \${AVG})\\",\\"stat\\":\\"p99\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":12,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors (rate)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Errors\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Faults (avg)\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"Faults (avg) > 0.1 for 3 datapoints within 15 minutes\\",\\"value\\":0.1,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":18,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Rates\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Throttles\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Throttles (avg)\\"}],[\\"AWS/Lambda\\",\\"ProvisionedConcurrencySpilloverInvocations\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Provisioned Concurrency Spillovers (avg)\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":0,\\"y\\":13,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Invocations\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Invocations\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Invocations\\",\\"stat\\":\\"Sum\\"}],[\\"AWS/Lambda\\",\\"Throttles\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Throttles\\",\\"stat\\":\\"Sum\\"}],[\\"AWS/Lambda\\",\\"ConcurrentExecutions\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Concurrent\\",\\"stat\\":\\"Maximum\\"}],[\\"AWS/Lambda\\",\\"ProvisionedConcurrencySpilloverInvocations\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Provisioned Concurrency Spillovers\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":8,\\"y\\":13,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Errors\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Faults\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":16,\\"y\\":18,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Iterator\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"IteratorAge\\",\\"FunctionName\\",\\"", + Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:SecondGroupFunction", + ], + ], + }, + ], + }, + ], + }, + "\\",{\\"label\\":\\"Iterator Age\\",\\"stat\\":\\"Maximum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}}}}]}", + ], + ], + }, + "DashboardName": "NestingTestFacade", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + }, + "Rules": Object { + "CheckBootstrapVersion": Object { + "Assertions": Array [ + Object { + "Assert": Object { + "Fn::Not": Array [ + Object { + "Fn::Contains": Array [ + Array [ + "1", + "2", + "3", + "4", + "5", + ], + Object { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`test of defaults createCompositeAlarmUsingTag creates a composite alarm from matching alarms 1`] = ` +Object { + "Parameters": Object { + "BootstrapVersion": Object { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": Object { + "CompositeAlarmTestFacadeCompTestCompositeFirstLevel21A6FDB8": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "OnAlarmTopicForCompositesC70E2B13", + }, + ], + "AlarmDescription": "Composite alarm", + "AlarmName": "CompTest-Composite-FirstLevel", + "AlarmRule": Object { + "Fn::Join": Array [ + "", + Array [ + "(ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "CompositeAlarmTestFacadeCompTestCompositeTestTableReadThrottledEventsCritical6E9971F4", + "Arn", + ], + }, + "\\") OR ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "CompositeAlarmTestFacadeCompTestCompositeTestTableLatencyAverageGetItemCriticalF6E113E5", + "Arn", + ], + }, + "\\"))", + ], + ], + }, + }, + "Type": "AWS::CloudWatch::CompositeAlarm", + }, + "CompositeAlarmTestFacadeCompTestCompositeSecondLevel94FEC718": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "OnAlarmTopicForCompositesC70E2B13", + }, + ], + "AlarmDescription": "Composite alarm", + "AlarmName": "CompTest-Composite-SecondLevel", + "AlarmRule": Object { + "Fn::Join": Array [ + "", + Array [ + "(ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "CompositeAlarmTestFacadeCompTestCompositeTestFunctionFaultRateCritical2102776D", + "Arn", + ], + }, + "\\") OR ALARM(\\"", + Object { + "Fn::GetAtt": Array [ + "CompositeAlarmTestFacadeCompTestCompositeFirstLevel21A6FDB8", + "Arn", + ], + }, + "\\"))", + ], + ], + }, + }, + "Type": "AWS::CloudWatch::CompositeAlarm", + }, + "CompositeAlarmTestFacadeCompTestCompositeTestFunctionFaultRateCritical2102776D": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "OnAlarmTopicForCompositesC70E2B13", + }, + ], + "AlarmDescription": "Fault rate is too high.", + "AlarmName": "CompTest-CompositeTestFunction-Fault-Rate-Critical", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "Faults (avg)", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": "TestFunction", + }, + ], + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 0.5, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CompositeAlarmTestFacadeCompTestCompositeTestTableLatencyAverageGetItemCriticalF6E113E5": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "OnAlarmTopicForCompositesC70E2B13", + }, + ], + "AlarmDescription": "Average latency is too high.", + "AlarmName": "CompTest-CompositeTestTable-Latency-Average-GetItem-Critical", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "GetItem", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "Operation", + "Value": "GetItem", + }, + Object { + "Name": "TableName", + "Value": "TestTable", + }, + ], + "MetricName": "SuccessfulRequestLatency", + "Namespace": "AWS/DynamoDB", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 10000, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CompositeAlarmTestFacadeCompTestCompositeTestTableReadThrottledEventsCritical6E9971F4": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmActions": Array [ + Object { + "Ref": "OnAlarmTopicForCompositesC70E2B13", + }, + ], + "AlarmDescription": "Read throttled events above threshold.", + "AlarmName": "CompTest-CompositeTestTable-Read-Throttled-Events-Critical", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Expression": "FILL(readThrottles,0)", + "Id": "expr_1", + "Label": "Read", + }, + Object { + "Id": "readThrottles", + "Label": "Read", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "TableName", + "Value": "TestTable", + }, + ], + "MetricName": "ReadThrottleEvents", + "Namespace": "AWS/DynamoDB", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": false, + }, + ], + "Threshold": 100, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CompositeAlarmTestFacadeCompositeAlarmTestFacadeDashboardsDashboard633A0A44": Object { + "Properties": Object { + "DashboardBody": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"start\\":\\"-PT8H\\",\\"periodOverride\\":\\"inherit\\",\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### Dynamo Table **[CompositeTestTable](https://", + Object { + "Ref": "AWS::Region", + }, + ".console.aws.amazon.com/dynamodb/home?region=", + Object { + "Ref": "AWS::Region", + }, + "#tables:selected=TestTable)**\\"}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":3,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Read Usage\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Consumed\\",\\"expression\\":\\"consumed_rcu_sum/PERIOD(consumed_rcu_sum)\\",\\"id\\":\\"consumed_read_cap\\"}],[\\"AWS/DynamoDB\\",\\"ConsumedReadCapacityUnits\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"consumed_rcu_sum\\"}],[\\"AWS/DynamoDB\\",\\"ProvisionedReadCapacityUnits\\",\\"TableName\\",\\"TestTable\\",{\\"label\\":\\"Provisioned\\",\\"id\\":\\"provisioned_read_cap\\"}],[{\\"label\\":\\"Utilization\\",\\"expression\\":\\"100*(consumed_read_cap/provisioned_read_cap)\\",\\"yAxis\\":\\"right\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false},\\"right\\":{\\"min\\":0,\\"max\\":100,\\"label\\":\\"%\\",\\"showUnits\\":false}},\\"legend\\":{\\"position\\":\\"right\\"}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":3,\\"x\\":0,\\"y\\":4,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Write Usage\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Consumed\\",\\"expression\\":\\"consumed_wcu_sum/PERIOD(consumed_wcu_sum)\\",\\"id\\":\\"consumed_write_cap\\"}],[\\"AWS/DynamoDB\\",\\"ConsumedWriteCapacityUnits\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"consumed_wcu_sum\\"}],[\\"AWS/DynamoDB\\",\\"ProvisionedWriteCapacityUnits\\",\\"TableName\\",\\"TestTable\\",{\\"label\\":\\"Provisioned\\",\\"id\\":\\"provisioned_write_cap\\"}],[{\\"label\\":\\"Utilization\\",\\"expression\\":\\"100*(consumed_write_cap/provisioned_write_cap)\\",\\"yAxis\\":\\"right\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false},\\"right\\":{\\"min\\":0,\\"max\\":100,\\"label\\":\\"%\\",\\"showUnits\\":false}},\\"legend\\":{\\"position\\":\\"right\\"}}},{\\"type\\":\\"metric\\",\\"width\\":9,\\"height\\":6,\\"x\\":6,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Latency (Average)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\" \\",\\"expression\\":\\"SEARCH('{\\\\\\"AWS/DynamoDB\\\\\\",\\\\\\"TableName\\\\\\",\\\\\\"Operation\\\\\\"} \\\\\\"TableName\\\\\\"=\\\\\\"TestTable\\\\\\" MetricName=\\\\\\"SuccessfulRequestLatency\\\\\\"', 'Average', 300)\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"GetItem > 10000 for 3 datapoints within 15 minutes\\",\\"value\\":10000,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}},\\"legend\\":{\\"position\\":\\"right\\"}}},{\\"type\\":\\"metric\\",\\"width\\":3,\\"height\\":6,\\"x\\":15,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Throttles\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"Read\\",\\"expression\\":\\"FILL(readThrottles,0)\\"}],[\\"AWS/DynamoDB\\",\\"ReadThrottleEvents\\",\\"TableName\\",\\"TestTable\\",{\\"label\\":\\"Read\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"readThrottles\\"}],[{\\"label\\":\\"Write\\",\\"expression\\":\\"FILL(writeThrottles,0)\\"}],[\\"AWS/DynamoDB\\",\\"WriteThrottleEvents\\",\\"TableName\\",\\"TestTable\\",{\\"label\\":\\"Write\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"writeThrottles\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"Read > 100 for 3 datapoints within 15 minutes\\",\\"value\\":100,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":6,\\"x\\":18,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"System Errors\\",\\"expression\\":\\"systemErrorGetItem+systemErrorBatchGetItem+systemErrorScan+systemErrorQuery+systemErrorGetRecords+systemErrorPutItem+systemErrorDeleteItem+systemErrorUpdateItem+systemErrorBatchWriteItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"GetItem\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorGetItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"BatchGetItem\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorBatchGetItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"Scan\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorScan\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"Query\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorQuery\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"GetRecords\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorGetRecords\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"PutItem\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorPutItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"DeleteItem\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorDeleteItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"UpdateItem\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorUpdateItem\\"}],[\\"AWS/DynamoDB\\",\\"SystemErrors\\",\\"Operation\\",\\"BatchWriteItem\\",\\"TableName\\",\\"TestTable\\",{\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"systemErrorBatchWriteItem\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":7,\\"properties\\":{\\"markdown\\":\\"### Lambda Function **[CompositeTestFunction](https://", + Object { + "Ref": "AWS::Region", + }, + ".console.aws.amazon.com/lambda/home?region=", + Object { + "Ref": "AWS::Region", + }, + "#/functions/TestFunction)**\\"}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":0,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"TPS\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[{\\"label\\":\\"TPS\\",\\"expression\\":\\"FILL(requests,0) / PERIOD(requests)\\"}],[\\"AWS/Lambda\\",\\"Invocations\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Invocations\\",\\"stat\\":\\"Sum\\",\\"visible\\":false,\\"id\\":\\"requests\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":6,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Latency\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Duration\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"P50 (avg: \${AVG})\\",\\"stat\\":\\"p50\\"}],[\\"AWS/Lambda\\",\\"Duration\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"P90 (avg: \${AVG})\\",\\"stat\\":\\"p90\\"}],[\\"AWS/Lambda\\",\\"Duration\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"P99 (avg: \${AVG})\\",\\"stat\\":\\"p99\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":12,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors (rate)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Errors\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Faults (avg)\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"Faults (avg) > 0.5 for 3 datapoints within 15 minutes\\",\\"value\\":0.5,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":5,\\"x\\":18,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Rates\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Throttles\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Throttles (avg)\\"}],[\\"AWS/Lambda\\",\\"ProvisionedConcurrencySpilloverInvocations\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Provisioned Concurrency Spillovers (avg)\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":0,\\"y\\":13,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Invocations\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Invocations\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Invocations\\",\\"stat\\":\\"Sum\\"}],[\\"AWS/Lambda\\",\\"Throttles\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Throttles\\",\\"stat\\":\\"Sum\\"}],[\\"AWS/Lambda\\",\\"ConcurrentExecutions\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Concurrent\\",\\"stat\\":\\"Maximum\\"}],[\\"AWS/Lambda\\",\\"ProvisionedConcurrencySpilloverInvocations\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Provisioned Concurrency Spillovers\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":8,\\"y\\":13,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"Errors\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Faults\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":16,\\"y\\":18,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Iterator\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/Lambda\\",\\"IteratorAge\\",\\"FunctionName\\",\\"TestFunction\\",{\\"label\\":\\"Iterator Age\\",\\"stat\\":\\"Maximum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}}}}]}", + ], + ], + }, + "DashboardName": "CompositeAlarmTestFacade", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + "OnAlarmTopicForCompositesC70E2B13": Object { + "Properties": Object { + "TopicName": "CompositeAlarmTopic", + }, + "Type": "AWS::SNS::Topic", + }, + }, + "Rules": Object { + "CheckBootstrapVersion": Object { + "Assertions": Array [ + Object { + "Assert": Object { + "Fn::Not": Array [ + Object { + "Fn::Contains": Array [ + Array [ + "1", + "2", + "3", + "4", + "5", + ], + Object { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + exports[`test of defaults typical usage 1`] = ` Object { "Parameters": Object {