Repository: ACloudGuru/serverless-plugin-aws-alerts
Pull request #36 (2017-07-17 16:59:16)

Set default comparison operators to "GreaterThanOrEqualToThreshold"
## What did you implement:

I changed the default `comparisonOperator` from `GreaterThanThreshold` to `GreaterThanOrEqualToThreshold`. For me, it was counter-intuitive for the `functionErrors` and `functionThrottles` alerts to need 2+ errors or throttles in a given period. It seems as if I string up the monitoring for errors and throttles, I'd be concerned about _any_ errors or throttles, or I would set a higher threshold if I had different requirements.

I changed the defaults for `functionInvocations` and `functionDuration` as well just to make it consistent across them. Since they have higher thresholds, it's unlikely to have as much of an effect -- the difference between 100 and 101 invocations is much different than the difference between 0 and 1 errors.

Feel free to ignore and close if you disagree with these new defaults 😄. It was surprising to me, but perhaps I'm an outlier. Either way, thanks for this plugin -- very helpful!




Patch:
diff --git a/README.md b/README.md
index e270906..144cf38 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ custom:
         statistic: Average
         period: 300
         evaluationPeriods: 1
-        comparisonOperator: GreaterThanThreshold
+        comparisonOperator: GreaterThanOrEqualToThreshold
     alarms:
       - functionThrottles
       - functionErrors
@@ -62,7 +62,7 @@ functions:
         statistic: Minimum
         period: 60
         evaluationPeriods: 1
