Skip to content

Commit

Permalink
Merge bb23c5e into 09d0c2a
Browse files Browse the repository at this point in the history
  • Loading branch information
direnakkoc committed Jul 4, 2022
2 parents 09d0c2a + bb23c5e commit aadfaa7
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 11 deletions.
119 changes: 119 additions & 0 deletions serverless-plugin/alarms-eventbridge.js
@@ -0,0 +1,119 @@
'use strict'

/**
* @param {object} eventsAlarmsConfig The fully resolved alarm configuration
*/
module.exports = function eventsAlarms (eventsAlarmsConfig, context) {
return {
createEventsAlarms
}

/**
* Add all required Events alarms to the provided CloudFormation template
* based on the Events resources found within
*
* @param {CloudFormationTemplate} cfTemplate A CloudFormation template object
*/
function createEventsAlarms (cfTemplate) {
const ruleResources = cfTemplate.getResourcesByType(
'AWS::Events::Rule'
)

for (const [ruleResourceName, ruleResource] of Object.entries(ruleResources)) {
if (eventsAlarmsConfig.FailedInvocations.enabled) {
const failedInvocations = createFailedInvocationsAlarm(
ruleResourceName,
ruleResource,
eventsAlarmsConfig.FailedInvocations
)
cfTemplate.addResource(failedInvocations.resourceName, failedInvocations.resource)
}

if (eventsAlarmsConfig.ThrottledRules.enabled) {
const throttledRules = createThrottledRulesAlarm(
ruleResourceName,
ruleResource,
eventsAlarmsConfig.ThrottledRules
)
cfTemplate.addResource(throttledRules.resourceName, throttledRules.resource)
}
}
}

function createEventsAlarm (
alarmName,
alarmDescription,
ruleName,
comparisonOperator,
threshold,
metricName,
statistic,
period,
evaluationPeriods,
treatMissingData
) {
const metricProperties = {
Dimensions: [{ Name: 'RuleName', Value: ruleName }],
MetricName: metricName,
Namespace: 'AWS/Events',
Period: period,
Statistic: statistic
}

return {
Type: 'AWS::CloudWatch::Alarm',
Properties: {
ActionsEnabled: true,
AlarmActions: context.alarmActions,
AlarmName: alarmName,
AlarmDescription: alarmDescription,
EvaluationPeriods: evaluationPeriods,
ComparisonOperator: comparisonOperator,
Threshold: threshold,
TreatMissingData: treatMissingData,
...metricProperties
}
}
}

function createFailedInvocationsAlarm (ruleResourceName, ruleResource, config) {
const ruleName = ruleResource.Properties.Name
const threshold = config.Threshold

return {
resourceName: `slicWatchEventsFailedInvocationsAlarm${ruleResourceName}`,
resource: createEventsAlarm(
`EventsFailedInvocationsAlarm_${ruleName}`, // alarmName
`Failed Invocations for ${ruleName} breaches ${threshold}`, // alarmDescription
ruleName,
config.ComparisonOperator,
threshold,
'FailedInvocations', // metricName
config.Statistic,
config.Period,
config.EvaluationPeriods,
config.TreatMissingData
)
}
}

function createThrottledRulesAlarm (ruleResourceName, ruleResource, config) {
const ruleName = ruleResource.Properties.Name
const threshold = config.Threshold
return {
resourceName: `slicWatchEventsThrottledRulesAlarm${ruleResourceName}`,
resource: createEventsAlarm(
`EventsThrottledRulesAlarm_${ruleName}`, // alarmName
`Throttled RulesAlarm for ${ruleName} breaches ${threshold}`, // alarmDescription
ruleName,
config.ComparisonOperator,
threshold,
'ThrottledRules', // metricName
config.Statistic,
config.Period,
config.EvaluationPeriods,
config.TreatMissingData
)
}
}
}
6 changes: 5 additions & 1 deletion serverless-plugin/alarms.js
Expand Up @@ -11,6 +11,7 @@ const kinesisAlarms = require('./alarms-kinesis')
const sqsAlarms = require('./alarms-sqs')
const ecsAlarms = require('./alarms-ecs')
const snsAlarms = require('./alarms-sns')
const eventsAlarms = require('./alarms-eventbridge')

