Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
92 changes: 59 additions & 33 deletions lib/bunyan-slack.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 = {
Expand All @@ -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;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
172 changes: 113 additions & 59 deletions test/bunyan_slack_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
});
});
});

Expand Down