Skip to content

Commit d0e50de

Browse files
New: [AEA-6520] - SnsAlarm (#708)
## Summary - ✨ New Feature ### Details Share SnsAlarm construct from PfP for use in PSU --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 727cb0a commit d0e50de

4 files changed

Lines changed: 300 additions & 8 deletions

File tree

package-lock.json

Lines changed: 25 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import {Duration} from "aws-cdk-lib"
2+
import {Construct} from "constructs"
3+
import {
4+
Alarm,
5+
ComparisonOperator,
6+
CreateAlarmOptions,
7+
Metric,
8+
MetricStatConfig,
9+
TreatMissingData,
10+
Unit
11+
} from "aws-cdk-lib/aws-cloudwatch"
12+
import {SnsAction} from "aws-cdk-lib/aws-cloudwatch-actions"
13+
import {ITopic} from "aws-cdk-lib/aws-sns"
14+
15+
/**
16+
* Alarm definition for SnsAlarm with defaults applied by the construct.
17+
*/
18+
export interface SnsAlarmDefinition
19+
extends Omit<CreateAlarmOptions, "threshold" | "evaluationPeriods"> {
20+
/**
21+
* The value against which the specified statistic is compared.
22+
*
23+
* @default 1
24+
*/
25+
readonly threshold?: number
26+
27+
/**
28+
* The number of periods over which data is compared to the specified threshold.
29+
*
30+
* @default 1
31+
*/
32+
readonly evaluationPeriods?: number
33+
}
34+
35+
/**
36+
* Metric stat configuration for SnsAlarm with defaults applied by the construct.
37+
*/
38+
export interface SnsMetricStatConfig extends Omit<MetricStatConfig, "period" | "statistic"> {
39+
/**
40+
* How many seconds to aggregate over.
41+
*
42+
* @default Duration.minutes(1)
43+
*/
44+
readonly period?: Duration
45+
46+
/**
47+
* Aggregation function to use.
48+
*
49+
* @default "Sum"
50+
*/
51+
readonly statistic?: string
52+
}
53+
54+
const toDimensionsMap = (
55+
dimensions: MetricStatConfig["dimensions"]
56+
): {[dimensionName: string]: string} | undefined => {
57+
if (!dimensions || dimensions.length === 0) {
58+
return undefined
59+
}
60+
61+
const dimensionMap: {[dimensionName: string]: string} = {}
62+
dimensions.forEach((dimension) => {
63+
dimensionMap[dimension.name] = String(dimension.value)
64+
})
65+
return dimensionMap
66+
}
67+
68+
/**
69+
* Constructs a concrete CloudWatch Metric from a MetricStatConfig.
70+
* @see {@link import("aws-cdk-lib/aws-cloudwatch").MetricConfig} for alternate concrete metric configs.
71+
*/
72+
export const metricFromStatConfig = (
73+
metricStatConfig: MetricStatConfig
74+
): Metric =>
75+
new Metric({
76+
namespace: metricStatConfig.namespace,
77+
metricName: metricStatConfig.metricName,
78+
dimensionsMap: toDimensionsMap(metricStatConfig.dimensions),
79+
statistic: metricStatConfig.statistic,
80+
period: metricStatConfig.period,
81+
unit: metricStatConfig.unitFilter,
82+
account: metricStatConfig.accountOverride ?? metricStatConfig.account,
83+
region: metricStatConfig.regionOverride ?? metricStatConfig.region
84+
})
85+
86+
/**
87+
* Configuration for creating a CloudWatch metric alarm with SNS publication construct.
88+
*/
89+
export interface SnsAlarmProps {
90+
91+
/** Prefix used in the generated CloudWatch alarm name. */
92+
readonly stackName: string
93+
/** Enables alarm actions when true, disabling notifications when false. */
94+
readonly enableAlerts: boolean
95+
/** CloudWatch metric and threshold settings for the alarm. */
96+
readonly alarmDefinition: SnsAlarmDefinition
97+
/** Defines the metric configuration to be monitored by the alarm. */
98+
readonly metricStatConfig: SnsMetricStatConfig
99+
/** SNS topic that receives alarm, OK, and insufficient data notifications. Common example is for Slack alerts. */
100+
readonly snsTopic: ITopic
101+
}
102+
103+
/**
104+
* Creates a single CloudWatch alarm and wires all alarm state changes to an SNS topic.
105+
*/
106+
export class SnsAlarm extends Construct {
107+
public readonly alarm: Alarm
108+
109+
/**
110+
* Creates a CloudWatch alarm and publishes alarm state changes to the provided SNS topic.
111+
*
112+
* @param props Alarm configuration including metric settings, threshold settings, and notification topic.
113+
* @example
114+
* new SnsAlarm(this, 'MyApiErrorAlarm', {
115+
* stackName: 'pfp-prod',
116+
* enableAlerts: true,
117+
* alarmDefinition: {
118+
* alarmDescription: 'API errors detected',
119+
* threshold: 1
120+
* },
121+
* metricStatConfig: {
122+
* namespace: 'LambdaLogFilterMetrics',
123+
* metricName: 'ErrorCount'
124+
* },
125+
* snsTopic: slackAlertTopic
126+
* })
127+
*/
128+
public constructor(scope: Construct, id: string, props: SnsAlarmProps) {
129+
super(scope, id)
130+
131+
const generatedAlarmName = props.alarmDefinition.alarmName ?? id
132+
const {
133+
threshold,
134+
evaluationPeriods,
135+
comparisonOperator,
136+
treatMissingData,
137+
...supportedAlarmDefinitionProps
138+
} = props.alarmDefinition
139+
140+
const alarm = new Alarm(this, `${generatedAlarmName}Alarm`, {
141+
...supportedAlarmDefinitionProps,
142+
alarmName: `${props.stackName}-${generatedAlarmName}`,
143+
metric: metricFromStatConfig({
144+
...props.metricStatConfig,
145+
unitFilter: props.metricStatConfig.unitFilter ?? Unit.COUNT,
146+
statistic: props.metricStatConfig.statistic ?? "Sum",
147+
period: props.metricStatConfig.period ?? Duration.minutes(1)
148+
}),
149+
threshold: threshold ?? 1,
150+
evaluationPeriods: evaluationPeriods ?? 1,
151+
comparisonOperator: comparisonOperator ?? ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
152+
treatMissingData: treatMissingData ?? TreatMissingData.NOT_BREACHING,
153+
actionsEnabled: props.enableAlerts
154+
})
155+
156+
const snsAction = new SnsAction(props.snsTopic)
157+
alarm.addAlarmAction(snsAction)
158+
alarm.addOkAction(snsAction)
159+
alarm.addInsufficientDataAction(snsAction)
160+
161+
this.alarm = alarm
162+
}
163+
}

packages/cdkConstructs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./constructs/RestApiGateway/accessLogFormat.js"
77
export * from "./constructs/RestApiGateway/LambdaEndpoint.js"
88
export * from "./constructs/RestApiGateway/StateMachineEndpoint.js"
99
export * from "./constructs/PythonLambdaFunction.js"
10+
export * from "./constructs/SnsAlarm.js"
1011
export * from "./constructs/SsmParametersConstruct.js"
1112
export * from "./apps/createApp.js"
1213
export * from "./config/index.js"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {App, Duration, Stack} from "aws-cdk-lib"
2+
import {Template} from "aws-cdk-lib/assertions"
3+
import {ComparisonOperator, Unit} from "aws-cdk-lib/aws-cloudwatch"
4+
import {Topic} from "aws-cdk-lib/aws-sns"
5+
import {describe, expect, it} from "vitest"
6+
import {SnsAlarm} from "../../src/constructs/SnsAlarm"
7+
8+
const importedSlackTopicArn = "arn:aws:sns:eu-west-2:111111111111:SlackAlertsTopic"
9+
10+
describe("SnsAlarm construct", () => {
11+
it("applies sane defaults for simple alarm definitions", () => {
12+
const app = new App()
13+
const stack = new Stack(app, "TestStack")
14+
const slackAlertTopic = Topic.fromTopicArn(stack, "SlackAlertsTopic", importedSlackTopicArn)
15+
16+
const metricAlarm = new SnsAlarm(stack, "SimpleMetricAlarm", {
17+
stackName: "pfp-test-stack",
18+
enableAlerts: true,
19+
alarmDefinition: {
20+
alarmName: "MySimpleAlarm",
21+
alarmDescription: "An alarm for any breach (threshold 1) in a single period"
22+
},
23+
metricStatConfig: {
24+
namespace: "LambdaLogFilterMetrics",
25+
metricName: "ErrorCount"
26+
},
27+
snsTopic: slackAlertTopic
28+
})
29+
30+
expect(metricAlarm.alarm).toBeDefined()
31+
32+
const template = Template.fromStack(stack)
33+
template.resourceCountIs("AWS::SNS::Topic", 0)
34+
35+
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
36+
AlarmName: "pfp-test-stack-MySimpleAlarm",
37+
Namespace: "LambdaLogFilterMetrics",
38+
MetricName: "ErrorCount",
39+
Threshold: 1,
40+
ComparisonOperator: "GreaterThanOrEqualToThreshold",
41+
Unit: "Count",
42+
Statistic: "Sum",
43+
Period: 60,
44+
EvaluationPeriods: 1,
45+
TreatMissingData: "notBreaching",
46+
AlarmDescription: "An alarm for any breach (threshold 1) in a single period",
47+
AlarmActions: [importedSlackTopicArn],
48+
OKActions: [importedSlackTopicArn],
49+
InsufficientDataActions: [importedSlackTopicArn],
50+
ActionsEnabled: true
51+
})
52+
})
53+
54+
it("allows overriding threshold, comparison operator, unit and dimensions", () => {
55+
const app = new App()
56+
const stack = new Stack(app, "OverrideStack")
57+
const slackAlertTopic = Topic.fromTopicArn(stack, "SlackAlertsTopic", importedSlackTopicArn)
58+
59+
const metricAlarm = new SnsAlarm(stack, "OverrideMetricAlarm", {
60+
stackName: "pfp-test-stack",
61+
enableAlerts: false,
62+
alarmDefinition: {
63+
alarmName: "MyOverrideAlarm",
64+
alarmDescription: "Override alarm",
65+
threshold: 250,
66+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
67+
evaluationPeriods: 3,
68+
datapointsToAlarm: 2
69+
},
70+
metricStatConfig: {
71+
namespace: "CustomNamespace",
72+
metricName: "Latency",
73+
unitFilter: Unit.MILLISECONDS,
74+
dimensions: [
75+
{
76+
name: "FunctionName",
77+
value: "my-function"
78+
}
79+
],
80+
period: Duration.minutes(1),
81+
statistic: "Sum"
82+
},
83+
snsTopic: slackAlertTopic
84+
})
85+
86+
expect(metricAlarm.alarm).toBeDefined()
87+
88+
const template = Template.fromStack(stack)
89+
90+
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
91+
AlarmName: "pfp-test-stack-MyOverrideAlarm",
92+
Namespace: "CustomNamespace",
93+
MetricName: "Latency",
94+
Threshold: 250,
95+
ComparisonOperator: "GreaterThanThreshold",
96+
DatapointsToAlarm: 2,
97+
Unit: "Milliseconds",
98+
Dimensions: [
99+
{
100+
Name: "FunctionName",
101+
Value: "my-function"
102+
}
103+
],
104+
AlarmDescription: "Override alarm",
105+
AlarmActions: [importedSlackTopicArn],
106+
OKActions: [importedSlackTopicArn],
107+
InsufficientDataActions: [importedSlackTopicArn],
108+
ActionsEnabled: false
109+
})
110+
})
111+
})

0 commit comments

Comments
 (0)