Skip to content

Commit

Permalink
Merge 4cae35b into 18c409e
Browse files Browse the repository at this point in the history
  • Loading branch information
direnakkoc committed Jun 13, 2022
2 parents 18c409e + 4cae35b commit b9c5137
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 11 deletions.
21 changes: 20 additions & 1 deletion README.md
Expand Up @@ -16,6 +16,7 @@ SLIC Watch provides a CloudWatch Dashboard and Alarms for:
5. SQS Queues
6. Step Functions
7. ECS (Fargate or EC2)
8. SNS

Currently, SLIC Watch is available as a Serverless Framework plugin. Serverless Framework v2 and v3 are supported.

Expand Down Expand Up @@ -141,10 +142,16 @@ The dashboard contains one widget per Step Function:

### ECS / Fargate

ECS alarms are created for Fargate or EC2 clusters:
ECS alarms are created for:
1. Memory Utilization
2. CPU Utilization

### SNS

SNS alarms are created for:
1. Number of Notifications Filtered Out Invalid Attributes
2. Number of Notifications Failed

## Configuration

Configuration is entirely optional - SLIC Watch provides defaults that work out of the box.
Expand Down Expand Up @@ -266,6 +273,13 @@ custom:
CPUUtilization:
Statistic: Average
Threshold: 90
SNS:
NumberOfNotificationsFilteredOut-InvalidAttributes:
Statistic: Sum
Threshold: 1
NumberOfNotificationsFailed:
Statistic: Sum
Threshold: 1

