Repository: ACloudGuru/serverless-plugin-aws-alerts
Pull request #106 (2019-09-02 15:36:57)

Add omitDefautlDimension param
## What did you implement:

Support a `omitDefaultDimension` parameter at the `alarm` level to avoid the `FunctionName` dimension that's added by default. This will allow the creation of alarms that aren't under the `AWS/Lambda` (API Gateway or SQS for example), which don't require the `FunctionName` dimension. This would address the following issues:
- #104
- #85 
- #19 

## How can we verify it:

Given that this is a relatively trivial change there's not much to verify. However, if you want to try creating an API Gateway alert you can use the following yaml configuration:
```yaml
 - name: function5XX
        description: '5XX response codes'
        namespace: 'AWS/ApiGateway'
        metric: 5XXError
        omitDefaultDimension: true
        dimensions:
          - Name: ApiName
            Value: api
          - Name: Resource
            Value: /api/endpoint
          - Name: Method
            Value: POST
          - Name: Stage
            Value: prod
        threshold: 5
        statistic: Sum
        period: 60
        evaluationPeriods: 1
        datapointsToAlarm: 1
        comparisonOperator: GreaterThanOrEqualToThreshold
```

If you check the alarm in CloudWatch you'll see that it doesn't include the `FunctionName` dimension.

## Todos:

- [X] Write tests
- [X] Write documentation
- [X] Fix linting errors
- [X] Provide verification config/commands/resources




Patch:
diff --git a/README.md b/README.md
index 1dafea1..1105a08 100644
--- a/README.md
+++ b/README.md
@@ -286,10 +286,9 @@ definitions:
 ```
 ## Additional dimensions
 
-The plugin allows users to provide custom dimensions for the alarm. Dimensions are provided in a list of key/value pairs {Name: foo, Value:bar} 
-The plugin will always apply dimension of {Name: FunctionName, Value: ((FunctionName))}
- For example:
- 
+The plugin allows users to provide custom dimensions for the alarm. Dimensions are provided in a list of key/value pairs {Name: foo, Value:bar}
+The plugin will always apply dimension of {Name: FunctionName, Value: ((FunctionName))}, except if the parameter `omitDefaultDimension: true` is passed. For example:
+
 ```yaml
     alarms: # merged with function alarms
       - name: fooAlarm
@@ -300,6 +299,7 @@ The plugin will always apply dimension of {Name: FunctionName, Value: ((Function
         period: 60
         evaluationPeriods: 1
         comparisonOperator: GreaterThanThreshold
+        omitDefaultDimension: true
         dimensions:
           -  Name: foo
              Value: bar
diff --git a/package.json b/package.json
index b4c7447..0a6f25f 100644
--- a/package.json
+++ b/package.json
@@ -38,4 +38,4 @@
       "<rootDir>/src/**/*.test.js"
     ]
   }
-}
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index c2bcb8a..f0f8f9c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -92,18 +92,17 @@ class AlertsPlugin {
 
 
     if (definition.okActions) {
-      definition.okActions.map( alertTopic => {okActions.push(alertTopics[alertTopic].ok)});
+      definition.okActions.map(alertTopic => { okActions.push(alertTopics[alertTopic].ok) });
     }
 
     if (definition.alarmActions) {
-      definition.alarmActions.map( alertTopic => {alarmActions.push(alertTopics[alertTopic].alarm)});
+      definition.alarmActions.map(alertTopic => { alarmActions.push(alertTopics[alertTopic].alarm) });
     }
 
     if (definition.insufficientDataActions) {
-      definition.insufficientDataActions.map( alertTopic => {insufficientDataActions.push(alertTopics[alertTopic].insufficientData)});
+      definition.insufficientDataActions.map(alertTopic => { insufficientDataActions.push(alertTopics[alertTopic].insufficientData) });
     }
 
-
     const stackName = this.awsProvider.naming.getStackName();
 
     const namespace = definition.pattern ?
@@ -114,7 +113,7 @@ class AlertsPlugin {
       this.naming.getPatternMetricName(definition.metric, functionRef) :
       definition.metric;
 
-    const dimensions = definition.pattern ? []: this.naming.getDimensionsList(definition.dimensions, functionRef);
+    const dimensions = definition.pattern ? [] : this.naming.getDimensionsList(definition.dimensions, functionRef, definition.omitDefaultDimension);
 
     const treatMissingData = definition.treatMissingData ? definition.treatMissingData : 'missing';
 
@@ -149,7 +148,7 @@ class AlertsPlugin {
       });
     }
 
