Skip to content

Commit

Permalink
feat: Add construct for EC2-based infrastructure alarm (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobwinch authored Apr 14, 2021
1 parent fb4c1fc commit f6dd30f
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 2 deletions.
232 changes: 232 additions & 0 deletions src/constructs/cloudwatch/__snapshots__/ec2-alarms.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The Gu5xxPercentageAlarm construct should create the correct alarm resource with minimal config 1`] = `
Object {
"Mappings": Object {
"stagemapping": Object {
"CODE": Object {
"alarmActionsEnabled": false,
},
"PROD": Object {
"alarmActionsEnabled": true,
},
},
},
"Parameters": Object {
"Stage": Object {
"AllowedValues": Array [
"CODE",
"PROD",
],
"Default": "CODE",
"Description": "Stage name",
"Type": "String",
},
},
"Resources": Object {
"ApplicationLoadBalancerTesting172A253B": Object {
"Properties": Object {
"LoadBalancerAttributes": Array [
Object {
"Key": "deletion_protection.enabled",
"Value": "true",
},
],
"Scheme": "internal",
"SecurityGroups": Array [
Object {
"Fn::GetAtt": Array [
"ApplicationLoadBalancerTestingSecurityGroup883A01A4",
"GroupId",
],
},
],
"Subnets": Array [
"",
],
"Tags": Array [
Object {
"Key": "App",
"Value": "testing",
},
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "Stack",
"Value": "test-stack",
},
Object {
"Key": "Stage",
"Value": Object {
"Ref": "Stage",
},
},
],
},
"Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
},
"ApplicationLoadBalancerTestingSecurityGroup883A01A4": Object {
"Properties": Object {
"GroupDescription": "Automatically created Security Group for ELB TestApplicationLoadBalancerTesting8F9EA5A8",
"SecurityGroupEgress": Array [
Object {
"CidrIp": "255.255.255.255/32",
"Description": "Disallow all traffic",
"FromPort": 252,
"IpProtocol": "icmp",
"ToPort": 86,
},
],
"Tags": Array [
Object {
"Key": "App",
"Value": "testing",
},
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "Stack",
"Value": "test-stack",
},
Object {
"Key": "Stage",
"Value": Object {
"Ref": "Stage",
},
},
],
"VpcId": "test",
},
"Type": "AWS::EC2::SecurityGroup",
},
"testAF53AC38": Object {
"Properties": Object {
"ActionsEnabled": Object {
"Fn::FindInMap": Array [
"stagemapping",
Object {
"Ref": "Stage",
},
"alarmActionsEnabled",
],
},
"AlarmActions": Array [
Object {
"Fn::Join": Array [
"",
Array [
"arn:aws:sns:",
Object {
"Ref": "AWS::Region",
},
":",
Object {
"Ref": "AWS::AccountId",
},
":test-topic",
],
],
},
],
"AlarmDescription": "testing exceeded 1% error rate",
"AlarmName": Object {
"Fn::Join": Array [
"",
Array [
"High 5XX error % from testing in ",
Object {
"Ref": "Stage",
},
],
],
},
"ComparisonOperator": "GreaterThanThreshold",
"EvaluationPeriods": 1,
"Metrics": Array [
Object {
"Expression": "100*(m1+m2)/m3",
"Id": "expr_1",
"Label": "% of 5XX responses served for testing (load balancer and instances combined)",
},
Object {
"Id": "m1",
"MetricStat": Object {
"Metric": Object {
"Dimensions": Array [
Object {
"Name": "LoadBalancer",
"Value": Object {
"Fn::GetAtt": Array [
"ApplicationLoadBalancerTesting172A253B",
"LoadBalancerFullName",
],
},
},
],
"MetricName": "HTTPCode_ELB_5XX_Count",
"Namespace": "AWS/ApplicationELB",
},
"Period": 300,
"Stat": "Sum",
},
"ReturnData": false,
},
Object {
"Id": "m2",
"MetricStat": Object {
"Metric": Object {
"Dimensions": Array [
Object {
"Name": "LoadBalancer",
"Value": Object {
"Fn::GetAtt": Array [
"ApplicationLoadBalancerTesting172A253B",
"LoadBalancerFullName",
],
},
},
],
"MetricName": "HTTPCode_Target_5XX_Count",
"Namespace": "AWS/ApplicationELB",
},
"Period": 300,
"Stat": "Sum",
},
"ReturnData": false,
},
Object {
"Id": "m3",
"MetricStat": Object {
"Metric": Object {
"Dimensions": Array [
Object {
"Name": "LoadBalancer",
"Value": Object {
"Fn::GetAtt": Array [
"ApplicationLoadBalancerTesting172A253B",
"LoadBalancerFullName",
],
},
},
],
"MetricName": "RequestCount",
"Namespace": "AWS/ApplicationELB",
},
"Period": 300,
"Stat": "Sum",
},
"ReturnData": false,
},
],
"Threshold": 1,
"TreatMissingData": "notBreaching",
},
"Type": "AWS::CloudWatch::Alarm",
},
},
}
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The GuLambdaErrorPercentageAlarm pattern should create the correct alarm resource with minimal config 1`] = `
exports[`The GuLambdaErrorPercentageAlarm construct should create the correct alarm resource with minimal config 1`] = `
Object {
"Mappings": Object {
"stagemapping": Object {
Expand Down
73 changes: 73 additions & 0 deletions src/constructs/cloudwatch/ec2-alarms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import "@aws-cdk/assert/jest";
import { SynthUtils } from "@aws-cdk/assert";
import { Vpc } from "@aws-cdk/aws-ec2";
import { Stack } from "@aws-cdk/core";
import { simpleGuStackForTesting } from "../../utils/test";
import type { AppIdentity } from "../core/identity";
import { GuApplicationLoadBalancer } from "../loadbalancing";
import { Gu5xxPercentageAlarm } from "./ec2-alarms";

const vpc = Vpc.fromVpcAttributes(new Stack(), "VPC", {
vpcId: "test",
availabilityZones: [""],
publicSubnetIds: [""],
});

const app: AppIdentity = {
app: "testing",
};

describe("The Gu5xxPercentageAlarm construct", () => {
it("should create the correct alarm resource with minimal config", () => {
const stack = simpleGuStackForTesting();
const alb = new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });
const props = {
tolerated5xxPercentage: 1,
snsTopicName: "test-topic",
};
new Gu5xxPercentageAlarm(stack, "test", { ...app, loadBalancer: alb, ...props });
expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot();
});

it("should use a custom description if one is provided", () => {
const stack = simpleGuStackForTesting();
const alb = new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });
const props = {
alarmDescription: "test-custom-alarm-description",
tolerated5xxPercentage: 1,
snsTopicName: "test-topic",
};
new Gu5xxPercentageAlarm(stack, "test", { ...app, loadBalancer: alb, ...props });
expect(stack).toHaveResource("AWS::CloudWatch::Alarm", {
AlarmDescription: "test-custom-alarm-description",
});
});

it("should use a custom alarm name if one is provided", () => {
const stack = simpleGuStackForTesting();
const alb = new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });
const props = {
alarmName: "test-custom-alarm-name",
tolerated5xxPercentage: 1,
snsTopicName: "test-topic",
};
new Gu5xxPercentageAlarm(stack, "test", { ...app, loadBalancer: alb, ...props });
expect(stack).toHaveResource("AWS::CloudWatch::Alarm", {
AlarmName: "test-custom-alarm-name",
});
});

it("should adjust the number of evaluation periods if a custom value is provided", () => {
const stack = simpleGuStackForTesting();
const alb = new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });
const props = {
tolerated5xxPercentage: 1,
numberOfFiveMinutePeriodsToEvaluate: 3,
snsTopicName: "test-topic",
};
new Gu5xxPercentageAlarm(stack, "test", { ...app, loadBalancer: alb, ...props });
expect(stack).toHaveResource("AWS::CloudWatch::Alarm", {
EvaluationPeriods: 3,
});
});
});
50 changes: 50 additions & 0 deletions src/constructs/cloudwatch/ec2-alarms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ComparisonOperator, MathExpression, TreatMissingData } from "@aws-cdk/aws-cloudwatch";
import { HttpCodeElb, HttpCodeTarget } from "@aws-cdk/aws-elasticloadbalancingv2";
import type { GuStack } from "../core";
import type { AppIdentity } from "../core/identity";
import type { GuApplicationLoadBalancer } from "../loadbalancing";
import type { GuAlarmProps } from "./alarm";
import { GuAlarm } from "./alarm";

/**
* Creates an alarm which is triggered whenever the percentage of requests with a 5xx response code exceeds
* the specified threshold.
*/
export interface Gu5xxPercentageMonitoringProps
extends Omit<GuAlarmProps, "evaluationPeriods" | "metric" | "period" | "threshold" | "treatMissingData">,
AppIdentity {
tolerated5xxPercentage: number;
numberOfFiveMinutePeriodsToEvaluate?: number;
noMonitoring?: false;
}

interface GuLoadBalancerAlarmProps extends Gu5xxPercentageMonitoringProps {
loadBalancer: GuApplicationLoadBalancer;
}

export class Gu5xxPercentageAlarm extends GuAlarm {
constructor(scope: GuStack, id: string, props: GuLoadBalancerAlarmProps) {
const mathExpression = new MathExpression({
expression: "100*(m1+m2)/m3",
usingMetrics: {
m1: props.loadBalancer.metricHttpCodeElb(HttpCodeElb.ELB_5XX_COUNT),
m2: props.loadBalancer.metricHttpCodeTarget(HttpCodeTarget.TARGET_5XX_COUNT),
m3: props.loadBalancer.metricRequestCount(),
},
label: `% of 5XX responses served for ${props.app} (load balancer and instances combined)`,
});
const defaultAlarmName = `High 5XX error % from ${props.app} in ${scope.stage}`;
const defaultDescription = `${props.app} exceeded ${props.tolerated5xxPercentage}% error rate`;
const alarmProps = {
...props,
metric: mathExpression,
treatMissingData: TreatMissingData.NOT_BREACHING,
threshold: props.tolerated5xxPercentage,
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
alarmName: props.alarmName ?? defaultAlarmName,
alarmDescription: props.alarmDescription ?? defaultDescription,
evaluationPeriods: props.numberOfFiveMinutePeriodsToEvaluate ?? 1,
};
super(scope, id, alarmProps);
}
}
1 change: 1 addition & 0 deletions src/constructs/cloudwatch/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./alarm";
export * from "./ec2-alarms";
export * from "./lambda-alarms";
export * from "./no-monitoring";
2 changes: 1 addition & 1 deletion src/constructs/cloudwatch/lambda-alarms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { simpleGuStackForTesting } from "../../utils/test";
import { GuLambdaFunction } from "../lambda";
import { GuLambdaErrorPercentageAlarm } from "./lambda-alarms";

describe("The GuLambdaErrorPercentageAlarm pattern", () => {
describe("The GuLambdaErrorPercentageAlarm construct", () => {
it("should create the correct alarm resource with minimal config", () => {
const stack = simpleGuStackForTesting();
const lambda = new GuLambdaFunction(stack, "lambda", {
Expand Down

0 comments on commit f6dd30f

Please sign in to comment.