From cdf94316f56690f6ee24e7318503a3b034cf790c Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Thu, 11 Jun 2020 19:09:07 +0530 Subject: [PATCH 1/5] feat(aws-cloudwatch): CompositeAlarm --- packages/@aws-cdk/aws-cloudwatch/README.md | 23 ++ packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 157 +++++++++---- .../aws-cloudwatch/lib/composite-alarm.ts | 208 ++++++++++++++++++ packages/@aws-cdk/aws-cloudwatch/lib/index.ts | 1 + packages/@aws-cdk/aws-cloudwatch/package.json | 3 +- .../test/integ.composite-alarm.expected.json | 82 +++++++ .../test/integ.composite-alarm.ts | 61 +++++ .../test/test.composite-alarm.ts | 98 +++++++++ 8 files changed, 588 insertions(+), 45 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index 94959b6e8de80..41ede4625eab8 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -174,6 +174,29 @@ const alarm = new cloudwatch.Alarm(stack, 'Alarm', { /* ... */ }); alarm.addAlarmAction(new cw_actions.SnsAction(topic)); ``` +### Composite Alarms + +[Composite Alarms](https://aws.amazon.com/about-aws/whats-new/2020/03/amazon-cloudwatch-now-allows-you-to-combine-multiple-alarms/) +can be created from existing Alarm resources. + +```ts +const alarmRule = new OrAlarmRule( + new OrAlarmRule( + new AndAlarmRule( + alarm1.toAlarmRule(AlarmState.ALARM), + alarm2.toAlarmRule(AlarmState.OK), + alarm3.toAlarmRule(AlarmState.ALARM), + ), + new NotAlarmRule(alarm4.toAlarmRule(AlarmState.INSUFFICIENT_DATA)), + ), + new BooleanAlarmRule(false), +); + +new CompositeAlarm(this, 'MyAwesomeCompositeAlarm', { + alarmRule, +}); +``` + ### A note on units In CloudWatch, Metrics datums are emitted with units, such as `seconds` or diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 4145922fcfdf7..9f36dabf1c3ee 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -104,10 +104,100 @@ export enum TreatMissingData { MISSING = 'missing' } +/** + * Enumeration indicates state of Alarm used in building Alarm Rule. + */ +export enum AlarmState { + + /** + * State indicates resource is in ALARM + */ + ALARM = 'ALARM', + + /** + * State indicates resource is not in ALARM + */ + OK = 'OK', + + /** + * State indicates there is not enough data to determine is resource is in ALARM + */ + INSUFFICIENT_DATA = 'INSUFFICIENT_DATA', + +} + +/** + * Interface for Alarm Rule. + */ +export interface IAlarmRule { + + /** + * serialized representation of Alarm Rule to be used when building the Composite Alarm resource. + */ + toAlarmRule(): string; + +} + +/** + * The base class for Alarm and CompositeAlarm resources. + */ +export abstract class AlarmBase extends Resource implements IAlarm { + + /** + * @attribute + */ + public abstract readonly alarmArn: string; + public abstract readonly alarmName: string; + + protected alarmActionArns?: string[]; + protected insufficientDataActionArns?: string[]; + protected okActionArns?: string[]; + + /** + * Trigger this action if the alarm fires + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addAlarmAction(...actions: IAlarmAction[]) { + if (this.alarmActionArns === undefined) { + this.alarmActionArns = []; + } + + this.alarmActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + + /** + * Trigger this action if there is insufficient data to evaluate the alarm + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addInsufficientDataAction(...actions: IAlarmAction[]) { + if (this.insufficientDataActionArns === undefined) { + this.insufficientDataActionArns = []; + } + + this.insufficientDataActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + + /** + * Trigger this action if the alarm returns from breaching state into ok state + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addOkAction(...actions: IAlarmAction[]) { + if (this.okActionArns === undefined) { + this.okActionArns = []; + } + + this.okActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + +} + /** * An alarm on a CloudWatch metric */ -export class Alarm extends Resource implements IAlarm { +export class Alarm extends AlarmBase { /** * Import an existing CloudWatch alarm provided an ARN @@ -143,10 +233,6 @@ export class Alarm extends Resource implements IAlarm { */ public readonly metric: IMetric; - private alarmActionArns?: string[]; - private insufficientDataActionArns?: string[]; - private okActionArns?: string[]; - /** * This metric as an annotation */ @@ -214,45 +300,6 @@ export class Alarm extends Resource implements IAlarm { }; } - /** - * Trigger this action if the alarm fires - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addAlarmAction(...actions: IAlarmAction[]) { - if (this.alarmActionArns === undefined) { - this.alarmActionArns = []; - } - - this.alarmActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - - /** - * Trigger this action if there is insufficient data to evaluate the alarm - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addInsufficientDataAction(...actions: IAlarmAction[]) { - if (this.insufficientDataActionArns === undefined) { - this.insufficientDataActionArns = []; - } - - this.insufficientDataActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - - /** - * Trigger this action if the alarm returns from breaching state into ok state - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addOkAction(...actions: IAlarmAction[]) { - if (this.okActionArns === undefined) { - this.okActionArns = []; - } - - this.okActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - /** * Turn this alarm into a horizontal annotation * @@ -273,6 +320,28 @@ export class Alarm extends Resource implements IAlarm { return this.annotation; } + /** + * Build AlarmRule from Alarm with given State (this is for use in composite alarm rule expressions). + * + * @param alarmState AlarmState to be used in Alarm Rule. + */ + public toAlarmRule(alarmState: AlarmState): IAlarmRule { + class AlarmRule implements IAlarmRule { + private readonly alarm: IAlarm; + private readonly state: AlarmState; + + constructor(alarm: IAlarm, state: AlarmState) { + this.alarm = alarm; + this.state = state; + } + + public toAlarmRule(): string { + return `${this.state}(${this.alarm.alarmArn})`; + } + } + return new AlarmRule(this, alarmState); + } + private renderMetric(metric: IMetric) { const self = this; return dispatchMetric(metric, { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts new file mode 100644 index 0000000000000..37a0247c7575a --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts @@ -0,0 +1,208 @@ +import { Construct, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { AlarmBase, IAlarm, IAlarmRule } from './alarm'; +import { CfnCompositeAlarm } from './cloudwatch.generated'; + +/** + * Enumeration of supported Composite Alarms operators. + */ +enum Operator { + + AND = 'AND', + OR = 'OR', + NOT = 'NOT', + +} + +/** + * AlarmRule to express TRUE/FALSE intent in Rule Expression. + */ +export class BooleanAlarmRule implements IAlarmRule { + + private readonly booleanValue: boolean; + + constructor(booleanValue: boolean) { + this.booleanValue = booleanValue; + } + + public toAlarmRule(): string { + return `${String(this.booleanValue).toUpperCase()}`; + } + +} + +abstract class BinaryAlarmRule implements IAlarmRule { + + private readonly operands: IAlarmRule[]; + private readonly operator: Operator; + + constructor(operator: Operator, ...operands: IAlarmRule[]) { + this.operands = operands; + this.operator = operator; + } + + public toAlarmRule(): string { + return this.operands + .map(operand => `(${operand.toAlarmRule()})`) + .join(` ${this.operator} `); + } + +} + +/** + * AlarmRule to join all provided AlarmRules with AND operator. + */ +export class AndAlarmRule extends BinaryAlarmRule { + + constructor(...operands: IAlarmRule[]) { + super(Operator.AND, ...operands); + } +} + +/** + * AlarmRule to join all provided AlarmRules with OR operator. + */ +export class OrAlarmRule extends BinaryAlarmRule { + + constructor(...operands: IAlarmRule[]) { + super(Operator.OR, ...operands); + } +} + +/** + * AlarmRule to wrap provided AlarmRule in NOT operator. + */ +export class NotAlarmRule implements IAlarmRule { + + private readonly operand: IAlarmRule; + + constructor(operand: IAlarmRule) { + this.operand = operand; + } + + public toAlarmRule(): string { + return `(NOT ${this.operand.toAlarmRule()})`; + } + +} + +/** + * Properties for creating a Composite Alarm + */ +export interface CompositeAlarmProps { + + /** + * Whether the actions for this alarm are enabled + * + * @default true + */ + readonly actionsEnabled?: boolean; + + /** + * Description for the alarm + * + * @default No description + */ + readonly alarmDescription?: string; + + /** + * Name of the alarm + * + * @default Automatically generated name + */ + readonly compositeAlarmName?: string; + + /** + * Expression that specifies which other alarms are to be evaluated to determine this composite alarm's state. + */ + readonly alarmRule: IAlarmRule; + +} + +/** + * A Composite Alarm based on Alarm Rule. + */ +export class CompositeAlarm extends AlarmBase { + + /** + * Import an existing CloudWatch composite alarm provided an Name. + * + * @param scope The parent creating construct (usually `this`) + * @param id The construct's name + * @param compositeAlarmName Composite Alarm Name + */ + public static fromCompositeAlarmName(scope: Construct, id: string, compositeAlarmName: string): IAlarm { + const stack = Stack.of(scope); + + return this.fromCompositeAlarmArn(scope, id, stack.formatArn({ + service: 'cloudwatch', + resource: 'alarm', + resourceName: compositeAlarmName, + })); + } + + /** + * Import an existing CloudWatch composite alarm provided an ARN. + * + * @param scope The parent creating construct (usually `this`) + * @param id The construct's name + * @param compositeAlarmArn Composite Alarm ARN (i.e. arn:aws:cloudwatch:::alarm/CompositeAlarmName) + */ + public static fromCompositeAlarmArn(scope: Construct, id: string, compositeAlarmArn: string): IAlarm { + class Import extends Resource implements IAlarm { + public readonly alarmArn = compositeAlarmArn; + public readonly alarmName = Stack.of(scope).parseArn(compositeAlarmArn).resourceName!; + } + return new Import(scope, id); + } + + /** + * ARN of this alarm + * + * @attribute + */ + public readonly alarmArn: string; + + /** + * Name of this alarm. + * + * @attribute + */ + public readonly alarmName: string; + + constructor(scope: Construct, id: string, props: CompositeAlarmProps) { + super(scope, id, { + physicalName: props.compositeAlarmName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), + }); + + if (props.alarmRule.toAlarmRule().length > 10240) { + throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule'); + } + + const alarm = new CfnCompositeAlarm(this, 'Resource', { + alarmName: this.physicalName, + alarmRule: props.alarmRule.toAlarmRule(), + alarmDescription: props.alarmDescription, + actionsEnabled: props.actionsEnabled, + alarmActions: Lazy.listValue({ produce: () => this.alarmActionArns }), + insufficientDataActions: Lazy.listValue({ produce: (() => this.insufficientDataActionArns) }), + okActions: Lazy.listValue({ produce: () => this.okActionArns }), + }); + + this.alarmName = this.getResourceNameAttribute(alarm.ref); + this.alarmArn = this.getResourceArnAttribute(alarm.attrArn, { + service: 'cloudwatch', + resource: 'alarm', + resourceName: this.physicalName, + }); + + } + + private generateUniqueId(): string { + const name = this.node.uniqueId; + if (name.length > 240) { + return name.substring(0, 120) + name.substring(name.length - 120); + } + return name; + } + +} diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts index 8fd53065f689d..b18e95edbcfcd 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts @@ -1,5 +1,6 @@ export * from './alarm'; export * from './alarm-action'; +export * from './composite-alarm'; export * from './dashboard'; export * from './graph'; export * from './layout'; diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 2f4d2e1fe139d..7d9738d6f98b3 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -99,7 +99,8 @@ "props-default-doc:@aws-cdk/aws-cloudwatch.MetricRenderingProperties.color", "props-default-doc:@aws-cdk/aws-cloudwatch.MetricRenderingProperties.label", "props-default-doc:@aws-cdk/aws-cloudwatch.MetricRenderingProperties.stat", - "duration-prop-type:@aws-cdk/aws-cloudwatch.MetricExpressionConfig.period" + "duration-prop-type:@aws-cdk/aws-cloudwatch.MetricExpressionConfig.period", + "resource-attribute:@aws-cdk/aws-cloudwatch.CompositeAlarm.compositeAlarmArn" ] }, "engines": { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json new file mode 100644 index 0000000000000..0088afbb56f8a --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json @@ -0,0 +1,82 @@ +{ + "Resources": { + "Alarm1F9009D71": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 100 + } + }, + "Alarm2A7122E13": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 1000 + } + }, + "Alarm32341D8D9": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 10000 + } + }, + "Alarm4671832C8": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 100000 + } + }, + "CompositeAlarmF4C3D082": { + "Type": "AWS::CloudWatch::CompositeAlarm", + "Properties": { + "AlarmName": "CompositeAlarmIntegrationTestCompositeAlarm742D2FBA", + "AlarmRule": { + "Fn::Join": [ + "", + [ + "(((ALARM(", + { + "Fn::GetAtt": [ "Alarm1F9009D71", "Arn" ] + }, + ")) OR (OK(", + { + "Fn::GetAtt": [ "Alarm2A7122E13", "Arn" ] + }, + ")) OR (ALARM(", + { + "Fn::GetAtt": [ "Alarm32341D8D9", "Arn" ] + }, + "))) AND ((NOT INSUFFICIENT_DATA(", + { + "Fn::GetAtt":[ "Alarm4671832C8", "Arn" ] + }, + ")))) OR (FALSE)" + ] + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts new file mode 100644 index 0000000000000..90f3e6318337b --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts @@ -0,0 +1,61 @@ +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Alarm, AlarmState, AndAlarmRule, BooleanAlarmRule, CompositeAlarm, Metric, NotAlarmRule, OrAlarmRule } from '../lib'; + +class CompositeAlarmIntegrationTest extends Stack { + + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + }); + + const alarm1 = new Alarm(this, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + const alarm2 = new Alarm(this, 'Alarm2', { + metric: testMetric, + threshold: 1000, + evaluationPeriods: 3, + }); + + const alarm3 = new Alarm(this, 'Alarm3', { + metric: testMetric, + threshold: 10000, + evaluationPeriods: 3, + }); + + const alarm4 = new Alarm(this, 'Alarm4', { + metric: testMetric, + threshold: 100000, + evaluationPeriods: 3, + }); + + const alarmRule = new OrAlarmRule( + new AndAlarmRule( + new OrAlarmRule( + alarm1.toAlarmRule(AlarmState.ALARM), + alarm2.toAlarmRule(AlarmState.OK), + alarm3.toAlarmRule(AlarmState.ALARM), + ), + new NotAlarmRule(alarm4.toAlarmRule(AlarmState.INSUFFICIENT_DATA)), + ), + new BooleanAlarmRule(false), + ); + + new CompositeAlarm(this, 'CompositeAlarm', { + alarmRule, + }); + } + +} + +const app = new App(); + +new CompositeAlarmIntegrationTest(app, 'CompositeAlarmIntegrationTest'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts new file mode 100644 index 0000000000000..3d37d0b0ffc66 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts @@ -0,0 +1,98 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { Alarm, AlarmState, AndAlarmRule, BooleanAlarmRule, CompositeAlarm, Metric, NotAlarmRule, OrAlarmRule } from '../lib'; + +export = { + 'test alarm rule expression builder'(test: Test) { + const stack = new Stack(); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + }); + + const alarm1 = new Alarm(stack, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + const alarm2 = new Alarm(stack, 'Alarm2', { + metric: testMetric, + threshold: 1000, + evaluationPeriods: 3, + }); + + const alarm3 = new Alarm(stack, 'Alarm3', { + metric: testMetric, + threshold: 10000, + evaluationPeriods: 3, + }); + + const alarm4 = new Alarm(stack, 'Alarm4', { + metric: testMetric, + threshold: 100000, + evaluationPeriods: 3, + }); + + const alarmRule = new OrAlarmRule( + new AndAlarmRule( + new OrAlarmRule( + alarm1.toAlarmRule(AlarmState.ALARM), + alarm2.toAlarmRule(AlarmState.OK), + alarm3.toAlarmRule(AlarmState.ALARM), + ), + new NotAlarmRule(alarm4.toAlarmRule(AlarmState.INSUFFICIENT_DATA)), + ), + new BooleanAlarmRule(false), + ); + + new CompositeAlarm(stack, 'CompositeAlarm', { + alarmRule, + }); + + expect(stack).to(haveResource('AWS::CloudWatch::CompositeAlarm', { + AlarmName: 'CompositeAlarm', + AlarmRule: { + 'Fn::Join': [ + '', + [ + '(((ALARM(', + { + 'Fn::GetAtt': [ + 'Alarm1F9009D71', + 'Arn', + ], + }, + ')) OR (OK(', + { + 'Fn::GetAtt': [ + 'Alarm2A7122E13', + 'Arn', + ], + }, + ')) OR (ALARM(', + { + 'Fn::GetAtt': [ + 'Alarm32341D8D9', + 'Arn', + ], + }, + '))) AND ((NOT INSUFFICIENT_DATA(', + { + 'Fn::GetAtt': [ + 'Alarm4671832C8', + 'Arn', + ], + }, + ')))) OR (FALSE)', + ], + ], + }, + })); + + test.done(); + }, + +}; From 471ccd53df8678ab1f0fe4f812305bc00aa417f2 Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Thu, 11 Jun 2020 21:13:10 +0530 Subject: [PATCH 2/5] feat(aws-cloudwatch): CompositeAlarm --- packages/@aws-cdk/aws-cloudwatch/README.md | 16 +- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 65 ++++---- .../aws-cloudwatch/lib/composite-alarm.ts | 143 +++++++++++------- .../test/integ.composite-alarm.ts | 18 +-- .../test/test.composite-alarm.ts | 18 +-- 5 files changed, 146 insertions(+), 114 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index 41ede4625eab8..07e35a30db8fd 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -180,16 +180,16 @@ alarm.addAlarmAction(new cw_actions.SnsAction(topic)); can be created from existing Alarm resources. ```ts -const alarmRule = new OrAlarmRule( - new OrAlarmRule( - new AndAlarmRule( - alarm1.toAlarmRule(AlarmState.ALARM), - alarm2.toAlarmRule(AlarmState.OK), - alarm3.toAlarmRule(AlarmState.ALARM), +const alarmRule = AlarmRule.anyOf( + AlarmRule.allOf( + AlarmRule.anyOf( + AlarmRule.fromAlarm(alarm1, AlarmState.ALARM), + AlarmRule.fromAlarm(alarm2, AlarmState.OK), + AlarmRule.fromAlarm(alarm3, AlarmState.ALARM), ), - new NotAlarmRule(alarm4.toAlarmRule(AlarmState.INSUFFICIENT_DATA)), + AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), ), - new BooleanAlarmRule(false), + AlarmRule.fromBoolean(false), ); new CompositeAlarm(this, 'MyAwesomeCompositeAlarm', { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 9f36dabf1c3ee..a9ecf05bcc634 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -9,10 +9,22 @@ import { dropUndefined } from './private/object'; import { MetricSet } from './private/rendering'; import { parseStatistic } from './private/statistic'; +/** + * Interface for Alarm Rule. + */ +export interface IAlarmRule { + + /** + * serialized representation of Alarm Rule to be used when building the Composite Alarm resource. + */ + toAlarmRule(): string; + +} + /** * Represents a CloudWatch Alarm */ -export interface IAlarm extends IResource { +export interface IAlarm extends IAlarmRule, IResource { /** * Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) * @@ -39,6 +51,14 @@ export interface AlarmProps extends CreateAlarmOptions { * custom Metric objects by instantiating one. */ readonly metric: IMetric; + + /** + * AlarmState to build composite alarm expressions. + * + * @default - ALARM + */ + readonly alarmState?: AlarmState; + } /** @@ -126,18 +146,6 @@ export enum AlarmState { } -/** - * Interface for Alarm Rule. - */ -export interface IAlarmRule { - - /** - * serialized representation of Alarm Rule to be used when building the Composite Alarm resource. - */ - toAlarmRule(): string; - -} - /** * The base class for Alarm and CompositeAlarm resources. */ @@ -153,6 +161,8 @@ export abstract class AlarmBase extends Resource implements IAlarm { protected insufficientDataActionArns?: string[]; protected okActionArns?: string[]; + public abstract toAlarmRule(): string; + /** * Trigger this action if the alarm fires * @@ -210,6 +220,10 @@ export class Alarm extends AlarmBase { class Import extends Resource implements IAlarm { public readonly alarmArn = alarmArn; public readonly alarmName = Stack.of(scope).parseArn(alarmArn, ':').resourceName!; + + public toAlarmRule(): string { + throw new Error('Method not implemented.'); + } } return new Import(scope, id); } @@ -238,6 +252,8 @@ export class Alarm extends AlarmBase { */ private readonly annotation: HorizontalAnnotation; + private readonly alarmState: AlarmState; + constructor(scope: Construct, id: string, props: AlarmProps) { super(scope, id, { physicalName: props.alarmName, @@ -298,6 +314,7 @@ export class Alarm extends AlarmBase { label: `${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${datapoints} datapoints within ${describePeriod(props.evaluationPeriods * metricPeriod(props.metric).toSeconds())}`, value: props.threshold, }; + this.alarmState = props.alarmState || AlarmState.ALARM; } /** @@ -320,26 +337,8 @@ export class Alarm extends AlarmBase { return this.annotation; } - /** - * Build AlarmRule from Alarm with given State (this is for use in composite alarm rule expressions). - * - * @param alarmState AlarmState to be used in Alarm Rule. - */ - public toAlarmRule(alarmState: AlarmState): IAlarmRule { - class AlarmRule implements IAlarmRule { - private readonly alarm: IAlarm; - private readonly state: AlarmState; - - constructor(alarm: IAlarm, state: AlarmState) { - this.alarm = alarm; - this.state = state; - } - - public toAlarmRule(): string { - return `${this.state}(${this.alarm.alarmArn})`; - } - } - return new AlarmRule(this, alarmState); + public toAlarmRule(): string { + return `${this.alarmState}(${this.alarmArn})`; } private renderMetric(metric: IMetric) { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts index 37a0247c7575a..92a840b072473 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts @@ -1,5 +1,5 @@ import { Construct, Lazy, Resource, Stack } from '@aws-cdk/core'; -import { AlarmBase, IAlarm, IAlarmRule } from './alarm'; +import { AlarmBase, AlarmState, IAlarm, IAlarmRule } from './alarm'; import { CfnCompositeAlarm } from './cloudwatch.generated'; /** @@ -14,73 +14,94 @@ enum Operator { } /** - * AlarmRule to express TRUE/FALSE intent in Rule Expression. + * Class with static functions to build AlarmRule for Composite Alarms. */ -export class BooleanAlarmRule implements IAlarmRule { +export class AlarmRule { - private readonly booleanValue: boolean; - - constructor(booleanValue: boolean) { - this.booleanValue = booleanValue; - } - - public toAlarmRule(): string { - return `${String(this.booleanValue).toUpperCase()}`; + /** + * function to join all provided AlarmRules with AND operator. + * + * @param operands IAlarmRules to be joined with AND operator. + */ + public static allOf(...operands: IAlarmRule[]): IAlarmRule { + return this.concat(Operator.AND, ...operands); } -} - -abstract class BinaryAlarmRule implements IAlarmRule { - - private readonly operands: IAlarmRule[]; - private readonly operator: Operator; - - constructor(operator: Operator, ...operands: IAlarmRule[]) { - this.operands = operands; - this.operator = operator; + /** + * function to join all provided AlarmRules with OR operator. + * + * @param operands IAlarmRules to be joined with OR operator. + */ + public static anyOf(...operands: IAlarmRule[]): IAlarmRule { + return this.concat(Operator.OR, ...operands); } - public toAlarmRule(): string { - return this.operands - .map(operand => `(${operand.toAlarmRule()})`) - .join(` ${this.operator} `); + /** + * function to wrap provided AlarmRule in NOT operator. + * + * @param operand IAlarmRule to be wrapped in NOT operator. + */ + public static not(operand: IAlarmRule): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public toAlarmRule(): string { + return `(NOT ${operand.toAlarmRule()})`; + } + }; } -} - -/** - * AlarmRule to join all provided AlarmRules with AND operator. - */ -export class AndAlarmRule extends BinaryAlarmRule { - - constructor(...operands: IAlarmRule[]) { - super(Operator.AND, ...operands); + /** + * function to build TRUE/FALSE intent for Rule Expression. + * + * @param value boolean value to be used in rule expression. + */ + public static fromBoolean(value: boolean): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public toAlarmRule(): string { + return `${String(value).toUpperCase()}`; + } + }; } -} - -/** - * AlarmRule to join all provided AlarmRules with OR operator. - */ -export class OrAlarmRule extends BinaryAlarmRule { - constructor(...operands: IAlarmRule[]) { - super(Operator.OR, ...operands); + /** + * function to build Rule Expression for given IAlarm and AlarmState. + * + * @param alarm IAlarm to be used in Rule Expression. + * @param alarmState AlarmState to be used in Rule Expression. + */ + public static fromAlarm(alarm: IAlarm, alarmState: AlarmState): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public toAlarmRule(): string { + return `${alarmState}(${alarm.alarmArn})`; + } + }; } -} - -/** - * AlarmRule to wrap provided AlarmRule in NOT operator. - */ -export class NotAlarmRule implements IAlarmRule { - - private readonly operand: IAlarmRule; - constructor(operand: IAlarmRule) { - this.operand = operand; + /** + * function to build Rule Expression for given Alarm Rule string. + * + * @param alarmRule string to be used in Rule Expression. + */ + public static fromString(alarmRule: string): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public toAlarmRule(): string { + return alarmRule; + } + }; } - public toAlarmRule(): string { - return `(NOT ${this.operand.toAlarmRule()})`; + private static concat(operator: Operator, ...operands: IAlarmRule[]): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public toAlarmRule(): string { + return operands + .map(operand => `(${operand.toAlarmRule()})`) + .join(` ${operator} `); + } + }; } } @@ -151,6 +172,10 @@ export class CompositeAlarm extends AlarmBase { class Import extends Resource implements IAlarm { public readonly alarmArn = compositeAlarmArn; public readonly alarmName = Stack.of(scope).parseArn(compositeAlarmArn).resourceName!; + + public toAlarmRule(): string { + throw new Error('Method not implemented.'); + } } return new Import(scope, id); } @@ -169,6 +194,8 @@ export class CompositeAlarm extends AlarmBase { */ public readonly alarmName: string; + private readonly alarmRule: string; + constructor(scope: Construct, id: string, props: CompositeAlarmProps) { super(scope, id, { physicalName: props.compositeAlarmName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), @@ -178,9 +205,11 @@ 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'); } + this.alarmRule = props.alarmRule.toAlarmRule(); + const alarm = new CfnCompositeAlarm(this, 'Resource', { alarmName: this.physicalName, - alarmRule: props.alarmRule.toAlarmRule(), + alarmRule: this.alarmRule, alarmDescription: props.alarmDescription, actionsEnabled: props.actionsEnabled, alarmActions: Lazy.listValue({ produce: () => this.alarmActionArns }), @@ -197,6 +226,10 @@ export class CompositeAlarm extends AlarmBase { } + public toAlarmRule(): string { + return this.alarmRule; + } + private generateUniqueId(): string { const name = this.node.uniqueId; if (name.length > 240) { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts index 90f3e6318337b..9354625e19154 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts @@ -1,5 +1,5 @@ import { App, Stack, StackProps } from '@aws-cdk/core'; -import { Alarm, AlarmState, AndAlarmRule, BooleanAlarmRule, CompositeAlarm, Metric, NotAlarmRule, OrAlarmRule } from '../lib'; +import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; class CompositeAlarmIntegrationTest extends Stack { @@ -35,16 +35,16 @@ class CompositeAlarmIntegrationTest extends Stack { evaluationPeriods: 3, }); - const alarmRule = new OrAlarmRule( - new AndAlarmRule( - new OrAlarmRule( - alarm1.toAlarmRule(AlarmState.ALARM), - alarm2.toAlarmRule(AlarmState.OK), - alarm3.toAlarmRule(AlarmState.ALARM), + const alarmRule = AlarmRule.anyOf( + AlarmRule.allOf( + AlarmRule.anyOf( + AlarmRule.fromAlarm(alarm1, AlarmState.ALARM), + AlarmRule.fromAlarm(alarm2, AlarmState.OK), + AlarmRule.fromAlarm(alarm3, AlarmState.ALARM), ), - new NotAlarmRule(alarm4.toAlarmRule(AlarmState.INSUFFICIENT_DATA)), + AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), ), - new BooleanAlarmRule(false), + AlarmRule.fromBoolean(false), ); new CompositeAlarm(this, 'CompositeAlarm', { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts index 3d37d0b0ffc66..1280165ab778e 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { Alarm, AlarmState, AndAlarmRule, BooleanAlarmRule, CompositeAlarm, Metric, NotAlarmRule, OrAlarmRule } from '../lib'; +import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; export = { 'test alarm rule expression builder'(test: Test) { @@ -36,16 +36,16 @@ export = { evaluationPeriods: 3, }); - const alarmRule = new OrAlarmRule( - new AndAlarmRule( - new OrAlarmRule( - alarm1.toAlarmRule(AlarmState.ALARM), - alarm2.toAlarmRule(AlarmState.OK), - alarm3.toAlarmRule(AlarmState.ALARM), + const alarmRule = AlarmRule.anyOf( + AlarmRule.allOf( + AlarmRule.anyOf( + AlarmRule.fromAlarm(alarm1, AlarmState.ALARM), + AlarmRule.fromAlarm(alarm2, AlarmState.OK), + AlarmRule.fromAlarm(alarm3, AlarmState.ALARM), ), - new NotAlarmRule(alarm4.toAlarmRule(AlarmState.INSUFFICIENT_DATA)), + AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), ), - new BooleanAlarmRule(false), + AlarmRule.fromBoolean(false), ); new CompositeAlarm(stack, 'CompositeAlarm', { From 3ed0c0e338477d04653be44048709c53d390f158 Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Fri, 19 Jun 2020 17:10:53 +0530 Subject: [PATCH 3/5] feat(aws-cloudwatch): CompositeAlarm --- packages/@aws-cdk/aws-cloudwatch/README.md | 4 +- .../aws-cloudwatch/lib/alarm-action.ts | 2 +- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 136 +----------------- .../aws-cloudwatch/lib/composite-alarm.ts | 122 +--------------- packages/@aws-cdk/aws-cloudwatch/lib/graph.ts | 2 +- packages/@aws-cdk/aws-cloudwatch/lib/index.ts | 2 + .../test/integ.composite-alarm.expected.json | 8 +- .../test/integ.composite-alarm.ts | 4 +- .../aws-cloudwatch/test/test.alarm.ts | 3 +- .../test/test.composite-alarm.ts | 12 +- 10 files changed, 28 insertions(+), 267 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index 07e35a30db8fd..d5299034d72f1 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -183,9 +183,9 @@ can be created from existing Alarm resources. const alarmRule = AlarmRule.anyOf( AlarmRule.allOf( AlarmRule.anyOf( - AlarmRule.fromAlarm(alarm1, AlarmState.ALARM), + alarm1, AlarmRule.fromAlarm(alarm2, AlarmState.OK), - AlarmRule.fromAlarm(alarm3, AlarmState.ALARM), + alarm3, ), AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), ), diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts index 51714fc9ff760..7b48d0f055873 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts @@ -1,5 +1,5 @@ import { Construct } from '@aws-cdk/core'; -import { IAlarm } from './alarm'; +import { IAlarm } from './alarm-base'; /** * Interface for objects that can be the targets of CloudWatch alarm actions diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index a9ecf05bcc634..a422b99daa0db 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -1,5 +1,5 @@ -import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; -import { IAlarmAction } from './alarm-action'; +import { Construct, Lazy, Stack, Token } from '@aws-cdk/core'; +import { AlarmBase, IAlarm } from './alarm-base'; import { CfnAlarm, CfnAlarmProps } from './cloudwatch.generated'; import { HorizontalAnnotation } from './graph'; import { CreateAlarmOptions } from './metric'; @@ -9,37 +9,6 @@ import { dropUndefined } from './private/object'; import { MetricSet } from './private/rendering'; import { parseStatistic } from './private/statistic'; -/** - * Interface for Alarm Rule. - */ -export interface IAlarmRule { - - /** - * serialized representation of Alarm Rule to be used when building the Composite Alarm resource. - */ - toAlarmRule(): string; - -} - -/** - * Represents a CloudWatch Alarm - */ -export interface IAlarm extends IAlarmRule, IResource { - /** - * Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) - * - * @attribute - */ - readonly alarmArn: string; - - /** - * Name of the alarm - * - * @attribute - */ - readonly alarmName: string; -} - /** * Properties for Alarms */ @@ -51,14 +20,6 @@ export interface AlarmProps extends CreateAlarmOptions { * custom Metric objects by instantiating one. */ readonly metric: IMetric; - - /** - * AlarmState to build composite alarm expressions. - * - * @default - ALARM - */ - readonly alarmState?: AlarmState; - } /** @@ -124,86 +85,6 @@ export enum TreatMissingData { MISSING = 'missing' } -/** - * Enumeration indicates state of Alarm used in building Alarm Rule. - */ -export enum AlarmState { - - /** - * State indicates resource is in ALARM - */ - ALARM = 'ALARM', - - /** - * State indicates resource is not in ALARM - */ - OK = 'OK', - - /** - * State indicates there is not enough data to determine is resource is in ALARM - */ - INSUFFICIENT_DATA = 'INSUFFICIENT_DATA', - -} - -/** - * The base class for Alarm and CompositeAlarm resources. - */ -export abstract class AlarmBase extends Resource implements IAlarm { - - /** - * @attribute - */ - public abstract readonly alarmArn: string; - public abstract readonly alarmName: string; - - protected alarmActionArns?: string[]; - protected insufficientDataActionArns?: string[]; - protected okActionArns?: string[]; - - public abstract toAlarmRule(): string; - - /** - * Trigger this action if the alarm fires - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addAlarmAction(...actions: IAlarmAction[]) { - if (this.alarmActionArns === undefined) { - this.alarmActionArns = []; - } - - this.alarmActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - - /** - * Trigger this action if there is insufficient data to evaluate the alarm - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addInsufficientDataAction(...actions: IAlarmAction[]) { - if (this.insufficientDataActionArns === undefined) { - this.insufficientDataActionArns = []; - } - - this.insufficientDataActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - - /** - * Trigger this action if the alarm returns from breaching state into ok state - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addOkAction(...actions: IAlarmAction[]) { - if (this.okActionArns === undefined) { - this.okActionArns = []; - } - - this.okActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - -} - /** * An alarm on a CloudWatch metric */ @@ -217,13 +98,9 @@ export class Alarm extends AlarmBase { * @param alarmArn Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) */ public static fromAlarmArn(scope: Construct, id: string, alarmArn: string): IAlarm { - class Import extends Resource implements IAlarm { + class Import extends AlarmBase implements IAlarm { public readonly alarmArn = alarmArn; public readonly alarmName = Stack.of(scope).parseArn(alarmArn, ':').resourceName!; - - public toAlarmRule(): string { - throw new Error('Method not implemented.'); - } } return new Import(scope, id); } @@ -252,8 +129,6 @@ export class Alarm extends AlarmBase { */ private readonly annotation: HorizontalAnnotation; - private readonly alarmState: AlarmState; - constructor(scope: Construct, id: string, props: AlarmProps) { super(scope, id, { physicalName: props.alarmName, @@ -314,7 +189,6 @@ export class Alarm extends AlarmBase { label: `${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${datapoints} datapoints within ${describePeriod(props.evaluationPeriods * metricPeriod(props.metric).toSeconds())}`, value: props.threshold, }; - this.alarmState = props.alarmState || AlarmState.ALARM; } /** @@ -337,10 +211,6 @@ export class Alarm extends AlarmBase { return this.annotation; } - public toAlarmRule(): string { - return `${this.alarmState}(${this.alarmArn})`; - } - private renderMetric(metric: IMetric) { const self = this; return dispatchMetric(metric, { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts index 92a840b072473..4d20f5f3f1300 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts @@ -1,111 +1,7 @@ -import { Construct, Lazy, Resource, Stack } from '@aws-cdk/core'; -import { AlarmBase, AlarmState, IAlarm, IAlarmRule } from './alarm'; +import { Construct, Lazy, Stack } from '@aws-cdk/core'; +import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base'; import { CfnCompositeAlarm } from './cloudwatch.generated'; -/** - * Enumeration of supported Composite Alarms operators. - */ -enum Operator { - - AND = 'AND', - OR = 'OR', - NOT = 'NOT', - -} - -/** - * Class with static functions to build AlarmRule for Composite Alarms. - */ -export class AlarmRule { - - /** - * function to join all provided AlarmRules with AND operator. - * - * @param operands IAlarmRules to be joined with AND operator. - */ - public static allOf(...operands: IAlarmRule[]): IAlarmRule { - return this.concat(Operator.AND, ...operands); - } - - /** - * function to join all provided AlarmRules with OR operator. - * - * @param operands IAlarmRules to be joined with OR operator. - */ - public static anyOf(...operands: IAlarmRule[]): IAlarmRule { - return this.concat(Operator.OR, ...operands); - } - - /** - * function to wrap provided AlarmRule in NOT operator. - * - * @param operand IAlarmRule to be wrapped in NOT operator. - */ - public static not(operand: IAlarmRule): IAlarmRule { - // tslint:disable-next-line:new-parens - return new class implements IAlarmRule { - public toAlarmRule(): string { - return `(NOT ${operand.toAlarmRule()})`; - } - }; - } - - /** - * function to build TRUE/FALSE intent for Rule Expression. - * - * @param value boolean value to be used in rule expression. - */ - public static fromBoolean(value: boolean): IAlarmRule { - // tslint:disable-next-line:new-parens - return new class implements IAlarmRule { - public toAlarmRule(): string { - return `${String(value).toUpperCase()}`; - } - }; - } - - /** - * function to build Rule Expression for given IAlarm and AlarmState. - * - * @param alarm IAlarm to be used in Rule Expression. - * @param alarmState AlarmState to be used in Rule Expression. - */ - public static fromAlarm(alarm: IAlarm, alarmState: AlarmState): IAlarmRule { - // tslint:disable-next-line:new-parens - return new class implements IAlarmRule { - public toAlarmRule(): string { - return `${alarmState}(${alarm.alarmArn})`; - } - }; - } - - /** - * function to build Rule Expression for given Alarm Rule string. - * - * @param alarmRule string to be used in Rule Expression. - */ - public static fromString(alarmRule: string): IAlarmRule { - // tslint:disable-next-line:new-parens - return new class implements IAlarmRule { - public toAlarmRule(): string { - return alarmRule; - } - }; - } - - private static concat(operator: Operator, ...operands: IAlarmRule[]): IAlarmRule { - // tslint:disable-next-line:new-parens - return new class implements IAlarmRule { - public toAlarmRule(): string { - return operands - .map(operand => `(${operand.toAlarmRule()})`) - .join(` ${operator} `); - } - }; - } - -} - /** * Properties for creating a Composite Alarm */ @@ -169,13 +65,9 @@ export class CompositeAlarm extends AlarmBase { * @param compositeAlarmArn Composite Alarm ARN (i.e. arn:aws:cloudwatch:::alarm/CompositeAlarmName) */ public static fromCompositeAlarmArn(scope: Construct, id: string, compositeAlarmArn: string): IAlarm { - class Import extends Resource implements IAlarm { + class Import extends AlarmBase implements IAlarm { public readonly alarmArn = compositeAlarmArn; public readonly alarmName = Stack.of(scope).parseArn(compositeAlarmArn).resourceName!; - - public toAlarmRule(): string { - throw new Error('Method not implemented.'); - } } return new Import(scope, id); } @@ -201,11 +93,11 @@ export class CompositeAlarm extends AlarmBase { physicalName: props.compositeAlarmName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), }); - if (props.alarmRule.toAlarmRule().length > 10240) { + if (props.alarmRule.renderAlarmRule().length > 10240) { throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule'); } - this.alarmRule = props.alarmRule.toAlarmRule(); + this.alarmRule = props.alarmRule.renderAlarmRule(); const alarm = new CfnCompositeAlarm(this, 'Resource', { alarmName: this.physicalName, @@ -226,10 +118,6 @@ export class CompositeAlarm extends AlarmBase { } - public toAlarmRule(): string { - return this.alarmRule; - } - private generateUniqueId(): string { const name = this.node.uniqueId; if (name.length > 240) { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts index 0c98e288c34ca..649baecd92794 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts @@ -1,5 +1,5 @@ import * as cdk from '@aws-cdk/core'; -import { IAlarm } from './alarm'; +import { IAlarm } from './alarm-base'; import { IMetric } from './metric-types'; import { allMetricsGraphJson } from './private/rendering'; import { ConcreteWidget } from './widget'; diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts index b18e95edbcfcd..05d2ce4f254a6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts @@ -1,5 +1,7 @@ export * from './alarm'; export * from './alarm-action'; +export * from './alarm-base'; +export * from './alarm-rule'; export * from './composite-alarm'; export * from './dashboard'; export * from './graph'; diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json index 0088afbb56f8a..4febb9e015123 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json @@ -60,19 +60,19 @@ { "Fn::GetAtt": [ "Alarm1F9009D71", "Arn" ] }, - ")) OR (OK(", + ") OR OK(", { "Fn::GetAtt": [ "Alarm2A7122E13", "Arn" ] }, - ")) OR (ALARM(", + ") OR ALARM(", { "Fn::GetAtt": [ "Alarm32341D8D9", "Arn" ] }, - "))) AND ((NOT INSUFFICIENT_DATA(", + ")) AND (NOT (INSUFFICIENT_DATA(", { "Fn::GetAtt":[ "Alarm4671832C8", "Arn" ] }, - ")))) OR (FALSE)" + ")))) OR FALSE)" ] ] } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts index 9354625e19154..7e2e215a13aab 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts @@ -38,9 +38,9 @@ class CompositeAlarmIntegrationTest extends Stack { const alarmRule = AlarmRule.anyOf( AlarmRule.allOf( AlarmRule.anyOf( - AlarmRule.fromAlarm(alarm1, AlarmState.ALARM), + alarm1, AlarmRule.fromAlarm(alarm2, AlarmState.OK), - AlarmRule.fromAlarm(alarm3, AlarmState.ALARM), + alarm3, ), AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), ), diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts index c86fca7f6a22d..5c485e7360fa3 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts @@ -1,7 +1,8 @@ import { ABSENT, expect, haveResource } from '@aws-cdk/assert'; import { Construct, Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { Alarm, IAlarm, IAlarmAction, Metric } from '../lib'; +import { Alarm, IAlarmAction, Metric } from '../lib'; +import { IAlarm } from '../lib/alarm-base'; const testMetric = new Metric({ namespace: 'CDK/Test', diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts index 1280165ab778e..633d792f79401 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts @@ -39,9 +39,9 @@ export = { const alarmRule = AlarmRule.anyOf( AlarmRule.allOf( AlarmRule.anyOf( - AlarmRule.fromAlarm(alarm1, AlarmState.ALARM), + alarm1, AlarmRule.fromAlarm(alarm2, AlarmState.OK), - AlarmRule.fromAlarm(alarm3, AlarmState.ALARM), + alarm3, ), AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), ), @@ -65,28 +65,28 @@ export = { 'Arn', ], }, - ')) OR (OK(', + ') OR OK(', { 'Fn::GetAtt': [ 'Alarm2A7122E13', 'Arn', ], }, - ')) OR (ALARM(', + ') OR ALARM(', { 'Fn::GetAtt': [ 'Alarm32341D8D9', 'Arn', ], }, - '))) AND ((NOT INSUFFICIENT_DATA(', + ')) AND (NOT (INSUFFICIENT_DATA(', { 'Fn::GetAtt': [ 'Alarm4671832C8', 'Arn', ], }, - ')))) OR (FALSE)', + ')))) OR FALSE)', ], ], }, From 80cbc3b64cb8425db835922b86d648bf1059ad4a Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Fri, 19 Jun 2020 17:35:53 +0530 Subject: [PATCH 4/5] feat(aws-cloudwatch): CompositeAlarm --- .../@aws-cdk/aws-cloudwatch/lib/alarm-base.ts | 96 +++++++++++++ .../@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts | 127 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts new file mode 100644 index 0000000000000..996079718edba --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts @@ -0,0 +1,96 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { IAlarmAction } from './alarm-action'; + +/** + * Interface for Alarm Rule. + */ +export interface IAlarmRule { + + /** + * serialized representation of Alarm Rule to be used when building the Composite Alarm resource. + */ + renderAlarmRule(): string; + +} + +/** + * Represents a CloudWatch Alarm + */ +export interface IAlarm extends IAlarmRule, IResource { + /** + * Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) + * + * @attribute + */ + readonly alarmArn: string; + + /** + * Name of the alarm + * + * @attribute + */ + readonly alarmName: string; +} + +/** + * The base class for Alarm and CompositeAlarm resources. + */ +export abstract class AlarmBase extends Resource implements IAlarm { + + /** + * @attribute + */ + public abstract readonly alarmArn: string; + public abstract readonly alarmName: string; + + protected alarmActionArns?: string[]; + protected insufficientDataActionArns?: string[]; + protected okActionArns?: string[]; + + /** + * AlarmRule indicating ALARM state for Alarm. + */ + public renderAlarmRule(): string { + return `ALARM(${this.alarmArn})`; + } + + /** + * Trigger this action if the alarm fires + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addAlarmAction(...actions: IAlarmAction[]) { + if (this.alarmActionArns === undefined) { + this.alarmActionArns = []; + } + + this.alarmActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + + /** + * Trigger this action if there is insufficient data to evaluate the alarm + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addInsufficientDataAction(...actions: IAlarmAction[]) { + if (this.insufficientDataActionArns === undefined) { + this.insufficientDataActionArns = []; + } + + this.insufficientDataActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + + /** + * Trigger this action if the alarm returns from breaching state into ok state + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addOkAction(...actions: IAlarmAction[]) { + if (this.okActionArns === undefined) { + this.okActionArns = []; + } + + this.okActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + +} diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts new file mode 100644 index 0000000000000..f860bcb095d44 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts @@ -0,0 +1,127 @@ +import { IAlarm, IAlarmRule } from './alarm-base'; + +/** + * Enumeration indicates state of Alarm used in building Alarm Rule. + */ +export enum AlarmState { + + /** + * State indicates resource is in ALARM + */ + ALARM = 'ALARM', + + /** + * State indicates resource is not in ALARM + */ + OK = 'OK', + + /** + * State indicates there is not enough data to determine is resource is in ALARM + */ + INSUFFICIENT_DATA = 'INSUFFICIENT_DATA', + +} + +/** + * Enumeration of supported Composite Alarms operators. + */ +enum Operator { + + AND = 'AND', + OR = 'OR', + NOT = 'NOT', + +} + +/** + * Class with static functions to build AlarmRule for Composite Alarms. + */ +export class AlarmRule { + + /** + * function to join all provided AlarmRules with AND operator. + * + * @param operands IAlarmRules to be joined with AND operator. + */ + public static allOf(...operands: IAlarmRule[]): IAlarmRule { + return this.concat(Operator.AND, ...operands); + } + + /** + * function to join all provided AlarmRules with OR operator. + * + * @param operands IAlarmRules to be joined with OR operator. + */ + public static anyOf(...operands: IAlarmRule[]): IAlarmRule { + return this.concat(Operator.OR, ...operands); + } + + /** + * function to wrap provided AlarmRule in NOT operator. + * + * @param operand IAlarmRule to be wrapped in NOT operator. + */ + public static not(operand: IAlarmRule): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return `(NOT (${operand.renderAlarmRule()}))`; + } + }; + } + + /** + * function to build TRUE/FALSE intent for Rule Expression. + * + * @param value boolean value to be used in rule expression. + */ + public static fromBoolean(value: boolean): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return `${String(value).toUpperCase()}`; + } + }; + } + + /** + * function to build Rule Expression for given IAlarm and AlarmState. + * + * @param alarm IAlarm to be used in Rule Expression. + * @param alarmState AlarmState to be used in Rule Expression. + */ + public static fromAlarm(alarm: IAlarm, alarmState: AlarmState): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return `${alarmState}(${alarm.alarmArn})`; + } + }; + } + + /** + * function to build Rule Expression for given Alarm Rule string. + * + * @param alarmRule string to be used in Rule Expression. + */ + public static fromString(alarmRule: string): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return alarmRule; + } + }; + } + + private static concat(operator: Operator, ...operands: IAlarmRule[]): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + const expression = operands + .map(operand => `${operand.renderAlarmRule()}`) + .join(` ${operator} `); + return `(${expression})`; + } + }; + } +} From bcc425291fcad95b36825af99ce40852cf675d0f Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Fri, 19 Jun 2020 20:11:55 +0530 Subject: [PATCH 5/5] feat(aws-cloudwatch): CompositeAlarm --- packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts index 5c485e7360fa3..c86fca7f6a22d 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts @@ -1,8 +1,7 @@ import { ABSENT, expect, haveResource } from '@aws-cdk/assert'; import { Construct, Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { Alarm, IAlarmAction, Metric } from '../lib'; -import { IAlarm } from '../lib/alarm-base'; +import { Alarm, IAlarm, IAlarmAction, Metric } from '../lib'; const testMetric = new Metric({ namespace: 'CDK/Test',