Repository: cdklabs/cdk-monitoring-constructs (language: python)

Issue:
Add suppressor alarm props to `AddCompositeAlarmProps`.
### Feature scope

AlarmFactory

### Describe your suggested feature

CloudWatch CompositeAlarm now supports specifying a suppressor alarm `actionsSuppressor` when initializing (see [CompositeAlarm API reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-compositealarm.html)). However, this additional parameter is not present within `AddCompositeAlarmProps`, and thus, it is not possible to create a CompositeAlarm using `AlarmFactory.addCompositeAlarm(...)` while simultaneously specifying a suppressor alarm.

Could you please add this parameter to `AddCompositeAlarmProps`, as well as the other associated props, i.e., ` ActionsSuppressorExtensionPeriod `, ` ActionsSuppressorWaitPeriod`, etc.? Thank you so much.


Discussion / hints:

Fix patch:
diff --git a/API.md b/API.md
index 568812ba..88aaf5c6 100644
--- a/API.md
+++ b/API.md
@@ -2814,6 +2814,9 @@ const addCompositeAlarmProps: AddCompositeAlarmProps = { ... }
 | <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.disambiguator">disambiguator</a></code> | <code>string</code> | Disambiguator is a string that differentiates this alarm from other similar ones. |
 | <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionOverride">actionOverride</a></code> | <code><a href="#cdk-monitoring-constructs.IAlarmActionStrategy">IAlarmActionStrategy</a></code> | Allows to override the default action strategy. |
 | <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsEnabled">actionsEnabled</a></code> | <code>boolean</code> | Enables the configured CloudWatch alarm ticketing actions. |
+| <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsSuppressor">actionsSuppressor</a></code> | <code>aws-cdk-lib.aws_cloudwatch.IAlarm</code> | Actions will be suppressed if the suppressor alarm is in the ALARM state. |
+| <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsSuppressorExtensionPeriod">actionsSuppressorExtensionPeriod</a></code> | <code>aws-cdk-lib.Duration</code> | The maximum time in seconds that the composite alarm waits after suppressor alarm goes out of the ALARM state. |
+| <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsSuppressorWaitPeriod">actionsSuppressorWaitPeriod</a></code> | <code>aws-cdk-lib.Duration</code> | The maximum duration that the composite alarm waits for the suppressor alarm to go into the ALARM state. |
 | <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.alarmDedupeStringSuffix">alarmDedupeStringSuffix</a></code> | <code>string</code> | If this is defined, the default resource-specific alarm dedupe string will be set and this will be added as a suffix. |
 | <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.alarmDescription">alarmDescription</a></code> | <code>string</code> | Alarm description is included in the ticket and therefore should describe what happened, with as much context as possible. |
 | <code><a href="#cdk-monitoring-constructs.AddCompositeAlarmProps.property.alarmDescriptionOverride">alarmDescriptionOverride</a></code> | <code>string</code> | A text included in the generated ticket description body, which fully replaces the generated text. |
@@ -2867,6 +2870,49 @@ Enables the configured CloudWatch alarm ticketing actions.
 
 ---
 
