Repository: ACloudGuru/serverless-plugin-aws-alerts
Pull request #100 (2019-07-19 07:02:26)

Economia master


Patch:
diff --git a/README.md b/README.md
index cbac742..359780e 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,67 @@ functions:
         comparisonOperator: GreaterThanOrEqualToThreshold
 ```
 
+## Multiple topic definitions
+
+You can define several topics for alarms. For example you want to have topics for critical alarms
+reaching your pagerduty, and different topics for noncritical alarms, which just send you emails.
+
+In each alarm definition you have to specify which topics you want to use. In following example
+you get an email for each function error, pagerduty gets alarm only if there are more than 20
+errors in 60s
+
+```yaml
+custom:
+  alerts:
+
+    topics:
+      critical:
+        ok:
+          topic: ${self:service}-${opt:stage}-critical-alerts-ok
+          notifications:
+          - protocol: https
+            endpoint: https://events.pagerduty.com/integration/.../enqueue
+        alarm:
+          topic: ${self:service}-${opt:stage}-critical-alerts-alarm
+          notifications:
+          - protocol: https
+            endpoint: https://events.pagerduty.com/integration/.../enqueue
+
+      nonCritical:
+        alarm:
+          topic: ${self:service}-${opt:stage}-nonCritical-alerts-alarm
+          notifications:
+          - protocol: email
+            endpoint: alarms@email.com
+
+    definitions:  # these defaults are merged with your definitions
+      criticalFunctionErrors:
+        namespace: 'AWS/Lambda'
+        metric: Errors
+        threshold: 20
+        statistic: Sum
+        period: 60
+        evaluationPeriods: 10
+        comparisonOperator: GreaterThanOrEqualToThreshold
+        okActions:
+          - critical
+        alarmActions:
+          - critical
+      nonCriticalFunctionErrors:
+        namespace: 'AWS/Lambda'
+        metric: Errors
+        threshold: 1
+        statistic: Sum
+        period: 60
+        evaluationPeriods: 10
+        comparisonOperator: GreaterThanOrEqualToThreshold
+        alarmActions:
+          - nonCritical
+    alarms:
+      - criticalFunctionErrors
+      - nonCriticalFunctionErrors
+
+```
 ## SNS Topics
 
 If topic name is specified, plugin assumes that topic does not exist and will create it. To use existing topics, specify ARNs or use Fn::ImportValue to use a topic exported with CloudFormation.
diff --git a/src/index.js b/src/index.js
index 5559aac..0f3ef49 100644
--- a/src/index.js
+++ b/src/index.js
@@ -90,7 +90,22 @@ class AlertsPlugin {
       insufficientDataActions.push(alertTopics.insufficientData);
     }
 
+
+    if (definition.okActions) {
+      definition.okActions.map( alertTopic => {okActions.push(alertTopics[alertTopic].ok)});
+    }
+
+    if (definition.alarmActions) {
+      definition.alarmActions.map( alertTopic => {alarmActions.push(alertTopics[alertTopic].alarm)});
+    }
+
+    if (definition.insufficientDataActions) {
+      definition.insufficientDataActions.map( alertTopic => {insufficientDataActions.push(alertTopics[alertTopic].insufficientData)});
+    }
+
+
     const stackName = this.awsProvider.naming.getStackName();
+
     const namespace = definition.pattern ?
       stackName :
       definition.namespace;
@@ -157,31 +172,50 @@ class AlertsPlugin {
     };
   }
 
+  _addAlertTopic(key, topics, alertTopics, customAlarmName) {
+    const topicConfig = topics[key];
+    const isTopicConfigAnObject = _.isObject(topicConfig);
+    const isTopicConfigAnImport = isTopicConfigAnObject && topicConfig['Fn::ImportValue'];
+
+    const topic = isTopicConfigAnObject ? topicConfig.topic : topicConfig;
+    const notifications = isTopicConfigAnObject ? topicConfig.notifications : [];
+
+    if (topic) {
+      if (isTopicConfigAnImport || topic.indexOf('arn:') === 0) {
+        alertTopics[key] = topic;
+      } else {
+        const cfRef = `AwsAlerts${customAlarmName ? _.upperFirst(customAlarmName) : ''}${_.upperFirst(key)}`;
+        if (customAlarmName) {
+          if (!alertTopics[customAlarmName]) {
+          alertTopics[customAlarmName] = {}
+                }
+          alertTopics[customAlarmName][key] = {
+            Ref: cfRef
+          };
+              } else {
+          alertTopics[key] = {
+            Ref: cfRef
+          };
+              }
+
+        this.addCfResources({
+          [cfRef]: this.getSnsTopicCloudFormation(topic, notifications),
+        });
+      }
+    }
+  }
+
   compileAlertTopics(config) {
     const alertTopics = {};
 
     if (config.topics) {
       Object.keys(config.topics).forEach((key) => {
-        const topicConfig = config.topics[key];
-        const isTopicConfigAnObject = _.isObject(topicConfig);
-        const isTopicConfigAnImport = isTopicConfigAnObject && topicConfig['Fn::ImportValue'];
-
-        const topic = isTopicConfigAnObject && !isTopicConfigAnImport ? topicConfig.topic : topicConfig;
-        const notifications = isTopicConfigAnObject && !isTopicConfigAnImport ? topicConfig.notifications : [];
-
-        if (topic) {
-          if (isTopicConfigAnImport || topic.indexOf('arn:') === 0) {
-            alertTopics[key] = topic;
-          } else {
-            const cfRef = `AwsAlerts${_.upperFirst(key)}`;
-            alertTopics[key] = {
-              Ref: cfRef
-            };
-
-            this.addCfResources({
-              [cfRef]: this.getSnsTopicCloudFormation(topic, notifications),
-            });
-          }
+        if (['ok', 'alarm', 'insufficientData'].indexOf(key) !== -1) {
+      this._addAlertTopic(key, config.topics, alertTopics)
+        } else {
+      Object.keys(config.topics[key]).forEach((subkey) => {
+        this._addAlertTopic(subkey, config.topics[key], alertTopics, key)
+      })
         }
       });
     }


Test patch:
diff --git a/src/index.test.js b/src/index.test.js
index 1e06286..39b4003 100644
--- a/src/index.test.js
+++ b/src/index.test.js
@@ -447,6 +447,74 @@ describe('#index', function () {
         }
       });
     });
+
+    it('should create SNS topic with nested definitions', () => {
+      const plugin = pluginFactory({
+        topics: {
+          critical: {
+            ok: 'critical-ok-topic',
+            alert: 'critical-alert-topic',
+            insufficientData: 'critical-insufficientData-topic'
+          },
+          nonCritical: {
+            alarm: 'nonCritical-alarm-topic'
+          }
+        }
+      });
+
+      const config = plugin.getConfig();
+      const topics = plugin.compileAlertTopics(config);
+
+      expect(topics).toEqual({
+        critical: {
+          ok: {
+            Ref: `AwsAlertsCriticalOk`
+          },
+      alert: {
+        Ref: `AwsAlertsCriticalAlert`
+      },
+      insufficientData: {
+        Ref: `AwsAlertsCriticalInsufficientData`
+      }
+        },
+        nonCritical: {
+          alarm: {
+            Ref: `AwsAlertsNonCriticalAlarm`
+          }
+        }
+      });
+
+      expect(plugin.serverless.service.provider.compiledCloudFormationTemplate.Resources).toEqual({
+        'AwsAlertsCriticalOk': {
+          Type: 'AWS::SNS::Topic',
+          Properties: {
+            TopicName: 'critical-ok-topic',
+            Subscription: [],
+          }
+        },
+        'AwsAlertsCriticalAlert': {
+          Type: 'AWS::SNS::Topic',
+          Properties: {
+            TopicName: 'critical-alert-topic',
+            Subscription: [],
+          }
+        },
+        'AwsAlertsCriticalInsufficientData': {
+          Type: 'AWS::SNS::Topic',
+          Properties: {
+            TopicName: 'critical-insufficientData-topic',
+            Subscription: [],
+          }
+        },
+        'AwsAlertsNonCriticalAlarm': {
+          Type: 'AWS::SNS::Topic',
+          Properties: {
+            TopicName: 'nonCritical-alarm-topic',
+            Subscription: [],
+          }
+        }
+      });
+    });
   });
 
   describe('#compileAlarms', () => {
@@ -795,6 +863,67 @@ describe('#index', function () {
       });
     });
 
+
+    it('should add nested actions - create topics', () => {
+      const alertTopics = {
+        critical: {
+          ok: 'critical-ok-topic',
+          alarm: 'critical-alarm-topic',
+          insufficientData: 'critical-insufficientData-topic',
+        },
+        nonCritical: {
+          ok: 'nonCritical-ok-topic',
+          alarm: 'nonCritical-alarm-topic',
+          insufficientData: 'nonCritical-insufficientData-topic',
+        }
+      };
+
+      const definition = {
+        description: 'An error alarm',
+        namespace: 'AWS/Lambda',
+        metric: 'Errors',
+        threshold: 1,
+        statistic: 'Maximum',
+        period: 300,
+        evaluationPeriods: 1,
+        comparisonOperator: 'GreaterThanOrEqualToThreshold',
+        treatMissingData: 'breaching',
+        okActions: ['critical', 'nonCritical'],
+        alarmActions: ['critical', 'nonCritical'],
+        insufficientDataActions: ['critical', 'nonCritical']
+      };
+
+      const functionName = 'func-name';
+      const functionRef = 'func-ref';
+
+      const cf = plugin.getAlarmCloudFormation(alertTopics, definition, functionName, functionRef);
+
+      expect(cf).toEqual({
+        Type: 'AWS::CloudWatch::Alarm',
+        Properties: {
+          AlarmDescription: definition.description,
+          Namespace: definition.namespace,
+          MetricName: definition.metric,
+          Threshold: definition.threshold,
+          Statistic: definition.statistic,
+          Period: definition.period,
+          EvaluationPeriods: definition.evaluationPeriods,
+          ComparisonOperator: definition.comparisonOperator,
+          OKActions: ['critical-ok-topic', 'nonCritical-ok-topic'],
+          AlarmActions: ['critical-alarm-topic', 'nonCritical-alarm-topic'],
+          InsufficientDataActions: ['critical-insufficientData-topic', 'nonCritical-insufficientData-topic'],
+          Dimensions: [{
+            Name: 'FunctionName',
+            Value: {
+              Ref: functionRef,
+            }
+          }],
+          TreatMissingData: 'breaching',
+        }
+      });
+    });
+
+
     it('should use the CloudFormation value ExtendedStatistic for p values', () => {
       const alertTopics = {
         ok: 'ok-topic',