Skip to content

Commit

Permalink
feat(facade): add ability to specify default actions by alarm disambi…
Browse files Browse the repository at this point in the history
…guator (#268)

Actions are resolved in the following order:

1. `actionOverride` from that alarm's props, if present
2. `disambiguatorAction` from the facade's defaults, if present (**new**)
3. Default `action` from the facade's defaults, if present
4. Finally fallbacks to a no-op action

---

_By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_
  • Loading branch information
echeung-amzn committed Nov 2, 2022
1 parent 78f588b commit 75f7b7c
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 3 deletions.
14 changes: 14 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 35 additions & 2 deletions lib/common/alarm/AlarmFactory.ts
Expand Up @@ -379,6 +379,16 @@ export interface AlarmFactoryDefaults {
*/
readonly action?: IAlarmActionStrategy;

/**
* Optional alarm action for each disambiguator.
*
* @default - Global alarm action if defined.
*/
readonly disambiguatorAction?: Record<
PredefinedAlarmDisambiguators | string,
IAlarmActionStrategy
>;

/**
* Custom strategy to create annotations for alarms.
*
Expand Down Expand Up @@ -487,8 +497,10 @@ export class AlarmFactory {
props.actionsEnabled,
props.disambiguator
);
const action =
props.actionOverride ?? this.globalAlarmDefaults.action ?? noopAction();
const action = this.determineAction(
props.disambiguator,
props.actionOverride
);
const alarmName = this.alarmNamingStrategy.getName(props);
const alarmNameSuffix = props.alarmNameSuffix;
const alarmLabel = this.alarmNamingStrategy.getWidgetLabel(props);
Expand Down Expand Up @@ -704,6 +716,27 @@ export class AlarmFactory {
return false;
}

protected determineAction(
disambiguator?: string,
actionOverride?: IAlarmActionStrategy
): IAlarmActionStrategy {
// Explicit override
if (actionOverride) {
return actionOverride;
}

// Default by disambiugator
if (
disambiguator &&
this.globalAlarmDefaults.disambiguatorAction?.[disambiguator]
) {
return this.globalAlarmDefaults.disambiguatorAction[disambiguator];
}

// Default global action
return this.globalAlarmDefaults.action ?? noopAction();
}

get shouldUseDefaultDedupeForError() {
return this.globalAlarmDefaults.useDefaultDedupeForError ?? true;
}
Expand Down
64 changes: 63 additions & 1 deletion test/common/alarm/AlarmFactory.test.ts
Expand Up @@ -7,6 +7,7 @@ import {
Shading,
TreatMissingData,
} from "aws-cdk-lib/aws-cloudwatch";
import { Topic } from "aws-cdk-lib/aws-sns";
import { Construct } from "constructs";

import {
Expand All @@ -15,11 +16,17 @@ import {
AlarmFactoryDefaults,
CompositeAlarmOperator,
MetricFactoryDefaults,
multipleActions,
noopAction,
SnsAlarmActionStrategy,
} from "../../../lib";

const stack = new Stack();
const construct = new Construct(stack, "SampleConstruct");

const snsAction = new SnsAlarmActionStrategy({
onAlarmTopic: new Topic(stack, "Dummy2"),
});
const globalMetricDefaults: MetricFactoryDefaults = {
namespace: "DummyNamespace",
};
Expand All @@ -39,6 +46,9 @@ const globalAlarmDefaultsWithDisambiguator: AlarmFactoryDefaults = {
datapointsToAlarm: 6,
// we do not care about alarm actions in this test
action: noopAction(),
disambiguatorAction: {
DisambiguatedAction: snsAction,
},
};
const factory = new AlarmFactory(construct, {
globalMetricDefaults,
Expand Down Expand Up @@ -116,6 +126,7 @@ test("addAlarm: verify actions enabled", () => {
...props,
alarmNameSuffix: "DisabledByDefault",
});

const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
ActionsEnabled: true,
Expand Down Expand Up @@ -154,6 +165,7 @@ test("addAlarm: description can be overridden", () => {
alarmNameSuffix: "Suffix6B",
alarmDescriptionOverride: "New Description",
});

expect(alarm1.alarmDescription).toEqual("Description");
expect(alarm2.alarmDescription).toEqual("New Description");
});
Expand All @@ -179,6 +191,7 @@ test("addAlarm: evaluateLowSampleCountPercentile can be overridden", () => {
evaluateLowSampleCountPercentile: true,
alarmNameSuffix: "TrueValue",
});

expect(Template.fromStack(stack)).toMatchSnapshot();
});

Expand All @@ -194,6 +207,7 @@ test("addAlarm: period override is propagated to alarm metric", () => {
alarmNameSuffix: "TwoHoursPeriod",
period: Duration.hours(2),
});

const alarm1hConfig = (alarm1h.alarm as Alarm).metric.toMetricConfig();
expect(alarm1hConfig.metricStat?.period).toStrictEqual(Duration.hours(1));
const alarm2hConfig = (alarm2h.alarm as Alarm).metric.toMetricConfig();
Expand Down Expand Up @@ -225,6 +239,7 @@ test("addAlarm: fill is propagated to alarm annotation", () => {
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
fillAlarmRange: true,
});

expect(alarmNone.annotation.fill).toBeUndefined();
expect(alarmAbove.annotation.fill).toStrictEqual(Shading.ABOVE);
expect(alarmBelow.annotation.fill).toStrictEqual(Shading.BELOW);
Expand All @@ -245,6 +260,7 @@ test("addAlarm: annotation overrides are applied", () => {
overrideAnnotationVisibility: false,
overrideAnnotationColor: "NewColor",
});

expect(alarm.annotation).toStrictEqual({
color: "NewColor",
label: "NewLabel",
Expand All @@ -266,8 +282,8 @@ test("addAlarm: check created alarms when minMetricSamplesToAlarm is used", () =
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
minMetricSamplesToAlarm: 42,
});
const template = Template.fromStack(stack);

const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
AlarmName: "DummyServiceAlarms-prefix-none",
MetricName: "DummyMetric1",
Expand All @@ -285,6 +301,7 @@ test("addAlarm: check created alarms when minMetricSamplesToAlarm is used", () =
Threshold: 42,
TreatMissingData: "breaching",
});

const alarmRuleCapture = new Capture();
template.hasResourceProperties("AWS::CloudWatch::CompositeAlarm", {
AlarmName: "DummyServiceAlarms-prefix-none-WithSamples",
Expand Down Expand Up @@ -345,5 +362,50 @@ test("addCompositeAlarm: snapshot for operator", () => {
alarmNameSuffix: "CompositeOr",
compositeOperator: CompositeAlarmOperator.OR,
});

expect(Template.fromStack(stack)).toMatchSnapshot();
});

test("addAlarm: original actionOverride with a different action gets preserved", () => {
const originalActionOverride = new SnsAlarmActionStrategy({
onAlarmTopic: new Topic(stack, "Dummy1"),
});

const alarm = factory.addAlarm(metric, {
...props,
alarmNameSuffix: "OriginalActionOverridePreserved",
actionOverride: originalActionOverride,
});

expect(alarm.action).toStrictEqual(originalActionOverride);
});

test("addAlarm: original actionOverride with multipleActions gets preserved", () => {
const action1 = snsAction;
const action2 = noopAction();

const originalActionOverride = multipleActions(action1, action2);

const alarm = factory.addAlarm(metric, {
...props,
alarmNameSuffix: "OriginalActionOverridePreservedInMultipleActions",
actionOverride: originalActionOverride,
});

expect(alarm.action).toStrictEqual(multipleActions(action1, action2));
});

test("addAlarm: disambigatorAction takes precedence over default action", () => {
const stack = new Stack();
const factory = new AlarmFactory(stack, {
globalMetricDefaults,
globalAlarmDefaults: globalAlarmDefaultsWithDisambiguator,
localAlarmNamePrefix: "prefix",
});
const alarm = factory.addAlarm(metric, {
...props,
disambiguator: "DisambiguatedAction",
});

expect(alarm.action).toStrictEqual(snsAction);
});

0 comments on commit 75f7b7c

Please sign in to comment.