diff --git a/README.md b/README.md index 684420b6..d29b9c7e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Parameters (specified as an options hash): * `sampleRate`: Sends only a sample of data to StatsD for all StatsD methods. Can be overriden at the method level. `default: 1` * `errorHandler`: A function with one argument. It is called to handle various errors. `default: none`, errors are thrown/logger to console * `useDefaultRoute`: Use the default interface on a Linux system. Useful when running in containers +* `protocol`: Use `tcp` option for TCP protocol. Defaults to UDP otherwise All StatsD methods other than event and close have the same API: * `name`: Stat name `required` diff --git a/lib/statsd.js b/lib/statsd.js index 2841b4e4..c10f1011 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -1,8 +1,9 @@ -"use strict"; +'use strict'; var dgram = require('dgram'), util = require('util'), dns = require('dns'), + net = require('net'), helpers = require('./helpers'), applyStatsFns = require('./statsFunctions'); @@ -13,7 +14,7 @@ var dgram = require('dgram'), * @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 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. * @option globalTags {Array=} Optional tags that will be added to every metric @@ -25,11 +26,11 @@ var dgram = require('dgram'), * @constructor */ var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock, - globalTags, maxBufferSize, bufferFlushInterval, telegraf, sampleRate) { + globalTags, maxBufferSize, bufferFlushInterval, telegraf, sampleRate, protocol) { var options = host || {}, self = this; - if(arguments.length > 1 || typeof(host) === 'string'){ + if (arguments.length > 1 || typeof(host) === 'string') { options = { host : host, port : port, @@ -42,26 +43,55 @@ var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock, maxBufferSize : maxBufferSize, bufferFlushInterval: bufferFlushInterval, telegraf : telegraf, - sampleRate : sampleRate + sampleRate : sampleRate, + protocol : protocol }; } + var createSocket = function createSocket(instance, args) { + var socket; + var errMessage; + + if (args.protocol === 'tcp') { + try { + socket = net.connect(args.port, args.host); + socket.setKeepAlive(true); + } catch (e) { + errMessage = 'Could not establish connection to ' + args.host + ':' + args.port; + if (instance.errorHandler) { + instance.errorHandler(new Error(errMessage)); + } else { + console.log(errMessage); + } + } + } else { + socket = dgram.createSocket('udp4'); + } + + return socket; + }; + // hidden global_tags option for backwards compatibility options.globalTags = options.globalTags || options.global_tags; + this.protocol = (options.protocol && options.protocol.toLowerCase()); this.host = options.host || 'localhost'; this.port = options.port || 8125; this.prefix = options.prefix || ''; this.suffix = options.suffix || ''; - this.socket = options.isChild ? options.socket : dgram.createSocket('udp4'); + this.socket = options.isChild ? options.socket : createSocket(this, { + host: this.host, + port: this.port, + protocol: this.protocol + }); this.mock = options.mock; - this.globalTags = typeof options.globalTags === "object" ? + this.globalTags = typeof options.globalTags === 'object' ? helpers.formatTags(options.globalTags, options.telegraf) : []; this.telegraf = options.telegraf || false; this.maxBufferSize = options.maxBufferSize || 0; this.sampleRate = options.sampleRate || 1; this.bufferFlushInterval = options.bufferFlushInterval || 1000; - this.bufferHolder = options.isChild ? options.bufferHolder : { buffer: "" }; + this.bufferHolder = options.isChild ? options.bufferHolder : { buffer: '' }; this.errorHandler = options.errorHandler; // If we're mocking the client, create a buffer to record the outgoing calls. @@ -70,7 +100,7 @@ var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock, } // We only want a single flush event per parent and all its child clients - if(!options.isChild && this.maxBufferSize > 0) { + if (!options.isChild && this.maxBufferSize > 0) { this.intervalHandle = setInterval(this.onBufferFlushInterval.bind(this), this.bufferFlushInterval); } @@ -78,9 +108,9 @@ var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock, if (options.dnsError) { this.dnsError = options.dnsError; } - } else if (options.cacheDns === true){ - dns.lookup(options.host, function(err, address, family){ - if(err === null){ + } else if (options.cacheDns === true) { + dns.lookup(options.host, function (err, address, family) { + if (err === null) { self.host = address; } else { self.dnsError = err; @@ -92,14 +122,14 @@ var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock, this.socket.on('error', options.errorHandler); } - if(options.globalize){ + if (options.globalize) { global.statsd = this; } - if(options.useDefaultRoute) { + if (options.useDefaultRoute) { var defaultRoute = helpers.getDefaultRoute(); if (defaultRoute) { - console.log('Got ' + defaultRoute + " for the system's default route"); + console.log('Got ' + defaultRoute + ' for the system\'s default route'); this.host = defaultRoute; } } @@ -123,19 +153,19 @@ applyStatsFns(Client); * @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.sendAll = function(stat, value, type, sampleRate, tags, callback){ +Client.prototype.sendAll = function (stat, value, type, sampleRate, tags, callback) { var completed = 0, calledback = false, sentBytes = 0, self = this; - if(sampleRate && typeof sampleRate !== 'number'){ + if (sampleRate && typeof sampleRate !== 'number') { callback = tags; tags = sampleRate; sampleRate = undefined; } - if(tags && typeof tags !== "object"){ + if (tags && typeof tags !== 'object') { callback = tags; tags = undefined; } @@ -145,32 +175,34 @@ Client.prototype.sendAll = function(stat, value, type, sampleRate, tags, callbac * call back from the function * @private */ - function onSend(error, bytes){ + function onSend(error, bytes) { completed += 1; - if(calledback){ + if (calledback) { return; } - if(error){ + if (error) { if (typeof callback === 'function') { calledback = true; callback(error); - } - else if (self.errorHandler) { + } else if (self.errorHandler) { calledback = true; self.errorHandler(error); } return; } - sentBytes += bytes; - if(completed === stat.length && typeof callback === 'function'){ + if (bytes) { + sentBytes += bytes; + } + + if (completed === stat.length && typeof callback === 'function') { callback(null, sentBytes); } } - if(Array.isArray(stat)){ - stat.forEach(function(item){ + if (Array.isArray(stat)) { + stat.forEach(function (item) { self.sendStat(item, value, type, sampleRate, tags, onSend); }); } else { @@ -191,11 +223,11 @@ Client.prototype.sendStat = function (stat, value, type, sampleRate, tags, callb var message = this.prefix + stat + this.suffix + ':' + value + '|' + type; sampleRate = sampleRate || this.sampleRate; - if(sampleRate && sampleRate < 1){ - if(Math.random() < sampleRate){ + if (sampleRate && sampleRate < 1) { + if (Math.random() < sampleRate) { message += '|@' + sampleRate; } else { - //don't want to send if we don't meet the sample ratio + // don't want to send if we don't meet the sample ratio return callback ? callback() : undefined; } } @@ -210,10 +242,10 @@ Client.prototype.sendStat = function (stat, value, type, sampleRate, tags, callb */ Client.prototype.send = function (message, tags, callback) { var mergedTags = this.globalTags; - if(tags && typeof tags === "object"){ + if (tags && typeof tags === 'object') { mergedTags = helpers.overrideTags(mergedTags, tags, this.telegraf); } - if(mergedTags.length > 0){ + if (mergedTags.length > 0) { if (this.telegraf) { message = message.split(':'); message = message[0] + ',' + mergedTags.join(',').replace(/:/g, '=') + ':' + message.slice(1).join(':'); @@ -221,6 +253,7 @@ Client.prototype.send = function (message, tags, callback) { message += '|#' + mergedTags.join(','); } } + this._send(message, callback); }; @@ -235,25 +268,22 @@ Client.prototype._send = function (message, callback) { if (this.dnsError) { if (callback) { return callback(this.dnsError); - } - else if (this.errorHandler) { + } else if (this.errorHandler) { return this.errorHandler(this.dnsError); } throw this.dnsError; } // Only send this stat if we're not a mock Client. - if(!this.mock) { - if(this.maxBufferSize === 0) { + if (!this.mock) { + if (this.maxBufferSize === 0) { this.sendMessage(message, callback); - } - else { + } else { this.enqueue(message, callback); } - } - else { + } else { this.mockBuffer.push(message); - if(typeof callback === 'function'){ + if (typeof callback === 'function') { callback(null, 0); } } @@ -264,8 +294,8 @@ Client.prototype._send = function (message, callback) { * * @param message {String} The constructed message without tags */ -Client.prototype.enqueue = function(message, callback){ - message += "\n"; +Client.prototype.enqueue = function (message, callback) { + message += '\n'; if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { this.flushQueue(callback); @@ -282,9 +312,9 @@ Client.prototype.enqueue = function(message, callback){ /** * Flush the buffer, sending on the messages */ -Client.prototype.flushQueue = function(callback){ +Client.prototype.flushQueue = function (callback) { this.sendMessage(this.bufferHolder.buffer, callback); - this.bufferHolder.buffer = ""; + this.bufferHolder.buffer = ''; }; /** @@ -293,28 +323,34 @@ Client.prototype.flushQueue = function(callback){ * @param message {String} The constructed message without tags * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.sendMessage = function(message, callback){ - // Guard against "RangeError: Offset into buffer too large" in node 0.10 +Client.prototype.sendMessage = function (message, callback) { + // Guard against 'RangeError: Offset into buffer too large' in node 0.10 // https://github.com/nodejs/node-v0.x-archive/issues/7884 - if (message === "") { + if (message === '') { if (callback) { callback(null); } return; } + + if (this.protocol === 'tcp' && message.lastIndexOf('\n') !== message.length - 1) { + message += '\n'; + } + var buf = new Buffer(message); try { - this.socket.send(buf, 0, buf.length, this.port, this.host, callback); - } - catch(err) { + if (this.protocol === 'tcp') { + this.socket.write(buf, 'ascii', callback); + } else { + this.socket.send(buf, 0, buf.length, this.port, this.host, callback); + } + } catch (err) { var errMessage = 'Error sending hot-shots message: ' + err; if (callback) { callback(new Error(errMessage)); - } - else if (this.errorHandler) { + } else if (this.errorHandler) { this.errorHandler(new Error(errMessage)); - } - else { + } else { console.log(errMessage); } } @@ -323,15 +359,15 @@ Client.prototype.sendMessage = function(message, callback){ /** * Called every bufferFlushInterval to flush any buffer that is around */ -Client.prototype.onBufferFlushInterval = function() { +Client.prototype.onBufferFlushInterval = function () { this.flushQueue(); }; /** * Close the underlying socket and stop listening for data on it. */ -Client.prototype.close = function(callback){ - if(this.intervalHandle) { +Client.prototype.close = function (callback) { + if (this.intervalHandle) { clearInterval(this.intervalHandle); } @@ -344,17 +380,18 @@ Client.prototype.close = function(callback){ } try { - this.socket.close(); - } - catch (err) { + if (this.protocol === 'tcp') { + this.socket.destroy(); + } else { + this.socket.close(); + } + } catch (err) { var errMessage = 'Error closing hot-shots socket: ' + err; if (callback) { callback(new Error(errMessage)); - } - else if (this.errorHandler) { + } else if (this.errorHandler) { this.errorHandler(new Error(errMessage)); - } - else { + } else { console.log(errMessage); } } @@ -369,19 +406,19 @@ var ChildClient = function (parent, options) { bufferHolder: parent.bufferHolder, dnsError : parent.dnsError, // Child inherits an error from parent (if it is there) errorHandler: options.errorHandler || parent.errorHandler, // Handler for callback errors - host : parent.host, port : parent.port, prefix : (options.prefix || '') + parent.prefix, // Child has its prefix prepended to parent's prefix suffix : parent.suffix + (options.suffix || ''), // Child has its suffix appended to parent's suffix - globalize : false, // Only "root" client can be global + globalize : false, // Only 'root' client can be global mock : parent.mock, // Append child's tags to parent's tags - globalTags : typeof options.globalTags === "object" ? + globalTags : typeof options.globalTags === 'object' ? helpers.overrideTags(parent.globalTags, options.globalTags, parent.telegraf) : parent.globalTags, maxBufferSize : parent.maxBufferSize, bufferFlushInterval: parent.bufferFlushInterval, - telegraf : parent.telegraf + telegraf : parent.telegraf, + protocol : parent.protocol }); }; util.inherits(ChildClient, Client); @@ -393,7 +430,7 @@ util.inherits(ChildClient, Client); * @option suffix {String} An optional suffix to assign to each stat name sent * @option globalTags {Array=} Optional tags that will be added to every metric */ -Client.prototype.childClient = function(options) { +Client.prototype.childClient = function (options) { return new ChildClient(this, options); }; diff --git a/package.json b/package.json index 786cade7..c0aff2bc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "scripts": { "coverage": "nyc --reporter=lcov npm test", - "test": "mocha -R spec", + "test": "mocha -R spec --timeout 5000 test/index.js", "lint": "jshint lib/**.js test/**.js", "pretest": "npm run lint" }, diff --git a/test/buffer.js b/test/buffer.js new file mode 100644 index 00000000..e7f5cb19 --- /dev/null +++ b/test/buffer.js @@ -0,0 +1,184 @@ +'use strict'; + +var assert = require('assert'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runBufferTestSuite() { + describe('#buffer', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should aggregate packets when maxBufferSize is set to non-zero', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 12 + }, index); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:1|c\nb:2|c\n'); + server.close(); + done(); + }); + }); + + it('should not aggregate packets when maxBufferSize is set to zero', function (done) { + var noOfMessages = 0; + var expected = ['a:1|c', 'b:2|c']; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 0 + }, index); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metric) { + var index = expected.indexOf(metric); + assert.equal(index >= 0, true); + expected.splice(index, 1); + noOfMessages++; + if (noOfMessages === 2) { + assert.equal(expected.length, 0); + server.close(); + done(); + } + }); + }); + + it('should not send batches larger then maxBufferSize', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 8 + }, index); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:1|c\n'); + server.close(); + done(); + }); + }); + + it('should flush the buffer when timeout value elapsed', function (done) { + var start; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 1220, + bufferFlushInterval: 1100 + }, index); + start = new Date(); + statsd.increment('a', 1); + }); + server.on('metrics', function (metric) { + var elapsed = Date.now() - start; + assert.equal(metric, 'a:1|c\n'); + assert.equal(elapsed > 1000, true); + server.close(); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should aggregate packets when maxBufferSize is set to non-zero', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 12, + protocol: 'tcp' + }, index); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:1|c\nb:2|c\n'); + server.close(); + done(); + }); + }); + + it('should aggregate packets when maxBufferSize is set to zero', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 0, + protocol: 'tcp' + }, index); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:1|c\nb:2|c\n'); + server.close(); + done(); + }); + }); + + it('should not send batches larger then maxBufferSize', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 8, + protocol: 'tcp' + }, index); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:1|c\n'); + statsd.close(); + server.close(); + done(); + }); + }); + + it('should flush the buffer when timeout value elapsed', function (done) { + var start; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + maxBufferSize: 1220, + bufferFlushInterval: 1100, + protocol: 'tcp' + }, index); + start = new Date(); + statsd.increment('a', 1); + }); + server.on('metrics', function (metric) { + var elapsed = Date.now() - start; + assert.equal(metric, 'a:1|c\n'); + assert.equal(elapsed > 1000, true); + server.close(); + done(); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/check.js b/test/check.js new file mode 100644 index 00000000..92001145 --- /dev/null +++ b/test/check.js @@ -0,0 +1,357 @@ +'use strict'; + +var assert = require('assert'); +var dgram = require('dgram'); +var net = require('net'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runCheckTestSuite() { + describe('#check', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should send proper check format for name and status', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.check('check.name', statsd.CHECKS.OK); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|check.name|0'); + server.close(); + done(); + }); + }); + + it('should send proper check format for name and status with global prefix and suffix', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix.', + suffix: '.suffix' + }, index); + statsd.check('check.name', statsd.CHECKS.OK); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|prefix.check.name.suffix|0'); + server.close(); + done(); + }); + }); + + it('should send proper check format for name, status, and options', function (done) { + var date = new Date(); + server = createUDPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + options = { + date_happened: date, + hostname: 'host', + message: 'message' + }; + statsd.check('check.name', statsd.CHECKS.WARNING, options); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|check.name|1|d:' + + Math.round(date.getTime() / 1000) + '|h:host|m:message' + ); + server.close(); + done(); + }); + }); + + it('should send proper check format for title, text, some options, and tags', function (done) { + server = createUDPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + options = { + hostname: 'host' + }; + statsd.event('test title', 'another desc', options, ['foo', 'bar']); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,12}:test title|another desc|h:host|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send proper check format for title, text, tags, and a callback', function (done) { + var called = false; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.check('check.name', statsd.CHECKS.OK, null, ['foo', 'bar'], function () { + called = true; + }); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|check.name|0|#foo,bar'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should send no event stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createUDPServer(function (address) { + statsd = createStatsdClient([ + address.address, address.port, 'prefix', 'suffix', false, false, true + ], index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.check('test', 1); + + statsd.check('test', 1, null, function (error, bytes) { + var socket = dgram.createSocket("udp4"); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.send(buf, 0, buf.length, address.port, address.address, function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + + it('should throw an exception when using telegraf format', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true + }, index); + assert.throws(function () { + statsd.check('check.name', statsd.CHECKS.OK, null, ['foo', 'bar']); + }, function (err) { + server.close(); + done(); + }); + }); + }); + + it('should use errorHandler', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + errorHandler: function () { + done(); + } + }, index); + statsd.check('check.name', statsd.CHECKS.OK); + }); + }); + }); + + describe('TCP', function () { + it('should send proper check format for name and status', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.check('check.name', statsd.CHECKS.OK); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|check.name|0\n'); + server.close(); + done(); + }); + }); + + it('should send proper check format for name and status with global prefix and suffix', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix.', + suffix: '.suffix', + protocol: 'tcp' + }, index); + statsd.check('check.name', statsd.CHECKS.OK); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|prefix.check.name.suffix|0\n'); + server.close(); + done(); + }); + }); + + it('should send proper check format for name, status, and options', function (done) { + var date = new Date(); + server = createTCPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + options = { + date_happened: date, + hostname: 'host', + message: 'message' + }; + statsd.check('check.name', statsd.CHECKS.WARNING, options); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|check.name|1|d:' + + Math.round(date.getTime() / 1000) + '|h:host|m:message\n' + ); + server.close(); + done(); + }); + }); + + it('should send proper check format for title, text, some options, and tags', function (done) { + server = createTCPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + options = { + hostname: 'host' + }; + statsd.event('test title', 'another desc', options, ['foo', 'bar']); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,12}:test title|another desc|h:host|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send proper check format for title, text, tags, and a callback', function (done) { + var called = false; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.check('check.name', statsd.CHECKS.OK, null, ['foo', 'bar'], function () { + called = true; + }); + }); + server.on('metrics', function (event) { + assert.equal(event, '_sc|check.name|0|#foo,bar\n'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should send no event stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix', + suffix: 'suffix', + mock: true, + protocol: 'tcp' + }, index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.check('test', 1); + + statsd.check('test', 1, null, function (error, bytes) { + var socket = net.connect(address.port, address.address); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.write(buf, 0, 'ascii', function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + + it('should throw an exception when using telegraf format', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + protocol: 'tcp' + }, index); + assert.throws(function () { + statsd.check('check.name', statsd.CHECKS.OK, null, ['foo', 'bar']); + }, function (err) { + server.close(); + done(); + }); + }); + }); + + it('should use errorHandler', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + protocol: 'tcp', + errorHandler: function () { + done(); + } + }, index); + statsd.check('check.name', statsd.CHECKS.OK); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/childClient.js b/test/childClient.js new file mode 100644 index 00000000..af818e50 --- /dev/null +++ b/test/childClient.js @@ -0,0 +1,142 @@ +'use strict'; + +var assert = require('assert'); + +var StatsD = require('../lib/statsd'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runChildClientTestSuite() { + describe('#childClient', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + describe('#init', function () { + it('should set the proper values when specified', function () { + statsd = new StatsD( + 'host', 1234, 'prefix', 'suffix', true, null, true, ['gtag', 'tag1:234234'] + ); + + var child = statsd.childClient({ + prefix: 'preff.', + suffix: '.suff', + globalTags: ['awesomeness:over9000', 'tag1:xxx', 'bar', ':baz'] + }); + + assert.equal(child.prefix, 'preff.prefix'); + assert.equal(child.suffix, 'suffix.suff'); + assert.equal(statsd, global.statsd); + assert.deepEqual(child.globalTags, ['gtag', 'tag1:xxx', 'awesomeness:over9000', 'bar', ':baz']); + }); + }); + + describe('#childClient', function () { + describe('UDP', function () { + it('should add tags, prefix and suffix without parent values', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.host, + port: address.port, + maxBufferSize: 500 + }, 0).childClient({ + prefix: 'preff.', + suffix: '.suff', + globalTags: ['awesomeness:over9000'] + }); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'preff.a.suff:1|c|#awesomeness:over9000\npreff.b.suff:2|c|#awesomeness:over9000\n'); + server.close(); + done(); + }); + }); + + it('should add tags, prefix and suffix with parent values', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.host, + port: address.port, + prefix: 'p.', + suffix: '.s', + globalTags: ['xyz'], + maxBufferSize: 500 + }, 0).childClient({ + prefix: 'preff.', + suffix: '.suff', + globalTags: ['awesomeness:over9000'] + }); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'preff.p.a.s.suff:1|c|#xyz,awesomeness:' + + 'over9000\npreff.p.b.s.suff:2|c|#xyz,awesomeness:over9000\n' + ); + server.close(); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should add tags, prefix and suffix without parent values', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.host, + port: address.port, + maxBufferSize: 500, + protocol: 'tcp' + }, 0).childClient({ + prefix: 'preff.', + suffix: '.suff', + globalTags: ['awesomeness:over9000'] + }); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'preff.a.suff:1|c|#awesomeness:over9000\npreff.b.suff:2|c|#awesomeness:over9000\n'); + server.close(); + done(); + }); + }); + + it('should add tags, prefix and suffix with parent values', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.host, + port: address.port, + prefix: 'p.', + suffix: '.s', + globalTags: ['xyz'], + maxBufferSize: 500, + protocol: 'tcp' + }, 0).childClient({ + prefix: 'preff.', + suffix: '.suff', + globalTags: ['awesomeness:over9000'] + }); + statsd.increment('a', 1); + statsd.increment('b', 2); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'preff.p.a.s.suff:1|c|#xyz,awesomeness:' + + 'over9000\npreff.p.b.s.suff:2|c|#xyz,awesomeness:over9000\n' + ); + server.close(); + done(); + }); + }); + }); + }); + }); +}; diff --git a/test/close.js b/test/close.js new file mode 100644 index 00000000..083a19a3 --- /dev/null +++ b/test/close.js @@ -0,0 +1,90 @@ +'use strict'; + +var assert = require('assert'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runCloseMethodTestSuite() { + describe('#close', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should call callback after close call', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.close(function () { + server.close(); + done(); + }); + }); + }); + + it('should use errorHandler', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + errorHandler: function (e) { + server.close(); + done(); + } + }, index); + statsd.socket.close = function () { + throw new Error('Boom!'); + }; + statsd.close(); + }); + }); + }); + + describe('TCP', function () { + it('should call callback after close call', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.close(function () { + server.close(); + done(); + }); + }); + }); + + it('should use errorHandler', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp', + errorHandler: function (e) { + server.close(); + } + }, index); + statsd.socket.destroy = function () { + throw new Error('Boom!'); + }; + statsd.close(function () { + done(); + }); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/event.js b/test/event.js new file mode 100644 index 00000000..48ff7abb --- /dev/null +++ b/test/event.js @@ -0,0 +1,359 @@ +'use strict'; + +var assert = require('assert'); +var dgram = require('dgram'); +var net = require('net'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runEventTestSuite() { + describe('#event', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should send proper event format for title and text', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.event('test', 'description'); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{4,11}:test|description'); + server.close(); + done(); + }); + }); + + it('should reuse the title when when text is missing', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.event('test'); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{4,4}:test|test'); + server.close(); + done(); + }); + }); + + it('should send proper event format for title, text, and options', function (done) { + var date = new Date(); + server = createUDPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + options = { + date_happened: date, + hostname: 'host', + aggregation_key: 'ag_key', + priority: 'low', + source_type_name: 'source_type', + alert_type: 'warning' + }; + statsd.event('test title', 'another\nmultiline\ndescription', options); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,31}:test title|another\\nmultiline\\ndescription|d:' + + Math.round(date.getTime() / 1000) + '|h:host|k:ag_key|p:low|s:source_type|t:warning' + ); + server.close(); + done(); + }); + }); + + it('should send proper event format for title, text, some options, and tags', function (done) { + server = createUDPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + options = { + hostname: 'host' + }; + statsd.event('test title', 'another desc', options, ['foo', 'bar']); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,12}:test title|another desc|h:host|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send proper event format for title, text, tags, and a callback', function (done) { + var called = false; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.event('test title', 'another desc', null, ['foo', 'bar'], function () { + called = true; + }); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,12}:test title|another desc|#foo,bar'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should send no event stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createUDPServer(function (address) { + statsd = createStatsdClient([ + address.address, address.port, 'prefix', 'suffix', false, false, true + ], index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.event('test', 1); + + statsd.event('test', 1, null, function (error, bytes) { + var socket = dgram.createSocket("udp4"); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.send(buf, 0, buf.length, address.port, address.address, function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + + it('should throw an exception when using telegraf format', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true + }, index); + assert.throws(function () { + statsd.event('test title', 'another desc', null, ['foo', 'bar']); + }, function (err) { + server.close(); + done(); + }); + }); + }); + + it('should use errorHandler', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + errorHandler: function () { + done(); + } + }, index); + statsd.event('test title', 'another desc'); + }); + }); + }); + + describe('TCP', function () { + it('should send proper event format for title and text', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.event('test', 'description'); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{4,11}:test|description\n'); + server.close(); + done(); + }); + }); + + it('should reuse the title when when text is missing', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.event('test'); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{4,4}:test|test\n'); + server.close(); + done(); + }); + }); + + it('should send proper event format for title, text, and options', function (done) { + var date = new Date(); + server = createTCPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + options = { + date_happened: date, + hostname: 'host', + aggregation_key: 'ag_key', + priority: 'low', + source_type_name: 'source_type', + alert_type: 'warning' + }; + statsd.event('test title', 'another\nmultiline\ndescription', options); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,31}:test title|another\\nmultiline\\ndescription|d:' + + Math.round(date.getTime() / 1000) + '|h:host|k:ag_key|p:low|s:source_type|t:warning\n' + ); + server.close(); + done(); + }); + }); + + it('should send proper event format for title, text, some options, and tags', function (done) { + server = createTCPServer(function (address) { + var options; + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + options = { + hostname: 'host' + }; + statsd.event('test title', 'another desc', options, ['foo', 'bar']); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,12}:test title|another desc|h:host|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send proper event format for title, text, tags, and a callback', function (done) { + var called = false; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.event('test title', 'another desc', null, ['foo', 'bar'], function () { + called = true; + }); + }); + server.on('metrics', function (event) { + assert.equal(event, '_e{10,12}:test title|another desc|#foo,bar\n'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should send no event stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix', + suffix: 'suffix', + mock: true, + protocol: 'tcp' + }, index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.event('test', 1); + + statsd.event('test', 1, null, function (error, bytes) { + var socket = net.connect(address.port, address.address); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.write(buf, 0, 'ascii', function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + + it('should throw an exception when using telegraf format', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + protocol: 'tcp' + }, index); + assert.throws(function () { + statsd.event('test title', 'another desc', null, ['foo', 'bar']); + }, function (err) { + server.close(); + done(); + }); + }); + }); + + it('should use errorHandler', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + protocol: 'tcp', + errorHandler: function () { + done(); + } + }, index); + statsd.event('test title', 'another desc'); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/globalTags.js b/test/globalTags.js new file mode 100644 index 00000000..4b697f17 --- /dev/null +++ b/test/globalTags.js @@ -0,0 +1,328 @@ +'use strict'; + +var assert = require('assert'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runGlobalTagsTestSuite() { + describe('#globalTags', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('TCP', function () { + it('should not add global tags if they are not specified', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c\n'); + server.close(); + done(); + }); + }); + + it('should add global tags if they are specified', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + global_tags: ['gtag'], + protocol: 'tcp' + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c|#gtag\n'); + server.close(); + done(); + }); + }); + + it('should combine global tags and metric tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + global_tags: ['gtag'], + protocol: 'tcp' + }, index); + statsd.increment('test', 1337, ['foo']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#gtag,foo\n'); + server.close(); + done(); + }); + }); + + it('should override global tags with metric tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + global_tags: ['foo', 'gtag:123'], + protocol: 'tcp' + }, index); + statsd.increment('test', 1337, ['gtag:234', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#foo,gtag:234,bar\n'); + server.close(); + done(); + }); + }); + + it('should format global tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: { gtag: "123", foo: "bar"}, + protocol: 'tcp' + }, index); + statsd.increment('test', 1337, { gtag: "234"}); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#gtag:234,foo:bar\n'); + server.close(); + done(); + }); + }); + + it('should replace reserved characters with underscores in tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: { foo: "b,a,r"}, + protocol: 'tcp' + }, index); + statsd.increment('test', 1337, { "reserved:character": "is@replaced@"}); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#foo:b_a_r,reserved_character:is_replaced_\n'); + server.close(); + done(); + }); + }); + + it('should add global tags using telegraf format when enabled', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: ['gtag:gvalue', 'gtag2:gvalue2'], + telegraf: true, + protocol: 'tcp' + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,gtag=gvalue,gtag2=gvalue2:1|c\n'); + server.close(); + done(); + }); + }); + + it('should combine global tags and metric tags using telegraf format when enabled', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: ['gtag=gvalue'], + telegraf: true, + protocol: 'tcp' + }, index); + statsd.increment('test', 1337, ['foo:bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,gtag=gvalue,foo=bar:1337|c\n'); + server.close(); + done(); + }); + }); + + it('should format global key-value tags using telegraf format when enabled', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: { gtag: "gvalue"}, + telegraf: true, + protocol: 'tcp' + }, index); + statsd.increment('test', 1337, { foo: "bar" }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,gtag=gvalue,foo=bar:1337|c\n'); + server.close(); + done(); + }); + }); + }); + + describe('UDP', function () { + it('should not add global tags if they are not specified', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c'); + server.close(); + done(); + }); + }); + + it('should add global tags if they are specified', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + global_tags: ['gtag'] + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c|#gtag'); + server.close(); + done(); + }); + }); + + it('should combine global tags and metric tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + global_tags: ['gtag'] + }, index); + statsd.increment('test', 1337, ['foo']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#gtag,foo'); + server.close(); + done(); + }); + }); + + it('should override global tags with metric tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + global_tags: ['foo', 'gtag:123'] + }, index); + statsd.increment('test', 1337, ['gtag:234', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#foo,gtag:234,bar'); + server.close(); + done(); + }); + }); + + it('should format global tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: { gtag: "123", foo: "bar"} + }, index); + statsd.increment('test', 1337, { gtag: "234"}); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#gtag:234,foo:bar'); + server.close(); + done(); + }); + }); + + it('should replace reserved characters with underscores in tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: { foo: "b,a,r"} + }, index); + statsd.increment('test', 1337, { "reserved:character": "is@replaced@"}); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1337|c|#foo:b_a_r,reserved_character:is_replaced_'); + server.close(); + done(); + }); + }); + + it('should add global tags using telegraf format when enabled', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: ['gtag:gvalue', 'gtag2:gvalue2'], + telegraf: true + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,gtag=gvalue,gtag2=gvalue2:1|c'); + server.close(); + done(); + }); + }); + + it('should combine global tags and metric tags using telegraf format when enabled', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: ['gtag=gvalue'], + telegraf: true + }, index); + statsd.increment('test', 1337, ['foo:bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,gtag=gvalue,foo=bar:1337|c'); + server.close(); + done(); + }); + }); + + it('should format global key-value tags using telegraf format when enabled', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + globalTags: { gtag: "gvalue"}, + telegraf: true + }, index); + statsd.increment('test', 1337, { foo: "bar" }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,gtag=gvalue,foo=bar:1337|c'); + server.close(); + done(); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 00000000..9c0a55f4 --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,67 @@ +'use strict'; + +var dgram = require('dgram'); +var net = require('net'); + +var StatsD = require('../lib/statsd'); + +function createStatsdClient(args, noOfChildren) { + function construct(ctor, args) { + function F() { + return ctor.apply(this, args); + } + F.prototype = ctor.prototype; + return new F(); + } + var client = Array.isArray(args) ? construct(StatsD, args) : new StatsD(args); + + switch (noOfChildren) { + case 0: + return client; + case 1: + return client.childClient({}); + case 2: + return client.childClient({}).childClient({}); + default: + return client; + } +} + +function createTCPServer(onListening) { + var server = net.createServer(function (socket) { + socket.setEncoding('ascii'); + socket.on('data', function (data) { + if (data) { + server.emit('metrics', data); + } + }); + }); + + server.on('listening', function () { + onListening(server.address()); + }); + + server.listen(0, '127.0.0.1'); + return server; +} + +function createUDPServer(onListening){ + var server = dgram.createSocket("udp4"); + server.on('message', function(message){ + var metrics = message.toString(); + server.emit('metrics', metrics); + }); + + server.on('listening', function(){ + onListening(server.address()); + }); + + server.bind(0, '127.0.0.1'); + return server; +} + +module.exports = { + createStatsdClient: createStatsdClient, + createTCPServer: createTCPServer, + createUDPServer: createUDPServer +}; diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..0d187092 --- /dev/null +++ b/test/index.js @@ -0,0 +1,42 @@ +'use strict'; + +var runBufferTestSuite = require('./buffer'); +var runChildClientTestSuite = require('./childClient'); +var runCheckMethodTestSuite = require('./check'); +var runCloseMethodTestSuite = require('./close'); +var runEventMethodTestSuite = require('./event'); +var runGlobalTagsTestSuite = require('./globalTags'); +var runInitTestSuite = require('./init'); +var runSendMethodTestSuite = require('./send'); +var runSendAllMethodTestSuite = require('./sendAll'); +var runSendMessageMethodTestSuite = require('./sendMessage'); +var runStatsFunctionsTestSuite = require('./statsFunctions'); +var runTimerTestSuite = require('./timer'); + +/** + * Since sampling uses random, we need to patch Math.random() to always give + * a consistent result + */ +Math.random = function () { + return 0.42; +}; + +beforeEach(function () { + // Remove it from the namespace to not fail other tests + delete global.statsd; +}); + +describe('StatsD', function () { + runBufferTestSuite(); + runChildClientTestSuite(); + runCheckMethodTestSuite(); + runCloseMethodTestSuite(); + runEventMethodTestSuite(); + runGlobalTagsTestSuite(); + runInitTestSuite(); + runSendMethodTestSuite(); + runSendAllMethodTestSuite(); + runSendMessageMethodTestSuite(); + runStatsFunctionsTestSuite(); + runTimerTestSuite(); +}); diff --git a/test/init.js b/test/init.js new file mode 100644 index 00000000..77a825fe --- /dev/null +++ b/test/init.js @@ -0,0 +1,186 @@ +'use strict'; + +var assert = require('assert'); +var dgram = require('dgram'); +var dns = require('dns'); +var net = require('net'); + +var StatsD = require('../lib/statsd'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; + +module.exports = function runInitTestSuite() { + describe('#init', function () { + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + if (description === 'main client') { // childClient #init is tested in test/childClient.js + it('should set the proper values when specified', function () { + // cachedDns isn't tested here, hence the null + var statsd = createStatsdClient( + ['host', 1234, 'prefix', 'suffix', true, null, true, ['gtag'], 0, 60, false, 0.5, 'udp'], + index + ); + + assert.equal(statsd.host, 'host'); + assert.equal(statsd.port, 1234); + assert.equal(statsd.prefix, 'prefix'); + assert.equal(statsd.suffix, 'suffix'); + assert.equal(statsd, global.statsd); + assert.equal(statsd.mock, true); + assert.deepEqual(statsd.globalTags, ['gtag']); + assert.equal(statsd.maxBufferSize, 0); + assert.equal(statsd.bufferFlushInterval, 60); + assert.equal(statsd.telegraf, false); + assert.equal(statsd.sampleRate, 0.5); + assert.equal(statsd.protocol, 'udp'); + }); + + it('should set the proper values with options hash format', function () { + // Don't do DNS lookup for this test + var originalLookup = dns.lookup; + dns.lookup = function () {}; + + // cachedDns isn't tested here, hence the null + var statsd = createStatsdClient({ + host: 'host', + port: 1234, + prefix: 'prefix', + suffix: 'suffix', + globalize: true, + mock: true, + globalTags: ['gtag'], + sampleRate: 0.6, + maxBufferSize: 0, + bufferFlushInterval: 60, + telegraf: false, + protocol: 'tcp' + }, index); + + assert.equal(statsd.host, 'host'); + assert.equal(statsd.port, 1234); + assert.equal(statsd.prefix, 'prefix'); + assert.equal(statsd.suffix, 'suffix'); + assert.equal(statsd, global.statsd); + assert.equal(statsd.mock, true); + assert.equal(statsd.sampleRate, 0.6); + assert.deepEqual(statsd.globalTags, ['gtag']); + assert.equal(statsd.maxBufferSize, 0); + assert.equal(statsd.bufferFlushInterval, 60); + assert.deepEqual(statsd.telegraf, false); + assert.equal(statsd.protocol, 'tcp'); + + dns.lookup = originalLookup; + }); + } + + it('should set default values when not specified', function () { + var statsd = createStatsdClient(); + assert.equal(statsd.host, 'localhost'); + assert.equal(statsd.port, 8125); + assert.equal(statsd.prefix, ''); + assert.equal(statsd.suffix, ''); + assert.equal(global.statsd, undefined); + assert.equal(statsd.mock, undefined); + assert.deepEqual(statsd.globalTags, []); + assert.ok(!statsd.mock); + assert.equal(statsd.sampleRate, 1); + assert.equal(statsd.maxBufferSize, 0); + assert.equal(statsd.bufferFlushInterval, 1000); + assert.equal(statsd.telegraf, false); + assert.equal(statsd.protocol, undefined); // Defaults to UDP + }); + + it('should map global_tags to globalTags for backwards compatibility', function () { + var statsd = createStatsdClient({ global_tags: ['gtag'] }); + assert.deepEqual(statsd.globalTags, ['gtag']); + }); + + it('should attempt to cache a dns record if dnsCache is specified', function (done) { + var originalLookup = dns.lookup; + var statsd; + + // Replace the dns lookup function with our mock dns lookup + dns.lookup = function (host, callback) { + process.nextTick(function( ) { + dns.lookup = originalLookup; + assert.equal(statsd.host, host); + callback(null, '127.0.0.1', 4); + assert.equal(statsd.host, '127.0.0.1'); + done(); + }); + }; + + statsd = createStatsdClient({ host: 'localhost', cacheDns: true }); + }); + + it('should not attempt to cache a dns record if dnsCache is not specified', function (done) { + var originalLookup = dns.lookup; + var statsd; + + // Replace the dns lookup function with our mock dns lookup + dns.lookup = function (host, callback) { + assert.ok(false, 'StatsD constructor should not invoke dns.lookup when dnsCache is unspecified'); + dns.lookup = originalLookup; + }; + + statsd = createStatsdClient({ host: 'localhost' }); + process.nextTick(function () { + dns.lookup = originalLookup; + done(); + }); + }); + + it('should given an error in callbacks for a bad dns record if dnsCache is specified', function (done) { + var originalLookup = dns.lookup; + var statsd; + + // Replace the dns lookup function with our mock dns lookup + dns.lookup = function(host, callback) { + return callback(new Error('Bad host')); + }; + + statsd = createStatsdClient({ host: 'localhost', cacheDns: true }); + + statsd.increment('test', 1, 1, null, function (err) { + assert.equal(err.message, 'Bad host'); + dns.lookup = originalLookup; + done(); + }); + }); + + it('should create a global variable set to StatsD() when specified', function () { + var statsd = createStatsdClient(['host', 1234, 'prefix', 'suffix', true]); + assert.ok(global.statsd instanceof StatsD); + }); + + it('should not create a global variable when not specified', function () { + var statsd = createStatsdClient(['host', 1234, 'prefix', 'suffix']); + assert.equal(global.statsd, undefined); + }); + + it('should create a mock Client when mock variable is specified', function(){ + var statsd = createStatsdClient(['host', 1234, 'prefix', 'suffix', false, false, true]); + assert.ok(statsd.mock); + }); + + it('should create a socket variable that is an instance of dgram.Socket', function () { + var statsd = createStatsdClient(); + assert.ok(statsd.socket instanceof dgram.Socket); + }); + + it('should create a socket variable that is an instance of net.Socket if set to TCP', function (done) { + createTCPServer(function (address) { + var statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + assert.ok(statsd.socket instanceof net.Socket); + done(); + }).close(); + }); + }); + }); + }); +}; diff --git a/test/send.js b/test/send.js new file mode 100644 index 00000000..7b917ce9 --- /dev/null +++ b/test/send.js @@ -0,0 +1,83 @@ +'use strict'; + +var assert = require('assert'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runSendAllMethodTestSuite() { + describe('#send', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should use errorHandler', function (done) { + server = createUDPServer(function () { + var err = new Error('Boom!'); + statsd = createStatsdClient({ + errorHandler: function (e) { + assert.equal(e, err); + server.close(); + done(); + } + }, index); + statsd.dnsError = err; + statsd.send('test title'); + }); + }); + + it('should record buffers when mocked', function (done) { + var statsd = createStatsdClient({ mock: true }); + statsd.send('test', {}, function() { + assert.deepEqual(statsd.mockBuffer, ['test']); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should use errorHandler', function (done) { + server = createTCPServer(function (address) { + var err = new Error('Boom!'); + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp', + errorHandler: function (e) { + assert.equal(e, err); + server.close(); + done(); + } + }, index); + statsd.dnsError = err; + statsd.send('test title'); + }); + }); + + it('should record buffers when mocked', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp', + mock: true + }, index); + statsd.send('test', {}, function() { + assert.deepEqual(statsd.mockBuffer, ['test']); + done(); + }); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/sendAll.js b/test/sendAll.js new file mode 100644 index 00000000..78d604a1 --- /dev/null +++ b/test/sendAll.js @@ -0,0 +1,70 @@ +'use strict'; + +var assert = require('assert'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runSendAllMethodTestSuite() { + describe('#sendAll', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should use errorHandler', function (done) { + server = createUDPServer(function (address) { + var err = new Error('Boom!'); + statsd = createStatsdClient({ + host: address.address, + port: address.port, + errorHandler: function (e) { + assert.equal(e, err); + } + }, index); + statsd.sendStat = function (item, value, type, sampleRate, tags, callback) { + callback(err); + }; + statsd.sendAll(['test title'], 'another desc'); + statsd.close(function () { + server.close(); + done(); + }); + }); + }); + }); + + describe('TCP', function () { + it('should use errorHandler', function (done) { + server = createTCPServer(function (address) { + var err = new Error('Boom!'); + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp', + errorHandler: function (e) { + assert.equal(e, err); + } + }, index); + statsd.sendStat = function (item, value, type, sampleRate, tags, callback) { + callback(err); + }; + statsd.sendAll(['test title'], 'another desc'); + statsd.close(function () { + server.close(); + done(); + }); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/sendMessage.js b/test/sendMessage.js new file mode 100644 index 00000000..d12d00ab --- /dev/null +++ b/test/sendMessage.js @@ -0,0 +1,138 @@ +'use strict'; + +var assert = require('assert'); +var domain = require('domain'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runSendMessageMethodTestSuite() { + describe('#sendMessage', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should use errorHandler', function (done) { + server = createUDPServer(function (address) { + var err = new Error('Boom!'); + statsd = createStatsdClient({ + host: address.address, + port: address.port, + errorHandler: function (e) { + assert.equal(e, err); + done(); + } + }, index); + statsd.dnsError = err; + statsd.send('test title'); + }); + }); + + it('should errback for an unresolvable host', function (done) { + statsd = createStatsdClient({ host: '...' }); + statsd.send('test title', [], function (error) { + assert.ok(error); + assert.equal(error.code, 'ENOTFOUND'); + done(); + }); + }); + + it('should use errorHandler for an unresolvable host', function (done) { + statsd = createStatsdClient({ + host: '...', + errorHandler: function (error) { + assert.ok(error); + assert.equal(error.code, 'ENOTFOUND'); + done(); + } + }); + statsd.send('test title'); + }); + + it('should throw for an unresolvable host', function (done) { + var d = domain.create(); + statsd = createStatsdClient({ host: '...' }); + + d.add(statsd.socket); + d.on('error', function (error) { + assert.ok(error); + assert.equal(error.code, 'ENOTFOUND'); + // Important to exit the domain or further tests will continue to run + // therein. + d.exit(); + done(); + }); + + d.run(function () { statsd.send('test title'); }); + }); + }); + + describe('TCP', function () { + it('should use errorHandler', function (done) { + server = createTCPServer(function (address) { + var err = new Error('Boom!'); + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp', + errorHandler: function (e) { + assert.equal(e, err); + done(); + } + }, index); + statsd.dnsError = err; + statsd.send('test title'); + }); + }); + + it.skip('should errback for an unresolvable host', function () { + // This one blows up on socket.on('error') level and cannot be + // catched inside the statsd.send callback. This case is the + // same as above. + }); + + it('should use errorHandler for an unresolvable host', function (done) { + statsd = createStatsdClient({ + host: '...', + protocol: 'tcp', + errorHandler: function (e) { + assert.ok(e); + assert.equal(e.code, 'ENOTFOUND'); + done(); + } + }, index); + statsd.send('test title'); + }); + + it('should throw for an unresolvable host', function (done) { + var d = domain.create(); + statsd = createStatsdClient({ + host: '...', + protocol: 'tcp' + }); + + d.add(statsd.socket); + d.on('error', function (error) { + assert.ok(error); + assert.equal(error.code, 'ENOTFOUND'); + // Important to exit the domain or further tests will continue to run + // therein. + d.exit(); + done(); + }); + + d.run(function () { statsd.send('test title'); }); + }); + }); + }); + }); + }); +}; diff --git a/test/statsFunctions.js b/test/statsFunctions.js new file mode 100644 index 00000000..003cadbf --- /dev/null +++ b/test/statsFunctions.js @@ -0,0 +1,892 @@ +'use strict'; + +var assert = require('assert'); +var dgram = require('dgram'); +var net = require('net'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runHistogramTestSuite() { + describe('#statsFunctions', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + [ { name: 'timing', unit: 'ms', bytes: 14 }, + { name: 'histogram', unit: 'h', bytes: 12 }, + { name: 'distribution', unit: 'd', bytes: 12 }, + { name: 'gauge', unit: 'g', bytes: 12 }, + { name: 'set', unit: 's', bytes: 12 }, + ].forEach(function (statFunction) { + describe('#' + statFunction.name, function () { + describe('UDP', function () { + it('should send proper ' + statFunction.name + + ' format without prefix, suffix, sampling and callback', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd[statFunction.name]('test', 42); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|' + statFunction.unit); + server.close(); + done(); + }); + }); + + it('should send proper ' + statFunction.name + ' format with tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd[statFunction.name]('test', 42, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|' + statFunction.unit + '|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send proper ' + statFunction.name + + ' format with prefix, suffix, sampling and callback', function (done) { + var called = false; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'foo.', + suffix: '.bar' + }, index); + statsd[statFunction.name]('test', 42, 0.5, function () { + called = true; + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'foo.test.bar:42|' + statFunction.unit + '|@0.5'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should properly send a and b with the same value', function (done) { + var called = 0; + var noOfMessages = 0; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }); + statsd[statFunction.name](['a', 'b'], 42, null, function (error, bytes) { + called += 1; + assert.ok(called === 1); // Ensure it only gets called once + assert.equal(error, null); + assert.equal(bytes, statFunction.bytes); + }); + }); + server.on('metrics', function (metric) { + if (noOfMessages === 0) { + assert.equal(metric, 'a:42|' + statFunction.unit); + noOfMessages += 1; + } else { + assert.equal(metric, 'b:42|' + statFunction.unit); + server.close(); + done(); + } + }); + }); + + it('should send no ' + statFunction.name + ' stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createUDPServer(function (address) { + statsd = createStatsdClient([ + address.address, address.port, 'prefix', 'suffix', false, false, true + ], index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd[statFunction.name]('test', 1); + + statsd[statFunction.name]('test', 1, null, function (error, bytes) { + var socket = dgram.createSocket("udp4"); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.send(buf, 0, buf.length, address.port, address.address, function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + + it('should format tags to datadog format by default', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }); + statsd[statFunction.name]('test', 42, {foo: 'bar'}); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|' + statFunction.unit + '|#foo:bar'); + server.close(); + done(); + }); + }); + + it('should format tags when using telegraf format', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true + }); + statsd[statFunction.name]('test', 42, { foo: 'bar'}); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,foo=bar:42|' + statFunction.unit); + server.close(); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should send proper ' + statFunction.name + + ' format without prefix, suffix, sampling and callback', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd[statFunction.name]('test', 42); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|' + statFunction.unit + '\n'); + server.close(); + done(); + }); + }); + + it('should send proper ' + statFunction.name + ' format with tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd[statFunction.name]('test', 42, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|' + statFunction.unit + '|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send proper ' + statFunction.name + + ' format with prefix, suffix, sampling and callback', function (done) { + var called = false; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'foo.', + suffix: '.bar', + protocol: 'tcp' + }, index); + statsd[statFunction.name]('test', 42, 0.5, function () { + called = true; + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'foo.test.bar:42|' + statFunction.unit + '|@0.5\n'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should properly send a and b with the same value', function (done) { + var called = 0; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }); + statsd[statFunction.name](['a', 'b'], 42, null, function (error, bytes) { + called += 1; + assert.ok(called === 1); // Ensure it only gets called once + assert.equal(error, null); + assert.equal(bytes, 0); + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:42|' + statFunction.unit + '\nb:42|' + statFunction.unit + '\n'); + server.close(); + done(); + }); + }); + + it('should send no ' + statFunction.unit + ' stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix', + suffix: 'suffix', + mock: true, + protocol: 'tcp' + }, index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd[statFunction.name]('test', 1); + + statsd[statFunction.name]('test', 1, null, function (error, bytes) { + var socket = net.connect(address.port, address.address); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.write(buf, 0, 'ascii', function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + + it('should format tags to datadog format by default', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }); + statsd[statFunction.name]('test', 42, { foo: 'bar' }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|' + statFunction.unit + '|#foo:bar\n'); + server.close(); + done(); + }); + }); + + it('should format tags when using telegraf format', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + telegraf: true, + protocol: 'tcp' + }); + statsd[statFunction.name]('test', 42, { foo: 'bar' }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test,foo=bar:42|' + statFunction.unit + '\n'); + server.close(); + done(); + }); + }); + }); + }); + }); + + describe('#increment', function () { + describe('UDP', function () { + it('should send count by 1 when no params are specified', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c'); + server.close(); + done(); + }); + }); + + it('should use when increment is 0', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.increment('test', 0); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:0|c'); + server.close(); + done(); + }); + }); + + it('should send proper count format with tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.increment('test', 42, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|c|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send default count 1 with tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.increment('test', ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send tags when sampleRate is omitted', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.increment('test', 23, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:23|c|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send proper count format with prefix, suffix, sampling and callback', function (done) { + var called = false; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'foo.', + suffix: '.bar' + }, index); + statsd.increment('test', 42, 0.5, function () { + called = true; + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'foo.test.bar:42|c|@0.5'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should properly send a and b with the same value', function (done) { + var called = 0; + var noOfMessages = 0; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }); + statsd.increment(['a', 'b'], null, function (error, bytes) { + called += 1; + assert.ok(called === 1); // Ensure it only gets called once + assert.equal(error, null); + assert.equal(bytes, 10); + }); + }); + server.on('metrics', function (metric) { + if (noOfMessages === 0) { + assert.equal(metric, 'a:1|c'); + noOfMessages += 1; + } else { + assert.equal(metric, 'b:1|c'); + server.close(); + done(); + } + }); + }); + + it('should send no increment stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createUDPServer(function (address) { + statsd = createStatsdClient([ + address.address, address.port, 'prefix', 'suffix', false, false, true + ], index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.increment('test', 1); + + statsd.increment('test', 1, null, function (error, bytes) { + var socket = dgram.createSocket("udp4"); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.send(buf, 0, buf.length, address.port, address.address, function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should send count by 1 when no params are specified', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.increment('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c\n'); + server.close(); + done(); + }); + }); + + it('should use when increment is 0', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.increment('test', 0); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:0|c\n'); + server.close(); + done(); + }); + }); + + it('should send proper count format with tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.increment('test', 42, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:42|c|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send default count 1 with tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.increment('test', ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:1|c|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send tags when sampleRate is omitted', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.increment('test', 23, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:23|c|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send proper count format with prefix, suffix, sampling and callback', function (done) { + var called = false; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'foo.', + suffix: '.bar', + protocol: 'tcp' + }, index); + statsd.increment('test', 42, 0.5, function () { + called = true; + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'foo.test.bar:42|c|@0.5\n'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should properly send a and b with the same value', function (done) { + var called = 0; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }); + statsd.increment(['a', 'b'], 42, null, function (error, bytes) { + called += 1; + assert.ok(called === 1); // Ensure it only gets called once + assert.equal(error, null); + assert.equal(bytes, 0); + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:42|c\nb:42|c\n'); + server.close(); + done(); + }); + }); + + it('should send no increment stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix', + suffix: 'suffix', + mock: true, + protocol: 'tcp' + }, index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.increment('test', 1); + + statsd.increment('test', 1, null, function (error, bytes) { + var socket = net.connect(address.port, address.address); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.write(buf, 0, 'ascii', function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + }); + }); + + describe('#decrement', function () { + describe('UDP', function () { + it('should send count by -1 when no params are specified', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.decrement('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:-1|c'); + server.close(); + done(); + }); + }); + + it('should send proper count format with tags', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + }, index); + statsd.decrement('test', 42, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:-42|c|#foo,bar'); + server.close(); + done(); + }); + }); + + it('should send proper count format with prefix, suffix, sampling and callback', function (done) { + var called = false; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'foo.', + suffix: '.bar' + }, index); + statsd.decrement('test', 42, 0.5, function () { + called = true; + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'foo.test.bar:-42|c|@0.5'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should properly send a and b with the same value', function (done) { + var called = 0; + var noOfMessages = 0; + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }); + statsd.decrement(['a', 'b'], null, function (error, bytes) { + called += 1; + assert.ok(called === 1); // Ensure it only gets called once + assert.equal(error, null); + assert.equal(bytes, 12); + }); + }); + server.on('metrics', function (metric) { + if (noOfMessages === 0) { + assert.equal(metric, 'a:-1|c'); + noOfMessages += 1; + } else { + assert.equal(metric, 'b:-1|c'); + server.close(); + done(); + } + }); + }); + + it('should send no increment stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createUDPServer(function (address) { + statsd = createStatsdClient([ + address.address, address.port, 'prefix', 'suffix', false, false, true + ], index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.decrement('test', 1); + + statsd.decrement('test', 1, null, function (error, bytes) { + var socket = dgram.createSocket("udp4"); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.send(buf, 0, buf.length, address.port, address.address, function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should send count by -1 when no params are specified', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.decrement('test'); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:-1|c\n'); + server.close(); + done(); + }); + }); + + it('should send proper count format with tags', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + statsd.decrement('test', 42, ['foo', 'bar']); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'test:-42|c|#foo,bar\n'); + server.close(); + done(); + }); + }); + + it('should send proper count format with prefix, suffix, sampling and callback', function (done) { + var called = false; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'foo.', + suffix: '.bar', + protocol: 'tcp' + }, index); + statsd.decrement('test', 42, 0.5, function () { + called = true; + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'foo.test.bar:-42|c|@0.5\n'); + assert.equal(called, true); + server.close(); + done(); + }); + }); + + it('should properly send a and b with the same value', function (done) { + var called = 0; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }); + statsd.decrement(['a', 'b'], 42, null, function (error, bytes) { + called += 1; + assert.ok(called === 1); // Ensure it only gets called once + assert.equal(error, null); + assert.equal(bytes, 0); + }); + }); + server.on('metrics', function (metrics) { + assert.equal(metrics, 'a:-42|c\nb:-42|c\n'); + server.close(); + done(); + }); + }); + + it('should send no increment stat when a mock Client is used', function (done) { + var TEST_FINISHED_MESSAGE = 'TEST_FINISHED'; + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + prefix: 'prefix', + suffix: 'suffix', + mock: true, + protocol: 'tcp' + }, index); + + // Regression test for "undefined is not a function" with missing + // callback on mock instance + statsd.decrement('test', 1); + + statsd.decrement('test', 1, null, function (error, bytes) { + var socket = net.connect(address.port, address.address); + var buf = new Buffer(TEST_FINISHED_MESSAGE); + + assert.ok(!error); + assert.equal(bytes, 0); + // We should call finished() here, but we have to work around + // https://github.com/joyent/node/issues/2867 on node 0.6, + // such that we don't close the socket within the `listening` event + // and pass a single message through instead. + socket.write(buf, 0, 'ascii', function () { + socket.close(); + }); + }); + }); + server.on('metrics', function (message) { + // We only expect to get our own test finished message, no stats + assert.equal(message, TEST_FINISHED_MESSAGE); + server.close(); + done(); + }); + }); + }); + }); + }); + }); + }); +}; diff --git a/test/test_statsd.js b/test/test_statsd.js deleted file mode 100644 index 5733bf30..00000000 --- a/test/test_statsd.js +++ /dev/null @@ -1,1680 +0,0 @@ -"use strict"; - -var dgram = require('dgram'), - domain = require('domain'), - assert = require('assert'), - mainStatsD = require('../').StatsD; - -/** - * Creates a test harness, that binds to an ephemeral port - * @param test {Function} The test to run, should take message as the argument - * @param callback {Function} The callback to call after the server is listening - * @private - */ -function udpTest(test, callback){ - var server = dgram.createSocket("udp4"); - server.on('message', function(message){ - test(message.toString(), server); - }); - - server.on('listening', function(){ - callback(server); - }); - - server.bind(0, '127.0.0.1'); -} - -/** - * Since sampling uses random, we need to patch Math.random() to always give - * a consistent result - */ -Math.random = function(){ - return 0.42; -}; - -beforeEach(function () { - //remove it from the namespace to not fail other tests - delete global.statsd; -}); - -describe('StatsD (main client only)', function (StatsD) { - describe('#init', function() { - - it('should set the proper values when specified', function(){ - // cachedDns isn't tested here; see below - var statsd = new StatsD('host', 1234, 'prefix', 'suffix', true, null, true, ['gtag']); - assert.equal(statsd.host, 'host'); - assert.equal(statsd.port, 1234); - assert.equal(statsd.prefix, 'prefix'); - assert.equal(statsd.suffix, 'suffix'); - assert.equal(statsd, global.statsd); - assert.equal(statsd.mock, true); - assert.deepEqual(statsd.globalTags, ['gtag']); - }); - - it('should set the proper values with options hash format', function(){ - // cachedDns isn't tested here; see below - var statsd = new StatsD({ - host: 'host', - port: 1234, - prefix: 'prefix', - suffix: 'suffix', - globalize: true, - mock: true, - globalTags: ['gtag'], - sampleRate: 0.6 - }); - assert.equal(statsd.host, 'host'); - assert.equal(statsd.port, 1234); - assert.equal(statsd.prefix, 'prefix'); - assert.equal(statsd.suffix, 'suffix'); - assert.equal(statsd, global.statsd); - assert.equal(statsd.mock, true); - assert.equal(statsd.sampleRate, 0.6); - assert.deepEqual(statsd.globalTags, ['gtag']); - }); - - it('should map global_tags to globalTags for backwards compatibility', function(){ - var statsd = new StatsD({ - global_tags: ['gtag'] - }); - assert.deepEqual(statsd.globalTags, ['gtag']); - }); - - it('should attempt to cache a dns record if dnsCache is specified', function(done){ - var dns = require('dns'), - originalLookup = dns.lookup, - statsd; - - // replace the dns lookup function with our mock dns lookup - dns.lookup = function(host, callback){ - process.nextTick(function(){ - dns.lookup = originalLookup; - assert.equal(statsd.host, host); - callback(null, '127.0.0.1', 4); - assert.equal(statsd.host, '127.0.0.1'); - done(); - }); - }; - - statsd = new StatsD({host: 'localhost', cacheDns: true}); - }); - - it('should given an error in callbacks for a bad dns record if dnsCache is specified', function(done){ - var dns = require('dns'), - originalLookup = dns.lookup, - statsd; - - // replace the dns lookup function with our mock dns lookup - dns.lookup = function(host, callback){ - return callback(new Error('that is a bad host')); - }; - - statsd = new StatsD({host: 'localhost', cacheDns: true}); - - statsd.increment('test', 1, 1, null, function(err) { - assert.equal(err.message, 'that is a bad host'); - dns.lookup = originalLookup; - done(); - }); - }); - - it('should create a global variable set to StatsD() when specified', function(){ - var statsd = new StatsD('host', 1234, 'prefix', 'suffix', true); - assert.ok(global.statsd instanceof StatsD); - }); - - }); - - describe('#buffer', function() { - it('should aggregate packets when maxBufferSize is set to non-zero', function (finished) { - udpTest(function (message, server) { - assert.equal(message, 'a:1|c\nb:2|c\n'); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 12 - }; - var statsd = new StatsD(options); - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - }); - -}.bind(null, mainStatsD)); - -describe('StatsD (child client only)', function (StatsD) { - describe('#init', function() { - - it('should set the proper values when specified', function(){ - var statsd = new StatsD('host', 1234, 'prefix', 'suffix', true, null, true, ['gtag', 'tag1:234234']); - var child = statsd.childClient({ - prefix: 'preff.', - suffix: '.suff', - globalTags: ['awesomeness:over9000', 'tag1:xxx', 'bar', ':baz'] - }); - assert.equal(child.prefix, 'preff.prefix'); - assert.equal(child.suffix, 'suffix.suff'); - assert.equal(statsd, global.statsd); - assert.deepEqual(child.globalTags, ['gtag', 'tag1:xxx', 'awesomeness:over9000', 'bar', ':baz']); - }); - - }); - - describe('#buffer', function() { - it('should aggregate packets when maxBufferSize is set to non-zero', function (finished) { - udpTest(function (message, server) { - assert.equal(message, 'a:1|c\nb:2|c\n'); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 12 - }; - var statsd = new StatsD(options).childClient(); - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - }); - - describe('#childClient', function() { - - it('should add tags, prefix and suffix without parent values', function (finished) { - udpTest(function (message, server) { - assert.equal(message, 'preff.a.suff:1|c|#awesomeness:over9000\npreff.b.suff:2|c|#awesomeness:over9000\n'); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 500 - }; - var statsd = new StatsD(options).childClient({ - prefix: 'preff.', - suffix: '.suff', - globalTags: ['awesomeness:over9000'] - }); - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - - it('should add tags, prefix and suffix with parent values', function (finished) { - udpTest(function (message, server) { - assert.equal(message, 'preff.p.a.s.suff:1|c|#xyz,awesomeness:' + - 'over9000\npreff.p.b.s.suff:2|c|#xyz,awesomeness:over9000\n'); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - prefix: 'p.', - suffix: '.s', - globalTags: ['xyz'], - maxBufferSize: 500 - }; - var statsd = new StatsD(options).childClient({ - prefix: 'preff.', - suffix: '.suff', - globalTags: ['awesomeness:over9000'] - }); - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - - }); - -}.bind(null, mainStatsD)); - -function doTests(StatsD) { - /** - * Given a StatsD method, make sure no data is sent to the server - * for this method when used on a mock Client. - */ - function assertMockClientMethod(method, finished){ - var testFinished = "test finished message"; - - udpTest(function(message, server){ - // We only expect to get our own test finished message, no stats. - assert.equal(message, testFinished); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'prefix', 'suffix', false, false, - /* mock = true */ true), - socket = dgram.createSocket("udp4"), - buf = new Buffer(testFinished), - callbackThrows = false; - - // Regression test for "undefined is not a function" with missing callback on mock instance. - try { - statsd[method]('test', 1); - } catch(e) { - callbackThrows = true; - } - assert.ok(!callbackThrows); - - statsd[method]('test', 1, null, function(error, bytes){ - assert.ok(!error); - assert.equal(bytes, 0); - // We should call finished() here, but we have to work around - // https://github.com/joyent/node/issues/2867 on node 0.6, - // such that we don't close the socket within the `listening` event - // and pass a single message through instead. - socket.send(buf, 0, buf.length, address.port, address.address, - function(){ socket.close(); }); - }); - }); - } - - describe('#init', function(){ - - it('should set default values when not specified', function(){ - // cachedDns isn't tested here; see below - var statsd = new StatsD(); - assert.equal(statsd.host, 'localhost'); - assert.equal(statsd.port, 8125); - assert.equal(statsd.prefix, ''); - assert.equal(statsd.suffix, ''); - assert.equal(global.statsd, undefined); - assert.equal(statsd.mock, undefined); - assert.deepEqual(statsd.globalTags, []); - assert.ok(!statsd.mock); - }); - - it('should not attempt to cache a dns record if dnsCache is not specified', function(done){ - var dns = require('dns'), - originalLookup = dns.lookup, - statsd; - - // replace the dns lookup function with our mock dns lookup - dns.lookup = function(host, callback){ - assert.ok(false, 'StatsD constructor should not invoke dns.lookup when dnsCache is unspecified'); - dns.lookup = originalLookup; - }; - - statsd = new StatsD({host: 'localhost'}); - process.nextTick(function(){ - dns.lookup = originalLookup; - done(); - }); - }); - - it('should not create a global variable when not specified', function(){ - var statsd = new StatsD('host', 1234, 'prefix', 'suffix'); - assert.equal(global.statsd, undefined); - }); - - it('should create a mock Client when mock variable is specified', function(){ - var statsd = new StatsD('host', 1234, 'prefix', 'suffix', false, false, true); - assert.ok(statsd.mock); - }); - - it('should create a socket variable that is an instance of dgram.Socket', function(){ - var statsd = new StatsD(); - assert.ok(statsd.socket instanceof dgram.Socket); - }); - - }); - - describe('#globalTags', function(){ - it('should not add global tags if they are not specified', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment('test'); - }); - }); - - it('should add global tags if they are specified', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1|c|#gtag'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: ['gtag'] - }); - - statsd.increment('test'); - }); - }); - - it('should combine global tags and metric tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1337|c|#gtag,foo'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: ['gtag'] - }); - - statsd.increment('test', 1337, ['foo']); - }); - }); - - it('should override global tags with metric tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1337|c|#foo,gtag:234,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: ['foo', 'gtag:123'] - }); - - statsd.increment('test', 1337, ['gtag:234', 'bar']); - }); - }); - - it('should format global tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1337|c|#gtag:234,foo:bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: { gtag: "123", foo: "bar"} - }); - - statsd.increment('test', 1337, { gtag: "234"}); - }); - }); - - it('should replace reserved characters with underscores in tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1337|c|#foo:b_a_r,reserved_character:is_replaced_'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: { foo: "b,a,r"} - }); - - statsd.increment('test', 1337, { "reserved:character": "is@replaced@"}); - }); - }); - - it('should add global tags using telegraf format when enabled', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test,gtag=gvalue,gtag2=gvalue2:1|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: ['gtag:gvalue', 'gtag2:gvalue2'], - telegraf: true - }); - - statsd.increment('test'); - }); - }); - - it('should combine global tags and metric tags using telegraf format when enabled', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test,gtag=gvalue,foo=bar:1337|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: ['gtag=gvalue'], - telegraf: true - }); - - statsd.increment('test', 1337, ['foo:bar']); - }); - }); - - it('should format global key-value tags using telegraf format when enabled', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test,gtag=gvalue,foo=bar:1337|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - globalTags: { gtag: "gvalue"}, - telegraf: true - }); - - statsd.increment('test', 1337, { foo: "bar" }); - }); - }); - - }); - - describe('#timer', function() { - it('should send stat and time to execute to timing function', function(finished) { - udpTest(function (message, server) { - // Search for a string similar to 'test:0.123|ms' - var re = RegExp("(test:)([0-9]+\.[0-9]+)\\|{1}(ms)"); - assert.equal(true, re.test(message)); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - var testFunc = function(a, b) { - return a + b; - }; - - statsd.timer(testFunc, 'test')(2, 2); - }); - }); - - it('should send data with tags to timing function', function(finished) { - udpTest(function (message, server) { - // Search for a string similar to 'test:0.123|ms|#foo,bar' - var re = RegExp("(test:)([0-9]+\.[0-9]+)\\|{1}(ms)\\|{1}\\#(foo,bar)"); - assert.equal(true, re.test(message)); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - var testFunc = function (a, b) { - return a + b; - }; - - statsd.timer(testFunc, 'test', undefined, ['foo', 'bar'])(2, 2); - }); - }); - }); - - describe('#timing', function(){ - it('should send proper time format without prefix, suffix, sampling and callback', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|ms'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.timing('test', 42); - }); - }); - - it('should send proper time format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|ms|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.timing('test', 42, ['foo', 'bar']); - }); - }); - - it('should send proper time format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:42|ms|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.timing('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - it('should properly send a and b with the same value', function(finished){ - var called = false, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:42|ms'); - messageNumber += 1; - } else { - assert.equal(message, 'b:42|ms'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.timing(['a', 'b'], 42, null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 14); - }); - }); - }); - - it('should send no timing stat when a mock Client is used', function(finished){ - assertMockClientMethod('timing', finished); - }); - - it('should format tags to datadog format by default', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|ms|#foo:bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.timing('test', 42, {foo: 'bar'}); - }); - }); - - it('should format tags when using telegraf format', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test,foo=bar:42|ms'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD({ - address: address.address, - port: address.port, - telegraf: true - }); - - statsd.timing('test', 42, { foo: 'bar'}); - }); - }); - - }); - - describe('#histogram', function(){ - it('should send proper histogram format without prefix, suffix, sampling and callback', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|h'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.histogram('test', 42); - }); - }); - - it('should send proper histogram format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|h|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.histogram('test', 42, ['foo', 'bar']); - }); - }); - - it('should send proper histogram format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:42|h|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.histogram('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - it('should properly send a and b with the same value', function(finished){ - var called = 0, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:42|h'); - messageNumber += 1; - } else { - assert.equal(message, 'b:42|h'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.histogram(['a', 'b'], 42, null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 12); - }); - }); - }); - - it('should send no histogram stat when a mock Client is used', function(finished){ - assertMockClientMethod('histogram', finished); - }); - - it('should call callback after histogram call', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|h'); - server.close(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.histogram('test', 42, null, null, function(err, data) { - assert.equal(data, 9); - finished(); - }); - }); - }); - - it('should have error in callback after bad histogram call', function(finished){ - udpTest(function(message, server){ - throw new Error('should not be called'); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.close(function() { - statsd.histogram('test', 42, null, null, function(err, data) { - assert.ok(err !== undefined); - assert.ok(data === undefined); - finished(); - }); - }); - }); - }); - - }); - - describe('#distribution', function(){ - it('should send proper distribution format without prefix, suffix, sampling and callback', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|d'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.distribution('test', 42); - }); - }); - - it('should send proper distribution format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|d|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.distribution('test', 42, ['foo', 'bar']); - }); - }); - - it('should send proper distribution format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:42|d|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.distribution('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - it('should properly send a and b with the same value', function(finished){ - var called = 0, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:42|d'); - messageNumber += 1; - } else { - assert.equal(message, 'b:42|d'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.distribution(['a', 'b'], 42, null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 12); - }); - }); - }); - - it('should send no distribution stat when a mock Client is used', function(finished){ - assertMockClientMethod('distribution', finished); - }); - - it('should call callback after distribution call', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|d'); - server.close(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.distribution('test', 42, null, null, function(err, data) { - assert.equal(data, 9); - finished(); - }); - }); - }); - - it('should have error in callback after bad distribution call', function(finished){ - udpTest(function(message, server){ - throw new Error('should not be called'); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.close(function() { - statsd.distribution('test', 42, null, null, function(err, data) { - assert.ok(err !== undefined); - assert.ok(data === undefined); - finished(); - }); - }); - }); - }); - - }); - - describe('#gauge', function(){ - it('should send proper gauge format without prefix, suffix, sampling and callback', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|g'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.gauge('test', 42); - }); - }); - - it('should send proper gauge format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|g|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.gauge('test', 42, ['foo', 'bar']); - }); - }); - - it('should send proper gauge format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:42|g|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.gauge('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - it('should properly send a and b with the same value', function(finished){ - var called = 0, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:42|g'); - messageNumber += 1; - } else { - assert.equal(message, 'b:42|g'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.gauge(['a', 'b'], 42, null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 12); - }); - }); - }); - - it('should send no gauge stat when a mock Client is used', function(finished){ - assertMockClientMethod('gauge', finished); - }); - }); - - describe('#increment', function(){ - it('should send count by 1 when no params are specified', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment('test'); - }); - }); - - it('should use when increment is 0', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:0|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment('test', 0); - }); - }); - - it('should send proper count format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|c|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment('test', 42, ['foo', 'bar']); - }); - }); - - it('should send default count 1 with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:1|c|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment('test', ['foo', 'bar']); - }); - }); - - it('should send tags when sampleRate is omitted', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:23|c|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment('test', 23, ['foo', 'bar']); - }); - }); - - it('should send proper count format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:42|c|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.increment('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - it('should properly send a and b with the same value', function(finished){ - var called = 0, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:1|c'); - messageNumber += 1; - } else { - assert.equal(message, 'b:1|c'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.increment(['a', 'b'], null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 10); - }); - }); - }); - - it('should send no increment stat when a mock Client is used', function(finished){ - assertMockClientMethod('increment', finished); - }); - }); - - describe('#decrement', function(){ - it('should send count by -1 when no params are specified', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:-1|c'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.decrement('test'); - }); - }); - - it('should send proper count format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:-42|c|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.decrement('test', 42, ['foo', 'bar']); - }); - }); - - it('should send proper count format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:-42|c|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.decrement('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - - it('should properly send a and b with the same value', function(finished){ - var called = 0, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:-1|c'); - messageNumber += 1; - } else { - assert.equal(message, 'b:-1|c'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.decrement(['a', 'b'], null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 12); - }); - }); - }); - - it('should send no decrement stat when a mock Client is used', function(finished){ - assertMockClientMethod('decrement', finished); - }); - }); - - describe('#set', function(){ - it('should send proper set format without prefix, suffix, sampling and callback', 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); - - statsd.set('test', 42); - }); - }); - - it('should send proper set format with tags', function(finished){ - udpTest(function(message, server){ - assert.equal(message, 'test:42|s|#foo,bar'); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.set('test', 42, ['foo', 'bar']); - }); - }); - - it('should send proper set format with prefix, suffix, sampling and callback', function(finished){ - var called = false; - udpTest(function(message, server){ - assert.equal(message, 'foo.test.bar:42|s|@0.5'); - assert.equal(called, true); - server.close(); - finished(); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port, 'foo.', '.bar'); - - statsd.unique('test', 42, 0.5, function(){ - called = true; - }); - }); - }); - - it('should properly send a and b with the same value', function(finished){ - var called = 0, - messageNumber = 0; - - udpTest(function(message, server){ - if(messageNumber === 0){ - assert.equal(message, 'a:42|s'); - messageNumber += 1; - } else { - assert.equal(message, 'b:42|s'); - server.close(); - finished(); - } - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.unique(['a', 'b'], 42, null, function(error, bytes){ - called += 1; - assert.ok(called === 1); //ensure it only gets called once - assert.equal(error, null); - assert.equal(bytes, 12); - }); - }); - }); - - it('should send no set stat when a mock Client is used', function(finished){ - assertMockClientMethod('set', finished); - }); - }); - describe('#event', function() { - it('should send proper event format for title and text', function (finished) { - udpTest(function (message, server) { - assert.equal(message, '_e{4,11}:test|description'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.event('test', 'description'); - }); - }); - - it('should reuse the title when when text is missing', function (finished) { - udpTest(function (message, server) { - assert.equal(message, '_e{4,4}:test|test'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.event('test'); - }); - }); - - it('should send proper event format for title, text, and options', function (finished) { - var date = new Date(); - udpTest(function (message, server) { - assert.equal(message, '_e{10,31}:test title|another\\nmultiline\\ndescription|d:' + - Math.round(date.getTime() / 1000) + '|h:host|k:ag_key|p:low|s:source_type|t:warning'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port), - options = { - date_happened: date, - hostname: 'host', - aggregation_key: 'ag_key', - priority: 'low', - source_type_name: 'source_type', - alert_type: 'warning' - }; - - statsd.event('test title', 'another\nmultiline\ndescription', options); - }); - }); - - it('should send proper event format for title, text, some options, and tags', function (finished) { - udpTest(function (message, server) { - assert.equal(message, '_e{10,12}:test title|another desc|h:host|#foo,bar'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port), - options = { - hostname: 'host' - }; - - statsd.event('test title', 'another desc', options, ['foo', 'bar']); - }); - }); - - it('should send proper event format for title, text, tags, and a callback', function (finished) { - var called = true; - udpTest(function (message, server) { - assert.equal(message, '_e{10,12}:test title|another desc|#foo,bar'); - assert.equal(called, true); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.event('test title', 'another desc', null, ['foo', 'bar'], function(){ - called = true; - }); - }); - }); - - it('should send no event stat when a mock Client is used', function(finished){ - assertMockClientMethod('event', finished); - }); - - it('should throw and execption when using telegraf format', function(finished){ - udpTest(function () { - // will not fire - }, function (server) { - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - telegraf: true - }); - - assert.throws(function () { - statsd.event('test title', 'another desc', null, ['foo', 'bar']); - }, function (err) { - server.close(); - finished(); - }); - }); - }); - - it('should use errorHandler', function (finished) { - var statsd = new StatsD({ - telegraf: true, - errorHandler: function () { - finished(); - } - }); - statsd.event('test title', 'another desc'); - }); - - }); - - describe('#check', function() { - it('should send proper check format for name and status', function (finished) { - udpTest(function (message, server) { - assert.equal(message, '_sc|check.name|0'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.check('check.name', statsd.CHECKS.OK); - }); - }); - - it('should send proper check format for name and status with global prefix and suffix', function (finished) { - udpTest(function (message, server) { - assert.equal(message, '_sc|prefix.check.name.suffix|0'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - prefix: 'prefix.', - suffix: '.suffix' - }); - - statsd.check('check.name', statsd.CHECKS.OK); - }); - }); - - it('should send proper check format for name, status, and options', function (finished) { - var date = new Date(); - udpTest(function (message, server) { - assert.equal(message, '_sc|check.name|1|d:' + - Math.round(date.getTime() / 1000) + '|h:host|m:message'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port), - options = { - date_happened: date, - hostname: 'host', - message: 'message' - }; - - statsd.check('check.name', statsd.CHECKS.WARNING, options); - }); - }); - - it('should send proper check format for name, status, some options, and tags', function (finished) { - udpTest(function (message, server) { - assert.equal(message, '_sc|check.name|2|h:host|#foo,bar|m:message'); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port), - options = { - hostname: 'host', - message: 'message' - }; - - statsd.check('check.name', statsd.CHECKS.CRITICAL, options, ['foo', 'bar']); - }); - }); - - it('should send proper event format for name, status, tags, and a callback', function (finished) { - var called = true; - udpTest(function (message, server) { - assert.equal(message, '_sc|check.name|0|#foo,bar'); - assert.equal(called, true); - server.close(); - finished(); - }, function (server) { - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.check('check.name', statsd.CHECKS.OK, null, ['foo', 'bar'], function(){ - called = true; - }); - }); - }); - - it('should send no event stat when a mock Client is used', function(finished){ - assertMockClientMethod('check', finished); - }); - - it('should throw and execption when using telegraf format', function(finished){ - udpTest(function () { - // will not fire - }, function (server) { - var address = server.address(), - statsd = new StatsD({ - host: address.address, - port: address.port, - telegraf: true - }); - - assert.throws(function () { - statsd.check('check.name', statsd.CHECKS.OK, null, ['foo', 'bar']); - }, function (err) { - server.close(); - finished(); - }); - }); - }); - - it('should use errorHandler', function (finished) { - var statsd = new StatsD({ - telegraf: true, - errorHandler: function () { - finished(); - } - }); - statsd.check('check.name', statsd.CHECKS.OK); - }); - - }); - - describe('#buffer', function() { - it('should aggregate packets when maxBufferSize is set to non-zero', function (finished) { - udpTest(function (message, server) { - assert.equal(message, 'a:1|c\nb:2|c\n'); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 12 - }; - var statsd = new StatsD(options); - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - - it('should not send batches larger then maxBufferSize', function (finished) { - udpTest(function (message, server) { - assert.equal(message, 'a:1|c\n'); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 8 - }; - var statsd = new StatsD(options); - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - - it('should not aggregate packets when maxBufferSize is set to zero', function (finished) { - var results = [ - 'a:1|c', - 'b:2|c' - ]; - var msgCount = 0; - udpTest(function (message, server) { - var index = results.indexOf(message); - assert.equal(index >= 0, true); - results.splice(index, 1); - msgCount++; - if (msgCount >= 2) { - assert.equal(results.length, 0); - server.close(); - finished(); - } - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 0 - }; - var statsd = new StatsD(options); - - statsd.increment('a', 1); - statsd.increment('b', 2); - }); - }); - - it('should flush the buffer when timeout value elapsed', function (finished) { - var timestamp; - udpTest(function (message, server) { - assert.equal(message, 'a:1|c\n'); - var elapsed = Date.now() - timestamp; - assert.equal(elapsed > 1000, true); - server.close(); - finished(); - }, function (server) { - var address = server.address(); - var options = { - host: address.host, - port: address.port, - maxBufferSize: 1220, - bufferFlushInterval: 1100 - }; - var statsd = new StatsD(options); - - timestamp = new Date(); - statsd.increment('a', 1); - }); - }); - }); - - describe('#close', function(){ - - it('should call callback after close call', function(finished){ - udpTest(function(message, server){ - throw new Error('should not be called'); - }, function(server){ - var address = server.address(), - statsd = new StatsD(address.address, address.port); - - statsd.close(function() { - finished(); - }); - }); - }); - - it('should use errorHandler', function (finished) { - var statsd = new StatsD({ - errorHandler: function (e) { - finished(); - } - }); - statsd.socket.close = function () { - throw new Error('boom!'); - }; - statsd.close(); - }); - - }); - - describe('#send', function() { - - it('should use errorHandler', function (finished) { - var err = new Error('boom!'); - var statsd = new StatsD({ - errorHandler: function (e) { - assert.equal(e, err); - finished(); - } - }); - statsd.dnsError = err; - statsd.send('test title'); - }); - - it('should record buffers when mocked', function (finished) { - var statsd = new StatsD({ - mock: true - }); - statsd.send('test', {}, function() { - assert.deepEqual(statsd.mockBuffer, ['test']); - finished(); - }); - }); - }); - - describe('#sendMessage', function() { - - it('should use errorHandler', function (finished) { - var err = new Error('boom!'); - var statsd = new StatsD({ - errorHandler: function (e) { - assert.equal(e, err); - finished(); - } - }); - statsd.dnsError = err; - statsd.send('test title'); - }); - - it('should errback for an unresolvable host', function (finished) { - var statsd = new StatsD({ - host: 'unresolvable' - }); - - statsd.send('test title', [], function (error) { - assert.ok(error); - assert.equal(error.code, 'ENOTFOUND'); - finished(); - }); - }); - - it('should use errorHandler for an unresolvable host', function (finished) { - var statsd = new StatsD({ - host: 'unresolvable', - errorHandler: function (error) { - assert.ok(error); - assert.equal(error.code, 'ENOTFOUND'); - finished(); - } - }); - - statsd.send('test title'); - }); - - it('should throw for an unresolvable host', function (finished) { - var d = domain.create(); - var statsd = new StatsD({ - host: 'unresolvable', - }); - - d.add(statsd.socket); - - d.on('error', function (error) { - assert.ok(error); - assert.equal(error.code, 'ENOTFOUND'); - - // Important to exit the domain or further tests will continue to run - // therein. - d.exit(); - - finished(); - }); - - d.run(function () { - statsd.send('test title'); - }); - }); - - }); - - describe('#sendAll', function() { - - it('should use errorHandler', function (finished) { - var err = new Error('boom!'); - var statsd = new StatsD({ - errorHandler: function (e) { - assert.equal(e, err); - finished(); - } - }); - statsd.sendStat = function (item, value, type, sampleRate, tags, callback) { - callback(err); - }; - statsd.sendAll(['test title'], 'another desc'); - }); - - }); -} - -describe('StatsD main client', doTests.bind(null, mainStatsD)); -describe('StatsD child client', doTests.bind(null, function () { - // https://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible - var statsd = new ( - Function.prototype.bind.apply(mainStatsD, [null].concat(Array.prototype.slice.call(arguments, 0))) - )(); - return statsd.childClient({ - // empty options to verify same behaviour - }); -})); -describe('StatsD child of a child client', doTests.bind(null, function () { - var statsd = new ( - Function.prototype.bind.apply(mainStatsD, [null].concat(Array.prototype.slice.call(arguments, 0))) - )(); - return statsd.childClient({ - // empty options to verify same behaviour - }).childClient({ - // empty options to verify same behaviour - }); -})); diff --git a/test/timer.js b/test/timer.js new file mode 100644 index 00000000..d3813280 --- /dev/null +++ b/test/timer.js @@ -0,0 +1,109 @@ +'use strict'; + +var assert = require('assert'); + +var createStatsdClient = require('./helpers').createStatsdClient; +var createTCPServer = require('./helpers').createTCPServer; +var createUDPServer = require('./helpers').createUDPServer; + +module.exports = function runTimerTestSuite() { + describe('#timer', function () { + var server; + var statsd; + + afterEach(function () { + server = null; + statsd = null; + }); + + ['main client', 'child client', 'child of child client'].forEach(function (description, index) { + describe(description, function () { + describe('UDP', function () { + it('should send stat and time to execute to timing function', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + var testFn = function (a, b) { + return a + b; + }; + statsd.timer(testFn, 'test')(2, 2); + }); + server.on('metrics', function (metrics) { + // Search for a string similar to 'test:0.123|ms' + var re = RegExp("(test:)([0-9]+\.[0-9]+)\\|{1}(ms)"); + assert.equal(true, re.test(metrics)); + server.close(); + done(); + }); + }); + + it('should send data with tags to timing function', function (done) { + server = createUDPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port + }, index); + var testFn = function (a, b) { + return a + b; + }; + statsd.timer(testFn, 'test', undefined, ['foo', 'bar'])(2, 2); + }); + server.on('metrics', function (metrics) { + // Search for a string similar to 'test:0.123|ms|#foo,bar' + var re = RegExp("(test:)([0-9]+\.[0-9]+)\\|{1}(ms)\\|{1}\\#(foo,bar)"); + assert.equal(true, re.test(metrics)); + server.close(); + done(); + }); + }); + }); + + describe('TCP', function () { + it('should send stat and time to execute to timing function', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + var testFn = function (a, b) { + return a + b; + }; + statsd.timer(testFn, 'test')(2, 2); + }); + server.on('metrics', function (metrics) { + // Search for a string similar to 'test:0.123|ms' + var re = RegExp("(test:)([0-9]+\.[0-9]+)\\|{1}(ms)"); + assert.equal(true, re.test(metrics)); + server.close(); + done(); + }); + }); + + it('should send data with tags to timing function', function (done) { + server = createTCPServer(function (address) { + statsd = createStatsdClient({ + host: address.address, + port: address.port, + protocol: 'tcp' + }, index); + var testFn = function (a, b) { + return a + b; + }; + statsd.timer(testFn, 'test', undefined, ['foo', 'bar'])(2, 2); + }); + server.on('metrics', function (metrics) { + // Search for a string similar to 'test:0.123|ms|#foo,bar' + var re = RegExp("(test:)([0-9]+\.[0-9]+)\\|{1}(ms)\\|{1}\\#(foo,bar)"); + assert.equal(true, re.test(metrics)); + server.close(); + done(); + }); + }); + }); + }); + }); + }); +};