From 2ba5048203ac76d662d14dcd3c728aff6df55ad0 Mon Sep 17 00:00:00 2001 From: Brian Deitte Date: Wed, 26 Aug 2015 14:54:10 -0400 Subject: [PATCH 1/2] Add the event API used by DogStatsD, including readme info and tests. Also add more details about DogStatsD to the readme. --- README.md | 30 ++++++++++++++-- lib/statsd.js | 75 ++++++++++++++++++++++++++++++++++---- package.json | 6 +--- test/test_statsd.js | 88 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 185 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1820e7f..2da76ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # node-statsd -A node.js client for [Etsy](http://etsy.com)'s [StatsD](https://github.com/etsy/statsd) server. +A node.js client for [Etsy](http://etsy.com)'s [StatsD](https://github.com/etsy/statsd) server and +Datadog's [DogStatsD](http://docs.datadoghq.com/guides/dogstatsd/) server. This client will let you fire stats at your StatsD server from a node.js application. @@ -28,7 +29,7 @@ Parameters (specified as an options hash): * `mock`: Create a mock StatsD instance, sending no stats to the server? `default: false` * `global_tags`: Optional tags that will be added to every metric `default: []` -All StatsD methods have the same API: +All StatsD methods other than event have the same API: * `name`: Stat name `required` * `value`: Stat value `required except in increment/decrement where it defaults to 1/-1 respectively` * `sampleRate`: Sends only a sample of data to StatsD `default: 1` @@ -37,6 +38,20 @@ All StatsD methods have the same API: If an array is specified as the `name` parameter each item in that array will be sent along with the specified value. +The event method has the following API: + +* `title`: Event title `required` +* `text`: Event description `default is title` +* `options`: Options for the event + * `date_happened` Assign a timestamp to the event `default is now` + * `hostname` Assign a hostname to the event. + * `aggregation_key` Assign an aggregation key to the event, to group it with some others. + * `priority` Can be ‘normal’ or ‘low’ `default: normal` + * `source_type_name` Assign a source type to the event. + * `alert_type` Can be ‘error’, ‘warning’, ‘info’ or ‘success’ `default: info` +* `tags`: The Array of tags to add to metrics `default: []` +* `callback`: The callback to execute once the metric has been sent + ```javascript var StatsD = require('node-statsd'), client = new StatsD(); @@ -60,6 +75,9 @@ If an array is specified as the `name` parameter each item in that array will be client.set('my_unique', 'foobar'); client.unique('my_unique', 'foobarbaz'); + // Event: sends the titled event + client.event('my_title', 'description'); + // Incrementing multiple items client.increment(['these', 'are', 'different', 'stats']); @@ -89,6 +107,14 @@ If an array is specified as the `name` parameter each item in that array will be client.histogram('my_histogram', 42, 0.25, ['tag'], next); ``` +## DogStatsD-specific usage + +Some of the functionality mentioned above is specific to DogStatsD and will not do anything if are using the regular statsd client. This includes: +* global_tags parameter +* tags parameter +* histogram API +* event API + ## Errors In the event that there is a socket error, `node-statsd` will allow this error to bubble up. If you would like to catch the errors, just attach a listener to the socket property on the instance. diff --git a/lib/statsd.js b/lib/statsd.js index f27383b..d846a88 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -126,10 +126,63 @@ Client.prototype.set = function (stat, value, sampleRate, tags, callback) { this.sendAll(stat, value, 's', sampleRate, tags, callback); }; +/** + * Send on an event + * @param title {String} The title of the event + * @param text {String} The description of the event. Optional- title is used if not given. + * @param options + * @option date_happened {Date} Assign a timestamp to the event. Default is now. + * @option hostname {String} Assign a hostname to the event. + * @option aggregation_key {String} Assign an aggregation key to the event, to group it with some others. + * @option priority {String} Can be ‘normal’ or ‘low’. Default is 'normal'. + * @option source_type_name {String} Assign a source type to the event. + * @option alert_type {String} Can be ‘error’, ‘warning’, ‘info’ or ‘success’. Default is 'info'. + * @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.event = function(title, text, options, tags, callback) { + var message, + msgTitle = title ? title : '', + msgText = text ? text : title; + + // start out the message with the event-specific title and text info + message = '_e{' + msgTitle.length + ',' + msgText.length + '}:' + msgTitle + '|' + msgText; + + // add in the event-specific options + if (options) { + if (options.date_happened && options.date_happened instanceof Date) { + message += '|d:' + options.date_happened.getTime(); + } + if (options.hostname) { + message += '|h:' + options.hostname; + } + if (options.aggregation_key) { + message += '|k:' + options.aggregation_key; + } + if (options.priority) { + message += '|p:' + options.priority; + } + if (options.source_type_name) { + message += '|s:' + options.source_type_name; + } + if (options.alert_type) { + message += '|t:' + options.alert_type; + } + } + + // allow for tags to be omitted and callback to be used in its place + if(typeof tags === 'function' && callback === undefined) { + callback = tags; + } + + this.send(message, tags, callback); +}; + /** * Checks if stats is an array and sends all stats calling back once all have sent * @param stat {String|Array} The stat(s) to send * @param value The value to send + * @param type The type of the metric * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. @@ -175,10 +228,10 @@ Client.prototype.sendAll = function(stat, value, type, sampleRate, tags, callbac if(Array.isArray(stat)){ stat.forEach(function(item){ - self.send(item, value, type, sampleRate, tags, onSend); + self.sendStat(item, value, type, sampleRate, tags, onSend); }); } else { - this.send(stat, value, type, sampleRate, tags, callback); + this.sendStat(stat, value, type, sampleRate, tags, callback); } }; @@ -191,10 +244,8 @@ Client.prototype.sendAll = function(stat, value, type, sampleRate, tags, callbac * @param tags {Array} The Array of tags to add to metrics * @param callback {Function=} Callback when message is done being delivered. Optional. */ -Client.prototype.send = function (stat, value, type, sampleRate, tags, callback) { - var message = this.prefix + stat + this.suffix + ':' + value + '|' + type, - buf, - merged_tags = []; +Client.prototype.sendStat = function (stat, value, type, sampleRate, tags, callback) { + var message = this.prefix + stat + this.suffix + ':' + value + '|' + type; if(sampleRate && sampleRate < 1){ if(Math.random() < sampleRate){ @@ -204,6 +255,18 @@ Client.prototype.send = function (stat, value, type, sampleRate, tags, callback) return; } } + this.send(message, tags, callback); +} + +/** + * Send a stat or event across the wire + * @param message {String} The constructed message without tags + * @param tags {Array} The tags to include (along with global tags). Optional. + * @param callback {Function=} Callback when message is done being delivered. Optional. + */ +Client.prototype.send = function (message, tags, callback) { + var buf, + merged_tags = []; if(tags && Array.isArray(tags)){ merged_tags = merged_tags.concat(tags); diff --git a/package.json b/package.json index 5a672b3..22ad262 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,5 @@ , "devDependencies": { "mocha":"*" } -, "licenses" : - [ { "type" : "MIT" - , "url" : "http://github.com/sivy/node-stats/raw/master/LICENSE" - } - ] +, "license" : "MIT" } diff --git a/test/test_statsd.js b/test/test_statsd.js index 0fbb314..8cbf1cd 100644 --- a/test/test_statsd.js +++ b/test/test_statsd.js @@ -48,7 +48,6 @@ function assertMockClientMethod(method, finished){ callbackThrows = true; } assert.ok(!callbackThrows); - statsd[method]('test', 1, null, function(error, bytes){ assert.ok(!error); assert.equal(bytes, 0); @@ -680,4 +679,91 @@ describe('StatsD', function(){ }); }); + describe('#event', function(finished) { + 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,12}:test title|another desc|d:' + date.getTime() + + '|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 desc', 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); + }); + }); }); From bdbca6b0808f68ccb408c212b381e0c069942f81 Mon Sep 17 00:00:00 2001 From: Brian Deitte Date: Wed, 26 Aug 2015 14:57:59 -0400 Subject: [PATCH 2/2] Fix up some formatting and naming --- README.md | 4 ++-- test/test_statsd.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2da76ba..673d6b1 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,8 @@ The event method has the following API: Some of the functionality mentioned above is specific to DogStatsD and will not do anything if are using the regular statsd client. This includes: * global_tags parameter * tags parameter -* histogram API -* event API +* histogram method +* event method ## Errors diff --git a/test/test_statsd.js b/test/test_statsd.js index 8cbf1cd..bc454c7 100644 --- a/test/test_statsd.js +++ b/test/test_statsd.js @@ -48,6 +48,7 @@ function assertMockClientMethod(method, finished){ callbackThrows = true; } assert.ok(!callbackThrows); + statsd[method]('test', 1, null, function(error, bytes){ assert.ok(!error); assert.equal(bytes, 0); @@ -717,13 +718,13 @@ describe('StatsD', function(){ 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' - }; + date_happened: date, + hostname: 'host', + aggregation_key: 'ag_key', + priority: 'low', + source_type_name: 'source_type', + alert_type: 'warning' + }; statsd.event('test title', 'another desc', options); });