Skip to content

Commit

Permalink
feat(aws-cloudwatch): composite alarm actions suppression (#22330)
Browse files Browse the repository at this point in the history
Recently CloudWatch launched [Alarm Suppression feature](https://www.amazonaws.cn/en/new/2022/amazon-cloudwatch-supports-composite-alarm-actions-suppression/) that is now available via [CFN](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-compositealarm.html#cfn-cloudwatch-compositealarm-actionssuppressor) as well. This PR enables feature parity between CDK and CFN and allows CDK customers to leverage the suppression feature.

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
hydro0 authored Oct 4, 2022
1 parent 983d26e commit 19c945e
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 18 deletions.
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,30 @@ new cloudwatch.CompositeAlarm(this, 'MyAwesomeCompositeAlarm', {
});
```

#### Actions Suppressor

If you want to disable actions of a Composite Alarm based on a certain condition, you can use [Actions Suppression](https://www.amazonaws.cn/en/new/2022/amazon-cloudwatch-supports-composite-alarm-actions-suppression/).

```ts
declare const childAlarm1: cloudwatch.Alarm;
declare const childAlarm2: cloudwatch.Alarm;
declare const onAlarmAction: cloudwatch.IAlarmAction;
declare const onOkAction: cloudwatch.IAlarmAction;
declare const actionsSuppressor: cloudwatch.Alarm;

const alarmRule = cloudwatch.AlarmRule.anyOf(alarm1, alarm2);

const myCompositeAlarm = new cloudwatch.CompositeAlarm(this, 'MyAwesomeCompositeAlarm', {
alarmRule,
actionsSuppressor,
});
myCompositeAlarm.addAlarmActions(onAlarmAction);
myComposireAlarm.addOkAction(onOkAction);
```

In the provided example, if `actionsSuppressor` is in `ALARM` state, `onAlarmAction` won't be triggered even if `myCompositeAlarm` goes into `ALARM` state.
Similar, if `actionsSuppressor` is in `ALARM` state and `myCompositeAlarm` goes from `ALARM` into `OK` state, `onOkAction` won't be triggered.

### A note on units

In CloudWatch, Metrics datums are emitted with units, such as `seconds` or
Expand Down
42 changes: 39 additions & 3 deletions packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArnFormat, Lazy, Names, Stack } from '@aws-cdk/core';
import { ArnFormat, Lazy, Names, Stack, Duration } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base';
import { CfnCompositeAlarm } from './cloudwatch.generated';
Expand All @@ -18,14 +18,14 @@ export interface CompositeAlarmProps {
/**
* Description for the alarm
*
* @default No description
* @default - No description.
*/
readonly alarmDescription?: string;

/**
* Name of the alarm
*
* @default Automatically generated name
* @default - Automatically generated name.
*/
readonly compositeAlarmName?: string;

Expand All @@ -34,6 +34,28 @@ export interface CompositeAlarmProps {
*/
readonly alarmRule: IAlarmRule;

/**
* Actions will be suppressed if the suppressor alarm is in the ALARM state.
*
* @default - alarm will not be suppressed.
*/
readonly actionsSuppressor?: IAlarm;

/**
* The maximum duration that the composite alarm waits after suppressor alarm goes out of the ALARM state.
* After this time, the composite alarm performs its actions.
*
* @default - 1 minute extension period will be set.
*/
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 - 1 minute wait period will be set.
*/
readonly actionsSuppressorWaitPeriod?: Duration;
}

/**
Expand Down Expand Up @@ -98,6 +120,17 @@ export class CompositeAlarm extends AlarmBase {
throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule');
}

let extensionPeriod = props.actionsSuppressorExtensionPeriod;
let waitPeriod = props.actionsSuppressorWaitPeriod;
if (props.actionsSuppressor === undefined) {
if (extensionPeriod !== undefined || waitPeriod !== undefined) {
throw new Error('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.');
}
} else {
extensionPeriod = extensionPeriod ?? Duration.minutes(1);
waitPeriod = waitPeriod ?? Duration.minutes(1);
}

this.alarmRule = props.alarmRule.renderAlarmRule();

const alarm = new CfnCompositeAlarm(this, 'Resource', {
Expand All @@ -108,6 +141,9 @@ export class CompositeAlarm extends AlarmBase {
alarmActions: Lazy.list({ produce: () => this.alarmActionArns }),
insufficientDataActions: Lazy.list({ produce: (() => this.insufficientDataActionArns) }),
okActions: Lazy.list({ produce: () => this.okActionArns }),
actionsSuppressor: props.actionsSuppressor?.alarmArn,
actionsSuppressorExtensionPeriod: extensionPeriod?.toSeconds(),
actionsSuppressorWaitPeriod: waitPeriod?.toSeconds(),
});

this.alarmName = this.getResourceNameAttribute(alarm.ref);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"version": "20.0.0",
"version": "21.0.0",
"files": {
"1f1d7f1c425488b9245a0ff851dae7650c25e5558781cc88a972edb6a36be237": {
"ad8a5012407e26a8fc0b1b169b0ab2373b8466d955070ee91a90193c5c70d1a4": {
"source": {
"path": "CompositeAlarmIntegrationTest.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "1f1d7f1c425488b9245a0ff851dae7650c25e5558781cc88a972edb6a36be237.json",
"objectKey": "ad8a5012407e26a8fc0b1b169b0ab2373b8466d955070ee91a90193c5c70d1a4.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,15 @@
"\")))) OR FALSE)"
]
]
}
},
"ActionsSuppressor": {
"Fn::GetAtt": [
"Alarm548383B2F",
"Arn"
]
},
"ActionsSuppressorExtensionPeriod": 60,
"ActionsSuppressorWaitPeriod": 60
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"20.0.0"}
{"version":"21.0.0"}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "20.0.0",
"version": "21.0.0",
"testCases": {
"integ.composite-alarm": {
"stacks": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "20.0.0",
"version": "21.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
Expand All @@ -23,7 +23,7 @@
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/1f1d7f1c425488b9245a0ff851dae7650c25e5558781cc88a972edb6a36be237.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ad8a5012407e26a8fc0b1b169b0ab2373b8466d955070ee91a90193c5c70d1a4.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"path": "Tree",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"version": "10.1.108"
}
},
"CompositeAlarmIntegrationTest": {
Expand Down Expand Up @@ -220,7 +220,15 @@
"\")))) OR FALSE)"
]
]
}
},
"actionsSuppressor": {
"Fn::GetAtt": [
"Alarm548383B2F",
"Arn"
]
},
"actionsSuppressorExtensionPeriod": 60,
"actionsSuppressorWaitPeriod": 60
}
},
"constructInfo": {
Expand All @@ -236,14 +244,14 @@
}
},
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"fqn": "@aws-cdk/core.Stack",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.85"
"fqn": "@aws-cdk/core.App",
"version": "0.0.0"
}
}
}
88 changes: 87 additions & 1 deletion packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Template } from '@aws-cdk/assertions';
import { Stack } from '@aws-cdk/core';
import { Duration, Stack } from '@aws-cdk/core';
import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib';

describe('CompositeAlarm', () => {
Expand Down Expand Up @@ -109,4 +109,90 @@ describe('CompositeAlarm', () => {

});

test('test action suppressor translates to a correct CFN properties', () => {
const stack = new Stack();

const testMetric = new Metric({
namespace: 'CDK/Test',
metricName: 'Metric',
});

const actionsSuppressor = new Alarm(stack, 'Alarm1', {
metric: testMetric,
threshold: 100,
evaluationPeriods: 3,
});


const alarmRule = AlarmRule.fromBoolean(true);

new CompositeAlarm(stack, 'CompositeAlarm', {
alarmRule,
actionsSuppressor,
actionsSuppressorExtensionPeriod: Duration.minutes(2),
actionsSuppressorWaitPeriod: Duration.minutes(5),
});

Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::CompositeAlarm', {
AlarmName: 'CompositeAlarm',
ActionsSuppressor: {
'Fn::GetAtt': [
'Alarm1F9009D71',
'Arn',
],
},
ActionsSuppressorExtensionPeriod: 120,
ActionsSuppressorWaitPeriod: 300,
});
});

test('test wait and extension periods set without action suppressor', () => {
const stack = new Stack();

const alarmRule = AlarmRule.fromBoolean(true);

var createAlarm = () => new CompositeAlarm(stack, 'CompositeAlarm', {
alarmRule,
actionsSuppressorExtensionPeriod: Duration.minutes(2),
actionsSuppressorWaitPeriod: Duration.minutes(5),
});

expect(createAlarm).toThrow('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.');
});

test('test action suppressor has correct defaults set', () => {
const stack = new Stack();

const testMetric = new Metric({
namespace: 'CDK/Test',
metricName: 'Metric',
});

const actionsSuppressor = new Alarm(stack, 'Alarm1', {
metric: testMetric,
threshold: 100,
evaluationPeriods: 3,
});


const alarmRule = AlarmRule.fromBoolean(true);

new CompositeAlarm(stack, 'CompositeAlarm', {
alarmRule,
actionsSuppressor,
});

Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::CompositeAlarm', {
AlarmName: 'CompositeAlarm',
ActionsSuppressor: {
'Fn::GetAtt': [
'Alarm1F9009D71',
'Arn',
],
},
ActionsSuppressorExtensionPeriod: 60,
ActionsSuppressorWaitPeriod: 60,
});
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class CompositeAlarmIntegrationTest extends Stack {

new CompositeAlarm(this, 'CompositeAlarm', {
alarmRule,
actionsSuppressor: alarm5,
});
}

Expand Down

0 comments on commit 19c945e

Please sign in to comment.