-    const statisticValues = [ 'SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum'];
+    const statisticValues = ['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum'];
     if (_.includes(statisticValues, definition.statistic)) {
       alarm.Properties.Statistic = definition.statistic
     } else {
@@ -188,16 +187,16 @@ class AlertsPlugin {
         const cfRef = `AwsAlerts${customAlarmName ? _.upperFirst(customAlarmName) : ''}${_.upperFirst(key)}`;
         if (customAlarmName) {
           if (!alertTopics[customAlarmName]) {
-          alertTopics[customAlarmName] = {}
-                }
+            alertTopics[customAlarmName] = {}
+          }
           alertTopics[customAlarmName][key] = {
             Ref: cfRef
           };
-              } else {
+        } else {
           alertTopics[key] = {
             Ref: cfRef
           };
-              }
+        }
 
         this.addCfResources({
           [cfRef]: this.getSnsTopicCloudFormation(topic, notifications),
@@ -212,11 +211,11 @@ class AlertsPlugin {
     if (config.topics) {
       Object.keys(config.topics).forEach((key) => {
         if (['ok', 'alarm', 'insufficientData'].indexOf(key) !== -1) {
-      this._addAlertTopic(key, config.topics, alertTopics)
+          this._addAlertTopic(key, config.topics, alertTopics)
         } else {
-      Object.keys(config.topics[key]).forEach((subkey) => {
-        this._addAlertTopic(subkey, config.topics[key], alertTopics, key)
-      })
+          Object.keys(config.topics[key]).forEach((subkey) => {
+            this._addAlertTopic(subkey, config.topics[key], alertTopics, key)
+          })
         }
       });
     }
@@ -318,7 +317,7 @@ class AlertsPlugin {
 
     const cf = _.chain(dashboardTemplates)
       .uniq()
-      .reduce( (acc, d) => {
+      .reduce((acc, d) => {
         const dashboard = dashboards.createDashboard(service.service, stage, region, functions, d);
 
         const cfResource = d === 'default'
diff --git a/src/naming.js b/src/naming.js
index 809b631..5bd45fa 100644
--- a/src/naming.js
+++ b/src/naming.js
@@ -23,17 +23,19 @@ class Naming {
     return `${_.upperFirst(metricName)}${functionName}`;
   }
 
-  getDimensionsList(dimensionsList, funcRef) {
-    let funcNameDimension =  {
+  getDimensionsList(dimensionsList, funcRef, omitDefaultDimension) {
+    if (omitDefaultDimension) {
+      return dimensionsList || []
+    }
+
+    let funcNameDimension = {
       'Name': 'FunctionName',
       'Value': {
         Ref: funcRef
       }
     };
-    if(dimensionsList == null) {
-      return [funcNameDimension];
-    }
-    let filteredDimensions = dimensionsList.filter( (dim) => {
+
+    let filteredDimensions = (dimensionsList || []).filter((dim) => {
       return dim.Name != 'FunctionName'
     })
     filteredDimensions.push(funcNameDimension);


Test patch:
diff --git a/src/naming.test.js b/src/naming.test.js
index 8c6588a..351c939 100644
--- a/src/naming.test.js
+++ b/src/naming.test.js
@@ -38,17 +38,30 @@ describe('#naming', function () {
 
   describe('#getDimensionsMap', () => {
     let naming = null;
-    beforeEach( () => naming = new Naming());
+    beforeEach(() => naming = new Naming());
 
     it('should use function name derived from funcref', () => {
-      const expected = [{"Name":"Duck", "Value":"QUACK"}, {"Name":"FunctionName", "Value": {'Ref': 'funcName'}}]
-      const actual = naming.getDimensionsList([{'Name':'FunctionName', 'Value':'overridden'},{'Name':'Duck', 'Value':'QUACK'}], 'funcName')
+      const expected = [{ "Name": "Duck", "Value": "QUACK" }, { "Name": "FunctionName", "Value": { 'Ref': 'funcName' } }]
+      const actual = naming.getDimensionsList([{ 'Name': 'FunctionName', 'Value': 'overridden' }, { 'Name': 'Duck', 'Value': 'QUACK' }], 'funcName')
+      expect(actual).toEqual(expected);
+    });
+
+    it('should use function name derived from funcref when dimensions are undefined', () => {
+      const expected = [{ "Name": "FunctionName", "Value": { 'Ref': 'funcName' } }]
+      const actual = naming.getDimensionsList(undefined, 'funcName')
       expect(actual).toEqual(expected);
     });
 
+
     it('should get a mapped dimensions object when FunctionName is missing', () => {
-      const expected = [{"Name":"Duck", "Value":"QUACK"}, {"Name":"FunctionName", "Value":{'Ref': 'funcName'}}]
-      const actual = naming.getDimensionsList([{'Name':'Duck', 'Value':'QUACK'}], 'funcName');
+      const expected = [{ "Name": "Duck", "Value": "QUACK" }, { "Name": "FunctionName", "Value": { 'Ref': 'funcName' } }]
+      const actual = naming.getDimensionsList([{ 'Name': 'Duck', 'Value': 'QUACK' }], 'funcName');
+      expect(actual).toEqual(expected);
+    });
+
+    it('should not include FunctionName when omitFunctionNameDimension is true', () => {
+      const expected = [{ "Name": "Duck", "Value": "QUACK" }]
+      const actual = naming.getDimensionsList([{ 'Name': 'Duck', 'Value': 'QUACK' }], 'funcName', true);
       expect(actual).toEqual(expected);
     });
   });