Repository: ACloudGuru/serverless-plugin-aws-alerts (js)

Issue:
Ability to create alarm on a dimension of a metric
# This is a Feature Proposal

## Description
I collect metric data on types of transactions. The types are separated by name and version. These are defined as dimensions on the metric.

I want to alarm on if a particular name/version combination occurs. This seems to be possible through the aws cloudwatch cli. I see references to the dimensions flag in your code, but I can't locate any documentation on it being exposed through the plugin or how to use it if it is.


Pull request description:
#78 Ability to create alarm on a dimension of a metric
## What did you implement:

Closes #78
Ability to create alarm on a dimension of a metric

## How did you implement it:

Adding the ability to parse a list of dimensions provided in the serverless yml for an alert. The FunctionName dimension will always be added by default, and be taken from the built cloud formation template.

## How can we verify it:

Run serverless deploy with dimensions list of key/values like mentioned in the README update. The generated alarm should be tagged with your custom dimensions in addition to the FunctionName

```yml
        dimensions:
          -  Name: foo
             Value: bar
```

## 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 fe6934b..9306f17 100644
--- a/README.md
+++ b/README.md
@@ -209,6 +209,35 @@ definitions:
     comparisonOperator: GreaterThanOrEqualToThreshold
     treatMissingData: missing
 ```
+## 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:
+ 
+```yaml
+    alarms: # merged with function alarms
+      - name: fooAlarm
+        namespace: 'AWS/Lambda'
+        metric: errors # define custom metrics here
+        threshold: 1
+        statistic: Minimum
+        period: 60
+        evaluationPeriods: 1
+        comparisonOperator: GreaterThanThreshold
+        dimensions:
+          -  Name: foo
+             Value: bar
+```
+
+```json
+'Dimensions': [
+                {
+                    'Name': 'foo',
+                    'Value': 'bar'
+                },
+            ]
+```
 
 ## Using Percentile Statistic for a Metric
 
diff --git a/src/index.js b/src/index.js
index ffad5bf..5902ad4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -98,12 +98,7 @@ class AlertsPlugin {
       this.naming.getPatternMetricName(definition.metric, functionRef) :
       definition.metric;
 
-    const dimensions = definition.pattern ? [] : [{
-      Name: 'FunctionName',
-      Value: {
-        Ref: functionRef,
-      }
-    }];
+    const dimensions = definition.pattern ? []: this.naming.getDimensionsList(definition.dimensions, functionRef);
 
     const treatMissingData = definition.treatMissingData ? definition.treatMissingData : 'missing';
 
diff --git a/src/naming.js b/src/naming.js
index cea9320..3e5a5b3 100644
--- a/src/naming.js
+++ b/src/naming.js
@@ -23,6 +23,23 @@ class Naming {
     return `${_.upperFirst(metricName)}${functionName}`;
   }
 
+  getDimensionsList(dimensionsList, funcRef) {
+    let funcNameDimension =  {
+      'Name': 'FunctionName',
+      'Value': {
+        Ref: funcRef
+      }
+    };
+    if(dimensionsList == null) {
+      return [funcNameDimension];
+    }
+    let filteredDimensions = dimensionsList.filter( (dim) => {
+      return dim.Name != 'FunctionName'
+    })
+    filteredDimensions.push(funcNameDimension);
+    return filteredDimensions
+  }
+
 }
 
 module.exports = Naming;


Test patch:
diff --git a/src/index.test.js b/src/index.test.js
index 51ef4a8..cc42055 100644
--- a/src/index.test.js
+++ b/src/index.test.js
@@ -754,5 +754,59 @@ describe('#index', function () {
         }
       });
     });
+    it('should allow user to provide custom dimensions', () => {
+      const alertTopics = {
+        ok: 'ok-topic',
+        alarm: 'alarm-topic',
+        insufficientData: 'insufficientData-topic',
+      };
+
+      const definition = {
+        description: 'An error alarm',
+        namespace: 'AWS/Lambda',
+        metric: 'Errors',
+        threshold: 1,
+        statistic: 'p95',
+        period: 300,
+        evaluationPeriods: 1,
+        comparisonOperator: 'GreaterThanThreshold',
+        treatMissingData: 'breaching',
+        dimensions: [{'Name':'Cow', 'Value':'MOO'}, {'Name':'Duck', 'Value':'QUACK'}]
+      };
+
+      const functionRef = 'func-ref';
+
+      const cf = plugin.getAlarmCloudFormation(alertTopics, definition, functionRef);
+
+      expect(cf).toEqual({
+        Type: 'AWS::CloudWatch::Alarm',
+        Properties: {
+          AlarmDescription: definition.description,
+          Namespace: definition.namespace,
+          MetricName: definition.metric,
+          Threshold: definition.threshold,
+          ExtendedStatistic: definition.statistic,
+          Period: definition.period,
+          EvaluationPeriods: definition.evaluationPeriods,
+          ComparisonOperator: definition.comparisonOperator,
+          OKActions: ['ok-topic'],
+          AlarmActions: ['alarm-topic'],
+          InsufficientDataActions: ['insufficientData-topic'],
+          Dimensions: [{
+            Name: "Cow",
+            Value: "MOO"
+            },{
+            Name: "Duck",
+            Value: "QUACK"
+            },{
+            Name: 'FunctionName',
+            Value: {
+              Ref: functionRef,
+            }
+          }],
+          TreatMissingData: 'breaching',
+        }
+      });
+    });    
   })
 });
diff --git a/src/naming.test.js b/src/naming.test.js
index 6072de7..4c93d05 100644
--- a/src/naming.test.js
+++ b/src/naming.test.js
@@ -35,4 +35,21 @@ describe('#naming', function () {
       expect(actual).toEqual(expected);
     });
   });
+
+  describe('#getDimensionsMap', () => {
+    let naming = null;
+    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')
+      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');
+      expect(actual).toEqual(expected);
+    });
+  });
 });