diff --git a/README.md b/README.md index 7f74b4a..1bf0998 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ By default the logs are formatted like so: `[LOG_LEVEL] message`, unless you spe ##Custom Formatter Options > Check the [slack docs](https://api.slack.com/incoming-webhooks) for custom formatter options. +##Rate limit + +Since Slack has a rate limit of one message per second, there is an option for how many messages that are allowed to be sent. + +The option is called `rate_limit_interval` and the value is the interval time between each attempt to send a message. Setting `1000` would make the application maximum send 1 message per second, `100` 10 messages per second and so forth. Messages are stored in a circular queue and the eldest message will be discarded if more than 10 messages is put on the queue. + ###Putting it all together ```javascript var bunyan = require("bunyan"), diff --git a/lib/bunyan-slack.js b/lib/bunyan-slack.js index 004c151..789bea1 100644 --- a/lib/bunyan-slack.js +++ b/lib/bunyan-slack.js @@ -1,16 +1,19 @@ var util = require('util'), -request = require('request'), -extend = require('extend.js'); + request = require('request'), + extend = require('extend.js'), + CBuffer = require('CBuffer'); + +var BunyanSlack = function (options, error) { -function BunyanSlack(options, error) { options = options || {}; if (!options.webhook_url && !options.webhookUrl) { throw new Error('webhook url cannot be null'); } else { this.customFormatter = options.customFormatter; - this.webhook_url = options.webhook_url || options.webhookUrl; - this.error = error || function() {}; + this.webhook_url = options.webhook_url || options.webhookUrl; + this.error = error || function () {}; + this.messageQueue = new CBuffer(10); if (options.icon_url || options.iconUrl) { this.icon_url = options.icon_url || options.iconUrl; @@ -28,34 +31,41 @@ function BunyanSlack(options, error) { this.username = options.username; } + if(options.rate_limit_interval !== undefined) { + this.rate_limit_interval = options.rate_limit_interval; + // Since slack can only handle one request per second + // messages are stacked on a queue and we pick the last + // message each second and send to slack. + setInterval(this.intervalRunner.bind(this), this.rate_limit_interval); + } + + this.nameFromLevel = { + 10: 'trace', + 20: 'debug', + 30: 'info', + 40: 'warn', + 50: 'error', + 60: 'fatal' + }; + } -} - -var nameFromLevel = { - 10: 'trace', - 20: 'debug', - 30: 'info', - 40: 'warn', - 50: 'error', - 60: 'fatal' }; BunyanSlack.prototype.write = function write(record) { - var self = this, - levelName, - message; - - if (typeof record === 'string') { - record = JSON.parse(record); - } + var self = this, + levelName, + message; - levelName = nameFromLevel[record.level]; + if (typeof record === 'string') { + record = JSON.parse(record); + } + levelName = this.nameFromLevel[record.level]; try { message = self.customFormatter ? self.customFormatter(record, levelName) : { - text: util.format('[%s] %s', levelName.toUpperCase(), record.msg) - }; - } catch(err) { + text: util.format('[%s] %s', levelName.toUpperCase(), record.msg) + }; + } catch (err) { return self.error(err); } var base = { @@ -64,16 +74,32 @@ BunyanSlack.prototype.write = function write(record) { icon_url: self.icon_url, icon_emoji: self.icon_emoji }; - message = extend(base, message); + if(self.rate_limit_interval !== undefined) { + self.messageQueue.push(message); + } else { + request.post({ + url: self.webhook_url, + body: JSON.stringify(message) + }) + .on('error', function(err) { + return self.error(err); + }); + } +}; - request.post({ - url: self.webhook_url, - body: JSON.stringify(message) - }) - .on('error', function(err) { - return self.error(err); - }); +BunyanSlack.prototype.intervalRunner = function intervalRunner() { + var self = this; + var message = self.messageQueue.pop(); + if(message !== undefined) { + request.post({ + url: self.webhook_url, + body: JSON.stringify(message) + }).on('error', function(err) { + return self.error(err); + }); + + } }; module.exports = BunyanSlack; diff --git a/package.json b/package.json index 9863571..779eefe 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "slack stream for bunyan", "main": "./lib/bunyan-slack", "dependencies": { + "CBuffer": "^2.0.0", "extend.js": "0.0.2", "request": "^2.51.0" }, diff --git a/test/bunyan_slack_test.js b/test/bunyan_slack_test.js index 5d6db59..30e64e0 100644 --- a/test/bunyan_slack_test.js +++ b/test/bunyan_slack_test.js @@ -54,11 +54,45 @@ describe('bunyan-slack', function() { }), url: 'mywebhookurl' }; - log.info('foobar'); sinon.assert.calledWith(request.post, expectedResponse); }); + it('should only deliver last message if there is a message burst', function() { + var log = Bunyan.createLogger({ + name: 'myapp', + stream: new BunyanSlack({ + webhook_url: 'mywebhookurl', + channel: '#bunyan-slack', + username: '@sethpollack', + icon_emoji: ':smile:', + icon_url: 'http://www.gravatar.com/avatar/3f5ce68fb8b38a5e08e7abe9ac0a34f1?s=200', + rate_limit_interval: 1000 + }), + level: 'info' + }); + + var expectedResponse = { + body: JSON.stringify({ + channel: '#bunyan-slack', + username: '@sethpollack', + icon_url: 'http://www.gravatar.com/avatar/3f5ce68fb8b38a5e08e7abe9ac0a34f1?s=200', + icon_emoji: ':smile:', + text: '[INFO] foobar 999' + }), + url: 'mywebhookurl' + }; + + for(var i=0; i>1000; i++) { + log.info('foobar ' + i); + } + + setTimeout(function() { + sinon.assert.calledWith(request.post, expectedResponse); + }, 1000); + + }); + it('should use the custom formatter', function() { var log = Bunyan.createLogger({ name: 'myapp', @@ -149,71 +183,91 @@ describe('bunyan-slack', function() { }); }); - describe('loggger arguments', function() { - it('should accept a single string argument', function() { - var log = Bunyan.createLogger({ - name: 'myapp', - stream: new BunyanSlack({ - webhook_url: 'mywebhookurl' - }), - level: 'info' - }); + describe('logger arguments', function() { + it('should accept a single string argument', function () { + var log = Bunyan.createLogger({ + name: 'myapp', + stream: new BunyanSlack({ + webhook_url: 'mywebhookurl' + }), + level: 'info' + }); - var expectedResponse = { - body: JSON.stringify({ - text: '[INFO] foobar' - }), - url: 'mywebhookurl' - }; + var expectedResponse = { + body: JSON.stringify({ + text: '[INFO] foobar' + }), + url: 'mywebhookurl' + }; - log.info('foobar'); - sinon.assert.calledWith(request.post, expectedResponse); - }); + log.info('foobar'); + sinon.assert.calledWith(request.post, expectedResponse); + }); - it('should accept a single object argument', function() { - var log = Bunyan.createLogger({ - name: 'myapp', - stream: new BunyanSlack({ - webhook_url: 'mywebhookurl', - customFormatter: function(record, levelName) { - return {text: util.format('[%s] %s', levelName.toUpperCase(), record.error)}; - } - }), - level: 'info' - }); + it('should accept a single object argument', function () { + var log = Bunyan.createLogger({ + name: 'myapp', + stream: new BunyanSlack({ + webhook_url: 'mywebhookurl', + customFormatter: function (record, levelName) { + return {text: util.format('[%s] %s', levelName.toUpperCase(), record.error)}; + } + }), + level: 'info' + }); - var expectedResponse = { - body: JSON.stringify({ - text: '[INFO] foobar' - }), - url: 'mywebhookurl' - }; + var expectedResponse = { + body: JSON.stringify({ + text: '[INFO] foobar' + }), + url: 'mywebhookurl' + }; - log.info({error: 'foobar'}); - sinon.assert.calledWith(request.post, expectedResponse); - }); + log.info({error: 'foobar'}); + sinon.assert.calledWith(request.post, expectedResponse); + }); - it('should accept an object and string as arguments', function() { - var log = Bunyan.createLogger({ - name: 'myapp', - stream: new BunyanSlack({ - webhook_url: 'mywebhookurl', - customFormatter: function(record, levelName) { - return {text: util.format('[%s] %s & %s', levelName.toUpperCase(), record.error, record.msg)}; - } - }), - level: 'info' - }); + it('should accept an object and string as arguments', function () { + var log = Bunyan.createLogger({ + name: 'myapp', + stream: new BunyanSlack({ + webhook_url: 'mywebhookurl', + customFormatter: function (record, levelName) { + return {text: util.format('[%s] %s & %s', levelName.toUpperCase(), record.error, record.msg)}; + } + }), + level: 'info' + }); + + var expectedResponse = { + body: JSON.stringify({ + text: '[INFO] this is the error & this is the message' + }), + url: 'mywebhookurl' + }; + log.info({error: 'this is the error'}, 'this is the message'); + sinon.assert.calledWith(request.post, expectedResponse); + }); + + it('should return a empty message queue after running the interval', function () { + var bs = new BunyanSlack({ + webhook_url: 'mywebhookurl', + rate_limit_interval: 1000 + }); + bs.messageQueue.push('message'); + bs.intervalRunner(); + expect(bs.messageQueue.pop()).to.equal(undefined); + }); + + it('should return a message on the queue', function () { + var bs = new BunyanSlack({ + webhook_url: 'mywebhookurl', + rate_limit_interval: 1000 + }); + bs.write({ level: 60, msg: 'Hello Sir' }); + expect(bs.messageQueue.pop().text).to.equal('[FATAL] Hello Sir'); + }); - var expectedResponse = { - body: JSON.stringify({ - text: '[INFO] this is the error & this is the message' - }), - url: 'mywebhookurl' - }; - log.info({error: 'this is the error'}, 'this is the message'); - sinon.assert.calledWith(request.post, expectedResponse); - }); }); });