├── .gitignore ├── LICENSE.txt ├── README.md ├── benchmarks └── child-client.js ├── docs.jsig ├── examples └── trivial-test.js ├── lib ├── backoff.js ├── dns-resolver.js ├── ephemeral-socket.js ├── messages.js ├── multi-statsd-client.js ├── null-statsd-client.js ├── packet-queue.js └── statsd-client.js ├── multi.js ├── null.js ├── package.json ├── statsd.js └── test ├── index.js ├── lib └── udp-server.js ├── multi.js ├── null.js ├── packet-queue.js ├── resolve-dns.js ├── socket.js └── statsd-client.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | coverage 14 | 15 | node_modules 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2012, Morten Siebuhr 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uber-statsd-client 2 | 3 | Node.js client for [statsd](https://github.com/etsy/statsd). 4 | 5 | ## Example 6 | 7 | ```js 8 | var createStatsd = require('uber-statsd-client'); 9 | 10 | var sdc = createStatsd({ 11 | host: 'statsd.example.com' 12 | }); 13 | 14 | var timer = new Date(); 15 | sdc.increment('some.counter'); // Increment by one. 16 | sdc.gauge('some.gauge', 10); // Set gauge to 10 17 | sdc.timing('some.timer', timer); // Calculates time diff 18 | 19 | sdc.close(); // Optional - stop NOW 20 | ``` 21 | 22 | ## Caveats 23 | 24 | Apparently a `udp` based statsd client does not work with `cluster` in node0.10 or node0.12. 25 | I'd recommend finding a `tcp` based statsd client or not using `cluster`. 26 | 27 | ## Docs 28 | 29 | ```js 30 | var createStatsd = require('uber-statsd-client') 31 | var sdc = createStatsd({ 32 | host: 'statsd.example.com', 33 | port: 8124 34 | }); 35 | ``` 36 | 37 | Available options: 38 | 39 | ### `options.prefix` 40 | 41 | type: `String`, default: `""` 42 | 43 | Prefix all stats written by the client with a particular string 44 | prefix value. This defaults to `""` 45 | 46 | ### `options.host` 47 | 48 | type: `String`, default: `""` 49 | 50 | The hostname where stats should be send. Defaults to 51 | `"localhost"` 52 | 53 | ### `options.port` 54 | 55 | type: `Number`, default: `8125` 56 | 57 | The port where stats should be send. Defaults to `8125` 58 | 59 | ### `options.socket_timeout` 60 | 61 | type: `Number`, default: `1000` 62 | 63 | The UDP socket will auto-close if it has not been written to 64 | within the socket_timeout period. Defaults to `1000` 65 | milliseconds. 66 | 67 | If `socket_timeout` is set to `0` then the UDP socket will never 68 | auto close. 69 | 70 | ### `options.highWaterMark` 71 | 72 | type: `Number`, default: `100` 73 | 74 | The UDP socket implementation has an internal `highWaterMark`. 75 | There is a known issue in node core where it will buffer 76 | packets being written to the UDP socket if it's currently 77 | in the "resolving DNS host" state. 78 | 79 | When the `highWaterMark` is reached all packets are dropped 80 | and ignored. 81 | 82 | ### `options.packetQueue` 83 | 84 | ```ocaml 85 | { 86 | block?: Number, 87 | flush?: Number, 88 | trailingNewLine?: Boolean 89 | } 90 | ``` 91 | 92 | The UDP socket uses an internal packet queue that can be 93 | configured. 94 | 95 | The semantics of the packet queue is that it will write to 96 | the UDP socket if it has buffered at least `block` amount 97 | of bytes OR it has elapsed at least `flush` amount of 98 | milliseconds. 99 | 100 | By default the packet queue will join multiple messages with 101 | a new line. It will also append a trailing new line, 102 | however you can opt out of the trailing new line by setting 103 | `trailingNewLine` field to `false`. 104 | 105 | The `block` field defaults to `1440` bytes. 106 | The `flush` field defaults to `1000` milliseconds. 107 | The `trailingNewLine` field defaults to `true`. 108 | 109 | ### `options.dnsResolver` 110 | 111 | ```ocaml 112 | { 113 | timeToLive?: Number, 114 | seedIP?: String, 115 | backoffSettings?: { 116 | maxDelay?: Number, 117 | minDelay?: Number, 118 | retries?: Number, 119 | factor?: Number 120 | } 121 | } 122 | ``` 123 | 124 | The UDP socket will optionally use a DNS resolver to resolve 125 | DNS once instead of resolving it for every UDP packet 126 | being written. 127 | 128 | It's strongly recommended you use the DNS resolver. 129 | 130 | The DNS resolver will resolve its DNS lookup based on the 131 | configured `timeToLive`. i.e. it caches the DNS host for 132 | at least that amount of time. 133 | 134 | The DNS resolver takes a `seedIP` value, this is used when 135 | it cannot resolve DNS due to DNS failures and will fallback 136 | to the static IP you gave it. 137 | 138 | The DNS resolver retries DNS lookups on DNS failures based on 139 | the `backoffSettings` you supply. 140 | 141 | The `timeToLive` field defaults to five minutes (in milliseconds) 142 | The `seedIP` field has no default. 143 | The `backoffSettings.maxDelay` field defaults to `Infinity` 144 | The `backoffSettings.minDelay` field defaults to `500` milliseconds 145 | The `backoffSettings.retries` field defaults to `10` retries 146 | The `backoffSettings.factor` field defaults to `2`. 147 | 148 | ### `options.isDisabled` 149 | 150 | `isDisabled` is an optional predicate function. You can pass in 151 | a predicate function that allows you to disabled the statsd 152 | client at run time. 153 | 154 | When this predicate function returns `true` the `EphemeralSocket` 155 | will stop writing to the statsd UDP server. 156 | 157 | ### Counting stuff 158 | 159 | Counters are supported, both as raw `.counter(metric, delta)` and with the 160 | shortcuts `.increment(metric, [delta=1])` and `.decrement(metric, [delta=-1])`: 161 | 162 | sdc.increment('systemname.subsystem.value'); // Increment by one 163 | sdc.decrement('systemname.subsystem.value', -10); // Decrement by 10 164 | sdc.counter('systemname.subsystem.value, 100); // Indrement by 100 165 | 166 | ### Gauges 167 | 168 | Sends an arbitrary number to the back-end: 169 | 170 | sdc.gauge('what.you.gauge', 100); 171 | 172 | ### Delays 173 | 174 | Keep track of how fast (or slow) your stuff is: 175 | 176 | var start = new Date(); 177 | setTimeout(function () { 178 | sdc.timing('random.timeout', start); 179 | }, 100 * Math.random()); 180 | 181 | If it is given a `Date`, it will calculate the difference, and anything else 182 | will be passed straight through. 183 | 184 | And don't let the name (or nifty interface) fool you - it can measure any kind 185 | of number, where you want to see the distribution (content lengths, list items, 186 | query sizes, ...) 187 | 188 | ### Stopping gracefully 189 | 190 | By default, the socket is closed if it hasn't been used for a second (see 191 | `socket_timeout` in the init-options), but it can also be force-closed with 192 | `.close()`: 193 | 194 | var start = new Date(); 195 | setTimeout(function () { 196 | sdc.timing('random.timeout', start); // 2 - implicitly re-creates socket. 197 | sdc.close(); // 3 - Closes socket after last use. 198 | }, 100 * Math.random()); 199 | sdc.close(); // 1 - Closes socket early. 200 | 201 | The call is idempotent, so you can call it "just to be sure". And if you submit 202 | new metrics later, the socket will automatically be re-created, and a new 203 | timeout-timer started. 204 | 205 | ### Prefix-magic 206 | 207 | The library supports getting "child" clients with extra prefixes, to help with 208 | making sane name-spacing in apps: 209 | 210 | // Create generic client 211 | var sdc = createSDC({host: 'statsd.example.com', prefix: 'systemname'); 212 | sdc.increment('foo'); // Increments 'systemname.foo' 213 | ... do great stuff ... 214 | 215 | // Subsystem A 216 | var sdcA = sdc.getChildClient('a'); 217 | sdcA.increment('foo'); // Increments 'systemname.a.foo' 218 | 219 | // Subsystem B 220 | var sdcB = sdc.getChildClient('b'); 221 | sdcB.increment('foo'); // Increments 'systemname.b.foo' 222 | 223 | Internally, they all use the same socket, so calling `.close()` on any of them 224 | will allow the entire program to stop gracefully. 225 | 226 | What's broken 227 | ------------- 228 | 229 | Check the [GitHub issues](https://github.com/uber/node-statsd-client/issues). 230 | 231 | LICENSE 232 | ------- 233 | 234 | ISC - see 235 | [LICENSE.txt](https://github.com/uber/node-statsd-client/blob/master/LICENSE.txt). 236 | -------------------------------------------------------------------------------- /benchmarks/child-client.js: -------------------------------------------------------------------------------- 1 | var SDC = require('../statsd.js'); 2 | 3 | var client = new SDC({ 4 | prefix: 'bar' 5 | }); 6 | 7 | 8 | var ITERATIONS = 10 * 1000 * 1000; 9 | 10 | var start = Date.now(); 11 | var children = new Array(10); 12 | 13 | for (var i = 0; i < ITERATIONS; i++) { 14 | var index = ITERATIONS % 10; 15 | 16 | children[index] = client.getChildClient('foo'); 17 | } 18 | 19 | var end = Date.now(); 20 | console.log('time: ', end - start); 21 | 22 | client.close(); 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs.jsig: -------------------------------------------------------------------------------- 1 | type StatsDClient : { 2 | gauge: (name: String, value: Number) => void, 3 | counter: (name: String, value: Number) => void, 4 | increment: (name: String, delta?: Number) => void, 5 | decrement: (name: String, delta?: Number) => void, 6 | timing: (name: String, time: Number) => void, 7 | close: () => void 8 | } 9 | 10 | -- Given a set of options returns a statsd client. 11 | uber-statsd-client : ({ 12 | prefix?: String, 13 | 14 | host?: String, 15 | port?: Number, 16 | socket_timeout?: Number, 17 | highWaterMark?: Number, 18 | isDisabled?: () => Boolean, 19 | 20 | packetQueue?: { 21 | block?: Number, 22 | flush?: Number, 23 | trailingNewLine?: Boolean 24 | }, 25 | dnsResolver?: { 26 | timeToLive?: Number, 27 | seedIP?: String, 28 | backoffSettings?: { 29 | maxDelay?: Number, 30 | minDelay?: Number, 31 | retries?: Number, 32 | factor?: Number 33 | } 34 | } 35 | }) => StatsDClient 36 | 37 | uber-statsd-client/null : (capacity?: Number) => StatsDClient 38 | -------------------------------------------------------------------------------- /examples/trivial-test.js: -------------------------------------------------------------------------------- 1 | var sdc = require('../lib/statsd-client'), 2 | SDC = new sdc({host: '10.111.12.113', debug: 1, prefix: "statsd-client"}), 3 | SDCTest = SDC.getChildClient('test'); 4 | 5 | 6 | var begin = new Date(); 7 | setTimeout(function () { 8 | // Set 'statsd-client.test.gauge' 9 | SDCTest.gauge('gauge', 100 * Math.random()); 10 | 11 | // Icrement 'statsd-client.test.counter' twice 12 | SDCTest.increment('counter'); 13 | SDC.increment('test.counter'); 14 | 15 | // Set some time 16 | SDC.timing('speed', begin); 17 | 18 | // Close socket 19 | SDC.close(); 20 | }, 100 * Math.random()); 21 | 22 | -------------------------------------------------------------------------------- /lib/backoff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Back = require('back'); 4 | var extend = require('xtend/immutable'); 5 | var clearTimeout = require('timers').clearTimeout; 6 | 7 | module.exports = Backoff; 8 | 9 | function Backoff(backoffSettings) { 10 | if (!(this instanceof Backoff)) { 11 | return new Backoff(backoffSettings); 12 | } 13 | 14 | this.settings = extend(backoffSettings); 15 | this.back = null; 16 | } 17 | 18 | Backoff.prototype.backoff = function backoff(cb) { 19 | this.back = Back(cb, this.settings); 20 | }; 21 | 22 | Backoff.prototype.close = function close() { 23 | if (this.back && this.back.timer) { 24 | return clearTimeout(this.back.timer); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/dns-resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var globalDns = require('dns'); 4 | var setInterval = require('timers').setInterval; 5 | var clearInterval = require('timers').clearInterval; 6 | 7 | var Backoff = require('./backoff.js'); 8 | 9 | var SECOND = 1000; 10 | var MINUTE = 60 * SECOND; 11 | var DEFAULT_REFRESH_RATE = 5 * MINUTE; 12 | 13 | /* 14 | type Options : { 15 | -- backoffSettings passed to `back` module. 16 | backoffSettings?: { 17 | maxDelay?: Number, 18 | minDelay?: Number, 19 | retries?: Number, 20 | factor?: Number 21 | }, 22 | seedIP: ip: String, 23 | timeToLive?: Number, 24 | 25 | onresolved: () => void | null 26 | } 27 | 28 | DNSResolver : (hostName: String, opts: Options) => { 29 | resolveHost: (key: String) => ip: String, 30 | lookupHost: (hostName: String) => void, 31 | close: () => void 32 | } 33 | 34 | */ 35 | module.exports = DNSResolver; 36 | 37 | function DNSResolver(hostname, opts) { 38 | var backoffSettings = opts.backoffSettings || {}; 39 | 40 | this._hostname = hostname; 41 | //TODO logger.warn() about no seedIP. 42 | this._ip = opts.seedIP || null; 43 | 44 | this._dns = opts.dns || globalDns; 45 | this._backoffSettings = backoffSettings; 46 | this._destroyed = false; 47 | this._dnsBackoff = null; 48 | this._timeToLive = opts.timeToLive || DEFAULT_REFRESH_RATE; 49 | this._refreshTimer = null; 50 | 51 | this.onresolved = opts.onresolved || null; 52 | this.onresolvegiveup = null; 53 | } 54 | 55 | var proto = DNSResolver.prototype; 56 | proto.resolveHost = function resolveHost(message) { 57 | // If we dont have any ipAddresses yet just return 58 | // the hostname. 59 | //TODO logger.warn() about falling back to hostname 60 | if (this._ip === null) { 61 | return this._hostname; 62 | } 63 | 64 | return this._ip; 65 | }; 66 | 67 | proto.lookupHost = function lookupHost() { 68 | var self = this; 69 | 70 | if (!this._refreshTimer) { 71 | this._refreshTimer = setInterval(retry, 72 | this._timeToLive); 73 | this._refreshTimer.unref(); 74 | } 75 | 76 | self._dns.lookup(self._hostname, onIP); 77 | 78 | function onIP(err, ip) { 79 | //TODO logger.warn() about destroyed mid-flight. 80 | if (self._destroyed) { 81 | return; 82 | } 83 | 84 | // on error try again with a backoff. 85 | if (err) { 86 | // lazy instantiate the backoff 87 | //TODO instrument _dnsBackoff so we know how 88 | //TODO we actually backoff. 89 | self._dnsBackoff = self._dnsBackoff || 90 | Backoff(self._backoffSettings); 91 | //TODO logger.warn() that we failed DNS 92 | return self._dnsBackoff.backoff(retry); 93 | } 94 | 95 | self._dnsBackoff = null; 96 | self._ip = ip; 97 | 98 | // braindead simple events. 99 | // notifies user that we resolved the host. 100 | if (self.onresolved) { 101 | self.onresolved(); 102 | } 103 | } 104 | 105 | function retry(backoffFailure) { 106 | // if backoff eventaully fails we just do not resolve 107 | // dns at all. 108 | if (backoffFailure) { 109 | //TODO logger.warn() that fetching DNS failed. 110 | self._dnsBackoff = null; 111 | 112 | // braindead simple events. 113 | // notifiers user that we gave up resolution. 114 | if (self.onresolvegiveup) { 115 | self.onresolvegiveup(); 116 | } 117 | 118 | return; 119 | } 120 | 121 | self.lookupHost(); 122 | } 123 | }; 124 | 125 | proto.close = function close() { 126 | this._destroyed = true; 127 | if (this._dnsBackoff) { 128 | this._dnsBackoff.close(); 129 | } 130 | 131 | if (this._refreshTimer) { 132 | clearInterval(this._refreshTimer); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /lib/ephemeral-socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var setTimeout = require('timers').setTimeout; 4 | var clearTimeout = require('timers').clearTimeout; 5 | var Buffer = require('buffer').Buffer; 6 | var console = require('console'); 7 | var globalDgram = require('dgram'); 8 | 9 | var DNSResolver = require('./dns-resolver.js'); 10 | 11 | var PacketQueue = require('./packet-queue.js'); 12 | 13 | function ephemeralSocket(options) { 14 | var self = this; 15 | var opts = self.options = options || {}; 16 | var queueOptions = opts.packetQueue; 17 | 18 | opts.host = opts.host || 'localhost'; 19 | opts.port = opts.port || 8125; 20 | //TODO remove debug flag. 21 | opts.debug = opts.debug || false; 22 | 23 | opts.socket_timeout = 'socket_timeout' in opts ? 24 | opts.socket_timeout : 1000; 25 | opts.highWaterMark = opts.highWaterMark || 100; 26 | opts.isDisabled = opts.isDisabled || null; 27 | 28 | // Set up re-usable socket 29 | self._socket = null; // Store the socket here 30 | self._queue = PacketQueue(onFlush, queueOptions); 31 | self._socket_used = false; // Flag if it has been used 32 | self._socket_timer = undefined; // Reference to check-timer 33 | self._dnsResolver = null; 34 | self._dgram = opts.dgram || globalDgram; 35 | 36 | // flush the packet queue 37 | function onFlush(buf) { 38 | self._writeToSocket(buf); 39 | } 40 | } 41 | 42 | /* 43 | * Allocate a dnsResolver to resolve the hosts to an IP. 44 | * 45 | */ 46 | ephemeralSocket.prototype.resolveDNS = function (opts) { 47 | this._dnsResolver = new DNSResolver(this.options.host, opts); 48 | 49 | this._dnsResolver.lookupHost(); 50 | }; 51 | 52 | /* 53 | * Check if the socket has been used in the previous socket_timeout-interval. 54 | * If it has, we leave it open and try again later. If it hasn't, close it. 55 | */ 56 | ephemeralSocket.prototype._socket_timeout = function () { 57 | // Is it already closed? -- then stop here 58 | //TODO logger.warn() about not closing socket on timeout 59 | if (!this._socket) { 60 | return; 61 | } 62 | 63 | // Has been used? -- reset use-flag and wait some more 64 | if (this._socket_used) { 65 | this._socket_used = false; 66 | this._socket_timer = setTimeout(this._socket_timeout.bind(this), this.options.socket_timeout); 67 | return; 68 | } 69 | 70 | // Not used? -- close the socket 71 | //TODO logger.warn() about closing socket on timeout 72 | this._close_socket(); 73 | }; 74 | 75 | /* 76 | * Close the socket, stop time-based queue flushing and close DNS resolution 77 | */ 78 | ephemeralSocket.prototype.close = function () { 79 | this._queue.destroy(); 80 | //TODO logger.warn() about double close() 81 | 82 | this._close_socket(); 83 | 84 | if (this._dnsResolver) { 85 | this._dnsResolver.close(); 86 | } 87 | }; 88 | 89 | /* 90 | * Tear-down the socket since it isn't being used 91 | */ 92 | ephemeralSocket.prototype._close_socket = function () { 93 | if (!this._socket) { 94 | return; 95 | } 96 | 97 | // Cancel the running timer 98 | clearTimeout(this._socket_timer); 99 | 100 | // Wait a tick or two, so any remaining stats can be sent. 101 | var that = this; 102 | setTimeout(function () { 103 | if (that._socket) { 104 | that._socket.close(); 105 | } 106 | that._socket = undefined; 107 | }, 10); 108 | }; 109 | 110 | ephemeralSocket.prototype.send = function (data) { 111 | this._queue.write(data); 112 | } 113 | 114 | /* 115 | * Send data. 116 | */ 117 | ephemeralSocket.prototype._writeToSocket = function (data, cb) { 118 | if (typeof this.options.isDisabled === 'function' && 119 | this.options.isDisabled() 120 | ) { 121 | return; 122 | } 123 | 124 | // Create socket if it isn't there 125 | allocSocket(this); 126 | this._socket_used = true; 127 | 128 | // Create buffer 129 | var buf = new Buffer(data); 130 | 131 | //TODO remove debug feature 132 | if (this.options.debug) { 133 | console.warn(data); 134 | } 135 | 136 | if (!this._socket._sendQueue || 137 | this._socket._sendQueue.length < this.options.highWaterMark 138 | ) { 139 | //TODO logger.warn() if no _dnsResolver 140 | var host = this._dnsResolver ? 141 | this._dnsResolver.resolveHost() : 142 | this.options.host; 143 | 144 | if (cb) { 145 | this._socket.send(buf, 0, buf.length, 146 | this.options.port, host, cb); 147 | } else { 148 | this._socket.send(buf, 0, buf.length, 149 | this.options.port, host); 150 | } 151 | 152 | } else { 153 | if (cb) { 154 | cb(new Error('could not send(). The dgram socket is full. Ignoring buffer')); 155 | } 156 | } 157 | //TODO logger.warn() in else statement 158 | }; 159 | 160 | module.exports = ephemeralSocket; 161 | 162 | function allocSocket(self) { 163 | //TODO mark EphemeralSocket as healthy or unhealthy based 164 | //TODO on how often we allocate sockets. 165 | //TODO If EphemeralSocket is unhealthy we should stop 166 | //TODO calling _writeToSocket() and wait until 167 | //TODO it is healthy. 168 | //TODO i.e. wrap in airlock. 169 | if (self._socket) { 170 | return; 171 | } 172 | 173 | //TODO instrument _socket creation so we know how often 174 | //TODO we allocate sockets 175 | self._socket = self._dgram.createSocket('udp4'); 176 | self._socket.unref(); 177 | 178 | // Start timer, if we have a positive timeout 179 | if (self.options.socket_timeout > 0) { 180 | self._socket_timer = setTimeout( 181 | self._socket_timeout.bind(self), 182 | self.options.socket_timeout 183 | ); 184 | } 185 | 186 | self._socket.once('error', onError); 187 | 188 | // When we error we should just close and de-allocate. 189 | function onError(err) { 190 | //TODO logger.error() about socket error 191 | 192 | if (!self._socket) { 193 | return; 194 | } 195 | 196 | // First attach an error listener so consequent error 197 | // events do not cause uncaught exceptions on this socket 198 | self._socket.on('error', function onDeadSocketError() { 199 | //TODO logger.error() about dead socket error 200 | }); 201 | 202 | self._socket.close(); 203 | self._socket = null; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | Gauge: Gauge, 5 | Counter: Counter, 6 | Timing: Timing 7 | }; 8 | 9 | function messageToString() { 10 | return this.name + ':' + this.value + '|' + this.type; 11 | } 12 | 13 | function Gauge(name, value) { 14 | this.type = 'g'; 15 | this.name = name; 16 | this.value = value; 17 | } 18 | 19 | Gauge.prototype.toString = messageToString; 20 | 21 | function Counter(name, value) { 22 | this.type = 'c'; 23 | this.name = name; 24 | this.value = value; 25 | } 26 | 27 | Counter.prototype.toString = messageToString; 28 | 29 | function Timing(name, value) { 30 | this.type = 'ms'; 31 | this.name = name; 32 | this.value = value; 33 | } 34 | 35 | Timing.prototype.toString = messageToString; 36 | -------------------------------------------------------------------------------- /lib/multi-statsd-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | module.exports = MultiStatsdClient; 6 | 7 | function MultiStatsdClient(clients) { 8 | assert( 9 | Array.isArray(clients), 10 | 'MultiStatsdClient requires an Array of statsd clients' 11 | ); 12 | 13 | this.clients = clients.slice(); 14 | } 15 | 16 | MultiStatsdClient.prototype.getChildClient = getChildClientMultiStatsd; 17 | MultiStatsdClient.prototype.gauge = gaugeMultiStatsd; 18 | MultiStatsdClient.prototype.counter = counterMultiStatsd; 19 | MultiStatsdClient.prototype.increment = incrementMultiStatsd; 20 | MultiStatsdClient.prototype.decrement = decrementMultiStatsd; 21 | MultiStatsdClient.prototype.timing = timingMultiStatsd; 22 | MultiStatsdClient.prototype.immediateGauge = immediateGaugeMultiStatsd; 23 | MultiStatsdClient.prototype.immediateCounter = immediateCounterMultiStatsd; 24 | MultiStatsdClient.prototype.immediateIncrement = immediateIncrementMultiStatsd; 25 | MultiStatsdClient.prototype.immediateDecrement = immediateDecrementMultiStatsd; 26 | MultiStatsdClient.prototype.immediateTiming = immediateTimingMultiStatsd; 27 | MultiStatsdClient.prototype.close = closeMultiStatsd; 28 | 29 | function getChildClientMultiStatsd(extraPrefix) { 30 | var clients = this.clients; 31 | var newClients = new Array(clients.length); 32 | 33 | for (var i = 0; i < clients.length; i++) { 34 | newClients[i] = clients[i].getChildClient(extraPrefix); 35 | } 36 | 37 | return new MultiStatsdClient(newClients); 38 | } 39 | 40 | function gaugeMultiStatsd(name, value) { 41 | var clients = this.clients; 42 | 43 | for (var i = 0; i < clients.length; i++) { 44 | clients[i].gauge(name, value); 45 | } 46 | } 47 | 48 | function counterMultiStatsd(name, delta) { 49 | var clients = this.clients; 50 | 51 | for (var i = 0; i < clients.length; i++) { 52 | clients[i].counter(name, delta); 53 | } 54 | } 55 | 56 | function incrementMultiStatsd(name, delta) { 57 | var clients = this.clients; 58 | 59 | for (var i = 0; i < clients.length; i++) { 60 | clients[i].increment(name, delta); 61 | } 62 | } 63 | 64 | function decrementMultiStatsd(name, delta) { 65 | var clients = this.clients; 66 | 67 | for (var i = 0; i < clients.length; i++) { 68 | clients[i].decrement(name, delta); 69 | } 70 | } 71 | 72 | function timingMultiStatsd(name, time) { 73 | var clients = this.clients; 74 | 75 | for (var i = 0; i < clients.length; i++) { 76 | clients[i].timing(name, time); 77 | } 78 | } 79 | 80 | function immediateGaugeMultiStatsd(name, value, cb) { 81 | var clients = this.clients; 82 | var callback = multiCallback(clients.length, cb); 83 | 84 | for (var i = 0; i < clients.length; i++) { 85 | clients[i].immediateGauge(name, value, callback); 86 | } 87 | } 88 | 89 | function immediateCounterMultiStatsd(name, delta, cb) { 90 | var clients = this.clients; 91 | var callback = multiCallback(clients.length, cb); 92 | 93 | for (var i = 0; i < clients.length; i++) { 94 | clients[i].immediateCounter(name, delta, callback); 95 | } 96 | } 97 | 98 | function immediateIncrementMultiStatsd(name, delta, cb) { 99 | var clients = this.clients; 100 | var callback = multiCallback(clients.length, cb); 101 | 102 | for (var i = 0; i < clients.length; i++) { 103 | clients[i].immediateIncrement(name, delta, callback); 104 | } 105 | } 106 | 107 | function immediateDecrementMultiStatsd(name, delta, cb) { 108 | var clients = this.clients; 109 | var callback = multiCallback(clients.length, cb); 110 | 111 | for (var i = 0; i < clients.length; i++) { 112 | clients[i].immediateDecrement(name, delta, callback); 113 | } 114 | } 115 | 116 | function immediateTimingMultiStatsd(name, time, cb) { 117 | var clients = this.clients; 118 | var callback = multiCallback(clients.length, cb); 119 | 120 | for (var i = 0; i < clients.length; i++) { 121 | clients[i].immediateTiming(name, time, callback); 122 | } 123 | } 124 | 125 | function closeMultiStatsd() { 126 | var clients = this.clients; 127 | 128 | for (var i = 0; i < clients.length; i++) { 129 | clients[i].close(); 130 | } 131 | } 132 | 133 | function multiCallback(count, cb) { 134 | var returnCount = 0; 135 | var returned = false; 136 | 137 | return function callbackAfterCount(err) { 138 | if (!returned) { 139 | if (err) { 140 | returned = true; 141 | return cb(err); 142 | } 143 | 144 | returnCount++; 145 | 146 | if (returnCount === count) { 147 | returned = true; 148 | return cb(null); 149 | } 150 | } 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /lib/null-statsd-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RingBuffer = require('ringbufferjs'); 4 | 5 | var DEFAULT_OPTS = { 6 | capacity: 50, 7 | prefix: '' 8 | }; 9 | 10 | module.exports = NullStatsd; 11 | 12 | function NullStatsd(opts, _buffer) { 13 | if (typeof opts === 'number') { 14 | opts = { 15 | capacity: opts 16 | }; 17 | } 18 | 19 | if (!opts) { 20 | opts = DEFAULT_OPTS; 21 | } 22 | 23 | this._buffer = _buffer || new RingBuffer( 24 | opts.capacity || DEFAULT_OPTS.capacity 25 | ); 26 | 27 | var prefix = opts.prefix; 28 | 29 | this.prefix = prefix ? 30 | (prefix[prefix.length - 1] === '.' ? prefix : prefix + '.') : 31 | ''; 32 | } 33 | 34 | function NullStatsdRecord(type, name, value, delta, time) { 35 | this.type = type; 36 | this.name = name; 37 | this.value = typeof value === 'number' ? value : null; 38 | this.delta = typeof delta === 'number' ? delta : null; 39 | this.time = typeof time === 'number' ? time : null; 40 | } 41 | 42 | var proto = NullStatsd.prototype; 43 | 44 | proto._write = function _write(record) { 45 | this._buffer.enq(record); 46 | }; 47 | 48 | proto.gauge = function gauge(name, value) { 49 | this._write(new NullStatsdRecord('g', this.prefix + name, value)); 50 | }; 51 | 52 | proto.counter = function counter(name, value) { 53 | this._write(new NullStatsdRecord('c', this.prefix + name, null, value)); 54 | }; 55 | 56 | proto.increment = function increment(name, delta) { 57 | this._write(new NullStatsdRecord( 58 | 'c', 59 | this.prefix + name, 60 | null, 61 | delta || 1 62 | )); 63 | }; 64 | 65 | proto.decrement = function decrement(name, delta) { 66 | this._write(new NullStatsdRecord( 67 | 'c', 68 | this.prefix + name, 69 | null, 70 | (-1 * Math.abs(delta || 1)) 71 | )); 72 | }; 73 | 74 | proto.timing = function timing(name, time) { 75 | this._write(new NullStatsdRecord( 76 | 'ms', 77 | this.prefix + name, 78 | null, 79 | null, 80 | time 81 | )); 82 | }; 83 | 84 | proto.close = function close() { 85 | for (var i = 0, len = this._buffer.size(); i < len; i++) { 86 | this._buffer.deq(); 87 | } 88 | }; 89 | 90 | proto.immediateGauge = function (name, value, cb) { 91 | this._write(new NullStatsdRecord( 92 | 'g', 93 | this.prefix + name, 94 | value 95 | )); 96 | if (cb) { 97 | process.nextTick(cb); 98 | } 99 | }; 100 | 101 | proto.immediateIncrement = function (name, delta, cb) { 102 | this._write(new NullStatsdRecord( 103 | 'c', 104 | this.prefix + name, 105 | null, 106 | delta || 1 107 | )); 108 | if (cb) { 109 | process.nextTick(cb); 110 | } 111 | }; 112 | 113 | proto.immediateDecrement = function (name, delta, cb) { 114 | this._write(new NullStatsdRecord( 115 | 'c', 116 | this.prefix + name, 117 | null, 118 | (-1 * Math.abs(delta || 1)) 119 | )); 120 | if (cb) { 121 | process.nextTick(cb); 122 | } 123 | }; 124 | 125 | proto.immediateCounter = function (name, value, cb) { 126 | this._write(new NullStatsdRecord( 127 | 'c', 128 | this.prefix + name, 129 | null, 130 | value 131 | )); 132 | if (cb) { 133 | process.nextTick(cb); 134 | } 135 | }; 136 | 137 | proto.immediateTiming = function (name, time, cb) { 138 | this._write(new NullStatsdRecord( 139 | 'ms', 140 | this.prefix + name, 141 | null, 142 | null, 143 | time 144 | )); 145 | if (cb) { 146 | process.nextTick(cb); 147 | } 148 | }; 149 | 150 | proto.getChildClient = function(extraPrefix) { 151 | return new NullStatsd({ 152 | prefix: this.prefix + extraPrefix 153 | }, this._buffer); 154 | }; 155 | -------------------------------------------------------------------------------- /lib/packet-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var setInterval = require('timers').setInterval; 4 | var clearInterval = require('timers').clearInterval; 5 | var Buffer = require('buffer').Buffer; 6 | 7 | /* 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2014 Voxer IP LLC. All rights reserved. 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | module.exports = PacketQueue; 31 | 32 | /// Coalesce metrics packets. 33 | /// 34 | /// send(buffer, offset, length) 35 | /// options - (optional) 36 | /// * block - Integer, maximum block size 37 | /// * flush - Integer, millisecond flush interval 38 | /// 39 | function PacketQueue(send, options) { 40 | if (!(this instanceof PacketQueue)) { 41 | return new PacketQueue(send, options); 42 | } 43 | 44 | options = options || {}; 45 | this._send = send; 46 | this._blockSize = options.block || 1440; 47 | this._trailingNewLine = 'trailingNewLine' in options ? 48 | options.trailingNewLine : true; 49 | this._queue = null; 50 | this._writePos = null; 51 | this._reset(); 52 | 53 | // Don't let stuff queue forever. 54 | var self = this; 55 | this._interval = setInterval(function() { 56 | //TODO instrument _sendPacket() so we know how often 57 | //TODO its called based on timeouts. 58 | if (self._queue.length) { 59 | self._sendPacket(); 60 | } 61 | }, options.flush || 1000); 62 | this._interval.unref(); 63 | } 64 | 65 | PacketQueue.prototype.destroy = function() { 66 | clearInterval(this._interval); 67 | } 68 | 69 | PacketQueue.prototype._reset = function() { 70 | this._queue = []; 71 | this._writePos = 0; 72 | } 73 | 74 | PacketQueue.prototype.write = function(str) { 75 | if (this._writePos + str.length >= this._blockSize && 76 | this._writePos !== 0 77 | ) { 78 | //TODO instrument _sendPacket() so we know how often 79 | //TODO its called based on blockSize 80 | this._sendPacket(); 81 | } 82 | 83 | this._queue.push(str); 84 | this._writePos += str.length + 1; 85 | } 86 | 87 | PacketQueue.prototype._sendPacket = function() { 88 | var str = this._queue.join('\n'); 89 | if (this._trailingNewLine) { 90 | str += '\n'; 91 | } 92 | 93 | var buf = new Buffer(str); 94 | this._send(buf, 0, buf.length); 95 | this._reset(); 96 | } 97 | -------------------------------------------------------------------------------- /lib/statsd-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Messages = require('./messages.js'); 4 | 5 | module.exports = StatsdClient; 6 | 7 | function StatsdClient(options, _ephemeralSocket) { 8 | this.options = options; 9 | this._ephemeralSocket = _ephemeralSocket; 10 | 11 | if (this.options.prefix && this.options.prefix !== '') { 12 | this.options.prefix = enforceDotOnPrefix(this.options.prefix); 13 | } 14 | } 15 | 16 | function StatsdClientOptions(prefix, isDisabled, dnsResolver) { 17 | this.prefix = prefix; 18 | this.isDisabled = isDisabled; 19 | this.dnsResolver = dnsResolver; 20 | } 21 | 22 | function enforceDotOnPrefix(prefix) { 23 | return prefix[prefix.length - 1] === '.' ? prefix : prefix + '.'; 24 | } 25 | 26 | /* 27 | * Get a "child" client with a sub-prefix. 28 | */ 29 | StatsdClient.prototype.getChildClient = function (extraPrefix) { 30 | return new StatsdClient( 31 | new StatsdClientOptions( 32 | this.options.prefix + extraPrefix, 33 | this.options.isDisabled, 34 | this.options.dnsResolver 35 | ), 36 | this._ephemeralSocket 37 | ); 38 | }; 39 | 40 | /* 41 | * gauge(name, value) 42 | */ 43 | StatsdClient.prototype.gauge = function (name, value) { 44 | name = this.options.prefix + name; 45 | var message = new Messages.Gauge(name, value); 46 | this._ephemeralSocket._writeToSocket(message.toString()); 47 | }; 48 | 49 | /* 50 | * counter(name, delta) 51 | */ 52 | StatsdClient.prototype.counter = function (name, delta) { 53 | name = this.options.prefix + name; 54 | var message = new Messages.Counter(name, delta); 55 | this._ephemeralSocket.send(message.toString()); 56 | }; 57 | 58 | /* 59 | * increment(name, [delta=1]) 60 | */ 61 | StatsdClient.prototype.increment = function (name, delta) { 62 | if (delta === 0) { 63 | return; 64 | } 65 | this.counter(name, Math.abs(delta || 1)); 66 | }; 67 | 68 | /* 69 | * decrement(name, [delta=-1]) 70 | */ 71 | StatsdClient.prototype.decrement = function (name, delta) { 72 | if (delta === 0) { 73 | return; 74 | } 75 | this.counter(name, -1 * Math.abs(delta || 1)); 76 | }; 77 | 78 | /* 79 | * timings(name, date-object | ms) 80 | */ 81 | StatsdClient.prototype.timing = function (name, time) { 82 | // Date-object or integer? 83 | var t = time instanceof Date ? new Date() - time : time; 84 | 85 | name = this.options.prefix + name; 86 | var message = new Messages.Timing(name, t); 87 | this._ephemeralSocket.send(message.toString()); 88 | }; 89 | 90 | /* 91 | * immediateGauge(name, delta) 92 | */ 93 | StatsdClient.prototype.immediateGauge = function (name, value, cb) { 94 | name = this.options.prefix + name; 95 | var message = new Messages.Gauge(name, value); 96 | this._ephemeralSocket._writeToSocket(message.toString(), cb); 97 | } 98 | 99 | /* 100 | * immediateCounter(name, delta) 101 | */ 102 | StatsdClient.prototype.immediateCounter = function (name, delta, cb) { 103 | name = this.options.prefix + name; 104 | var message = new Messages.Counter(name, delta); 105 | this._ephemeralSocket._writeToSocket(message.toString(), cb); 106 | } 107 | 108 | /* 109 | * immediateIncrement(name, [delta=1]) 110 | */ 111 | StatsdClient.prototype.immediateIncrement = function (name, delta, cb) { 112 | if (delta === 0) { 113 | return; 114 | } 115 | this.immediateCounter(name, Math.abs(delta || 1), cb); 116 | } 117 | 118 | /* 119 | * immediateDecrement(name, [delta=-1]) 120 | */ 121 | StatsdClient.prototype.immediateDecrement = function (name, delta, cb) { 122 | if (delta === 0) { 123 | return; 124 | } 125 | this.immediateCounter(name, -1 * Math.abs(delta || 1), cb); 126 | }; 127 | 128 | /* 129 | * immediateTimings(name, date-object | ms) 130 | */ 131 | StatsdClient.prototype.immediateTiming = function (name, time, cb) { 132 | // Date-object or integer? 133 | var t = time instanceof Date ? new Date() - time : time; 134 | 135 | name = this.options.prefix + name; 136 | var message = new Messages.Timing(name, t); 137 | this._ephemeralSocket._writeToSocket(message.toString(), cb); 138 | }; 139 | 140 | /* 141 | * Close the socket, if in use and cancel the interval-check, if running. 142 | */ 143 | StatsdClient.prototype.close = function () { 144 | this._ephemeralSocket.close(); 145 | }; 146 | -------------------------------------------------------------------------------- /multi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MultiStatsdClient = require('./lib/multi-statsd-client.js'); 4 | 5 | module.exports = createMultiStatsd; 6 | 7 | function createMultiStatsd(clients) { 8 | return new MultiStatsdClient(clients); 9 | } 10 | -------------------------------------------------------------------------------- /null.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var NullStatsdClient = require('./lib/null-statsd-client.js'); 4 | 5 | module.exports = createNullStatsd; 6 | 7 | function createNullStatsd(opts) { 8 | return new NullStatsdClient(opts); 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Morten Siebuhr ", 3 | "name": "uber-statsd-client", 4 | "description": "Yet another client for Etsy's statsd", 5 | "keywords": [ 6 | "statsd", 7 | "client", 8 | "metrics", 9 | "udp" 10 | ], 11 | "version": "1.7.3", 12 | "homepage": "https://github.com/uber/node-statsd-client", 13 | "bugs": "https://github.com/uber/node-statsd-client/issues", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/uber/node-statsd-client.git" 17 | }, 18 | "main": "./statsd.js", 19 | "scripts": { 20 | "test": "node test/index.js", 21 | "cover": "istanbul cover --print detail --report html test/index.js", 22 | "view-cover": "istanbul report html && opn ./coverage/index.html" 23 | }, 24 | "dependencies": { 25 | "back": "^0.1.5", 26 | "ringbufferjs": "0.0.1", 27 | "xtend": "^4.0.0" 28 | }, 29 | "devDependencies": { 30 | "istanbul": "^0.4.1", 31 | "process": "0.11.1", 32 | "tape": "4.0.0" 33 | }, 34 | "optionalDependencies": {}, 35 | "engines": { 36 | "node": "*" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /statsd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EphemeralSocket = require('./lib/ephemeral-socket.js'); 4 | var StatsdClient = require('./lib/statsd-client.js'); 5 | 6 | module.exports = createStatsdClient; 7 | 8 | /* 9 | * Set up the statsd-client. 10 | * 11 | * Requires the `hostname`. Options currently allows for `port` and `debug` to 12 | * be set. 13 | */ 14 | function createStatsdClient(options) { 15 | options = options || {}; 16 | options.prefix = options.prefix || ''; 17 | options.isDisabled = options.isDisabled || null; 18 | 19 | var _ephemeralSocket = (options && options._ephemeralSocket) || 20 | new EphemeralSocket(options); 21 | 22 | if (options.dnsResolver && 23 | _ephemeralSocket.resolveDNS && 24 | !_ephemeralSocket._dnsResolver 25 | ) { 26 | _ephemeralSocket.resolveDNS(options.dnsResolver); 27 | } 28 | 29 | return new StatsdClient(options, _ephemeralSocket); 30 | } 31 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./socket.js'); 4 | require('./packet-queue.js'); 5 | require('./statsd-client.js'); 6 | require('./resolve-dns.js'); 7 | require('./null.js'); 8 | require('./multi.js'); 9 | -------------------------------------------------------------------------------- /test/lib/udp-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dgram = require('dgram'); 4 | 5 | module.exports = UDPServer; 6 | 7 | function UDPServer(opts, onBound) { 8 | if (!opts || !opts.port) { 9 | throw new Error('UDPServer: `opts.port` required'); 10 | } 11 | if (typeof onBound !== 'function') { 12 | throw new Error('UDPServer: `onBound` function is required'); 13 | } 14 | 15 | var port = opts.port; 16 | 17 | var server = dgram.createSocket('udp4'); 18 | server.bind(port, onBound); 19 | 20 | return server; 21 | } 22 | -------------------------------------------------------------------------------- /test/multi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | 5 | var createMultiStatsd = require('../multi.js'); 6 | var createNullStatsd = require('../null.js'); 7 | 8 | test('multi throws on non-array', function t(assert) { 9 | assert.throws(function testMultiConctuctor() { 10 | return createMultiStatsd(); 11 | }, 'MultiStatsdClient requires an Array of statsd clients'); 12 | assert.end(); 13 | }); 14 | 15 | test('empty multi statsd is a valid null statsd', function t(assert) { 16 | var client = createMultiStatsd([]); 17 | client.increment('foo'); 18 | assert.strictEqual(client.clients.length, 0); 19 | assert.end(); 20 | }); 21 | 22 | test('close will close all child clients', function t(assert) { 23 | function MockClient() { 24 | this.closed = 0; 25 | } 26 | MockClient.prototype.close = function closeMockClient() { 27 | this.closed++; 28 | }; 29 | 30 | var clients = [ 31 | new MockClient(), 32 | new MockClient(), 33 | new MockClient() 34 | ]; 35 | 36 | var multiClient = createMultiStatsd(clients); 37 | multiClient.close(); 38 | 39 | for (var i = 0; i < clients.length; i++) { 40 | assert.strictEqual(clients[i].closed, 1); 41 | } 42 | 43 | assert.end(); 44 | }); 45 | 46 | test('first callback error is surfaced', function t(assert) { 47 | function MockClient() { 48 | this.closed = 0; 49 | } 50 | MockClient.prototype.immediateIncrement = immediateIncrementMockClient; 51 | 52 | function immediateIncrementMockClient(key, value, cb) { 53 | return cb(new Error('oops')); 54 | } 55 | 56 | var callbackCount = 0; 57 | var clients = [ 58 | new MockClient(), 59 | new MockClient(), 60 | new MockClient() 61 | ]; 62 | var multiClient = createMultiStatsd(clients); 63 | multiClient.immediateIncrement('foo', 1, onImmediateIncrement); 64 | 65 | function onImmediateIncrement(err) { 66 | assert.strictEqual(callbackCount, 0); 67 | callbackCount++; 68 | 69 | assert.ok(err); 70 | assert.strictEqual(err.message, 'oops'); 71 | assert.end(); 72 | } 73 | }); 74 | 75 | test('multi.gauge()', function t(assert) { 76 | var clients = [ 77 | createNullStatsd(), 78 | createNullStatsd(), 79 | createNullStatsd() 80 | ]; 81 | 82 | var m = createMultiStatsd(clients); 83 | 84 | m.gauge('some.key', 10); 85 | 86 | for (var i = 0; i < clients.length; i++) { 87 | var c = clients[i]; 88 | 89 | assert.deepEqual(c._buffer.peek(), { 90 | type: 'g', 91 | name: 'some.key', 92 | value: 10, 93 | delta: null, 94 | time: null 95 | }); 96 | 97 | c.close(); 98 | } 99 | 100 | assert.end(); 101 | }); 102 | 103 | test('multi.counter()', function t(assert) { 104 | var clients = [ 105 | createNullStatsd(), 106 | createNullStatsd(), 107 | createNullStatsd() 108 | ]; 109 | 110 | var m = createMultiStatsd(clients); 111 | 112 | m.counter('some.key', 5); 113 | 114 | for (var i = 0; i < clients.length; i++) { 115 | var c = clients[i]; 116 | 117 | assert.deepEqual(c._buffer.peek(), { 118 | type: 'c', 119 | name: 'some.key', 120 | value: null, 121 | delta: 5, 122 | time: null 123 | }); 124 | 125 | c.close(); 126 | } 127 | 128 | assert.end(); 129 | }); 130 | 131 | test('multi.increment()', function t(assert) { 132 | var clients = [ 133 | createNullStatsd(), 134 | createNullStatsd(), 135 | createNullStatsd() 136 | ]; 137 | 138 | var m = createMultiStatsd(clients); 139 | 140 | m.increment('some.key'); 141 | 142 | for (var i = 0; i < clients.length; i++) { 143 | var c = clients[i]; 144 | 145 | assert.deepEqual(c._buffer.peek(), { 146 | type: 'c', 147 | name: 'some.key', 148 | value: null, 149 | delta: 1, 150 | time: null 151 | }); 152 | 153 | c._buffer.deq(); 154 | c.increment('some.key2', 3); 155 | 156 | assert.deepEqual(c._buffer.peek(), { 157 | type: 'c', 158 | name: 'some.key2', 159 | value: null, 160 | delta: 3, 161 | time: null 162 | }); 163 | 164 | c.close(); 165 | } 166 | 167 | assert.end(); 168 | }); 169 | 170 | test('multi.decrement()', function t(assert) { 171 | var clients = [ 172 | createNullStatsd(), 173 | createNullStatsd(), 174 | createNullStatsd() 175 | ]; 176 | 177 | var m = createMultiStatsd(clients); 178 | 179 | m.decrement('some.key'); 180 | 181 | for (var i = 0; i < clients.length; i++) { 182 | var c = clients[i]; 183 | 184 | assert.deepEqual(c._buffer.peek(), { 185 | type: 'c', 186 | name: 'some.key', 187 | value: null, 188 | delta: -1, 189 | time: null 190 | }); 191 | 192 | c._buffer.deq(); 193 | c.decrement('some.key2', 3); 194 | 195 | assert.deepEqual(c._buffer.peek(), { 196 | type: 'c', 197 | name: 'some.key2', 198 | value: null, 199 | delta: -3, 200 | time: null 201 | }); 202 | 203 | c._buffer.deq(); 204 | c.decrement('some.key3', -3); 205 | 206 | assert.deepEqual(c._buffer.peek(), { 207 | type: 'c', 208 | name: 'some.key3', 209 | value: null, 210 | delta: -3, 211 | time: null 212 | }); 213 | 214 | c.close(); 215 | } 216 | 217 | assert.end(); 218 | }); 219 | 220 | test('multi.timing()', function t(assert) { 221 | var clients = [ 222 | createNullStatsd(), 223 | createNullStatsd(), 224 | createNullStatsd() 225 | ]; 226 | 227 | var m = createMultiStatsd(clients); 228 | 229 | m.timing('some.key', 500); 230 | 231 | for (var i = 0; i < clients.length; i++) { 232 | var c = clients[i]; 233 | 234 | assert.deepEqual(c._buffer.peek(), { 235 | type: 'ms', 236 | name: 'some.key', 237 | value: null, 238 | delta: null, 239 | time: 500 240 | }); 241 | 242 | c.close(); 243 | } 244 | 245 | assert.end(); 246 | }); 247 | 248 | test('multi.immediateGauge()', function t(assert) { 249 | var clients = [ 250 | createNullStatsd(), 251 | createNullStatsd(), 252 | createNullStatsd() 253 | ]; 254 | 255 | var m = createMultiStatsd(clients); 256 | var cbCount = 0; 257 | 258 | m.immediateGauge('some.key', 10, function onImmediateGauge(err) { 259 | assert.ifError(err); 260 | assert.strictEqual(cbCount, 0); 261 | 262 | cbCount++; 263 | 264 | for (var i = 0; i < clients.length; i++) { 265 | var c = clients[i]; 266 | 267 | assert.deepEqual(c._buffer.peek(), { 268 | type: 'g', 269 | name: 'some.key', 270 | value: 10, 271 | delta: null, 272 | time: null 273 | }); 274 | 275 | c.close(); 276 | } 277 | 278 | assert.end(); 279 | }); 280 | }); 281 | 282 | test('multi.immediateCounter()', function t(assert) { 283 | var clients = [ 284 | createNullStatsd(), 285 | createNullStatsd(), 286 | createNullStatsd() 287 | ]; 288 | 289 | var m = createMultiStatsd(clients); 290 | var cbCount = 0; 291 | 292 | m.immediateCounter('some.key', 5, function onImmediateCounter(err) { 293 | assert.ifError(err); 294 | assert.strictEqual(cbCount, 0); 295 | 296 | cbCount++; 297 | 298 | for (var i = 0; i < clients.length; i++) { 299 | var c = clients[i]; 300 | 301 | assert.deepEqual(c._buffer.peek(), { 302 | type: 'c', 303 | name: 'some.key', 304 | value: null, 305 | delta: 5, 306 | time: null 307 | }); 308 | 309 | c.close(); 310 | } 311 | 312 | assert.end(); 313 | }); 314 | }); 315 | 316 | test('multi.immediateIncrement()', function t(assert) { 317 | var clients = [ 318 | createNullStatsd(), 319 | createNullStatsd(), 320 | createNullStatsd() 321 | ]; 322 | 323 | var m = createMultiStatsd(clients); 324 | var cbCount = 0; 325 | 326 | m.immediateIncrement('some.key', 1, function onImmediateIncrement(err) { 327 | assert.ifError(err); 328 | assert.strictEqual(cbCount, 0); 329 | 330 | cbCount++; 331 | 332 | for (var i = 0; i < clients.length; i++) { 333 | var c = clients[i]; 334 | 335 | assert.deepEqual(c._buffer.peek(), { 336 | type: 'c', 337 | name: 'some.key', 338 | value: null, 339 | delta: 1, 340 | time: null 341 | }); 342 | 343 | c._buffer.deq(); 344 | c.increment('some.key2', 3); 345 | 346 | assert.deepEqual(c._buffer.peek(), { 347 | type: 'c', 348 | name: 'some.key2', 349 | value: null, 350 | delta: 3, 351 | time: null 352 | }); 353 | 354 | c.close(); 355 | } 356 | 357 | assert.end(); 358 | }); 359 | }); 360 | 361 | test('multi.immediateDecrement()', function t(assert) { 362 | var clients = [ 363 | createNullStatsd(), 364 | createNullStatsd(), 365 | createNullStatsd() 366 | ]; 367 | 368 | var m = createMultiStatsd(clients); 369 | var cbCount = 0; 370 | 371 | m.immediateDecrement('some.key', 1, function onImmediateDecrement(err) { 372 | assert.ifError(err); 373 | assert.strictEqual(cbCount, 0); 374 | 375 | cbCount++; 376 | 377 | for (var i = 0; i < clients.length; i++) { 378 | var c = clients[i]; 379 | 380 | assert.deepEqual(c._buffer.peek(), { 381 | type: 'c', 382 | name: 'some.key', 383 | value: null, 384 | delta: -1, 385 | time: null 386 | }); 387 | 388 | c._buffer.deq(); 389 | c.decrement('some.key2', 3); 390 | 391 | assert.deepEqual(c._buffer.peek(), { 392 | type: 'c', 393 | name: 'some.key2', 394 | value: null, 395 | delta: -3, 396 | time: null 397 | }); 398 | 399 | c._buffer.deq(); 400 | c.decrement('some.key3', -3); 401 | 402 | assert.deepEqual(c._buffer.peek(), { 403 | type: 'c', 404 | name: 'some.key3', 405 | value: null, 406 | delta: -3, 407 | time: null 408 | }); 409 | 410 | c.close(); 411 | } 412 | 413 | assert.end(); 414 | }); 415 | }); 416 | 417 | test('multi.immediateTiming()', function t(assert) { 418 | var clients = [ 419 | createNullStatsd(), 420 | createNullStatsd(), 421 | createNullStatsd() 422 | ]; 423 | 424 | var m = createMultiStatsd(clients); 425 | var cbCount = 0; 426 | 427 | m.immediateTiming('some.key', 500, function onImmediateTiming(err) { 428 | assert.ifError(err); 429 | assert.strictEqual(cbCount, 0); 430 | 431 | cbCount++; 432 | 433 | for (var i = 0; i < clients.length; i++) { 434 | var c = clients[i]; 435 | 436 | assert.deepEqual(c._buffer.peek(), { 437 | type: 'ms', 438 | name: 'some.key', 439 | value: null, 440 | delta: null, 441 | time: 500 442 | }); 443 | 444 | c.close(); 445 | } 446 | 447 | assert.end(); 448 | }); 449 | }); 450 | 451 | test('child multi.gauge()', function t(assert) { 452 | var clients = [ 453 | createNullStatsd(), 454 | createNullStatsd(), 455 | createNullStatsd() 456 | ]; 457 | 458 | var m = createMultiStatsd(clients).getChildClient('foo'); 459 | 460 | m.gauge('some.key', 10); 461 | 462 | for (var i = 0; i < clients.length; i++) { 463 | var c = m.clients[i]; 464 | 465 | assert.deepEqual(c._buffer.peek(), { 466 | type: 'g', 467 | name: 'foo.some.key', 468 | value: 10, 469 | delta: null, 470 | time: null 471 | }); 472 | 473 | c.close(); 474 | } 475 | 476 | assert.end(); 477 | }); 478 | 479 | test('child multi.counter()', function t(assert) { 480 | var clients = [ 481 | createNullStatsd(), 482 | createNullStatsd(), 483 | createNullStatsd() 484 | ]; 485 | 486 | var m = createMultiStatsd(clients).getChildClient('foo'); 487 | 488 | m.counter('some.key', 5); 489 | 490 | for (var i = 0; i < clients.length; i++) { 491 | var c = m.clients[i]; 492 | 493 | assert.deepEqual(c._buffer.peek(), { 494 | type: 'c', 495 | name: 'foo.some.key', 496 | value: null, 497 | delta: 5, 498 | time: null 499 | }); 500 | 501 | c.close(); 502 | } 503 | 504 | assert.end(); 505 | }); 506 | 507 | test('child multi.increment()', function t(assert) { 508 | var clients = [ 509 | createNullStatsd(), 510 | createNullStatsd(), 511 | createNullStatsd() 512 | ]; 513 | 514 | var m = createMultiStatsd(clients).getChildClient('foo'); 515 | 516 | m.increment('some.key'); 517 | 518 | for (var i = 0; i < clients.length; i++) { 519 | var c = m.clients[i]; 520 | 521 | assert.deepEqual(c._buffer.peek(), { 522 | type: 'c', 523 | name: 'foo.some.key', 524 | value: null, 525 | delta: 1, 526 | time: null 527 | }); 528 | 529 | c._buffer.deq(); 530 | c.increment('some.key2', 3); 531 | 532 | assert.deepEqual(c._buffer.peek(), { 533 | type: 'c', 534 | name: 'foo.some.key2', 535 | value: null, 536 | delta: 3, 537 | time: null 538 | }); 539 | 540 | c.close(); 541 | } 542 | 543 | assert.end(); 544 | }); 545 | 546 | test('child multi.decrement()', function t(assert) { 547 | var clients = [ 548 | createNullStatsd(), 549 | createNullStatsd(), 550 | createNullStatsd() 551 | ]; 552 | 553 | var m = createMultiStatsd(clients).getChildClient('foo'); 554 | 555 | m.decrement('some.key'); 556 | 557 | for (var i = 0; i < clients.length; i++) { 558 | var c = m.clients[i]; 559 | 560 | assert.deepEqual(c._buffer.peek(), { 561 | type: 'c', 562 | name: 'foo.some.key', 563 | value: null, 564 | delta: -1, 565 | time: null 566 | }); 567 | 568 | c._buffer.deq(); 569 | c.decrement('some.key2', 3); 570 | 571 | assert.deepEqual(c._buffer.peek(), { 572 | type: 'c', 573 | name: 'foo.some.key2', 574 | value: null, 575 | delta: -3, 576 | time: null 577 | }); 578 | 579 | c._buffer.deq(); 580 | c.decrement('some.key3', -3); 581 | 582 | assert.deepEqual(c._buffer.peek(), { 583 | type: 'c', 584 | name: 'foo.some.key3', 585 | value: null, 586 | delta: -3, 587 | time: null 588 | }); 589 | 590 | c.close(); 591 | } 592 | 593 | assert.end(); 594 | }); 595 | 596 | test('child multi.timing()', function t(assert) { 597 | var clients = [ 598 | createNullStatsd(), 599 | createNullStatsd(), 600 | createNullStatsd() 601 | ]; 602 | 603 | var m = createMultiStatsd(clients).getChildClient('foo'); 604 | 605 | m.timing('some.key', 500); 606 | 607 | for (var i = 0; i < clients.length; i++) { 608 | var c = m.clients[i]; 609 | 610 | assert.deepEqual(c._buffer.peek(), { 611 | type: 'ms', 612 | name: 'foo.some.key', 613 | value: null, 614 | delta: null, 615 | time: 500 616 | }); 617 | 618 | c.close(); 619 | } 620 | 621 | assert.end(); 622 | }); 623 | 624 | test('multi.immediateGauge()', function t(assert) { 625 | var clients = [ 626 | createNullStatsd(), 627 | createNullStatsd(), 628 | createNullStatsd() 629 | ]; 630 | 631 | var m = createMultiStatsd(clients).getChildClient('foo'); 632 | var cbCount = 0; 633 | 634 | m.immediateGauge('some.key', 10, function onImmediateGauge(err) { 635 | assert.ifError(err); 636 | assert.strictEqual(cbCount, 0); 637 | 638 | cbCount++; 639 | 640 | for (var i = 0; i < clients.length; i++) { 641 | var c = m.clients[i]; 642 | 643 | assert.deepEqual(c._buffer.peek(), { 644 | type: 'g', 645 | name: 'foo.some.key', 646 | value: 10, 647 | delta: null, 648 | time: null 649 | }); 650 | 651 | c.close(); 652 | } 653 | 654 | assert.end(); 655 | }); 656 | }); 657 | 658 | test('multi.immediateCounter()', function t(assert) { 659 | var clients = [ 660 | createNullStatsd(), 661 | createNullStatsd(), 662 | createNullStatsd() 663 | ]; 664 | 665 | var m = createMultiStatsd(clients).getChildClient('foo'); 666 | var cbCount = 0; 667 | 668 | m.immediateCounter('some.key', 5, function onImmediateCounter(err) { 669 | assert.ifError(err); 670 | assert.strictEqual(cbCount, 0); 671 | 672 | cbCount++; 673 | 674 | for (var i = 0; i < clients.length; i++) { 675 | var c = m.clients[i]; 676 | 677 | assert.deepEqual(c._buffer.peek(), { 678 | type: 'c', 679 | name: 'foo.some.key', 680 | value: null, 681 | delta: 5, 682 | time: null 683 | }); 684 | 685 | c.close(); 686 | } 687 | 688 | assert.end(); 689 | }); 690 | }); 691 | 692 | test('multi.immediateIncrement()', function t(assert) { 693 | var clients = [ 694 | createNullStatsd(), 695 | createNullStatsd(), 696 | createNullStatsd() 697 | ]; 698 | 699 | var m = createMultiStatsd(clients).getChildClient('foo'); 700 | var cbCount = 0; 701 | 702 | m.immediateIncrement('some.key', 1, function onImmediateIncrement(err) { 703 | assert.ifError(err); 704 | assert.strictEqual(cbCount, 0); 705 | 706 | cbCount++; 707 | 708 | for (var i = 0; i < clients.length; i++) { 709 | var c = m.clients[i]; 710 | 711 | assert.deepEqual(c._buffer.peek(), { 712 | type: 'c', 713 | name: 'foo.some.key', 714 | value: null, 715 | delta: 1, 716 | time: null 717 | }); 718 | 719 | c._buffer.deq(); 720 | c.increment('some.key2', 3); 721 | 722 | assert.deepEqual(c._buffer.peek(), { 723 | type: 'c', 724 | name: 'foo.some.key2', 725 | value: null, 726 | delta: 3, 727 | time: null 728 | }); 729 | 730 | c.close(); 731 | } 732 | 733 | assert.end(); 734 | }); 735 | }); 736 | 737 | test('multi.immediateDecrement()', function t(assert) { 738 | var clients = [ 739 | createNullStatsd(), 740 | createNullStatsd(), 741 | createNullStatsd() 742 | ]; 743 | 744 | var m = createMultiStatsd(clients).getChildClient('foo'); 745 | var cbCount = 0; 746 | 747 | m.immediateDecrement('some.key', 1, function onImmediateDecrement(err) { 748 | assert.ifError(err); 749 | assert.strictEqual(cbCount, 0); 750 | 751 | cbCount++; 752 | 753 | for (var i = 0; i < clients.length; i++) { 754 | var c = m.clients[i]; 755 | 756 | assert.deepEqual(c._buffer.peek(), { 757 | type: 'c', 758 | name: 'foo.some.key', 759 | value: null, 760 | delta: -1, 761 | time: null 762 | }); 763 | 764 | c._buffer.deq(); 765 | c.decrement('some.key2', 3); 766 | 767 | assert.deepEqual(c._buffer.peek(), { 768 | type: 'c', 769 | name: 'foo.some.key2', 770 | value: null, 771 | delta: -3, 772 | time: null 773 | }); 774 | 775 | c._buffer.deq(); 776 | c.decrement('some.key3', -3); 777 | 778 | assert.deepEqual(c._buffer.peek(), { 779 | type: 'c', 780 | name: 'foo.some.key3', 781 | value: null, 782 | delta: -3, 783 | time: null 784 | }); 785 | 786 | c.close(); 787 | } 788 | 789 | assert.end(); 790 | }); 791 | }); 792 | 793 | test('multi.immediateTiming()', function t(assert) { 794 | var clients = [ 795 | createNullStatsd(), 796 | createNullStatsd(), 797 | createNullStatsd() 798 | ]; 799 | 800 | var m = createMultiStatsd(clients).getChildClient('foo'); 801 | var cbCount = 0; 802 | 803 | m.immediateTiming('some.key', 500, function onImmediateTiming(err) { 804 | assert.ifError(err); 805 | assert.strictEqual(cbCount, 0); 806 | 807 | cbCount++; 808 | 809 | for (var i = 0; i < clients.length; i++) { 810 | var c = m.clients[i]; 811 | 812 | assert.deepEqual(c._buffer.peek(), { 813 | type: 'ms', 814 | name: 'foo.some.key', 815 | value: null, 816 | delta: null, 817 | time: 500 818 | }); 819 | 820 | c.close(); 821 | } 822 | 823 | assert.end(); 824 | }); 825 | }); 826 | -------------------------------------------------------------------------------- /test/null.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | 5 | var createNullStatsd = require('../null.js'); 6 | 7 | test('null statsd capacity option back compat', function t(assert) { 8 | var c = createNullStatsd(123); 9 | assert.strictEqual(c._buffer.capacity(), 123); 10 | c.close(); 11 | assert.end(); 12 | }); 13 | 14 | test('null statsd has a statsd compatible interface', function t(assert) { 15 | var c = createNullStatsd({ 16 | capacity: 456, 17 | prefix: 'a.b' 18 | }); 19 | 20 | c.gauge('some.key', 10); 21 | 22 | assert.strictEqual(c._buffer.capacity(), 456); 23 | assert.deepEqual(c._buffer.peek(), { 24 | type: 'g', 25 | name: 'a.b.some.key', 26 | value: 10, 27 | delta: null, 28 | time: null 29 | }); 30 | c.close(); 31 | 32 | assert.end(); 33 | }); 34 | 35 | test('null.gauge()', function t(assert) { 36 | var c = createNullStatsd(); 37 | c.gauge('some.key', 10); 38 | 39 | assert.deepEqual(c._buffer.peek(), { 40 | type: 'g', 41 | name: 'some.key', 42 | value: 10, 43 | delta: null, 44 | time: null 45 | }); 46 | 47 | c.close(); 48 | assert.end(); 49 | }); 50 | 51 | test('null.counter()', function t(assert) { 52 | var c = createNullStatsd(); 53 | c.counter('some.key', 5); 54 | 55 | assert.deepEqual(c._buffer.peek(), { 56 | type: 'c', 57 | name: 'some.key', 58 | value: null, 59 | delta: 5, 60 | time: null 61 | }); 62 | 63 | c.close(); 64 | assert.end(); 65 | }); 66 | 67 | test('null.increment()', function t(assert) { 68 | var c = createNullStatsd(); 69 | 70 | c.increment('some.key'); 71 | 72 | assert.deepEqual(c._buffer.peek(), { 73 | type: 'c', 74 | name: 'some.key', 75 | value: null, 76 | delta: 1, 77 | time: null 78 | }); 79 | 80 | c._buffer.deq(); 81 | c.increment('some.key2', 3); 82 | 83 | assert.deepEqual(c._buffer.peek(), { 84 | type: 'c', 85 | name: 'some.key2', 86 | value: null, 87 | delta: 3, 88 | time: null 89 | }); 90 | 91 | c.close(); 92 | assert.end(); 93 | }); 94 | 95 | test('null.decrement()', function t(assert) { 96 | var c = createNullStatsd(); 97 | 98 | c.decrement('some.key'); 99 | 100 | assert.deepEqual(c._buffer.peek(), { 101 | type: 'c', 102 | name: 'some.key', 103 | value: null, 104 | delta: -1, 105 | time: null 106 | }); 107 | 108 | c._buffer.deq(); 109 | c.decrement('some.key2', 3); 110 | 111 | assert.deepEqual(c._buffer.peek(), { 112 | type: 'c', 113 | name: 'some.key2', 114 | value: null, 115 | delta: -3, 116 | time: null 117 | }); 118 | 119 | c._buffer.deq(); 120 | c.decrement('some.key3', -3); 121 | 122 | assert.deepEqual(c._buffer.peek(), { 123 | type: 'c', 124 | name: 'some.key3', 125 | value: null, 126 | delta: -3, 127 | time: null 128 | }); 129 | 130 | c.close(); 131 | assert.end(); 132 | }); 133 | 134 | test('null.timing()', function t(assert) { 135 | var c = createNullStatsd(); 136 | 137 | c.timing('some.key', 500); 138 | 139 | assert.deepEqual(c._buffer.peek(), { 140 | type: 'ms', 141 | name: 'some.key', 142 | value: null, 143 | delta: null, 144 | time: 500 145 | }); 146 | 147 | c.close(); 148 | assert.end(); 149 | }); 150 | -------------------------------------------------------------------------------- /test/packet-queue.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Voxer IP LLC. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the 'Software'), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 'use strict'; 25 | 26 | var test = require('tape'); 27 | var process = require('process'); 28 | var setTimeout = require('timers').setTimeout; 29 | 30 | var PacketQueue = require('../lib/packet-queue') 31 | 32 | function noop() {} 33 | 34 | test('PacketQueue', function(assert) { 35 | var pq = new PacketQueue(noop, {block: 10}) 36 | assert.equals(pq._send, noop) 37 | assert.equals(pq._blockSize, 10) 38 | assert.deepEquals(pq._queue, []) 39 | assert.end(); 40 | pq.destroy() 41 | }) 42 | 43 | test('PacketQueue#reset', function(assert) { 44 | var pq = new PacketQueue(noop) 45 | pq.write('foo') 46 | pq._reset() 47 | 48 | assert.deepEquals(pq._queue, []) 49 | assert.end(); 50 | pq.destroy() 51 | }) 52 | 53 | test('PacketQueue#write queue', function(assert) { 54 | var pq = new PacketQueue(function() { 55 | assert.fail() 56 | }, { 57 | block: 30 58 | }) 59 | var pos = pq._writePos 60 | 61 | pq.write('12345') 62 | assert.deepEquals(pq._queue, ['12345']) 63 | assert.equals(pq._writePos, pos + 6) 64 | assert.end(); 65 | pq.destroy() 66 | }); 67 | 68 | test('PacketQueue#write flush', function(assert) { 69 | var blockSize = 30 70 | var w = 0 71 | var pq = new PacketQueue(send, { 72 | block: blockSize, flush: 10 73 | }) 74 | 75 | pq.write('123') 76 | pq.write('456') 77 | 78 | setTimeout(function() { 79 | assert.equals(w, 1) 80 | assert.end(); 81 | pq.destroy() 82 | }, 15) 83 | 84 | function send(data, offset, len) { 85 | w++ 86 | assert.deepEquals(data.toString(), '123\n456\n') 87 | assert.equals(offset, 0) 88 | assert.equals(len, data.length) 89 | } 90 | }) 91 | 92 | test('PacketQueue#write overflow', function(assert) { 93 | var pq = new PacketQueue(send, { 94 | block: 9 95 | }) 96 | pq.write('123') 97 | pq.write('456') 98 | pq.write('789') 99 | 100 | function send(data, offset, len) { 101 | assert.deepEquals(data.toString(), '123\n456\n') 102 | assert.equals(offset, 0) 103 | assert.equals(len, '123456\n'.length + 1) 104 | 105 | process.nextTick(function() { 106 | assert.deepEquals(pq._queue, ['789']) 107 | assert.end(); 108 | pq.destroy() 109 | }) 110 | } 111 | }) 112 | 113 | test('PacketQueue late write', function (assert) { 114 | var called = 0; 115 | var pq = new PacketQueue(send, { 116 | block: 30, flush: 10 117 | }); 118 | 119 | setTimeout(function () { 120 | assert.equal(called, 0); 121 | pq.write('hello'); 122 | 123 | setTimeout(function () { 124 | assert.equal(called, 1); 125 | assert.end(); 126 | pq.destroy(); 127 | }, 15); 128 | }, 25); 129 | 130 | function send(data, offset, len) { 131 | called++; 132 | assert.equal(data.toString(), 'hello\n'); 133 | } 134 | }); 135 | 136 | test('PacketQueue write large buffer', function (assert) { 137 | var called = 0; 138 | var pq = new PacketQueue(send, { 139 | flush: 10, block: 10 140 | }); 141 | 142 | pq.write('hellohellohello'); 143 | 144 | setTimeout(function () { 145 | assert.equal(called, 1); 146 | 147 | assert.end(); 148 | pq.destroy(); 149 | }, 15); 150 | 151 | function send(buf) { 152 | assert.equal(String(buf), 'hellohellohello\n'); 153 | called++; 154 | } 155 | }) 156 | 157 | test('PacketQueue without trailing new line', function (assert) { 158 | var called = { 159 | one: 0, 160 | two: 0 161 | }; 162 | var pq = new PacketQueue(send, { 163 | flush: 10 164 | }); 165 | var pq2 = new PacketQueue(send2, { 166 | flush: 10, 167 | trailingNewLine: false 168 | }) 169 | 170 | pq.write('hello'); 171 | pq2.write('hello'); 172 | 173 | setTimeout(function () { 174 | assert.equal(called.one, 1); 175 | assert.equal(called.two, 1); 176 | 177 | assert.end(); 178 | pq.destroy(); 179 | pq2.destroy(); 180 | }, 15); 181 | 182 | function send(data, offset, len) { 183 | assert.equal(String(data), 'hello\n'); 184 | called.one++ 185 | } 186 | 187 | function send2(data, offset, len) { 188 | assert.equal(String(data), 'hello'); 189 | called.two++ 190 | } 191 | }); 192 | -------------------------------------------------------------------------------- /test/resolve-dns.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var isIPv4 = require('net').isIPv4; 5 | var process = require('process'); 6 | 7 | var DNSResolver = require('../lib/dns-resolver.js'); 8 | 9 | var REAL_HOST = 'google.com'; 10 | var FAKE_HOST = 'not.a.real.domain.example.com'; 11 | 12 | test('DNS resolver defaults to host', function t(assert) { 13 | var resolver = new DNSResolver('google.com', {}); 14 | resolver.lookupHost(); 15 | 16 | var hostname = resolver.resolveHost('some-key'); 17 | assert.equal(hostname, REAL_HOST); 18 | 19 | resolver.close(); 20 | assert.end(); 21 | }); 22 | 23 | test('can resolve DNS', function t(assert) { 24 | var resolver = new DNSResolver(REAL_HOST, {}); 25 | resolver.onresolved = onresolved; 26 | resolver.lookupHost(); 27 | 28 | function onresolved() { 29 | var hostname = resolver.resolveHost('some-key'); 30 | assert.ok(isIPv4(hostname)); 31 | 32 | resolver.close(); 33 | assert.end(); 34 | } 35 | }); 36 | 37 | test('retries on DNS failures', function t(assert) { 38 | var resolver = new DNSResolver(REAL_HOST, { 39 | dns: { 40 | counter: 0, 41 | lookup: function (hostname, cb) { 42 | var self = this; 43 | process.nextTick(function () { 44 | if (self.counter === 0) { 45 | cb(new Error('DNS failure')); 46 | } else { 47 | cb(null, '0.0.0.0'); 48 | } 49 | 50 | self.counter++; 51 | }); 52 | } 53 | }, 54 | backoffSettings: { 55 | minDelay: 0, 56 | maxDelay: 50 57 | } 58 | }); 59 | 60 | resolver.onresolved = onresolved; 61 | resolver.lookupHost(); 62 | 63 | function onresolved() { 64 | var hostname = resolver.resolveHost('some-key'); 65 | assert.ok(isIPv4(hostname)); 66 | 67 | resolver.close(); 68 | assert.end(); 69 | } 70 | }); 71 | 72 | test('can close a DNS resolver', function t(assert) { 73 | var resolver = new DNSResolver(FAKE_HOST, { 74 | backoffSettings: { 75 | retries: Infinity 76 | } 77 | }); 78 | 79 | resolver.lookupHost(); 80 | 81 | resolver.close(); 82 | assert.end(); 83 | }); 84 | 85 | test('can close a DNS resolver after DNS', function t(assert) { 86 | var resolver = new DNSResolver(FAKE_HOST, { 87 | backoffSettings: { 88 | retries: Infinity 89 | }, 90 | dns: { 91 | lookup: function (host, cb) { 92 | process.nextTick(function () { 93 | cb(new Error('no')); 94 | 95 | process.nextTick(close); 96 | }); 97 | } 98 | } 99 | }); 100 | 101 | resolver.lookupHost(); 102 | 103 | function close() { 104 | var hostname = resolver.resolveHost('some-key'); 105 | assert.equal(hostname, FAKE_HOST); 106 | 107 | resolver.close(); 108 | assert.end(); 109 | } 110 | }); 111 | 112 | test('DNS resolver will give up', function t(assert) { 113 | var resolver = new DNSResolver(FAKE_HOST, { 114 | backoffSettings: { 115 | retries: 3, 116 | minDelay: 0, 117 | maxDelay: 50 118 | } 119 | }); 120 | 121 | resolver.onresolvegiveup = onresolvegiveup; 122 | resolver.lookupHost(); 123 | 124 | function onresolvegiveup() { 125 | var hostname = resolver.resolveHost('some-key'); 126 | assert.equal(hostname, FAKE_HOST); 127 | 128 | resolver.close(); 129 | assert.end(); 130 | } 131 | }) 132 | 133 | test('DNS resolver defaults to seedList', function t(assert) { 134 | var resolver = new DNSResolver(FAKE_HOST, { 135 | backoffSettings: { 136 | retries: Infinity 137 | }, 138 | seedIP: '0.0.0.0' 139 | }); 140 | 141 | var hostname = resolver.resolveHost('some-key'); 142 | assert.ok(isIPv4(hostname)); 143 | 144 | resolver.close(); 145 | assert.end(); 146 | }) 147 | 148 | test('Calls dns.resolve on interval', function t(assert) { 149 | var counter = 0; 150 | var resolver = new DNSResolver(FAKE_HOST, { 151 | timeToLive: 100, 152 | dns: { 153 | lookup: function (host, cb) { 154 | process.nextTick(function () { 155 | counter++; 156 | cb(null, '0.0.0.' + counter); 157 | }); 158 | } 159 | } 160 | }); 161 | 162 | resolver.lookupHost(); 163 | setTimeout(close, 250); 164 | 165 | function close() { 166 | assert.equal(counter, 3); 167 | var hostname = resolver.resolveHost('some-key'); 168 | assert.equal(hostname, '0.0.0.3'); 169 | 170 | resolver.close(); 171 | assert.end(); 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /test/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var setTimeout = require('timers').setTimeout; 5 | var isIPv4 = require('net').isIPv4; 6 | var process = require('process'); 7 | 8 | var UDPServer = require('./lib/udp-server.js'); 9 | var EphemeralSocket = require('../lib/ephemeral-socket.js'); 10 | 11 | var PORT = 8125; 12 | 13 | test('creates a socket', function t(assert) { 14 | var sock = new EphemeralSocket({ 15 | host: 'localhost', 16 | port: PORT, 17 | packetQueue: { flush: 10 } 18 | }); 19 | 20 | assert.equal(typeof sock.close, 'function'); 21 | assert.equal(typeof sock.send, 'function'); 22 | 23 | sock.close(); 24 | assert.end(); 25 | }); 26 | 27 | test('can write to socket', function t(assert) { 28 | var server = UDPServer({ port: PORT }, function onBound() { 29 | var sock = new EphemeralSocket({ 30 | host: 'localhost', 31 | port: PORT, 32 | packetQueue: { flush: 10 } 33 | }); 34 | 35 | server.once('message', onMessage); 36 | sock.send('hello'); 37 | 38 | function onMessage(msg) { 39 | var str = String(msg); 40 | assert.equal(str, 'hello\n'); 41 | 42 | sock.close(); 43 | server.close(); 44 | assert.end(); 45 | } 46 | }); 47 | }); 48 | 49 | test('respects isDisabled', function t(assert) { 50 | var isDisabledBool = false; 51 | var server = UDPServer({ port: PORT }, function onBound() { 52 | var sock = new EphemeralSocket({ 53 | host: 'localhost', 54 | port: PORT, 55 | packetQueue: { flush: 10 }, 56 | isDisabled: function isDisabled() { 57 | return isDisabledBool; 58 | } 59 | }); 60 | 61 | server.once('message', onMessage); 62 | sock.send('hello'); 63 | 64 | function onMessage(msg) { 65 | var str = String(msg); 66 | assert.equal(str, 'hello\n'); 67 | 68 | isDisabledBool = true; 69 | server.on('message', failure); 70 | sock.send('hello'); 71 | 72 | setTimeout(next, 100); 73 | 74 | function failure() { 75 | assert.ok(false, 'unexpected message'); 76 | } 77 | 78 | function next() { 79 | isDisabledBool = false; 80 | server.removeListener('message', failure); 81 | 82 | server.once('message', onMessage2); 83 | sock.send('hello'); 84 | } 85 | 86 | function onMessage2(msg) { 87 | assert.ok(String(msg)); 88 | 89 | end(); 90 | } 91 | } 92 | 93 | function end() { 94 | sock.close(); 95 | server.close(); 96 | assert.end(); 97 | } 98 | }); 99 | }); 100 | 101 | test('has default ports & hosts', function t(assert) { 102 | var server = UDPServer({ port: PORT }, function onBound() { 103 | var sock = new EphemeralSocket(); 104 | 105 | server.once('message', onMessage); 106 | sock.send('hello'); 107 | sock._queue._sendPacket(); 108 | 109 | function onMessage(msg) { 110 | var str = String(msg); 111 | assert.equal(str, 'hello\n'); 112 | 113 | sock.close(); 114 | server.close(); 115 | assert.end(); 116 | } 117 | }); 118 | }); 119 | 120 | test('can send multiple packets', function t(assert) { 121 | var server = UDPServer({ port: PORT }, function onBound() { 122 | var sock = new EphemeralSocket({ 123 | host: 'localhost', 124 | port: PORT, 125 | packetQueue: { flush: 10 } 126 | }); 127 | var messages = ''; 128 | 129 | server.on('message', onMessage); 130 | sock.send('hello'); 131 | sock.send(' '); 132 | sock.send('world'); 133 | 134 | function onMessage(msg) { 135 | messages += msg; 136 | 137 | if (messages.length === 14) { 138 | onEnd(); 139 | } 140 | } 141 | 142 | function onEnd() { 143 | assert.equal(messages, 'hello\n \nworld\n'); 144 | 145 | sock.close(); 146 | server.close(); 147 | assert.end(); 148 | } 149 | }); 150 | }); 151 | 152 | 153 | test('can send multiple packets', function t(assert) { 154 | var server = UDPServer({ port: PORT }, function onBound() { 155 | var sock = new EphemeralSocket({ 156 | host: 'localhost', 157 | port: PORT, 158 | packetQueue: { flush: 10, block: 5 } 159 | }); 160 | var messages = []; 161 | 162 | server.on('message', onMessage); 163 | sock.send('hello'); 164 | sock.send(' '); 165 | sock.send('world'); 166 | 167 | function onMessage(msg) { 168 | messages.push(msg); 169 | 170 | if (messages.join('').length === 14) { 171 | onEnd(); 172 | } 173 | } 174 | 175 | function onEnd() { 176 | assert.equal(messages.sort().join(''), 177 | ' \nhello\nworld\n'); 178 | 179 | sock.close(); 180 | server.close(); 181 | assert.end(); 182 | } 183 | }); 184 | }); 185 | 186 | test('socket will close and timeout', function t(assert) { 187 | var server = UDPServer({ port: PORT }, function onBound() { 188 | var sock = new EphemeralSocket({ 189 | host: 'localhost', 190 | port: PORT, 191 | socket_timeout: 10, 192 | packetQueue: { flush: 10 } 193 | }); 194 | 195 | server.once('message', onMessage); 196 | sock.send('hello'); 197 | 198 | function onMessage(msg) { 199 | var str = String(msg); 200 | assert.equal(str, 'hello\n'); 201 | 202 | setTimeout(expectClosed, 50); 203 | } 204 | 205 | function expectClosed() { 206 | assert.equal(sock._socket, undefined); 207 | 208 | server.close(); 209 | assert.end(); 210 | } 211 | }); 212 | }); 213 | 214 | test('continues time-based queue flushing after socket timeout', function t(assert) { 215 | var server = UDPServer({ port: PORT }, function onBound() { 216 | var sock = new EphemeralSocket({ 217 | host: 'localhost', 218 | port: PORT, 219 | socket_timeout: 10, 220 | packetQueue: { flush: 10 } 221 | }); 222 | 223 | server.once('message', onMessage); 224 | sock.send('hello'); 225 | 226 | function onMessage(msg) { 227 | setTimeout(expectClosed, 50); 228 | } 229 | 230 | function expectClosed() { 231 | server.once('message', onSecondMessage); 232 | sock.send('jello'); 233 | } 234 | 235 | function onSecondMessage(msg) { 236 | server.close(); 237 | 238 | var str = String(msg); 239 | assert.equal(str, 'jello\n'); 240 | assert.end(); 241 | } 242 | }); 243 | }); 244 | 245 | test('continues using dns resolver after socket timeout', function t(assert) { 246 | var server = UDPServer({ port: PORT }, function onBound() { 247 | var sock = new EphemeralSocket({ 248 | host: 'localhost', 249 | port: PORT, 250 | socket_timeout: 10, 251 | packetQueue: { flush: 10 } 252 | }); 253 | sock.resolveDNS({}); 254 | 255 | server.once('message', onMessage); 256 | sock.send('hello'); 257 | 258 | function onMessage(msg) { 259 | assert.notok(sock._dnsResolver._destroyed); 260 | setTimeout(expectClosed, 50); 261 | } 262 | 263 | function expectClosed() { 264 | server.close(); 265 | 266 | assert.notok(sock._dnsResolver._destroyed); 267 | assert.end(); 268 | } 269 | }); 270 | }); 271 | 272 | test('writing to a bad host does not blow up', function t(assert) { 273 | var sock = new EphemeralSocket({ 274 | host: 'lol.example.com', 275 | port: PORT, 276 | socket_timeout: 0 277 | }); 278 | 279 | sock.send('hello'); 280 | 281 | setTimeout(function onTimer() { 282 | assert.equal(sock._socket, null); 283 | assert.end(); 284 | }, 50); 285 | }); 286 | 287 | test('can write to socket with DNS resolver', function t(assert) { 288 | var server = UDPServer({ port: PORT }, function onBound() { 289 | var sock = new EphemeralSocket({ 290 | host: 'localhost', 291 | port: PORT, 292 | packetQueue: { flush: 10 } 293 | }); 294 | sock.resolveDNS({}); 295 | 296 | server.once('message', onMessage); 297 | sock.send('hello'); 298 | 299 | function onMessage(msg) { 300 | var str = String(msg); 301 | assert.equal(str, 'hello\n'); 302 | 303 | sock.close(); 304 | server.close(); 305 | assert.end(); 306 | } 307 | }); 308 | }); 309 | 310 | test('DNS resolver will send IP address', function t(assert) { 311 | var called = false; 312 | var sock = new EphemeralSocket({ 313 | host: 'localhost', 314 | port: PORT, 315 | packetQueue: { 316 | flush: 10 317 | }, 318 | dgram: { 319 | createSocket: function createSocket() { 320 | var socket = {}; 321 | socket.send = function send(buf, s, e, port, host) { 322 | var str = String(buf); 323 | assert.equal(str, 'hello\n'); 324 | 325 | assert.ok(isIPv4(host)); 326 | called = true; 327 | }; 328 | socket.close = function close() {}; 329 | socket.unref = function unref() {}; 330 | socket.once = function once() {}; 331 | 332 | return socket; 333 | } 334 | } 335 | }); 336 | 337 | sock.resolveDNS({ 338 | onresolved: function onresolved() { 339 | sock.send('hello'); 340 | } 341 | }); 342 | 343 | setTimeout(function fini() { 344 | assert.ok(called); 345 | 346 | sock.close(); 347 | assert.end(); 348 | }, 50); 349 | }); 350 | 351 | test('writing to a bad host does not blow up on multiple writes crossing the queue boundary', function t(assert) { 352 | var sock = new EphemeralSocket({ 353 | host: 'lol.example.com', 354 | port: PORT, 355 | socket_timeout: 0, 356 | packetQueue: { 357 | block: 1, 358 | flush: 1 359 | } 360 | }); 361 | 362 | sock.send('hello'); 363 | sock.send('hello'); 364 | 365 | var uncaughtExceptionCount = 0; 366 | process.on('uncaughtException', onUncaughtException); 367 | 368 | setTimeout(function onTime() { 369 | if (sock._socket) { 370 | setTimeout(onTime, 1500); 371 | } else { 372 | assertState(); 373 | } 374 | }, 100); 375 | 376 | function assertState() { 377 | assert.equal(sock._socket, null); 378 | assert.equal(uncaughtExceptionCount, 0); 379 | 380 | process.removeListener('uncaughtException', onUncaughtException); 381 | assert.end(); 382 | } 383 | 384 | function onUncaughtException(err) { 385 | assert.ifError(err); 386 | uncaughtExceptionCount++; 387 | } 388 | }); 389 | -------------------------------------------------------------------------------- /test/statsd-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var setTimeout = require('timers').setTimeout; 5 | 6 | var UDPServer = require('./lib/udp-server.js'); 7 | var StatsdClient = require('../statsd.js'); 8 | 9 | var PORT = 8125; 10 | 11 | test('can write gauge to client', function t(assert) { 12 | var server = UDPServer({ port: PORT }, function onBound() { 13 | var client = new StatsdClient({ 14 | host: 'localhost', 15 | port: PORT, 16 | packetQueue: { flush: 10 } 17 | }); 18 | 19 | client.gauge('foo', 'bar'); 20 | server.once('message', function (msg) { 21 | assert.equal(msg.toString(), 'foo:bar|g'); 22 | 23 | server.close(); 24 | client.close(); 25 | assert.end(); 26 | }); 27 | }); 28 | }); 29 | 30 | test('respects isDisabled', function t(assert) { 31 | var isDisabledBool = false; 32 | var server = UDPServer({ port: PORT }, function onBound() { 33 | var client = new StatsdClient({ 34 | host: 'localhost', 35 | port: PORT, 36 | packetQueue: { flush: 10 }, 37 | isDisabled: function isDisabled() { 38 | return isDisabledBool; 39 | } 40 | }); 41 | 42 | server.once('message', onMessage); 43 | client.counter('foo', 1); 44 | 45 | function onMessage(msg) { 46 | assert.equal(msg.toString(), 'foo:1|c\n'); 47 | 48 | isDisabledBool = true; 49 | server.on('message', failure); 50 | client.counter('foo', 1); 51 | 52 | setTimeout(next, 100); 53 | 54 | function failure() { 55 | assert.ok(false, 'unexpected message'); 56 | } 57 | 58 | function next() { 59 | isDisabledBool = false; 60 | server.removeListener('message', failure); 61 | 62 | server.once('message', onMessage2); 63 | client.counter('foo', 1); 64 | } 65 | 66 | function onMessage2(msg) { 67 | assert.ok(String(msg)); 68 | 69 | end(); 70 | } 71 | } 72 | 73 | function end() { 74 | client.close(); 75 | server.close(); 76 | assert.end(); 77 | } 78 | }); 79 | }) 80 | 81 | test('can write timing to client', function t(assert) { 82 | var server = UDPServer({ port: PORT }, function onBound() { 83 | var client = new StatsdClient({ 84 | host: 'localhost', 85 | port: PORT, 86 | packetQueue: { flush: 10 } 87 | }); 88 | 89 | client.counter('foo', 1); 90 | server.once('message', function (msg) { 91 | assert.equal(msg.toString(), 'foo:1|c\n'); 92 | 93 | server.close(); 94 | client.close(); 95 | assert.end(); 96 | }); 97 | }); 98 | }); 99 | 100 | test('can write with prefix', function t(assert) { 101 | var server = UDPServer({ port: PORT }, function onBound() { 102 | var client = new StatsdClient({ 103 | prefix: 'bar', 104 | packetQueue: { flush: 10 } 105 | }); 106 | 107 | client.timing('foo', 42); 108 | server.once('message', function (msg) { 109 | assert.equal(msg.toString(), 'bar.foo:42|ms\n'); 110 | 111 | server.close(); 112 | client.close(); 113 | assert.end(); 114 | }); 115 | }); 116 | }) 117 | 118 | test('can write with prefix trailing dot', function t(assert) { 119 | var server = UDPServer({ port: PORT }, function onBound() { 120 | var client = new StatsdClient({ 121 | prefix: 'bar.', 122 | packetQueue: { flush: 10 } 123 | }); 124 | 125 | client.timing('foo', 42); 126 | server.once('message', function (msg) { 127 | assert.equal(msg.toString(), 'bar.foo:42|ms\n'); 128 | 129 | server.close(); 130 | client.close(); 131 | assert.end(); 132 | }); 133 | }); 134 | }); 135 | 136 | test('can write with child prefix', function t(assert) { 137 | var server = UDPServer({ port: PORT }, function onBound() { 138 | var client = new StatsdClient({ 139 | prefix: 'bar.', 140 | packetQueue: { flush: 10 } 141 | }); 142 | 143 | client = client.getChildClient('baz'); 144 | 145 | client.timing('foo', 42); 146 | server.once('message', function (msg) { 147 | assert.equal(msg.toString(), 'bar.baz.foo:42|ms\n'); 148 | 149 | server.close(); 150 | client.close(); 151 | assert.end(); 152 | }); 153 | }); 154 | }); 155 | 156 | 157 | test('can write counter to client', function t(assert) { 158 | var server = UDPServer({ port: PORT }, function onBound() { 159 | var client = new StatsdClient(); 160 | 161 | client.timing('foo', 42); 162 | client._ephemeralSocket._queue._sendPacket(); 163 | server.once('message', function (msg) { 164 | assert.equal(msg.toString(), 'foo:42|ms\n'); 165 | 166 | server.close(); 167 | client.close(); 168 | assert.end(); 169 | }); 170 | }); 171 | }); 172 | 173 | test('client.counter()', function t(assert) { 174 | var server = UDPServer({ port: PORT }, function onBound() { 175 | var sock = new StatsdClient({ 176 | host: 'localhost', 177 | port: PORT, 178 | packetQueue: { flush: 10 } 179 | }); 180 | 181 | server.once('message', onMessage); 182 | sock.counter('hello', 10); 183 | 184 | function onMessage(msg) { 185 | var str = String(msg); 186 | assert.equal(str, 'hello:10|c\n'); 187 | 188 | sock.close(); 189 | server.close(); 190 | assert.end(); 191 | } 192 | }); 193 | }); 194 | 195 | test('client.increment()', function t(assert) { 196 | var server = UDPServer({ port: PORT }, function onBound() { 197 | var sock = new StatsdClient({ 198 | host: 'localhost', 199 | port: PORT, 200 | packetQueue: { flush: 10 } 201 | }); 202 | 203 | server.once('message', onMessage); 204 | sock.increment('hello'); 205 | 206 | function onMessage(msg) { 207 | var str = String(msg); 208 | assert.equal(str, 'hello:1|c\n'); 209 | 210 | sock.close(); 211 | server.close(); 212 | assert.end(); 213 | } 214 | }); 215 | }); 216 | 217 | test('client.decrement()', function t(assert) { 218 | var server = UDPServer({ port: PORT }, function onBound() { 219 | var sock = new StatsdClient({ 220 | host: 'localhost', 221 | port: PORT, 222 | packetQueue: { flush: 10 } 223 | }); 224 | 225 | server.once('message', onMessage); 226 | sock.decrement('hello'); 227 | 228 | function onMessage(msg) { 229 | var str = String(msg); 230 | assert.equal(str, 'hello:-1|c\n'); 231 | 232 | sock.close(); 233 | server.close(); 234 | assert.end(); 235 | } 236 | }); 237 | }); 238 | 239 | test('client.gauge()', function t(assert) { 240 | var server = UDPServer({ port: PORT }, function onBound() { 241 | var sock = new StatsdClient({ 242 | host: 'localhost', 243 | port: PORT, 244 | packetQueue: { flush: 10 } 245 | }); 246 | 247 | server.once('message', onMessage); 248 | sock.gauge('hello', 10); 249 | 250 | function onMessage(msg) { 251 | var str = String(msg); 252 | assert.equal(str, 'hello:10|g'); 253 | 254 | sock.close(); 255 | server.close(); 256 | assert.end(); 257 | } 258 | }); 259 | }); 260 | 261 | test('client.immediateCounter()', function t(assert) { 262 | var server = UDPServer({ port: PORT }, function onBound() { 263 | var sock = new StatsdClient({ 264 | host: 'localhost', 265 | port: PORT 266 | }); 267 | 268 | var messageSent = false; 269 | server.once('message', onMessage); 270 | sock.immediateCounter('hello', 10, function onCompleteSending() { 271 | messageSent = true; 272 | }); 273 | 274 | function onMessage(msg) { 275 | var str = String(msg); 276 | assert.equal(str, 'hello:10|c'); 277 | assert.equal(messageSent, true); 278 | sock.close(); 279 | server.close(); 280 | assert.end(); 281 | } 282 | }); 283 | }); 284 | 285 | test('client.immediateIncrement()', function t(assert) { 286 | var server = UDPServer({ port: PORT }, function onBound() { 287 | var sock = new StatsdClient({ 288 | host: 'localhost', 289 | port: PORT 290 | }); 291 | 292 | var messageSent = false; 293 | server.once('message', onMessage); 294 | sock.immediateIncrement('hello', null, function onCompleteSending() { 295 | messageSent = true; 296 | }); 297 | 298 | function onMessage(msg) { 299 | var str = String(msg); 300 | assert.equal(str, 'hello:1|c'); 301 | assert.equal(messageSent, true); 302 | sock.close(); 303 | server.close(); 304 | assert.end(); 305 | } 306 | }); 307 | }); 308 | 309 | test('client.immediateDecrement()', function t(assert) { 310 | var server = UDPServer({ port: PORT }, function onBound() { 311 | var sock = new StatsdClient({ 312 | host: 'localhost', 313 | port: PORT 314 | }); 315 | 316 | var messageSent = false; 317 | server.once('message', onMessage); 318 | sock.immediateDecrement('hello', null, function onCompleteSending() { 319 | messageSent = true; 320 | }); 321 | 322 | function onMessage(msg) { 323 | var str = String(msg); 324 | assert.equal(str, 'hello:-1|c'); 325 | assert.equal(messageSent, true); 326 | sock.close(); 327 | server.close(); 328 | assert.end(); 329 | } 330 | }); 331 | }); 332 | 333 | test('client.immediateGauge()', function t(assert) { 334 | var server = UDPServer({ port: PORT }, function onBound() { 335 | var sock = new StatsdClient({ 336 | host: 'localhost', 337 | port: PORT 338 | }); 339 | 340 | var messageSent = false; 341 | server.once('message', onMessage); 342 | sock.immediateGauge('hello', 10, function onCompleteSending() { 343 | messageSent = true; 344 | }); 345 | 346 | function onMessage(msg) { 347 | var str = String(msg); 348 | assert.equal(str, 'hello:10|g'); 349 | assert.equal(messageSent, true); 350 | 351 | sock.close(); 352 | server.close(); 353 | assert.end(); 354 | } 355 | }); 356 | }) 357 | 358 | test('client.immediateTiming() with Date', function t(assert) { 359 | var server = UDPServer({ port: PORT }, function onBound() { 360 | var client = new StatsdClient({ 361 | prefix: 'bar' 362 | }); 363 | 364 | var messageSent = false; 365 | client.immediateTiming('foo', new Date(), function onCompleteSending() { 366 | messageSent = true; 367 | }); 368 | server.once('message', function onMessage(msg) { 369 | var msgStr = msg.toString(); 370 | assert.ok(msgStr === 'bar.foo:0|ms' || 371 | msgStr === 'bar.foo:1|ms'); 372 | assert.equal(messageSent, true); 373 | 374 | server.close(); 375 | client.close(); 376 | assert.end(); 377 | }); 378 | }); 379 | }) 380 | 381 | 382 | test('can write with DNS resolver', function t(assert) { 383 | var server = UDPServer({ port: PORT }, function onBound() { 384 | var client = new StatsdClient({ 385 | dnsResolver: {}, 386 | packetQueue: { flush: 10 } 387 | }); 388 | 389 | client.timing('foo', 42); 390 | server.once('message', function (msg) { 391 | assert.equal(msg.toString(), 'foo:42|ms\n'); 392 | 393 | server.close(); 394 | client.close(); 395 | assert.end(); 396 | }); 397 | }); 398 | }); 399 | 400 | test('dnsResolver only resolves once', function t(assert) { 401 | var counter = 0; 402 | 403 | var server = UDPServer({ port: PORT }, function onBound() { 404 | var client = new StatsdClient({ 405 | dnsResolver: { 406 | dns: { 407 | lookup: function (hostname, cb) { 408 | counter++; 409 | process.nextTick(function onTick() { 410 | cb(null, '127.0.0.1'); 411 | }); 412 | } 413 | } 414 | }, 415 | packetQueue: { flush: 10 } 416 | }); 417 | 418 | var client1 = client.getChildClient('p1'); 419 | var client2 = client.getChildClient('p2'); 420 | 421 | setTimeout(function fini() { 422 | 423 | client1.timing('foo', 42); 424 | client2.timing('foo', 43); 425 | server.once('message', function (msg) { 426 | assert.equal( 427 | msg.toString(), 428 | 'p1.foo:42|ms\np2.foo:43|ms\n' 429 | ); 430 | assert.equal(counter, 1); 431 | 432 | server.close(); 433 | client.close(); 434 | assert.end(); 435 | }); 436 | }, 50); 437 | }); 438 | }); 439 | 440 | test('client.timing() with Date', function t(assert) { 441 | var server = UDPServer({ port: PORT }, function onBound() { 442 | var client = new StatsdClient({ 443 | prefix: 'bar', 444 | packetQueue: { flush: 10 } 445 | }); 446 | 447 | client.timing('foo', new Date()); 448 | server.once('message', function (msg) { 449 | var msgStr = msg.toString(); 450 | assert.ok(msgStr === 'bar.foo:0|ms\n' || 451 | msgStr === 'bar.foo:1|ms\n'); 452 | 453 | server.close(); 454 | client.close(); 455 | assert.end(); 456 | }); 457 | }); 458 | }) 459 | --------------------------------------------------------------------------------