├── .gitignore ├── LICENSE.txt ├── Procfile ├── README.md ├── handlers.js ├── package.json ├── slack.js └── web.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | 3 | Copyright (c) 2014 Scopely 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node web.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slack/SNS Bridge 2 | ================ 3 | This [Heroku](https://heroku.com/)-targetted Node.JS app handles receiving 4 | various [Amazon SNS](http://aws.amazon.com/sns/) messages from services like 5 | Cloudwatch, autoscaling, and Stackdriver. Each message is converted into a rich 6 | [Slack](https://slack.com/) message and sent to the specified channel. 7 | 8 | Deployment 9 | ---------- 10 | Deploy the code to Heroku under your own app. This should be as easy as cloning, 11 | running `heroku apps:create `, and pushing. 12 | 13 | To authorize your application with Slack, go to the 14 | [new integration](https://slack.com/services/new) page and create an Incoming 15 | Webhook. You'll get a url in the format of 16 | `https://hooks.slack.com/services/TOKEN`. Take note of this token 17 | and give the Heroku app your Slack information: 18 | 19 | ``` 20 | $ heroku config:set SLACK_TOKEN=yourintegrationtoken 21 | ``` 22 | 23 | (Note that Slack lets you customize your integration's image and name. This 24 | bridge generally overrides the image and name dynamically, so you don't need 25 | to customize the integration for `slack-sns`.) 26 | 27 | Once this is set, you just need to add the bridge to your SNS topics. Use the 28 | URL format `https://.herokuapp.com/channel/noise` to send messages 29 | to the `#noise` channel on Slack. 30 | 31 | Creating Handlers 32 | ----------------- 33 | Handlers are small functions which take a message in and format it for Slack. 34 | 35 | They are defined in `handlers.js` and referenced from `web.js` in the `message` 36 | function. When creating a handler the most important thing to do is return an 37 | object with a `text` field. All other fields are optional. For inspiration and 38 | help making a new handler, check out the existing ones! 39 | 40 | License 41 | ------- 42 | Apache 2.0. Check out `LICENSE.txt` 43 | 44 | Copyright (c) 2014 Scopely 45 | -------------------------------------------------------------------------------- /handlers.js: -------------------------------------------------------------------------------- 1 | exports.stackdriver = function (msg) { 2 | var event = msg.incident; 3 | 4 | return { 5 | name: event.resource_name + ' incident ' + event.state, 6 | icon: (event.state == 'open') ? ':cloud:' : ':sunny:', 7 | text: event.summary + ' [<' + event.url + '|info>]', 8 | }; 9 | }; 10 | 11 | var cwIcons = { 12 | INSUFFICIENT_DATA: ':open_hands:', 13 | OK: ':ok_hand:', 14 | ALARM: ':wave:', 15 | }; 16 | exports.cloudwatch = function (msg) { 17 | if (msg.OldStateValue == 'INSUFFICIENT_DATA' && msg.NewStateValue == 'OK') { 18 | return null; // drop state changes that aren't useful/notable 19 | } 20 | 21 | return { 22 | icon: cwIcons[msg.NewStateValue], 23 | text: 'Description: ' + msg.AlarmDescription + '\r\n>' + msg.NewStateReason, 24 | }; 25 | }; 26 | 27 | var asIcons = { 28 | EC2_INSTANCE_TERMINATE: ':heavy_minus_sign:', 29 | EC2_INSTANCE_LAUNCH: ':heavy_plus_sign:', 30 | EC2_INSTANCE_TERMINATE_ERROR: ':no_entry:', 31 | EC2_INSTANCE_LAUNCH_ERROR: ':no_entry:', 32 | }; 33 | exports.autoscaling = function (msg) { 34 | var text = msg.Description; 35 | 36 | if (msg.Details && msg.Details['Availability Zone']) { 37 | var zone = msg.Details['Availability Zone']; 38 | text += ' (zone ' + zone + ')'; 39 | } 40 | 41 | if (msg.Cause) { 42 | text += '\r\n>' + msg.Cause; 43 | } 44 | 45 | return { 46 | icon: msg.Event ? asIcons[msg.Event.split(':')[1]] : ':question:', 47 | text: text, 48 | }; 49 | }; 50 | 51 | exports.plaintext = function (msg) { 52 | var text = msg.text; 53 | var icon; 54 | 55 | if (text.match(/(Writes|Reads): UP from/)) { 56 | icon = ':point_up_2:'; 57 | } else if (text.match(/(Writes|Reads): DOWN from/)) { 58 | icon = ':point_down:'; 59 | } else if (text.match(/Consumed (Write|Read) Capacity (\d+)% was greater than/)) { 60 | icon = ':information_desk_person:'; 61 | } 62 | 63 | return { 64 | icon: icon || ':interrobang:', 65 | text: text, 66 | }; 67 | }; 68 | 69 | var alarmColors = { 70 | ALARM: '#e74c3c', 71 | OK: '#27ae60', 72 | INSUFFICIENT_DATA: '#f39c12' 73 | }; 74 | exports.alarm = function (msg) { 75 | return { 76 | icon_url: msg.appIcon, 77 | rich: true, 78 | username: msg.appName, 79 | attachments: [{ 80 | fallback: msg.title, 81 | color: alarmColors[msg.type], 82 | title: msg.title, 83 | title_link: msg.metricUrl, 84 | text: msg.message, 85 | image_url: msg.graphUrl 86 | }] 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-sns", 3 | "version": "0.1.0", 4 | "description": "Customizable Amazon SNS bridge to Slack", 5 | "main": "web.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Daniel Lamando", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "logfmt": "^1.1.2", 13 | "express": "^4.3.0", 14 | "body-parser": "^1.2.0" 15 | }, 16 | "engines": { 17 | "node": "0.10.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /slack.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | 3 | exports.send = function (opts) { 4 | var options = { 5 | host: 'hooks.slack.com', 6 | port: 443, 7 | method: 'POST', 8 | path: '/services/' + process.env.SLACK_TOKEN, 9 | headers: {'Content-type': 'application/json'}, 10 | }; 11 | 12 | var req = https.request(options, function (res) { 13 | res.on('data', function (data) { 14 | console.log('Slack said', data); 15 | }).setEncoding('utf8'); 16 | }); 17 | 18 | if(!opts.rich) { 19 | opts = clean(opts); 20 | } 21 | req.write(JSON.stringify(opts)); 22 | req.end(); 23 | }; 24 | 25 | function clean (opts) { 26 | return { 27 | username: opts.name, 28 | icon_emoji: opts.icon || ':ghost:', 29 | text: opts.text, 30 | channel: opts.chan, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /web.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser') 3 | var logfmt = require('logfmt'); 4 | var https = require('https'); 5 | 6 | var slack = require('./slack'); 7 | var handlers = require('./handlers'); 8 | 9 | var app = express(); 10 | app.use(logfmt.requestLogger()); 11 | app.use(bodyParser.json({ type: 'text/plain' })); // because SNS doesn't type 12 | 13 | app.post('/channel/:channel', function(req, res) { 14 | if (!req.body) { 15 | console.warn('[WARN] no body received'); 16 | res.send('no body...?'); 17 | 18 | } else if (req.body.SubscribeURL) { 19 | subscribe(req.body, res, '#' + req.params.channel); 20 | 21 | } else if (req.body.Message) { 22 | message(req.body, res, '#' + req.params.channel); 23 | 24 | } else { 25 | console.warn('[WARN] meaningless body received.', Object.keys(req.body)); 26 | res.send('wut?'); 27 | } 28 | }); 29 | 30 | var port = Number(process.env.PORT || 5000); 31 | app.listen(port, function() { 32 | console.log('Listening on', port); 33 | }); 34 | 35 | 36 | function subscribe (body, res, channel) { 37 | var subUrl = body.SubscribeURL; 38 | console.log('Got subscription request, visiting', subUrl); 39 | 40 | https.get(subUrl, function (result) { 41 | console.log('Subscribed with ', result.statusCode); 42 | slack.send({text: 'FYI: I was subscribed to ' + body.TopicArn, chan: channel}); 43 | res.send('i gotcha, amazon'); 44 | 45 | }).on('error', function (e) { 46 | console.log('Error while subscribing:', e.message); 47 | res.send('sub error!?'); 48 | }); 49 | } 50 | 51 | function message (body, res, channel) { 52 | console.log('Got', body.Type, 'via', body.TopicArn, 'timestamped', body.Timestamp, 53 | 'with', body.Message.length, 'bytes'); 54 | 55 | var msg = {text: body.Message}; 56 | try { 57 | var msg = JSON.parse(body.Message); 58 | } catch (ex) {} 59 | 60 | var opts; 61 | if (msg.incident) { 62 | opts = handlers.stackdriver(msg); 63 | } else if (msg.AlarmName) { 64 | opts = handlers.cloudwatch(msg); 65 | } else if (msg.AutoScalingGroupName) { 66 | opts = handlers.autoscaling(msg); 67 | } else if (msg.type) { 68 | opts = handlers.alarm(msg); 69 | opts.channel = channel; 70 | } else if (msg.text) { 71 | opts = handlers.plaintext(msg); 72 | } else { 73 | opts = { 74 | icon: ':interrobang:', 75 | text: 'Unrecognized SNS message ```' + body.Message + '```', 76 | }; 77 | } 78 | 79 | if (!opts) { 80 | console.info('Dropping message on behalf of handler'); 81 | res.send('skipped'); 82 | return; 83 | } 84 | 85 | if (!opts.name && !opts.rich) { 86 | opts.name = body.Subject || 'Amazon SNS bridge'; 87 | } 88 | 89 | if (!opts.chan) { 90 | opts.chan = channel; 91 | } 92 | 93 | slack.send(opts); 94 | res.send('thanks for the heads-up'); 95 | } 96 | --------------------------------------------------------------------------------