Skip to content

Commit 18a448b

Browse files
authored
Merge pull request #172 from theburningmonk/feature/make_alarm_missing_data_configurable
Feature/make alarm missing data configurable
2 parents 49fafe8 + dada834 commit 18a448b

File tree

3 files changed

+163
-19
lines changed

3 files changed

+163
-19
lines changed

README.md

+31-12
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ stepFunctions:
5757
- executionsTimeOut
5858
- executionsFailed
5959
- executionsAborted
60-
- executionThrottled
60+
- metric: executionThrottled
61+
treatMissingData: breaching # overrides below default
62+
treatMissingData: ignore # optional
6163
hellostepfunc2:
6264
definition:
6365
StartAt: HelloWorld2
@@ -70,16 +72,6 @@ stepFunctions:
7072
- DynamoDBTable
7173
- KinesisStream
7274
- CUstomIamRole
73-
alarms:
74-
topics:
75-
ok: arn:aws:sns:us-east-1:1234567890:NotifyMe
76-
alarm: arn:aws:sns:us-east-1:1234567890:NotifyMe
77-
insufficientData: arn:aws:sns:us-east-1:1234567890:NotifyMe
78-
metrics:
79-
- executionsTimeOut
80-
- executionsFailed
81-
- executionsAborted
82-
- executionThrottled
8375
activities:
8476
- myTask
8577
- yourTask
@@ -172,10 +164,20 @@ stepFunctions:
172164
- executionsFailed
173165
- executionsAborted
174166
- executionThrottled
167+
treatMissingData: missing
175168
```
176169

177170
Both `topics` and `metrics` are required properties. There are 4 supported metrics, each map to the CloudWatch Metrics that Step Functions publishes for your executions.
178171

172+
You can configure how the CloudWatch Alarms should treat missing data:
173+
174+
* `missing` (AWS default): The alarm does not consider missing data points when evaluating whether to change state.
175+
* `ignore`: The current alarm state is maintained.
176+
* `breaching`: Missing data points are treated as breaching the threshold.
177+
* `notBreaching`: Missing data points are treated as being within the threshold.
178+
179+
For more information, please refer to the [official documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-and-missing-data).
180+
179181
The generated CloudWatch alarms would have the following configurations:
180182
```yaml
181183
namespace: 'AWS/States'
@@ -185,12 +187,29 @@ period: 60
185187
evaluationPeriods: 1
186188
ComparisonOperator: GreaterThanOrEqualToThreshold
187189
Statistic: Sum
188-
treatMissingData: missing
190+
treatMissingData: <missing (default) | ignore | breaching | notBreaching>
189191
Dimensions:
190192
- Name: StateMachineArn
191193
Value: <ArnOfTheStateMachine>
192194
```
193195

196+
You can also override the default `treatMissingData` setting for a particular alarm by specifying an override:
197+
198+
```yml
199+
alarms:
200+
topics:
201+
ok: arn:aws:sns:us-east-1:1234567890:NotifyMe
202+
alarm: arn:aws:sns:us-east-1:1234567890:NotifyMe
203+
insufficientData: arn:aws:sns:us-east-1:1234567890:NotifyMe
204+
metrics:
205+
- executionsTimeOut
206+
- executionsFailed
207+
- executionsAborted
208+
- metric: executionThrottled
209+
treatMissingData: breaching # override
210+
treatMissingData: ignore # default
211+
```
212+
194213
#### Current Gotcha
195214
Please keep this gotcha in mind if you want to reference the `name` from the `resources` section. To generate Logical ID for CloudFormation, the plugin transforms the specified name in serverless.yml based on the following scheme.
196215

lib/deploy/stepFunctions/compileAlarms.js

+20-7
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ function getCloudWatchAlarms(
2424
const alarmActions = alarmAction ? [alarmAction] : [];
2525
const insufficientDataAction = _.get(alarmsObj, 'topics.insufficientData');
2626
const insufficientDataActions = insufficientDataAction ? [insufficientDataAction] : [];
27+
const defaultTreatMissingData = _.get(alarmsObj, 'treatMissingData', 'missing');
2728

2829
const metrics = _.uniq(_.get(alarmsObj, 'metrics', []));
29-
const [valid, invalid] = _.partition(metrics, m => _.has(cloudWatchMetricNames, m));
30+
const [valid, invalid] = _.partition(
31+
metrics,
32+
m => _.has(cloudWatchMetricNames, _.get(m, 'metric', m)));
3033

3134
if (!_.isEmpty(invalid)) {
3235
serverless.cli.consoleLog(
@@ -37,18 +40,21 @@ function getCloudWatchAlarms(
3740
}
3841

3942
return valid.map(metric => {
40-
const MetricName = cloudWatchMetricNames[metric];
43+
// metric can be either a string or object
44+
const metricName = _.get(metric, 'metric', metric);
45+
const cloudWatchMetricName = cloudWatchMetricNames[metricName];
4146
const AlarmDescription =
42-
`${stateMachineName}[${stage}][${region}]: ${alarmDescriptions[metric]}`;
43-
const logicalId = `${stateMachineLogicalId}${MetricName}Alarm`;
47+
`${stateMachineName}[${stage}][${region}]: ${alarmDescriptions[metricName]}`;
48+
const logicalId = `${stateMachineLogicalId}${cloudWatchMetricName}Alarm`;
49+
const treatMissingData = _.get(metric, 'treatMissingData', defaultTreatMissingData);
4450

4551
return {
4652
logicalId,
4753
alarm: {
4854
Type: 'AWS::CloudWatch::Alarm',
4955
Properties: {
5056
Namespace: 'AWS/States',
51-
MetricName,
57+
MetricName: cloudWatchMetricName,
5258
AlarmDescription,
5359
Threshold: 1,
5460
Period: 60,
@@ -58,7 +64,7 @@ function getCloudWatchAlarms(
5864
OKActions: okActions,
5965
AlarmActions: alarmActions,
6066
InsufficientDataActions: insufficientDataActions,
61-
TreatMissingData: 'missing',
67+
TreatMissingData: treatMissingData,
6268
Dimensions: [
6369
{
6470
Name: 'StateMachineArn',
@@ -79,9 +85,16 @@ function validateConfig(serverless, stateMachineName, alarmsObj) {
7985
return false;
8086
}
8187

88+
// metrics can be either short form (e.g. "executionsTimeOut") or
89+
// long form, which allows you to optionally specify treatMissingData override, e.g.
90+
// { "metric": "executionsTimeOut", "treatMissingData": "ignore" }
91+
const validateMetric = x =>
92+
_.isString(x) ||
93+
(_.isObject(x) && _.has(x, 'metric') && _.isString(x.metric));
94+
8295
if (!_.isObject(alarmsObj.topics) ||
8396
!_.isArray(alarmsObj.metrics) ||
84-
!_.every(alarmsObj.metrics, _.isString)) {
97+
!_.every(alarmsObj.metrics, validateMetric)) {
8598
serverless.cli.consoleLog(
8699
`state machine [${stateMachineName}] : alarms config is malformed. ` +
87100
'Please see https://github.com/horike37/serverless-step-functions for examples');

lib/deploy/stepFunctions/compileAlarms.test.js

+112
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,116 @@ describe('#compileAlarms', () => {
268268

269269
expect(consoleLogSpy.callCount).equal(2);
270270
});
271+
272+
it('should use specified treatMissingData for all alarms', () => {
273+
const genStateMachine = (name) => ({
274+
name,
275+
definition: {
276+
StartAt: 'A',
277+
States: {
278+
A: {
279+
Type: 'Pass',
280+
End: true,
281+
},
282+
},
283+
},
284+
alarms: {
285+
topics: {
286+
ok: '${self:service}-${opt:stage}-alerts-ok',
287+
alarm: '${self:service}-${opt:stage}-alerts-alarm',
288+
insufficientData: '${self:service}-${opt:stage}-alerts-missing',
289+
},
290+
metrics: [
291+
'executionsTimeOut',
292+
'executionsFailed',
293+
'executionsAborted',
294+
'executionThrottled',
295+
],
296+
treatMissingData: 'ignore',
297+
},
298+
});
299+
300+
serverless.service.stepFunctions = {
301+
stateMachines: {
302+
myStateMachine1: genStateMachine('stateMachineBeta1'),
303+
myStateMachine2: genStateMachine('stateMachineBeta2'),
304+
},
305+
};
306+
307+
serverlessStepFunctions.compileAlarms();
308+
const resources = serverlessStepFunctions.serverless.service
309+
.provider.compiledCloudFormationTemplate.Resources;
310+
311+
const verify = (resourceName) => {
312+
expect(resources).to.have.property(resourceName);
313+
expect(resources[resourceName].Properties.TreatMissingData).to.equal('ignore');
314+
};
315+
316+
verify('StateMachineBeta1ExecutionsTimeOutAlarm');
317+
verify('StateMachineBeta1ExecutionsFailedAlarm');
318+
verify('StateMachineBeta1ExecutionsAbortedAlarm');
319+
verify('StateMachineBeta1ExecutionThrottledAlarm');
320+
verify('StateMachineBeta2ExecutionsTimeOutAlarm');
321+
verify('StateMachineBeta2ExecutionsFailedAlarm');
322+
verify('StateMachineBeta2ExecutionsAbortedAlarm');
323+
verify('StateMachineBeta2ExecutionThrottledAlarm');
324+
325+
expect(consoleLogSpy.callCount).equal(0);
326+
});
327+
328+
it('should allow individual alarms to override default treatMissingData', () => {
329+
const genStateMachine = (name) => ({
330+
name,
331+
definition: {
332+
StartAt: 'A',
333+
States: {
334+
A: {
335+
Type: 'Pass',
336+
End: true,
337+
},
338+
},
339+
},
340+
alarms: {
341+
topics: {
342+
ok: '${self:service}-${opt:stage}-alerts-ok',
343+
alarm: '${self:service}-${opt:stage}-alerts-alarm',
344+
insufficientData: '${self:service}-${opt:stage}-alerts-missing',
345+
},
346+
metrics: [
347+
'executionsTimeOut',
348+
{ metric: 'executionsFailed', treatMissingData: 'breaching' },
349+
'executionsAborted',
350+
'executionThrottled',
351+
],
352+
treatMissingData: 'ignore',
353+
},
354+
});
355+
356+
serverless.service.stepFunctions = {
357+
stateMachines: {
358+
myStateMachine1: genStateMachine('stateMachineBeta1'),
359+
myStateMachine2: genStateMachine('stateMachineBeta2'),
360+
},
361+
};
362+
363+
serverlessStepFunctions.compileAlarms();
364+
const resources = serverlessStepFunctions.serverless.service
365+
.provider.compiledCloudFormationTemplate.Resources;
366+
367+
const verify = (resourceName, expectedConfig = 'ignore') => {
368+
expect(resources).to.have.property(resourceName);
369+
expect(resources[resourceName].Properties.TreatMissingData).to.equal(expectedConfig);
370+
};
371+
372+
verify('StateMachineBeta1ExecutionsTimeOutAlarm');
373+
verify('StateMachineBeta1ExecutionsFailedAlarm', 'breaching');
374+
verify('StateMachineBeta1ExecutionsAbortedAlarm');
375+
verify('StateMachineBeta1ExecutionThrottledAlarm');
376+
verify('StateMachineBeta2ExecutionsTimeOutAlarm');
377+
verify('StateMachineBeta2ExecutionsFailedAlarm', 'breaching');
378+
verify('StateMachineBeta2ExecutionsAbortedAlarm');
379+
verify('StateMachineBeta2ExecutionThrottledAlarm');
380+
381+
expect(consoleLogSpy.callCount).equal(0);
382+
});
271383
});

0 commit comments

Comments
 (0)