Skip to content

Commit cdac2a3

Browse files
- added support for declaring CW alarms
1 parent 4f35201 commit cdac2a3

File tree

4 files changed

+365
-1
lines changed

4 files changed

+365
-1
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
const _ = require('lodash');
3+
const BbPromise = require('bluebird');
4+
5+
const cloudWatchMetricNames = {
6+
executionsTimeOut: 'ExecutionsTimeOut',
7+
executionsFailed: 'ExecutionsFailed',
8+
executionsAborted: 'ExecutionsAborted',
9+
executionThrottled: 'ExecutionThrottled',
10+
};
11+
12+
const alarmDescriptions = {
13+
executionsTimeOut: 'executions timed out',
14+
executionsFailed: 'executions failed',
15+
executionsAborted: 'executions were aborted',
16+
executionThrottled: 'execution were throttled',
17+
};
18+
19+
function getCloudWatchAlarms(
20+
serverless, region, stage, stateMachineName, stateMachineLogicalId, alarmsObj) {
21+
const okAction = _.get(alarmsObj, 'topics.ok');
22+
const okActions = okAction ? [okAction] : [];
23+
const alarmAction = _.get(alarmsObj, 'topics.alarm');
24+
const alarmActions = alarmAction ? [alarmAction] : [];
25+
const insufficientDataAction = _.get(alarmsObj, 'topics.insufficientData');
26+
const insufficientDataActions = insufficientDataAction ? [insufficientDataAction] : [];
27+
28+
const metrics = _.uniq(_.get(alarmsObj, 'metrics', []));
29+
const [valid, invalid] = _.partition(metrics, m => _.has(cloudWatchMetricNames, m));
30+
31+
if (!_.isEmpty(invalid)) {
32+
serverless.cli.consoleLog(
33+
`state machine [${stateMachineName}] : alarms.metrics has invalid metrics `,
34+
`[${invalid.join(',')}]. ` +
35+
'No CloudWatch Alarms would be created for these. ' +
36+
'Please see https://github.com/horike37/serverless-step-functions for supported metrics');
37+
}
38+
39+
return valid.map(metric => {
40+
const MetricName = cloudWatchMetricNames[metric];
41+
const AlarmDescription =
42+
`${stateMachineName}[${stage}][${region}]: ${alarmDescriptions[metric]}`;
43+
const logicalId = `${stateMachineLogicalId}${MetricName}Alarm`;
44+
45+
return {
46+
logicalId,
47+
alarm: {
48+
Type: 'AWS::CloudWatch::Alarm',
49+
Properties: {
50+
Namespace: 'AWS/States',
51+
MetricName,
52+
AlarmDescription,
53+
Threshold: 1,
54+
Period: 60,
55+
EvaluationPeriods: 1,
56+
ComparisonOperator: 'GreaterThanOrEqualToThreshold',
57+
Statistic: 'Sum',
58+
OKActions: okActions,
59+
AlarmActions: alarmActions,
60+
InsufficientDataActions: insufficientDataActions,
61+
TreatMissingData: 'missing',
62+
Dimensions: [
63+
{
64+
Name: 'StateMachineArn',
65+
Value: {
66+
Ref: stateMachineLogicalId,
67+
},
68+
},
69+
],
70+
},
71+
},
72+
};
73+
});
74+
}
75+
76+
function validateConfig(serverless, stateMachineName, alarmsObj) {
77+
if (!_.isObject(alarmsObj) ||
78+
!_.isObject(alarmsObj.topics) ||
79+
!_.isArray(alarmsObj.metrics) ||
80+
!_.every(alarmsObj.metrics, _.isString)) {
81+
serverless.cli.consoleLog(
82+
`state machine [${stateMachineName}] : alarms config is malformed. ` +
83+
'Please see https://github.com/horike37/serverless-step-functions for examples');
84+
return false;
85+
}
86+
87+
if (!_.has(alarmsObj.topics, 'ok') &&
88+
!_.has(alarmsObj.topics, 'alarm') &&
89+
!_.has(alarmsObj.topics, 'insufficientData')) {
90+
serverless.cli.consoleLog(
91+
`state machine [${stateMachineName}] : alarms config is malformed. ` +
92+
"alarms.topics must specify 'ok', 'alarms' or 'insufficientData'"
93+
);
94+
return false;
95+
}
96+
97+
return true;
98+
}
99+
100+
module.exports = {
101+
compileAlarms() {
102+
const cloudWatchAlarms = _.flatMap(this.getAllStateMachines(), (name) => {
103+
const stateMachineObj = this.getStateMachine(name);
104+
const stateMachineLogicalId = this.getStateMachineLogicalId(name, stateMachineObj);
105+
const stateMachineName = stateMachineObj.name || name;
106+
const alarmsObj = stateMachineObj.alarms;
107+
108+
if (!validateConfig(this.serverless, stateMachineName, alarmsObj)) {
109+
return [];
110+
}
111+
112+
return getCloudWatchAlarms(
113+
this.serverless,
114+
this.region,
115+
this.stage,
116+
stateMachineName,
117+
stateMachineLogicalId,
118+
alarmsObj);
119+
});
120+
121+
const newResources = _.mapValues(_.keyBy(cloudWatchAlarms, 'logicalId'), 'alarm');
122+
123+
_.merge(
124+
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
125+
newResources);
126+
return BbPromise.resolve();
127+
},
128+
};
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
const expect = require('chai').expect;
5+
const Serverless = require('serverless/lib/Serverless');
6+
const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider');
7+
const ServerlessStepFunctions = require('./../../index');
8+
9+
describe('#compileAlarms', () => {
10+
let serverless;
11+
let serverlessStepFunctions;
12+
13+
beforeEach(() => {
14+
serverless = new Serverless();
15+
serverless.servicePath = true;
16+
serverless.service.service = 'step-functions';
17+
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} };
18+
serverless.setProvider('aws', new AwsProvider(serverless));
19+
serverless.cli = { consoleLog: console.log };
20+
const options = {
21+
stage: 'dev',
22+
region: 'ap-northeast-1',
23+
};
24+
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
25+
});
26+
27+
const validateCloudWatchAlarm = (alarm) => {
28+
expect(alarm.Type).to.equal('AWS::CloudWatch::Alarm');
29+
expect(alarm.Properties.Namespace).to.equal('AWS/States');
30+
expect(alarm.Properties.Threshold).to.equal(1);
31+
expect(alarm.Properties.Period).to.equal(60);
32+
expect(alarm.Properties.Statistic).to.equal('Sum');
33+
expect(alarm.Properties.Dimensions).to.have.lengthOf(1);
34+
expect(alarm.Properties.Dimensions[0].Name).to.equal('StateMachineArn');
35+
};
36+
37+
it('should generate CloudWatch Alarms', () => {
38+
const genStateMachine = (name) => ({
39+
name,
40+
definition: {
41+
StartAt: 'A',
42+
States: {
43+
A: {
44+
Type: 'Pass',
45+
End: true,
46+
},
47+
},
48+
},
49+
alarms: {
50+
topics: {
51+
ok: '${self:service}-${opt:stage}-alerts-ok',
52+
alarm: '${self:service}-${opt:stage}-alerts-alarm',
53+
insufficientData: '${self:service}-${opt:stage}-alerts-missing',
54+
},
55+
metrics: [
56+
'executionsTimeOut',
57+
'executionsFailed',
58+
'executionsAborted',
59+
'executionThrottled',
60+
],
61+
},
62+
});
63+
64+
serverless.service.stepFunctions = {
65+
stateMachines: {
66+
myStateMachine1: genStateMachine('stateMachineBeta1'),
67+
myStateMachine2: genStateMachine('stateMachineBeta2'),
68+
},
69+
};
70+
71+
serverlessStepFunctions.compileAlarms();
72+
const resources = serverlessStepFunctions.serverless.service
73+
.provider.compiledCloudFormationTemplate.Resources;
74+
expect(resources).to.have.property('StateMachineBeta1ExecutionsTimeOutAlarm');
75+
validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionsTimeOutAlarm);
76+
expect(resources).to.have.property('StateMachineBeta1ExecutionsFailedAlarm');
77+
validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionsFailedAlarm);
78+
expect(resources).to.have.property('StateMachineBeta1ExecutionsAbortedAlarm');
79+
validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionsAbortedAlarm);
80+
expect(resources).to.have.property('StateMachineBeta1ExecutionThrottledAlarm');
81+
validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionThrottledAlarm);
82+
expect(resources).to.have.property('StateMachineBeta2ExecutionsTimeOutAlarm');
83+
validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionsTimeOutAlarm);
84+
expect(resources).to.have.property('StateMachineBeta2ExecutionsFailedAlarm');
85+
validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionsFailedAlarm);
86+
expect(resources).to.have.property('StateMachineBeta2ExecutionsAbortedAlarm');
87+
validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionsAbortedAlarm);
88+
expect(resources).to.have.property('StateMachineBeta2ExecutionThrottledAlarm');
89+
validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionThrottledAlarm);
90+
});
91+
92+
it('should not generate CloudWatch Alarms when alarms.topics is missing', () => {
93+
const genStateMachine = (name) => ({
94+
name,
95+
definition: {
96+
StartAt: 'A',
97+
States: {
98+
A: {
99+
Type: 'Pass',
100+
End: true,
101+
},
102+
},
103+
},
104+
alarms: {
105+
metrics: [
106+
'executionsTimeOut',
107+
],
108+
},
109+
});
110+
111+
serverless.service.stepFunctions = {
112+
stateMachines: {
113+
myStateMachine1: genStateMachine('stateMachineBeta1'),
114+
myStateMachine2: genStateMachine('stateMachineBeta2'),
115+
},
116+
};
117+
118+
serverlessStepFunctions.compileAlarms();
119+
const resources = serverlessStepFunctions.serverless.service
120+
.provider.compiledCloudFormationTemplate.Resources;
121+
expect(_.keys(resources)).to.have.lengthOf(0);
122+
});
123+
124+
it('should not generate CloudWatch Alarms when alarms.topics is empty', () => {
125+
const genStateMachine = (name) => ({
126+
name,
127+
definition: {
128+
StartAt: 'A',
129+
States: {
130+
A: {
131+
Type: 'Pass',
132+
End: true,
133+
},
134+
},
135+
},
136+
alarms: {
137+
topics: {},
138+
metrics: [
139+
'executionsTimeOut',
140+
],
141+
},
142+
});
143+
144+
serverless.service.stepFunctions = {
145+
stateMachines: {
146+
myStateMachine1: genStateMachine('stateMachineBeta1'),
147+
myStateMachine2: genStateMachine('stateMachineBeta2'),
148+
},
149+
};
150+
151+
serverlessStepFunctions.compileAlarms();
152+
const resources = serverlessStepFunctions.serverless.service
153+
.provider.compiledCloudFormationTemplate.Resources;
154+
expect(_.keys(resources)).to.have.lengthOf(0);
155+
});
156+
157+
it('should not generate CloudWatch Alarms when alarms.metrics is missing', () => {
158+
const genStateMachine = (name) => ({
159+
name,
160+
definition: {
161+
StartAt: 'A',
162+
States: {
163+
A: {
164+
Type: 'Pass',
165+
End: true,
166+
},
167+
},
168+
},
169+
alarms: {
170+
topics: {
171+
ok: '${self:service}-${opt:stage}-alerts-ok',
172+
},
173+
},
174+
});
175+
176+
serverless.service.stepFunctions = {
177+
stateMachines: {
178+
myStateMachine1: genStateMachine('stateMachineBeta1'),
179+
myStateMachine2: genStateMachine('stateMachineBeta2'),
180+
},
181+
};
182+
183+
serverlessStepFunctions.compileAlarms();
184+
const resources = serverlessStepFunctions.serverless.service
185+
.provider.compiledCloudFormationTemplate.Resources;
186+
expect(_.keys(resources)).to.have.lengthOf(0);
187+
});
188+
189+
it('should not generate CloudWatch Alarms for unsupported metrics', () => {
190+
const genStateMachine = (name) => ({
191+
name,
192+
definition: {
193+
StartAt: 'A',
194+
States: {
195+
A: {
196+
Type: 'Pass',
197+
End: true,
198+
},
199+
},
200+
},
201+
alarms: {
202+
topics: {
203+
ok: '${self:service}-${opt:stage}-alerts-ok',
204+
},
205+
metrics: [
206+
'executionsFailed',
207+
'executionsFail',
208+
],
209+
},
210+
});
211+
212+
serverless.service.stepFunctions = {
213+
stateMachines: {
214+
myStateMachine1: genStateMachine('stateMachineBeta1'),
215+
myStateMachine2: genStateMachine('stateMachineBeta2'),
216+
},
217+
};
218+
219+
serverlessStepFunctions.compileAlarms();
220+
const resources = serverlessStepFunctions.serverless.service
221+
.provider.compiledCloudFormationTemplate.Resources;
222+
// valid metrics => CW alarms
223+
expect(resources).to.have.property('StateMachineBeta1ExecutionsFailedAlarm');
224+
expect(resources).to.have.property('StateMachineBeta2ExecutionsFailedAlarm');
225+
226+
// but invalid metric names are skipped
227+
expect(_.keys(resources)).to.have.lengthOf(2);
228+
});
229+
});

