├── .travis.yml ├── .gitignore ├── .editorconfig ├── package.json ├── LICENSE ├── README.md ├── lib └── index.js └── test └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 8 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | npm-debug.log 4 | dump.rdb 5 | node_modules 6 | results.tap 7 | results.xml 8 | npm-shrinkwrap.json 9 | config.json 10 | .DS_Store 11 | */.DS_Store 12 | */*/.DS_Store 13 | ._* 14 | */._* 15 | */*/._* 16 | coverage.* 17 | lib-cov 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "good-slack", 3 | "version": "4.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/dmacosta/good-slack.git" 7 | }, 8 | "description": "Slack Webhook message posting for Good process monitor", 9 | "main": "lib/index.js", 10 | "scripts": { 11 | "test": "lab -t 100 -vLa @hapi/code" 12 | }, 13 | "author": "Diego Acosta ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@hapi/hoek": "6.x.x", 17 | "@hapi/wreck": "15.x.x", 18 | "json-stringify-safe": "5.x.x", 19 | "moment": "2.x.x" 20 | }, 21 | "devDependencies": { 22 | "@hapi/code": "5.x.x", 23 | "@hapi/lab": "18.x.x" 24 | }, 25 | "peerDependencies": { 26 | "@hapi/good": ">= 8.x.x" 27 | }, 28 | "engines": { 29 | "node": ">= 8.x.x" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/dmacosta/good-slack/issues" 33 | }, 34 | "homepage": "https://github.com/dmacosta/good-slack#readme", 35 | "directories": { 36 | "test": "test" 37 | }, 38 | "keywords": [ 39 | "hapi", 40 | "good", 41 | "reporter", 42 | "slack" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Diego Acosta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # good-slack 2 | 3 | Slack Webhook message posting for Good process monitor 4 | 5 | [![Build Status](https://travis-ci.org/nakardo/good-slack.svg)](https://travis-ci.org/nakardo/good-slack) ![Current Version](https://img.shields.io/npm/v/good-slack.svg) 6 | 7 | ## Usage 8 | 9 | `good-slack` is a [good](https://github.com/hapijs/good) reporter implementation to send [hapi](http://hapijs.com/) server events to 10 | [Slack](https://api.slack.com/) using Incoming Webhooks. 11 | 12 | ## `new GoodSlack(config)` 13 | Creates a new GoodSlack object with the following arguments: 14 | 15 | - `config` - config object 16 | - `url` - a string with the Webhook URL 17 | - `[slack]` - an object of slack overridable parameters (See [Incoming Webhooks](https://api.slack.com/incoming-webhooks)) 18 | - `[format]` - [MomentJS](http://momentjs.com/docs/#/displaying/format/) format string. Defaults to 'YYMMDD/HHmmss.SSS'. 19 | - `[host]` - a string with the server hostname. - Defaults to actual hostname. 20 | - `[basicLogEvent]` - a boolean to set the style of `log` events. When set to true, `log` events will be sent as text instead of attachments. Defaults to `false`. 21 | 22 | ## Using with Hapi 23 | 24 | Below is an example, based on the [hapi plugin documentation examples](https://hapijs.com/tutorials/plugins), of using `good-slack` and `good-squeeze` together in a Hapi server to log all internal error messages to a slack channel. 25 | 26 | ```js 27 | const Hapi = require('@hapi/hapi'); 28 | const start = async function () { 29 | 30 | const server = Hapi.server(); 31 | 32 | await server.register({ 33 | plugin: require('@hapi/good'), 34 | options: { 35 | reporters: { 36 | slack: [{ 37 | module: '@hapi/good-squeeze', 38 | name: 'Squeeze', 39 | args: [{ error: '*' }] 40 | }, { 41 | module: 'good-slack', 42 | args: [{ url: 'https://hook.slack.com/services/UNIQUE_SLACK_CHANNEL_URL' }] 43 | }] 44 | } 45 | } 46 | }) 47 | }; 48 | ``` 49 | 50 | ## Compatibility 51 | 52 | * This version (v4) is compatible with `@hapi/good@8.x.x`, in which hapi moved to scoped package names. 53 | * Use v3 for `good@7.x.x`, which introduced major changes on [reporter interface](https://github.com/hapijs/good/blob/master/API.md#reporter-interface). 54 | * Use v2 for legacy support of `good@6.x.x`. 55 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Os = require('os'); 6 | const Util = require('util'); 7 | const Stream = require('stream'); 8 | const Hoek = require('@hapi/hoek'); 9 | const Wreck = require('@hapi/wreck'); 10 | const Stringify = require('json-stringify-safe'); 11 | const Moment = require('moment'); 12 | 13 | 14 | // Declare internals 15 | 16 | const internals = { 17 | defaults: { 18 | slack: {}, 19 | format: 'YYMMDD/HHmmss.SSS', 20 | host: Os.hostname() 21 | } 22 | }; 23 | 24 | 25 | internals.utility = { 26 | codeFormat: (data) => Util.format('```\n%s\n```', data), 27 | 28 | createAttachment(event, payload, config) { 29 | 30 | const time = Moment.utc(event.timestamp).format(config.format); 31 | 32 | const defaults = { 33 | pretext: Util.format('`%s` event from *%s* at %s', event.event, 34 | config.host, time), 35 | 'mrkdwn_in': ['pretext', 'text', 'fields'] 36 | }; 37 | 38 | return Hoek.merge(defaults, payload); 39 | }, 40 | 41 | formatOps(event) { 42 | 43 | const mem = Math.round(event.proc.mem.rss / (1024 * 1024)) + ' Mb.'; 44 | const load = event.os.load.map((v) => v.toFixed(2)); 45 | 46 | return { 47 | fallback: `L: ${load[1]} | M: ${mem} | U: ${event.proc.uptime}`, 48 | fields: [ 49 | { 50 | title: 'Memory', 51 | value: mem, 52 | short: true 53 | }, { 54 | title: 'Uptime (seconds)', 55 | value: event.proc.uptime, 56 | short: true 57 | }, { 58 | title: 'Load', 59 | value: load.join(' | '), 60 | short: true 61 | } 62 | ] 63 | }; 64 | }, 65 | 66 | formatResponse(event) { 67 | 68 | const method = event.method.toUpperCase(); 69 | const query = Stringify(event.query); 70 | 71 | const text = `*${method}* ${event.path} ${query} ${event.statusCode} ` + 72 | `(${event.responseTime}ms)`; 73 | 74 | return { 75 | fallback: `${event.statusCode} ${method} ${event.path}`, 76 | color: event.statusCode >= 400 ? 'danger' : 'good', 77 | text 78 | }; 79 | }, 80 | 81 | formatError(event) { 82 | 83 | const message = `${event.error.name}: ${event.error.message}`; 84 | 85 | return { 86 | fallback: message, 87 | text: `*${event.method.toUpperCase()}* ${event.url.pathname}`, 88 | color: 'danger', 89 | fields: [ 90 | { 91 | title: 'Error', 92 | value: message 93 | }, { 94 | title: 'Stack', 95 | value: internals.utility.codeFormat(event.error.stack) 96 | } 97 | ] 98 | }; 99 | }, 100 | 101 | formatRequest(event) { 102 | 103 | let data = event.data; 104 | let message = event.data; 105 | 106 | const text = `*${event.method.toUpperCase()}* ${event.path}`; 107 | const tags = event.tags.join(', '); 108 | 109 | if (typeof event.data === 'object') { 110 | data = internals.utility.codeFormat(Stringify(event.data, null, 2)); 111 | message = Stringify(event.data); 112 | } 113 | 114 | return { 115 | fallback: `${tags} ${message}`, 116 | text, 117 | color: event.tags.indexOf('error') > -1 ? 'danger' : undefined, 118 | fields: [ 119 | { title: 'PID', value: event.pid }, 120 | { title: 'Request ID', value: event.id }, 121 | { title: 'Tags', value: tags }, 122 | { title: 'Data', value: data } 123 | ] 124 | }; 125 | }, 126 | 127 | formatLog(event) { 128 | 129 | const tags = event.tags || []; 130 | let data = event.data; 131 | let message = event.data; 132 | 133 | if (typeof event.data === 'object') { 134 | data = internals.utility.codeFormat(Stringify(event.data, null, 2)); 135 | message = Stringify(event.data); 136 | } 137 | 138 | return { 139 | fallback: `${tags} ${message}`.trim(), 140 | fields: [ 141 | { title: 'Tags', value: tags.toString() }, 142 | { title: 'Data', value: data } 143 | ] 144 | }; 145 | }, 146 | 147 | formatBasic(event) { 148 | 149 | return { 150 | text: event.data 151 | }; 152 | } 153 | }; 154 | 155 | 156 | class GoodSlack extends Stream.Writable { 157 | 158 | constructor(config) { 159 | 160 | config = config || {}; 161 | 162 | Hoek.assert(typeof config.url === 'string', 'url must be a string'); 163 | 164 | super({ objectMode: true }); 165 | this._config = Hoek.applyToDefaults(internals.defaults, config); 166 | } 167 | 168 | _write(data, encoding, next) { 169 | 170 | let content; 171 | 172 | switch (data.event) { 173 | case 'ops': 174 | content = internals.utility.formatOps(data); 175 | break; 176 | case 'response': 177 | content = internals.utility.formatResponse(data); 178 | break; 179 | case 'error': 180 | content = internals.utility.formatError(data); 181 | break; 182 | case 'request': 183 | content = internals.utility.formatRequest(data); 184 | break; 185 | default: 186 | const formatStyle = (this._config.basicLogEvent) ? 'formatBasic' : 'formatLog'; 187 | content = internals.utility[formatStyle](data); 188 | } 189 | 190 | let message = {}; 191 | 192 | if (this._config.basicLogEvent) { 193 | message = content; 194 | } 195 | else { 196 | message.attachments = [internals.utility.createAttachment(data, content, this._config)]; 197 | } 198 | 199 | this._send(message, next); 200 | } 201 | 202 | _send(messagePayload, callback) { 203 | 204 | const payload = Hoek.applyToDefaults(this._config.slack, messagePayload); 205 | 206 | const data = { 207 | payload: Stringify(payload) 208 | }; 209 | 210 | Wreck.post(this._config.url, data) 211 | .then((result) => callback(null, result)) 212 | .catch(callback); 213 | } 214 | } 215 | 216 | 217 | module.exports = GoodSlack; 218 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Util = require('util'); 6 | const Http = require('http'); 7 | const Stream = require('stream'); 8 | const Code = require('@hapi/code'); 9 | const Hoek = require('@hapi/hoek'); 10 | const Lab = require('@hapi/lab'); 11 | const Moment = require('moment'); 12 | const Stringify = require('json-stringify-safe'); 13 | const GoodSlack = require('..'); 14 | 15 | 16 | // Declare internals 17 | 18 | const internals = { 19 | readStream() { 20 | 21 | const result = new Stream.Readable({ objectMode: true }); 22 | result._read = () => { }; 23 | return result; 24 | }, 25 | 26 | getUri(server) { 27 | 28 | const address = server.address(); 29 | return `http://${address.address}:${address.port}`; 30 | } 31 | }; 32 | 33 | 34 | internals.events = { 35 | ops: { 36 | event: 'ops', 37 | timestamp: 1411583264547, 38 | os: { 39 | load: [1.650390625, 1.6162109375, 1.65234375], 40 | mem: { total: 17179869184, free: 8190681088 }, 41 | uptime: 704891 42 | }, 43 | proc: { 44 | uptime: 6, 45 | mem: { 46 | rss: 30019584, 47 | heapTotal: 18635008, 48 | heapUsed: 9989304 49 | }, 50 | delay: 0.03084501624107361 51 | }, 52 | load: { requests: {}, concurrents: {}, responseTimes: {} }, 53 | pid: 64291 54 | }, 55 | 56 | response: { 57 | event: 'response', 58 | method: 'post', 59 | statusCode: 200, 60 | timestamp: Date.now(), 61 | instance: 'localhost', 62 | path: '/data', 63 | responseTime: 150, 64 | query: { 65 | name: 'diego' 66 | }, 67 | responsePayload: { 68 | foo: 'bar', 69 | value: 1 70 | } 71 | }, 72 | 73 | request: { 74 | event: 'request', 75 | timestamp: Date.now(), 76 | tags: ['info'], 77 | path: '/data', 78 | method: 'post', 79 | data: 'This is a request log', 80 | pid: '10001', 81 | id: '23147901234:Machine1:73489:8uasdf98:10000' 82 | }, 83 | 84 | error: { 85 | event: 'error', 86 | timestamp: 1418869888194, 87 | url: { 88 | protocol: null, 89 | slashes: null, 90 | auth: null, 91 | host: null, 92 | port: null, 93 | hostname: null, 94 | hash: null, 95 | search: '?name=diego', 96 | query: { name: 'diego' }, 97 | pathname: '/search', 98 | path: '/search?name=diego', 99 | href: '/search?name=diego' 100 | }, 101 | method: 'get', 102 | pid: 91426, 103 | error: new Error('Something bad had happened') 104 | }, 105 | 106 | log: { 107 | event: 'log', 108 | timestamp: 1418873719797, 109 | tags: ['info'], 110 | data: 'Server started at http://localhost', 111 | pid: 92682 112 | } 113 | }; 114 | 115 | 116 | // Test shortcuts 117 | 118 | const lab = exports.lab = Lab.script(); 119 | const expect = Code.expect; 120 | const describe = lab.describe; 121 | const it = lab.it; 122 | 123 | 124 | it('has to be created with new', () => { 125 | 126 | const reporter = new GoodSlack({ url: 'localhost' }); 127 | expect(reporter).to.exist(); 128 | }); 129 | 130 | it('throws an error if no config is passed', () => { 131 | 132 | expect(() => new GoodSlack()).to.throw('url must be a string'); 133 | }); 134 | 135 | it('throws an error if missing url', () => { 136 | 137 | expect(() => new GoodSlack({ })).to.throw('url must be a string'); 138 | }); 139 | 140 | it('applies config to defaults', () => { 141 | 142 | const config = { 143 | url: 'localhost', 144 | slack: { 145 | username: 'testing-bot', 146 | channel: '#test' 147 | }, 148 | format: 'lll', 149 | host: 'localhost' 150 | }; 151 | 152 | const reporter = new GoodSlack(config); 153 | expect(reporter).to.exist(); 154 | 155 | expect(reporter._config).to.equal(config); 156 | }); 157 | 158 | describe('events', () => { 159 | 160 | const now = Date.now(); 161 | const timestamp = Moment.utc(now).format('YYMMDD/HHmmss.SSS'); 162 | 163 | it('sends message on "response" event on success', async () => { 164 | 165 | return await new Promise((resolve) => { 166 | 167 | const payload = Stringify({ 168 | attachments: [{ 169 | pretext: '`response` event from *localhost* at ' + timestamp, 170 | 'mrkdwn_in': ['pretext','text','fields'], 171 | fallback: '200 POST /data', 172 | color: 'good', 173 | text: '*POST* /data {"name":"diego"} 200 (150ms)' 174 | }] 175 | }); 176 | 177 | const stream = internals.readStream(); 178 | const server = Http.createServer((req, res) => { 179 | 180 | let data = ''; 181 | 182 | req.on('data', (chunk) => { 183 | 184 | data += chunk; 185 | }); 186 | 187 | req.on('end', () => { 188 | 189 | expect(data).to.equal(payload); 190 | res.end(); 191 | server.close(); 192 | return resolve(); 193 | }); 194 | }); 195 | 196 | server.listen(0, 'localhost', () => { 197 | 198 | const reporter = new GoodSlack({ 199 | url: internals.getUri(server), 200 | host: 'localhost' 201 | }); 202 | 203 | const event = Hoek.clone(internals.events.response); 204 | event.timestamp = now; 205 | 206 | stream.pipe(reporter); 207 | stream.push(event); 208 | }); 209 | }); 210 | }); 211 | 212 | it('sends message on "request" event with object', async () => { 213 | 214 | return await new Promise((resolve) => { 215 | 216 | const objectData = { name: 'diego' }; 217 | const stringifiedData = '{"name":"diego"}'; 218 | const prettifiedData = '{\n "name": "diego"\n}'; 219 | 220 | const payload = Stringify({ 221 | attachments: [{ 222 | pretext: '`request` event from *localhost* at ' + timestamp, 223 | 'mrkdwn_in': ['pretext','text','fields'], 224 | fallback: `info ${stringifiedData}`, 225 | text: '*POST* /data', 226 | fields: [{ 227 | title: 'PID', 228 | value: '10001' 229 | }, { 230 | title: 'Request ID', 231 | value: '23147901234:Machine1:73489:8uasdf98:10000' 232 | }, { 233 | title: 'Tags', 234 | value: 'info' 235 | }, { 236 | title: 'Data', 237 | value: Util.format('```\n%s\n```', prettifiedData) 238 | }] 239 | }] 240 | }); 241 | 242 | const stream = internals.readStream(); 243 | const server = Http.createServer((req, res) => { 244 | 245 | let data = ''; 246 | 247 | req.on('data', (chunk) => { 248 | 249 | data += chunk; 250 | }); 251 | 252 | req.on('end', () => { 253 | 254 | expect(data).to.equal(payload); 255 | res.end(); 256 | server.close(); 257 | return resolve(); 258 | }); 259 | }); 260 | 261 | server.listen(0, 'localhost', () => { 262 | 263 | const reporter = new GoodSlack({ 264 | url: internals.getUri(server), 265 | host: 'localhost' 266 | }); 267 | 268 | const event = Hoek.clone(internals.events.request); 269 | event.timestamp = now; 270 | event.data = objectData; 271 | 272 | stream.pipe(reporter); 273 | stream.push(event); 274 | }); 275 | }); 276 | }); 277 | 278 | it('sends message on "request" event on error', async () => { 279 | 280 | return await new Promise((resolve) => { 281 | 282 | const payload = Stringify({ 283 | attachments: [{ 284 | pretext: '`request` event from *localhost* at ' + timestamp, 285 | 'mrkdwn_in': ['pretext','text','fields'], 286 | fallback: 'error This is a request log', 287 | text: '*POST* /data', 288 | color: 'danger', 289 | fields: [{ 290 | title: 'PID', 291 | value: '10001' 292 | }, { 293 | title: 'Request ID', 294 | value: '23147901234:Machine1:73489:8uasdf98:10000' 295 | }, { 296 | title: 'Tags', 297 | value: 'error' 298 | }, { 299 | title: 'Data', 300 | value: 'This is a request log' 301 | }] 302 | }] 303 | }); 304 | 305 | const stream = internals.readStream(); 306 | const server = Http.createServer((req, res) => { 307 | 308 | let data = ''; 309 | 310 | req.on('data', (chunk) => { 311 | 312 | data += chunk; 313 | }); 314 | 315 | req.on('end', () => { 316 | 317 | expect(data).to.equal(payload); 318 | res.end(); 319 | server.close(); 320 | return resolve(); 321 | }); 322 | }); 323 | 324 | server.listen(0, 'localhost', () => { 325 | 326 | const reporter = new GoodSlack({ 327 | url: internals.getUri(server), 328 | host: 'localhost' 329 | }); 330 | 331 | const event = Hoek.clone(internals.events.request); 332 | event.tags = ['error']; 333 | event.timestamp = now; 334 | 335 | stream.pipe(reporter); 336 | stream.push(event); 337 | }); 338 | }); 339 | }); 340 | 341 | it('sends message on "response" event on error', async () => { 342 | 343 | return await new Promise((resolve) => { 344 | 345 | const payload = Stringify({ 346 | attachments: [{ 347 | pretext: '`response` event from *localhost* at ' + timestamp, 348 | 'mrkdwn_in': ['pretext','text','fields'], 349 | fallback: '404 POST /data', 350 | color: 'danger', 351 | text: '*POST* /data {"name":"diego"} 404 (150ms)' 352 | }] 353 | }); 354 | 355 | const stream = internals.readStream(); 356 | const server = Http.createServer((req, res) => { 357 | 358 | let data = ''; 359 | 360 | req.on('data', (chunk) => { 361 | 362 | data += chunk; 363 | }); 364 | 365 | req.on('end', () => { 366 | 367 | expect(data).to.equal(payload); 368 | res.end(); 369 | server.close(); 370 | return resolve(); 371 | }); 372 | }); 373 | 374 | server.listen(0, 'localhost', () => { 375 | 376 | const reporter = new GoodSlack({ 377 | url: internals.getUri(server), 378 | host: 'localhost' 379 | }); 380 | 381 | const event = Hoek.clone(internals.events.response); 382 | event.timestamp = now; 383 | event.statusCode = 404; 384 | 385 | stream.pipe(reporter); 386 | stream.push(event); 387 | }); 388 | }); 389 | }); 390 | 391 | it('sends message on "ops" event', async () => { 392 | 393 | return await new Promise((resolve) => { 394 | 395 | const payload = Stringify({ 396 | attachments: [{ 397 | pretext: '`ops` event from *localhost* at ' + timestamp, 398 | 'mrkdwn_in': ['pretext','text','fields'], 399 | fallback: 'L: 1.62 | M: 29 Mb. | U: 6', 400 | fields: [{ 401 | title: 'Memory', 402 | value:'29 Mb.', 403 | short: true 404 | }, { 405 | title: 'Uptime (seconds)', 406 | value: 6, 407 | short: true 408 | }, { 409 | title: 'Load', 410 | value: '1.65 | 1.62 | 1.65', 411 | short: true 412 | }] 413 | }] 414 | }); 415 | 416 | const stream = internals.readStream(); 417 | const server = Http.createServer((req, res) => { 418 | 419 | let data = ''; 420 | 421 | req.on('data', (chunk) => { 422 | 423 | data += chunk; 424 | }); 425 | 426 | req.on('end', () => { 427 | 428 | expect(data).to.equal(payload); 429 | res.end(); 430 | server.close(); 431 | return resolve(); 432 | }); 433 | }); 434 | 435 | server.listen(0, 'localhost', () => { 436 | 437 | const reporter = new GoodSlack({ 438 | url: internals.getUri(server), 439 | host: 'localhost' 440 | }); 441 | 442 | const event = Hoek.clone(internals.events.ops); 443 | event.timestamp = now; 444 | event.statusCode = 404; 445 | 446 | stream.pipe(reporter); 447 | stream.push(event); 448 | }); 449 | }); 450 | }); 451 | 452 | it('sends message on "error" event', async () => { 453 | 454 | return await new Promise((resolve) => { 455 | 456 | const error = new Error('Something bad had happened'); 457 | error.stack = 'Error: Something bad had happened\n' + 458 | ' at Object. (/good-slack/test/index.js:79:10)'; 459 | 460 | const payload = Stringify({ 461 | attachments: [{ 462 | pretext: '`error` event from *localhost* at ' + timestamp, 463 | 'mrkdwn_in': ['pretext','text','fields'], 464 | fallback: 'Error: Something bad had happened', 465 | text: '*GET* /search', 466 | color: 'danger', 467 | fields:[{ 468 | title: 'Error', 469 | value: 'Error: Something bad had happened' 470 | },{ 471 | title: 'Stack', 472 | value: Util.format('```\n%s\n```', error.stack) 473 | }] 474 | }] 475 | }); 476 | 477 | const stream = internals.readStream(); 478 | const server = Http.createServer((req, res) => { 479 | 480 | let data = ''; 481 | 482 | req.on('data', (chunk) => { 483 | 484 | data += chunk; 485 | }); 486 | 487 | req.on('end', () => { 488 | 489 | expect(data).to.equal(payload); 490 | res.end(); 491 | server.close(); 492 | return resolve(); 493 | }); 494 | }); 495 | 496 | server.listen(0, 'localhost', () => { 497 | 498 | const reporter = new GoodSlack({ 499 | url: internals.getUri(server), 500 | host: 'localhost' 501 | }); 502 | 503 | const event = Hoek.clone(internals.events.error); 504 | event.timestamp = now; 505 | event.error = error; 506 | 507 | stream.pipe(reporter); 508 | stream.push(event); 509 | }); 510 | }); 511 | }); 512 | 513 | it('sends message on "log" string event', async () => { 514 | 515 | return await new Promise((resolve) => { 516 | 517 | const payload = Stringify({ 518 | attachments: [{ 519 | pretext: '`log` event from *localhost* at ' + timestamp, 520 | 'mrkdwn_in': ['pretext','text','fields'], 521 | fallback: 'info Server started at http://localhost', 522 | fields: [{ 523 | title: 'Tags', 524 | value: 'info' 525 | }, { 526 | title: 'Data', 527 | value: 'Server started at http://localhost' 528 | }] 529 | }] 530 | }); 531 | 532 | const stream = internals.readStream(); 533 | const server = Http.createServer((req, res) => { 534 | 535 | let data = ''; 536 | 537 | req.on('data', (chunk) => { 538 | 539 | data += chunk; 540 | }); 541 | 542 | req.on('end', () => { 543 | 544 | expect(data).to.equal(payload); 545 | res.end(); 546 | server.close(); 547 | return resolve(); 548 | }); 549 | }); 550 | 551 | server.listen(0, 'localhost', () => { 552 | 553 | const reporter = new GoodSlack({ 554 | url: internals.getUri(server), 555 | host: 'localhost' 556 | }); 557 | 558 | const event = Hoek.clone(internals.events.log); 559 | event.timestamp = now; 560 | 561 | stream.pipe(reporter); 562 | stream.push(event); 563 | }); 564 | }); 565 | }); 566 | 567 | it('sends message on "log" object event', async () => { 568 | 569 | return await new Promise((resolve) => { 570 | 571 | const objectData = { foo: 'bar', baz: 'foo' }; 572 | const stringifiedData = '{"foo":"bar","baz":"foo"}'; 573 | const prettifiedData = '{\n "foo": "bar",\n "baz": "foo"\n}'; 574 | 575 | const payload = Stringify({ 576 | attachments: [{ 577 | pretext: '`log` event from *localhost* at ' + timestamp, 578 | 'mrkdwn_in': ['pretext','text','fields'], 579 | fallback: `info ${stringifiedData}`, 580 | fields: [{ 581 | title: 'Tags', 582 | value: 'info' 583 | }, { 584 | title: 'Data', 585 | value: Util.format('```\n%s\n```', prettifiedData) 586 | }] 587 | }] 588 | }); 589 | 590 | const stream = internals.readStream(); 591 | const server = Http.createServer((req, res) => { 592 | 593 | let data = ''; 594 | 595 | req.on('data', (chunk) => { 596 | 597 | data += chunk; 598 | }); 599 | 600 | req.on('end', () => { 601 | 602 | expect(data).to.equal(payload); 603 | res.end(); 604 | server.close(); 605 | return resolve(); 606 | }); 607 | }); 608 | 609 | server.listen(0, 'localhost', () => { 610 | 611 | const reporter = new GoodSlack({ 612 | url: internals.getUri(server), 613 | host: 'localhost' 614 | }); 615 | 616 | const event = Hoek.clone(internals.events.log); 617 | event.timestamp = now; 618 | event.data = objectData; 619 | 620 | stream.pipe(reporter); 621 | stream.push(event); 622 | }); 623 | }); 624 | }); 625 | 626 | it('sends message on "log" event without tags', async () => { 627 | 628 | return await new Promise((resolve) => { 629 | 630 | const payload = Stringify({ 631 | attachments: [{ 632 | pretext: '`log` event from *localhost* at ' + timestamp, 633 | 'mrkdwn_in': ['pretext','text','fields'], 634 | fallback: 'Server started at http://localhost', 635 | fields: [{ 636 | title: 'Tags', 637 | value: '' 638 | }, { 639 | title: 'Data', 640 | value: 'Server started at http://localhost' 641 | }] 642 | }] 643 | }); 644 | 645 | const stream = internals.readStream(); 646 | const server = Http.createServer((req, res) => { 647 | 648 | let data = ''; 649 | 650 | req.on('data', (chunk) => { 651 | 652 | data += chunk; 653 | }); 654 | 655 | req.on('end', () => { 656 | 657 | expect(data).to.equal(payload); 658 | res.end(); 659 | server.close(); 660 | return resolve(); 661 | }); 662 | }); 663 | 664 | server.listen(0, 'localhost', () => { 665 | 666 | const reporter = new GoodSlack({ 667 | url: internals.getUri(server), 668 | host: 'localhost' 669 | }); 670 | 671 | const event = Hoek.clone(internals.events.log); 672 | event.timestamp = now; 673 | delete event.tags; 674 | 675 | stream.pipe(reporter); 676 | stream.push(event); 677 | }); 678 | }); 679 | }); 680 | 681 | it('sends message on "log" event as basic text message', async () => { 682 | 683 | return await new Promise((resolve) => { 684 | 685 | const payload = Stringify({ 686 | text: 'Server started at http://localhost' 687 | }); 688 | 689 | const stream = internals.readStream(); 690 | const server = Http.createServer((req, res) => { 691 | 692 | let data = ''; 693 | 694 | req.on('data', (chunk) => { 695 | 696 | data += chunk; 697 | }); 698 | 699 | req.on('end', () => { 700 | 701 | expect(data).to.equal(payload); 702 | res.end(); 703 | server.close(); 704 | return resolve(); 705 | }); 706 | }); 707 | 708 | server.listen(0, 'localhost', () => { 709 | 710 | const reporter = new GoodSlack({ 711 | url: internals.getUri(server), 712 | host: 'localhost', 713 | basicLogEvent: true 714 | }); 715 | 716 | const event = Hoek.clone(internals.events.log); 717 | event.timestamp = now; 718 | delete event.tags; 719 | 720 | stream.pipe(reporter); 721 | stream.push(event); 722 | }); 723 | }); 724 | }); 725 | 726 | it('sends one message per event', async () => { 727 | 728 | return await new Promise((resolve) => { 729 | 730 | const payload = Stringify({ 731 | attachments: [{ 732 | pretext: '`response` event from *localhost* at ' + timestamp, 733 | 'mrkdwn_in': ['pretext','text','fields'], 734 | fallback: '404 POST /data', 735 | color: 'danger', 736 | text: '*POST* /data {"name":"diego"} 404 (150ms)' 737 | }] 738 | }); 739 | 740 | let hitCount = 0; 741 | 742 | const stream = internals.readStream(); 743 | const server = Http.createServer((req, res) => { 744 | 745 | let data = ''; 746 | hitCount++; 747 | 748 | req.on('data', (chunk) => { 749 | 750 | data += chunk; 751 | }); 752 | 753 | req.on('end', () => { 754 | 755 | expect(data).to.equal(payload); 756 | res.end(); 757 | 758 | if (hitCount === 2) { 759 | server.close(); 760 | return resolve(); 761 | } 762 | }); 763 | }); 764 | 765 | server.listen(0, 'localhost', () => { 766 | 767 | const reporter = new GoodSlack({ 768 | url: internals.getUri(server), 769 | host: 'localhost' 770 | }); 771 | 772 | const event = Hoek.clone(internals.events.response); 773 | event.timestamp = now; 774 | event.statusCode = 404; 775 | 776 | stream.pipe(reporter); 777 | stream.push(event); 778 | stream.push(event); 779 | }); 780 | }); 781 | }); 782 | 783 | it('handles slack write errors', async () => { 784 | 785 | return await new Promise((resolve) => { 786 | 787 | const stream = internals.readStream(); 788 | const server = Http.createServer((req, res) => { 789 | 790 | req.on('data', (chunk) => {}); 791 | 792 | req.on('end', () => { 793 | 794 | res.statusCode = 404; 795 | res.end(); 796 | }); 797 | }); 798 | 799 | server.listen(0, 'localhost', () => { 800 | 801 | const reporter = new GoodSlack({ 802 | url: internals.getUri(server), 803 | host: 'localhost' 804 | }); 805 | 806 | reporter.on('error', (e) => { 807 | 808 | expect(e.isBoom).to.be.true(); 809 | expect(e.output.statusCode).to.equal(404); 810 | return resolve(); 811 | }); 812 | 813 | const event = Hoek.clone(internals.events.log); 814 | event.timestamp = now; 815 | 816 | stream.pipe(reporter); 817 | stream.push(event); 818 | }); 819 | }); 820 | }); 821 | }); 822 | --------------------------------------------------------------------------------