From d9e337c903cfa136f7f9780b76abb1596cad82e2 Mon Sep 17 00:00:00 2001 From: nakultt Date: Wed, 28 Oct 2026 14:30:10 +0530 Subject: [PATCH] feat(events): add CloudWatchAlarmEvent for Lambda alarm actions Add the event type CloudWatch sends when a Lambda function is configured directly as an alarm action. Covers metric alarms, composite alarms, and alarm tags. Timestamps are strings because CloudWatch uses a +0000 offset that time.Time cannot parse, matching CloudWatchAlarmSNSPayload. Fixes #555 --- events/cloudwatch_alarms.go | 69 +++++++++++++++++++ events/cloudwatch_alarms_test.go | 60 ++++++++++++++++ .../cloudwatch-alarm-composite-event.json | 30 ++++++++ .../cloudwatch-alarm-metric-event.json | 49 +++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 events/cloudwatch_alarms.go create mode 100644 events/cloudwatch_alarms_test.go create mode 100644 events/testdata/cloudwatch-alarm-composite-event.json create mode 100644 events/testdata/cloudwatch-alarm-metric-event.json diff --git a/events/cloudwatch_alarms.go b/events/cloudwatch_alarms.go new file mode 100644 index 00000000..be24cfe5 --- /dev/null +++ b/events/cloudwatch_alarms.go @@ -0,0 +1,69 @@ +package events + +// CloudWatchAlarmEvent is the event sent to a Lambda function when it is configured as a CloudWatch alarm action. +// See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/alarms-and-actions-Lambda.html +type CloudWatchAlarmEvent struct { + Source string `json:"source"` + AlarmARN string `json:"alarmArn"` + AccountID string `json:"accountId"` + Time string `json:"time"` + Region string `json:"region"` + AlarmData CloudWatchAlarmData `json:"alarmData"` +} + +// CloudWatchAlarmData describes the alarm, its current and previous state, and its configuration. +type CloudWatchAlarmData struct { + AlarmName string `json:"alarmName"` + State CloudWatchAlarmState `json:"state"` + PreviousState CloudWatchAlarmState `json:"previousState"` + Configuration CloudWatchAlarmConfiguration `json:"configuration"` + AlarmTags []CloudWatchAlarmTag `json:"alarmTags,omitempty"` + AlarmTagsStatus string `json:"alarmTagsStatus,omitempty"` +} + +// CloudWatchAlarmState is a state of the alarm. ReasonData is a JSON-encoded string. +type CloudWatchAlarmState struct { + Value string `json:"value"` + Reason string `json:"reason"` + ReasonData string `json:"reasonData,omitempty"` + Timestamp string `json:"timestamp"` + ActionsSuppressedBy string `json:"actionsSuppressedBy,omitempty"` + ActionsSuppressedReason string `json:"actionsSuppressedReason,omitempty"` +} + +// CloudWatchAlarmConfiguration holds the configuration of a metric alarm (Metrics) +// or a composite alarm (AlarmRule and the ActionsSuppressor fields). +type CloudWatchAlarmConfiguration struct { + Description string `json:"description,omitempty"` + Metrics []CloudWatchAlarmMetricDataQuery `json:"metrics,omitempty"` + AlarmRule string `json:"alarmRule,omitempty"` + ActionsSuppressor string `json:"actionsSuppressor,omitempty"` + ActionsSuppressorWaitPeriod int64 `json:"actionsSuppressorWaitPeriod,omitempty"` + ActionsSuppressorExtensionPeriod int64 `json:"actionsSuppressorExtensionPeriod,omitempty"` +} + +type CloudWatchAlarmMetricDataQuery struct { + ID string `json:"id"` + Expression string `json:"expression,omitempty"` + Label string `json:"label,omitempty"` + MetricStat *CloudWatchAlarmMetricStat `json:"metricStat,omitempty"` + ReturnData bool `json:"returnData"` +} + +type CloudWatchAlarmMetricStat struct { + Metric CloudWatchAlarmMetric `json:"metric"` + Period int64 `json:"period"` + Stat string `json:"stat"` + Unit string `json:"unit,omitempty"` +} + +type CloudWatchAlarmMetric struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Dimensions map[string]string `json:"dimensions,omitempty"` +} + +type CloudWatchAlarmTag struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/events/cloudwatch_alarms_test.go b/events/cloudwatch_alarms_test.go new file mode 100644 index 00000000..d8d767e7 --- /dev/null +++ b/events/cloudwatch_alarms_test.go @@ -0,0 +1,60 @@ +package events + +import ( + "encoding/json" + "testing" + + "github.com/aws/aws-lambda-go/events/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCloudWatchAlarmEventMarshaling(t *testing.T) { + tests := []struct { + name string + filePath string + }{ + {"metric alarm", "./testdata/cloudwatch-alarm-metric-event.json"}, + {"composite alarm", "./testdata/cloudwatch-alarm-composite-event.json"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + inputJSON := test.ReadJSONFromFile(t, tc.filePath) + + var inputEvent CloudWatchAlarmEvent + require.NoError(t, json.Unmarshal(inputJSON, &inputEvent)) + + outputJSON, err := json.Marshal(inputEvent) + require.NoError(t, err) + + assert.JSONEq(t, string(inputJSON), string(outputJSON)) + }) + } +} + +func TestCloudWatchAlarmEventFields(t *testing.T) { + var metricEvent CloudWatchAlarmEvent + require.NoError(t, json.Unmarshal(test.ReadJSONFromFile(t, "./testdata/cloudwatch-alarm-metric-event.json"), &metricEvent)) + + assert.Equal(t, "ALARM", metricEvent.AlarmData.State.Value) + assert.Equal(t, "INSUFFICIENT_DATA", metricEvent.AlarmData.PreviousState.Value) + require.Len(t, metricEvent.AlarmData.Configuration.Metrics, 1) + metric := metricEvent.AlarmData.Configuration.Metrics[0] + require.NotNil(t, metric.MetricStat) + assert.Equal(t, "i-12345678", metric.MetricStat.Metric.Dimensions["InstanceId"]) + assert.True(t, metric.ReturnData) + assert.Equal(t, []CloudWatchAlarmTag{{Key: "Environment", Value: "prod"}}, metricEvent.AlarmData.AlarmTags) + + var compositeEvent CloudWatchAlarmEvent + require.NoError(t, json.Unmarshal(test.ReadJSONFromFile(t, "./testdata/cloudwatch-alarm-composite-event.json"), &compositeEvent)) + + assert.Equal(t, "ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", compositeEvent.AlarmData.Configuration.AlarmRule) + assert.Equal(t, int64(120), compositeEvent.AlarmData.Configuration.ActionsSuppressorWaitPeriod) + assert.Equal(t, "WaitPeriod", compositeEvent.AlarmData.PreviousState.ActionsSuppressedBy) + assert.Empty(t, compositeEvent.AlarmData.Configuration.Metrics) +} + +func TestCloudWatchAlarmEventMalformedJson(t *testing.T) { + test.TestMalformedJson(t, &CloudWatchAlarmEvent{}) +} diff --git a/events/testdata/cloudwatch-alarm-composite-event.json b/events/testdata/cloudwatch-alarm-composite-event.json new file mode 100644 index 00000000..8d402ec4 --- /dev/null +++ b/events/testdata/cloudwatch-alarm-composite-event.json @@ -0,0 +1,30 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + "accountId": "111122223333", + "time": "2023-08-04T12:56:46.138+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "CompositeDemo.Main", + "state": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12:56:46.138+0000" + }, + "previousState": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12:54:46.138+0000", + "actionsSuppressedBy": "WaitPeriod", + "actionsSuppressedReason": "Actions suppressed by WaitPeriod" + }, + "configuration": { + "alarmRule": "ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", + "actionsSuppressor": "CompositeDemo.ActionsSuppressor", + "actionsSuppressorWaitPeriod": 120, + "actionsSuppressorExtensionPeriod": 180 + } + } +} diff --git a/events/testdata/cloudwatch-alarm-metric-event.json b/events/testdata/cloudwatch-alarm-metric-event.json new file mode 100644 index 00000000..94475896 --- /dev/null +++ b/events/testdata/cloudwatch-alarm-metric-event.json @@ -0,0 +1,49 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1:444455556666:alarm:lambda-demo-metric-alarm", + "accountId": "444455556666", + "time": "2023-08-04T12:36:15.490+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "lambda-demo-metric-alarm", + "state": { + "value": "ALARM", + "reason": "test", + "timestamp": "2023-08-04T12:36:15.490+0000" + }, + "previousState": { + "value": "INSUFFICIENT_DATA", + "reason": "Insufficient Data: 5 datapoints were unknown.", + "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2023-08-04T12:31:29.591+0000\",\"statistic\":\"Average\",\"period\":60,\"recentDatapoints\":[],\"threshold\":5.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-08-04T12:30:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:29:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:28:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:27:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:26:00.000+0000\"}]}", + "timestamp": "2023-08-04T12:31:29.595+0000" + }, + "configuration": { + "description": "Metric Alarm to test Lambda actions", + "metrics": [ + { + "id": "1234e046-06f0-a3da-9534-EXAMPLEe4c", + "metricStat": { + "metric": { + "namespace": "AWS/Logs", + "name": "CallCount", + "dimensions": { + "InstanceId": "i-12345678" + } + }, + "period": 60, + "stat": "Average", + "unit": "Percent" + }, + "returnData": true + } + ] + }, + "alarmTags": [ + { + "key": "Environment", + "value": "prod" + } + ], + "alarmTagsStatus": "SUCCESS" + } +}