Repository: dbader/node-datadog-metrics (js)

Issue:
Add median for histogram metrics
When using DogStatsD, the histogram type calculates a `<metric_name>.median` metric that this package does not. It would be great to add that here.

(Context: I’m experimenting with transitioning an app from DogStatsD to this for an environment where the other system/host metrics DogStatsD collects aren’t needed, so we can reduce configuration overhead and cost.)

I’d be happy to submit a PR to add this, if helpful.

Pull request description:
Add median aggregation for histograms
This adds a `<metric>.median` output for histograms, to go along with the existing `<metric>.max`, `<metric>.avg`, etc. The matches the DataDog StatsD agent, which outputs a similar median metric when send a histogram.

Fixes #68.

Patch:
diff --git a/README.md b/README.md
index 9e28caf..db7b4cf 100644
--- a/README.md
+++ b/README.md
@@ -178,11 +178,11 @@ metrics.increment('test.awesomeness_factor', 10);
 
 ### Histograms
 
-`metrics.histogram(key, value[, tags[, timestamp]])`
+`metrics.histogram(key, value[, tags[, timestamp[, options]]])`
 
 Sample a histogram value. Histograms will produce metrics that
 describe the distribution of the recorded values, namely the minimum,
-maximum, average, count and the 75th, 85th, 95th and 99th percentiles.
+maximum, average, median, count and the 75th, 85th, 95th and 99th percentiles.
 Optionally, specify a list of *tags* to associate with the metric.
 The optional timestamp is in milliseconds since 1 Jan 1970 00:00:00 UTC,
 e.g. from `Date.now()`.
