Skip to content

Commit 898752e

Browse files
authored
Merge pull request #495 from lucasvieirasilva/issue-494
feat(alarm): support for custom alarm names
2 parents ba29512 + ebf98ef commit 898752e

File tree

4 files changed

+271
-1
lines changed

4 files changed

+271
-1
lines changed

README.md

+65
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,71 @@ alarms:
333333
treatMissingData: ignore # default
334334
```
335335

336+
#### Custom CloudWatch Alarm names
337+
338+
By default, the CloudFormation assigns names to the alarms based on the CloudFormation stack and the resource logical Id, and in some cases and these names could be confusing.
339+
340+
To use custom names to the alarms add `nameTemplate` property in the `alarms` object.
341+
342+
example:
343+
344+
```yaml
345+
service: myservice
346+
347+
plugins:
348+
- serverless-step-functions
349+
350+
stepFunctions:
351+
stateMachines:
352+
main-workflow:
353+
name: main
354+
alarms:
355+
nameTemplate: $[stateMachineName]-$[cloudWatchMetricName]-alarm
356+
topics:
357+
alarm: !Ref AwsAlertsGenericAlarmTopicAlarm
358+
metrics:
359+
- executionsFailed
360+
- executionsAborted
361+
- executionsTimedOut
362+
- executionThrottled
363+
treatMissingData: ignore
364+
definition: ${file(./step-functions/main.asl.yaml)}
365+
```
366+
367+
Supported variables to the `nameTemplate` property:
368+
369+
- `stateMachineName`
370+
- `metricName`
371+
- `cloudWatchMetricName`
372+
373+
##### Per-Metric Alarm Name
374+
375+
To overwrite the alarm name for a specific metric, add the `alarmName` property in the metric object.
376+
377+
```yaml
378+
service: myservice
379+
380+
plugins:
381+
- serverless-step-functions
382+
383+
stepFunctions:
384+
stateMachines:
385+
main-workflow:
386+
name: main
387+
alarms:
388+
nameTemplate: $[stateMachineName]-$[cloudWatchMetricName]-alarm
389+
topics:
390+
alarm: !Ref AwsAlertsGenericAlarmTopicAlarm
391+
metrics:
392+
- metric: executionsFailed
393+
alarmName: mycustom-name-${self:stage.region}-Failed-alarm
394+
- executionsAborted
395+
- executionsTimedOut
396+
- executionThrottled
397+
treatMissingData: ignore
398+
definition: ${file(./step-functions/main.asl.yaml)}
399+
```
400+
336401
### CloudWatch Notifications
337402

338403
You can monitor the execution state of your state machines [via CloudWatch Events](https://aws.amazon.com/about-aws/whats-new/2019/05/aws-step-functions-adds-support-for-workflow-execution-events/). It allows you to be alerted when the status of your state machine changes to `ABORTED`, `FAILED`, `RUNNING`, `SUCCEEDED` or `TIMED_OUT`.

lib/deploy/stepFunctions/compileAlarms.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,39 @@ const alarmDescriptions = {
2222
executionsSucceeded: 'executions succeeded',
2323
};
2424

25+
const supportedVariables = [
26+
'stateMachineName',
27+
'cloudWatchMetricName',
28+
'metricName',
29+
];
30+
const variableRegxPattern = /(?<=\$\[)[^\][]*(?=])/g;
31+
32+
function getAlarmNameFromTemplate(nameTemplate, variables) {
33+
let alarmName = null;
34+
if (!_.isNil(nameTemplate)) {
35+
const matches = nameTemplate.match(variableRegxPattern);
36+
let validVariables = true;
37+
for (const match of matches) {
38+
if (!supportedVariables.includes(match)) {
39+
logger.log(
40+
`Invalid alarms.nameTemplate property, variable '${match}' `
41+
+ `does not match with the supported variables: ${supportedVariables.join(', ')}`,
42+
);
43+
validVariables = false;
44+
}
45+
}
46+
47+
if (validVariables) {
48+
alarmName = nameTemplate;
49+
for (const match of matches) {
50+
alarmName = alarmName.replace(`$[${match}]`, variables[match]);
51+
}
52+
}
53+
}
54+
55+
return alarmName;
56+
}
57+
2558
function getCloudWatchAlarms(
2659
serverless, region, stage, stateMachineName, stateMachineLogicalId, alarmsObj,
2760
) {
@@ -32,6 +65,7 @@ function getCloudWatchAlarms(
3265
const insufficientDataAction = _.get(alarmsObj, 'topics.insufficientData');
3366
const insufficientDataActions = insufficientDataAction ? [insufficientDataAction] : [];
3467
const defaultTreatMissingData = _.get(alarmsObj, 'treatMissingData', 'missing');
68+
const nameTemplate = _.get(alarmsObj, 'nameTemplate', null);
3569

3670
const metrics = _.uniq(_.get(alarmsObj, 'metrics', []));
3771
const [valid, invalid] = _.partition(
@@ -57,7 +91,15 @@ function getCloudWatchAlarms(
5791
const logicalId = _.get(metric, 'logicalId', defaultLogicalId);
5892
const treatMissingData = _.get(metric, 'treatMissingData', defaultTreatMissingData);
5993

60-
return {
94+
const templateAlarmName = getAlarmNameFromTemplate(nameTemplate, {
95+
metricName,
96+
cloudWatchMetricName,
97+
stateMachineName,
98+
});
99+
100+
const alarmName = _.get(metric, 'alarmName', templateAlarmName);
101+
102+
const cfnResource = {
61103
logicalId,
62104
alarm: {
63105
Type: 'AWS::CloudWatch::Alarm',
@@ -85,6 +127,12 @@ function getCloudWatchAlarms(
85127
},
86128
},
87129
};
130+
131+
if (alarmName) {
132+
cfnResource.alarm.Properties.AlarmName = alarmName;
133+
}
134+
135+
return cfnResource;
88136
});
89137
}
90138

lib/deploy/stepFunctions/compileAlarms.schema.js

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const simpleMetric = Joi.string()
4444
const complexMetric = Joi.object().keys({
4545
metric: simpleMetric.required(),
4646
logicalId: Joi.string(),
47+
alarmName: Joi.string(),
4748
treatMissingData,
4849
});
4950

@@ -54,9 +55,12 @@ const metric = Joi.alternatives().try(
5455

5556
const metrics = Joi.array().items(metric).min(1);
5657

58+
const nameTemplate = Joi.string();
59+
5760
const schema = Joi.object().keys({
5861
topics: topics.required(),
5962
metrics: metrics.required(),
63+
nameTemplate,
6064
treatMissingData,
6165
});
6266

lib/deploy/stepFunctions/compileAlarms.test.js

+153
Original file line numberDiff line numberDiff line change
@@ -426,4 +426,157 @@ describe('#compileAlarms', () => {
426426
expect(resources).to.have.property('MyAlarm');
427427
expect(consoleLogSpy.callCount).equal(0);
428428
});
429+
430+
it('should generate CloudWatch Alarms with nameTemplate', () => {
431+
const genStateMachine = name => ({
432+
name,
433+
definition: {
434+
StartAt: 'A',
435+
States: {
436+
A: {
437+
Type: 'Pass',
438+
End: true,
439+
},
440+
},
441+
},
442+
alarms: {
443+
topics: {
444+
ok: '${self:service}-${opt:stage}-alerts-ok',
445+
alarm: '${self:service}-${opt:stage}-alerts-alarm',
446+
insufficientData: '${self:service}-${opt:stage}-alerts-missing',
447+
},
448+
nameTemplate: '$[stateMachineName]-$[cloudWatchMetricName]-alarm',
449+
metrics: [
450+
'executionsTimedOut',
451+
'executionsFailed',
452+
'executionsAborted',
453+
'executionThrottled',
454+
'executionsSucceeded',
455+
],
456+
},
457+
});
458+
459+
serverless.service.stepFunctions = {
460+
stateMachines: {
461+
myStateMachine: genStateMachine('stateCustomName1'),
462+
},
463+
};
464+
465+
serverlessStepFunctions.compileAlarms();
466+
const resources = serverlessStepFunctions.serverless.service
467+
.provider.compiledCloudFormationTemplate.Resources;
468+
469+
expect(resources.StateCustomName1ExecutionsTimedOutAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsTimedOut-alarm');
470+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionsTimedOutAlarm);
471+
expect(resources.StateCustomName1ExecutionsFailedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsFailed-alarm');
472+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionsFailedAlarm);
473+
expect(resources.StateCustomName1ExecutionsAbortedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsAborted-alarm');
474+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionsAbortedAlarm);
475+
expect(resources.StateCustomName1ExecutionThrottledAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionThrottled-alarm');
476+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionThrottledAlarm);
477+
expect(consoleLogSpy.callCount).equal(0);
478+
});
479+
480+
it('should generate CloudWatch Alarms with invalid nameTemplate', () => {
481+
const genStateMachine = name => ({
482+
name,
483+
definition: {
484+
StartAt: 'A',
485+
States: {
486+
A: {
487+
Type: 'Pass',
488+
End: true,
489+
},
490+
},
491+
},
492+
alarms: {
493+
topics: {
494+
ok: '${self:service}-${opt:stage}-alerts-ok',
495+
alarm: '${self:service}-${opt:stage}-alerts-alarm',
496+
insufficientData: '${self:service}-${opt:stage}-alerts-missing',
497+
},
498+
nameTemplate: '$[stateMachineName]-$[invalidProp]-alarm',
499+
metrics: [
500+
'executionsTimedOut',
501+
'executionsFailed',
502+
'executionsAborted',
503+
'executionThrottled',
504+
'executionsSucceeded',
505+
],
506+
},
507+
});
508+
509+
serverless.service.stepFunctions = {
510+
stateMachines: {
511+
myStateMachine: genStateMachine('stateCustomName2'),
512+
},
513+
};
514+
515+
serverlessStepFunctions.compileAlarms();
516+
const resources = serverlessStepFunctions.serverless.service
517+
.provider.compiledCloudFormationTemplate.Resources;
518+
519+
expect(resources.StateCustomName2ExecutionsTimedOutAlarm.Properties).not.have.property('AlarmName');
520+
validateCloudWatchAlarm(resources.StateCustomName2ExecutionsTimedOutAlarm);
521+
expect(resources.StateCustomName2ExecutionsFailedAlarm.Properties).not.have.property('AlarmName');
522+
validateCloudWatchAlarm(resources.StateCustomName2ExecutionsFailedAlarm);
523+
expect(resources.StateCustomName2ExecutionsAbortedAlarm.Properties).not.have.property('AlarmName');
524+
validateCloudWatchAlarm(resources.StateCustomName2ExecutionsAbortedAlarm);
525+
expect(resources.StateCustomName2ExecutionThrottledAlarm.Properties).not.have.property('AlarmName');
526+
validateCloudWatchAlarm(resources.StateCustomName2ExecutionThrottledAlarm);
527+
expect(consoleLogSpy.callCount).equal(5);
528+
});
529+
530+
it('should generate CloudWatch Alarms with custom alarm name', () => {
531+
const genStateMachine = name => ({
532+
name,
533+
definition: {
534+
StartAt: 'A',
535+
States: {
536+
A: {
537+
Type: 'Pass',
538+
End: true,
539+
},
540+
},
541+
},
542+
alarms: {
543+
topics: {
544+
ok: '${self:service}-${opt:stage}-alerts-ok',
545+
alarm: '${self:service}-${opt:stage}-alerts-alarm',
546+
insufficientData: '${self:service}-${opt:stage}-alerts-missing',
547+
},
548+
nameTemplate: '$[stateMachineName]-$[cloudWatchMetricName]-alarm',
549+
metrics: [
550+
{
551+
metric: 'executionsTimedOut',
552+
alarmName: 'mycustom-name',
553+
},
554+
'executionsFailed',
555+
'executionsAborted',
556+
'executionThrottled',
557+
'executionsSucceeded',
558+
],
559+
},
560+
});
561+
562+
serverless.service.stepFunctions = {
563+
stateMachines: {
564+
myStateMachine: genStateMachine('stateCustomName1'),
565+
},
566+
};
567+
568+
serverlessStepFunctions.compileAlarms();
569+
const resources = serverlessStepFunctions.serverless.service
570+
.provider.compiledCloudFormationTemplate.Resources;
571+
572+
expect(resources.StateCustomName1ExecutionsTimedOutAlarm.Properties.AlarmName).to.be.equal('mycustom-name');
573+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionsTimedOutAlarm);
574+
expect(resources.StateCustomName1ExecutionsFailedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsFailed-alarm');
575+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionsFailedAlarm);
576+
expect(resources.StateCustomName1ExecutionsAbortedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsAborted-alarm');
577+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionsAbortedAlarm);
578+
expect(resources.StateCustomName1ExecutionThrottledAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionThrottled-alarm');
579+
validateCloudWatchAlarm(resources.StateCustomName1ExecutionThrottledAlarm);
580+
expect(consoleLogSpy.callCount).equal(0);
581+
});
429582
});

0 commit comments

Comments
 (0)