From c95921e4e4e04de18a1309075b66f30efe6f0b27 Mon Sep 17 00:00:00 2001 From: German Quinteros <53574912+gquinteros93@users.noreply.github.com> Date: Wed, 7 Sep 2022 02:40:33 +0200 Subject: [PATCH] Support choosing aggregates and percentiles in histograms --- lib/aggregators.js | 4 ++-- lib/loggers.js | 8 ++++---- lib/metrics.js | 45 +++++++++++++++++++++++++++++-------------- test/metrics_tests.js | 26 +++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/lib/aggregators.js b/lib/aggregators.js index e735874..fca8d0b 100644 --- a/lib/aggregators.js +++ b/lib/aggregators.js @@ -14,10 +14,10 @@ Aggregator.prototype.makeBufferKey = function(key, tags) { return key + '#' + tags.concat().sort().join('.'); }; -Aggregator.prototype.addPoint = function(Type, key, value, tags, host, timestampInMillis) { +Aggregator.prototype.addPoint = function(Type, key, value, tags, host, timestampInMillis, options) { const bufferKey = this.makeBufferKey(key, tags); if (!this.buffer.hasOwnProperty(bufferKey)) { - this.buffer[bufferKey] = new Type(key, tags, host); + this.buffer[bufferKey] = new Type(key, tags, host, options); } this.buffer[bufferKey].addPoint(value, timestampInMillis); diff --git a/lib/loggers.js b/lib/loggers.js index 97a9ef9..d8060e7 100644 --- a/lib/loggers.js +++ b/lib/loggers.js @@ -64,8 +64,8 @@ function BufferedMetricsLogger(opts) { } // Prepend the global key prefix and set the default host. -BufferedMetricsLogger.prototype.addPoint = function(Type, key, value, tags, timestampInMillis) { - this.aggregator.addPoint(Type, this.prefix + key, value, tags, this.host, timestampInMillis); +BufferedMetricsLogger.prototype.addPoint = function(Type, key, value, tags, timestampInMillis, options) { + this.aggregator.addPoint(Type, this.prefix + key, value, tags, this.host, timestampInMillis, options); }; BufferedMetricsLogger.prototype.gauge = function(key, value, tags, timestampInMillis) { @@ -80,8 +80,8 @@ BufferedMetricsLogger.prototype.increment = function(key, value, tags, timestamp } }; -BufferedMetricsLogger.prototype.histogram = function(key, value, tags, timestampInMillis) { - this.addPoint(Histogram, key, value, tags, timestampInMillis); +BufferedMetricsLogger.prototype.histogram = function(key, value, tags, timestampInMillis, options = {}) { + this.addPoint(Histogram, key, value, tags, timestampInMillis, options); }; BufferedMetricsLogger.prototype.flush = function(onSuccess, onError) { diff --git a/lib/metrics.js b/lib/metrics.js index 061da74..02dbd7d 100644 --- a/lib/metrics.js +++ b/lib/metrics.js @@ -5,6 +5,9 @@ const util = require('util'); // --- Metric (base class) // +const DEFAULT_HISTOGRAM_AGGREGATES = ['max', 'min', 'sum', 'avg', 'count']; +const DEFAULT_HISTOGRAM_PERCENTILES = [0.75, 0.85, 0.95, 0.99]; + function Metric(key, tags, host) { this.key = key; this.tags = tags || []; @@ -110,14 +113,15 @@ Counter.prototype.flush = function() { // Optionally, specify a list of *tags* to associate with the metric. // -function Histogram(key, tags, host) { +function Histogram(key, tags, host, options = {}) { Metric.call(this, key, tags, host); this.min = Infinity; this.max = -Infinity; this.sum = 0; this.count = 0; this.samples = []; - this.percentiles = [0.75, 0.85, 0.95, 0.99]; + this.aggregates = options.aggregates || DEFAULT_HISTOGRAM_AGGREGATES; + this.percentiles = options.percentiles || DEFAULT_HISTOGRAM_PERCENTILES; } util.inherits(Histogram, Metric); @@ -136,26 +140,39 @@ Histogram.prototype.addPoint = function(val, timestampInMillis) { this.samples.push(val); }; -Histogram.prototype.flush = function() { - const points = [ - this.serializeMetric(this.min, 'gauge', this.key + '.min'), - this.serializeMetric(this.max, 'gauge', this.key + '.max'), - this.serializeMetric(this.sum, 'gauge', this.key + '.sum'), - this.serializeMetric(this.count, 'count', this.key + '.count'), +Histogram.prototype.flush = function () { + let points = []; + if (this.aggregates.indexOf('min') !== -1) { + points.push(this.serializeMetric(this.min, 'gauge', this.key + '.min')); + } + if (this.aggregates.indexOf('max') !== -1) { + points.push(this.serializeMetric(this.max, 'gauge', this.key + '.max')); + } + if (this.aggregates.indexOf('sum') !== -1) { + points.push(this.serializeMetric(this.sum, 'gauge', this.key + '.sum')); + } + if (this.aggregates.indexOf('count') !== -1) { + points.push(this.serializeMetric(this.count, 'count', this.key + '.count')); + } + if (this.aggregates.indexOf('avg') !== -1) { + points.push( this.serializeMetric(this.average(), 'gauge', this.key + '.avg') - ]; - + ); + } + // Careful, calling samples.sort() will sort alphabetically giving // the wrong result. We must define our own compare function. - const numericalSortAscending = function(a, b) { return a - b; }; + const numericalSortAscending = function (a, b) { + return a - b; + }; this.samples.sort(numericalSortAscending); - - const calcPercentile = function(p) { + + const calcPercentile = function (p) { const val = this.samples[Math.round(p * this.samples.length) - 1]; const suffix = '.' + Math.floor(p * 100) + 'percentile'; return this.serializeMetric(val, 'gauge', this.key + suffix); }; - + const percentiles = this.percentiles.map(calcPercentile, this); return points.concat(percentiles); }; diff --git a/test/metrics_tests.js b/test/metrics_tests.js index 69541ed..eb59911 100644 --- a/test/metrics_tests.js +++ b/test/metrics_tests.js @@ -196,4 +196,30 @@ describe('Histogram', function() { f.should.have.deep.property('[8].metric', 'hist.99percentile'); f.should.have.deep.property('[8].points[0][1]', 99); }); + + it('should use custom percentiles and aggregates', function() { + const aggregates = ['avg']; + const percentiles = [0.85]; + const h = new metrics.Histogram('hist', [], 'myhost', { aggregates, percentiles }); + h.addPoint(1); + var f = h.flush(); + + f.should.have.deep.property('[0].metric', 'hist.avg'); + f.should.have.deep.property('[0].points[0][1]', 1); + + f.should.have.deep.property('[1].metric', 'hist.85percentile'); + f.should.have.deep.property('[1].points[0][1]', 1); + + // Create 100 samples from [1..100] so we can + // verify the calculated percentiles. + for (var i = 2; i <= 100; i++) { + h.addPoint(i); + } + f = h.flush(); + + f.should.have.deep.property('[1].metric', 'hist.85percentile'); + f.should.have.deep.property('[1].points[0][1]', 85); + + }); + });