|
| 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 | +}; |
0 commit comments