}
* @property {COUNTER} The type for Counters.
* @property {GAUGE} The type for Gauges.
* @property {HISTOGRAM} The type for Histograms.
* @property {METER} The type for Meters.
* @property {TIMER} The type for Timers.
*/
const MetricTypes = {
COUNTER: 'Counter',
GAUGE: 'Gauge',
HISTOGRAM: 'Histogram',
METER: 'Meter',
TIMER: 'Timer'
};
module.exports = {
MetricTypes
};
================================================
FILE: packages/measured-core/lib/metrics/NoOpMeter.js
================================================
const { MetricTypes } = require('./Metric');
/**
* A No-Op Impl of Meter that can be used with a timer, to only create histogram data.
* This is useful for some time series aggregators that can calculate rates for you just off of sent count.
*
* @implements {Metric}
* @example
* const { NoOpMeter, Timer } = require('measured')
* const meter = new NoOpMeter();
* const timer = new Timer({meter: meter});
* ...
* // do some stuff with the timer and stopwatch api
* ...
*/
// eslint-disable-next-line padded-blocks
class NoOpMeter {
/**
* No-Op impl
* @param {number} n Number of events to mark.
*/
// eslint-disable-next-line no-unused-vars
mark(n) {}
/**
* No-Op impl
*/
start() {}
/**
* No-Op impl
*/
end() {}
/**
* No-Op impl
*/
ref() {}
/**
* No-Op impl
*/
unref() {}
/**
* No-Op impl
*/
reset() {}
/**
* No-Op impl
*/
meanRate() {}
/**
* No-Op impl
*/
currentRate() {}
/**
* Returns an empty object
* @return {{}}
*/
toJSON() {
return {};
}
/**
* The type of the Metric Impl. {@link MetricTypes}.
* @return {string} The type of the Metric Impl.
*/
getType() {
return MetricTypes.METER;
}
}
module.exports = NoOpMeter;
================================================
FILE: packages/measured-core/lib/metrics/SettableGauge.js
================================================
const { MetricTypes } = require('./Metric');
/**
* Works like a {@link Gauge}, but rather than getting its value from a callback, the value
* is set when needed. This can be useful for setting a gauges value for asynchronous operations.
* @implements {Metric}
* @example
* const settableGauge = new SettableGauge();
* // Update the settable gauge ever 10'ish seconds
* setInterval(() => {
* calculateSomethingAsync().then((value) => {
* settableGauge.setValue(value);
* });
* }, 10000);
*/
class SettableGauge {
/**
* @param {SettableGaugeProperties} [options] See {@link SettableGaugeProperties}.
*/
constructor(options) {
options = options || {};
this._value = options.initialValue || 0;
}
setValue(value) {
this._value = value;
}
/**
* @return {number} Settable Gauges directly return there current value.
*/
toJSON() {
return this._value;
}
/**
* The type of the Metric Impl. {@link MetricTypes}.
* @return {string} The type of the Metric Impl.
*/
getType() {
return MetricTypes.GAUGE;
}
}
module.exports = SettableGauge;
/**
* Properties that can be supplied to the constructor of a {@link Counter}
*
* @interface SettableGaugeProperties
* @typedef SettableGaugeProperties
* @type {Object}
* @property {number} initialValue An initial value to use for this settable gauge. Defaults to 0.
* @example
* // Creates a Gauge that with an initial value of 500.
* const settableGauge = new SettableGauge({ initialValue: 500 })
*
*/
================================================
FILE: packages/measured-core/lib/metrics/Timer.js
================================================
const { MetricTypes } = require('./Metric');
const Histogram = require('./Histogram');
const Meter = require('./Meter');
const Stopwatch = require('../util/Stopwatch');
/**
*
* Timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events.
*
* Since they are frequently used for tracking how long certain things take, they expose an API for that: See example 1.
*
* But you can also use them as generic histograms that also track the rate of events: See example 2.
*
* @example
* var Measured = require('measured')
* var timer = new Measured.Timer();
* http.createServer(function(req, res) {
* var stopwatch = timer.start();
* req.on('end', function() {
* stopwatch.end();
* });
* });
*
*
* @example
* var Measured = require('measured')
* var timer = new Measured.Timer();
* http.createServer(function(req, res) {
* if (req.headers['content-length']) {
* timer.update(parseInt(req.headers['content-length'], 10));
* }
* });
*
* @implements {Metric}
*/
class Timer {
/**
* @param {TimerProperties} [properties] See {@link TimerProperties}.
*/
constructor(properties) {
properties = properties || {};
this._meter = properties.meter || new Meter({});
this._histogram = properties.histogram || new Histogram({});
this._getTime = properties.getTime;
this._keepAlive = !!properties.keepAlive;
if (!properties.keepAlive) {
this.unref();
}
}
/**
* @return {Stopwatch} Returns a Stopwatch that has been started.
*/
start() {
const self = this;
const watch = new Stopwatch({ getTime: this._getTime });
watch.once('end', elapsed => {
self.update(elapsed);
});
return watch;
}
/**
* Updates the internal histogram with value and marks one event on the internal meter.
* @param {number} value
*/
update(value) {
this._meter.mark();
this._histogram.update(value);
}
/**
* Resets all values. Timers initialized with custom options will be reset to the default settings.
*/
reset() {
this._meter.reset();
this._histogram.reset();
}
end() {
this._meter.end();
}
/**
* Refs the backing timer again. Idempotent.
*/
ref() {
this._meter.ref();
}
/**
* Unrefs the backing timer. The meter will not keep the event loop alive. Idempotent.
*/
unref() {
this._meter.unref();
}
/**
* toJSON output:
*
*
meter: See Meter#toJSON output docs above.
* histogram: See Histogram#toJSON output docs above.
*
* @return {any}
*/
toJSON() {
return {
meter: this._meter.toJSON(),
histogram: this._histogram.toJSON()
};
}
/**
* The type of the Metric Impl. {@link MetricTypes}.
* @return {string} The type of the Metric Impl.
*/
getType() {
return MetricTypes.TIMER;
}
}
module.exports = Timer;
/**
* @interface TimerProperties
* @typedef TimerProperties
* @type {Object}
* @property {Meter} meter The internal meter to use. Defaults to a new {@link Meter}.
* @property {Histogram} histogram The internal histogram to use. Defaults to a new {@link Histogram}.
* @property {function} getTime optional function override for supplying time to the {@link Stopwatch}
* @property {boolean} keepAlive Optional flag to unref the associated timer. Defaults to `false`.
*/
================================================
FILE: packages/measured-core/lib/util/BinaryHeap.js
================================================
/**
* Based on http://en.wikipedia.org/wiki/Binary_Heap
* as well as http://eloquentjavascript.net/appendix2.html
*/
class BinaryHeap {
constructor(options) {
options = options || {};
this._elements = options.elements || [];
this._score = options.score || this._score;
}
/**
* Add elements to the binary heap.
* @param {any[]} elements
*/
add(...elements) {
elements.forEach(element => {
this._elements.push(element);
this._bubble(this._elements.length - 1);
});
}
first() {
return this._elements[0];
}
removeFirst() {
const root = this._elements[0];
const last = this._elements.pop();
if (this._elements.length > 0) {
this._elements[0] = last;
this._sink(0);
}
return root;
}
clone() {
return new BinaryHeap({
elements: this.toArray(),
score: this._score
});
}
toSortedArray() {
const array = [];
const clone = this.clone();
let element;
while (true) {
element = clone.removeFirst();
if (element === undefined) {
break;
}
array.push(element);
}
return array;
}
toArray() {
return [].concat(this._elements);
}
size() {
return this._elements.length;
}
_bubble(bubbleIndex) {
const bubbleElement = this._elements[bubbleIndex];
const bubbleScore = this._score(bubbleElement);
let parentIndex;
let parentElement;
let parentScore;
while (bubbleIndex > 0) {
parentIndex = this._parentIndex(bubbleIndex);
parentElement = this._elements[parentIndex];
parentScore = this._score(parentElement);
if (bubbleScore <= parentScore) {
break;
}
this._elements[parentIndex] = bubbleElement;
this._elements[bubbleIndex] = parentElement;
bubbleIndex = parentIndex;
}
}
_sink(sinkIndex) {
const sinkElement = this._elements[sinkIndex];
const sinkScore = this._score(sinkElement);
const { length } = this._elements;
let swapIndex;
let swapScore;
let swapElement;
let childIndexes;
let i;
let childIndex;
let childElement;
let childScore;
while (true) {
swapIndex = null;
swapScore = null;
swapElement = null;
childIndexes = this._childIndexes(sinkIndex);
for (i = 0; i < childIndexes.length; i++) {
childIndex = childIndexes[i];
if (childIndex >= length) {
break;
}
childElement = this._elements[childIndex];
childScore = this._score(childElement);
if (childScore > sinkScore) {
if (swapScore === null || swapScore < childScore) {
swapIndex = childIndex;
swapScore = childScore;
swapElement = childElement;
}
}
}
if (swapIndex === null) {
break;
}
this._elements[swapIndex] = sinkElement;
this._elements[sinkIndex] = swapElement;
sinkIndex = swapIndex;
}
}
_parentIndex(index) {
return Math.floor((index - 1) / 2);
}
_childIndexes(index) {
return [2 * index + 1, 2 * index + 2];
}
_score(element) {
return element.valueOf();
}
}
module.exports = BinaryHeap;
================================================
FILE: packages/measured-core/lib/util/ExponentiallyDecayingSample.js
================================================
const BinaryHeap = require('./BinaryHeap');
const units = require('./units');
const RESCALE_INTERVAL = units.HOURS;
const ALPHA = 0.015;
const SIZE = 1028;
/**
* ExponentiallyDecayingSample
*/
class ExponentiallyDecayingSample {
constructor(options) {
options = options || {};
this._elements = new BinaryHeap({
score: element => -element.priority
});
this._rescaleInterval = options.rescaleInterval || RESCALE_INTERVAL;
this._alpha = options.alpha || ALPHA;
this._size = options.size || SIZE;
this._random = options.random || this._random;
this._landmark = null;
this._nextRescale = null;
}
update(value, timestamp) {
const now = Date.now();
if (!this._landmark) {
this._landmark = now;
this._nextRescale = this._landmark + this._rescaleInterval;
}
timestamp = timestamp || now;
const newSize = this._elements.size() + 1;
const element = {
priority: this._priority(timestamp - this._landmark),
value: value
};
if (newSize <= this._size) {
this._elements.add(element);
} else if (element.priority > this._elements.first().priority) {
this._elements.removeFirst();
this._elements.add(element);
}
if (now >= this._nextRescale) {
this._rescale(now);
}
}
toSortedArray() {
return this._elements.toSortedArray().map(element => element.value);
}
toArray() {
return this._elements.toArray().map(element => element.value);
}
toArrayWithWeights() {
return this._elements.toArray();
}
_weight(age) {
// We divide by 1000 to not run into huge numbers before reaching a
// rescale event.
return Math.exp(this._alpha * (age / 1000));
}
_priority(age) {
return this._weight(age) / this._random();
}
_random() {
return Math.random();
}
_rescale(now) {
now = now || Date.now();
const self = this;
const oldLandmark = this._landmark;
this._landmark = now || Date.now();
this._nextRescale = now + this._rescaleInterval;
const factor = self._priority(-(self._landmark - oldLandmark));
this._elements.toArray().forEach(element => {
element.priority *= factor;
});
}
}
module.exports = ExponentiallyDecayingSample;
================================================
FILE: packages/measured-core/lib/util/ExponentiallyMovingWeightedAverage.js
================================================
const units = require('./units');
const TICK_INTERVAL = 5 * units.SECONDS;
/**
* ExponentiallyMovingWeightedAverage
*/
class ExponentiallyMovingWeightedAverage {
constructor(timePeriod, tickInterval) {
this._timePeriod = timePeriod || units.MINUTE;
this._tickInterval = tickInterval || TICK_INTERVAL;
this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod);
this._count = 0;
this._rate = 0;
}
update(n) {
this._count += n;
}
tick() {
const instantRate = this._count / this._tickInterval;
this._count = 0;
this._rate += this._alpha * (instantRate - this._rate);
}
rate(timeUnit) {
return (this._rate || 0) * timeUnit;
}
}
module.exports = ExponentiallyMovingWeightedAverage;
================================================
FILE: packages/measured-core/lib/util/Stopwatch.js
================================================
const { EventEmitter } = require('events');
/**
* A simple object for tracking elapsed time
*
* @extends {EventEmitter}
*/
class Stopwatch extends EventEmitter {
/**
* Creates a started Stopwatch
* @param {StopwatchProperties} [options] See {@link StopwatchProperties}
*/
constructor(options) {
super();
options = options || {};
EventEmitter.call(this);
if (options.getTime) {
this._getTime = options.getTime;
}
this._start = this._getTime();
this._ended = false;
}
/**
* Called to mark the end of the timer task
* @return {number} the total execution time
*/
end() {
if (this._ended) {
return null;
}
this._ended = true;
const elapsed = this._getTime() - this._start;
this.emit('end', elapsed);
return elapsed;
}
_getTime() {
if (!process.hrtime) {
return Date.now();
}
const hrtime = process.hrtime();
return hrtime[0] * 1000 + hrtime[1] / (1000 * 1000);
}
}
module.exports = Stopwatch;
/**
* @interface StopwatchProperties
* @typedef StopwatchProperties
* @type {Object}
* @property {function} getTime optional function override for supplying time., defaults to new Date() / process.hrt()
*/
================================================
FILE: packages/measured-core/lib/util/units.js
================================================
const NANOSECONDS = 1 / (1000 * 1000);
const MICROSECONDS = 1 / 1000;
const MILLISECONDS = 1;
const SECONDS = 1000 * MILLISECONDS;
const MINUTES = 60 * SECONDS;
const HOURS = 60 * MINUTES;
const DAYS = 24 * HOURS;
/**
* Time units, as found in Java: {@link http://download.oracle.com/javase/6/docs/api/java/util/concurrent/TimeUnit.html}
* @module timeUnits
* @example
* const timeUnit = require('measured-core').unit
* setTimeout(() => {}, 5 * timeUnit.MINUTES)
*/
module.exports = {
/**
* nanoseconds in milliseconds
* @type {number}
*/
NANOSECONDS,
/**
* microseconds in milliseconds
* @type {number}
*/
MICROSECONDS,
/**
* milliseconds in milliseconds
* @type {number}
*/
MILLISECONDS,
/**
* seconds in milliseconds
* @type {number}
*/
SECONDS,
/**
* minutes in milliseconds
* @type {number}
*/
MINUTES,
/**
* hours in milliseconds
* @type {number}
*/
HOURS,
/**
* days in milliseconds
* @type {number}
*/
DAYS
};
================================================
FILE: packages/measured-core/lib/validators/metricValidators.js
================================================
const { MetricTypes } = require('../metrics/Metric');
// TODO: Object.values(...) does not exist in Node.js 6.x, switch after LTS period ends.
// const metricTypeValues = Object.values(MetricTypes);
const metricTypeValues = Object.keys(MetricTypes).map(key => MetricTypes[key]);
/**
* This module contains various validators to validate publicly exposed input.
*
* @module metricValidators
*/
module.exports = {
/**
* Validates that a metric implements the metric interface.
*
* @param {Metric} metric The object that is supposed to be a metric.
*/
validateMetric: metric => {
if (!metric) {
throw new TypeError('The metric was undefined, when it was required');
}
if (typeof metric.toJSON !== 'function') {
throw new TypeError('Metrics must implement toJSON(), see the Metric interface in the docs.');
}
if (typeof metric.getType !== 'function') {
throw new TypeError('Metrics must implement getType(), see the Metric interface in the docs.');
}
const type = metric.getType();
if (!metricTypeValues.includes(type)) {
throw new TypeError(
`Metric#getType(), must return a type defined in MetricsTypes. Found: ${type}, Valid values: ${metricTypeValues.join(
', '
)}`
);
}
}
};
================================================
FILE: packages/measured-core/package.json
================================================
{
"name": "measured-core",
"description": "A Node library for measuring and reporting application-level metrics.",
"version": "2.0.0",
"homepage": "https://yaorg.github.io/node-measured/",
"engines": {
"node": ">= 5.12"
},
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"scripts": {
"clean": "rm -fr build",
"format": "prettier --write './lib/**/*.{ts,js}'",
"lint": "eslint lib --ext .js",
"test:node": "mocha './test/**/test-*.js'",
"test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'",
"test:browser": "mochify './test/**/test-*.js'",
"test": "yarn test:node:coverage && yarn test:browser",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"dependencies": {
"binary-search": "^1.3.3",
"optional-js": "^2.0.0"
},
"repository": {
"url": "git://github.com/yaorg/node-measured.git"
},
"files": [
"lib",
"README.md"
],
"license": "MIT",
"devDependencies": {
"jsdoc": "^3.5.5"
}
}
================================================
FILE: packages/measured-core/test/common.js
================================================
'use strict';
/*
var common = exports;
var path = require('path');
common.dir = {};
common.dir.root = path.dirname(__dirname);
common.dir.lib = path.join(common.dir.root, 'lib');
common.measured = require(common.dir.root);
*/
exports.measured = require('../lib/index');
================================================
FILE: packages/measured-core/test/integration/test-Collection_end.js
================================================
'use strict';
var common = require('../common');
var collection = new common.measured.Collection();
collection.timer('a').start();
collection.meter('b').start();
collection.counter('c');
collection.end();
================================================
FILE: packages/measured-core/test/unit/metrics/test-CachedGauge.js
================================================
/*global describe, it, beforeEach, afterEach*/
const TimeUnits = require('../../../lib/util/units');
const CachedGauge = require('../../../lib/metrics/CachedGauge');
const assert = require('assert');
describe('CachedGauge', () => {
let cachedGauge;
it('A cachedGauge immediately calls the callback to set its initial value', () => {
cachedGauge = new CachedGauge(
() => {
return new Promise(resolve => {
resolve(10);
});
},
1,
TimeUnits.MINUTES
); // Shouldn't update in the unit test.
return wait(5 * TimeUnits.MILLISECONDS).then(() => {
assert.equal(cachedGauge.toJSON(), 10);
});
});
it('A cachedGauge calls the callback at the interval provided', () => {
const values = [1, 2];
cachedGauge = new CachedGauge(
() => {
return new Promise(resolve => {
resolve(values.shift());
});
},
5,
TimeUnits.MILLISECONDS
);
return wait(7 * TimeUnits.MILLISECONDS).then(() => {
assert.equal(cachedGauge.toJSON(), 2);
assert.equal(values.length, 0, 'the callback should have been called 2x, emptying the values array');
});
});
afterEach(() => {
if (cachedGauge) {
cachedGauge.end();
}
});
});
const wait = waitInterval => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, waitInterval);
});
};
================================================
FILE: packages/measured-core/test/unit/metrics/test-Counter.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var Counter = common.measured.Counter;
describe('Counter', function() {
var counter;
beforeEach(function() {
counter = new Counter();
});
it('has initial value of 0', function() {
var json = counter.toJSON();
assert.deepEqual(json, 0);
});
it('can be initialized with a given count', function() {
counter = new Counter({ count: 5 });
assert.equal(counter.toJSON(), 5);
});
it('#inc works incrementally', function() {
counter.inc(5);
assert.equal(counter.toJSON(), 5);
counter.inc(3);
assert.equal(counter.toJSON(), 8);
});
it('#inc defaults to 1', function() {
counter.inc();
assert.equal(counter.toJSON(), 1);
counter.inc();
assert.equal(counter.toJSON(), 2);
});
it('#inc adds zero', function() {
counter.inc(0);
assert.equal(counter.toJSON(), 0);
});
it('#dec works incrementally', function() {
counter.dec(3);
assert.equal(counter.toJSON(), -3);
counter.dec(2);
assert.equal(counter.toJSON(), -5);
});
it('#dec defaults to 1', function() {
counter.dec();
assert.equal(counter.toJSON(), -1);
counter.dec();
assert.equal(counter.toJSON(), -2);
});
it('#dec substracts zero', function() {
counter.dec(0);
assert.equal(counter.toJSON(), 0);
});
it('#reset works', function() {
counter.inc(23);
assert.equal(counter.toJSON(), 23);
counter.reset();
assert.equal(counter.toJSON(), 0);
counter.reset(50);
assert.equal(counter.toJSON(), 50);
});
it('returns the expected type', () => {
assert.equal(counter.getType(), 'Counter');
});
});
================================================
FILE: packages/measured-core/test/unit/metrics/test-Gauge.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
describe('Gauge', function() {
it('reads value from function', function() {
var i = 0;
var gauge = new common.measured.Gauge(function() {
return i++;
});
assert.equal(gauge.toJSON(), 0);
assert.equal(gauge.toJSON(), 1);
});
it('returns the expected type', () => {
const gauge = new common.measured.SettableGauge();
assert.equal(gauge.getType(), 'Gauge');
});
});
================================================
FILE: packages/measured-core/test/unit/metrics/test-Histogram.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var sinon = require('sinon');
var Histogram = common.measured.Histogram;
var EDS = common.measured.ExponentiallyDecayingSample;
describe('Histogram', function() {
var histogram;
beforeEach(function() {
histogram = new Histogram();
});
it('all values are null in the beginning', function() {
var json = histogram.toJSON();
assert.strictEqual(json.min, null);
assert.strictEqual(json.max, null);
assert.strictEqual(json.sum, 0);
assert.strictEqual(json.variance, null);
assert.strictEqual(json.mean, 0);
assert.strictEqual(json.stddev, null);
assert.strictEqual(json.count, 0);
assert.strictEqual(json.median, null);
assert.strictEqual(json.p75, null);
assert.strictEqual(json.p95, null);
assert.strictEqual(json.p99, null);
assert.strictEqual(json.p999, null);
});
it('returns the expected type', () => {
assert.equal(histogram.getType(), 'Histogram');
});
});
describe('Histogram#update', function() {
var sample;
var histogram;
beforeEach(function() {
sample = sinon.stub(new EDS());
histogram = new Histogram({ sample: sample });
sample.toArray.returns([]);
});
it('updates underlaying sample', function() {
histogram.update(5);
assert.ok(sample.update.calledWith(5));
});
it('keeps track of min', function() {
histogram.update(5);
histogram.update(3);
histogram.update(6);
assert.equal(histogram.toJSON().min, 3);
});
it('keeps track of max', function() {
histogram.update(5);
histogram.update(9);
histogram.update(3);
assert.equal(histogram.toJSON().max, 9);
});
it('keeps track of sum', function() {
histogram.update(5);
histogram.update(1);
histogram.update(12);
assert.equal(histogram.toJSON().sum, 18);
});
it('keeps track of count', function() {
histogram.update(5);
histogram.update(1);
histogram.update(12);
assert.equal(histogram.toJSON().count, 3);
});
it('keeps track of mean', function() {
histogram.update(5);
histogram.update(1);
histogram.update(12);
assert.equal(histogram.toJSON().mean, 6);
});
it('keeps track of variance (example without variance)', function() {
histogram.update(5);
histogram.update(5);
histogram.update(5);
assert.equal(histogram.toJSON().variance, 0);
});
it('keeps track of variance (example with variance)', function() {
histogram.update(1);
histogram.update(2);
histogram.update(3);
histogram.update(4);
assert.equal(histogram.toJSON().variance.toFixed(3), '1.667');
});
it('keeps track of stddev', function() {
histogram.update(1);
histogram.update(2);
histogram.update(3);
histogram.update(4);
assert.equal(histogram.toJSON().stddev.toFixed(3), '1.291');
});
it('keeps track of percentiles', function() {
var values = [];
var i;
for (i = 1; i <= 100; i++) {
values.push(i);
}
sample.toArray.returns(values);
var json = histogram.toJSON();
assert.equal(json.median.toFixed(3), '50.500');
assert.equal(json.p75.toFixed(3), '75.750');
assert.equal(json.p95.toFixed(3), '95.950');
assert.equal(json.p99.toFixed(3), '99.990');
assert.equal(json.p999.toFixed(3), '100.000');
});
});
describe('Histogram#percentiles', function() {
var sample;
var histogram;
beforeEach(function() {
sample = sinon.stub(new EDS());
histogram = new Histogram({ sample: sample });
var values = [];
var i;
for (i = 1; i <= 100; i++) {
values.push(i);
}
var swapWith;
var value;
for (i = 0; i < 100; i++) {
swapWith = Math.floor(Math.random() * 100);
value = values[i];
values[i] = values[swapWith];
values[swapWith] = value;
}
sample.toArray.returns(values);
});
it('calculates single percentile correctly', function() {
var percentiles = histogram._percentiles([0.5]);
assert.equal(percentiles[0.5], 50.5);
percentiles = histogram._percentiles([0.99]);
assert.equal(percentiles[0.99], 99.99);
});
});
describe('Histogram#weightedPercentiles', function() {
var sample;
var histogram;
beforeEach(function() {
sample = sinon.stub(new EDS());
histogram = new Histogram({
sample: sample,
percentilesMethod: Histogram.weightedPercentiles
});
var values = [];
var i;
for (i = 1; i <= 100; i++) {
values.push({ value: i, priority: 1 });
}
var swapWith;
var value;
for (i = 0; i < 100; i++) {
swapWith = Math.floor(Math.random() * 100);
value = values[i];
values[i] = values[swapWith];
values[swapWith] = value;
}
sample.toArrayWithWeights.returns(values);
sample.toArray.returns(
values.map(function(item) {
return item.value;
})
);
});
it('calculates single percentile correctly', function() {
var percentiles = histogram._percentiles([0.5]);
assert.equal(percentiles[0.5], 50.5);
percentiles = histogram._percentiles([0.99]);
assert.equal(percentiles[0.99], 99.99);
});
});
describe('Histogram#reset', function() {
var sample;
var histogram;
beforeEach(function() {
sample = new EDS();
histogram = new Histogram({ sample: sample });
});
it('resets all values', function() {
histogram.update(5);
histogram.update(2);
var json = histogram.toJSON();
var key;
for (key in json) {
if (json.hasOwnProperty(key)) {
assert.ok(typeof json[key] === 'number');
}
}
histogram.reset();
json = histogram.toJSON();
for (key in json) {
if (json.hasOwnProperty(key)) {
assert.ok(json[key] === 0 || json[key] === null);
}
}
});
});
describe('Histogram#hasValues', function() {
var histogram;
beforeEach(function() {
histogram = new Histogram();
});
it('has values', function() {
histogram.update(5);
assert.ok(histogram.hasValues());
});
it('has no values', function() {
assert.equal(histogram.hasValues(), false);
});
});
================================================
FILE: packages/measured-core/test/unit/metrics/test-Meter.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var sinon = require('sinon');
var units = common.measured.units;
describe('Meter', function() {
var meter;
var clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
meter = new common.measured.Meter({
getTime: function() {
return new Date().getTime();
}
});
});
afterEach(function() {
clock.restore();
});
it('all values are correctly initialized', function() {
assert.deepEqual(meter.toJSON(), {
mean: 0,
count: 0,
currentRate: 0,
'1MinuteRate': 0,
'5MinuteRate': 0,
'15MinuteRate': 0
});
});
it('supports rates override from opts', function() {
var rate = sinon.stub().returns(666);
var properties = {
m1Rate: { rate: rate },
m5Rate: { rate: rate },
m15Rate: { rate: rate }
};
var json = new common.measured.Meter(properties).toJSON();
assert.equal(json['1MinuteRate'].toFixed(0), '666');
assert.equal(json['5MinuteRate'].toFixed(0), '666');
assert.equal(json['15MinuteRate'].toFixed(0), '666');
});
it('decay over two marks and ticks', function() {
meter.mark(5);
meter._tick();
var json = meter.toJSON();
assert.equal(json.count, 5);
assert.equal(json['1MinuteRate'].toFixed(4), '0.0800');
assert.equal(json['5MinuteRate'].toFixed(4), '0.0165');
assert.equal(json['15MinuteRate'].toFixed(4), '0.0055');
meter.mark(10);
meter._tick();
json = meter.toJSON();
assert.equal(json.count, 15);
assert.equal(json['1MinuteRate'].toFixed(3), '0.233');
assert.equal(json['5MinuteRate'].toFixed(3), '0.049');
assert.equal(json['15MinuteRate'].toFixed(3), '0.017');
});
it('mean rate', function() {
meter.mark(5);
clock.tick(5000);
var json = meter.toJSON();
assert.equal(json.mean, 1);
clock.tick(5000);
json = meter.toJSON();
assert.equal(json.mean, 0.5);
});
it('currentRate is the observed rate since the last toJSON call', function() {
meter.mark(1);
meter.mark(2);
meter.mark(3);
clock.tick(3000);
assert.equal(meter.toJSON().currentRate, 2);
});
it('currentRate resets by reading it', function() {
meter.mark(1);
meter.mark(2);
meter.mark(3);
meter.toJSON();
assert.strictEqual(meter.toJSON().currentRate, 0);
});
it('currentRate also resets internal duration timer by reading it', function() {
meter.mark(1);
meter.mark(2);
meter.mark(3);
clock.tick(1000);
meter.toJSON();
clock.tick(1000);
meter.toJSON();
meter.mark(1);
clock.tick(1000);
assert.strictEqual(meter.toJSON().currentRate, 1);
});
it('#reset resets all values', function() {
meter.mark(1);
var json = meter.toJSON();
var key, value;
for (key in json) {
if (json.hasOwnProperty(key)) {
value = json[key];
assert.ok(typeof value === 'number');
}
}
meter.reset();
json = meter.toJSON();
for (key in json) {
if (json.hasOwnProperty(key)) {
value = json[key];
assert.ok(value === 0 || value === null);
}
}
});
it('returns the expected type', () => {
assert.equal(meter.getType(), 'Meter');
});
});
================================================
FILE: packages/measured-core/test/unit/metrics/test-NoOpMeter.js
================================================
/*global describe, it, beforeEach, afterEach*/
var common = require('../../common');
var assert = require('assert');
describe('NoOpMeter', () => {
let meter;
beforeEach(() => {
meter = new common.measured.NoOpMeter();
});
it('always returns empty object', () => {
assert.deepEqual(meter.toJSON(), {});
});
it('returns the expected type', () => {
assert.equal(meter.getType(), 'Meter');
});
});
================================================
FILE: packages/measured-core/test/unit/metrics/test-SettableGauge.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
describe('SettableGauge', function() {
it('can be set with an initial value', () => {
const gauge = new common.measured.SettableGauge({ initialValue: 5 });
assert.equal(gauge.toJSON(), 5);
gauge.setValue(11);
assert.equal(gauge.toJSON(), 11);
});
it('reads value from internal state', () => {
const gauge = new common.measured.SettableGauge();
assert.equal(gauge.toJSON(), 0);
gauge.setValue(5);
assert.equal(gauge.toJSON(), 5);
});
it('returns the expected type', () => {
const gauge = new common.measured.SettableGauge();
assert.equal(gauge.getType(), 'Gauge');
});
});
================================================
FILE: packages/measured-core/test/unit/metrics/test-Timer.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var sinon = require('sinon');
var Timer = common.measured.Timer;
var Histogram = common.measured.Histogram;
var Meter = common.measured.Meter;
describe('Timer', function() {
var timer;
var meter;
var histogram;
var clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
meter = sinon.stub(new Meter());
histogram = sinon.stub(new Histogram());
timer = new Timer({
meter: meter,
histogram: histogram,
getTime: function() {
return new Date().getTime();
}
});
});
afterEach(function() {
clock.restore();
});
it('can be initialized without options', function() {
timer = new Timer();
});
it('#update() marks the meter', function() {
timer.update(5);
assert.ok(meter.mark.calledOnce);
});
it('#update() updates the histogram', function() {
timer.update(5);
assert.ok(histogram.update.calledWith(5));
});
it('#toJSON() contains meter info', function() {
meter.toJSON.returns({ a: 1, b: 2 });
var json = timer.toJSON();
assert.deepEqual(json.meter, { a: 1, b: 2 });
});
it('#toJSON() contains histogram info', function() {
histogram.toJSON.returns({ c: 3, d: 4 });
var json = timer.toJSON();
assert.deepEqual(json.histogram, { c: 3, d: 4 });
});
it('#start returns a Stopwatch which updates the timer', function() {
clock.tick(10);
var watch = timer.start();
clock.tick(50);
watch.end();
assert.ok(meter.mark.calledOnce);
assert.equal(histogram.update.args[0][0], 50);
});
it('#reset is delegated to histogram and meter', function() {
timer.reset();
assert.ok(meter.reset.calledOnce);
assert.ok(histogram.reset.calledOnce);
});
it('returns the expected type', () => {
assert.equal(timer.getType(), 'Timer');
});
});
================================================
FILE: packages/measured-core/test/unit/test-Collection.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../common');
var assert = require('assert');
describe('Collection', function() {
var collection;
beforeEach(function() {
collection = common.measured.createCollection();
});
it('with two counters', function() {
collection = new common.measured.Collection('counters');
var a = collection.counter('a');
var b = collection.counter('b');
a.inc(3);
b.inc(5);
assert.deepEqual(collection.toJSON(), {
counters: {
a: 3,
b: 5
}
});
});
it('returns same metric object when given the same name', function() {
var a1 = collection.counter('a');
var a2 = collection.counter('a');
assert.strictEqual(a1, a2);
});
it('throws exception when creating a metric without name', function() {
assert.throws(function() {
collection.counter();
}, /You must supply a metric name/);
});
});
================================================
FILE: packages/measured-core/test/unit/util/test-BinaryHeap.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var BinaryHeap = common.measured.BinaryHeap;
describe('BinaryHeap#toArray', function() {
it('is empty in the beginning', function() {
var heap = new BinaryHeap();
assert.deepEqual(heap.toArray(), []);
});
it('does not leak internal references', function() {
var heap = new BinaryHeap();
var array = heap.toArray();
array.push(1);
assert.deepEqual(heap.toArray(), []);
});
});
describe('BinaryHeap#toSortedArray', function() {
it('is empty in the beginning', function() {
var heap = new BinaryHeap();
assert.deepEqual(heap.toSortedArray(), []);
});
it('does not leak internal references', function() {
var heap = new BinaryHeap();
var array = heap.toSortedArray();
array.push(1);
assert.deepEqual(heap.toSortedArray(), []);
});
it('returns a sorted array', function() {
var heap = new BinaryHeap();
heap.add(1, 2, 3, 4, 5, 6, 7, 8);
assert.deepEqual(heap.toSortedArray(), [8, 7, 6, 5, 4, 3, 2, 1]);
});
});
describe('BinaryHeap#add', function() {
var heap;
beforeEach(function() {
heap = new BinaryHeap();
});
it('lets you add one element', function() {
heap.add(1);
assert.deepEqual(heap.toArray(), [1]);
});
it('lets you add two elements', function() {
heap.add(1);
heap.add(2);
assert.deepEqual(heap.toArray(), [2, 1]);
});
it('lets you add two elements at once', function() {
heap.add(1, 2);
assert.deepEqual(heap.toArray(), [2, 1]);
});
it('places elements according to their valueOf()', function() {
heap.add(2);
heap.add(1);
heap.add(3);
assert.deepEqual(heap.toArray(), [3, 1, 2]);
});
});
describe('BinaryHeap#removeFirst', function() {
var heap;
beforeEach(function() {
heap = new BinaryHeap();
heap.add(1, 2, 3, 4, 5, 6, 7, 8);
});
it('removeFirst returns the last element', function() {
var element = heap.removeFirst();
assert.equal(element, 8);
});
it('removeFirst removes the last element', function() {
heap.removeFirst();
assert.equal(heap.toArray().length, 7);
});
it('removeFirst works multiple times', function() {
assert.equal(heap.removeFirst(), 8);
assert.equal(heap.removeFirst(), 7);
assert.equal(heap.removeFirst(), 6);
assert.equal(heap.removeFirst(), 5);
assert.equal(heap.removeFirst(), 4);
assert.equal(heap.removeFirst(), 3);
assert.equal(heap.removeFirst(), 2);
assert.equal(heap.removeFirst(), 1);
assert.equal(heap.removeFirst(), undefined);
});
});
describe('BinaryHeap#first', function() {
var heap;
beforeEach(function() {
heap = new BinaryHeap();
heap.add(1, 2, 3);
});
it('returns the first element but does not remove it', function() {
var element = heap.first();
assert.equal(element, 3);
assert.equal(heap.toArray().length, 3);
});
});
describe('BinaryHeap#size', function() {
it('takes custom score function', function() {
var heap = new BinaryHeap({ elements: [1, 2, 3] });
assert.equal(heap.size(), 3);
});
});
describe('BinaryHeap', function() {
it('takes custom score function', function() {
var heap = new BinaryHeap({
score: function(obj) {
return -obj;
}
});
heap.add(8, 7, 6, 5, 4, 3, 2, 1);
assert.deepEqual(heap.toSortedArray(), [1, 2, 3, 4, 5, 6, 7, 8]);
});
});
================================================
FILE: packages/measured-core/test/unit/util/test-ExponentiallyDecayingSample.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var EDS = common.measured.ExponentiallyDecayingSample;
var units = common.measured.units;
describe('ExponentiallyDecayingSample#toSortedArray', function() {
var sample;
beforeEach(function() {
sample = new EDS({
size: 3,
random: function() {
return 1;
}
});
});
it('returns an empty array by default', function() {
assert.deepEqual(sample.toSortedArray(), []);
});
it('is always sorted by priority', function() {
sample.update('a', Date.now() + 3000);
sample.update('b', Date.now() + 2000);
sample.update('c', Date.now());
assert.deepEqual(sample.toSortedArray(), ['c', 'b', 'a']);
});
});
describe('ExponentiallyDecayingSample#toArray', function() {
var sample;
beforeEach(function() {
sample = new EDS({
size: 3,
random: function() {
return 1;
}
});
});
it('returns an empty array by default', function() {
assert.deepEqual(sample.toArray(), []);
});
it('may return an unsorted array', function() {
sample.update('a', Date.now() + 3000);
sample.update('b', Date.now() + 2000);
sample.update('c', Date.now());
assert.deepEqual(sample.toArray(), ['c', 'a', 'b']);
});
});
describe('ExponentiallyDecayingSample#update', function() {
var sample;
beforeEach(function() {
sample = new EDS({
size: 2,
random: function() {
return 1;
}
});
});
it('can add one item', function() {
sample.update('a');
assert.deepEqual(sample.toSortedArray(), ['a']);
});
it('sorts items according to priority ascending', function() {
sample.update('a', Date.now());
sample.update('b', Date.now() + 1000);
assert.deepEqual(sample.toSortedArray(), ['a', 'b']);
});
it('pops items with lowest priority', function() {
sample.update('a', Date.now());
sample.update('b', Date.now() + 1000);
sample.update('c', Date.now() + 2000);
assert.deepEqual(sample.toSortedArray(), ['b', 'c']);
});
it('items with too low of a priority do not make it in', function() {
sample.update('a', Date.now() + 1000);
sample.update('b', Date.now() + 2000);
sample.update('c', Date.now());
assert.deepEqual(sample.toSortedArray(), ['a', 'b']);
});
});
describe('ExponentiallyDecayingSample#_rescale', function() {
var sample;
beforeEach(function() {
sample = new EDS({
size: 2,
random: function() {
return 1;
}
});
});
it('works as expected', function() {
sample.update('a', Date.now() + 50 * units.MINUTES);
sample.update('b', Date.now() + 55 * units.MINUTES);
var elements = sample._elements.toSortedArray();
assert.ok(elements[0].priority > 1000);
assert.ok(elements[1].priority > 1000);
sample._rescale(Date.now() + 60 * units.MINUTES);
elements = sample._elements.toSortedArray();
assert.ok(elements[0].priority < 1);
assert.ok(elements[0].priority > 0);
assert.ok(elements[1].priority < 1);
assert.ok(elements[1].priority > 0);
});
});
================================================
FILE: packages/measured-core/test/unit/util/test-ExponentiallyMovingWeightedAverage.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var units = common.measured.units;
var EMWA = common.measured.ExponentiallyMovingWeightedAverage;
describe('ExponentiallyMovingWeightedAverage', function() {
it('decay over several updates and ticks', function() {
var ewma = new EMWA(units.MINUTES, 5 * units.SECONDS);
ewma.update(5);
ewma.tick();
assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.080');
ewma.update(5);
ewma.update(5);
ewma.tick();
assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.233');
ewma.update(15);
ewma.tick();
assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.455');
var i;
for (i = 0; i < 200; i++) {
ewma.update(15);
ewma.tick();
}
assert.equal(ewma.rate(units.SECONDS).toFixed(3), '3.000');
});
});
================================================
FILE: packages/measured-core/test/unit/util/test-Stopwatch.js
================================================
/*global describe, it, beforeEach, afterEach*/
'use strict';
var common = require('../../common');
var assert = require('assert');
var Stopwatch = common.measured.Stopwatch;
var sinon = require('sinon');
describe('Stopwatch', function() {
var watch;
var clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
watch = new Stopwatch({
getTime: function() {
return new Date().getTime();
}
});
});
afterEach(function() {
clock.restore();
});
it('returns time on end', function() {
clock.tick(100);
var elapsed = watch.end();
assert.equal(elapsed, 100);
});
it('emits time on end', function() {
clock.tick(20);
var time;
watch.on('end', function(_time) {
time = _time;
});
watch.end();
assert.equal(time, 20);
});
it('becomes useless after being ended once', function() {
clock.tick(20);
var time;
watch.on('end', function(_time) {
time = _time;
});
assert.equal(watch.end(), 20);
assert.equal(time, 20);
time = null;
assert.equal(watch.end(), undefined);
assert.equal(time, null);
});
});
================================================
FILE: packages/measured-node-metrics/README.md
================================================
# Measured Node Metrics
Various metrics generators and http framework middlewares that can be used with a self reporting metrics registry to easily instrument metrics for a node app.
[](https://www.npmjs.com/package/measured-node-metrics)
## Install
```
npm install measured-node-metrics
```
## What is in this package
### [Measured Node Metrics Module](https://yaorg.github.io/node-measured/module-measured-node-metrics.html)
See the docs for the main module to see the exported helper functions and maps of metric generators for various system and os metrics.
## Example usage
```javascript
const express = require('express');
const { createProcessMetrics, createOSMetrics, createExpressMiddleware } = require('measured-node-metrics');
const registry = new SelfReportingMetricsRegistry(new SomeReporterImple());
// Create and register default OS metrics
createOSMetrics(registry);
// Create and register default process metrics
createProcessMetrics(registry);
// Use the express middleware
const app = express();
app.use(createExpressMiddleware(registry));
// Implement the rest of app
```
You can also create your own middleware if your not using express, (please contribute it)
```javascript
const { onRequestStart, onRequestEnd } = require('measured-node-metrics');
/**
* Creates an Express middleware that reports a timer on request data.
* With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths.
*
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {number} [reportingIntervalInSeconds]
* @return {Function}
*/
createExpressMiddleware: (metricsRegistry, reportingIntervalInSeconds) => {
return (req, res, next) => {
const stopwatch = onRequestStart();
req.on('end', () => {
const { method } = req;
const { statusCode } = res;
// path variables should be stripped in order to avoid runaway time series creation,
// /v1/cars/:id should be one dimension rather than n, one for each id.
const uri = req.route ? req.route.path : '_unknown';
onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds);
});
next();
};
}
```
================================================
FILE: packages/measured-node-metrics/lib/index.js
================================================
const { nodeProcessMetrics, createProcessMetrics } = require('./nodeProcessMetrics');
const { nodeOsMetrics, createOSMetrics } = require('./nodeOsMetrics');
const { createExpressMiddleware, createKoaMiddleware, onRequestStart, onRequestEnd } = require('./nodeHttpRequestMetrics');
/**
* The main module for the measured-node-metrics lib.
*
* Various functions to help create node metrics and http framework middlewares
* that can be used with a self reporting metrics registry to easily instrument metrics for a node app.
*
* @module measured-node-metrics
*/
module.exports = {
/**
* Map of metric names and a functions that can be used to generate that metric object that can be registered with a
* self reporting metrics registry or used as seen fit.
*
* See {@link nodeProcessMetrics}.
*
* @type {Object.}
*/
nodeProcessMetrics,
/**
* Method that can be used to add a set of default node process metrics to your node app.
*
* registers all metrics defined in the {@link nodeProcessMetrics} map.
*
* @function
* @name createProcessMetrics
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {Dimensions} customDimensions
* @param {number} reportingIntervalInSeconds
*/
createProcessMetrics,
/**
* Map of metric names and a functions that can be used to generate that metric object that can be registered with a
* self reporting metrics registry or used as seen fit.
*
* See {@link nodeOsMetrics}.
*
* @type {Object.}
*/
nodeOsMetrics,
/**
* Method that can be used to add a set of default node process metrics to your app.
*
* registers all metrics defined in the {@link nodeOsMetrics} map.
*
* @function
* @name createOSMetrics
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {Dimensions} customDimensions
* @param {number} reportingIntervalInSeconds
*/
createOSMetrics,
/**
* Creates an Express middleware that reports a timer on request data.
* With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths.
*
* @function
* @name createExpressMiddleware
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {number} [reportingIntervalInSeconds]
* @return {Function}
*/
createExpressMiddleware,
/**
* Creates a Koa middleware that reports a timer on request data.
* With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths.
*
* @function
* @name createExpressMiddleware
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {number} [reportingIntervalInSeconds]
* @return {Function}
*/
createKoaMiddleware,
/**
* At the start of the request, create a stopwatch, that starts tracking how long the request is taking.
* @function
* @name onRequestStart
* @return {Stopwatch}
*/
onRequestStart,
/**
* When the request ends stop the stop watch and create or update the timer for requests that tracked by method, statuscode, path.
* The timers (meters and histograms) that get reported will be filterable by status codes, http method, the uri path.
* You will be able to create dash boards such as success percentage, latency percentiles by path and method, etc.
*
* @function
* @name onRequestEnd
* @param metricsRegistry The Self Reporting Metrics Registry
* @param stopwatch The stopwatch created by onRequestStart
* @param method The Http Method for the request
* @param statusCode The status code for the response
* @param [uri] The uri for the request. Please note to avoid out of control time series dimension creation spread,
* you would want to strip out ids and or other variables from the uri path.
* @param [reportingIntervalInSeconds] override the reporting interval defaults to every 10 seconds.
*/
onRequestEnd
};
================================================
FILE: packages/measured-node-metrics/lib/nodeHttpRequestMetrics.js
================================================
const { Stopwatch } = require('measured-core');
/**
* The default reporting interval for requests
* @type {number}
*/
const DEFAULT_REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10;
/**
* This module has functions needed to create middlewares for frameworks such as express and koa.
* It also exports the 2 functions needed to implement your own middleware.
* If you implement a middleware for a framework not implemented here, please contribute it back.
*
* @module node-http-request-metrics
*/
module.exports = {
/**
* Creates an Express middleware that reports a timer on request data.
* With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths.
*
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {number} [reportingIntervalInSeconds]
* @return {Function}
*/
createExpressMiddleware: (metricsRegistry, reportingIntervalInSeconds) => {
return (req, res, next) => {
const stopwatch = module.exports.onRequestStart();
res.on('finish', () => {
const { method } = req;
const { statusCode } = res;
const uri = req.route ? req.route.path : '_unknown';
module.exports.onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds);
});
next();
};
},
/**
* Creates a Koa middleware that reports a timer on request data.
* With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths.
*
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {number} [reportingIntervalInSeconds]
* @return {Function}
*/
createKoaMiddleware: (metricsRegistry, reportingIntervalInSeconds) => async (ctx, next) => {
const stopwatch = module.exports.onRequestStart();
const { req, res } = ctx;
res.once('finish', () => {
const { method } = req;
const { statusCode } = res;
const uri = ctx._matchedRoute || '_unknown';
module.exports.onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds);
});
await next();
},
/**
* At the start of the request, create a stopwatch, that starts tracking how long the request is taking.
* @return {Stopwatch}
*/
onRequestStart: () => {
return new Stopwatch();
},
/**
* When the request ends stop the stop watch and create or update the timer for requests that tracked by method, status code, path.
* The timers (meters and histograms) that get reported will be filterable by status codes, http method, the uri path.
* You will be able to create dash boards such as success percentage, latency percentiles by uri path and method, etc.
*
* @param {SelfReportingMetricsRegistry} metricsRegistry The Self Reporting Metrics Registry
* @param {Stopwatch} stopwatch The stopwatch created by onRequestStart
* @param {string} method The Http Method for the request
* @param {string|number} statusCode The status code for the response
* @param {string} [uri] The uri path for the request. Please note to avoid out of control time series dimension creation spread,
* you would want to strip out ids and or other variables from the uri path.
* @param {number} [reportingIntervalInSeconds] override the reporting interval defaults to every 10 seconds.
*/
onRequestEnd: (metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds) => {
reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS;
const customDimensions = {
statusCode: `${statusCode}`,
method: `${method}`
};
if (uri) {
customDimensions.uri = uri;
}
// get or create the timer for the request count/latency timer
const requestTimer = metricsRegistry.getOrCreateTimer('requests', customDimensions, reportingIntervalInSeconds);
// stop the request latency counter
const time = stopwatch.end();
requestTimer.update(time);
}
};
================================================
FILE: packages/measured-node-metrics/lib/nodeOsMetrics.js
================================================
const { Gauge, CachedGauge } = require('measured-core');
const { cpuAverage, calculateCpuUsagePercent } = require('./utils/CpuUtils');
const os = require('os');
/**
* The default reporting interval for node os metrics is 30 seconds.
*
* @type {number}
*/
const DEFAULT_NODE_OS_METRICS_REPORTING_INTERVAL_IN_SECONDS = 30;
/**
* A map of Metric generating functions, that create Metrics to measure node os stats.
*/
const nodeOsMetrics = {
/**
* https://nodejs.org/api/os.html#os_os_loadavg
* @return {Gauge}
*/
'node.os.loadavg.1m': () => {
return new Gauge(() => {
return os.loadavg()[0];
});
},
/**
* https://nodejs.org/api/os.html#os_os_loadavg
* @return {Gauge}
*/
'node.os.loadavg.5m': () => {
return new Gauge(() => {
return os.loadavg()[1];
});
},
/**
* https://nodejs.org/api/os.html#os_os_loadavg
* @return {Gauge}
*/
'node.os.loadavg.15m': () => {
return new Gauge(() => {
return os.loadavg()[2];
});
},
'node.os.freemem': () => {
return new Gauge(() => {
return os.freemem();
});
},
'node.os.totalmem': () => {
return new Gauge(() => {
return os.totalmem();
});
},
/**
* Gauge to track how long the os has been running.
*\
*]=-
* See {@link https://nodejs.org/api/os.html#os_os_uptime} for more information.
* @return {Gauge}
*/
'node.os.uptime': () => {
return new Gauge(() => {
// The os.uptime() method returns the system uptime in number of seconds.
return os.uptime();
});
},
/**
* Creates a {@link CachedGauge} that will self update every updateIntervalInSeconds and sample the
* cpu usage across all cores for sampleTimeInSeconds.
*
* @param {number} [updateIntervalInSeconds] How often to update and cache the cpu usage average, defaults to 30 seconds.
* @param {number} [sampleTimeInSeconds] How long to sample the cpu usage over, defaults to 5 seconds.
*/
'node.os.cpu.all-cores-avg': (updateIntervalInSeconds, sampleTimeInSeconds) => {
updateIntervalInSeconds = updateIntervalInSeconds || 30;
sampleTimeInSeconds = sampleTimeInSeconds || 5;
return new CachedGauge(() => {
return new Promise(resolve => {
//Grab first CPU Measure
const startMeasure = cpuAverage();
setTimeout(() => {
//Grab second Measure
const endMeasure = cpuAverage();
const percentageCPU = calculateCpuUsagePercent(startMeasure, endMeasure);
resolve(percentageCPU);
}, sampleTimeInSeconds);
});
}, updateIntervalInSeconds);
}
};
/**
* This module contains the methods to create and register default node os metrics to a metrics registry.
*
* @module node-os-metrics
*/
module.exports = {
/**
* Method that can be used to add a set of default node process metrics to your app.
*
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {Dimensions} customDimensions
* @param {number} reportingIntervalInSeconds
*/
createOSMetrics: (metricsRegistry, customDimensions, reportingIntervalInSeconds) => {
customDimensions = customDimensions || {};
reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_NODE_OS_METRICS_REPORTING_INTERVAL_IN_SECONDS;
Object.keys(nodeOsMetrics).forEach(metricName => {
metricsRegistry.register(metricName, nodeOsMetrics[metricName](), customDimensions, reportingIntervalInSeconds);
});
},
/**
* Map of metric names to a corresponding function that creates and returns a Metric that tracks it.
* See {@link nodeOsMetrics}
*/
nodeOsMetrics
};
================================================
FILE: packages/measured-node-metrics/lib/nodeProcessMetrics.js
================================================
const { Gauge } = require('measured-core');
const process = require('process');
/**
* The default reporting interval for node process metrics is 30 seconds.
*
* @type {number}
*/
const DEFAULT_NODE_PROCESS_METRICS_REPORTING_INTERVAL_IN_SECONDS = 30;
/**
* A map of Metric generating functions, that create Metrics to measure node process stats.
* @type {Object.}
*/
const nodeProcessMetrics = {
/**
* Creates a gauge that reports the rss from the node memory usage api.
* See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information.
*
* @return {Gauge}
*/
'node.process.memory-usage.rss': () => {
return new Gauge(() => {
return process.memoryUsage().rss;
});
},
/**
* See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information.
*
* @return {Gauge}
*/
'node.process.memory-usage.heap-total': () => {
return new Gauge(() => {
return process.memoryUsage().heapTotal;
});
},
/**
* See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information.
*
* @return {Gauge}
*/
'node.process.memory-usage.heap-used': () => {
return new Gauge(() => {
return process.memoryUsage().heapUsed;
});
},
/**
* See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information.
*
* @return {Gauge}
*/
'node.process.memory-usage.external': () => {
return new Gauge(() => {
const mem = process.memoryUsage();
return Object.prototype.hasOwnProperty.call(mem, 'external') ? mem.external : 0;
});
},
/**
* Gauge to track how long the node process has been running.
*
* See {@link https://nodejs.org/api/process.html#process_process_uptime} for more information.
* @return {Gauge}
*/
'node.process.uptime': () => {
return new Gauge(() => {
return Math.floor(process.uptime());
});
}
};
/**
* This module contains the methods to create and register default node process metrics to a metrics registry.
*
* @module node-process-metrics
*/
module.exports = {
/**
* Method that can be used to add a set of default node process metrics to your app.
*
* @param {SelfReportingMetricsRegistry} metricsRegistry
* @param {Dimensions} [customDimensions]
* @param {number} [reportingIntervalInSeconds]
*/
createProcessMetrics: (metricsRegistry, customDimensions, reportingIntervalInSeconds) => {
customDimensions = customDimensions || {};
reportingIntervalInSeconds =
reportingIntervalInSeconds || DEFAULT_NODE_PROCESS_METRICS_REPORTING_INTERVAL_IN_SECONDS;
Object.keys(nodeProcessMetrics).forEach(metricName => {
metricsRegistry.register(
metricName,
nodeProcessMetrics[metricName](),
customDimensions,
reportingIntervalInSeconds
);
});
},
/**
* Map of metric names to a corresponding function that creates and returns a Metric that tracks it.
* See {@link nodeProcessMetrics}
*/
nodeProcessMetrics
};
================================================
FILE: packages/measured-node-metrics/lib/utils/CpuUtils.js
================================================
const os = require('os');
/**
* @module CpuUtils
*/
module.exports = {
/**
*
* @return {{idle: number, total: number}}
*/
cpuAverage: () => {
//Initialise sum of idle and time of cores and fetch CPU info
let totalIdle = 0,
totalTick = 0;
const cpus = os.cpus();
cpus.forEach(cpu => {
//Total up the time in the cores tick
Object.keys(cpu.times).forEach(type => {
totalTick += cpu.times[type];
});
//Total up the idle time of the core
totalIdle += cpu.times.idle;
});
//Return the average Idle and Tick times
return { idle: totalIdle / cpus.length, total: totalTick / cpus.length };
},
/**
*
* @param {{idle: number, total: number}} startMeasure
* @param {{idle: number, total: number}} endMeasure
*/
calculateCpuUsagePercent: (startMeasure, endMeasure) => {
//Calculate the difference in idle and total time between the measures
const idleDifference = endMeasure.idle - startMeasure.idle;
const totalDifference = endMeasure.total - startMeasure.total;
//Calculate the average percentage CPU usage
return Math.ceil(100 - 100 * idleDifference / totalDifference);
}
};
================================================
FILE: packages/measured-node-metrics/package.json
================================================
{
"name": "measured-node-metrics",
"description": "Various metrics generators and http framework middlewares that can be used with a self reporting metrics registry to easily instrument metrics for a node app.",
"version": "2.0.0",
"homepage": "https://yaorg.github.io/node-measured/",
"engines": {
"node": ">= 5.12"
},
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"scripts": {
"clean": "rm -fr build",
"format": "prettier --write './lib/**/*.{ts,js}'",
"lint": "eslint lib --ext .js",
"test:node": "mocha './test/**/test-*.js'",
"test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'",
"test:browser": "exit 0",
"test": "yarn test:node:coverage",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"repository": {
"url": "git://github.com/yaorg/node-measured.git"
},
"dependencies": {
"measured-core": "^2.0.0"
},
"files": [
"lib",
"README.md"
],
"license": "MIT",
"devDependencies": {
"express": "^4.16.3",
"find-free-port": "^1.2.0",
"jsdoc": "^3.5.5",
"measured-reporting": "^2.0.0"
}
}
================================================
FILE: packages/measured-node-metrics/test/integration/test-express-middleware.js
================================================
/*global describe, it, beforeEach, afterEach*/
const express = require('express');
const Registry = require('measured-reporting').SelfReportingMetricsRegistry;
const TestReporter = require('../unit/TestReporter');
const { createExpressMiddleware } = require('../../lib');
const findFreePort = require('find-free-port');
const assert = require('assert');
const http = require('http');
describe('express-middleware', () => {
let port;
let reporter;
let registry;
let middleware;
let app;
let httpServer;
beforeEach(() => {
return new Promise(resolve => {
reporter = new TestReporter();
registry = new Registry(reporter);
middleware = createExpressMiddleware(registry, 1);
app = express();
app.use(middleware);
app.use(express.json());
app.get('/hello', (req, res) => res.send('Hello World!'));
app.post('/world', (req, res) => res.status(201).send('Hello World!'));
app.get('/users/:userId', (req, res) => {
res.send(`id: ${req.params.userId}`);
});
findFreePort(3000).then(portArr => {
port = portArr.shift();
httpServer = http.createServer(app);
httpServer.listen(port);
resolve();
});
});
});
afterEach(() => {
httpServer.close();
registry.shutdown();
});
it('creates a single timer that has 1 count for requests, when an http call is made once', () => {
return callLocalHost(port, 'hello').then(() => {
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
assert.equal(registeredKeys[0], 'requests-GET-200-/hello');
const metricWrapper = registry._registry.getMetricWrapperByKey('requests-GET-200-/hello');
const { name, dimensions } = metricWrapper;
assert.equal(name, 'requests');
assert.deepEqual(dimensions, { statusCode: '200', method: 'GET', uri: '/hello' });
});
});
it('creates a single timer that has 1 count for requests, when an http POST call is made once', () => {
const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } };
return callLocalHost(port, 'world', options).then(() => {
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
assert.equal(registeredKeys[0], 'requests-POST-201-/world');
const metricWrapper = registry._registry.getMetricWrapperByKey('requests-POST-201-/world');
const { name, dimensions } = metricWrapper;
assert.equal(name, 'requests');
assert.deepEqual(dimensions, { statusCode: '201', method: 'POST', uri: '/world' });
});
});
it('does not create runaway n metrics in the registry for n ids in the path', () => {
return Promise.all([
callLocalHost(port, 'users/foo'),
callLocalHost(port, 'users/bar'),
callLocalHost(port, 'users/bop')
]).then(() => {
assert.equal(registry._registry.allKeys().length, 1, 'There should only be one metric for /users and GET');
});
});
});
const callLocalHost = (port, endpoint, options) => {
return new Promise((resolve, reject) => {
const req = Object.assign({ protocol: 'http:',
host: '127.0.0.1',
port: `${port}`,
path: `/${endpoint}`,
method: 'GET' },
options || {});
http
.request(req, resp => {
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
console.log(JSON.stringify(data));
resolve();
});
})
.on('error', err => {
console.log('Error: ', JSON.stringify(err));
reject();
})
.end();
});
};
================================================
FILE: packages/measured-node-metrics/test/integration/test-koa-middleware.js
================================================
const Koa = require('koa');
const KoaBodyParser = require('koa-bodyparser');
const Router = require('koa-router');
const Registry = require('measured-reporting').SelfReportingMetricsRegistry;
const TestReporter = require('../unit/TestReporter');
const { createKoaMiddleware } = require('../../lib');
const findFreePort = require('find-free-port');
const assert = require('assert');
const http = require('http');
describe('koa-middleware', () => {
let port;
let reporter;
let registry;
let middleware;
let app;
let httpServer;
let router;
beforeEach(() => {
return new Promise(resolve => {
reporter = new TestReporter();
registry = new Registry(reporter);
middleware = createKoaMiddleware(registry, 1);
app = new Koa();
router = new Router();
router.get('/hello', ({ response }) => {
response.body = 'Hello World!';
return response;
});
router.post('/world', ({ response }) => {
response.body = 'Hello World!';
response.status = 201;
return response;
});
router.get('/users/:userId', ({ params, response }) => {
response.body = `id: ${params.userId}`;
return response;
});
app.use(middleware);
app.use(KoaBodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.on('error', (err) => console.error(err));
findFreePort(3000).then(portArr => {
port = portArr.shift();
httpServer = app.listen(port);
resolve();
});
});
});
afterEach(() => {
httpServer.close();
registry.shutdown();
});
it('creates a single timer that has 1 count for requests, when an http call is made once', () => {
return callLocalHost(port, 'hello').then(() => {
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
assert.equal(registeredKeys[0], 'requests-GET-200-/hello');
const metricWrapper = registry._registry.getMetricWrapperByKey('requests-GET-200-/hello');
const { name, dimensions } = metricWrapper;
assert.equal(name, 'requests');
assert.deepEqual(dimensions, { statusCode: '200', method: 'GET', uri: '/hello' });
});
});
it('creates a single timer that has 1 count for requests, when an http POST call is made once', () => {
const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } };
return callLocalHost(port, 'world', options).then(() => {
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
assert.equal(registeredKeys[0], 'requests-POST-201-/world');
const metricWrapper = registry._registry.getMetricWrapperByKey('requests-POST-201-/world');
const { name, dimensions } = metricWrapper;
assert.equal(name, 'requests');
assert.deepEqual(dimensions, { statusCode: '201', method: 'POST', uri: '/world' });
});
});
it('does not create runaway n metrics in the registry for n ids in the path', () => {
return Promise.all([
callLocalHost(port, 'users/foo'),
callLocalHost(port, 'users/bar'),
callLocalHost(port, 'users/bop')
]).then(() => {
assert.equal(registry._registry.allKeys().length, 1, 'There should only be one metric for /users and GET');
});
});
});
const callLocalHost = (port, endpoint, options) => {
return new Promise((resolve, reject) => {
const req = Object.assign({ protocol: 'http:',
host: '127.0.0.1',
port: `${port}`,
path: `/${endpoint}`,
method: 'GET' },
options || {});
http
.request(req, resp => {
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
console.log(JSON.stringify(data));
resolve();
});
})
.on('error', err => {
console.log('Error: ', JSON.stringify(err));
reject();
})
.end();
});
};
================================================
FILE: packages/measured-node-metrics/test/unit/TestReporter.js
================================================
const { Reporter } = require('measured-reporting');
/**
* @extends Reporter
*/
class TestReporter extends Reporter {
constructor(options) {
super(options);
this._reportedMetrics = [];
}
getReportedMetrics() {
return this._reportedMetrics;
}
_reportMetrics(metrics) {
this._reportedMetrics.push(metrics);
}
}
module.exports = TestReporter;
================================================
FILE: packages/measured-node-metrics/test/unit/test-nodeHttpRequestMetrics.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const EventEmitter = require('events');
const { Stopwatch } = require('measured-core');
const { createExpressMiddleware, createKoaMiddleware, onRequestStart, onRequestEnd } = require('../../lib');
const TestReporter = require('./TestReporter');
const Registry = require('measured-reporting').SelfReportingMetricsRegistry;
class MockResponse extends EventEmitter {
constructor() {
super();
this.statusCode = 200;
}
finish() {
this.emit('finish');
}
}
describe('onRequestStart', () => {
it('returns a stopwatch', () => {
const stopwatch = onRequestStart();
assert(stopwatch.constructor.name === 'Stopwatch');
});
});
describe('onRequestEnd', () => {
it('stops the stopwatch and gets or creates a timer and then updates it with the elapsed time with the appropriate dimensions', () => {
const stopwatch = new Stopwatch();
const registry = new Registry(new TestReporter());
onRequestEnd(registry, stopwatch, 'POST', 201, '/some/path');
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
const expectedKey = 'requests-POST-201-/some/path';
assert.equal(registeredKeys[0], expectedKey);
const metricWrapper = registry._registry.getMetricWrapperByKey(expectedKey);
assert.equal(metricWrapper.name, 'requests');
assert.deepEqual(metricWrapper.dimensions, { statusCode: '201', method: 'POST', uri: '/some/path' });
assert.equal(metricWrapper.metricImpl.getType(), 'Timer');
assert.equal(metricWrapper.metricImpl._histogram._count, 1);
registry.shutdown();
});
});
describe('createExpressMiddleware', () => {
it('creates and registers a metric called request that is a timer', () => {
const reporter = new TestReporter();
const registry = new Registry(reporter);
const middleware = createExpressMiddleware(registry);
const res = new MockResponse();
middleware(
{
method: 'GET',
routine: { path: '/v1/rest/some-end-point' }
},
res,
() => {}
);
res.finish();
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
assert(registeredKeys[0].includes('requests-GET'));
registry.shutdown();
});
});
describe('createKoaMiddleware', () => {
it('creates and registers a metric called request that is a timer', async () => {
const reporter = new TestReporter();
const registry = new Registry(reporter);
const middleware = createKoaMiddleware(registry);
const res = new MockResponse();
middleware(
{
req: {
method: 'GET',
url: '/v1/rest/some-end-point',
},
res,
},
() => Promise.resolve()
).then(() => {
const registeredKeys = registry._registry.allKeys();
assert(registeredKeys.length === 1);
assert(registeredKeys[0].includes('requests-GET'));
registry.shutdown();
});
res.finish();
});
});
================================================
FILE: packages/measured-node-metrics/test/unit/test-nodeOsMetrics.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const { validateMetric } = require('measured-core').metricValidators;
const { nodeOsMetrics, createOSMetrics } = require('../../lib');
const TestReporter = require('./TestReporter');
const Registry = require('measured-reporting').SelfReportingMetricsRegistry;
const { MetricTypes } = require('measured-core');
describe('nodeOsMetrics', () => {
it('contains a map of string to functions that generate a valid metric object', () => {
Object.keys(nodeOsMetrics).forEach(metricName => {
assert(typeof metricName === 'string', 'The key should be a string');
const metricGeneratingFunction = nodeOsMetrics[metricName];
assert(typeof metricGeneratingFunction === 'function', 'metric generating function should be a function');
const metric = metricGeneratingFunction();
validateMetric(metric);
const value = metric.toJSON();
const type = metric.getType();
if ([MetricTypes.COUNTER, MetricTypes.GAUGE].includes(type)) {
assert(typeof value === 'number');
} else {
assert(typeof value === 'object');
}
if (metric.end) {
metric.end();
}
});
});
});
describe('createOSMetrics', () => {
it('creates and registers a metric for every metric defined in nodeOsMetrics', () => {
const reporter = new TestReporter();
const registry = new Registry(reporter);
createOSMetrics(registry);
const registeredKeys = registry._registry.allKeys();
const expectedKeys = Object.keys(nodeOsMetrics);
assert(registeredKeys.length > 1);
assert.deepEqual(registeredKeys, expectedKeys);
registry.shutdown();
});
});
================================================
FILE: packages/measured-node-metrics/test/unit/test-nodeProcessMetrics.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const { validateMetric } = require('measured-core').metricValidators;
const { nodeProcessMetrics, createProcessMetrics } = require('../../lib');
const TestReporter = require('./TestReporter');
const Registry = require('measured-reporting').SelfReportingMetricsRegistry;
const { MetricTypes } = require('measured-core');
describe('nodeProcessMetrics', () => {
it('contains a map of string to functions that generate a valid metric object', () => {
Object.keys(nodeProcessMetrics).forEach(metricName => {
assert(typeof metricName === 'string', 'The key should be a string');
const metricGeneratingFunction = nodeProcessMetrics[metricName];
assert(typeof metricGeneratingFunction === 'function', 'metric generating function should be a function');
const metric = metricGeneratingFunction();
validateMetric(metric);
const value = metric.toJSON();
const type = metric.getType();
if ([MetricTypes.COUNTER, MetricTypes.GAUGE].includes(type)) {
assert(typeof value === 'number');
} else {
assert(typeof value === 'object');
}
});
});
});
describe('createProcessMetrics', () => {
it('creates and registers a metric for every metric defined in nodeProcessMetrics', () => {
const reporter = new TestReporter();
const registry = new Registry(reporter);
createProcessMetrics(registry);
const registeredKeys = registry._registry.allKeys();
const expectedKeys = Object.keys(nodeProcessMetrics);
assert(registeredKeys.length > 1);
assert.deepEqual(registeredKeys, expectedKeys);
registry.shutdown();
});
});
================================================
FILE: packages/measured-node-metrics/test/unit/utils/test-CpuUtils.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const CpuUtils = require('../../../lib/utils/CpuUtils');
describe('CpuUtils', () => {
it('#cpuAverage ', () => {
const measure = CpuUtils.cpuAverage();
assert(typeof measure.idle === 'number');
assert(measure.idle > 0);
assert(typeof measure.total === 'number');
assert(measure.total > 0);
});
it('#calculateCpuUsagePercent calculates a percent', () => {
const start = CpuUtils.cpuAverage();
for (let i = 0; i < 10000000; i++) {
Math.floor(Math.random() * Math.floor(10000000));
}
const end = CpuUtils.cpuAverage();
const percent = CpuUtils.calculateCpuUsagePercent(start, end);
assert(typeof percent === 'number');
assert(percent > 0);
});
});
================================================
FILE: packages/measured-reporting/README.md
================================================
# Measured Reporting
The registry and reporting library that has the classes needed to create a dimension aware, self reporting metrics registry.
[](https://www.npmjs.com/package/measured-reporting)
## Install
```
npm install measured-reporting
```
## What is in this package
### [Self Reporting Metrics Registry](https://yaorg.github.io/node-measured/SelfReportingMetricsRegistry.html)
A dimensional aware self-reporting metrics registry, just supply this class with a reporter implementation at instantiation and this is all you need to instrument application level metrics in your app.
See the [SelfReportingMetricsRegistryOptions](https://yaorg.github.io/node-measured/global.html#SelfReportingMetricsRegistryOptions) for advanced configuration.
```javascript
const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting');
const registry = new SelfReportingMetricsRegistry(new LoggingReporter({
defaultDimensions: {
hostname: os.hostname()
}
}));
// The metric will flow through LoggingReporter#_reportMetrics(metrics) every 10 seconds by default
const myCounter = registry.getOrCreateCounter('my-counter');
```
### [Reporter Abstract Class](https://yaorg.github.io/node-measured/Reporter.html)
Extend this class and override the [_reportMetrics(metrics)](https://yaorg.github.io/node-measured/Reporter.html#_reportMetrics__anchor) method to create a vendor specific reporter implementation.
See the [ReporterOptions](https://yaorg.github.io/node-measured/global.html#ReporterOptions) for advanced configuration.
#### Current Implementations
- [SignalFx Reporter](https://yaorg.github.io/node-measured/SignalFxMetricsReporter.html) in the `measured-signalfx-reporter` package.
- reports metrics to SignalFx.
- [Logging Reporter](https://yaorg.github.io/node-measured/LoggingReporter.html) in the `measured-reporting` package.
- A reporter impl that simply logs the metrics via the Logger
#### Creating an anonymous Implementation
You can technically create an anonymous instance of this, see the following example.
```javascript
const os = require('os');
const process = require('process');
const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting');
// Create a self reporting registry with an anonymous Reporter instance;
const registry = new SelfReportingMetricsRegistry(
new class extends Reporter {
constructor() {
super({
defaultDimensions: {
hostname: os.hostname(),
env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset'
}
})
}
_reportMetrics(metrics) {
metrics.forEach(metric => {
console.log(JSON.stringify({
metricName: metric.name,
dimensions: this._getDimensions(metric),
data: metric.metricImpl.toJSON()
}))
});
}
}()
);
// create a gauge that reports the process uptime every second
const processUptimeGauge = registry.getOrCreateGauge('node.process.uptime', () => process.uptime(), {}, 1);
```
Example output:
```bash
APP5HTD6ACCD8C:foo jfiel2$ NODE_ENV=development node index.js
{"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":0.092}
{"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":1.099}
{"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":2.104}
{"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":3.105}
{"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":4.106}
```
Consider creating a proper class and contributing it back to Measured if it is generic and sharable.
### [Logging Reporter Class](https://yaorg.github.io/node-measured/LoggingReporter.html)
A simple reporter that logs the metrics via the Logger.
See the [ReporterOptions](http://yaorg.github.io/node-measured/build/docs/packages/measured-reporting/global.html#ReporterOptions) for advanced configuration.
```javascript
const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting');
const registry = new SelfReportingMetricsRegistry(new LoggingReporter({
logger: myLogerImpl, // defaults to new console logger if not supplied
defaultDimensions: {
hostname: require('os').hostname()
}
}));
```
## What are dimensions?
As described by Signal Fx:
*A dimension is a key/value pair that, along with the metric name, is part of the identity of a time series.
You can filter and aggregate time series by those dimensions across SignalFx.*
DataDog has a [nice blog post](https://www.datadoghq.com/blog/the-power-of-tagged-metrics/) about how they are used in their aggregator api.
Graphite also supports the concept via [tags](http://graphite.readthedocs.io/en/latest/tags.html).
================================================
FILE: packages/measured-reporting/lib/@types/types.js
================================================
/**
* A wrapper object around a {@link Metric}, {@link Dimensions} and the metric name
*
* @interface MetricWrapper
* @typedef MetricWrapper
* @type {Object}
* @property {string} name The supplied name of the Metric
* @property {Metric} metricImpl The {@link Metric} object
* @property {Dimensions} dimensions The {@link Dimensions} for the given {@link Metric}
*/
/**
* A Dictionary of string, string key value pairs
*
* @interface Dimensions
* @typedef Dimensions
* @type {Object.}
*
* @example
* {
* path: "/api/foo"
* method: "GET"
* statusCode: "200"
* }
*/
================================================
FILE: packages/measured-reporting/lib/index.js
================================================
const SelfReportingMetricsRegistry = require('./registries/SelfReportingMetricsRegistry');
const Reporter = require('./reporters/Reporter');
const LoggingReporter = require('./reporters/LoggingReporter');
const inputValidators = require('./validators/inputValidators');
/**
* The main measured module that is referenced when require('measured-reporting') is used.
* @module measured-reporting
*/
module.exports = {
/**
* The Self Reporting Metrics Registry Class.
*
* @type {SelfReportingMetricsRegistry}
*/
SelfReportingMetricsRegistry,
/**
* The abstract / base Reporter class.
*
* @type {Reporter}
*/
Reporter,
/**
* The basic included reference reporter, simply logs the metrics.
* See {ReporterOptions} for options.
*
* @type {LoggingReporter}
*/
LoggingReporter,
/**
* Various Input Validation functions.
*
* @type {inputValidators}
*/
inputValidators
};
================================================
FILE: packages/measured-reporting/lib/registries/DimensionAwareMetricsRegistry.js
================================================
const mapcap = require('mapcap');
/**
* Simple registry that stores Metrics by name and dimensions.
*/
class DimensionAwareMetricsRegistry {
/**
* @param {DimensionAwareMetricsRegistryOptions} [options] Configurable options for the Dimension Aware Metrics Registry
*/
constructor(options) {
options = options || {};
let metrics = new Map();
if (options.metricLimit) {
metrics = mapcap(metrics, options.metricLimit, options.lru);
}
this._metrics = metrics;
}
/**
* Checks to see if a metric with the given name and dimensions is present.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The dimensions for the metric
* @returns {boolean} true if the metric with given dimensions is present
*/
hasMetric(name, dimensions) {
const key = this._generateStorageKey(name, dimensions);
return this._metrics.has(key);
}
/**
* Retrieves a metric with a given name and dimensions is present.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The dimensions for the metric
* @returns {Metric} a wrapper object around name, dimension and {@link Metric}
*/
getMetric(name, dimensions) {
const key = this._generateStorageKey(name, dimensions);
return this._metrics.get(key).metricImpl;
}
/**
* Retrieves a metric by the calculated key (name / dimension combo).
*
* @param {string} key The registered key for the given registered {@link MetricWrapper}
* @returns {MetricWrapper} a wrapper object around name, dimension and {@link Metric}
*/
getMetricWrapperByKey(key) {
return this._metrics.get(key);
}
/**
* Upserts a {@link Metric} in the internal storage map for a given name, dimension combo
*
* @param {string} name The metric name
* @param {Metric} metric The {@link Metric} impl
* @param {Dimensions} dimensions The dimensions for the metric
* @return {string} The registry key for the metric, dimension combo
*/
putMetric(name, metric, dimensions) {
const key = this._generateStorageKey(name, dimensions);
this._metrics.set(key, {
name: name,
metricImpl: metric,
dimensions: dimensions || {}
});
return key;
}
/**
* Returns an array of all keys of metrics stored in this registry.
* @return {string[]} all keys of metrics stored in this registry.
*/
allKeys() {
return Array.from(this._metrics.keys());
}
/**
* Generates a unique key off of the metric name and custom dimensions for internal use in the registry maps.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The dimensions for the metric
* @return {string} a unique key based off of the metric nae and dimensions
* @private
*/
_generateStorageKey(name, dimensions) {
let key = name;
if (dimensions) {
Object.keys(dimensions)
.sort()
.forEach(dimensionKey => {
key = `${key}-${dimensions[dimensionKey]}`;
});
}
return key;
}
}
module.exports = DimensionAwareMetricsRegistry;
/**
* Configurable options for the Dimension Aware Metrics Registry
*
* @interface DimensionAwareMetricsRegistryOptions
* @typedef DimensionAwareMetricsRegistryOptions
* @property {Number} metricLimit the maximum number of metrics the registry may hold before dropping metrics
* @property {Boolean} lru switch dropping strategy from "least recently added" to "least recently used"
*/
================================================
FILE: packages/measured-reporting/lib/registries/SelfReportingMetricsRegistry.js
================================================
const consoleLogLevel = require('console-log-level');
const { CachedGauge, SettableGauge, Gauge, Timer, Counter, Meter, Histogram } = require('measured-core');
const DimensionAwareMetricsRegistry = require('./DimensionAwareMetricsRegistry');
const {
validateSelfReportingMetricsRegistryParameters,
validateRegisterOptions,
validateGaugeOptions,
validateCounterOptions,
validateHistogramOptions,
validateTimerOptions,
validateSettableGaugeOptions,
validateCachedGaugeOptions
} = require('../validators/inputValidators');
function prefix() {
return `${new Date().toISOString()}: `;
}
/**
* A dimensional aware self-reporting metrics registry
*/
class SelfReportingMetricsRegistry {
/**
* @param {Reporter|Reporter[]} reporters A single {@link Reporter} or an array of reporters that will be used to report metrics on an interval.
* @param {SelfReportingMetricsRegistryOptions} [options] Configurable options for the Self Reporting Metrics Registry
*/
constructor(reporters, options) {
options = options || {};
if (!Array.isArray(reporters)) {
reporters = [reporters];
}
validateSelfReportingMetricsRegistryParameters(reporters, options);
/**
* @type {Reporter}
* @protected
*/
this._reporters = reporters;
/**
* @type {DimensionAwareMetricsRegistry}
* @protected
*/
this._registry = options.registry || new DimensionAwareMetricsRegistry();
this._reporters.forEach(reporter => reporter.setRegistry(this._registry));
/**
* Loggers to use, defaults to a new console logger if nothing is supplied in options
* @type {Logger}
* @protected
*/
this._log =
options.logger ||
consoleLogLevel({ name: 'SelfReportingMetricsRegistry', level: options.logLevel || 'info', prefix: prefix });
}
/**
* Registers a manually created Metric.
*
* @param {string} name The Metric name
* @param {Metric} metric The {@link Metric} to register
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @example
* const settableGauge = new SettableGauge(5);
* // register the gauge and have it report to every 10 seconds
* registry.register('my-gauge', settableGauge, {}, 10);
* interval(() => {
* // such as cpu % used
* determineAValueThatCannotBeSync((value) => {
* settableGauge.update(value);
* })
* }, 10000)
*/
register(name, metric, dimensions, publishingIntervalInSeconds) {
validateRegisterOptions(name, metric, dimensions, publishingIntervalInSeconds);
if (this._registry.hasMetric(name, dimensions)) {
throw new Error(
`Metric with name: ${name} and dimensions: ${JSON.stringify(dimensions)} has already been registered`
);
} else {
const key = this._registry.putMetric(name, metric, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return metric;
}
/**
* Creates a {@link Gauge} or gets the existing Gauge for a given name and dimension combo
*
* @param {string} name The Metric name
* @param {function} callback The callback that will return a value to report to signal fx
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @return {Gauge}
* @example
* // https://nodejs.org/api/process.html#process_process_memoryusage
* // Report heap total and heap used at the default interval
* registry.getOrCreateGauge(
* 'process-memory-heap-total',
* () => {
* return process.memoryUsage().heapTotal
* }
* );
* registry.getOrCreateGauge(
* 'process-memory-heap-used',
* () => {
* return process.memoryUsage().heapUsed
* }
* )
*/
getOrCreateGauge(name, callback, dimensions, publishingIntervalInSeconds) {
validateGaugeOptions(name, callback, dimensions, publishingIntervalInSeconds);
let gauge;
if (this._registry.hasMetric(name, dimensions)) {
gauge = this._registry.getMetric(name, dimensions);
} else {
gauge = new Gauge(callback);
const key = this._registry.putMetric(name, gauge, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return gauge;
}
/**
* Creates a {@link Histogram} or gets the existing Histogram for a given name and dimension combo
*
* @param {string} name The Metric name
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @return {Histogram}
*/
getOrCreateHistogram(name, dimensions, publishingIntervalInSeconds) {
validateHistogramOptions(name, dimensions, publishingIntervalInSeconds);
let histogram;
if (this._registry.hasMetric(name, dimensions)) {
histogram = this._registry.getMetric(name, dimensions);
} else {
histogram = new Histogram();
const key = this._registry.putMetric(name, histogram, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return histogram;
}
/**
* Creates a {@link Meter} or gets the existing Meter for a given name and dimension combo
*
* @param {string} name The Metric name
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @return {Meter}
*/
getOrCreateMeter(name, dimensions, publishingIntervalInSeconds) {
// todo validate options
let meter;
if (this._registry.hasMetric(name, dimensions)) {
meter = this._registry.getMetric(name, dimensions);
} else {
meter = new Meter();
const key = this._registry.putMetric(name, meter, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return meter;
}
/**
* Creates a {@link Counter} or gets the existing Counter for a given name and dimension combo
*
* @param {string} name The Metric name
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @return {Counter}
*/
getOrCreateCounter(name, dimensions, publishingIntervalInSeconds) {
validateCounterOptions(name, dimensions, publishingIntervalInSeconds);
let counter;
if (this._registry.hasMetric(name, dimensions)) {
counter = this._registry.getMetric(name, dimensions);
} else {
counter = new Counter();
const key = this._registry.putMetric(name, counter, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return counter;
}
/**
* Creates a {@link Timer} or gets the existing Timer for a given name and dimension combo.
*
* @param {string} name The Metric name
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @return {Timer}
*/
getOrCreateTimer(name, dimensions, publishingIntervalInSeconds) {
validateTimerOptions(name, dimensions, publishingIntervalInSeconds);
let timer;
if (this._registry.hasMetric(name, dimensions)) {
timer = this._registry.getMetric(name, dimensions);
} else {
timer = new Timer();
const key = this._registry.putMetric(name, timer, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return timer;
}
/**
* Creates a {@link SettableGauge} or gets the existing SettableGauge for a given name and dimension combo.
*
* @param {string} name The Metric name
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval
* @return {SettableGauge}
*/
getOrCreateSettableGauge(name, dimensions, publishingIntervalInSeconds) {
validateSettableGaugeOptions(name, dimensions, publishingIntervalInSeconds);
let settableGauge;
if (this._registry.hasMetric(name, dimensions)) {
settableGauge = this._registry.getMetric(name, dimensions);
} else {
settableGauge = new SettableGauge();
const key = this._registry.putMetric(name, settableGauge, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return settableGauge;
}
/**
* Creates a {@link CachedGauge} or gets the existing CachedGauge for a given name and dimension combo.
*
* @param {string} name The Metric name.
* @param {function} valueProducingPromiseCallback.
* @param {number} cachedGaugeUpdateIntervalInSeconds.
* @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric.
* @param {number} [publishingIntervalInSeconds] a optional custom publishing interval.
* @return {CachedGauge}
*/
getOrCreateCachedGauge(
name,
valueProducingPromiseCallback,
cachedGaugeUpdateIntervalInSeconds,
dimensions,
publishingIntervalInSeconds
) {
validateCachedGaugeOptions(name, valueProducingPromiseCallback, dimensions, publishingIntervalInSeconds);
let cachedGauge;
if (this._registry.hasMetric(name, dimensions)) {
cachedGauge = this._registry.getMetric(name, dimensions);
} else {
cachedGauge = new CachedGauge(valueProducingPromiseCallback, cachedGaugeUpdateIntervalInSeconds);
const key = this._registry.putMetric(name, cachedGauge, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return cachedGauge;
}
/**
* Calls end on all metrics in the registry that support end() and calls end on the reporter
*/
shutdown() {
// shutdown the reporter
this._reporters.forEach(reporter => reporter.shutdown());
// shutdown any metrics that have an end method
this._registry.allKeys().forEach(key => {
const metricWrapper = this._registry.getMetricWrapperByKey(key);
if (metricWrapper.metricImpl.end) {
metricWrapper.metricImpl.end();
}
});
}
}
module.exports = SelfReportingMetricsRegistry;
/**
* Configurable options for the Self Reporting Metrics Registry
*
* @interface SelfReportingMetricsRegistryOptions
* @typedef SelfReportingMetricsRegistryOptions
* @property {Logger} logger the Logger to use
* @property {string} logLevel The Log level to use if defaulting to included logger
* @property {DimensionAwareMetricsRegistry} registry The registry to use, defaults to new DimensionAwareMetricsRegistry
*/
================================================
FILE: packages/measured-reporting/lib/reporters/LoggingReporter.js
================================================
const Reporter = require('./Reporter');
/**
* A reporter impl that simply logs the metrics via the Logger.
*
* @example
* const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting');
* const registry = new SelfReportingMetricsRegistry(new LoggingReporter());
*
* @extends {Reporter}
*/
class LoggingReporter extends Reporter {
/**
* @param {LoggingReporterOptions} [options]
*/
constructor(options) {
super(options);
const level = (options || {}).logLevelToLogAt;
this._logLevel = (level || 'info').toLowerCase();
}
/**
* Logs the metrics via the inherited logger instance.
* @param {MetricWrapper[]} metrics
* @protected
*/
_reportMetrics(metrics) {
metrics.forEach(metric => {
this._log[this._logLevel](
JSON.stringify({
metricName: metric.name,
dimensions: this._getDimensions(metric),
data: metric.metricImpl.toJSON()
})
);
});
}
}
module.exports = LoggingReporter;
/**
* @interface LoggingReporterOptions
* @typedef LoggingReporterOptions
* @type {Object}
* @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported
* @property {Logger} [logger] The logger to use, if not supplied a new Buynan logger will be created
* @property {string} [logLevel] The log level to use with the created console logger if you didn't supply your own logger.
* @property {number} [defaultReportingIntervalInSeconds] The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds.
* @property {string} [logLevelToLogAt] You can specify the log level ['debug', 'info', 'warn', 'error'] that this reporter will use when logging the metrics via the logger.
*/
================================================
FILE: packages/measured-reporting/lib/reporters/Reporter.js
================================================
const consoleLogLevel = require('console-log-level');
const Optional = require('optional-js');
const { validateReporterParameters } = require('../validators/inputValidators');
const DEFAULT_REPORTING_INTERVAL_IN_SECONDS = 10;
function prefix() {
return `${new Date().toISOString()}: `;
}
/**
* The abstract reporter that specific implementations can extend to create a Self Reporting Metrics Registry Reporter.
*
* {@link SelfReportingMetricsRegistry}
*
* @example
* const os = require('os');
* const process = require('process');
* const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting');
*
* // Create a self reporting registry with a named anonymous reporter instance;
* const registry = new SelfReportingMetricsRegistry(
* new class ConsoleReporter extends Reporter {
* constructor() {
* super({
* defaultDimensions: {
* hostname: os.hostname(),
* env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset'
* }
* })
* }
*
* _reportMetrics(metrics) {
* metrics.forEach(metric => {
* console.log(JSON.stringify({
* metricName: metric.name,
* dimensions: this._getDimensions(metric),
* data: metric.metricImpl.toJSON()
* }))
* });
* }
* }()
* );
*
* @example
* // Create a regular class that extends Reporter
* class LoggingReporter extends Reporter {
* _reportMetrics(metrics) {
* metrics.forEach(metric => {
* this._log.info(JSON.stringify({
* metricName: metric.name,
* dimensions: this._getDimensions(metric),
* data: metric.metricImpl.toJSON()
* }))
* });
* }
* }
*
* @abstract
*/
class Reporter {
/**
* @param {ReporterOptions} [options] The optional params to supply when creating a reporter.
*/
constructor(options) {
if (this.constructor === Reporter) {
throw new TypeError("Can't instantiate abstract class!");
}
options = options || {};
validateReporterParameters(options);
/**
* Map of intervals to metric keys, this will be used to look up what metrics should be reported at a given interval.
*
* @type {Object.>}
* @private
*/
this._intervalToMetric = {};
this._intervals = [];
/**
* Map of default dimensions, that should be sent with every metric.
*
* @type {Dimensions}
* @protected
*/
this._defaultDimensions = options.defaultDimensions || {};
/**
* Loggers to use, defaults to a new console logger if nothing is supplied in options
* @type {Logger}
* @protected
*/
this._log =
options.logger || consoleLogLevel({ name: 'Reporter', level: options.logLevel || 'info', prefix: prefix });
/**
* The default reporting interval, a number in seconds.
* If not overridden via the {@see ReporterOptions}, defaults to 10 seconds.
*
* @type {number}
* @protected
*/
this._defaultReportingIntervalInSeconds =
options.defaultReportingIntervalInSeconds || DEFAULT_REPORTING_INTERVAL_IN_SECONDS;
/**
* Flag to indicate if reporting timers should be unref'd.
* If not overridden via the {@see ReporterOptions}, defaults to false.
*
* @type {boolean}
* @protected
*/
this._unrefTimers = !!options.unrefTimers;
/**
* Flag to indicate if metrics should be reset on each reporting interval.
* If not overridden via the {@see ReporterOptions}, defaults to false.
*
* @type {boolean}
* @protected
*/
this._resetMetricsOnInterval = !!options.resetMetricsOnInterval;
}
/**
* Sets the registry, this must be called before reportMetricOnInterval.
*
* @param {DimensionAwareMetricsRegistry} registry
*/
setRegistry(registry) {
this._registry = registry;
}
/**
* Informs the reporter to report a metric on a given interval in seconds.
*
* @param {string} metricKey The metric key for the metric in the metric registry.
* @param {number} intervalInSeconds The interval in seconds to report the metric on.
*/
reportMetricOnInterval(metricKey, intervalInSeconds) {
intervalInSeconds = intervalInSeconds || this._defaultReportingIntervalInSeconds;
if (!this._registry) {
throw new Error(
'You must call setRegistry(registry) before telling a Reporter to report a metric on an interval.'
);
}
if (Object.prototype.hasOwnProperty.call(this._intervalToMetric, intervalInSeconds)) {
this._intervalToMetric[intervalInSeconds].add(metricKey);
} else {
this._intervalToMetric[intervalInSeconds] = new Set([metricKey]);
this._createIntervalCallback(intervalInSeconds);
setImmediate(() => {
this._reportMetricsWithInterval(intervalInSeconds);
});
}
}
/**
* Creates the timed callback loop for the given interval.
*
* @param {number} intervalInSeconds the interval in seconds for the timeout callback
* @private
*/
_createIntervalCallback(intervalInSeconds) {
this._log.debug(`_createIntervalCallback() called with intervalInSeconds: ${intervalInSeconds}`);
const timer = setInterval(() => {
this._reportMetricsWithInterval(intervalInSeconds);
}, intervalInSeconds * 1000);
if (this._unrefTimers) {
timer.unref();
}
this._intervals.push(timer);
}
/**
* Gathers all the metrics that have been registered to report on the given interval.
*
* @param {number} interval The interval to look up what metrics to report
* @private
*/
_reportMetricsWithInterval(interval) {
this._log.debug(`_reportMetricsWithInterval() called with intervalInSeconds: ${interval}`);
try {
Optional.of(this._intervalToMetric[interval]).ifPresent(metrics => {
const metricsToSend = [];
metrics.forEach(metricKey => {
metricsToSend.push(this._registry.getMetricWrapperByKey(metricKey));
});
this._reportMetrics(metricsToSend);
if (this._resetMetricsOnInterval) {
metricsToSend.forEach(({ name, metricImpl }) => {
if (metricImpl && metricImpl.reset) {
this._log.debug('Resetting metric', name);
metricImpl.reset();
}
});
}
});
} catch (error) {
this._log.error('Failed to send metrics to signal fx', error);
}
}
/**
* This method gets called with an array of {@link MetricWrapper} on an interval, when metrics should be reported.
*
* This is the main method that needs to get implemented when created an aggregator specific reporter.
*
* @param {MetricWrapper[]} metrics The array of metrics to report.
* @protected
* @abstract
*/
_reportMetrics(metrics) {
throw new TypeError('Abstract method _reportMetrics(metrics) must be implemented in implementation class');
}
/**
*
* @param {MetricWrapper} metric The Wrapped Metric Object.
* @return {Dimensions} The left merged default dimensions with the metric specific dimensions
* @protected
*/
_getDimensions(metric) {
return Object.assign({}, this._defaultDimensions, metric.dimensions);
}
/**
* Clears the intervals that are running to report metrics at an interval, and resets the state.
*/
shutdown() {
this._intervals.forEach(interval => clearInterval(interval));
this._intervals = [];
this._intervalToMetric = {};
}
}
/**
* Options for creating a {@link Reporter}
* @interface ReporterOptions
* @typedef ReporterOptions
* @type {Object}
* @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported
* @property {Logger} logger The logger to use, if not supplied a new Buynan logger will be created
* @property {string} logLevel The log level to use with the created console logger if you didn't supply your own logger.
* @property {number} defaultReportingIntervalInSeconds The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds.
* @property {boolean} unrefTimers Indicate if reporting timers should be unref'd, defaults to false.
* @property {boolean} resetMetricsOnInterval Indicate if metrics should be reset on each reporting interval, defaults to false.
*/
module.exports = Reporter;
================================================
FILE: packages/measured-reporting/lib/validators/inputValidators.js
================================================
const Optional = require('optional-js');
const { validateMetric } = require('measured-core').metricValidators;
/**
* This module contains various validators to validate publicly exposed input.
*
* @module inputValidators
*/
module.exports = {
/**
* Validates @{link Gauge} options.
*
* @param {string} name The metric name
* @param {function} callback The callback for the Gauge
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateGaugeOptions: (name, callback, dimensions, publishingIntervalInSeconds) => {
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
module.exports.validateNumberReturningCallback(callback);
},
/**
* Validates @{link Gauge} options.
*
* @param {string} name The metric name
* @param {function} callback The callback for the CachedGauge
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateCachedGaugeOptions: (name, callback, dimensions, publishingIntervalInSeconds) => {
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
// Should we validate the promise call back, it may be expensive or produce a race condition in some use-cases.
},
/**
* Validates the create histogram Options.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateHistogramOptions: (name, dimensions, publishingIntervalInSeconds) => {
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
},
/**
* Validates the create counter Options.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateCounterOptions: (name, dimensions, publishingIntervalInSeconds) => {
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
},
/**
* Validates the create timer Options.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateTimerOptions: (name, dimensions, publishingIntervalInSeconds) => {
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
},
/**
* Validates the create timer Options.
*
* @param {string} name The metric name
* @param {Metric} metric The metric instance
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateRegisterOptions: (name, metric, dimensions, publishingIntervalInSeconds) => {
module.exports.validateMetric(metric);
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
},
/**
* Validates the create settable gauge Options.
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateSettableGaugeOptions: (name, dimensions, publishingIntervalInSeconds) => {
module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds);
},
/**
* Validates the options that are common amoung all create metric methods
*
* @param {string} name The metric name
* @param {Dimensions} dimensions The optional custom dimensions
* @param {number} publishingIntervalInSeconds the optional publishing interval
*/
validateCommonMetricParameters: (name, dimensions, publishingIntervalInSeconds) => {
module.exports.validateMetricName(name);
module.exports.validateOptionalDimensions(dimensions);
module.exports.validateOptionalPublishingInterval(publishingIntervalInSeconds);
},
/**
* Validates the metric name.
*
* @param name The metric name.
*/
validateMetricName: name => {
const type = typeof name;
if (type !== 'string') {
throw new TypeError(`options.name is a required option and must be of type string, actual type: ${type}`);
}
},
/**
* Validates that a metric implements the metric interface.
*
* @function
* @name validateMetric
* @param {Metric} metric The object that is supposed to be a metric.
*/
validateMetric,
/**
* Validates the provided callback.
*
* @param callback The provided callback for a gauge.
*/
validateNumberReturningCallback: callback => {
const type = typeof callback;
if (type !== 'function') {
throw new TypeError(`options.callback is a required option and must be function, actual type: ${type}`);
}
const callbackType = typeof callback();
if (callbackType !== 'number') {
throw new TypeError(`options.callback must return a number, actual return type: ${callbackType}`);
}
},
/**
* Validates a set of optional dimensions
* @param dimensionsOptional
*/
validateOptionalDimensions: dimensionsOptional => {
Optional.ofNullable(dimensionsOptional).ifPresent(dimensions => {
const type = typeof dimensions;
if (type !== 'object') {
throw new TypeError(`options.dimensions should be an object, actual type: ${type}`);
}
if (Array.isArray(dimensions)) {
throw new TypeError('dimensions where detected to be an array, expected Object');
}
Object.keys(dimensions).forEach(key => {
const valueType = typeof dimensions[key];
if (valueType !== 'string') {
throw new TypeError(`options.dimensions.${key} should be of type string, actual type: ${type}`);
}
});
});
},
/**
* Validates that an optional logger instance at least has the methods we expect.
* @param loggerOptional
*/
validateOptionalLogger: loggerOptional => {
Optional.ofNullable(loggerOptional).ifPresent(logger => {
if (
typeof logger.debug !== 'function' ||
typeof logger.info !== 'function' ||
typeof logger.warn !== 'function' ||
typeof logger.error !== 'function'
) {
throw new TypeError(
'The logger that was passed in does not support all required ' +
'logging methods, expected object to have functions debug, info, warn, and error with ' +
'method signatures (...msgs) => {}'
);
}
});
},
/**
* Validates the optional publishing interval.
*
* @param publishingIntervalInSecondsOptional The optional publishing interval.
*/
validateOptionalPublishingInterval: publishingIntervalInSecondsOptional => {
Optional.ofNullable(publishingIntervalInSecondsOptional).ifPresent(publishingIntervalInSeconds => {
const type = typeof publishingIntervalInSeconds;
if (type !== 'number') {
throw new TypeError(`options.publishingIntervalInSeconds must be of type number, actual type: ${type}`);
}
});
},
/**
* Validates optional params for a Reporter
* @param {ReporterOptions} options The optional params
*/
validateReporterParameters: options => {
if (options) {
module.exports.validateOptionalDimensions(options.defaultDimensions);
module.exports.validateOptionalLogger(options.logger);
const type = typeof options.unrefTimers;
if (type !== 'boolean' && type !== 'undefined') {
throw new TypeError(`options.unrefTimers should be a boolean or undefined, actual type: ${type}`);
}
}
},
/**
* Validates that a valid Reporter object has been supplied
*
* @param {Reporter} reporter
*/
validateReporterInstance: reporter => {
if (!reporter) {
throw new TypeError('The reporter was undefined, when it was required');
}
if (typeof reporter.setRegistry !== 'function') {
throw new TypeError(
'A reporter must implement setRegistry(registry), see the abstract Reporter class in the docs.'
);
}
if (typeof reporter.reportMetricOnInterval !== 'function') {
throw new TypeError(
'A reporter must implement reportMetricOnInterval(metricKey, intervalInSeconds), see the abstract Reporter class in the docs.'
);
}
},
/**
* Validates the input parameters for a {@link SelfReportingMetricsRegistry}
* @param {Reporter[]} reporters
* @param {SelfReportingMetricsRegistryOptions} [options]
*/
validateSelfReportingMetricsRegistryParameters: (reporters, options) => {
reporters.forEach(reporter => module.exports.validateReporterInstance(reporter));
if (options) {
module.exports.validateOptionalLogger(options.logger);
}
}
};
================================================
FILE: packages/measured-reporting/package.json
================================================
{
"name": "measured-reporting",
"description": "The classes needed to create self reporting dimension aware metrics registries",
"version": "2.0.0",
"homepage": "https://yaorg.github.io/node-measured/",
"engines": {
"node": ">= 5.12"
},
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"scripts": {
"clean": "rm -fr build",
"format": "prettier --write './lib/**/*.{ts,js}'",
"lint": "eslint lib --ext .js",
"test:node": "mocha './test/**/test-*.js'",
"test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'",
"test:browser": "exit 0",
"test": "yarn test:node:coverage",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"repository": {
"url": "git://github.com/yaorg/node-measured.git"
},
"dependencies": {
"console-log-level": "^1.4.1",
"mapcap": "^1.0.0",
"measured-core": "^2.0.0",
"optional-js": "^2.0.0"
},
"files": [
"lib",
"README.md"
],
"license": "MIT",
"devDependencies": {
"jsdoc": "^3.5.5",
"loglevel": "^1.6.1",
"winston": "^2.4.2"
}
}
================================================
FILE: packages/measured-reporting/test/unit/registries/test-DimensionAwareMetricsRegistry.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const { Counter } = require('measured-core');
const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry');
describe('DimensionAwareMetricsRegistry', () => {
let registry;
beforeEach(() => {
registry = new DimensionAwareMetricsRegistry();
});
it('hasMetric() returns true after putMetric() and getMetric() retrieves it, and it has the expected value', () => {
const counter = new Counter({
count: 10
});
const metricName = 'counter';
const dimensions = {
foo: 'bar'
};
assert(!registry.hasMetric(metricName, dimensions));
registry.putMetric(metricName, counter, dimensions);
assert(registry.hasMetric(metricName, dimensions));
assert(counter === registry.getMetric(metricName, dimensions));
assert.equal(10, registry.getMetric(metricName, dimensions).toJSON());
});
it('getMetricByKey() returns the proper metric wrapper', () => {
const counter = new Counter({
count: 10
});
const metricName = 'counter';
const dimensions = {
foo: 'bar'
};
const key = registry.putMetric(metricName, counter, dimensions);
assert(key.includes('counter-bar'));
const wrapper = registry.getMetricWrapperByKey(key);
assert.deepEqual(counter, wrapper.metricImpl);
assert.deepEqual(dimensions, wrapper.dimensions);
assert.equal(metricName, wrapper.name);
});
it('#_generateStorageKey generates the same key for a metric name and dimensions with different ordering', () => {
const metricName = 'the-metric-name';
const demensions1 = {
foo: 'bar',
bam: 'boo'
};
const demensions2 = {
bam: 'boo',
foo: 'bar'
};
const key1 = registry._generateStorageKey(metricName, demensions1);
const key2 = registry._generateStorageKey(metricName, demensions2);
assert.equal(key1, key2);
});
it('#_generateStorageKey generates the same key for a metric name and dimensions when called 2x', () => {
const metricName = 'the-metric-name';
const demensions1 = {
foo: 'bar',
bam: 'boo'
};
const key1 = registry._generateStorageKey(metricName, demensions1);
const key2 = registry._generateStorageKey(metricName, demensions1);
assert.equal(key1, key2);
});
it('#_generateStorageKey generates the same key for a metric name and no dimensions when called 2x', () => {
const metricName = 'the-metric-name';
const demensions1 = {};
const key1 = registry._generateStorageKey(metricName, demensions1);
const key2 = registry._generateStorageKey(metricName, demensions1);
assert.equal(key1, key2);
});
it('metricLimit limits metric count', () => {
const limitedRegistry = new DimensionAwareMetricsRegistry({
metricLimit: 10
});
const counter = new Counter({
count: 10
});
const dimensions = {
foo: 'bar'
};
for (let i = 0; i < 20; i++) {
limitedRegistry.putMetric(`metric #${i}`, counter, dimensions);
}
assert.equal(10, limitedRegistry._metrics.size);
assert(!limitedRegistry.hasMetric('metric #0', dimensions));
});
it('lru changes metric dropping strategy', () => {
const limitedRegistry = new DimensionAwareMetricsRegistry({
metricLimit: 10,
lru: true
});
const counter = new Counter({
count: 10
});
const dimensions = {
foo: 'bar'
};
for (let i = 0; i < 10; i++) {
limitedRegistry.putMetric(`metric #${i}`, counter, dimensions);
}
// Touch the first added metric
limitedRegistry.getMetric('metric #0', dimensions);
// Put a new metric in to trigger a drop
limitedRegistry.putMetric('metric #11', counter, dimensions);
// Verify that it dropped metric #1, not metric #0
assert(limitedRegistry.hasMetric('metric #0', dimensions));
assert(!limitedRegistry.hasMetric('metric #1', dimensions));
});
});
================================================
FILE: packages/measured-reporting/test/unit/registries/test-SelfReportingMetricsRegistry.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const sinon = require('sinon');
const { Counter } = require('measured-core');
const { SelfReportingMetricsRegistry } = require('../../../lib');
const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry');
describe('SelfReportingMetricsRegistry', () => {
let selfReportingRegistry;
let reporter;
let mockReporter;
let registry;
beforeEach(() => {
registry = new DimensionAwareMetricsRegistry();
reporter = {
reportMetricOnInterval() {},
setRegistry() {}
};
mockReporter = sinon.mock(reporter);
selfReportingRegistry = new SelfReportingMetricsRegistry(reporter, { registry });
});
it('throws an error if a metric has already been registered', () => {
registry.putMetric('my-metric', new Counter(), {});
assert.throws(() => {
selfReportingRegistry.register('my-metric', new Counter(), {});
});
});
it('#register registers the metric and informs the reporter to report', () => {
const metricName = 'foo';
const reportInterval = 1;
const metricKey = metricName;
mockReporter
.expects('reportMetricOnInterval')
.once()
.withArgs(metricKey, reportInterval);
selfReportingRegistry.register(metricKey, new Counter(), {}, reportInterval);
assert.equal(1, registry._metrics.size);
mockReporter.restore();
mockReporter.verify();
});
it('#getOrCreateGauge creates a gauge and when called a second time returns the same gauge', () => {
mockReporter.expects('reportMetricOnInterval').once();
const gauge = selfReportingRegistry.getOrCreateGauge('the-metric-name', () => 10, {}, 1);
const theSameGauge = selfReportingRegistry.getOrCreateGauge('the-metric-name', () => 10, {}, 1);
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(gauge, theSameGauge);
});
it('#getOrCreateHistogram creates and registers the metric and when called a second time returns the same metric', () => {
mockReporter.expects('reportMetricOnInterval').once();
const metric = selfReportingRegistry.getOrCreateHistogram('the-metric-name', {}, 1);
const theSameMetric = selfReportingRegistry.getOrCreateHistogram('the-metric-name', {}, 1);
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(metric, theSameMetric);
});
it('#getOrCreateMeter creates and registers the metric and when called a second time returns the same metric', () => {
mockReporter.expects('reportMetricOnInterval').once();
const metric = selfReportingRegistry.getOrCreateMeter('the-metric-name', {}, 1);
const theSameMetric = selfReportingRegistry.getOrCreateMeter('the-metric-name', {}, 1);
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(metric, theSameMetric);
metric.end();
});
it('#getOrCreateCounter creates and registers the metric and when called a second time returns the same metric', () => {
mockReporter.expects('reportMetricOnInterval').once();
const metric = selfReportingRegistry.getOrCreateCounter('the-metric-name', {}, 1);
const theSameMetric = selfReportingRegistry.getOrCreateCounter('the-metric-name', {}, 1);
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(metric, theSameMetric);
});
it('#getOrCreateTimer creates and registers the metric and when called a second time returns the same metric', () => {
mockReporter.expects('reportMetricOnInterval').once();
const metric = selfReportingRegistry.getOrCreateTimer('the-metric-name', {}, 1);
const theSameMetric = selfReportingRegistry.getOrCreateTimer('the-metric-name', {}, 1);
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(metric, theSameMetric);
metric.end();
});
it('#getOrCreateSettableGauge creates and registers the metric and when called a second time returns the same metric', () => {
mockReporter.expects('reportMetricOnInterval').once();
const metric = selfReportingRegistry.getOrCreateSettableGauge('the-metric-name', {}, 1);
const theSameMetric = selfReportingRegistry.getOrCreateSettableGauge('the-metric-name', {}, 1);
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(metric, theSameMetric);
});
it('#getOrCreateCachedGauge creates and registers the metric and when called a second time returns the same metric', () => {
mockReporter.expects('reportMetricOnInterval').once();
const metric = selfReportingRegistry.getOrCreateCachedGauge('the-metric-name', () => {
return new Promise((r) => { r(10); });
}, 1, {}, 1);
const theSameMetric = selfReportingRegistry.getOrCreateCachedGauge('the-metric-name', () => {
return new Promise((r) => { r(10); });
}, 1, {}, 1);
// clear the interval
metric.end();
mockReporter.restore();
mockReporter.verify();
assert.deepEqual(metric, theSameMetric);
});
});
================================================
FILE: packages/measured-reporting/test/unit/reporters/test-LoggingReporter.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const { LoggingReporter } = require('../../../lib');
describe('LoggingReporter', () => {
let loggedMessages = [];
let logger;
beforeEach(() => {
logger = {
debug: (...msgs) => {
loggedMessages.push('debug: ', ...msgs);
},
info: (...msgs) => {
loggedMessages.push('info: ', ...msgs);
},
warn: (...msgs) => {
loggedMessages.push('warn: ', ...msgs);
},
error: (...msgs) => {
loggedMessages.push('error: ', ...msgs);
}
};
});
it('uses the supplied log level', () => {
let reporter = new LoggingReporter({
logger: logger,
logLevelToLogAt: 'debug'
});
reporter._reportMetrics([{
name: 'test',
dimensions: {},
metricImpl: {toJSON: () => 5}
}]);
assert.equal(loggedMessages.shift(), "debug: ");
assert.equal(loggedMessages.shift(), "{\"metricName\":\"test\",\"dimensions\":{},\"data\":5}")
});
it('defaults to info level, if no override supplied', () => {
it('uses the supplied log level', () => {
let reporter = new LoggingReporter({
logger: logger,
});
reporter._reportMetrics([{
name: 'test',
dimensions: {},
metricImpl: {toJSON: () => 5}
}]);
assert.equal(loggedMessages.shift(), "info: ");
assert.equal(loggedMessages.shift(), "{\"metricName\":\"test\",\"dimensions\":{},\"data\":5}")
});
});
});
================================================
FILE: packages/measured-reporting/test/unit/reporters/test-Reporter.js
================================================
/*global describe, it, beforeEach, afterEach*/
const assert = require('assert');
const TimeUnits = require('measured-core').units;
const { Counter } = require('measured-core');
const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry');
const { Reporter } = require('../../../lib');
const { validateReporterInstance } = require('../../../lib/validators/inputValidators');
/**
* @extends Reporter
*/
class TestReporter extends Reporter {
constructor(options) {
super(options);
this._reportedMetrics = [];
}
getReportedMetrics() {
return this._reportedMetrics;
}
_reportMetrics(metrics) {
this._reportedMetrics.push(metrics);
}
}
describe('Reporter', () => {
let reporter;
let counter = new Counter({ count: 5 });
let metricName = 'my-test-metric-key';
let metricKey;
let metricInterval = 1;
let metricDimensions = {
hostname: 'instance-hostname',
foo: 'bar'
};
let registry;
beforeEach(() => {
registry = new DimensionAwareMetricsRegistry();
metricKey = registry.putMetric(metricName, counter, metricDimensions);
reporter = new TestReporter();
reporter.setRegistry(registry);
});
it('throws an error if you try to instantiate the abstract class', () => {
assert.throws(() => {
new Reporter();
}, /^TypeError: Can\'t instantiate abstract class\!$/);
});
it('throws an error if _reportMetrics is not implemented', () => {
class BadImpl extends Reporter {}
assert.throws(() => {
const badImpl = new BadImpl();
badImpl._reportMetrics([]);
}, /method _reportMetrics\(metrics\) must be implemented/);
});
it('throws an error if reportMetricOnInterval is called before setRegistry', () => {
assert.throws(() => {
let unsetReporter = new TestReporter();
unsetReporter.reportMetricOnInterval(metricKey, metricInterval);
}, /must call setRegistry/);
});
it('_reportMetricsWithInterval reports the test metric wrapper', () => {
reporter._intervalToMetric[metricInterval] = new Set([metricKey]);
reporter._reportMetricsWithInterval(metricInterval);
assert.equal(reporter.getReportedMetrics().length, 1);
assert.deepEqual(reporter.getReportedMetrics().shift(), [registry.getMetricWrapperByKey(metricKey)]);
});
it('should report 5 times in a 5 second window with a metric set to be reporting every 1 second', (done, fail) => {
reportAndWait(reporter, metricKey, metricInterval)
.then(() => {
reporter.shutdown();
const numberOfReports = reporter.getReportedMetrics().length;
assert.equal(numberOfReports, 5);
done();
})
.catch(() => {
reporter.shutdown();
assert.fail('', '', '');
});
}).timeout(10000);
it('should only create 1 interval for 2 metrics with the same reporting interval', () => {
reporter.reportMetricOnInterval(metricKey, metricInterval);
metricKey = registry.putMetric('foo', counter, metricDimensions);
reporter.reportMetricOnInterval('foo', metricInterval);
const intervalCount = reporter._intervals.length;
assert.equal(1, intervalCount);
reporter.shutdown();
});
it('should left merge dimensions with the metric dimensions taking precedence when _getDimensions is called', () => {
let defaultDimensions = {
hostname: 'instance-hostname',
foo: 'bar'
};
reporter = new TestReporter({ defaultDimensions });
const customDimensions = {
foo: 'bam',
region: 'us-west-2'
};
const merged = reporter._getDimensions({ dimensions: customDimensions });
const expected = {
hostname: 'instance-hostname',
foo: 'bam',
region: 'us-west-2'
};
assert.deepEqual(expected, merged);
});
it('Can be used to create an anonymous instance of a reporter', () => {
const anonymousReporter = new class extends Reporter {
_reportMetrics(metrics) {
metrics.forEach(metric => console.log(JSON.stringify(metric)));
}
}();
validateReporterInstance(anonymousReporter);
});
it('unrefs timers, when configured to', () => {
let calledUnref = false;
const timer = setTimeout(() => {}, 100);
clearTimeout(timer);
const proto = timer.constructor.prototype;
const { unref } = proto;
proto.unref = function wrappedUnref() {
calledUnref = true;
return unref.call(this);
};
reporter = new TestReporter({ unrefTimers: true });
reporter.setRegistry(registry);
reporter.reportMetricOnInterval(metricKey, metricInterval);
proto.unref = unref;
assert.ok(calledUnref);
});
it('resets metrics, when configured to', () => {
reporter = new TestReporter({ resetMetricsOnInterval: true });
reporter.setRegistry(registry);
reporter._intervalToMetric[metricInterval] = new Set([metricKey]);
reporter._reportMetricsWithInterval(metricInterval);
const [[{ metricImpl }]] = reporter.getReportedMetrics();
assert.equal(metricImpl.toJSON(), 0);
});
});
const reportAndWait = (reporter, metricKey, metricInterval) => {
return new Promise(resolve => {
reporter.reportMetricOnInterval(metricKey, metricInterval);
setTimeout(() => {
resolve();
}, 5 * TimeUnits.SECONDS);
});
};
================================================
FILE: packages/measured-reporting/test/unit/validators/test-inputValidators.js
================================================
/*global describe, it, beforeEach, afterEach*/
const consoleLogLevel = require('console-log-level');
const loglevel = require('loglevel');
const winston = require('winston');
const assert = require('assert');
const { Counter } = require('measured-core');
const {
validateGaugeOptions,
validateOptionalLogger,
validateMetric,
validateReporterInstance,
validateSelfReportingMetricsRegistryParameters,
validateOptionalPublishingInterval,
validateOptionalDimensions,
validateMetricName,
validateNumberReturningCallback
} = require('../../../lib/validators/inputValidators');
describe('validateGaugeOptions', () => {
it('it does nothing for the happy path', () => {
validateGaugeOptions('foo', () => 10, { foo: 'bar' }, 1);
});
it('throws an error if the call back returns an object', () => {
assert.throws(() => {
validateGaugeOptions(
'foo',
() => {
return { value: 10, anotherValue: 10 };
},
{ foo: 'bar' },
1
);
}, /options.callback must return a number, actual return type: object/);
});
});
describe('validateNumberReturningCallback', () => {
it('throws an error if a non function is supplied', () => {
assert.throws(() => {
validateNumberReturningCallback({});
}, /must be function/);
});
});
describe('validateOptionalLogger', () => {
it('validates a Buynan logger', () => {
const logger = consoleLogLevel({ name: 'consoleLogLevel-logger' });
validateOptionalLogger(logger);
});
it('validates a Winston logger', () => {
validateOptionalLogger(winston);
});
it('validates a Loglevel logger', () => {
validateOptionalLogger(loglevel);
});
it('validates an artisanal logger', () => {
validateOptionalLogger({
debug: (...msgs) => {
console.log('debug: ', ...msgs);
},
info: (...msgs) => {
console.log('info: ', ...msgs);
},
warn: (...msgs) => {
console.log('warn: ', ...msgs);
},
error: (...msgs) => {
console.log('error: ', ...msgs);
}
});
});
it('throws an error when a logger is missing an expected method', () => {
assert.throws(() => {
validateOptionalLogger({});
}, /The logger that was passed in does not support/);
});
it('does not throw an error if a logger is not passed in as an arg', () => {
validateOptionalLogger(null);
});
});
describe('validateMetric', () => {
it('throws an error if the metric is undefined', () => {
assert.throws(() => {
validateMetric(undefined);
}, /^TypeError: The metric was undefined, when it was required$/);
});
it('throws an error if the metric is null', () => {
assert.throws(() => {
validateMetric(null);
}, /^TypeError: The metric was undefined, when it was required$/);
});
it('throws an error if toJSON is not a function', () => {
assert.throws(() => {
validateMetric({});
}, /must implement toJSON()/);
});
it('throws an error if getType is not a function', () => {
assert.throws(() => {
validateMetric({ toJSON: () => {} });
}, /must implement getType()/);
});
it('throws an error if #getType() does not return an expected value', () => {
assert.throws(() => {
validateMetric({
toJSON: () => {},
getType: () => {
return 'foo';
}
});
}, /Metric#getType\(\), must return a type defined in MetricsTypes/);
});
it('does nothing when a valid metric is supplied', () => {
validateMetric(new Counter());
});
});
describe('validateReporterInstance', () => {
it('throws an error if undefined was passed in', () => {
assert.throws(() => {
validateReporterInstance(null);
}, /The reporter was undefined/);
});
it('throws an error if setRegistry is not a function', () => {
assert.throws(() => {
validateReporterInstance({});
}, /must implement setRegistry/);
});
it('throws an error if reportMetricOnInterval is not a function', () => {
assert.throws(() => {
validateReporterInstance({ setRegistry: () => {} });
}, /must implement reportMetricOnInterval/);
});
it('does nothing for a valid reporter instance', () => {
validateReporterInstance({ setRegistry: () => {}, reportMetricOnInterval: () => {} });
});
});
describe('validateSelfReportingMetricsRegistryParameters', () => {
it('does nothing when a reporter is passed in', () => {
validateSelfReportingMetricsRegistryParameters([{ setRegistry: () => {}, reportMetricOnInterval: () => {} }]);
});
});
describe('validateOptionalPublishingInterval', () => {
it('throws an error if validateOptionalPublishingInterval is not a number', () => {
assert.throws(() => {
validateOptionalPublishingInterval('1');
}, /must be of type number/);
});
});
describe('validateOptionalDimensions', () => {
it('throws an error if passed dimensions is not an object', () => {
assert.throws(() => {
validateOptionalDimensions(1);
}, /options.dimensions should be an object/);
});
it('throws an error if passed dimensions is not an object.', () => {
assert.throws(() => {
validateOptionalDimensions(['thing', 'otherthing']);
}, /dimensions where detected to be an array/);
});
it('throws an error if passed dimensions is not an object has non string values for demension keys', () => {
assert.throws(() => {
validateOptionalDimensions({ someKeyThatIsANumber: 1 });
}, /should be of type string/);
});
});
describe('validateMetricName', () => {
it('throw an error if a non-string is passed', () => {
assert.throws(() => {
validateMetricName({});
}, /options.name is a required option and must be of type string/);
});
});
================================================
FILE: packages/measured-signalfx-reporter/README.md
================================================
# Measured SignalFx Reporter
This package ties together [measured-core](../measured-core) and [measured-reporting](../measured-reporting) to create a dimensional aware self reporting metrics registry that reports metrics to [SignalFx](https://signalfx.com/).
## Install
```
npm install measured-signalfx-reporter
```
## What is in this package
### [SignalFxMetricsReporter](https://yaorg.github.io/node-measured/SignalFxMetricsReporter.html)
A SignalFx specific implementation of the [Reporter Abstract Class](https://yaorg.github.io/node-measured/Reporter.html).
### [SignalFxSelfReportingMetricsRegistry](https://yaorg.github.io/node-measured/SignalFxSelfReportingMetricsRegistry.html)
Extends [Self Reporting Metrics Registry](https://yaorg.github.io/node-measured/SelfReportingMetricsRegistry.html) but overrides methods that generate Meters to use the NoOpMeter.
### NoOpMeters
Please note that this package ignores Meters by default. Meters do not make sense to use with SignalFx because the same
values can be calculated using simple counters and the aggregation functions available within SignalFx itself.
Additionally, this saves you money because SignalFx charges based on your DPM (Datapoints per Minute) consumption.
This can be changed if anyone has a good argument for using Meters. Please file an issue.
### Usage
See the full end to end example here: [SignalFx Express Full End to End Example](https://yaorg.github.io/node-measured/packages/measured-signalfx-reporter/tutorial-SignalFx%20Express%20Full%20End%20to%20End%20Example.html)
### Dev
There is a user acceptance test server to test this library end-to-end with [SignalFx](https://signalfx.com/).
```bash
SIGNALFX_API_KEY=xxxxx yarn uat:server
```
================================================
FILE: packages/measured-signalfx-reporter/lib/SignalFxEventCategories.js
================================================
/**
* Different categories of events supported, within the SignalFx Event API.
*
* @example
* const registry = new SignalFxSelfReportingMetricsRegistry(...);
* registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT);
*
* @module SignalFxEventCategories
*/
module.exports = {
/**
* Created by user via UI or API, e.g. a deployment event
* @type {SignalFxEventCategoryId}
*/
USER_DEFINED: 'USER_DEFINED',
/**
* Output by anomaly detectors
* @type {SignalFxEventCategoryId}
*/
ALERT: 'ALERT',
/**
* Audit trail events
* @type {SignalFxEventCategoryId}
*/
AUDIT: 'AUDIT',
/**
* Generated by analytics server
* @type {SignalFxEventCategoryId}
*/
JOB: 'JOB',
/**
* Event originated within collectd
* @type {SignalFxEventCategoryId}
*/
COLLECTD: 'COLLECTD',
/**
* Service discovery event
* @type {SignalFxEventCategoryId}
*/
SERVICE_DISCOVERY: 'SERVICE_DISCOVERY',
/**
* Created by exception appenders to denote exceptional events
* @type {SignalFxEventCategoryId}
*/
EXCEPTION: 'EXCEPTION'
};
/**
* @interface SignalFxEventCategoryId
* @typedef SignalFxEventCategoryId
* @type {string}
* @example
* const registry = new SignalFxSelfReportingMetricsRegistry(...);
* registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT);
*/
================================================
FILE: packages/measured-signalfx-reporter/lib/index.js
================================================
const SignalFxMetricsReporter = require('./reporters/SignalFxMetricsReporter');
const SignalFxSelfReportingMetricsRegistry = require('./registries/SignalFxSelfReportingMetricsRegistry');
const SignalFxEventCategories = require('./SignalFxEventCategories');
/**
* The main measured module that is referenced when require('measured-signalfx-reporter') is used.
* @module measured-signalfx-reporter
*/
module.exports = {
/**
* {@type SignalFxMetricsReporter}
*/
SignalFxMetricsReporter,
/**
* {@type SignalFxSelfReportingMetricsRegistry}
*/
SignalFxSelfReportingMetricsRegistry,
/**
* {@type SignalFxEventCategories}
*/
SignalFxEventCategories
};
================================================
FILE: packages/measured-signalfx-reporter/lib/registries/SignalFxSelfReportingMetricsRegistry.js
================================================
const { NoOpMeter, Timer } = require('measured-core');
const { SelfReportingMetricsRegistry } = require('measured-reporting');
const { validateTimerOptions } = require('measured-reporting').inputValidators;
/**
* A SignalFx Self Reporting Metrics Registry that disallows the use of meters.
* Meters don't make sense to use with SignalFx because the rate aggregations can be done within SignalFx itself.
* Meters simply waste DPM (Datapoints per Minute).
*
* @extends {SelfReportingMetricsRegistry}
*/
class SignalFxSelfReportingMetricsRegistry extends SelfReportingMetricsRegistry {
/**
* Creates a {@link Timer} or get the existing Timer for a given name and dimension combo with a NoOpMeter.
*
* @param {string} name The Metric name
* @param {Dimensions} dimensions any custom {@link Dimensions} for the Metric
* @param {number} publishingIntervalInSeconds a optional custom publishing interval
* @return {Timer}
*/
getOrCreateTimer(name, dimensions, publishingIntervalInSeconds) {
validateTimerOptions(name, dimensions, publishingIntervalInSeconds);
let timer;
if (this._registry.hasMetric(name, dimensions)) {
timer = this._registry.getMetric(name, dimensions);
} else {
timer = new Timer({ meter: new NoOpMeter() });
const key = this._registry.putMetric(name, timer, dimensions);
this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds));
}
return timer;
}
/**
* Meters are not reported to SignalFx.
* Meters do not make sense to use with SignalFx because the same values can be calculated
* using simple counters and aggregations within SignalFx itself.
*
* @param {string} name The Metric name
* @param {Dimensions} dimensions any custom {@link Dimensions} for the Metric
* @param {number} publishingIntervalInSeconds a optional custom publishing interval
* @return {NoOpMeter|*}
*/
getOrCreateMeter(name, dimensions, publishingIntervalInSeconds) {
this._log.error(
'Meters will not get reported using the SignalFx reporter as they waste DPM, please use a counter instead'
);
return new NoOpMeter();
}
/**
* Function exposes the event API of Signal Fx.
* See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details.
*
* @param {string} eventType The event type (name of the event time series).
* @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED.
* @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default
* @param {Object.} [properties] a map of extra properties on that event, empty dictionary by default
* @param {number} [timestamp] a timestamp, by default is current time.
*
* @example
* const {
* SignalFxSelfReportingMetricsRegistry,
* SignalFxMetricsReporter,
* SignalFxEventCategories
* } = require('measured-signalfx-reporter');
* const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient));
* registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT);
*/
sendEvent(eventType, category, dimensions, properties, timestamp) {
return Promise.all(
this._reporters.filter(reporter => typeof reporter.sendEvent === 'function').map(reporter =>
reporter.sendEvent(eventType, category, dimensions, properties, timestamp).catch(error => {
return error;
})
)
);
}
}
module.exports = SignalFxSelfReportingMetricsRegistry;
================================================
FILE: packages/measured-signalfx-reporter/lib/reporters/SignalFxMetricsReporter.js
================================================
const { Reporter } = require('measured-reporting');
const { MetricTypes } = require('measured-core');
const { validateSignalFxClient } = require('../validators/inputValidators');
const { USER_DEFINED } = require('../SignalFxEventCategories');
/**
* A Reporter that reports metrics to Signal Fx
* @extends {Reporter}
*/
class SignalFxMetricsReporter extends Reporter {
/**
* @param {SignalFxClient} signalFxClient The configured signal fx client.
* @param {ReporterOptions} [options] See {@link ReporterOptions}.
*/
constructor(signalFxClient, options) {
options = options || {};
super(options);
validateSignalFxClient(signalFxClient);
this._signalFxClient = signalFxClient;
this._log.debug(`SignalFx Metrics Reporter Created with the following default reporting interval: ${options.defaultReportingIntervalInSeconds}, default dimensions: ${JSON.stringify(options.defaultDimensions, null, 2)}`);
}
/**
* Sends metrics to signal fx, converting name and dimensions and {@link Metric} to data signal fx can ingest
* @param {MetricWrapper[]} metrics The array of metrics to send to signal fx.
* @protected
*/
_reportMetrics(metrics) {
this._log.debug('_reportMetrics() called');
let signalFxDataPointRequest = {};
metrics.forEach(metric => {
if (!metric) {
this._log.warn('Metric was null when it should not have been');
return;
}
signalFxDataPointRequest = this._processMetric(metric, signalFxDataPointRequest);
});
this._log.debug(`Sending data to Signal Fx. Request: ${JSON.stringify(signalFxDataPointRequest)}`);
this._signalFxClient.send(signalFxDataPointRequest).catch(error => {
this._log.error('Failed to send metrics to signal fx error:', error);
});
}
/**
* Method for getting raw signal fx api request values from the Timer Object.
*
* @param {MetricWrapper} metric metric The Wrapped Metric Object.
* @param {any} currentBuiltRequest The signal fx request that is being built.
* @return {any} the currentBuiltRequest The signal fx request that is being built with the given metric in it.
* @protected
*/
_processMetric(metric, currentBuiltRequest) {
const newRequest = Object.assign({}, currentBuiltRequest);
const { name, metricImpl } = metric;
const mergedDimensions = this._getDimensions(metric);
const valuesToProcess = this._getValuesToProcessForType(name, metricImpl);
valuesToProcess.forEach(metricValueTypeWrapper => {
const signalFxDataPointMetric = {
metric: metricValueTypeWrapper.metric,
value: metricValueTypeWrapper.value,
dimensions: mergedDimensions
};
if (Object.prototype.hasOwnProperty.call(newRequest, metricValueTypeWrapper.type)) {
newRequest[metricValueTypeWrapper.type].push(signalFxDataPointMetric);
} else {
newRequest[metricValueTypeWrapper.type] = [signalFxDataPointMetric];
}
});
return newRequest;
}
/**
* Maps Measured Metrics Object JSON outputs to their respective signal fx metrics using logic from
* com.signalfx.codahale.reporter.AggregateMetricSenderSessionWrapper in the java lib to derive naming
*
* @param {string} name The registered metric base name
* @param {Metric} metric The metric.
* @return {MetricValueTypeWrapper[]} an array of MetricValueTypeWrapper that can be used to
* build the SignalFx data point request
* @protected
*/
_getValuesToProcessForType(name, metric) {
const type = metric.getType();
switch (type) {
case MetricTypes.TIMER:
return this._getValuesToProcessForTimer(name, metric);
case MetricTypes.GAUGE:
return this._getValuesToProcessForGauge(name, metric);
case MetricTypes.COUNTER:
return this._getValuesToProcessForCounter(name, metric);
case MetricTypes.HISTOGRAM:
return this._getValuesToProcessForHistogram(name, metric);
case MetricTypes.METER:
this._log.warn(
'Meters are not reported to SignalFx. Meters do not make sense to use with SignalFx because the same values ' +
'can be calculated using simple counters and aggregations within SignalFx itself.'
);
return [];
default:
this._log.error(`Metric Type: ${type} has not been implemented to report to signal fx`);
return [];
}
}
/**
* Maps and Filters values from a Timer to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Timer} timer The Timer
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForTimer(name, timer) {
let valuesToProcess = [];
// only grab histogram data as Meters can be accomplished with signal fx using the count from the histogram
valuesToProcess = valuesToProcess.concat(this._getValuesToProcessForHistogram(name, timer._histogram));
return valuesToProcess;
}
/**
* Maps values from a Gauge to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Gauge} gauge The Gauge
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForGauge(name, gauge) {
const valuesToProcess = [];
valuesToProcess.push({
metric: `${name}`,
value: gauge.toJSON(),
type: SIGNAL_FX_GAUGE
});
return valuesToProcess;
}
/**
* Maps values from a Counter to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Counter} counter The data from the measure metric object
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForCounter(name, counter) {
const valuesToProcess = [];
valuesToProcess.push({
metric: `${name}.count`,
value: counter.toJSON(),
type: SIGNAL_FX_CUMULATIVE_COUNTER
});
return valuesToProcess;
}
/**
* Maps and Filters values from a Histogram to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Histogram} histogram The Histogram
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForHistogram(name, histogram) {
// TODO add full list of histogram metrics but enable filter
const data = histogram.toJSON();
const valuesToProcess = [];
valuesToProcess.push({
metric: `${name}.count`,
value: data.count || 0,
type: SIGNAL_FX_CUMULATIVE_COUNTER
});
valuesToProcess.push({
metric: `${name}.max`,
value: data.max || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.min`,
value: data.min || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.mean`,
value: data.mean || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.p95`,
value: data.p95 || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.p99`,
value: data.p99 || 0,
type: SIGNAL_FX_GAUGE
});
return valuesToProcess;
}
/**
* Function exposes the event API of Signal Fx.
* See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details.
*
* @param {string} eventType The event type (name of the event time series).
* @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED.
* @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default
* @param {Object.} [properties] a map of extra properties on that event, empty dictionary by default
* @param {number} [timestamp] a timestamp, by default is current time.
*
* @example
* const {
* SignalFxSelfReportingMetricsRegistry,
* SignalFxMetricsReporter,
* SignalFxEventCategories
* } = require('measured-signalfx-reporter');
* const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient));
* registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT);
*/
sendEvent(eventType, category, dimensions, properties, timestamp) {
const body = {
eventType,
category: category || USER_DEFINED,
dimensions: this._getDimensions({ dimensions }),
properties,
timestamp
};
Object.keys(body).forEach(key => body[key] == null && delete body[key]);
return this._signalFxClient.sendEvent(body);
}
}
// const SIGNAL_FX_COUNTER = 'counters';
const SIGNAL_FX_GAUGE = 'gauges';
const SIGNAL_FX_CUMULATIVE_COUNTER = 'cumulative_counters';
module.exports = SignalFxMetricsReporter;
/**
* Wrapper object to wrap metric value and SFX metadata needed to send metric value to SFX data ingestion.
*
* @interface MetricValueTypeWrapper
* @typedef MetricValueTypeWrapper
* @type {Object}
* @property {string} metric The metric name to report to SignalFx
* @property {number} value the value to report to SignalFx
* @property {string} type The mapped SignalFx metric type
*/
================================================
FILE: packages/measured-signalfx-reporter/lib/validators/inputValidators.js
================================================
/**
* Validation functions for validating public input
* @module SignalFxReporterInputValidators
* @private
*/
module.exports = {
/**
* Validates that the object supplied for the sfx client at least has a send function
* @param signalFxClient
*/
validateSignalFxClient: signalFxClient => {
if (signalFxClient === undefined) {
throw new Error('signalFxClient was undefined when it is required');
}
if (typeof signalFxClient.send !== 'function' || signalFxClient.length < 1) {
throw new Error('signalFxClient must implement send(data: any)');
}
}
};
================================================
FILE: packages/measured-signalfx-reporter/package.json
================================================
{
"name": "measured-signalfx-reporter",
"description": "A Registry Reporter that knows how to report core metrics to SignalFx",
"version": "2.0.0",
"homepage": "https://yaorg.github.io/node-measured/",
"engines": {
"node": ">= 5.12"
},
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"scripts": {
"clean": "rm -fr build",
"format": "prettier --write './lib/**/*.{ts,js}'",
"lint": "eslint lib --ext .js",
"test:node": "mocha './test/**/test-*.js'",
"test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'",
"test:browser": "exit 0",
"test": "yarn test:node:coverage",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"uat:server": "node --inspect test/user-acceptance-test/index.js"
},
"repository": {
"url": "git://github.com/yaorg/node-measured.git"
},
"dependencies": {
"console-log-level": "^1.4.1",
"measured-core": "^2.0.0",
"measured-reporting": "^2.0.0",
"optional-js": "^2.0.0"
},
"files": [
"lib",
"README.md"
],
"license": "MIT",
"devDependencies": {
"express": "^4.16.3",
"jsdoc": "^3.5.5",
"signalfx": "^6.0.0"
}
}
================================================
FILE: packages/measured-signalfx-reporter/test/unit/registries/test-SignalFxSelfReportingMetricsRegistry.js
================================================
/*global describe, it, beforeEach, afterEach*/
const { SignalFxSelfReportingMetricsRegistry } = require('../../../lib');
const { Reporter } = require('measured-reporting');
const sinon = require('sinon');
const assert = require('assert');
/**
* @extends Reporter
*/
class TestReporter extends Reporter {
reportMetricOnInterval(metricKey, intervalInSeconds) {}
_reportMetrics(metrics) {}
}
describe('SignalFxSelfReportingMetricsRegistry', () => {
let registry;
let reporter;
let mockReporter;
beforeEach(() => {
reporter = new TestReporter();
mockReporter = sinon.mock(reporter);
registry = new SignalFxSelfReportingMetricsRegistry(reporter);
});
it('#getOrCreateTimer uses a no-op meter', () => {
mockReporter.expects('reportMetricOnInterval').once();
const timer = registry.getOrCreateTimer('my-timer');
assert.equal(timer._meter.constructor.name, 'NoOpMeter');
const theSameTimer = registry.getOrCreateTimer('my-timer');
assert(timer === theSameTimer);
});
it('#getOrCreateMeter uses a no-op meter', () => {
mockReporter.expects('reportMetricOnInterval').never();
const meter = registry.getOrCreateMeter('my-meter');
assert.equal(meter.constructor.name, 'NoOpMeter');
});
});
================================================
FILE: packages/measured-signalfx-reporter/test/unit/reporters/test-SignalFxMetricsReporter.js
================================================
/*global describe, it, beforeEach, afterEach*/
const { SignalFxMetricsReporter } = require('../../../lib');
const { Histogram, MetricTypes, Gauge, Timer, Meter, Counter } = require('measured-core');
const assert = require('assert');
const sinon = require('sinon');
describe('SignalFxMetricsReporter', () => {
const name = 'request';
const dimensions = {
foo: 'bar'
};
let reporter;
let signalFxClient;
let clientSpy;
beforeEach(() => {
signalFxClient = {
send: data => {
return new Promise(resolve => {
resolve();
});
}
};
clientSpy = sinon.spy(signalFxClient, 'send');
// noinspection JSCheckFunctionSignatures
reporter = new SignalFxMetricsReporter(signalFxClient);
});
it('#_reportMetrics sends the expected data to signal fx for a histogram', () => {
const metric = new Histogram();
const metricMock = sinon.mock(metric);
metricMock
.expects('getType')
.once()
.returns(MetricTypes.HISTOGRAM);
metricMock
.expects('toJSON')
.once()
.returns({
min: 1,
max: 10,
sum: 100,
variance: 55,
mean: 5,
stddev: 54,
count: 20,
median: 50,
p75: 4,
p95: 6,
p99: 7,
p999: 9
});
const metricWrapper = {
name: name,
metricImpl: metric,
dimensions: dimensions
};
const expected = {
gauges: [
{
metric: `${name}.max`,
value: '10',
dimensions: dimensions
},
{
metric: `${name}.min`,
value: '1',
dimensions: dimensions
},
{
metric: `${name}.mean`,
value: '5',
dimensions: dimensions
},
{
metric: `${name}.p95`,
value: '6',
dimensions: dimensions
},
{
metric: `${name}.p99`,
value: '7',
dimensions: dimensions
}
],
cumulative_counters: [
{
metric: `${name}.count`,
value: '20',
dimensions: dimensions
}
]
};
reporter._reportMetrics([metricWrapper]);
assert(
clientSpy.withArgs(
sinon.match(actual => {
assert.deepEqual(expected, actual);
return true;
})
).calledOnce
);
});
it('#_reportMetrics sends the expected data to signal fx for a gauge', () => {
const metric = new Gauge(() => 10);
const metricWrapper = {
name: name,
metricImpl: metric,
dimensions: dimensions
};
const expected = {
gauges: [
{
metric: `${name}`,
value: '10',
dimensions: dimensions
}
]
};
reporter._reportMetrics([metricWrapper]);
assert(
clientSpy.withArgs(
sinon.match(actual => {
assert.deepEqual(expected, actual);
return true;
})
).calledOnce
);
});
it('#_reportMetrics sends the expected data to signal fx for a counter', () => {
const metric = new Counter({ count: 5 });
const metricWrapper = {
name: name,
metricImpl: metric,
dimensions: dimensions
};
const expected = {
cumulative_counters: [
{
metric: `${name}.count`,
value: '5',
dimensions: dimensions
}
]
};
reporter._reportMetrics([metricWrapper]);
assert(
clientSpy.withArgs(
sinon.match(actual => {
assert.deepEqual(expected, actual);
return true;
})
).calledOnce
);
});
it('#_reportMetrics sends the expected data to signal fx for a meter', () => {
const metric = new Meter();
const metricWrapper = {
name: name,
metricImpl: metric,
dimensions: dimensions
};
reporter._reportMetrics([metricWrapper]);
assert(clientSpy.withArgs({}).calledOnce);
metric.end();
});
it('#_reportMetrics sends the expected data to signal fx for an array multiple metrics', () => {
const metric = new Histogram();
const metricMock = sinon.mock(metric);
metricMock
.expects('getType')
.once()
.returns(MetricTypes.HISTOGRAM);
metricMock
.expects('toJSON')
.once()
.returns({
min: 1,
max: 10,
sum: 100,
variance: 55,
mean: 5,
stddev: 54,
count: 20,
median: 50,
p75: 4,
p95: 6,
p99: 7,
p999: 9
});
const histogramWRapper = {
name: name,
metricImpl: metric,
dimensions: dimensions
};
const counterWrapper = {
name: 'my-counter',
metricImpl: new Counter({ count: 6 })
};
const gaugeWrapper = {
name: 'my-gauge',
metricImpl: new Gauge(() => 8)
};
const expected = {
gauges: [
{
metric: `${name}.max`,
value: '10',
dimensions: dimensions
},
{
metric: `${name}.min`,
value: '1',
dimensions: dimensions
},
{
metric: `${name}.mean`,
value: '5',
dimensions: dimensions
},
{
metric: `${name}.p95`,
value: '6',
dimensions: dimensions
},
{
metric: `${name}.p99`,
value: '7',
dimensions: dimensions
},
{
metric: 'my-gauge',
value: '8',
dimensions: {}
}
],
cumulative_counters: [
{
metric: `${name}.count`,
value: '20',
dimensions: dimensions
},
{
metric: 'my-counter.count',
value: '6',
dimensions: {}
}
]
};
reporter._reportMetrics([histogramWRapper, counterWrapper, gaugeWrapper]);
assert(
clientSpy.withArgs(
sinon.match(actual => {
assert.deepEqual(expected, actual);
return true;
})
).calledOnce
);
});
it('#_reportMetrics doesnt add metrics tp]o send if a bad metric was supplied', () => {
reporter._reportMetrics([
{
name: 'something',
metricImpl: { getType: () => 'something random' }
}
]);
assert(clientSpy.withArgs({}).calledOnce);
});
it('#_reportMetrics sends the expected data to signal fx for a timer', () => {});
});
================================================
FILE: packages/measured-signalfx-reporter/test/unit/validators/test-inputValidators.js
================================================
/*global describe, it, beforeEach, afterEach*/
const { validateSignalFxClient } = require('../../../lib/validators/inputValidators');
const assert = require('assert');
describe('validateRequiredSignalFxMetricsReporterParameters', () => {
it('does nothing for the happy path', () => {
validateSignalFxClient({ send: () => {} });
});
it('throws an error when a bad signal fx client is supplied', () => {
assert.throws(() => {
validateSignalFxClient({});
}, /signalFxClient must implement send\(data: any\)/);
});
});
================================================
FILE: packages/measured-signalfx-reporter/test/user-acceptance-test/index.js
================================================
const signalfx = require('signalfx');
const express = require('express');
const consoleLogLevel = require('console-log-level');
const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry, SignalFxEventCategories } = require('../../lib');
const { createOSMetrics, createProcessMetrics, createExpressMiddleware } = require('../../../measured-node-metrics/lib');
const libraryMetadata = require('../../package');
const log = consoleLogLevel({ name: 'SelfReportingMetricsRegistry', level: 'info' });
const library = libraryMetadata.name;
const version = libraryMetadata.version;
const defaultDimensions = {
app: library,
app_version: version,
env: 'test'
};
/**
* Get your api key from a secrets provider of some kind.
*
* Good examples:
*
* S3 with KMS
* Cerberus
* AWS Secrets Manager
* Vault
* Confidant
*
* Bad examples:
*
* Checked into SCM in plaintext as a property
* Set as a plaintext environment variable
*
* @return {string} Returns the resolved Signal Fx Api Key
*/
const apiKeyResolver = () => {
// https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/
return process.env.SIGNALFX_API_KEY;
};
// Create the signal fx client
const signalFxClient = new signalfx.Ingest(apiKeyResolver(), {
userAgents: library
});
// Create the signal fx reporter with the client
const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, {
defaultDimensions: defaultDimensions,
defaultReportingIntervalInSeconds: 10,
logLevel: 'debug'
});
// Create the self reporting metrics registry with the signal fx reporter
const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' });
metricsRegistry.sendEvent('events.app.starting');
createOSMetrics(metricsRegistry);
createProcessMetrics(metricsRegistry);
const app = express();
// wire up the metrics middleware
app.use(createExpressMiddleware(metricsRegistry));
app.get('/hello', (req, res) => {
res.send('hello world');
});
app.get('/path2', (req, res) => {
res.send('path2');
});
app.listen(8080, () => log.info('Example app listening on port 8080!'));
process.on('SIGINT', async () => {
log.info('SIG INT, exiting');
await metricsRegistry.sendEvent('events.app.exiting');
process.exit(0);
});
process.on('uncaughtException', async (err) => {
log.error('There was an uncaught error', err);
await metricsRegistry.sendEvent('events.app.uncaught-exception', SignalFxEventCategories.ALERT, {err: JSON.stringify(err)});
});
================================================
FILE: scripts/generate-docs.sh
================================================
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT_DIR="${SCRIPT_DIR}/.."
DOCSTRAP_PATH="${ROOT_DIR}/node_modules/ink-docstrap/template/"
# Clear out old docs
rm -fr ${ROOT_DIR}/build/docs
# create the directory structure
mkdir -p ${ROOT_DIR}/build/docs/{img,packages/{measured-core,measured-reporting,measured-signalfx-reporter}}
# Copy the image assets
cp ${ROOT_DIR}/documentation/assets/measured.* ${ROOT_DIR}/build/docs/img/
# Copy our docpath customizations into the docstrap template dir
cp ${ROOT_DIR}/documentation/docstrap_customized/template/* ${DOCSTRAP_PATH}
# Generate the complete API docs for all packages
export PACKAGE_NAME=root
jsdoc --recurse --configure ./.jsdoc.json \
--tutorials ${ROOT_DIR}/tutorials \
--template ${DOCSTRAP_PATH} \
--readme ${ROOT_DIR}/Readme.md \
--destination build/docs/ \
${ROOT_DIR}/packages/**/lib/
# Create the docs for measured-core
export PACKAGE_NAME=measured-core
jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \
--tutorials ${ROOT_DIR}/tutorials \
--template ${DOCSTRAP_PATH} \
--readme ${ROOT_DIR}/packages/measured-core/README.md \
--destination build/docs/packages/measured-core/ \
${ROOT_DIR}/packages/measured-core/lib/
# Create the docs for measured-reporting
export PACKAGE_NAME=measured-reporting
jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \
--tutorials ${ROOT_DIR}/tutorials \
--template ${DOCSTRAP_PATH} \
--readme ${ROOT_DIR}/packages/measured-reporting/README.md \
--destination build/docs/packages/measured-reporting/ \
${ROOT_DIR}/packages/measured-reporting/lib/
# Create the docs for measured-signalfx-reporter
export PACKAGE_NAME=measured-signalfx-reporter
jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \
--tutorials ${ROOT_DIR}/tutorials \
--template ${DOCSTRAP_PATH} \
--readme ${ROOT_DIR}/packages/measured-signalfx-reporter/README.md \
--destination build/docs/packages/measured-signalfx-reporter/ \
${ROOT_DIR}/packages/measured-signalfx-reporter/lib/
# Create the docs for measured-node-metrics
export PACKAGE_NAME=measured-node-metrics
jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \
--tutorials ${ROOT_DIR}/tutorials \
--template ${DOCSTRAP_PATH} \
--readme ${ROOT_DIR}/packages/measured-node-metrics/README.md \
--destination build/docs/packages/measured-node-metrics/ \
${ROOT_DIR}/packages/measured-node-metrics/lib/
================================================
FILE: scripts/publish.sh
================================================
#!/bin/bash
######################################################################
#
# This script is intended to be used in a Travis CI/CD env.
# It assumes Travis has been set up with jq, and awk and the following secure env vars.
#
# GH_TOKEN: A github oath token with perms to create and push tags and call the releases API.
# NPM_TOKEN: A npm auth token that can publish the packages.
#
######################################################################
set -e
LATEST_RELEASE_DATA=$(curl -s --header "Accept: application/json" -L https://github.com/yaorg/node-measured/releases/latest)
LATEST_GITHUB_RELEASE=$(echo $LATEST_RELEASE_DATA | jq --raw-output ".tag_name" | sed 's/v\(.*\)/\1/')
CURRENT_VERSION=$(cat lerna.json | jq --raw-output ".version")
echo "Processing tag information to determine if release is major, minor or patch."
echo "The current version tag is: ${CURRENT_VERSION}"
echo "The new version tag is: ${LATEST_GITHUB_RELEASE}"
if [ -z ${GH_TOKEN} ]
then
echo "GH_TOKEN is null, you must supply oath token for github. Aborting!"
exit 1
fi
if [ -z ${NPM_TOKEN} ]
then
echo "NPM_TOKEN is null, you must supply auth token for npm. Aborting!"
exit 1
fi
if [ -z ${LATEST_GITHUB_RELEASE} ]
then
echo "NEW_VERSION is null, aborting!"
exit 1
fi
if [ -z ${CURRENT_VERSION} ]
then
echo "CURRENT_VERSION is null, aborting!"
exit 1
fi
if [ ${CURRENT_VERSION} == ${LATEST_GITHUB_RELEASE} ]
then
echo "The current version and the new version are the same, aborting!"
exit 1
fi
CD_VERSION=$(awk -v NEW_VERSION=${LATEST_GITHUB_RELEASE} -v CURRENT_VERSION=${CURRENT_VERSION} 'BEGIN{
split(NEW_VERSION,newVersionParts,/\./)
split(CURRENT_VERSION,currentVersionParts,/\./)
for (i=1;i in currentVersionParts;i++) {
if (newVersionParts[i] != currentVersionParts[i]) {
if (i == 1) {
printf "major\n"
}
if (i == 2) {
printf "minor\n"
}
if (i == 3) {
printf "patch\n"
}
break
}
}
}')
echo
echo "determined to use semver: '${CD_VERSION}' flag for lerna publish --cd-version"
echo
echo "Re-wireing origin remote to use GH_TOKEN"
git remote rm origin
git remote add origin https://fieldju:${GH_TOKEN}@github.com/yaorg/node-measured.git
git fetch --all
git checkout master
echo "Deleting tag created by github to allow lerna to create it"
RELEASE="v${LATEST_GITHUB_RELEASE}"
git tag -d ${RELEASE}
git push origin :refs/tags/${RELEASE}
echo "Preparing .npmrc"
echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc
echo 'registry=http://registry.npmjs.org' >> .npmrc
lerna publish --cd-version ${CD_VERSION} --yes --force-publish
echo "Re-binding orphaned github release to tag, so that it shows up as latest release rather than draft release"
RELEASE_ID=$(curl -s --header "Accept: application/vnd.github.v3+json" -H "Authorization: token ${GH_TOKEN}" -L https://api.github.com/repos/yaorg/node-measured/releases | jq --arg RELEASE ${RELEASE} -r '.[] | select(.name==$RELEASE) | .id')
curl --request PATCH --data '{"draft":"false"}' -s --header "Accept: application/vnd.github.v3+json" -H "Authorization: token ${GH_TOKEN}" -L https://api.github.com/repos/yaorg/node-measured/releases/${RELEASE_ID}
================================================
FILE: tutorials/SignalFx Express Full End to End Example.md
================================================
### Using Measured to instrument OS, Process and Express Metrics.
This tutorial shows how to use the measured libraries to fully instrument OS and Node Process metrics as well as create an express middleware.
The middleware will measure request count, latency distributions (req/res time histogram) and add dimensions to make it filterable by request method, response status code, request uri path.
**NOTE:** You must add `app.use(createExpressMiddleware(...))` **before** the use of any express bodyParsers like `app.use(express.json())` because requests that are first handled by a bodyParser will not get measured.
```javascript
const os = require('os');
const signalfx = require('signalfx');
const express = require('express');
const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry } = require('measured-signalfx-reporter');
const { createProcessMetrics, createOSMetrics, createExpressMiddleware } = require('measured-node-metrics');
const libraryMetadata = require('./package'); // get metadata from package.json
const library = libraryMetadata.name;
const version = libraryMetadata.version;
// Report process and os stats 1x per minute
const PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS = 60;
// Report the request count and histogram stats every 10 seconds
const REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10;
const defaultDimensions = {
app: library,
app_version: version,
env: 'test'
};
/**
* Get your api key from a secrets provider of some kind.
*
* Good examples:
*
* S3 with KMS
* Cerberus
* AWS Secrets Manager
* Vault
* Confidant
*
* Bad examples:
*
* Checked into SCM in plaintext as a property
* Set as a plaintext environment variable
*
* @return {string} Returns the resolved Signal Fx Api Key
*/
const apiKeyResolver = () => {
// https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/
return process.env.SIGNALFX_API_KEY;
};
// Create the signal fx client
const signalFxClient = new signalfx.Ingest(apiKeyResolver(), {
userAgents: library
});
// Create the signal fx reporter with the client
const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, {
defaultDimensions: defaultDimensions,
defaultReportingIntervalInSeconds: 10,
logLevel: 'debug'
});
// Create the self reporting metrics registry with the signal fx reporter
const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' });
createOSMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS);
createProcessMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS);
const app = express();
// wire up the metrics middleware
app.use(createExpressMiddleware(metricsRegistry, REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS));
app.get('/hello', (req, res) => {
res.send('hello world');
});
app.get('/path2', (req, res) => {
res.send('path2');
});
app.listen(8080, () => log.info('Example app listening on port 8080!'));
```
================================================
FILE: tutorials/SignalFx Koa Full End to End Example.md
================================================
### Using Measured to instrument OS, Process and Koa Metrics.
This tutorial shows how to use the measured libraries to fully instrument OS and Node Process metrics as well as create a Koa middleware.
The middleware will measure request count, latency distributions (req/res time histogram) and add dimensions to make it filterable by request method, response status code, request uri path.
**NOTE:** You must add `app.use(createKoaMiddleware(...))` **before** the use of any Koa bodyParsers like `app.use(KoaBodyParser())` because requests that are first handled by a bodyParser will not get measured.
```javascript
const os = require('os');
const signalfx = require('signalfx');
const Koa = require('koa');
const KoaBodyParser = require('koa-bodyparser');
const Router = require('koa-router');
const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry } = require('measured-signalfx-reporter');
const { createProcessMetrics, createOSMetrics, createKoaMiddleware } = require('measured-node-metrics');
const libraryMetadata = require('./package'); // get metadata from package.json
const library = libraryMetadata.name;
const version = libraryMetadata.version;
// Report process and os stats 1x per minute
const PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS = 60;
// Report the request count and histogram stats every 10 seconds
const REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10;
const defaultDimensions = {
app: library,
app_version: version,
env: 'test'
};
/**
* Get your api key from a secrets provider of some kind.
*
* Good examples:
*
* S3 with KMS
* Cerberus
* AWS Secrets Manager
* Vault
* Confidant
*
* Bad examples:
*
* Checked into SCM in plaintext as a property
* Set as a plaintext environment variable
*
* @return {string} Returns the resolved Signal Fx Api Key
*/
const apiKeyResolver = () => {
// https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/
return process.env.SIGNALFX_API_KEY;
};
// Create the signal fx client
const signalFxClient = new signalfx.Ingest(apiKeyResolver(), {
userAgents: library
});
// Create the signal fx reporter with the client
const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, {
defaultDimensions: defaultDimensions,
defaultReportingIntervalInSeconds: 10,
logLevel: 'debug'
});
// Create the self reporting metrics registry with the signal fx reporter
const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' });
createOSMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS);
createProcessMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS);
const app = new Koa();
router = new Router();
router.get('/hello', (req, res) => {
res.send('hello world');
});
router.get('/path2', (req, res) => {
res.send('path2');
});
// wire up the metrics middleware
app.use(createKoaMiddleware(metricsRegistry, REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS));
app.use(KoaBodyParser());
app.use(router.routes());
app.listen(8080, () => log.info('Example app listening on port 8080!'));
```