+##### `actionsSuppressor`<sup>Optional</sup> <a name="actionsSuppressor" id="cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsSuppressor"></a>
+
+```typescript
+public readonly actionsSuppressor: IAlarm;
+```
+
+- *Type:* aws-cdk-lib.aws_cloudwatch.IAlarm
+- *Default:* no suppressor alarm
+
+Actions will be suppressed if the suppressor alarm is in the ALARM state.
+
+---
+
+##### `actionsSuppressorExtensionPeriod`<sup>Optional</sup> <a name="actionsSuppressorExtensionPeriod" id="cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsSuppressorExtensionPeriod"></a>
+
+```typescript
+public readonly actionsSuppressorExtensionPeriod: Duration;
+```
+
+- *Type:* aws-cdk-lib.Duration
+- *Default:* 60 seconds
+
+The maximum time in seconds that the composite alarm waits after suppressor alarm goes out of the ALARM state.
+
+After this time, the composite alarm performs its actions.
+
+---
+
+##### `actionsSuppressorWaitPeriod`<sup>Optional</sup> <a name="actionsSuppressorWaitPeriod" id="cdk-monitoring-constructs.AddCompositeAlarmProps.property.actionsSuppressorWaitPeriod"></a>
+
+```typescript
+public readonly actionsSuppressorWaitPeriod: Duration;
+```
+
+- *Type:* aws-cdk-lib.Duration
+- *Default:* 60 seconds
+
+The maximum duration that the composite alarm waits for the suppressor alarm to go into the ALARM state.
+
+After this time, the composite alarm performs its actions.
+
+---
+
 ##### `alarmDedupeStringSuffix`<sup>Optional</sup> <a name="alarmDedupeStringSuffix" id="cdk-monitoring-constructs.AddCompositeAlarmProps.property.alarmDedupeStringSuffix"></a>
 
 ```typescript
diff --git a/lib/common/alarm/AlarmFactory.ts b/lib/common/alarm/AlarmFactory.ts
index e2a7b37f..305e5ca5 100644
--- a/lib/common/alarm/AlarmFactory.ts
+++ b/lib/common/alarm/AlarmFactory.ts
@@ -6,6 +6,7 @@ import {
   ComparisonOperator,
   CompositeAlarm,
   HorizontalAnnotation,
+  IAlarm,
   IAlarmRule,
   IMetric,
   MathExpression,
@@ -388,6 +389,29 @@ export interface AddCompositeAlarmProps {
    * @default - OR
    */
   readonly compositeOperator?: CompositeAlarmOperator;
+
+  /**
+   * Actions will be suppressed if the suppressor alarm is in the ALARM state.
+   *
+   * @default - no suppressor alarm
+   */
+  readonly actionsSuppressor?: IAlarm;
+
+  /**
+   * The maximum time in seconds that the composite alarm waits after suppressor alarm goes out of the ALARM state.
+   * After this time, the composite alarm performs its actions.
+   *
+   * @default - 60 seconds
+   */
+  readonly actionsSuppressorExtensionPeriod?: Duration;
+
+  /**
+   * The maximum duration that the composite alarm waits for the suppressor alarm to go into the ALARM state.
+   * After this time, the composite alarm performs its actions.
+   *
+   * @default - 60 seconds
+   */
+  readonly actionsSuppressorWaitPeriod?: Duration;
 }
 
 /**
@@ -779,6 +803,9 @@ export class AlarmFactory {
       alarmDescription,
       alarmRule,
       actionsEnabled,
+      actionsSuppressor: props?.actionsSuppressor,
+      actionsSuppressorExtensionPeriod: props?.actionsSuppressorExtensionPeriod,
+      actionsSuppressorWaitPeriod: props?.actionsSuppressorWaitPeriod,
     });
 
     action.addAlarmActions({


Test patch:
diff --git a/test/common/alarm/AlarmFactory.test.ts b/test/common/alarm/AlarmFactory.test.ts
index 5aedc87c..77e4309a 100644
--- a/test/common/alarm/AlarmFactory.test.ts
+++ b/test/common/alarm/AlarmFactory.test.ts
@@ -584,6 +584,93 @@ test("addCompositeAlarm: snapshot for operator", () => {
   expect(Template.fromStack(stack)).toMatchSnapshot();
 });
 
+test("addCompositeAlarm: snapshot for suppressor alarm props", () => {
+  const stack = new Stack();
+  const factory = new AlarmFactory(stack, {
+    globalMetricDefaults,
+    globalAlarmDefaults: globalAlarmDefaultsWithDisambiguator,
+    localAlarmNamePrefix: "prefix",
+  });
+  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,
+  });
+  const suppressorAlarm = factory.addCompositeAlarm([alarm1, alarm2], {
+    disambiguator: "SuppressorAlarm",
+    alarmNameSuffix: "SuppressorAlarm",
+  });
+  factory.addCompositeAlarm([alarm1, alarm2], {
+    disambiguator: "SuppressedAlarmDefault",
+    alarmNameSuffix: "SuppressedAlarmDefault",
+    actionsSuppressor: suppressorAlarm,
+  });
+  factory.addCompositeAlarm([alarm1, alarm2], {
+    disambiguator: "SuppressedAlarmTestExtensionPeriod",
+    alarmNameSuffix: "SuppressedAlarmTestExtensionPeriod",
+    actionsSuppressor: suppressorAlarm,
+    actionsSuppressorExtensionPeriod: Duration.seconds(100),
+  });
+  factory.addCompositeAlarm([alarm1, alarm2], {
+    disambiguator: "SuppressedAlarmTestWaitPeriod",
+    alarmNameSuffix: "SuppressedAlarmTestWaitPeriod",
+    actionsSuppressor: suppressorAlarm,
+    actionsSuppressorWaitPeriod: Duration.seconds(100),
+  });
+  factory.addCompositeAlarm([alarm1, alarm2], {
+    disambiguator: "SuppressedAlarmTestBothExtensionAndWaitPeriod",
+    alarmNameSuffix: "SuppressedAlarmTestBothExtensionAndWaitPeriod",
+    actionsSuppressor: suppressorAlarm,
+    actionsSuppressorExtensionPeriod: Duration.seconds(100),
+    actionsSuppressorWaitPeriod: Duration.seconds(100),
+  });
+
+  expect(Template.fromStack(stack)).toMatchSnapshot();
+});
+
+test("addCompositeAlarm: actions suppressor extension period specified but no actions suppressor throws error", () => {
+  expect(() => {
+    factory.addCompositeAlarm([], {
+      disambiguator: "CompositeAlarmExtensionPeriodSpecifiedNoSuppressorAlarm",
+      alarmNameSuffix:
+        "CompositeAlarmExtensionPeriodSpecifiedNoSuppressorAlarm",
+      actionsSuppressorExtensionPeriod: Duration.seconds(100),
+    });
+  }).toThrow(Error);
+});
+
+test("addCompositeAlarm: actions suppressor wait period specified but no actions suppressor throws error", () => {
+  expect(() => {
+    factory.addCompositeAlarm([], {
+      disambiguator: "CompositeAlarmWaitPeriodSpecifiedNoSuppressorAlarm",
+      alarmNameSuffix: "CompositeAlarmWaitPeriodSpecifiedNoSuppressorAlarm",
+      actionsSuppressorWaitPeriod: Duration.seconds(100),
+    });
+  }).toThrow(Error);
+});
+
+test("addCompositeAlarm: actions suppressor both extension and wait periods specified but no actions suppressor throws error", () => {
+  expect(() => {
+    factory.addCompositeAlarm([], {
+      disambiguator:
+        "CompositeAlarmBothExtensionAndWaitPeriodSpecifiedNoSuppressorAlarm",
+      alarmNameSuffix:
+        "CompositeAlarmBothExtensionAndWaitPeriodSpecifiedNoSuppressorAlarm",
+      actionsSuppressorExtensionPeriod: Duration.seconds(100),
+      actionsSuppressorWaitPeriod: Duration.seconds(100),
+    });
+  }).toThrow(Error);
+});
+
 test("addAlarm: original actionOverride with a different action gets preserved", () => {
   const originalActionOverride = new SnsAlarmActionStrategy({
     onAlarmTopic: new Topic(stack, "Dummy1"),
diff --git a/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap b/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap
index d92e173a..7dbd76be 100644
--- a/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap
+++ b/test/common/alarm/__snapshots__/AlarmFactory.test.ts.snap
@@ -245,3 +245,272 @@ Object {
   },
 }
 `;