-        comparisonOperator: GreaterThanThreshold
+        comparisonOperator: GreaterThanOrEqualToThreshold
 ```
 
 ## SNS Topics
@@ -133,7 +133,7 @@ definitions:
     statistic: Sum
     period: 60
     evaluationPeriods: 1
-    comparisonOperator: GreaterThanThreshold
+    comparisonOperator: GreaterThanOrEqualToThreshold
     treatMissingData: missing
   functionErrors:
     namespace: 'AWS/Lambda'
@@ -142,7 +142,7 @@ definitions:
     statistic: Maximum
     period: 60
     evaluationPeriods: 1
-    comparisonOperator: GreaterThanThreshold
+    comparisonOperator: GreaterThanOrEqualToThreshold
     treatMissingData: missing
   functionDuration:
     namespace: 'AWS/Lambda'
@@ -151,7 +151,7 @@ definitions:
     statistic: Maximum
     period: 60
     evaluationPeriods: 1
-    comparisonOperator: GreaterThanThreshold
+    comparisonOperator: GreaterThanOrEqualToThreshold
     treatMissingData: missing
   functionThrottles:
     namespace: 'AWS/Lambda'
@@ -160,6 +160,23 @@ definitions:
     statistic: Sum
     period: 60
     evaluationPeriods: 1
+    comparisonOperator: GreaterThanOrEqualToThreshold
+    treatMissingData: missing
+```
+
+## Using Percentile Statistic for a Metric
+
+Statistic not only supports SampleCount, Average, Sum, Minimum or Maximum as defined in CloudFormation [here](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cw-alarm.html#cfn-cloudwatch-alarms-statistic), but also percentiles. This is possible by leveraging  [ExtendedStatistic](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cw-alarm.html#cfn-cloudwatch-alarms-extendedstatistic) under the hood. This plugin will automatically choose the correct key for you. See an example below:
+
+```yaml
+definitions:
+  functionDuration:
+    namespace: 'AWS/Lambda'
+    metric: Duration
+    threshold: 100
+    statistic: 'p95'
+    period: 60
+    evaluationPeriods: 1
     comparisonOperator: GreaterThanThreshold
     treatMissingData: missing
 ```
diff --git a/src/defaults/definitions.js b/src/defaults/definitions.js
index 11eefcc..c38a34e 100644
--- a/src/defaults/definitions.js
+++ b/src/defaults/definitions.js
@@ -10,7 +10,7 @@ module.exports = {
     statistic: 'Sum',
     period: 60,
     evaluationPeriods: 1,
-    comparisonOperator: 'GreaterThanThreshold',
+    comparisonOperator: 'GreaterThanOrEqualToThreshold',
   },
   functionErrors: {
     namespace: lambdaNamespace,
@@ -19,7 +19,7 @@ module.exports = {
     statistic: 'Sum',
     period: 60,
     evaluationPeriods: 1,
-    comparisonOperator: 'GreaterThanThreshold',
+    comparisonOperator: 'GreaterThanOrEqualToThreshold',
   },
   functionDuration: {
     namespace: lambdaNamespace,
@@ -28,7 +28,7 @@ module.exports = {
     statistic: 'Average',
     period: 60,
     evaluationPeriods: 1,
-    comparisonOperator: 'GreaterThanThreshold',
+    comparisonOperator: 'GreaterThanOrEqualToThreshold',
   },
   functionThrottles: {
     namespace: lambdaNamespace,
@@ -37,6 +37,6 @@ module.exports = {
     statistic: 'Sum',
     period: 60,
     evaluationPeriods: 1,
-    comparisonOperator: 'GreaterThanThreshold',
+    comparisonOperator: 'GreaterThanOrEqualToThreshold',
   }
 };
diff --git a/src/index.js b/src/index.js
index b31ab41..be5c355 100644
--- a/src/index.js
+++ b/src/index.js
@@ -107,14 +107,13 @@ class Plugin {
 
     const treatMissingData = definition.treatMissingData ? definition.treatMissingData : 'missing';
 
-    return {
+    const alarm = {
       Type: 'AWS::CloudWatch::Alarm',
       Properties: {
         Namespace: namespace,
         MetricName: metricName,
         AlarmDescription: definition.description,
         Threshold: definition.threshold,
-        Statistic: definition.statistic,
         Period: definition.period,
         EvaluationPeriods: definition.evaluationPeriods,
         ComparisonOperator: definition.comparisonOperator,
@@ -125,6 +124,14 @@ class Plugin {
         TreatMissingData: treatMissingData,
       }
     };
+
+    const statisticValues = [ 'SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum'];
+    if (_.includes(statisticValues, definition.statistic)) {
+      alarm.Properties.Statistic = definition.statistic
+    } else {
+      alarm.Properties.ExtendedStatistic = definition.statistic
+    }
+    return alarm;
   }
 
   getSnsTopicCloudFormation(topicName, notifications) {


Test patch:
diff --git a/src/index.test.js b/src/index.test.js
index 25c3ed3..a7eac59 100644
--- a/src/index.test.js
+++ b/src/index.test.js
@@ -158,7 +158,7 @@ describe('#index', function () {
             statistic: 'Maximum',
             period: 300,
             evaluationPeriods: 1,
-            comparisonOperator: 'GreaterThanThreshold',
+            comparisonOperator: 'GreaterThanOrEqualToThreshold',
           },
           customDefinition: {
             namespace: 'AWS/Lambda',
@@ -167,7 +167,7 @@ describe('#index', function () {
             statistic: 'Minimum',
             period: 120,
             evaluationPeriods: 2,
-            comparisonOperator: 'GreaterThanThreshold',
+            comparisonOperator: 'GreaterThanOrEqualToThreshold',
           }
         }
       };
@@ -183,7 +183,7 @@ describe('#index', function () {
           statistic: 'Sum',
           period: 60,
           evaluationPeriods: 1,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         },
         functionErrors: {
           namespace: 'AWS/Lambda',
@@ -192,7 +192,7 @@ describe('#index', function () {
           statistic: 'Maximum',
           period: 300,
           evaluationPeriods: 1,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         },
         functionDuration: {
           namespace: 'AWS/Lambda',
@@ -201,7 +201,7 @@ describe('#index', function () {
           statistic: 'Average',
           period: 60,
           evaluationPeriods: 1,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         },
         functionThrottles: {
           namespace: 'AWS/Lambda',
@@ -210,7 +210,7 @@ describe('#index', function () {
           statistic: 'Sum',
           period: 60,
           evaluationPeriods: 1,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         },
         customDefinition: {
           namespace: 'AWS/Lambda',
@@ -219,7 +219,7 @@ describe('#index', function () {
           statistic: 'Minimum',
           period: 120,
           evaluationPeriods: 2,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         }
       });
     });
@@ -235,7 +235,7 @@ describe('#index', function () {
           statistic: 'Minimum',
           period: 120,
           evaluationPeriods: 2,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         }
       },
       global: ['functionThrottles'],
@@ -279,7 +279,7 @@ describe('#index', function () {
         statistic: 'Minimum',
         period: 120,
         evaluationPeriods: 2,
-        comparisonOperator: 'GreaterThanThreshold',
+        comparisonOperator: 'GreaterThanOrEqualToThreshold',
       }]);
     });
 
@@ -295,7 +295,7 @@ describe('#index', function () {
           statistic: 'Minimum',
           period: 120,
           evaluationPeriods: 2,
-          comparisonOperator: 'GreaterThanThreshold',
+          comparisonOperator: 'GreaterThanOrEqualToThreshold',
         }]
       }, config, definitions);
 
@@ -307,7 +307,7 @@ describe('#index', function () {
         statistic: 'Minimum',
         period: 120,
         evaluationPeriods: 2,
-        comparisonOperator: 'GreaterThanThreshold',
+        comparisonOperator: 'GreaterThanOrEqualToThreshold',
       }]);
     });
 
@@ -432,7 +432,7 @@ describe('#index', function () {
             Statistic: 'Sum',
             Period: 60,
             EvaluationPeriods: 1,
-            ComparisonOperator: 'GreaterThanThreshold',
+            ComparisonOperator: 'GreaterThanOrEqualToThreshold',
             AlarmActions: [],
             OKActions: [],
             InsufficientDataActions: [],
@@ -456,7 +456,7 @@ describe('#index', function () {
             statistic: 'Sum',
             period: 60,
             evaluationPeriods: 1,
-            comparisonOperator: 'GreaterThanThreshold',
+            comparisonOperator: 'GreaterThanOrEqualToThreshold',
             pattern: '{$.level > 40}'
           }
         },
@@ -480,7 +480,7 @@ describe('#index', function () {
             Statistic: 'Sum',
             Period: 60,
             EvaluationPeriods: 1,
-            ComparisonOperator: 'GreaterThanThreshold',
+            ComparisonOperator: 'GreaterThanOrEqualToThreshold',
             OKActions: [],
             AlarmActions: [],
             InsufficientDataActions: [],
@@ -630,7 +630,7 @@ describe('#index', function () {
         statistic: 'Maximum',
         period: 300,
         evaluationPeriods: 1,
-        comparisonOperator: 'GreaterThanThreshold',
+        comparisonOperator: 'GreaterThanOrEqualToThreshold',
         treatMissingData: 'breaching',
       };
 
@@ -662,5 +662,53 @@ describe('#index', function () {
         }
       });
     });
+
+    it('should user the CloudFormation value ExtendedStatistic for p values', () => {
+      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',
+      };
+
+      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: 'FunctionName',
+            Value: {
+              Ref: functionRef,
+            }
+          }],
+          TreatMissingData: 'breaching',
+        }
+      });
+    });
   })
 });