diff --git a/.editorconfig b/.editorconfig index 52efa0f..564bd83 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,8 +6,8 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 -indent_style = tab -tab_width = 2 +indent_style = space +tab_width = 4 [*.yml] indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..62295a8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": [ + "@fiverr/eslint-config-fiverr/rules/base", + "@fiverr/eslint-config-fiverr/rules/es6", + "@fiverr/eslint-config-fiverr/rules/mocha" + ] +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index c26cb5a..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,45 +0,0 @@ -module.exports = { - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - env: { - node: true, - es6: true, - }, - plugins: [ - 'log', - ], - extends: 'eslint:recommended', - rules: { - indent: [ - 'error', 'tab', { - SwitchCase: 1, - FunctionDeclaration: { - body: 1, - parameters: 2, - }, - }, - ], - semi: ['error', 'always'], - 'arrow-parens': [2, 'as-needed'], - 'comma-dangle': ['error', 'always-multiline'], - quotes: ['error', 'single'], - 'dot-location': ['error', 'property'], - 'dot-notation': 'error', - 'no-implicit-globals': 'error', - }, - overrides: [ - { - files: [ '**/spec.js' ], - env: { - mocha: true, - }, - globals: { - expect: true, - assert: true, - wait: true, - }, - }, - ], -}; diff --git a/.mocha.js b/.mocha.js index a6cb013..35a4041 100644 --- a/.mocha.js +++ b/.mocha.js @@ -3,11 +3,11 @@ chai.use(require('chai-string')); const wait = require('@lets/wait'); Object.assign( - global, - chai, - { - wait, - } + global, + chai, + { + wait + } ); -process.on('unhandledRejection', error => { throw error; }); +process.on('unhandledRejection', (error) => { throw error; }); diff --git a/README.md b/README.md index 091e1cd..e658887 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,13 @@ Exposes a client with the functions: `count`, `time`, `gauge`, `set`, `histogram | Argument | Type | Default | Meaning | - | - | - | - | metric | String | [Mandatory] | The metric name (key) -| value | Number|Date | 1 | The value to report (A date instance will send the time diff) +| value | Number|Date|BigInt | 1 | The value to report † | options.rate | Number | - | Sample rate - a fraction of 1 (.01 is one percent) | options.tags | Object | - | Key-value pairs of tags set as object literal +> † If `value` if a Date - instance will send the time diff (`Date.now()`) +> † If `value` if a BigInt - instance will send the time diff (`process.hrtime.bigint()`) **in milliseconds** with nanoseconds accuracy + #### Count ```js stats.count('some.counter'); // Increment by one. @@ -82,7 +85,16 @@ stats.count('some.counter', 10); // Increment by ten. #### Time ```js stats.time('some.timer', 200); // Send time value in milliseconds -stats.time('some.timer', date); // If you send a date instance - it'll report the time diff + +// Send date +const start = new Date(); +... +stats.time('some.timer', start); // instance will send the time diff (`Date.now()`) + +// Send high resolution time +const start = process.hrtime.bigint(); +... +stats.time('some.timer', start); // instance will send the time diff (`process.hrtime.bigint()`) in milliseconds with nanoseconds accuracy ``` #### Gauge diff --git a/index.js b/index.js index e1acd61..fe71ffc 100644 --- a/index.js +++ b/index.js @@ -5,15 +5,16 @@ const push = require('./lib/push'); const sender = require('./lib/sender'); const spread = require('./lib/spread'); const types = require('./lib/types'); + const TYPES_LIST = Object.keys(types); const TYPES = Object.freeze( - TYPES_LIST.reduce( - (accumulator, type) => Object.assign( - accumulator, - {[type]: type} - ), - {} - ) + TYPES_LIST.reduce( + (accumulator, type) => Object.assign( + accumulator, + {[type]: type} + ), + {} + ) ); /** @@ -36,7 +37,7 @@ const TYPES = Object.freeze( * @property {Function} histogram */ class SDC { - /** + /** * @static * @getter * @type {Object} @@ -46,11 +47,11 @@ class SDC { * @property {String} set 'set' * @property {String} histogram 'histogram' */ - static get TYPES() { - return TYPES; - } + static get TYPES() { + return TYPES; + } - /** + /** * SDC constructor * @param {String} [options.host='127.0.0.1'] * @param {String} [options.port=8125] @@ -65,52 +66,52 @@ class SDC { * @param {Function} [options.errorHandler] * @param {Boolean} [options.enforceRate=true] */ - constructor( - { - host = '127.0.0.1', - port = 8125, - protocol = 'UDP', - protocol_version = 'ipv4', - MTU = 576, - timeout = 1000, - tags, - scheme, - prefix, - sanitise, - errorHandler, - enforceRate = true, - } = {} - ) { - Object.assign( - this, - { - MTU, // Maximum Transmission Unit - timeout, - tags, - errorHandler, - enforceRate, - bulk: [], - timer: null, - send: sender({host, port, protocol, protocol_version, errorHandler, timeout}), - format: formatter({sanitise, prefix, scheme}), - flush: flush.bind(this), - } - ); + constructor( + { + host = '127.0.0.1', + port = 8125, + protocol = 'UDP', + protocol_version = 'ipv4', + MTU = 576, + timeout = 1000, + tags, + scheme, + prefix, + sanitise, + errorHandler, + enforceRate = true + } = {} + ) { + Object.assign( + this, + { + MTU, // Maximum Transmission Unit + timeout, + tags, + errorHandler, + enforceRate, + bulk: [], + timer: null, + send: sender({host, port, protocol, protocol_version, errorHandler, timeout}), + format: formatter({sanitise, prefix, scheme}), + flush: flush.bind(this) + } + ); - [...TYPES_LIST, 'generic'].forEach(fn => { - this[fn] = this[fn].bind(this); - }); - } + [...TYPES_LIST, 'generic'].forEach((fn) => { + this[fn] = this[fn].bind(this); + }); + } - /** + /** * The size of current bulk * @return {Number} */ - get size() { - return this.bulk.join('\n').length; - } + get size() { + return this.bulk.join('\n').length; + } - /** + /** * Generic metric send method * @param {String} type Metric type * @param {String} key The metric name (key) @@ -119,51 +120,52 @@ class SDC { * @param {Object} [options.tags] Key-value pairs of tags set as object literal * @return {Number} current size of the bulk */ - generic(...args) { - let [ - type, - key, - value = 1, - rate, - tags, - ] = spread(args); + generic(...args) { + let [ + type, // eslint-disable-line prefer-const + key, // eslint-disable-line prefer-const + value = 1, // eslint-disable-line prefer-const + rate, // eslint-disable-line prefer-const + tags + ] = spread(args); - if (rate) { - if (typeof rate !== 'number') { - throw new TypeError(`Expected 'rate' to be a number, instead got a ${typeof rate}`); - } - if (rate > 1) { - throw new TypeError(`Expected 'rate' to be a number between 0 and 1, instead got ${rate}`); - } + if (rate) { + if (typeof rate !== 'number') { + throw new TypeError(`Expected 'rate' to be a number, instead got a ${typeof rate}`); + } + if (rate > 1) { + throw new TypeError(`Expected 'rate' to be a number between 0 and 1, instead got ${rate}`); + } - if (this.enforceRate && !sample(rate)) { - return this.size; - } - } + if (this.enforceRate && !sample(rate)) { + return this.size; + } + } - if (this.tags) { - tags = Object.assign({}, this.tags, tags || {}); - } - return push.call( - this, - this.format( - type, - key, - value, - { rate, tags } - ) - ); - } + if (this.tags) { + tags = Object.assign({}, this.tags, tags || {}); + } + return push.call( + this, + this.format( + type, + key, + value, + { rate, tags } + ) + ); + } } Object.defineProperties( - SDC.prototype, - TYPES_LIST.reduce( - (accumulator, type) => Object.assign( - accumulator, - { - [type]: { - /** + SDC.prototype, + TYPES_LIST.reduce( + (accumulator, type) => Object.assign( + accumulator, + { + [type]: { + + /** * Specific metric type send method * @param {String} key The metric name (key) * @param {Number} [value] The value to report @@ -171,17 +173,17 @@ Object.defineProperties( * @param {Object} [options.tags] Key-value pairs of tags set as object literal * @return {Number} current size of the bulk */ - value: function(...args) { - return this.generic(type, ...args); - }, - configurable: true, - enumerable: true, - writable: true, - }, - } - ), - {} - ) + value: function(...args) { + return this.generic(type, ...args); + }, + configurable: true, + enumerable: true, + writable: true + } + } + ), + {} + ) ); module.exports = SDC; diff --git a/lib/flush/index.js b/lib/flush/index.js index 1e1c6f7..83e2e5b 100644 --- a/lib/flush/index.js +++ b/lib/flush/index.js @@ -3,8 +3,8 @@ * @return {undefined} */ module.exports = function flush() { - clearTimeout(this.timer); - this.timer = null; - this.send(this.bulk.join('\n')); - this.bulk.length = 0; + clearTimeout(this.timer); + this.timer = null; + this.send(this.bulk.join('\n')); + this.bulk.length = 0; }; diff --git a/lib/flush/spec.js b/lib/flush/spec.js index 41ddbad..1533e0f 100644 --- a/lib/flush/spec.js +++ b/lib/flush/spec.js @@ -1,42 +1,44 @@ +const wait = require('@lets/wait'); const flush = require('.'); + let result; const context = ({ - timer = 0, - bulk = [], - send = blob => { result = blob; }, + timer = 0, + bulk = [], + send = (blob) => { result = blob; } }) => ({ - timer, - bulk, - send, + timer, + bulk, + send }); describe('flush', () => { - it('Should clear instance timeout', async() => { - let called = false; - const instace = context({ - timer: setTimeout(() => { called = true; }, 4), - }); - flush.call(instace); - await wait(8); - expect(called).to.be.false; - }); - it('Should send the bulk (separated by newlines)', async() => { - const bulk = [1, 2, 3]; - const instace = context({bulk}); - flush.call(instace); - expect(result).to.equal('1\n2\n3'); - }); - it('Should empty the bulk after flushing', async() => { - const bulk = [1, 2, 3]; - const instace = context({bulk}); - flush.call(instace); - expect(bulk).to.have.lengthOf(0); - }); - it('Should reset instance timer', () => { - const bulk = [1, 2, 3]; - const instace = context({bulk}); - instace.timer = setTimeout(() => null, 0); - flush.call(instace); - expect(instace.timer).to.be.null; - }); + it('Should clear instance timeout', async() => { + let called = false; + const instace = context({ + timer: setTimeout(() => { called = true; }, 4) + }); + flush.call(instace); + await wait(8); + expect(called).to.be.false; + }); + it('Should send the bulk (separated by newlines)', async() => { + const bulk = [1, 2, 3]; + const instace = context({bulk}); + flush.call(instace); + expect(result).to.equal('1\n2\n3'); + }); + it('Should empty the bulk after flushing', async() => { + const bulk = [1, 2, 3]; + const instace = context({bulk}); + flush.call(instace); + expect(bulk).to.have.lengthOf(0); + }); + it('Should reset instance timer', () => { + const bulk = [1, 2, 3]; + const instace = context({bulk}); + instace.timer = setTimeout(() => null, 0); + flush.call(instace); + expect(instace.timer).to.be.null; + }); }); diff --git a/lib/formatter/index.js b/lib/formatter/index.js index 35cabbc..ec25c69 100644 --- a/lib/formatter/index.js +++ b/lib/formatter/index.js @@ -1,7 +1,8 @@ const isNumber = require('is-number'); const sanitiser = require('../sanitiser'); const types = require('../types'); -const letterLeading = string => /^[a-zA-Z]/.test(string); + +const letterLeading = (string) => /^[a-zA-Z]/.test(string); const schemes = require('../../schemes'); /** @@ -12,43 +13,43 @@ const schemes = require('../../schemes'); * @return {Function} */ module.exports = function formatter({prefix, sanitise = sanitiser, scheme = 'datadog'} = {}) { - if (prefix) { - if (typeof prefix !== 'string') { - throw new TypeError(`Expected 'prefix' to be a string, instead got a ${typeof prefix}`); - } - if (!letterLeading(prefix)) { - throw new Error(`Prefix must start with an alphabetical character (${prefix}).`); - } - } + if (prefix) { + if (typeof prefix !== 'string') { + throw new TypeError(`Expected 'prefix' to be a string, instead got a ${typeof prefix}`); + } + if (!letterLeading(prefix)) { + throw new Error(`Prefix must start with an alphabetical character (${prefix}).`); + } + } - // If "sanitise" is falsy, it should do nothing to the input - sanitise = sanitise || (string => string); - if (typeof sanitise !== 'function') { - throw new TypeError(`Expected 'sanitise' to be a function, instead got a ${typeof sanitise}`); - } + // If "sanitise" is falsy, it should do nothing to the input + sanitise = sanitise || ((string) => string); + if (typeof sanitise !== 'function') { + throw new TypeError(`Expected 'sanitise' to be a function, instead got a ${typeof sanitise}`); + } - if (typeof scheme === 'string') { - if (schemes.hasOwnProperty(scheme)) { - scheme = schemes[scheme]; - } else { - throw new Error(`Could not find scheme "${scheme}". Available schemes are ${Object.keys(schemes)}.`); - } - } + if (typeof scheme === 'string') { + if (scheme in schemes) { + scheme = schemes[scheme]; + } else { + throw new Error(`Could not find scheme "${scheme}". Available schemes are ${Object.keys(schemes)}.`); + } + } - if (typeof scheme !== 'function') { - throw new Error(`Requiring scheme function (${scheme}).`); - } + if (typeof scheme !== 'function') { + throw new Error(`Requiring scheme function (${scheme}).`); + } - const sanitiseKeys = object => Object.entries(object) - .reduce( - (accumulator, [key, value]) => Object.assign( - accumulator, - {[sanitise(key)]: sanitise(value)} - ), - {} - ); + const sanitiseKeys = (object) => Object.entries(object) + .reduce( + (accumulator, [key, value]) => Object.assign( + accumulator, + {[sanitise(key)]: sanitise(value)} + ), + {} + ); - /** + /** * Format a StatsD metric * @param {String} type The type of metric to report * @param {String} key The metric name (key) @@ -57,35 +58,38 @@ module.exports = function formatter({prefix, sanitise = sanitiser, scheme = 'dat * @param {Object} options.tags Key-value pairs of tags set as object literal * @return {String} Formatted StatsD metric */ - return function format(type = 'count', key, value = 1, {rate, tags} = {}) { - if (types.hasOwnProperty(type)) { - type = types[type]; - } else { - throw new RangeError(`Expected 'type' to be one of ${Object.keys(types).join(', ')}, instead got ${type}`); - } - if (typeof key !== 'string') { - throw new TypeError(`Expected 'key' to be a string, instead got a ${typeof key}`); - } - if (!prefix && !letterLeading(key)) { - throw new Error(`Expected 'key' to start with an alphabetical character (${key}).`); - } - if (value instanceof Date) { - value = new Date() - value; - } - if (typeof value !== 'number' || !isNumber(value)) { - throw new TypeError(`Expected 'value' to be a number, instead got a ${typeof value}`); - } + return function format(type = 'count', key, value = 1, {rate, tags} = {}) { + if (type in types) { + type = types[type]; + } else { + throw new RangeError(`Expected 'type' to be one of ${Object.keys(types).join(', ')}, instead got ${type}`); + } + if (typeof key !== 'string') { + throw new TypeError(`Expected 'key' to be a string, instead got a ${typeof key}`); + } + if (!prefix && !letterLeading(key)) { + throw new Error(`Expected 'key' to start with an alphabetical character (${key}).`); + } + if (value instanceof Date) { + value = new Date() - value; + } + if (typeof value === 'bigint') { + value = Number(process.hrtime.bigint() - value) / 1e6; + } + if (typeof value !== 'number' || !isNumber(value)) { + throw new TypeError(`Expected 'value' to be a number, instead got a ${typeof value}`); + } - if (prefix) { - key = [prefix, key].join('.'); - } + if (prefix) { + key = [prefix, key].join('.'); + } - key = sanitise(key); + key = sanitise(key); - if (tags) { - tags = sanitiseKeys(tags); - } + if (tags) { + tags = sanitiseKeys(tags); + } - return scheme({type, key, value, rate, tags}); - }; + return scheme({type, key, value, rate, tags}); + }; }; diff --git a/lib/formatter/spec.js b/lib/formatter/spec.js index d7d084c..b635e04 100644 --- a/lib/formatter/spec.js +++ b/lib/formatter/spec.js @@ -1,196 +1,211 @@ +const wait = require('@lets/wait'); + const stubs = {}; let formatter; function clean() { - delete require.cache[require.resolve('../../schemes/datadog')]; - delete require.cache[require.resolve('../../schemes/graphite')]; - delete require.cache[require.resolve('../../schemes')]; + delete require.cache[require.resolve('../../schemes/datadog')]; + delete require.cache[require.resolve('../../schemes/graphite')]; + delete require.cache[require.resolve('../../schemes')]; } describe('formatter', () => { - let format; - let schemas; - before(() => { - schemas = require('../../schemes'); - require.cache[require.resolve('../../schemes')].exports = { - datadog: (...args) => stubs.datadog(...args), - graphite: (...args) => stubs.graphite(...args), - }; - formatter = require('.'); - format = formatter(); - }); - beforeEach(() => Object.assign(stubs, schemas)); - after(clean); - it( - 'Should throw error when metric name is not a string', - () => [ - 1, - null, - {}, - [], - /\w/, - undefined, - ].forEach( - metric => expect(() => format(undefined, metric)).to.throw() - ) - ); - it( - 'Should throw error when metric value is not a number', - () => [ - '1', - NaN, - Infinity, - null, - {}, - [], - /\w/, - ].forEach( - value => expect(() => format(undefined, 'Hello', value)).to.throw() - ) - ); - it( - 'Should throw error when type is not one of pre defined types', - () => [ - 'counter', - 'Count', - 'timing', - 'Set', - null, - {}, - [], - /\w/, - ].forEach( - type => expect(() => format(type, 'Hello', undefined)).to.throw() - ) - ); - it( - 'Should default type to counter', - () => { - let t; - const format = formatter({ - scheme: ({type}) => { - t = type; - }, - }); - format(undefined, 'hello'); - expect(t).to.equal('c'); - } - ); - it( - 'Should default value to one', - () => expect(format(undefined, 'hello')).to.contain(':1|') - ); - [ - ['count', 'c'], - ['time', 'ms'], - ['gauge', 'g'], - ['set', 's'], - ['histogram', 'h'], - ].forEach( - ([full, symbol]) => { - it( - `Should map type ${full} to symbol ${symbol}`, - () => { - let t; - const format = formatter({ - scheme: ({type}) => { - t = type; - }, - }); + let format; + let schemas; + before(() => { + schemas = require('../../schemes'); + require.cache[require.resolve('../../schemes')].exports = { + datadog: (...args) => stubs.datadog(...args), + graphite: (...args) => stubs.graphite(...args) + }; + formatter = require('.'); + format = formatter(); + }); + beforeEach(() => Object.assign(stubs, schemas)); + after(clean); + it( + 'Should throw error when metric name is not a string', + () => [ + 1, + null, + {}, + [], + /\w/, + undefined + ].forEach( + (metric) => expect(() => format(undefined, metric)).to.throw() + ) + ); + it( + 'Should throw error when metric value is not a number', + () => [ + '1', + NaN, + Infinity, + null, + {}, + [], + /\w/ + ].forEach( + (value) => expect(() => format(undefined, 'Hello', value)).to.throw() + ) + ); + it( + 'Should throw error when type is not one of pre defined types', + () => [ + 'counter', + 'Count', + 'timing', + 'Set', + null, + {}, + [], + /\w/ + ].forEach( + (type) => expect(() => format(type, 'Hello', undefined)).to.throw() + ) + ); + it( + 'Should default type to counter', + () => { + let t; + const format = formatter({ + scheme: ({type}) => { + t = type; + } + }); + format(undefined, 'hello'); + expect(t).to.equal('c'); + } + ); + it( + 'Should default value to one', + () => expect(format(undefined, 'hello')).to.contain(':1|') + ); + [ + ['count', 'c'], + ['time', 'ms'], + ['gauge', 'g'], + ['set', 's'], + ['histogram', 'h'] + ].forEach( + ([full, symbol]) => { + it( + `Should map type ${full} to symbol ${symbol}`, + () => { + let t; + const format = formatter({ + scheme: ({type}) => { + t = type; + } + }); - t = null; - format(full, 'hello', undefined); - expect(t).to.equal(symbol); - } - ); - } - ); - it('Should pass variables to scheme method', () => { - let args; - const format = formatter({ - scheme: object => { - args = object; - }, - }); + t = null; + format(full, 'hello', undefined); + expect(t).to.equal(symbol); + } + ); + } + ); + it('Should pass variables to scheme method', () => { + let args; + const format = formatter({ + scheme: (object) => { + args = object; + } + }); - format('count', 'metric', 10, {rate: .1, tags: {a: 1, b: 2}}); - expect(args).to.deep.equal({ - type: 'c', - key: 'metric', - value: 10, - rate: .1, - tags: {a: '1', b: '2'}, - }); - }); - it('Should use a date in order to get time diff', async() => { - let time; - const format = formatter({scheme: ({value}) => { - time = value; - }}); - const date = new Date(); - await wait(10); - format('time', 'metric', date); - expect(time).to.be.a('number'); - expect(time).to.be.at.least(9); - }); - it('Should call datadog scheme module by default', () => { - let called = false; - const format = formatter(); - stubs.datadog = () => { called = true; }; - format('count', 'metric'); - expect(called).to.be.true; - }); - it('Should call custom scheme function', () => { - let called = false; - const format = formatter({scheme: () => { called = true; }}); - format('count', 'metric'); - expect(called).to.be.true; - }); - it('Should call datadog scheme module', () => { - let called = false; - const format = formatter({scheme: 'datadog'}); - stubs.datadog = () => { called = true; }; - format('count', 'metric'); - expect(called).to.be.true; - }); - it('Should call graphite scheme module', () => { - let called = false; - const format = formatter({scheme: 'graphite'}); - stubs.graphite = () => { called = true; }; - format('count', 'metric'); - expect(called).to.be.true; - }); - it('Should add a prefix', () => { - let metric; - const format = formatter({prefix: 'some_prefix', scheme: ({key}) => { - metric = key; - }}); - format('count', 'metric'); - expect(metric).to.equal('some_prefix.metric'); - format('time', 'something.else'); - expect(metric).to.equal('some_prefix.something.else'); - }); - it('Should sanitise tags\' keys', () => { - let keys; - const format = formatter({scheme: ({tags}) => { - keys = Object.keys(tags); - }}); - format('count', 'metric', 10, {tags: {'name ': 'SHlomo', Age: 4, 'ano@@er': 'det4$il'}}); - expect(keys).to.deep.equal(['name_', 'age', 'ano__er']); - }); - it('Should sanitise tags\' values', () => { - let values; - const format = formatter({scheme: ({tags}) => { - values = Object.values(tags); - }}); - format('count', 'metric', 10, {tags: {'name ': 'SHlomo', Age: 4, 'ano@@er': 'det4$il'}}); - expect(values).to.deep.equal(['shlomo', '4', 'det4_il']); - }); - it('Should sanitise the prefix as well', () => { - let metric; - stubs.datadog = ({key}) => { metric = key; }; - const format = formatter({prefix: 'some-prefix$'}); - format('count', 'metric'); - expect(metric).to.equal('some_prefix_.metric'); - }); + format('count', 'metric', 10, {rate: 0.1, tags: {a: 1, b: 2}}); + expect(args).to.deep.equal({ + type: 'c', + key: 'metric', + value: 10, + rate: 0.1, + tags: {a: '1', b: '2'} + }); + }); + it('Should use a date in order to get time diff', async() => { + let time; + const format = formatter({scheme: ({value}) => { + time = value; + }}); + const date = new Date(); + await wait(10); + format('time', 'metric', date); + expect(time).to.be.a('number'); + expect(time).to.be.at.least(9); + }); + it('Should use a BigInt inorder to get the time diff', async() => { + let time; + const format = formatter({scheme: ({value}) => { + time = value; + }}); + const start = process.hrtime.bigint(); + await wait(10); + format('time', 'metric', start); + expect(time).to.be.a('number'); + expect(time).to.be.at.least(9); + expect(time.toString()).to.include('.'); + expect(time).to.be.at.below(13); + }); + it('Should call datadog scheme module by default', () => { + let called = false; + const format = formatter(); + stubs.datadog = () => { called = true; }; + format('count', 'metric'); + expect(called).to.be.true; + }); + it('Should call custom scheme function', () => { + let called = false; + const format = formatter({scheme: () => { called = true; }}); + format('count', 'metric'); + expect(called).to.be.true; + }); + it('Should call datadog scheme module', () => { + let called = false; + const format = formatter({scheme: 'datadog'}); + stubs.datadog = () => { called = true; }; + format('count', 'metric'); + expect(called).to.be.true; + }); + it('Should call graphite scheme module', () => { + let called = false; + const format = formatter({scheme: 'graphite'}); + stubs.graphite = () => { called = true; }; + format('count', 'metric'); + expect(called).to.be.true; + }); + it('Should add a prefix', () => { + let metric; + const format = formatter({prefix: 'some_prefix', scheme: ({key}) => { + metric = key; + }}); + format('count', 'metric'); + expect(metric).to.equal('some_prefix.metric'); + format('time', 'something.else'); + expect(metric).to.equal('some_prefix.something.else'); + }); + it('Should sanitise tags\' keys', () => { + let keys; + const format = formatter({scheme: ({tags}) => { + keys = Object.keys(tags); + }}); + format('count', 'metric', 10, {tags: {'name ': 'SHlomo', Age: 4, 'ano@@er': 'det4$il'}}); + expect(keys).to.deep.equal(['name_', 'age', 'ano__er']); + }); + it('Should sanitise tags\' values', () => { + let values; + const format = formatter({scheme: ({tags}) => { + values = Object.values(tags); + }}); + format('count', 'metric', 10, {tags: {'name ': 'SHlomo', Age: 4, 'ano@@er': 'det4$il'}}); + expect(values).to.deep.equal(['shlomo', '4', 'det4_il']); + }); + it('Should sanitise the prefix as well', () => { + let metric; + stubs.datadog = ({key}) => { metric = key; }; + const format = formatter({prefix: 'some-prefix$'}); + format('count', 'metric'); + expect(metric).to.equal('some_prefix_.metric'); + }); }); diff --git a/lib/push/index.js b/lib/push/index.js index b0a24d4..b2915bb 100644 --- a/lib/push/index.js +++ b/lib/push/index.js @@ -4,26 +4,26 @@ * @return {Number} current size of the bulk */ module.exports = function push(entry) { - this.bulk.push(entry); + this.bulk.push(entry); - const {size} = this; + const {size} = this; - // Accumulated size does not surpass the MTU - if (size < this.MTU) { - this.timer = this.timer || setTimeout(this.flush, this.timeout); - return size; - } + // Accumulated size does not surpass the MTU + if (size < this.MTU) { + this.timer = this.timer || setTimeout(this.flush, this.timeout); + return size; + } - // The accumulated size matches the MTU exactly OR - // Metric fills the MTU on it's own - if (size === this.MTU || this.bulk.length === 1) { - this.flush(); - return 0; - } + // The accumulated size matches the MTU exactly OR + // Metric fills the MTU on it's own + if (size === this.MTU || this.bulk.length === 1) { + this.flush(); + return 0; + } - // Bulk is full, flush without current metric - this.bulk.pop(); - this.flush(); + // Bulk is full, flush without current metric + this.bulk.pop(); + this.flush(); - return push.call(this, entry); + return push.call(this, entry); }; diff --git a/lib/push/spec.js b/lib/push/spec.js index f865397..f83c855 100644 --- a/lib/push/spec.js +++ b/lib/push/spec.js @@ -1,4 +1,6 @@ +const wait = require('@lets/wait'); const push = require('.'); + let called = false; /** @@ -11,84 +13,84 @@ let called = false; * @return {Object} */ const context = ({ - bulk = [], - timer = 0, - flush = function() { - called = true; - this.bulk.length = 0; - }, - MTU = 576, - timeout = 1000, + bulk = [], + timer = 0, + flush = function() { + called = true; + this.bulk.length = 0; + }, + MTU = 576, + timeout = 1000 }) => { - const instance = { - bulk, - timer, - flush, - MTU, - timeout, - }; - instance.flush = instance.flush.bind(instance); - Object.defineProperty( - instance, 'size', { - get: function() { - return this.bulk.join('\n').length; - }, - } - ); - return instance; + const instance = { + bulk, + timer, + flush, + MTU, + timeout + }; + instance.flush = instance.flush.bind(instance); + Object.defineProperty( + instance, 'size', { + get: function() { + return this.bulk.join('\n').length; + } + } + ); + return instance; }; describe('push', () => { - beforeEach(() => { - called = false; - }); - it('Should not flush immediately when metric is smaller than MTU', () => { - push.call(context({MTU: 10}), '123456789'); - expect(called).to.be.false; - }); - it('Should flush immediately when metric is larger than MTU', () => { - push.call(context({MTU: 10}), '1234567890a'); - expect(called).to.be.true; - }); - it('Should flush when bulk size matches MTU', () => { - const that = context({MTU: 10}); - expect(that.bulk).to.have.lengthOf(0); - push.call(that, '12345'); - expect(called).to.be.false; - expect(that.bulk).to.have.lengthOf(1); - push.call(that, '1234'); - expect(called).to.be.true; - expect(that.bulk).to.have.lengthOf(0); - }); - it('Should flush when bulk size "will pass" MTU', () => { - const that = context({MTU: 10}); - expect(that.bulk).to.have.lengthOf(0); - push.call(that, '1234'); - expect(called).to.be.false; - expect(that.bulk).to.have.lengthOf(1); - push.call(that, '1234'); - expect(called).to.be.false; - expect(that.bulk).to.have.lengthOf(2); - push.call(that, '1234'); - expect(called).to.be.true; - expect(that.bulk).to.have.lengthOf(1); - }); - it('Should send metrics after timeout has ended', async() => { - const that = context({timeout: 10}); - expect(that.bulk).to.have.lengthOf(0); - push.call(that, '1234'); - expect(called).to.be.false; - expect(that.bulk).to.have.lengthOf(1); - await wait(5); - push.call(that, '1234'); - expect(called).to.be.false; - expect(that.bulk).to.have.lengthOf(2); - await wait(6); - expect(called).to.be.true; - expect(that.bulk).to.have.lengthOf(0); - called = false; - push.call(that, '1234'); - expect(called).to.be.false; - expect(that.bulk).to.have.lengthOf(1); - }); + beforeEach(() => { + called = false; + }); + it('Should not flush immediately when metric is smaller than MTU', () => { + push.call(context({MTU: 10}), '123456789'); + expect(called).to.be.false; + }); + it('Should flush immediately when metric is larger than MTU', () => { + push.call(context({MTU: 10}), '1234567890a'); + expect(called).to.be.true; + }); + it('Should flush when bulk size matches MTU', () => { + const ctx = context({MTU: 10}); + expect(ctx.bulk).to.have.lengthOf(0); + push.call(ctx, '12345'); + expect(called).to.be.false; + expect(ctx.bulk).to.have.lengthOf(1); + push.call(ctx, '1234'); + expect(called).to.be.true; + expect(ctx.bulk).to.have.lengthOf(0); + }); + it('Should flush when bulk size "will pass" MTU', () => { + const ctx = context({MTU: 10}); + expect(ctx.bulk).to.have.lengthOf(0); + push.call(ctx, '1234'); + expect(called).to.be.false; + expect(ctx.bulk).to.have.lengthOf(1); + push.call(ctx, '1234'); + expect(called).to.be.false; + expect(ctx.bulk).to.have.lengthOf(2); + push.call(ctx, '1234'); + expect(called).to.be.true; + expect(ctx.bulk).to.have.lengthOf(1); + }); + it('Should send metrics after timeout has ended', async() => { + const ctx = context({timeout: 10}); + expect(ctx.bulk).to.have.lengthOf(0); + push.call(ctx, '1234'); + expect(called).to.be.false; + expect(ctx.bulk).to.have.lengthOf(1); + await wait(5); + push.call(ctx, '1234'); + expect(called).to.be.false; + expect(ctx.bulk).to.have.lengthOf(2); + await wait(6); + expect(called).to.be.true; + expect(ctx.bulk).to.have.lengthOf(0); + called = false; // eslint-disable-line require-atomic-updates + push.call(ctx, '1234'); + expect(called).to.be.false; + expect(ctx.bulk).to.have.lengthOf(1); + }); }); diff --git a/lib/sanitiser/index.js b/lib/sanitiser/index.js index a814f06..0901563 100644 --- a/lib/sanitiser/index.js +++ b/lib/sanitiser/index.js @@ -3,4 +3,4 @@ * @param {String} string * @return {String} */ -module.exports = string => `${string}`.replace(/(?!\.)\W/g, '_').toLowerCase(); +module.exports = (string) => `${string}`.replace(/(?!\.)\W/g, '_').toLowerCase(); diff --git a/lib/sanitiser/spec.js b/lib/sanitiser/spec.js index deb05df..1d6bd54 100644 --- a/lib/sanitiser/spec.js +++ b/lib/sanitiser/spec.js @@ -1,31 +1,31 @@ const sanitiser = require('.'); describe('sanitiser', () => { - it( - 'Should convert non alphanumeric characters to underscores', - () => [ - '$', - '*', - '-', - ' ', - ].forEach(char => expect(sanitiser(char + char)).to.equal('__')) - ); - it( - 'Should lowercase input', - () => expect(sanitiser('HELLO')).to.equal('hello') - ); - it( - 'Should not change letters, numbers, underscores and dots', - () => [ - 'hello', - '1234', - '...', - '___', - 'abc_8ii.hello__', - ].forEach(char => expect(sanitiser(char + char)).to.equal(`${char}${char}`)) - ); - it( - 'Should convert characters that are not alphanumeric or dots to underscores, lowercase', - () => expect(sanitiser('abc$8ii.HEllo😜')).to.equal('abc_8ii.hello__') - ); + it( + 'Should convert non alphanumeric characters to underscores', + () => [ + '$', + '*', + '-', + ' ' + ].forEach((char) => expect(sanitiser(char + char)).to.equal('__')) + ); + it( + 'Should lowercase input', + () => expect(sanitiser('HELLO')).to.equal('hello') + ); + it( + 'Should not change letters, numbers, underscores and dots', + () => [ + 'hello', + '1234', + '...', + '___', + 'abc_8ii.hello__' + ].forEach((char) => expect(sanitiser(char + char)).to.equal(`${char}${char}`)) + ); + it( + 'Should convert characters that are not alphanumeric or dots to underscores, lowercase', + () => expect(sanitiser('abc$8ii.HEllo😜')).to.equal('abc_8ii.hello__') + ); }); diff --git a/lib/send-tcp/index.js b/lib/send-tcp/index.js index 2e7aaaa..1c5bba6 100644 --- a/lib/send-tcp/index.js +++ b/lib/send-tcp/index.js @@ -1,4 +1,5 @@ const { createConnection } = require('net'); + const ENCODING = 'ascii'; /** @@ -21,12 +22,12 @@ let timer = null; * @return {Socket} */ function getSocket(port, host, timeout) { - socket = socket || createConnection(port, host).setKeepAlive(true); + socket = socket || createConnection(port, host).setKeepAlive(true); - clearTimeout(timer); - timer = setTimeout(endSocket, timeout); + clearTimeout(timer); + timer = setTimeout(endSocket, timeout); - return socket; + return socket; } /** @@ -34,8 +35,8 @@ function getSocket(port, host, timeout) { * @return {undefined} */ function endSocket() { - socket && socket.destroy(); - socket = null; + socket && socket.destroy(); + socket = null; } @@ -54,12 +55,12 @@ function endSocket() { * @param {Function} [errorHandler] * @return {Send} */ -module.exports = (port, host, errorHandler, timeout) => data => getSocket(port, host, timeout) - .write( - Buffer.from(data), - ENCODING, - error => { - endSocket(); - error && errorHandler(error, data); - } - ); +module.exports = (port, host, errorHandler, timeout) => (data) => getSocket(port, host, timeout) + .write( + Buffer.from(data), + ENCODING, + (error) => { + endSocket(); + error && errorHandler(error, data); + } + ); diff --git a/lib/send-tcp/spec.js b/lib/send-tcp/spec.js index 44b74e2..11f6158 100644 --- a/lib/send-tcp/spec.js +++ b/lib/send-tcp/spec.js @@ -1,76 +1,78 @@ +const wait = require('@lets/wait'); + const defaults = [ - 1337, // port - '0.0.0.0', // host - null, // errorHandler - 40, // timeout + 1337, // port + '0.0.0.0', // host + null, // errorHandler + 40 // timeout ]; const args = {}; const called = {}; describe('send-tcp', () => { - const { createConnection } = require('net'); - let sendTCP; - beforeEach(() => { - delete require.cache[require.resolve('.')]; - Object.keys(args).forEach(key => { - args[key] = null; - }); + const { createConnection } = require('net'); + let sendTCP; + beforeEach(() => { + delete require.cache[require.resolve('.')]; + Object.keys(args).forEach((key) => { + args[key] = null; + }); - Object.keys(called).forEach(key => { - called[key] = false; - }); - require('net').createConnection = (..._args) => { - called.createConnection = true; - args.createConnection = _args; + Object.keys(called).forEach((key) => { + called[key] = false; + }); + require('net').createConnection = (..._args) => { + called.createConnection = true; + args.createConnection = _args; - return ['write', 'destroy', 'setKeepAlive'].reduce( - (accumulator, fn) => Object.assign(accumulator, { - [fn]: (..._args) => { - called[fn] = true; - args[fn] = _args; + return ['write', 'destroy', 'setKeepAlive'].reduce( + (accumulator, fn) => Object.assign(accumulator, { + [fn]: (..._args) => { + called[fn] = true; + args[fn] = _args; - return accumulator; - }, - }), - {} - ); - }; + return accumulator; + } + }), + {} + ); + }; - sendTCP = require('.'); - }); + sendTCP = require('.'); + }); - after(() => { - require('net').createConnection = createConnection; - }); + after(() => { + require('net').createConnection = createConnection; + }); - it('Should write a Buffer to a TCP socket', () => { - sendTCP(defaults)('hello'); - const [result] = args.write; - expect(result).to.be.an.instanceof(Buffer); - expect(result.toString()).to.equal('hello'); - }); - it('Should send consecutive messages to the same socket', () => { - const send = sendTCP(defaults); - send('message'); - expect(called.createConnection).to.be.true; - called.createConnection = false; - expect(called.write).to.be.true; - called.write = false; - send('message'); - expect(called.write).to.be.true; - expect(called.createConnection).to.be.false; - }); - it('Should close socket after timeout has passed', async() => { - const send = sendTCP(defaults); - send('message'); - expect(called.createConnection).to.be.true; - called.createConnection = false; - expect(called.write).to.be.true; - expect(called.createConnection).to.be.false; - called.write = false; - await wait(50); - send('message'); - expect(called.write).to.be.true; - expect(called.createConnection).to.be.true; - }); + it('Should write a Buffer to a TCP socket', () => { + sendTCP(defaults)('hello'); + const [result] = args.write; + expect(result).to.be.an.instanceof(Buffer); + expect(result.toString()).to.equal('hello'); + }); + it('Should send consecutive messages to the same socket', () => { + const send = sendTCP(defaults); + send('message'); + expect(called.createConnection).to.be.true; + called.createConnection = false; + expect(called.write).to.be.true; + called.write = false; + send('message'); + expect(called.write).to.be.true; + expect(called.createConnection).to.be.false; + }); + it('Should close socket after timeout has passed', async() => { + const send = sendTCP(defaults); + send('message'); + expect(called.createConnection).to.be.true; + called.createConnection = false; + expect(called.write).to.be.true; + expect(called.createConnection).to.be.false; + called.write = false; + await wait(50); + send('message'); + expect(called.write).to.be.true; + expect(called.createConnection).to.be.true; + }); }); diff --git a/lib/send-udp/index.js b/lib/send-udp/index.js index ccb9530..0386b62 100644 --- a/lib/send-udp/index.js +++ b/lib/send-udp/index.js @@ -19,12 +19,12 @@ let timer = null; * @return {Socket} */ function getSocket(sockettype, timeout) { - socket = socket || createSocket(sockettype); + socket = socket || createSocket(sockettype); - clearTimeout(timer); - timer = setTimeout(endSocket, timeout); + clearTimeout(timer); + timer = setTimeout(endSocket, timeout); - return socket; + return socket; } /** @@ -32,8 +32,8 @@ function getSocket(sockettype, timeout) { * @return {undefined} */ function endSocket() { - socket && socket.close(); - socket = null; + socket && socket.close(); + socket = null; } /** @@ -51,18 +51,18 @@ function endSocket() { * @param {Function} [errorHandler] * @return {Send} */ -module.exports = (port, host, sockettype, errorHandler, timeout) => data => { - const buffer = Buffer.from(data); +module.exports = (port, host, sockettype, errorHandler, timeout) => (data) => { + const buffer = Buffer.from(data); - getSocket(sockettype, timeout).send( - buffer, - 0, - buffer.length, - port, - host, - error => { - endSocket(); - error && errorHandler(error, data); - } - ); + getSocket(sockettype, timeout).send( + buffer, + 0, + buffer.length, + port, + host, + (error) => { + endSocket(); + error && errorHandler(error, data); + } + ); }; diff --git a/lib/send-udp/spec.js b/lib/send-udp/spec.js index 62a5552..3a30149 100644 --- a/lib/send-udp/spec.js +++ b/lib/send-udp/spec.js @@ -1,73 +1,75 @@ +const wait = require('@lets/wait'); + const defaults = [ - 1337, // port - '0.0.0.0', // host - 'udp4', // sockettype - null, // errorHandler - 40, // timeout + 1337, // port + '0.0.0.0', // host + 'udp4', // sockettype + null, // errorHandler + 40 // timeout ]; const args = {}; const called = {}; describe('send-udp', () => { - const {createSocket} = require('dgram'); - let sendUDP; - beforeEach(() => { - delete require.cache[require.resolve('.')]; - Object.keys(args).forEach(key => { - args[key] = null; - }); + const {createSocket} = require('dgram'); + let sendUDP; + beforeEach(() => { + delete require.cache[require.resolve('.')]; + Object.keys(args).forEach((key) => { + args[key] = null; + }); - Object.keys(called).forEach(key => { - called[key] = false; - }); + Object.keys(called).forEach((key) => { + called[key] = false; + }); - require('dgram').createSocket = (..._args) => { - called.createSocket = true; - args.createSocket = _args; + require('dgram').createSocket = (..._args) => { + called.createSocket = true; + args.createSocket = _args; - return ['send', 'close'].reduce( - (accumulator, fn) => Object.assign(accumulator, { - [fn]: (..._args) => { - called[fn] = true; - args[fn] = _args; - }, - }), - {} - ); - }; - sendUDP = require('.'); - }); - after(() => { - require('dgram').createSocket = createSocket; - }); - it('Should send a Buffer to a UDP socket', () => { - sendUDP(defaults)('hello'); - const [result] = args.send; - expect(result).to.be.an.instanceof(Buffer); - expect(result.toString()).to.equal('hello'); - }); - it('Should send consecutive messages to the same socket', () => { - const send = sendUDP(defaults); - send('message'); - expect(called.createSocket).to.be.true; - called.createSocket = false; - expect(called.send).to.be.true; - called.send = false; - send('message'); - expect(called.send).to.be.true; - expect(called.createSocket).to.be.false; - }); - it('Should close socket after timeout has passed', async() => { - const send = sendUDP(defaults); - send('message'); - expect(called.createSocket).to.be.true; - called.createSocket = false; - expect(called.send).to.be.true; - expect(called.createSocket).to.be.false; - called.send = false; - await wait(50); - send('message'); - expect(called.send).to.be.true; - expect(called.createSocket).to.be.true; - }); + return ['send', 'close'].reduce( + (accumulator, fn) => Object.assign(accumulator, { + [fn]: (..._args) => { + called[fn] = true; + args[fn] = _args; + } + }), + {} + ); + }; + sendUDP = require('.'); + }); + after(() => { + require('dgram').createSocket = createSocket; + }); + it('Should send a Buffer to a UDP socket', () => { + sendUDP(defaults)('hello'); + const [result] = args.send; + expect(result).to.be.an.instanceof(Buffer); + expect(result.toString()).to.equal('hello'); + }); + it('Should send consecutive messages to the same socket', () => { + const send = sendUDP(defaults); + send('message'); + expect(called.createSocket).to.be.true; + called.createSocket = false; + expect(called.send).to.be.true; + called.send = false; + send('message'); + expect(called.send).to.be.true; + expect(called.createSocket).to.be.false; + }); + it('Should close socket after timeout has passed', async() => { + const send = sendUDP(defaults); + send('message'); + expect(called.createSocket).to.be.true; + called.createSocket = false; + expect(called.send).to.be.true; + expect(called.createSocket).to.be.false; + called.send = false; + await wait(50); + send('message'); + expect(called.send).to.be.true; + expect(called.createSocket).to.be.true; + }); }); diff --git a/lib/sender/index.js b/lib/sender/index.js index bfd3f9b..94f1600 100644 --- a/lib/sender/index.js +++ b/lib/sender/index.js @@ -14,32 +14,32 @@ const PATTERN_PROTOCOL_VERSION = /ipv[4|6]/i; * @return {Function} send method */ module.exports = function sender({host, port, protocol, protocol_version, errorHandler, timeout} = {}) { - if (protocol) { - if (!PATTERN_PROTOCOL.test(protocol)) { - throw new Error(`Protocol must match ${PATTERN_PROTOCOL}. Instead got ${protocol}.`); - } - } - - errorHandler = typeof errorHandler === 'function' - ? errorHandler - : () => null + if (protocol) { + if (!PATTERN_PROTOCOL.test(protocol)) { + throw new Error(`Protocol must match ${PATTERN_PROTOCOL}. Instead got ${protocol}.`); + } + } + + errorHandler = typeof errorHandler === 'function' + ? errorHandler + : () => null + ; + + if ((/tcp/i).test(protocol)) { + return sendTCP(port, host, errorHandler, timeout); + } + + if (protocol_version) { + if (!PATTERN_PROTOCOL_VERSION.test(protocol_version)) { + throw new Error(`Protocol version must match ${PATTERN_PROTOCOL_VERSION}. Instead got ${protocol_version}.`); + } + } + + const sockettype = (/ipv6/i).test(protocol_version) + ? 'udp6' + : 'udp4' ; - if ((/tcp/i).test(protocol)) { - return sendTCP(port, host, errorHandler, timeout); - } - - if (protocol_version) { - if (!PATTERN_PROTOCOL_VERSION.test(protocol_version)) { - throw new Error(`Protocol version must match ${PATTERN_PROTOCOL_VERSION}. Instead got ${protocol_version}.`); - } - } - - const sockettype = (/ipv6/i).test(protocol_version) - ? 'udp6' - : 'udp4' - ; - - return sendUDP(port, host, sockettype, errorHandler, timeout); + return sendUDP(port, host, sockettype, errorHandler, timeout); }; diff --git a/lib/sender/spec.js b/lib/sender/spec.js index ecf2cc8..9e2cabc 100644 --- a/lib/sender/spec.js +++ b/lib/sender/spec.js @@ -4,65 +4,65 @@ const args = {}; const called = {}; function cleanup() { - delete require.cache[require.resolve('.')]; - delete require.cache[require.resolve('../send-udp')]; - delete require.cache[require.resolve('../send-tcp')]; + delete require.cache[require.resolve('.')]; + delete require.cache[require.resolve('../send-udp')]; + delete require.cache[require.resolve('../send-tcp')]; } describe('sender', () => { - before(cleanup); - beforeEach(() => { - cleanup(); - [ - 'send-tcp', - 'send-udp', - ].forEach(name => { - called[name] = false; - args[name] = null; + before(cleanup); + beforeEach(() => { + cleanup(); + [ + 'send-tcp', + 'send-udp' + ].forEach((name) => { + called[name] = false; + args[name] = null; - const route = `../${name}`; - require(route); - require.cache[require.resolve(route)].exports = (..._args) => { - called[name] = true; - args[name] = _args; - return data => { - called.send = true; - args.send = data; - }; - }; - }); - sender = require('.'); - }); - after(cleanup); - it('Should create a TCP sender', () => { - sender({protocol: 'tcp'}); - expect(called['send-tcp']).to.be.true; - }); - it('Should create a TCP sender with host and port', () => { - sender({host: '10.200.0.1', port: 2004, protocol: 'tcp'}); - const [port, host] = args['send-tcp']; - expect(host).to.equal('10.200.0.1'); - expect(port).to.equal(2004); - }); - [ - 'ipv6', - 'IPV6', - ].forEach(protocol_version => { - it(`Should create an IPv6 UDP socket when ${protocol_version} specified`, () => { - sender({protocol_version}); - const [, , sockettype] = args['send-udp']; - expect(sockettype).to.equal('udp6'); - }); - }); - [ - 'ipv4', - undefined, - null, - ].forEach(protocol_version => { - it(`Should create an IPv4 UDP socket when ${protocol_version} specified`, () => { - sender({protocol_version}); - const [, , sockettype] = args['send-udp']; - expect(sockettype).to.equal('udp4'); - }); - }); + const route = `../${name}`; + require(route); + require.cache[require.resolve(route)].exports = (..._args) => { + called[name] = true; + args[name] = _args; + return (data) => { + called.send = true; + args.send = data; + }; + }; + }); + sender = require('.'); + }); + after(cleanup); + it('Should create a TCP sender', () => { + sender({protocol: 'tcp'}); + expect(called['send-tcp']).to.be.true; + }); + it('Should create a TCP sender with host and port', () => { + sender({host: '10.200.0.1', port: 2004, protocol: 'tcp'}); + const [port, host] = args['send-tcp']; + expect(host).to.equal('10.200.0.1'); + expect(port).to.equal(2004); + }); + [ + 'ipv6', + 'IPV6' + ].forEach((protocol_version) => { + it(`Should create an IPv6 UDP socket when ${protocol_version} specified`, () => { + sender({protocol_version}); + const [, , sockettype] = args['send-udp']; + expect(sockettype).to.equal('udp6'); + }); + }); + [ + 'ipv4', + undefined, + null + ].forEach((protocol_version) => { + it(`Should create an IPv4 UDP socket when ${protocol_version} specified`, () => { + sender({protocol_version}); + const [, , sockettype] = args['send-udp']; + expect(sockettype).to.equal('udp4'); + }); + }); }); diff --git a/lib/spread/index.js b/lib/spread/index.js index be4034b..2687c27 100644 --- a/lib/spread/index.js +++ b/lib/spread/index.js @@ -12,14 +12,14 @@ * @example spread('count', 'metric', {rate: .1, tags: {...tags}}) ['count', 'metric', undefined, .1, {...tags}] */ module.exports = function spread(args) { - let type, key, value, rate, tags; - const lastArg = args.pop(); + let type, key, value, rate, tags; + const lastArg = args.pop(); - if (typeof lastArg === 'object') { - ({rate, tags} = lastArg); - ([type, key, value] = args); - } else { - ([type, key, value] = [...args, lastArg]); - } - return [type, key, value, rate, tags]; + if (typeof lastArg === 'object') { + ({rate, tags} = lastArg); + ([type, key, value] = args); + } else { + ([type, key, value] = [...args, lastArg]); + } + return [type, key, value, rate, tags]; }; diff --git a/lib/spread/spec.js b/lib/spread/spec.js index a5321ca..0168308 100644 --- a/lib/spread/spec.js +++ b/lib/spread/spec.js @@ -1,32 +1,32 @@ const spread = require('.'); describe('spread', () => { - it('Should extract args structure into an array', () => { - expect( - spread(['type', 'key', 'value', {rate: 'rate', tags: 'tags'}]) - ).to.deep.equal( - ['type', 'key', 'value', 'rate', 'tags'] - ); - }); - it('Should retrieve options', () => { - expect( - spread(['type', 'key', 'value']) - ).to.deep.equal( - ['type', 'key', 'value', undefined, undefined] - ); - }); - it('Should treat last arg as options', () => { - expect( - spread(['type', 'key', {rate: 'rate', tags: 'tags'}]) - ).to.deep.equal( - ['type', 'key', undefined, 'rate', 'tags'] - ); - }); - it('Should fill in everything else as undefined', () => { - expect( - spread(['type', 'key']) - ).to.deep.equal( - ['type', 'key', undefined, undefined, undefined] - ); - }); + it('Should extract args structure into an array', () => { + expect( + spread(['type', 'key', 'value', {rate: 'rate', tags: 'tags'}]) + ).to.deep.equal( + ['type', 'key', 'value', 'rate', 'tags'] + ); + }); + it('Should retrieve options', () => { + expect( + spread(['type', 'key', 'value']) + ).to.deep.equal( + ['type', 'key', 'value', undefined, undefined] + ); + }); + it('Should treat last arg as options', () => { + expect( + spread(['type', 'key', {rate: 'rate', tags: 'tags'}]) + ).to.deep.equal( + ['type', 'key', undefined, 'rate', 'tags'] + ); + }); + it('Should fill in everything else as undefined', () => { + expect( + spread(['type', 'key']) + ).to.deep.equal( + ['type', 'key', undefined, undefined, undefined] + ); + }); }); diff --git a/package.json b/package.json index 6404018..8d9c727 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fiverr/statsd-client", - "version": "0.2.1", + "version": "0.2.2", "description": "📈 A feature packed, highly customisable StatsD client", "keywords": [ "StatsD", @@ -26,18 +26,19 @@ "main": "index.js", "scripts": { "test": "mocha 'spec.js' '**/spec.js' --require .mocha", - "lint": "eslint '.*.js' '*.js' '**/*.js' -c .eslintrc.js" + "lint": "eslint '.*.js' '*.js' '**/*.js'" }, "dependencies": { "is-number": "^7.0.0", - "sample-size": "^1.0.0" + "sample-size": "^1.0.1" }, "devDependencies": { + "@fiverr/eslint-config-fiverr": "^3.1.2", "@lets/wait": "^1.0.0", "chai": "^4.2.0", "chai-string": "^1.5.0", - "eslint": "^5.16.0", - "eslint-plugin-log": "^1.0.0", - "mocha": "^6.1.2" + "eslint": "^6.1.0", + "eslint-plugin-log": "^1.2.3", + "mocha": "^6.2.0" } } diff --git a/schemes/datadog/index.js b/schemes/datadog/index.js index f3794f2..15609bd 100644 --- a/schemes/datadog/index.js +++ b/schemes/datadog/index.js @@ -6,20 +6,20 @@ const DELIMETER_TAGS_K_V = ':'; const DELIMETER_TAGS_LIST = ','; module.exports = function scheme({type, key, value, rate, tags}) { - const parts = [ - key, - DELIMETER_KEY_VALUE, - value, - DELIMETER_METRIC_TYPE, - type, - ]; + const parts = [ + key, + DELIMETER_KEY_VALUE, + value, + DELIMETER_METRIC_TYPE, + type + ]; - rate && parts.push(DELIMETER_RATE, rate); - tags && parts.push(DELIMETER_TAGS, tagsString(tags)); + rate && parts.push(DELIMETER_RATE, rate); + tags && parts.push(DELIMETER_TAGS, tagsString(tags)); - return parts.join(''); + return parts.join(''); }; -const tagsString = tags => Object.entries(tags).map( - ([key, value]) => [key, value].join(DELIMETER_TAGS_K_V) +const tagsString = (tags) => Object.entries(tags).map( + ([key, value]) => [key, value].join(DELIMETER_TAGS_K_V) ).join(DELIMETER_TAGS_LIST); diff --git a/schemes/datadog/spec.js b/schemes/datadog/spec.js index eb8db74..3a44190 100644 --- a/schemes/datadog/spec.js +++ b/schemes/datadog/spec.js @@ -1,42 +1,42 @@ const scheme = require('.'); describe('datadog scheme', () => { - it('Should format basic metrics', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - })).to.equal('k:b|t'); - }); - it('Should append formatted carbon tags after value', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - tags: { - a: 'A', - b: 'B', - }, - })).to.equal('k:b|t#a:A,b:B'); - }); - it('Should append rate after the value', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - rate: .1, - })).to.equal('k:b|t@0.1'); - }); - it('Should append formatted carbon tags after rate', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - rate: .1, - tags: { - a: 'A', - b: 'B', - }, - })).to.equal('k:b|t@0.1#a:A,b:B'); - }); + it('Should format basic metrics', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't' + })).to.equal('k:b|t'); + }); + it('Should append formatted carbon tags after value', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't', + tags: { + a: 'A', + b: 'B' + } + })).to.equal('k:b|t#a:A,b:B'); + }); + it('Should append rate after the value', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't', + rate: 0.1 + })).to.equal('k:b|t@0.1'); + }); + it('Should append formatted carbon tags after rate', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't', + rate: 0.1, + tags: { + a: 'A', + b: 'B' + } + })).to.equal('k:b|t@0.1#a:A,b:B'); + }); }); diff --git a/schemes/graphite/index.js b/schemes/graphite/index.js index cf84a89..11136ac 100644 --- a/schemes/graphite/index.js +++ b/schemes/graphite/index.js @@ -6,21 +6,21 @@ const DELIMETER_TAGS_K_V = '='; const DELIMETER_TAGS_LIST = ';'; module.exports = function scheme({type, key, value, rate, tags}) { - const parts = [key]; - tags && parts.push(DELIMETER_TAGS, tagsString(tags)); + const parts = [key]; + tags && parts.push(DELIMETER_TAGS, tagsString(tags)); - parts.push( - DELIMETER_KEY_VALUE, - value, - DELIMETER_METRIC_TYPE, - type - ); + parts.push( + DELIMETER_KEY_VALUE, + value, + DELIMETER_METRIC_TYPE, + type + ); - rate && parts.push(DELIMETER_RATE, rate); + rate && parts.push(DELIMETER_RATE, rate); - return parts.join(''); + return parts.join(''); }; -const tagsString = tags => Object.entries(tags).map( - ([key, value]) => [key, value].join(DELIMETER_TAGS_K_V) +const tagsString = (tags) => Object.entries(tags).map( + ([key, value]) => [key, value].join(DELIMETER_TAGS_K_V) ).join(DELIMETER_TAGS_LIST); diff --git a/schemes/graphite/spec.js b/schemes/graphite/spec.js index e96a809..9a7b5f3 100644 --- a/schemes/graphite/spec.js +++ b/schemes/graphite/spec.js @@ -1,30 +1,30 @@ const scheme = require('.'); describe('graphite scheme', () => { - it('Should format basic metrics', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - })).to.equal('k:b|t'); - }); - it('Should append formatted carbon tags before value', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - tags: { - a: 'A', - b: 'B', - }, - })).to.equal('k;a=A;b=B:b|t'); - }); - it('Should append rate after the value', () => { - expect(scheme({ - key: 'k', - value: 'b', - type: 't', - rate: .1, - })).to.equal('k:b|t@0.1'); - }); + it('Should format basic metrics', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't' + })).to.equal('k:b|t'); + }); + it('Should append formatted carbon tags before value', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't', + tags: { + a: 'A', + b: 'B' + } + })).to.equal('k;a=A;b=B:b|t'); + }); + it('Should append rate after the value', () => { + expect(scheme({ + key: 'k', + value: 'b', + type: 't', + rate: 0.1 + })).to.equal('k:b|t@0.1'); + }); }); diff --git a/schemes/index.js b/schemes/index.js index d79fc0b..ad5e84f 100644 --- a/schemes/index.js +++ b/schemes/index.js @@ -2,6 +2,6 @@ const datadog = require('./datadog'); const graphite = require('./graphite'); module.exports = { - datadog, - graphite, + datadog, + graphite }; diff --git a/spec.js b/spec.js index 66e2a8c..3f1f491 100644 --- a/spec.js +++ b/spec.js @@ -1,8 +1,8 @@ const dependencies = [ - 'formatter', - 'sender', - 'push', - 'flush', + 'formatter', + 'sender', + 'push', + 'flush' ]; const stubs = {}; const originals = {}; @@ -12,198 +12,198 @@ const contexts = {}; const called = {}; function piggyback(dependency, fn) { - stubs[dependency] = function(...args) { - const result = originals[dependency].apply(this, args); - results[dependency] = fn(...args); - contexts[dependency] = this; - parameters[dependency] = args; - called[dependency] = true; - return result; - }; + stubs[dependency] = function(...args) { + const result = originals[dependency].apply(this, args); + results[dependency] = fn(...args); + contexts[dependency] = this; + parameters[dependency] = args; + called[dependency] = true; + return result; + }; } describe('SDC', () => { - let SDC; - let {random} = Math; - before(() => { - require('sample-size'); - delete require.cache[require.resolve('.')]; - require.cache[require.resolve('sample-size')].exports = (...args) => stubs.sample(...args); - dependencies.forEach(dependency => { - const route = `./lib/${dependency}`; - originals[dependency] = require(route); - piggyback(dependency, () => null); - require.cache[require.resolve(route)].exports = function(...args) { - return stubs[dependency].apply(this, args); - }; - }); + let SDC; + const {random} = Math; + before(() => { + require('sample-size'); + delete require.cache[require.resolve('.')]; + require.cache[require.resolve('sample-size')].exports = (...args) => stubs.sample(...args); + dependencies.forEach((dependency) => { + const route = `./lib/${dependency}`; + originals[dependency] = require(route); + piggyback(dependency, () => null); + require.cache[require.resolve(route)].exports = function(...args) { + return stubs[dependency].apply(this, args); + }; + }); - SDC = require('.'); - }); - beforeEach(() => { - dependencies.forEach(dependency => { - piggyback(dependency, () => null); - stubs.sample = () => null; - delete results[dependency]; - delete contexts[dependency]; - delete parameters[dependency]; - delete called[dependency]; - }); - }); - afterEach(() => { - Math.random = random; - }); - after(() => { - dependencies.forEach(dependency => { - delete require.cache[require.resolve(`./lib/${dependency}`)]; - }); - delete require.cache[require.resolve('sample-size')]; - delete require.cache[require.resolve('.')]; - }); + SDC = require('.'); + }); + beforeEach(() => { + dependencies.forEach((dependency) => { + piggyback(dependency, () => null); + stubs.sample = () => null; + delete results[dependency]; + delete contexts[dependency]; + delete parameters[dependency]; + delete called[dependency]; + }); + }); + afterEach(() => { + Math.random = random; + }); + after(() => { + dependencies.forEach((dependency) => { + delete require.cache[require.resolve(`./lib/${dependency}`)]; + }); + delete require.cache[require.resolve('sample-size')]; + delete require.cache[require.resolve('.')]; + }); - it('Should have a static getter containing all send types', () => { - expect(SDC.TYPES).to.deep.equal({ - count: 'count', - time: 'time', - gauge: 'gauge', - set: 'set', - histogram: 'histogram', - }); - }); - it('Should not accept changes to the static types object (freeze)', () => { - const {TYPES} = SDC; - TYPES.counter = 'count'; - expect(TYPES.counter).to.be.undefined; - }); - it('Should have properties passed by constructor options', () => { - const options = { - MTU: 1234, - timeout: 1050, - timer: null, - }; - const client = new SDC(options); - expect(client).to.include({ - MTU: 1234, - timeout: 1050, - timer: null, - }); - }); - [ - 'count', - 'time', - 'gauge', - 'set', - 'histogram', - 'generic', - ].forEach( - method => it( - `Should expose method "${method}"`, - () => expect(new SDC()[method]).to.be.a('function') - ) - ); - it('Should have some (non prototype) functionality', () => { - const client = new SDC(); - expect(client.bulk).to.deep.equal([]); - expect(client.timer).to.equal(null); - expect(client.send).to.be.a('function'); - expect(client.format).to.be.a('function'); - expect(client.flush).to.be.a('function'); - }); - it('Should have specific metrics functions to call on generic function', () => { - const client = new SDC(); - const excepted = []; - const sent = ['A', 2, {key: 'balue'}]; - client.generic = (...args) => excepted.push(...args); - [ - 'count', - 'time', - 'gauge', - 'set', - 'histogram', - ].forEach(type => { - excepted.length = 0; - client[type](...sent); - expect(excepted).to.deep.equal([type, ...sent]); - }); - }); - it('Should assign default tags to metrics', () => { - const client = new SDC({tags: {environment: 'production'}}); - let _tags; - client.format = (type, key, value, {tags} = {}) => { - _tags = tags; - }; - client.generic('count', 'a'); - expect(_tags).to.include({environment: 'production'}); - }); - it('Should accept options as last argument', () => { - const client = new SDC(); - let _tags; - client.format = (type, key, value, {tags} = {}) => { - _tags = tags; - }; - client.generic('count', 'a', {tags: {environment: 'development'}}); - expect(_tags).to.include({environment: 'development'}); - }); - it('Should override default tags', () => { - const client = new SDC({tags: {environment: 'production'}}); - let _tags; - client.format = (type, key, value, {tags} = {}) => { - _tags = tags; - }; - client.generic('count', 'a', 1, {tags: {environment: 'development'}}); - expect(_tags).to.include({environment: 'development'}); - }); - it('Should push when sample is true', () => { - const client = new SDC(); - stubs.sample = () => true; - client.generic('count', 'a', 1, {rate: .4}); - expect(called.push).to.be.true; - }); - it('Should push when sample is false, but enforceRate is true', () => { - const client = new SDC({enforceRate: true}); - stubs.sample = () => true; - client.generic('count', 'a', 1, {rate: .4}); - expect(called.push).to.be.true; - }); - it('Should skip when sample is false', () => { - const client = new SDC(); - stubs.sample = () => false; - client.generic('count', 'a', 1, {rate: .4}); - expect(called.push).to.be.undefined; - }); - it('Should call push in context', () => { - const client = new SDC(); - client.generic('count', 'a'); - expect(contexts.push).to.equal(client); - }); - it('Should expose it\'s bulk size', () => { - const client = new SDC(); - const before = client.size; - client.generic('count', 'a'); - expect(client.size).to.be.above(before); - }); - it('Should return a number (bulk size)', () => { - const client = new SDC(); - const bulkSize = client.generic('count', 'a'); - expect(bulkSize).to.be.a('number'); - expect(bulkSize).to.equal(client.size); - }); - it('Should follow functional pipeline', () => { - const order = []; - const client = new SDC(); - piggyback('push', () => order.push('push')); - const format = client.format; - client.format = function(...args) { - order.push('format'); - return format.apply(this, args); - }; - client.generic('count', 'a'); - expect(order).to.deep.equal(['format', 'push']); - }); - it('Should push whatever is returned from format method', () => { - const client = new SDC(); - client.format = () => 'some string'; - client.generic('count', 'a'); - expect(parameters.push).to.deep.equal(['some string']); - }); + it('Should have a static getter containing all send types', () => { + expect(SDC.TYPES).to.deep.equal({ + count: 'count', + time: 'time', + gauge: 'gauge', + set: 'set', + histogram: 'histogram' + }); + }); + it('Should not accept changes to the static types object (freeze)', () => { + const {TYPES} = SDC; + TYPES.counter = 'count'; + expect(TYPES.counter).to.be.undefined; + }); + it('Should have properties passed by constructor options', () => { + const options = { + MTU: 1234, + timeout: 1050, + timer: null + }; + const client = new SDC(options); + expect(client).to.include({ + MTU: 1234, + timeout: 1050, + timer: null + }); + }); + [ + 'count', + 'time', + 'gauge', + 'set', + 'histogram', + 'generic' + ].forEach( + (method) => it( + `Should expose method "${method}"`, + () => expect(new SDC()[method]).to.be.a('function') + ) + ); + it('Should have some (non prototype) functionality', () => { + const client = new SDC(); + expect(client.bulk).to.deep.equal([]); + expect(client.timer).to.equal(null); + expect(client.send).to.be.a('function'); + expect(client.format).to.be.a('function'); + expect(client.flush).to.be.a('function'); + }); + it('Should have specific metrics functions to call on generic function', () => { + const client = new SDC(); + const excepted = []; + const sent = ['A', 2, {key: 'balue'}]; + client.generic = (...args) => excepted.push(...args); + [ + 'count', + 'time', + 'gauge', + 'set', + 'histogram' + ].forEach((type) => { + excepted.length = 0; + client[type](...sent); + expect(excepted).to.deep.equal([type, ...sent]); + }); + }); + it('Should assign default tags to metrics', () => { + const client = new SDC({tags: {environment: 'production'}}); + let _tags; + client.format = (type, key, value, {tags} = {}) => { + _tags = tags; + }; + client.generic('count', 'a'); + expect(_tags).to.include({environment: 'production'}); + }); + it('Should accept options as last argument', () => { + const client = new SDC(); + let _tags; + client.format = (type, key, value, {tags} = {}) => { + _tags = tags; + }; + client.generic('count', 'a', {tags: {environment: 'development'}}); + expect(_tags).to.include({environment: 'development'}); + }); + it('Should override default tags', () => { + const client = new SDC({tags: {environment: 'production'}}); + let _tags; + client.format = (type, key, value, {tags} = {}) => { + _tags = tags; + }; + client.generic('count', 'a', 1, {tags: {environment: 'development'}}); + expect(_tags).to.include({environment: 'development'}); + }); + it('Should push when sample is true', () => { + const client = new SDC(); + stubs.sample = () => true; + client.generic('count', 'a', 1, {rate: 0.4}); + expect(called.push).to.be.true; + }); + it('Should push when sample is false, but enforceRate is true', () => { + const client = new SDC({enforceRate: true}); + stubs.sample = () => true; + client.generic('count', 'a', 1, {rate: 0.4}); + expect(called.push).to.be.true; + }); + it('Should skip when sample is false', () => { + const client = new SDC(); + stubs.sample = () => false; + client.generic('count', 'a', 1, {rate: 0.4}); + expect(called.push).to.be.undefined; + }); + it('Should call push in context', () => { + const client = new SDC(); + client.generic('count', 'a'); + expect(contexts.push).to.equal(client); + }); + it('Should expose it\'s bulk size', () => { + const client = new SDC(); + const before = client.size; + client.generic('count', 'a'); + expect(client.size).to.be.above(before); + }); + it('Should return a number (bulk size)', () => { + const client = new SDC(); + const bulkSize = client.generic('count', 'a'); + expect(bulkSize).to.be.a('number'); + expect(bulkSize).to.equal(client.size); + }); + it('Should follow functional pipeline', () => { + const order = []; + const client = new SDC(); + piggyback('push', () => order.push('push')); + const format = client.format; + client.format = function(...args) { + order.push('format'); + return format.apply(this, args); + }; + client.generic('count', 'a'); + expect(order).to.deep.equal(['format', 'push']); + }); + it('Should push whatever is returned from format method', () => { + const client = new SDC(); + client.format = () => 'some string'; + client.generic('count', 'a'); + expect(parameters.push).to.deep.equal(['some string']); + }); }); diff --git a/test/spec.js b/test/spec.js index 9e35c62..38d95ad 100644 --- a/test/spec.js +++ b/test/spec.js @@ -1,144 +1,145 @@ const { resolve } = require('path'); +const wait = require('@lets/wait'); -let results = []; +const results = []; describe('Integration', () => { - let SDC; - before(() => { - cacheCleanup(); - require('../lib/sender'); - require.cache[require.resolve('../lib/sender')].exports = () => (...args) => results.push(...args); - SDC = require('..'); - }); - beforeEach(() => { - results.length = 0; - }); - after(cacheCleanup); - it('Should sanitise key', () => { - const client = new SDC({MTU: 0}); - client.count('Hello1-there$.'); - expect(results[0]).to.contain('hello1_there_.'); - }); - it('Should only fire when the bulk gets full', () => { - const client = new SDC({MTU: 100}); - client.count(new Array(46).join('a')); - expect(results).to.have.lengthOf(0); - client.count(new Array(46).join('a')); - expect(results).to.have.lengthOf(0); - client.count(new Array(2).join('a')); - expect(results).to.have.lengthOf(1); - }); - it('Should allow for no MTU by setting it to 0 (or anything lower that 5)', () => { - const client = new SDC({MTU: 0}); - expect(results).to.have.lengthOf(0); - client.count(new Array(2).join('a')); - expect(results).to.have.lengthOf(1); - }); - it('Should fire after ttl has expired', async() => { - const client = new SDC({timeout: 50}); - client.count(new Array(10).join('a')); - expect(results, 'Expected not to fire immediately').to.have.lengthOf(0); - await wait(21); - expect(results, 'Expected not to fire before timeout has expired').to.have.lengthOf(0); - await wait(32); - expect(results, 'Expected to fire once timeout has expired').to.have.lengthOf(1); - }); - it('Should maintain ttl after adding to bulk', async() => { - const client = new SDC({timeout: 50}); - client.count(new Array(10).join('a')); - expect(results, 'Expected not to fire immediately').to.have.lengthOf(0); - await wait(21); - client.count(new Array(10).join('a')); - expect(results, 'Expected not to fire before timeout has expired').to.have.lengthOf(0); - await wait(30); - expect(results, 'Expected to fire once timeout has expired').to.have.lengthOf(1); - }); - it('Should flush immaterially explicitly', async() => { - const client = new SDC({timeout: 50}); - client.count('a'); - client.flush(); - expect(results, 'Expected to fire immaterially').to.have.lengthOf(1); - }); + let SDC; + before(() => { + cacheCleanup(); + require('../lib/sender'); + require.cache[require.resolve('../lib/sender')].exports = () => (...args) => results.push(...args); + SDC = require('..'); + }); + beforeEach(() => { + results.length = 0; + }); + after(cacheCleanup); + it('Should sanitise key', () => { + const client = new SDC({MTU: 0}); + client.count('Hello1-there$.'); + expect(results[0]).to.contain('hello1_there_.'); + }); + it('Should only fire when the bulk gets full', () => { + const client = new SDC({MTU: 100}); + client.count(new Array(46).join('a')); + expect(results).to.have.lengthOf(0); + client.count(new Array(46).join('a')); + expect(results).to.have.lengthOf(0); + client.count(new Array(2).join('a')); + expect(results).to.have.lengthOf(1); + }); + it('Should allow for no MTU by setting it to 0 (or anything lower that 5)', () => { + const client = new SDC({MTU: 0}); + expect(results).to.have.lengthOf(0); + client.count(new Array(2).join('a')); + expect(results).to.have.lengthOf(1); + }); + it('Should fire after ttl has expired', async() => { + const client = new SDC({timeout: 50}); + client.count(new Array(10).join('a')); + expect(results, 'Expected not to fire immediately').to.have.lengthOf(0); + await wait(21); + expect(results, 'Expected not to fire before timeout has expired').to.have.lengthOf(0); + await wait(32); + expect(results, 'Expected to fire once timeout has expired').to.have.lengthOf(1); + }); + it('Should maintain ttl after adding to bulk', async() => { + const client = new SDC({timeout: 50}); + client.count(new Array(10).join('a')); + expect(results, 'Expected not to fire immediately').to.have.lengthOf(0); + await wait(21); + client.count(new Array(10).join('a')); + expect(results, 'Expected not to fire before timeout has expired').to.have.lengthOf(0); + await wait(30); + expect(results, 'Expected to fire once timeout has expired').to.have.lengthOf(1); + }); + it('Should flush immaterially explicitly', async() => { + const client = new SDC({timeout: 50}); + client.count('a'); + client.flush(); + expect(results, 'Expected to fire immaterially').to.have.lengthOf(1); + }); }); describe('Integration: bulk sending', () => { - const { Socket: { prototype: UDPsocket } } = require('dgram'); - const { Socket: { prototype: TCPsocket } } = require('net'); + const { Socket: { prototype: UDPsocket } } = require('dgram'); + const { Socket: { prototype: TCPsocket } } = require('net'); - const { send } = UDPsocket; - const { write } = TCPsocket; - function mock(fn) { - UDPsocket.send = function(...args) { - fn(...args); - }; - TCPsocket.write = function(...args) { - if (`${args[0]}`.startsWith(' ')) { - write.apply(this, args); // required for test suite to communicate - } else { - fn(...args); - } - }; - } - before(cacheCleanup); - beforeEach(() => { - mock(() => null); - }); - afterEach(() => { - UDPsocket.send = send; - TCPsocket.write = write; - }); - after(cacheCleanup); + const { send } = UDPsocket; + const { write } = TCPsocket; + function mock(fn) { + UDPsocket.send = function(...args) { + fn(...args); + }; + TCPsocket.write = function(...args) { + if (`${args[0]}`.startsWith(' ')) { + write.apply(this, args); // required for test suite to communicate + } else { + fn(...args); + } + }; + } + before(cacheCleanup); + beforeEach(() => { + mock(() => null); + }); + afterEach(() => { + UDPsocket.send = send; + TCPsocket.write = write; + }); + after(cacheCleanup); - [ - {protocol: 'TCP', port: 80}, - {protocol: 'UDP', port: 2003}, - ].forEach(({protocol, port}) => { - if (process.env.CI && protocol === 'TCP') { - it('Problems testing TCP connections on CI machines'); - return; - } + [ + {protocol: 'TCP', port: 8081}, + {protocol: 'UDP', port: 2003} + ].forEach(({protocol, port}) => { + if (process.env.CI && protocol === 'TCP') { + it('Problems testing TCP connections on CI machines'); + return; + } - it(`Should flush metrics to ${protocol} socket in bulk`, async() => { - let metrics; - mock(buffer => { - metrics = buffer; - }); - const SDC = require('..'); - const client = new SDC({protocol, port}); + it(`Should flush metrics to ${protocol} socket in bulk`, async() => { + let metrics; + mock((buffer) => { + metrics = buffer; + }); + const SDC = require('..'); + const client = new SDC({protocol, port}); - new Array(4).fill('a').forEach(client.count); - expect(metrics).to.be.undefined; + new Array(4).fill('a').forEach(client.count); + expect(metrics).to.be.undefined; - client.flush(); - expect(metrics).to.be.instanceof(Buffer); - expect(metrics.toString()).to.have.entriesCount('\n', 3); - }); + client.flush(); + expect(metrics).to.be.instanceof(Buffer); + expect(metrics.toString()).to.have.entriesCount('\n', 3); + }); - it(`Should send metrics to ${protocol} socket in bulk`, async() => { - let metrics; - mock(buffer => { - metrics = buffer; - }); - const SDC = require('..'); - const client = new SDC({protocol, port, timeout: 1}); + it(`Should send metrics to ${protocol} socket in bulk`, async() => { + let metrics; + mock((buffer) => { + metrics = buffer; + }); + const SDC = require('..'); + const client = new SDC({protocol, port, timeout: 1}); - new Array(4).fill('a').forEach(client.count); - expect(metrics).to.be.undefined; - await wait(5); - expect(metrics).to.be.instanceof(Buffer); - expect(metrics.toString()).to.have.entriesCount('\n', 3); - }); - }); + new Array(4).fill('a').forEach(client.count); + expect(metrics).to.be.undefined; + await wait(5); + expect(metrics).to.be.instanceof(Buffer); + expect(metrics.toString()).to.have.entriesCount('\n', 3); + }); + }); }); function cacheCleanup() { - delete require.cache[require.resolve('..')]; - const lib = resolve(__dirname, '..', 'lib'); + delete require.cache[require.resolve('..')]; + const lib = resolve(__dirname, '..', 'lib'); - Object.keys(require.cache) - .filter(key => key.startsWith(lib)) - .forEach(key => { - delete require.cache[key]; - }) - ; + Object.keys(require.cache) + .filter((key) => key.startsWith(lib)) + .forEach((key) => { + delete require.cache[key]; + }) + ; }