lib/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const BbPromise = require('bluebird');
44
const compileStateMachines = require('./deploy/stepFunctions/compileStateMachines');
55
const compileActivities = require('./deploy/stepFunctions/compileActivities');
66
const compileIamRole = require('./deploy/stepFunctions/compileIamRole');
7+
const compileAlarms = require('./deploy/stepFunctions/compileAlarms');
78
const httpValidate = require('./deploy/events/apiGateway/validate');
89
const httpResources = require('./deploy/events/apiGateway/resources');
910
const httpMethods = require('./deploy/events/apiGateway/methods');
@@ -40,6 +41,7 @@ class ServerlessStepFunctions {
4041
compileStateMachines,
4142
compileActivities,
4243
compileIamRole,
44+
compileAlarms,
4345
httpRestApi,
4446
httpInfo,
4547
httpValidate,
@@ -105,7 +107,8 @@ class ServerlessStepFunctions {
105107
'package:compileFunctions': () => BbPromise.bind(this)
106108
.then(this.compileIamRole)
107109
.then(this.compileStateMachines)
108-
.then(this.compileActivities),
110+
.then(this.compileActivities)
111+
.then(this.compileAlarms),
109112
'package:compileEvents': () =>
110113
this.compileScheduledEvents().then(() => {
111114
// FIXME: Rename pluginhttpValidated to validated

0 commit comments

Comments
 (0)