module.exports = function alarms (serverless, alarmConfig, functionAlarmConfigs, context) {
const {
Expand All @@ -21,7 +22,8 @@ module.exports = function alarms (serverless, alarmConfig, functionAlarmConfigs,
SQS: sqsConfig,
Lambda: lambdaConfig,
ECS: ecsConfig,
SNS: snsConfig
SNS: snsConfig,
Events: eventsConfig
} = cascade(alarmConfig)

const cascadedFunctionAlarmConfigs = applyAlarmConfig(lambdaConfig, functionAlarmConfigs)
Expand All @@ -33,6 +35,7 @@ module.exports = function alarms (serverless, alarmConfig, functionAlarmConfigs,
const { createSQSAlarms } = sqsAlarms(sqsConfig, context, serverless)
const { createECSAlarms } = ecsAlarms(ecsConfig, context, serverless)
const { createSNSAlarms } = snsAlarms(snsConfig, context, serverless)
const { createEventsAlarms } = eventsAlarms(eventsConfig, context, serverless)

return {
addAlarms
Expand All @@ -54,6 +57,7 @@ module.exports = function alarms (serverless, alarmConfig, functionAlarmConfigs,
sqsConfig.enabled && createSQSAlarms(cfTemplate)
ecsConfig.enabled && createECSAlarms(cfTemplate)
snsConfig.enabled && createSNSAlarms(cfTemplate)
eventsConfig.enabled && createEventsAlarms(cfTemplate)
}
}
}
6 changes: 4 additions & 2 deletions serverless-plugin/config-schema.js
Expand Up @@ -19,7 +19,8 @@ const supportedAlarms = {
Kinesis: ['GetRecords.IteratorAgeMilliseconds', 'ReadProvisionedThroughputExceeded', 'WriteProvisionedThroughputExceeded', 'PutRecord.Success', 'PutRecords.Success', 'GetRecords.Success'],
SQS: ['AgeOfOldestMessage', 'InFlightMessagesPc'],
ECS: ['MemoryUtilization', 'CPUUtilization'],
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed']
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed'],
Events: ['FailedInvocations', 'ThrottledRules']
}

const supportedWidgets = {
Expand All @@ -30,7 +31,8 @@ const supportedWidgets = {
Kinesis: ['GetRecords.IteratorAgeMilliseconds', 'ReadProvisionedThroughputExceeded', 'WriteProvisionedThroughputExceeded', 'PutRecord.Success', 'PutRecords.Success', 'GetRecords.Success'],
SQS: ['NumberOfMessagesSent', 'NumberOfMessagesReceived', 'NumberOfMessagesDeleted', 'ApproximateAgeOfOldestMessage', 'ApproximateNumberOfMessagesVisible'],
ECS: ['MemoryUtilization', 'CPUUtilization'],
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed']
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed'],
Events: ['FailedInvocations', 'ThrottledRules', 'Invocations']
}

const commonAlarmProperties = {
Expand Down
47 changes: 45 additions & 2 deletions serverless-plugin/dashboard.js
Expand Up @@ -22,7 +22,8 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
Kinesis: kinesisDashConfig,
SQS: sqsDashConfig,
ECS: ecsDashConfig,
SNS: snsDashConfig
SNS: snsDashConfig,
Events: eventsDashConfig
}
} = cascade(dashboardConfig)

Expand Down Expand Up @@ -61,6 +62,9 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
const topicResources = cfTemplate.getResourcesByType(
'AWS::SNS::Topic'
)
const ruleResources = cfTemplate.getResourcesByType(
'AWS::Events::Rule'
)

const eventSourceMappingFunctions = cfTemplate.getEventSourceMappingFunctions()
const apiWidgets = createApiWidgets(apiResources)
Expand All @@ -74,6 +78,7 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
const queueWidgets = createQueueWidgets(queueResources)
const ecsWidgets = createEcsWidgets(ecsServiceResources)
const topicWidgets = createTopicWidgets(topicResources)
const ruleWidgets = createRuleWidgets(ruleResources)

const positionedWidgets = layOutWidgets([
...apiWidgets,
Expand All @@ -83,7 +88,8 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
...streamWidgets,
...queueWidgets,
...ecsWidgets,
...topicWidgets
...topicWidgets,
...ruleWidgets
])

if (positionedWidgets.length > 0) {
Expand Down Expand Up @@ -501,6 +507,43 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
return topicWidgets
}

/**
* Create a set of CloudWatch Dashboard widgets for SNS services.
*
* @param {object} ruleResources Object of SNS Service resources by resource name
*/
function createRuleWidgets (ruleResources) {
const ruleWidgets = []
for (const res of Object.values(ruleResources)) {
const ruleName = res.Properties.Name

const widgetMetrics = []
for (const [metric, metricConfig] of Object.entries(getConfiguredMetrics(eventsDashConfig))) {
if (metricConfig.enabled) {
for (const stat of metricConfig.Statistic) {
widgetMetrics.push({
namespace: 'AWS/Events',
metric,
dimensions: {
RuleName: ruleName
},
stat
})
}
}
}
if (widgetMetrics.length > 0) {
const metricStatWidget = createMetricWidget(
`Events rule ${ruleName}`,
widgetMetrics,
eventsDashConfig
)
ruleWidgets.push(metricStatWidget)
}
}
return ruleWidgets
}

/**
* Set the location and dimension properties of each provided widget
*
Expand Down
17 changes: 16 additions & 1 deletion serverless-plugin/default-config.yaml
Expand Up @@ -101,6 +101,14 @@ alarms:
NumberOfNotificationsFailed:
Statistic: Sum
Threshold: 1
Events:
#EventBridge
FailedInvocations:
Statistic: Sum
Threshold: 1
ThrottledRules:
Statistic: Sum
Threshold: 1

dashboard:
enabled: true
Expand Down Expand Up @@ -185,5 +193,12 @@ dashboard:
Statistic: ["Sum"]
NumberOfNotificationsFailed:
Statistic: ["Sum"]

Events:
#EventBridge
FailedInvocations:
Statistic: ["Sum"]
ThrottledRules:
Statistic: ["Sum"]
Invocations:
Statistic: ["Sum"]

99 changes: 99 additions & 0 deletions serverless-plugin/tests/alarms-eventbridge.test.js
@@ -0,0 +1,99 @@
'use strict'

const eventsAlarms = require('../alarms-eventbridge')
const { test } = require('tap')
const defaultConfig = require('../default-config')
const {
assertCommonAlarmProperties,
alarmNameToType,
createTestConfig,
createTestCloudFormationTemplate,
testContext
} = require('./testing-utils')

test('Events alarms are created', (t) => {
const alarmConfig = createTestConfig(
defaultConfig.alarms,
{
Period: 120,
EvaluationPeriods: 2,
TreatMissingData: 'breaching',
ComparisonOperator: 'GreaterThanOrEqualToThreshold',
Events: {
FailedInvocations: {
Threshold: 50
},
ThrottledRules: {
Threshold: 50
}
}
}
)

const eventsAlarmConfig = alarmConfig.Events

const { createEventsAlarms } = eventsAlarms(eventsAlarmConfig, testContext)
const cfTemplate = createTestCloudFormationTemplate()
createEventsAlarms(cfTemplate)

const alarmResources = cfTemplate.getResourcesByType('AWS::CloudWatch::Alarm')

const expectedTypes = {
EventsFailedInvocationsAlarm: 'FailedInvocations',
EventsThrottledRulesAlarm: 'ThrottledRules'
}

t.equal(Object.keys(alarmResources).length, Object.keys(expectedTypes).length)
for (const alarmResource of Object.values(alarmResources)) {
const al = alarmResource.Properties
assertCommonAlarmProperties(t, al)
const alarmType = alarmNameToType(al.AlarmName)
const expectedMetric = expectedTypes[alarmType]
t.equal(al.MetricName, expectedMetric)
t.ok(al.Statistic)
t.equal(al.Threshold, eventsAlarmConfig[expectedMetric].Threshold)
t.equal(al.EvaluationPeriods, 2)
t.equal(al.TreatMissingData, 'breaching')
t.equal(al.ComparisonOperator, 'GreaterThanOrEqualToThreshold')
t.equal(al.Namespace, 'AWS/Events')
t.equal(al.Period, 120)
t.same(al.Dimensions, [
{
Name: 'RuleName',
Value: 'serverless-test-project-dev-eventsRule-rule-1'
}
])
}

t.end()
})

test('Events alarms are not created when disabled globally', (t) => {
const alarmConfig = createTestConfig(
defaultConfig.alarms,
{
Events: {
enabled: false, // disabled globally
Period: 60,
FailedInvocations: {
Threshold: 50
},
ThrottledRules: {
Threshold: 50
}
}
}
)

const eventsAlarmConfig = alarmConfig.Events

const { createEventsAlarms } = eventsAlarms(eventsAlarmConfig, testContext)

const cfTemplate = createTestCloudFormationTemplate()
createEventsAlarms(cfTemplate)

const alarmResources = cfTemplate.getResourcesByType('AWS::CloudWatch::Alarm')

t.same({}, alarmResources)
t.end()
})
2 changes: 1 addition & 1 deletion serverless-plugin/tests/alarms.test.js
Expand Up @@ -22,7 +22,7 @@ test('Alarms create all service alarms', (t) => {
namespaces.add(resource.Properties.Namespace)
}
}
t.same(namespaces, new Set(['AWS/Lambda', 'AWS/ApiGateway', 'AWS/States', 'AWS/DynamoDB', 'AWS/Kinesis', 'AWS/SQS', 'AWS/ECS', 'AWS/SNS']))
t.same(namespaces, new Set(['AWS/Lambda', 'AWS/ApiGateway', 'AWS/States', 'AWS/DynamoDB', 'AWS/Kinesis', 'AWS/SQS', 'AWS/ECS', 'AWS/SNS', 'AWS/Events']))
t.end()
})

Expand Down

0 comments on commit aadfaa7

Please sign in to comment.