From e0e4da04fddc7a8d84e69afa6927d19afc66b578 Mon Sep 17 00:00:00 2001 From: Kristian Rekstad Date: Fri, 10 May 2024 17:13:17 +0200 Subject: [PATCH] feat: add lambda invocation failure alarm --- .../__snapshots__/lambda-alarms.test.ts.snap | 38 ++++++++++++ src/alarms/__tests__/lambda-alarms.test.ts | 26 ++++++++ src/alarms/lambda-alarms.ts | 62 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/alarms/__tests__/__snapshots__/lambda-alarms.test.ts.snap create mode 100644 src/alarms/__tests__/lambda-alarms.test.ts create mode 100644 src/alarms/lambda-alarms.ts diff --git a/src/alarms/__tests__/__snapshots__/lambda-alarms.test.ts.snap b/src/alarms/__tests__/__snapshots__/lambda-alarms.test.ts.snap new file mode 100644 index 00000000..94a83688 --- /dev/null +++ b/src/alarms/__tests__/__snapshots__/lambda-alarms.test.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`create alarms 1`] = ` +Object { + "Resources": Object { + "LambdaAlarmsFailedInvocationAlarmF0FCB95D": Object { + "Properties": Object { + "AlarmActions": Array [ + Object { + "Fn::ImportValue": "SupportStack:ExportsOutputRefTopicBFC7AF6ECB4A357A", + }, + ], + "AlarmDescription": "Invocation for 'lambda-function-name' failed. Runbook at https://liflig.no", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": Array [ + Object { + "Name": "FunctionName", + "Value": "lambda-function-name", + }, + ], + "EvaluationPeriods": 1, + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + "OKActions": Array [ + Object { + "Fn::ImportValue": "SupportStack:ExportsOutputRefTopicBFC7AF6ECB4A357A", + }, + ], + "Period": 60, + "Statistic": "Sum", + "Threshold": 1, + "TreatMissingData": "ignore", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + }, +} +`; diff --git a/src/alarms/__tests__/lambda-alarms.test.ts b/src/alarms/__tests__/lambda-alarms.test.ts new file mode 100644 index 00000000..32aed7ec --- /dev/null +++ b/src/alarms/__tests__/lambda-alarms.test.ts @@ -0,0 +1,26 @@ +import "@aws-cdk/assert/jest" +import { App, Stack } from "aws-cdk-lib" +import * as cloudwatchActions from "aws-cdk-lib/aws-cloudwatch-actions" +import * as sns from "aws-cdk-lib/aws-sns" +import "jest-cdk-snapshot" +import { LambdaAlarms } from "../lambda-alarms" + +test("create alarms", () => { + const app = new App() + const supportStack = new Stack(app, "SupportStack") + const stack = new Stack(app, "Stack") + + const topic = new sns.Topic(supportStack, "Topic") + const action = new cloudwatchActions.SnsAction(topic) + + const alarms = new LambdaAlarms(stack, "LambdaAlarms", { + actions: [action], + lambdaFunctionName: "lambda-function-name", + }) + + alarms.addInvocationErrorAlarm({ + appendToAlarmDescription: "Runbook at https://liflig.no", + }) + + expect(stack).toMatchCdkSnapshot() +}) diff --git a/src/alarms/lambda-alarms.ts b/src/alarms/lambda-alarms.ts new file mode 100644 index 00000000..d37c6db9 --- /dev/null +++ b/src/alarms/lambda-alarms.ts @@ -0,0 +1,62 @@ +import * as constructs from "constructs" +import * as cdk from "aws-cdk-lib" +import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch" + +export interface LambdaAlarmsProps { + actions: cloudwatch.IAlarmAction[] + lambdaFunctionName: string +} + +export class LambdaAlarms extends constructs.Construct { + private readonly actions: cloudwatch.IAlarmAction[] + private readonly lambdaFunctionName: string + + constructor( + scope: constructs.Construct, + id: string, + props: LambdaAlarmsProps, + ) { + super(scope, id) + + this.actions = props.actions + this.lambdaFunctionName = props.lambdaFunctionName + } + + /** + * Sets up a CloudWatch Alarm that triggers if the Lambda fails invocations. + * This usually happens from uncaught exceptions in the lambda. + */ + addInvocationErrorAlarm( + /** + * Configuration for an alarm. + */ + props?: { + /** + * Add extra information to the alarm description, like Runbook URL or steps to triage. + */ + appendToAlarmDescription?: string + }, + ): void { + const alarm = new cloudwatch.Metric({ + metricName: "Errors", + namespace: "AWS/Lambda", + statistic: "Sum", + period: cdk.Duration.seconds(60), // Standard resolution metric has a minimum of 60s period + dimensionsMap: { + FunctionName: this.lambdaFunctionName, + }, + }).createAlarm(this, "FailedInvocationAlarm", { + alarmDescription: `Invocation for '${this.lambdaFunctionName}' failed. ${props?.appendToAlarmDescription ?? ""}`, + comparisonOperator: + cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + evaluationPeriods: 1, + threshold: 1, + treatMissingData: cloudwatch.TreatMissingData.IGNORE, + }) + + this.actions.forEach((action) => { + alarm.addAlarmAction(action) + alarm.addOkAction(action) + }) + } +}