@@ -193,6 +193,19 @@ Example:
 metrics.histogram('test.service_time', 0.248);
 ```
 
+You can also specify an options object to adjust which aggregations and
+percentiles should be calculated. For example, to only calculate an average,
+count, and 99th percentile:
+
+```js
+metrics.histogram('test.service_time', 0.248, ['tag:value'], Date.now(), {
+    // Aggregates can include 'max', 'min', 'sum', 'avg', 'median', or 'count'.
+    aggregates: ['avg', 'count'],
+    // Percentiles can include any decimal between 0 and 1.
+    percentiles: [0.99]
+});
+```
+
 ### Distributions
 
 `metrics.distribution(key, value[, tags[, timestamp]])`
diff --git a/lib/metrics.js b/lib/metrics.js
index 2c092ae..14954a8 100644
--- a/lib/metrics.js
+++ b/lib/metrics.js
@@ -5,7 +5,7 @@ const util = require('util');
 // --- Metric (base class)
 //
 
-const DEFAULT_HISTOGRAM_AGGREGATES = ['max', 'min', 'sum', 'avg', 'count'];
+const DEFAULT_HISTOGRAM_AGGREGATES = ['max', 'min', 'sum', 'avg', 'count', 'median'];
 const DEFAULT_HISTOGRAM_PERCENTILES = [0.75, 0.85, 0.95, 0.99];
 
 function Metric(key, tags, host) {
@@ -166,6 +166,12 @@ Histogram.prototype.flush = function () {
       return a - b;
     };
     this.samples.sort(numericalSortAscending);
+
+    if (this.aggregates.includes('median')) {
+        points.push(
+            this.serializeMetric(this.median(this.samples), 'gauge', this.key + '.median')
+        );
+    }
   
     const calcPercentile = function (p) {
         const val = this.samples[Math.round(p * this.samples.length) - 1];
@@ -185,6 +191,16 @@ Histogram.prototype.average = function() {
     }
 };
 
+Histogram.prototype.median = function(sortedSamples) {
+    if (this.count === 0) {
+        return 0;
+    } else if (this.count % 2 === 1) {
+        return sortedSamples[(this.count - 1) / 2];
+    } else {
+        return (sortedSamples[this.count / 2 - 1] + sortedSamples[this.count / 2]) / 2;
+    }
+};
+
 //
 // --- Distribution
 //


Test patch:
diff --git a/test/metrics_tests.js b/test/metrics_tests.js
index 8a90677..7491908 100644
--- a/test/metrics_tests.js
+++ b/test/metrics_tests.js
@@ -166,19 +166,41 @@ describe('Histogram', function() {
         f.should.have.deep.property('[4].points[0][1]', 2.5);
     });
 
+    it('should report the median', function() {
+        var h = new metrics.Histogram('hist');
+        var f = h.flush();
+
+        f.should.have.deep.property('[5].metric', 'hist.median');
+        f.should.have.deep.property('[5].points[0][1]', 0);
+
+        h.addPoint(2);
+        h.addPoint(3);
+        h.addPoint(10);
+
+        f = h.flush();
+        f.should.have.deep.property('[5].metric', 'hist.median');
+        f.should.have.deep.property('[5].points[0][1]', 3);
+
+        h.addPoint(4);
+
+        f = h.flush();
+        f.should.have.deep.property('[5].metric', 'hist.median');
+        f.should.have.deep.property('[5].points[0][1]', 3.5);
+    });
+
     it('should report the correct percentiles', function() {
         var h = new metrics.Histogram('hist');
         h.addPoint(1);
         var f = h.flush();
 
-        f.should.have.deep.property('[5].metric', 'hist.75percentile');
-        f.should.have.deep.property('[5].points[0][1]', 1);
-        f.should.have.deep.property('[6].metric', 'hist.85percentile');
+        f.should.have.deep.property('[6].metric', 'hist.75percentile');
         f.should.have.deep.property('[6].points[0][1]', 1);
-        f.should.have.deep.property('[7].metric', 'hist.95percentile');
+        f.should.have.deep.property('[7].metric', 'hist.85percentile');
         f.should.have.deep.property('[7].points[0][1]', 1);
-        f.should.have.deep.property('[8].metric', 'hist.99percentile');
+        f.should.have.deep.property('[8].metric', 'hist.95percentile');
         f.should.have.deep.property('[8].points[0][1]', 1);
+        f.should.have.deep.property('[9].metric', 'hist.99percentile');
+        f.should.have.deep.property('[9].points[0][1]', 1);
 
         // Create 100 samples from [1..100] so we can
         // verify the calculated percentiles.
@@ -187,14 +209,14 @@ describe('Histogram', function() {
         }
         f = h.flush();
 
-        f.should.have.deep.property('[5].metric', 'hist.75percentile');
-        f.should.have.deep.property('[5].points[0][1]', 75);
-        f.should.have.deep.property('[6].metric', 'hist.85percentile');
-        f.should.have.deep.property('[6].points[0][1]', 85);
-        f.should.have.deep.property('[7].metric', 'hist.95percentile');
-        f.should.have.deep.property('[7].points[0][1]', 95);
-        f.should.have.deep.property('[8].metric', 'hist.99percentile');
-        f.should.have.deep.property('[8].points[0][1]', 99);
+        f.should.have.deep.property('[6].metric', 'hist.75percentile');
+        f.should.have.deep.property('[6].points[0][1]', 75);
+        f.should.have.deep.property('[7].metric', 'hist.85percentile');
+        f.should.have.deep.property('[7].points[0][1]', 85);
+        f.should.have.deep.property('[8].metric', 'hist.95percentile');
+        f.should.have.deep.property('[8].points[0][1]', 95);
+        f.should.have.deep.property('[9].metric', 'hist.99percentile');
+        f.should.have.deep.property('[9].points[0][1]', 99);
     });
 
     it('should use custom percentiles and aggregates', function() {
diff --git a/test/module_tests.js b/test/module_tests.js
index f3b00fe..24cecab 100644
--- a/test/module_tests.js
+++ b/test/module_tests.js
@@ -30,7 +30,7 @@ describe('datadog-metrics', function() {
             flushIntervalSeconds: 0,
             reporter: {
                 report: function(series, onSuccess, onError) {
-                    series.should.have.length(11); // 3 + 8 for the histogram.
+                    series.should.have.length(12); // 3 + 9 for the histogram.
                     series[0].should.have.deep.property('points[0][1]', 23);
                     series[0].should.have.deep.property('metric', 'test.gauge');
                     series[0].tags.should.have.length(0);