Skip to content
This repository has been archived by the owner on Oct 25, 2023. It is now read-only.

Commit

Permalink
Merge pull request #125 from HeroesDieYoung/master
Browse files Browse the repository at this point in the history
Support for AnomalyDetection Alarms and disabling alarms per-function
  • Loading branch information
adikari authored Jun 8, 2020
2 parents e33608f + b4a1bf2 commit 95eae15
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 55 deletions.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,41 @@ functions:
comparisonOperator: GreaterThanOrEqualToThreshold
```
## Anomaly Detection Alarms
You can create alarms using [CloudWatch AnomalyDetection](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Anomaly_Detection.html) to alarm when data is outside a number of standard deviations of normal, rather than at a static threshold.
When using anomaly detection alarms the threshold property specifies the "Anomaly Detection Threshold" seen in the AWS console.
Default alarms can also be updated to be anomaly detection alarms by adding the `type: anomalyDetection` property.

```yaml
functions:
foo:
handler: foo.handler
alarms:
- name: fooAlarm
type: anomalyDetection
namespace: 'AWS/Lamabda'
metric: Invocations
threshold: 2
statistic: Sum
period: 60
evaluationPeriods: 1
datapointsToAlarm: 1
comparisonOperator: LessThanLowerOrGreaterThanUpperThreshold
bar:
handler: bar.handler
alarms:
- name: functionErrors
threshold: 2
type: anomalyDetection
comparisonOperator: LessThanLowerOrGreaterThanUpperThreshold
- name: functionInvocations
threshold: 2
type: anomalyDetection
comparisonOperator: LessThanLowerOrGreaterThanUpperThreshold
```

## Multiple topic definitions

You can define several topics for alarms. For example you want to have topics for critical alarms
Expand Down Expand Up @@ -284,6 +319,29 @@ definitions:
comparisonOperator: GreaterThanOrEqualToThreshold
treatMissingData: missing
```

## Disabling default alarms for specific functions

Default alarms can be disabled on a per-function basis:

```yaml
custom:
alerts:
alarms:
- functionThrottles
- functionErrors
- functionInvocations
- functionDuration
functions:
bar:
handler: bar.handler
alarms:
- name: functionInvocations
enabled: false
```