dashboard:
enabled: true
Expand Down Expand Up @@ -346,6 +360,11 @@ custom:
Statistic: ["Average"]
CPUUtilization:
Statistic: ["Average"]
SNS:
NumberOfNotificationsFilteredOut-InvalidAttributes:
Statistic: ["Sum"]
NumberOfNotificationsFailed:
Statistic: ["Sum"]
```

An example project is provided for reference: [serverless-test-project](./serverless-test-project)
Expand Down
121 changes: 121 additions & 0 deletions serverless-plugin/alarms-sns.js
@@ -0,0 +1,121 @@
'use strict'

/**
* @param {object} snsAlarmsConfig The fully resolved alarm configuration
*/
module.exports = function snsAlarms (snsAlarmsConfig, context) {
return {
createSNSAlarms
}

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

for (const [topicResourceName, topicResource] of Object.entries(
topicResources
)) {
if (snsAlarmsConfig['NumberOfNotificationsFilteredOut-InvalidAttributes'].enabled) {
const numberOfNotificationsFilteredOutInvalidAttributes = createNumberOfNotificationsFilteredOutInvalidAttributesAlarm(
topicResourceName,
topicResource,
snsAlarmsConfig['NumberOfNotificationsFilteredOut-InvalidAttributes']
)
cfTemplate.addResource(numberOfNotificationsFilteredOutInvalidAttributes.resourceName, numberOfNotificationsFilteredOutInvalidAttributes.resource)
}

if (snsAlarmsConfig.NumberOfNotificationsFailed.enabled) {
const numberOfNotificationsFailed = createNumberOfNotificationsFailedAlarm(
topicResourceName,
topicResource,
snsAlarmsConfig.NumberOfNotificationsFailed
)
cfTemplate.addResource(numberOfNotificationsFailed.resourceName, numberOfNotificationsFailed.resource)
}
}
}

function createSNSAlarm (
alarmName,
alarmDescription,
topicName,
comparisonOperator,
threshold,
metricName,
statistic,
period,
evaluationPeriods,
treatMissingData
) {
const metricProperties = {
Dimensions: [{ Name: 'TopicName', Value: topicName }],
MetricName: metricName,
Namespace: 'AWS/SNS',
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 createNumberOfNotificationsFilteredOutInvalidAttributesAlarm (topicResourceName, topicResource, config) {
const topicName = topicResource.Properties.TopicName
const threshold = config.Threshold

return {
resourceName: `slicWatchSNSNumberOfNotificationsFilteredOutInvalidAttributesAlarm${topicResourceName}`,
resource: createSNSAlarm(
`SNSNumberOfNotificationsFilteredOutInvalidAttributesAlarm_${topicName}`, // alarmName
`Number of Notifications Filtered out Invalid Attributes for ${topicName} breaches (${threshold}`, // alarmDescription
topicName,
config.ComparisonOperator,
threshold,
'NumberOfNotificationsFilteredOut-InvalidAttributes', // metricName
config.Statistic,
config.Period,
config.EvaluationPeriods,
config.TreatMissingData
)
}
}

function createNumberOfNotificationsFailedAlarm (topicResourceName, topicResource, config) {
const topicName = topicResource.Properties.TopicName
const threshold = config.Threshold
return {
resourceName: `slicWatchSNSNumberOfNotificationsFailedAlarm${topicResourceName}`,
resource: createSNSAlarm(
`SNSNumberOfNotificationsFailedAlarm_${topicName}`, // alarmName
`Number of Notifications Failed for ${topicName} breaches ${threshold}`, // alarmDescription
topicName,
config.ComparisonOperator,
threshold,
'NumberOfNotificationsFailed', // metricName
config.Statistic,
config.Period,
config.EvaluationPeriods,
config.TreatMissingData
)
}
}
}
6 changes: 5 additions & 1 deletion serverless-plugin/alarms.js
Expand Up @@ -10,6 +10,7 @@ const dynamoDbAlarms = require('./alarms-dynamodb')
const kinesisAlarms = require('./alarms-kinesis')
const sqsAlarms = require('./alarms-sqs')
const ecsAlarms = require('./alarms-ecs')
const snsAlarms = require('./alarms-sns')

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

const cascadedFunctionAlarmConfigs = applyAlarmConfig(lambdaConfig, functionAlarmConfigs)
Expand All @@ -30,6 +32,7 @@ module.exports = function alarms (serverless, alarmConfig, functionAlarmConfigs,
const { createKinesisAlarms } = kinesisAlarms(kinesisConfig, context, serverless)
const { createSQSAlarms } = sqsAlarms(sqsConfig, context, serverless)
const { createECSAlarms } = ecsAlarms(ecsConfig, context, serverless)
const { createSNSAlarms } = snsAlarms(snsConfig, context, serverless)

return {
addAlarms
Expand All @@ -50,6 +53,7 @@ module.exports = function alarms (serverless, alarmConfig, functionAlarmConfigs,
kinesisConfig.enabled && createKinesisAlarms(cfTemplate)
sqsConfig.enabled && createSQSAlarms(cfTemplate)
ecsConfig.enabled && createECSAlarms(cfTemplate)
snsConfig.enabled && createSNSAlarms(cfTemplate)
}
}
}
6 changes: 4 additions & 2 deletions serverless-plugin/config-schema.js
Expand Up @@ -18,7 +18,8 @@ const supportedAlarms = {
DynamoDB: ['ReadThrottleEvents', 'WriteThrottleEvents', 'UserErrors', 'SystemErrors'],
Kinesis: ['GetRecords.IteratorAgeMilliseconds', 'ReadProvisionedThroughputExceeded', 'WriteProvisionedThroughputExceeded', 'PutRecord.Success', 'PutRecords.Success', 'GetRecords.Success'],
SQS: ['AgeOfOldestMessage', 'InFlightMessagesPc'],
ECS: ['MemoryUtilization', 'CPUUtilization']
ECS: ['MemoryUtilization', 'CPUUtilization'],
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed']
}

const supportedWidgets = {
Expand All @@ -28,7 +29,8 @@ const supportedWidgets = {
DynamoDB: ['ReadThrottleEvents', 'WriteThrottleEvents'],
Kinesis: ['GetRecords.IteratorAgeMilliseconds', 'ReadProvisionedThroughputExceeded', 'WriteProvisionedThroughputExceeded', 'PutRecord.Success', 'PutRecords.Success', 'GetRecords.Success'],
SQS: ['NumberOfMessagesSent', 'NumberOfMessagesReceived', 'NumberOfMessagesDeleted', 'ApproximateAgeOfOldestMessage', 'ApproximateNumberOfMessagesVisible'],
ECS: ['MemoryUtilization', 'CPUUtilization']
ECS: ['MemoryUtilization', 'CPUUtilization'],
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed']
}

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

Expand Down Expand Up @@ -57,6 +58,9 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
const ecsServiceResources = cfTemplate.getResourcesByType(
'AWS::ECS::Service'
)
const topicResources = cfTemplate.getResourcesByType(
'AWS::SNS::Topic'
)

const eventSourceMappingFunctions = cfTemplate.getEventSourceMappingFunctions()
const apiWidgets = createApiWidgets(apiResources)
Expand All @@ -69,6 +73,7 @@ module.exports = function dashboard (serverless, dashboardConfig, functionDashbo
const streamWidgets = createStreamWidgets(streamResources)
const queueWidgets = createQueueWidgets(queueResources)
const ecsWidgets = createEcsWidgets(ecsServiceResources)
const topicWidgets = createTopicWidgets(topicResources)

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

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

/**
* Create a set of CloudWatch Dashboard widgets for SNS services.
*
* @param {object} topicResources Object of SNS Service resources by resource name
*/
function createTopicWidgets (topicResources) {
const topicWidgets = []
for (const res of Object.values(topicResources)) {
const topicName = res.Properties.TopicName

const widgetMetrics = []
for (const [metric, metricConfig] of Object.entries(getConfiguredMetrics(snsDashConfig))) {
if (metricConfig.enabled) {
for (const stat of metricConfig.Statistic) {
widgetMetrics.push({
namespace: 'AWS/SNS',
metric,
dimensions: {
TopicName: topicName
},
stat
})
}
}
}
if (widgetMetrics.length > 0) {
const metricStatWidget = createMetricWidget(
`SNS Topic ${topicName}`,
widgetMetrics,
snsDashConfig
)
topicWidgets.push(metricStatWidget)
}
}
return topicWidgets
}

/**
* Set the location and dimension properties of each provided widget
*
Expand Down
16 changes: 15 additions & 1 deletion serverless-plugin/default-config.yaml
Expand Up @@ -94,6 +94,13 @@ alarms:
CPUUtilization:
Statistic: Average
Threshold: 90
SNS:
NumberOfNotificationsFilteredOut-InvalidAttributes:
Statistic: Sum
Threshold: 1
NumberOfNotificationsFailed:
Statistic: Sum
Threshold: 1

dashboard:
enabled: true
Expand Down Expand Up @@ -172,4 +179,11 @@ dashboard:
MemoryUtilization:
Statistic: ["Average"]
CPUUtilization:
Statistic: ["Average"]
Statistic: ["Average"]
SNS:
NumberOfNotificationsFilteredOut-InvalidAttributes:
Statistic: ["Sum"]
NumberOfNotificationsFailed:
Statistic: ["Sum"]


2 changes: 1 addition & 1 deletion serverless-plugin/tests/alarms-ecs.test.js
Expand Up @@ -72,7 +72,7 @@ test('ECS MemoryUtilization is created', (t) => {
t.end()
})

test('Kinesis data stream alarms are not created when disabled globally', (t) => {
test('ECS alarms are not created when disabled globally', (t) => {
const alarmConfig = createTestConfig(
defaultConfig.alarms,
{
Expand Down

0 comments on commit b9c5137

Please sign in to comment.