UNPKG

lambda-cloudwatch-slack

Version:
437 lines (390 loc) 15.4 kB
var AWS = require('aws-sdk'); var url = require('url'); var https = require('https'); var config = require('./config'); var _ = require('lodash'); var hookUrl; var baseSlackMessage = {} var postMessage = function(message, callback) { var body = JSON.stringify(message); var options = url.parse(hookUrl); options.method = 'POST'; options.headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }; var postReq = https.request(options, function(res) { var chunks = []; res.setEncoding('utf8'); res.on('data', function(chunk) { return chunks.push(chunk); }); res.on('end', function() { var body = chunks.join(''); if (callback) { callback({ body: body, statusCode: res.statusCode, statusMessage: res.statusMessage }); } }); return res; }); postReq.write(body); postReq.end(); }; var handleElasticBeanstalk = function(event, context) { var timestamp = (new Date(event.Records[0].Sns.Timestamp)).getTime()/1000; var subject = event.Records[0].Sns.Subject || "AWS Elastic Beanstalk Notification"; var message = event.Records[0].Sns.Message; var stateRed = message.indexOf(" to RED"); var stateSevere = message.indexOf(" to Severe"); var butWithErrors = message.indexOf(" but with errors"); var noPermission = message.indexOf("You do not have permission"); var failedDeploy = message.indexOf("Failed to deploy application"); var failedConfig = message.indexOf("Failed to deploy configuration"); var failedQuota = message.indexOf("Your quota allows for 0 more running instance"); var unsuccessfulCommand = message.indexOf("Unsuccessful command execution"); var stateYellow = message.indexOf(" to YELLOW"); var stateDegraded = message.indexOf(" to Degraded"); var stateInfo = message.indexOf(" to Info"); var removedInstance = message.indexOf("Removed instance "); var addingInstance = message.indexOf("Adding instance "); var abortedOperation = message.indexOf(" aborted operation."); var abortedDeployment = message.indexOf("some instances may have deployed the new application version"); var color = "good"; if (stateRed != -1 || stateSevere != -1 || butWithErrors != -1 || noPermission != -1 || failedDeploy != -1 || failedConfig != -1 || failedQuota != -1 || unsuccessfulCommand != -1) { color = "danger"; } if (stateYellow != -1 || stateDegraded != -1 || stateInfo != -1 || removedInstance != -1 || addingInstance != -1 || abortedOperation != -1 || abortedDeployment != -1) { color = "warning"; } var slackMessage = { text: "*" + subject + "*", attachments: [ { "fields": [ { "title": "Subject", "value": event.Records[0].Sns.Subject, "short": false}, { "title": "Message", "value": message, "short": false} ], "color": color, "ts": timestamp } ] }; return _.merge(slackMessage, baseSlackMessage); }; var handleCodeDeploy = function(event, context) { var subject = "AWS CodeDeploy Notification"; var timestamp = (new Date(event.Records[0].Sns.Timestamp)).getTime()/1000; var snsSubject = event.Records[0].Sns.Subject; var message; var fields = []; var color = "warning"; try { message = JSON.parse(event.Records[0].Sns.Message); if(message.status === "SUCCEEDED"){ color = "good"; } else if(message.status === "FAILED"){ color = "danger"; } fields.push({ "title": "Message", "value": snsSubject, "short": false }); fields.push({ "title": "Deployment Group", "value": message.deploymentGroupName, "short": true }); fields.push({ "title": "Application", "value": message.applicationName, "short": true }); fields.push({ "title": "Status Link", "value": "https://console.aws.amazon.com/codedeploy/home?region=" + message.region + "#/deployments/" + message.deploymentId, "short": false }); } catch(e) { color = "good"; message = event.Records[0].Sns.Message; fields.push({ "title": "Message", "value": snsSubject, "short": false }); fields.push({ "title": "Detail", "value": message, "short": false }); } var slackMessage = { text: "*" + subject + "*", attachments: [ { "color": color, "fields": fields, "ts": timestamp } ] }; return _.merge(slackMessage, baseSlackMessage); }; var handleCodePipeline = function(event, context) { var subject = "AWS CodePipeline Notification"; var timestamp = (new Date(event.Records[0].Sns.Timestamp)).getTime()/1000; var snsSubject = event.Records[0].Sns.Subject; var message; var fields = []; var color = "warning"; var changeType = ""; try { message = JSON.parse(event.Records[0].Sns.Message); detailType = message['detail-type']; if(detailType === "CodePipeline Pipeline Execution State Change"){ changeType = ""; } else if(detailType === "CodePipeline Stage Execution State Change"){ changeType = "STAGE " + message.detail.stage; } else if(detailType === "CodePipeline Action Execution State Change"){ changeType = "ACTION"; } if(message.detail.state === "SUCCEEDED"){ color = "good"; } else if(message.detail.state === "FAILED"){ color = "danger"; } header = message.detail.state + ": CodePipeline " + changeType; fields.push({ "title": "Message", "value": header, "short": false }); fields.push({ "title": "Pipeline", "value": message.detail.pipeline, "short": true }); fields.push({ "title": "Region", "value": message.region, "short": true }); fields.push({ "title": "Status Link", "value": "https://console.aws.amazon.com/codepipeline/home?region=" + message.region + "#/view/" + message.detail.pipeline, "short": false }); } catch(e) { color = "good"; message = event.Records[0].Sns.Message; header = message.detail.state + ": CodePipeline " + message.detail.pipeline; fields.push({ "title": "Message", "value": header, "short": false }); fields.push({ "title": "Detail", "value": message, "short": false }); } var slackMessage = { text: "*" + subject + "*", attachments: [ { "color": color, "fields": fields, "ts": timestamp } ] }; return _.merge(slackMessage, baseSlackMessage); }; var handleElasticache = function(event, context) { var subject = "AWS ElastiCache Notification" var message = JSON.parse(event.Records[0].Sns.Message); var timestamp = (new Date(event.Records[0].Sns.Timestamp)).getTime()/1000; var region = event.Records[0].EventSubscriptionArn.split(":")[3]; var eventname, nodename; var color = "good"; for(key in message){ eventname = key; nodename = message[key]; break; } var slackMessage = { text: "*" + subject + "*", attachments: [ { "color": color, "fields": [ { "title": "Event", "value": eventname.split(":")[1], "short": true }, { "title": "Node", "value": nodename, "short": true }, { "title": "Link to cache node", "value": "https://console.aws.amazon.com/elasticache/home?region=" + region + "#cache-nodes:id=" + nodename + ";nodes", "short": false } ], "ts": timestamp } ] }; return _.merge(slackMessage, baseSlackMessage); }; var handleCloudWatch = function(event, context) { var timestamp = (new Date(event.Records[0].Sns.Timestamp)).getTime()/1000; var message = JSON.parse(event.Records[0].Sns.Message); var region = event.Records[0].EventSubscriptionArn.split(":")[3]; var subject = "AWS CloudWatch Notification"; var alarmName = message.AlarmName; var metricName = message.Trigger.MetricName; var oldState = message.OldStateValue; var newState = message.NewStateValue; var alarmDescription = message.AlarmDescription; var alarmReason = message.NewStateReason; var trigger = message.Trigger; var color = "warning"; if (message.NewStateValue === "ALARM") { color = "danger"; } else if (message.NewStateValue === "OK") { color = "good"; } var slackMessage = { text: "*" + subject + "*", attachments: [ { "color": color, "fields": [ { "title": "Alarm Name", "value": alarmName, "short": true }, { "title": "Alarm Description", "value": alarmDescription, "short": false}, { "title": "Trigger", "value": trigger.Statistic + " " + metricName + " " + trigger.ComparisonOperator + " " + trigger.Threshold + " for " + trigger.EvaluationPeriods + " period(s) of " + trigger.Period + " seconds.", "short": false }, { "title": "Old State", "value": oldState, "short": true }, { "title": "Current State", "value": newState, "short": true }, { "title": "Link to Alarm", "value": "https://console.aws.amazon.com/cloudwatch/home?region=" + region + "#alarm:alarmFilter=ANY;name=" + encodeURIComponent(alarmName), "short": false } ], "ts": timestamp } ] }; return _.merge(slackMessage, baseSlackMessage); }; var handleAutoScaling = function(event, context) { var subject = "AWS AutoScaling Notification" var message = JSON.parse(event.Records[0].Sns.Message); var timestamp = (new Date(event.Records[0].Sns.Timestamp)).getTime()/1000; var eventname, nodename; var color = "good"; for(key in message){ eventname = key; nodename = message[key]; break; } var slackMessage = { text: "*" + subject + "*", attachments: [ { "color": color, "fields": [ { "title": "Message", "value": event.Records[0].Sns.Subject, "short": false }, { "title": "Description", "value": message.Description, "short": false }, { "title": "Event", "value": message.Event, "short": false }, { "title": "Cause", "value": message.Cause, "short": false } ], "ts": timestamp } ] }; return _.merge(slackMessage, baseSlackMessage); }; var handleCatchAll = function(event, context) { var record = event.Records[0] var subject = record.Sns.Subject var timestamp = new Date(record.Sns.Timestamp).getTime() / 1000; var message = JSON.parse(record.Sns.Message) var color = "warning"; if (message.NewStateValue === "ALARM") { color = "danger"; } else if (message.NewStateValue === "OK") { color = "good"; } // Add all of the values from the event message to the Slack message description var description = "" for(key in message) { var renderedMessage = typeof message[key] === 'object' ? JSON.stringify(message[key]) : message[key] description = description + "\n" + key + ": " + renderedMessage } var slackMessage = { text: "*" + subject + "*", attachments: [ { "color": color, "fields": [ { "title": "Message", "value": record.Sns.Subject, "short": false }, { "title": "Description", "value": description, "short": false } ], "ts": timestamp } ] } return _.merge(slackMessage, baseSlackMessage); } var processEvent = function(event, context) { console.log("sns received:" + JSON.stringify(event, null, 2)); var slackMessage = null; var eventSubscriptionArn = event.Records[0].EventSubscriptionArn; var eventSnsSubject = event.Records[0].Sns.Subject || 'no subject'; var eventSnsMessageRaw = event.Records[0].Sns.Message; var eventSnsMessage = null; try { eventSnsMessage = JSON.parse(eventSnsMessageRaw); } catch (e) { } if(eventSubscriptionArn.indexOf(config.services.codepipeline.match_text) > -1 || eventSnsSubject.indexOf(config.services.codepipeline.match_text) > -1 || eventSnsMessageRaw.indexOf(config.services.codepipeline.match_text) > -1){ console.log("processing codepipeline notification"); slackMessage = handleCodePipeline(event,context) } else if(eventSubscriptionArn.indexOf(config.services.elasticbeanstalk.match_text) > -1 || eventSnsSubject.indexOf(config.services.elasticbeanstalk.match_text) > -1 || eventSnsMessageRaw.indexOf(config.services.elasticbeanstalk.match_text) > -1){ console.log("processing elasticbeanstalk notification"); slackMessage = handleElasticBeanstalk(event,context) } else if(eventSnsMessage && 'AlarmName' in eventSnsMessage && 'AlarmDescription' in eventSnsMessage){ console.log("processing cloudwatch notification"); slackMessage = handleCloudWatch(event,context); } else if(eventSubscriptionArn.indexOf(config.services.codedeploy.match_text) > -1 || eventSnsSubject.indexOf(config.services.codedeploy.match_text) > -1 || eventSnsMessageRaw.indexOf(config.services.codedeploy.match_text) > -1){ console.log("processing codedeploy notification"); slackMessage = handleCodeDeploy(event,context); } else if(eventSubscriptionArn.indexOf(config.services.elasticache.match_text) > -1 || eventSnsSubject.indexOf(config.services.elasticache.match_text) > -1 || eventSnsMessageRaw.indexOf(config.services.elasticache.match_text) > -1){ console.log("processing elasticache notification"); slackMessage = handleElasticache(event,context); } else if(eventSubscriptionArn.indexOf(config.services.autoscaling.match_text) > -1 || eventSnsSubject.indexOf(config.services.autoscaling.match_text) > -1 || eventSnsMessageRaw.indexOf(config.services.autoscaling.match_text) > -1){ console.log("processing autoscaling notification"); slackMessage = handleAutoScaling(event, context); } else{ slackMessage = handleCatchAll(event, context); } postMessage(slackMessage, function(response) { if (response.statusCode < 400) { console.info('message posted successfully'); context.succeed(); } else if (response.statusCode < 500) { console.error("error posting message to slack API: " + response.statusCode + " - " + response.statusMessage); // Don't retry because the error is due to a problem with the request context.succeed(); } else { // Let Lambda retry context.fail("server error when processing message: " + response.statusCode + " - " + response.statusMessage); } }); }; exports.handler = function(event, context) { if (hookUrl) { processEvent(event, context); } else if (config.unencryptedHookUrl) { hookUrl = config.unencryptedHookUrl; processEvent(event, context); } else if (config.kmsEncryptedHookUrl && config.kmsEncryptedHookUrl !== '<kmsEncryptedHookUrl>') { var encryptedBuf = new Buffer(config.kmsEncryptedHookUrl, 'base64'); var cipherText = { CiphertextBlob: encryptedBuf }; var kms = new AWS.KMS(); kms.decrypt(cipherText, function(err, data) { if (err) { console.log("decrypt error: " + err); processEvent(event, context); } else { hookUrl = "https://" + data.Plaintext.toString('ascii'); processEvent(event, context); } }); } else { context.fail('hook url has not been set.'); } };