+
+exports[`addCompositeAlarm: snapshot for suppressor alarm props 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<String>",
+    },
+  },
+  "Resources": Object {
+    "DummyServiceAlarmsprefixAlarm1F4FCF957": Object {
+      "Properties": Object {
+        "ActionsEnabled": false,
+        "AlarmDescription": "Testing alarm 1",
+        "AlarmName": "DummyServiceAlarms-prefix-Alarm1",
+        "ComparisonOperator": "GreaterThanThreshold",
+        "DatapointsToAlarm": 6,
+        "Dimensions": Array [
+          Object {
+            "Name": "CustomDimension",
+            "Value": "CustomDimensionValue",
+          },
+        ],
+        "EvaluationPeriods": 6,
+        "MetricName": "DummyMetric1",
+        "Namespace": "DummyCustomNamespace",
+        "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,
+        "Dimensions": Array [
+          Object {
+            "Name": "CustomDimension",
+            "Value": "CustomDimensionValue",
+          },
+        ],
+        "EvaluationPeriods": 6,
+        "MetricName": "DummyMetric1",
+        "Namespace": "DummyCustomNamespace",
+        "Period": 300,
+        "Statistic": "Average",
+        "Threshold": 2,
+        "TreatMissingData": "missing",
+      },
+      "Type": "AWS::CloudWatch::Alarm",
+    },
+    "DummyServiceAlarmsprefixSuppressedAlarmDefault3397EEAA": Object {
+      "Properties": Object {
+        "ActionsEnabled": false,
+        "ActionsSuppressor": Object {
+          "Fn::GetAtt": Array [
+            "DummyServiceAlarmsprefixSuppressorAlarm7A8145E7",
+            "Arn",
+          ],
+        },
+        "ActionsSuppressorExtensionPeriod": 60,
+        "ActionsSuppressorWaitPeriod": 60,
+        "AlarmDescription": "Composite alarm",
+        "AlarmName": "DummyServiceAlarms-prefix-SuppressedAlarmDefault",
+        "AlarmRule": Object {
+          "Fn::Join": Array [
+            "",
+            Array [
+              "(ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm1F4FCF957",
+                  "Arn",
+                ],
+              },
+              "\\") OR ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm25ADF8B37",
+                  "Arn",
+                ],
+              },
+              "\\"))",
+            ],
+          ],
+        },
+      },
+      "Type": "AWS::CloudWatch::CompositeAlarm",
+    },
+    "DummyServiceAlarmsprefixSuppressedAlarmTestBothExtensionAndWaitPeriodAD8EE66D": Object {
+      "Properties": Object {
+        "ActionsEnabled": false,
+        "ActionsSuppressor": Object {
+          "Fn::GetAtt": Array [
+            "DummyServiceAlarmsprefixSuppressorAlarm7A8145E7",
+            "Arn",
+          ],
+        },
+        "ActionsSuppressorExtensionPeriod": 100,
+        "ActionsSuppressorWaitPeriod": 100,
+        "AlarmDescription": "Composite alarm",
+        "AlarmName": "DummyServiceAlarms-prefix-SuppressedAlarmTestBothExtensionAndWaitPeriod",
+        "AlarmRule": Object {
+          "Fn::Join": Array [
+            "",
+            Array [
+              "(ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm1F4FCF957",
+                  "Arn",
+                ],
+              },
+              "\\") OR ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm25ADF8B37",
+                  "Arn",
+                ],
+              },
+              "\\"))",
+            ],
+          ],
+        },
+      },
+      "Type": "AWS::CloudWatch::CompositeAlarm",
+    },
+    "DummyServiceAlarmsprefixSuppressedAlarmTestExtensionPeriodE8E43F86": Object {
+      "Properties": Object {
+        "ActionsEnabled": false,
+        "ActionsSuppressor": Object {
+          "Fn::GetAtt": Array [
+            "DummyServiceAlarmsprefixSuppressorAlarm7A8145E7",
+            "Arn",
+          ],
+        },
+        "ActionsSuppressorExtensionPeriod": 100,
+        "ActionsSuppressorWaitPeriod": 60,
+        "AlarmDescription": "Composite alarm",
+        "AlarmName": "DummyServiceAlarms-prefix-SuppressedAlarmTestExtensionPeriod",
+        "AlarmRule": Object {
+          "Fn::Join": Array [
+            "",
+            Array [
+              "(ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm1F4FCF957",
+                  "Arn",
+                ],
+              },
+              "\\") OR ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm25ADF8B37",
+                  "Arn",
+                ],
+              },
+              "\\"))",
+            ],
+          ],
+        },
+      },
+      "Type": "AWS::CloudWatch::CompositeAlarm",
+    },
+    "DummyServiceAlarmsprefixSuppressedAlarmTestWaitPeriod09DC1879": Object {
+      "Properties": Object {
+        "ActionsEnabled": false,
+        "ActionsSuppressor": Object {
+          "Fn::GetAtt": Array [
+            "DummyServiceAlarmsprefixSuppressorAlarm7A8145E7",
+            "Arn",
+          ],
+        },
+        "ActionsSuppressorExtensionPeriod": 60,
+        "ActionsSuppressorWaitPeriod": 100,
+        "AlarmDescription": "Composite alarm",
+        "AlarmName": "DummyServiceAlarms-prefix-SuppressedAlarmTestWaitPeriod",
+        "AlarmRule": Object {
+          "Fn::Join": Array [
+            "",
+            Array [
+              "(ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm1F4FCF957",
+                  "Arn",
+                ],
+              },
+              "\\") OR ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm25ADF8B37",
+                  "Arn",
+                ],
+              },
+              "\\"))",
+            ],
+          ],
+        },
+      },
+      "Type": "AWS::CloudWatch::CompositeAlarm",
+    },
+    "DummyServiceAlarmsprefixSuppressorAlarm7A8145E7": Object {
+      "Properties": Object {
+        "ActionsEnabled": false,
+        "AlarmDescription": "Composite alarm",
+        "AlarmName": "DummyServiceAlarms-prefix-SuppressorAlarm",
+        "AlarmRule": Object {
+          "Fn::Join": Array [
+            "",
+            Array [
+              "(ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm1F4FCF957",
+                  "Arn",
+                ],
+              },
+              "\\") OR ALARM(\\"",
+              Object {
+                "Fn::GetAtt": Array [
+                  "DummyServiceAlarmsprefixAlarm25ADF8B37",
+                  "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.",
+        },
+      ],
+    },
+  },
+}
+`;