diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8bce80c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +# Description + +Please include a summary of the change and which issues are fixed. Please also include relevant motivation and context. + +Fixes: +- Link to Clubhouse story + +## Type of change + +Please delete options that are not relevant. + +- Frontend only. Little-to-no backend code was modified. +- Backend only. Does not require any frontend code changes. +- Affects the pipeline, (ETLs, Workers, Periodic). +- Requires a DB migration to be run. +- Affects signup/login/onboarding. +- Affects billing/pricing. +- Has required changes in other repos, (LIST THEM HERE). +- Other (styles, tests, build, etc). + +--- + +[PR Guidelines](https://github.com/rollbar/internal/blob/master/code-style/GUIDELINES.md) \ No newline at end of file diff --git a/README.md b/README.md index 1833c78..90b41c0 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,14 @@ If an array is specified as the `name` parameter each item in that array will be client.histogram('my_histogram', 42, 0.25, next); client.histogram('my_histogram', 42, ['tag'], next); client.histogram('my_histogram', 42, 0.25, ['tag'], next); + + // Send multiple metrics + var multi = client.multi(); + multi.set('my_unique', 'foobar'); + multi.increment(['these', 'are', 'different', 'stats']); + multi.gauge('my_gauge', 123.45); + // callback is optional + multi.send(next); ``` ## Errors diff --git a/lib/statsd.js b/lib/statsd.js index 9ef0a37..108aa19 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -1,53 +1,7 @@ var dgram = require('dgram'), dns = require('dns'); -/** - * The UDP Client for StatsD - * @param options - * @option host {String} The host to connect to default: localhost - * @option port {String|Integer} The port to connect to default: 8125 - * @option prefix {String} An optional prefix to assign to each stat name sent - * @option suffix {String} An optional suffix to assign to each stat name sent - * @option globalize {boolean} An optional boolean to add "statsd" as an object in the global namespace - * @option cacheDns {boolean} An optional option to only lookup the hostname -> ip address once - * @option mock {boolean} An optional boolean indicating this Client is a mock object, no stats are sent. - * @constructor - */ -var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock) { - var options = host || {}, - self = this; - - if(arguments.length > 1 || typeof(host) === 'string'){ - options = { - host : host, - port : port, - prefix : prefix, - suffix : suffix, - globalize : globalize, - cacheDns : cacheDns, - mock : mock === true - }; - } - - this.host = options.host || 'localhost'; - this.port = options.port || 8125; - this.prefix = options.prefix || ''; - this.suffix = options.suffix || ''; - this.socket = dgram.createSocket('udp4'); - this.mock = options.mock; - - if(options.cacheDns === true){ - dns.lookup(options.host, function(err, address, family){ - if(err == null){ - self.host = address; - } - }); - } - - if(options.globalize){ - global.statsd = this; - } -}; +var functions = {}; /** * Represents the timing stat @@ -57,8 +11,10 @@ var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock) { * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.timing = function (stat, time, sampleRate, tags, callback) { - this.sendAll(stat, time, 'ms', sampleRate, tags, callback); +functions.timing = function (method) { + return function (stat, time, sampleRate, tags, callback) { + return this[method](stat, time, 'ms', sampleRate, tags, callback); + }; }; /** @@ -69,8 +25,10 @@ Client.prototype.timing = function (stat, time, sampleRate, tags, callback) { * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.increment = function (stat, value, sampleRate, tags, callback) { - this.sendAll(stat, value || 1, 'c', sampleRate, tags, callback); +functions.increment = function (method) { + return function (stat, value, sampleRate, tags, callback) { + return this[method](stat, value || 1, 'c', sampleRate, tags, callback); + }; }; /** @@ -81,8 +39,10 @@ Client.prototype.increment = function (stat, value, sampleRate, tags, callback) * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.decrement = function (stat, value, sampleRate, tags, callback) { - this.sendAll(stat, -value || -1, 'c', sampleRate, tags, callback); +functions.decrement = function (method) { + return function (stat, value, sampleRate, tags, callback) { + return this[method](stat, -value || -1, 'c', sampleRate, tags, callback); + }; }; /** @@ -93,8 +53,10 @@ Client.prototype.decrement = function (stat, value, sampleRate, tags, callback) * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.histogram = function (stat, value, sampleRate, tags, callback) { - this.sendAll(stat, value, 'h', sampleRate, tags, callback); +functions.histogram = function (method) { + return function (stat, value, sampleRate, tags, callback) { + return this[method](stat, value, 'h', sampleRate, tags, callback); + }; }; @@ -106,8 +68,10 @@ Client.prototype.histogram = function (stat, value, sampleRate, tags, callback) * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.gauge = function (stat, value, sampleRate, tags, callback) { - this.sendAll(stat, value, 'g', sampleRate, tags, callback); +functions.gauge = function (method) { + return function (stat, value, sampleRate, tags, callback) { + return this[method](stat, value, 'g', sampleRate, tags, callback); + }; }; /** @@ -118,11 +82,65 @@ Client.prototype.gauge = function (stat, value, sampleRate, tags, callback) { * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.unique = -Client.prototype.set = function (stat, value, sampleRate, tags, callback) { - this.sendAll(stat, value, 's', sampleRate, tags, callback); +functions.unique = +functions.set = function (method) { + return function (stat, value, sampleRate, tags, callback) { + return this[method](stat, value, 's', sampleRate, tags, callback); + } }; +/** + * The UDP Client for StatsD + * @param options + * @option host {String} The host to connect to default: localhost + * @option port {String|Integer} The port to connect to default: 8125 + * @option prefix {String} An optional prefix to assign to each stat name sent + * @option suffix {String} An optional suffix to assign to each stat name sent + * @option globalize {boolean} An optional boolean to add "statsd" as an object in the global namespace + * @option cacheDns {boolean} An optional option to only lookup the hostname -> ip address once + * @option mock {boolean} An optional boolean indicating this Client is a mock object, no stats are sent. + * @constructor + */ +var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock) { + var options = host || {}, + self = this; + + if(arguments.length > 1 || typeof(host) === 'string'){ + options = { + host : host, + port : port, + prefix : prefix, + suffix : suffix, + globalize : globalize, + cacheDns : cacheDns, + mock : mock === true + }; + } + + this.host = options.host || 'localhost'; + this.port = options.port || 8125; + this.prefix = options.prefix || ''; + this.suffix = options.suffix || ''; + this.socket = dgram.createSocket('udp4'); + this.mock = options.mock; + + if(options.cacheDns === true){ + dns.lookup(options.host, function(err, address, family){ + if(err == null){ + self.host = address; + } + }); + } + + if(options.globalize){ + global.statsd = this; + } +}; + +Object.keys(functions).forEach(function(fn) { + Client.prototype[fn] = functions[fn]('sendAll'); +}); + /** * Checks if stats is an array and sends all stats calling back once all have sent * @param stat {String|Array} The stat(s) to send @@ -181,7 +199,7 @@ Client.prototype.sendAll = function(stat, value, type, sampleRate, tags, callbac /** * Sends a stat across the wire - * @param stat {String|Array} The stat(s) to send + * @param stat {String} The stat to send * @param value The value to send * @param type {String} The type of message to send to statsd * @param sampleRate {Number} The Number of times to sample (0 to 1) @@ -189,15 +207,44 @@ Client.prototype.sendAll = function(stat, value, type, sampleRate, tags, callbac * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.send = function (stat, value, type, sampleRate, tags, callback) { - var message = this.prefix + stat + this.suffix + ':' + value + '|' + type, - buf; + var message = this.message(stat, value, type, sampleRate, tags); + this.sendMessage(message, callback); +}; + +/** + * Checks if stat is an array and returns an array of messages + * @param stat {String|Array} The stat(s) to send + * @param value The value to send + * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. + * @param tags {Array=} The Array of tags to add to metrics. Optional. + */ +Client.prototype.allMessages = function(stat, value, type, sampleRate, tags) { + var self = this; + var stats = Array.isArray(stat) ? stat : [stat]; + return stats.map(function(item) { + return self.message(item, value, type, sampleRate, tags); + }).filter(function(item) { + return !!item; + }); +}; + +/** + * Creates the message + * @param stat {String} The stat to send + * @param value The value to send + * @param type {String} The type of message to send to statsd + * @param sampleRate {Number} The Number of times to sample (0 to 1) + * @param tags {Array} The Array of tags to add to metrics + */ +Client.prototype.message = function(stat, value, type, sampleRate, tags) { + var message = this.prefix + stat + this.suffix + ':' + value + '|' + type; if(sampleRate && sampleRate < 1){ if(Math.random() < sampleRate){ message += '|@' + sampleRate; } else { //don't want to send if we don't meet the sample ratio - return; + return null; } } @@ -205,6 +252,17 @@ Client.prototype.send = function (stat, value, type, sampleRate, tags, callback) message += '|#' + tags.join(','); } + return message; +}; + +/** + * Sends a stat message across the wire + * @param message {String} the full StatsD message + * @param callback {Function=} Callback when message is done being delivered. Optional. + */ +Client.prototype.sendMessage = function(message, callback) { + var buf; + // Only send this stat if we're not a mock Client. if(!this.mock) { buf = new Buffer(message); @@ -216,6 +274,13 @@ Client.prototype.send = function (stat, value, type, sampleRate, tags, callback) } }; +/** + * Returns a multi object that allows you to send multiple metrics at once + */ +Client.prototype.multi = function() { + return new Multi(this); +}; + /** * Close the underlying socket and stop listening for data on it. */ @@ -224,3 +289,36 @@ Client.prototype.close = function(){ } exports.StatsD = Client; + +/** + * The Multi class that allows you to send multiple metrics at once + */ +var Multi = function (client) { + this.client = client; + this.messages = []; +}; + +/** + * Send the multi-metric + * @param callback {Function=} Callback when message is done being delivered. Optional. + */ +Multi.prototype.send = function(callback) { + if (!this.messages.length) { + if(typeof callback === 'function'){ + callback(null, 0); + } + return; + } + + var message = this.messages.join('\n'); + this.messages = []; + this.client.sendMessage(message, callback); +}; + +Object.keys(functions).forEach(function(fn) { + Multi.prototype[fn] = function() { + var messages = functions[fn]('allMessages').apply(this.client, arguments); + this.messages = this.messages.concat(messages); + }; +}); + diff --git a/test/test_statsd.js b/test/test_statsd.js index 42f4130..62ca8d7 100644 --- a/test/test_statsd.js +++ b/test/test_statsd.js @@ -610,4 +610,88 @@ describe('StatsD', function(){ }); }); + describe('#multi', function(finished){ + it('should allow you to send one message', function(finished){ + udpTest(function(message, server){ + assert.equal(message, 'test:42|s'); + server.close(); + finished(); + }, function(server){ + var address = server.address(), + statsd = new StatsD(address.address, address.port); + + var multi = statsd.multi(); + multi.set('test', 42); + multi.send(); + }); + }); + + it('should allow you to send multiple messages at once', function(finished){ + udpTest(function(message, server){ + assert.equal(message, 'test:42|s\nbar:42|ms|@0.5'); + server.close(); + finished(); + }, function(server){ + var address = server.address(), + statsd = new StatsD(address.address, address.port); + + var multi = statsd.multi(); + multi.set('test', 42); + multi.timing('bar', 42, 0.5); + multi.send(); + }); + }); + + it('should allow multiple stats names', function(finished){ + udpTest(function(message, server){ + assert.equal(message, 'test:42|s\nfoo:42|ms|@0.5\nbar:42|ms|@0.5'); + server.close(); + finished(); + }, function(server){ + var address = server.address(), + statsd = new StatsD(address.address, address.port); + + var multi = statsd.multi(); + multi.set('test', 42); + multi.timing(['foo', 'bar'], 42, 0.5); + multi.send(); + }); + }); + + it('should ignore messages that do not meet the sample ratio', function(finished){ + udpTest(function(message, server){ + assert.equal(message, 'test:42|s\nfoo:42|ms|@0.5'); + server.close(); + finished(); + }, function(server){ + var address = server.address(), + statsd = new StatsD(address.address, address.port); + + var multi = statsd.multi(); + multi.set('test', 42); + multi.timing('foo', 42, 0.5); + multi.timing('bar', 42, 0.4); + multi.send(); + }); + }); + + it('should callback', function(finished){ + var called = false; + udpTest(function(message, server){ + assert.equal(message, 'test:42|h'); + assert.equal(called, true); + server.close(); + finished(); + }, function(server){ + var address = server.address(), + statsd = new StatsD(address.address, address.port); + + var multi = statsd.multi(); + statsd.histogram('test', 42); + multi.send(function() { + called = true; + }); + }); + }); + }); });