## 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}
Expand Down
8 changes: 8 additions & 0 deletions src/defaults/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const lambdaNamespace = 'AWS/Lambda';
module.exports = {
functionInvocations: {
namespace: lambdaNamespace,
enabled: true,
type: 'static',
metric: 'Invocations',
threshold: 100,
statistic: 'Sum',
Expand All @@ -15,6 +17,8 @@ module.exports = {
},
functionErrors: {
namespace: lambdaNamespace,
enabled: true,
type: 'static',
metric: 'Errors',
threshold: 1,
statistic: 'Sum',
Expand All @@ -25,6 +29,8 @@ module.exports = {
},
functionDuration: {
namespace: lambdaNamespace,
enabled: true,
type: 'static',
metric: 'Duration',
threshold: 500,
statistic: 'Average',
Expand All @@ -35,6 +41,8 @@ module.exports = {
},
functionThrottles: {
namespace: lambdaNamespace,
enabled: true,
type: 'static',
metric: 'Throttles',
threshold: 1,
statistic: 'Sum',
Expand Down
123 changes: 91 additions & 32 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,25 @@ class AlertsPlugin {
throw new Error(`Alarm definition ${alarm} does not exist!`);
}

result.push(Object.assign({}, definition, {
name: alarm
}));
result.push(Object.assign(
{
enabled: true,
type: 'static'
},
definition,
{
name: alarm
})
);
} else if (_.isObject(alarm)) {
result.push(_.merge({}, definitions[alarm.name], alarm));
result.push(_.merge(
{
enabled: true,
type: 'static'
},
definitions[alarm.name],
alarm)
);
}

return result;
Expand Down Expand Up @@ -117,24 +131,72 @@ class AlertsPlugin {

const treatMissingData = definition.treatMissingData ? definition.treatMissingData : 'missing';

const alarm = {
Type: 'AWS::CloudWatch::Alarm',
Properties: {
Namespace: namespace,
MetricName: metricId,
AlarmDescription: definition.description,
Threshold: definition.threshold,
Period: definition.period,
EvaluationPeriods: definition.evaluationPeriods,
DatapointsToAlarm: definition.datapointsToAlarm,
ComparisonOperator: definition.comparisonOperator,
OKActions: okActions,
AlarmActions: alarmActions,
InsufficientDataActions: insufficientDataActions,
Dimensions: dimensions,
TreatMissingData: treatMissingData,
const statisticValues = ['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum'];
let alarm;
if (definition.type === 'static') {
alarm = {
Type: 'AWS::CloudWatch::Alarm',
Properties: {
Namespace: namespace,
MetricName: metricId,
AlarmDescription: definition.description,
Threshold: definition.threshold,
Period: definition.period,
EvaluationPeriods: definition.evaluationPeriods,
DatapointsToAlarm: definition.datapointsToAlarm,
ComparisonOperator: definition.comparisonOperator,
OKActions: okActions,
AlarmActions: alarmActions,
InsufficientDataActions: insufficientDataActions,
Dimensions: dimensions,
TreatMissingData: treatMissingData,
}
};

if (_.includes(statisticValues, definition.statistic)) {
alarm.Properties.Statistic = definition.statistic
} else {
alarm.Properties.ExtendedStatistic = definition.statistic
}
};
} else if (definition.type === 'anomalyDetection') {
alarm = {
Type: 'AWS::CloudWatch::Alarm',
Properties: {
AlarmDescription: definition.description,
EvaluationPeriods: definition.evaluationPeriods,
DatapointsToAlarm: definition.datapointsToAlarm,
ComparisonOperator: definition.comparisonOperator,
TreatMissingData: treatMissingData,
OKActions: okActions,
AlarmActions: alarmActions,
InsufficientDataActions: insufficientDataActions,
Metrics: [
{
Id: 'm1',
ReturnData: true,
MetricStat: {
Metric: {
Namespace: namespace,
MetricName: metricId,
Dimensions: dimensions
},
Period: definition.period,
Stat: definition.statistic
}
},
{
Id: 'ad1',
Expression: `ANOMALY_DETECTION_BAND(m1, ${definition.threshold})`,
Label: `${metricId} (expected)`,
ReturnData: true
}
],
ThresholdMetricId: 'ad1'
}
}
} else {
throw new Error(`Missing type for alarm ${alarm.name} on function ${functionName}, must be one of 'static' or 'anomalyDetection'`);
}

if (definition.nameTemplate) {
alarm.Properties.AlarmName = this.naming.getAlarmName({
Expand All @@ -147,13 +209,6 @@ class AlertsPlugin {
stackName
});
}

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;
}

Expand Down Expand Up @@ -278,12 +333,16 @@ class AlertsPlugin {

const alarmStatements = alarms.reduce((statements, alarm) => {
const key = this.naming.getAlarmCloudFormationRef(alarm.name, functionName);
const cf = this.getAlarmCloudFormation(alertTopics, alarm, functionName, normalizedFunctionName);
if (alarm.enabled) {
const cf = this.getAlarmCloudFormation(alertTopics, alarm, functionName, normalizedFunctionName);

statements[key] = cf;
statements[key] = cf;

const logMetricCF = this.getLogMetricCloudFormation(alarm, functionName, normalizedFunctionName, functionObj);
_.merge(statements, logMetricCF);
const logMetricCF = this.getLogMetricCloudFormation(alarm, functionName, normalizedFunctionName, functionObj);
_.merge(statements, logMetricCF);
} else {
delete statements[key];
}

return statements;
}, {});
Expand Down
Loading

1 comment on commit 95eae15

@HeroesDieYoung
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adikari Thanks for the quick review! Any schedule for when the next release will occur to include this change?

Please sign in to comment.