Repository: dbader/node-datadog-metrics
Pull request #70 (2022-09-15 23:04:48)

Only call `unref()` when it exists
Not all JavaScript environments have an `.unref()` function on timers. This guards the one usage of `.unref()` so as not to cause exceptions in environments that do not have it.

It solves the one issue explicitly called out in #43, but I didn’t actually do any testing with browsers, and don’t know if there might be other barriers to browser-based usage yet to be solved.


Patch:
diff --git a/index.js b/index.js
index dd575e0..a0d68a1 100644
--- a/index.js
+++ b/index.js
@@ -43,6 +43,7 @@ module.exports = {
     gauge: callOnSharedLogger.bind(undefined, 'gauge'),
     increment: callOnSharedLogger.bind(undefined, 'increment'),
     histogram: callOnSharedLogger.bind(undefined, 'histogram'),
+    distribution: callOnSharedLogger.bind(undefined, 'distribution'),
 
     BufferedMetricsLogger: loggers.BufferedMetricsLogger
 };
diff --git a/lib/loggers.js b/lib/loggers.js
index d8060e7..4f5e095 100644
--- a/lib/loggers.js
+++ b/lib/loggers.js
@@ -6,6 +6,7 @@ const DataDogReporter = require('./reporters').DataDogReporter;
 const Gauge = require('./metrics').Gauge;
 const Counter = require('./metrics').Counter;
 const Histogram = require('./metrics').Histogram;
+const Distribution = require('./metrics').Distribution;
 
 //
 // --- BufferedMetricsLogger
@@ -56,7 +57,7 @@ function BufferedMetricsLogger(opts) {
             const interval = self.flushIntervalSeconds * 1000;
             const tid = setTimeout(autoFlushCallback, interval);
             // Let the event loop exit if this is the only active timer.
-            tid.unref();
+            if (tid.unref) tid.unref();
         }
     };
 
@@ -84,6 +85,10 @@ BufferedMetricsLogger.prototype.histogram = function(key, value, tags, timestamp
     this.addPoint(Histogram, key, value, tags, timestampInMillis, options);
 };
 
+BufferedMetricsLogger.prototype.distribution = function(key, value, tags, timestampInMillis) {
+    this.addPoint(Distribution, key, value, tags, timestampInMillis);
+};
+
 BufferedMetricsLogger.prototype.flush = function(onSuccess, onError) {
     const series = this.aggregator.flush();
     if (series.length > 0) {
diff --git a/lib/metrics.js b/lib/metrics.js
index 02dbd7d..2c092ae 100644
--- a/lib/metrics.js
+++ b/lib/metrics.js
@@ -185,9 +185,61 @@ Histogram.prototype.average = function() {
     }
 };
 
+//
+// --- Distribution
+//
+
+//
+// DISTRIBUTION
+// ------------
+// Similar to a histogram, but sends every point to DataDog and does the
+// calculations server-side.
+// 
+// This is higher overhead than Counter or Histogram, but is particularly useful
+// for serverless functions or other environments where many instances of your
+// application may be running concurrently or constantly starting and stopping,
+// and it does not make sense to tag each of them separately so metrics from
+// each don't overwrite each other.
+// 
+// See more documentation of use cases and how distribution work at:
+// https://docs.datadoghq.com/metrics/types/?tab=distribution#metric-types
+//
+
+function Distribution(key, tags, host) {
+    Metric.call(this, key, tags, host);
+    this.points = [];
+}
+
+util.inherits(Distribution, Metric);
+
+Distribution.prototype.addPoint = function(val, timestampInMillis) {
+    const lastTimestamp = this.timestamp;
+    this.updateTimestamp(timestampInMillis);
+    if (lastTimestamp === this.timestamp) {
+        this.points[this.points.length - 1][1].push(val);
+    } else {
+        this.points.push([this.timestamp, [val]]);
+    }
+};
+
+Distribution.prototype.flush = function() {
+    return [this.serializeMetric(this.points, 'distribution')];
+};
+
+Distribution.prototype.serializeMetric = function(points, type, key) {
+    return {
+        metric: key || this.key,
+        points: points || this.points,
+        type: type,
+        host: this.host,
+        tags: this.tags
+    };
+};
+
 module.exports = {
     Metric: Metric,
     Gauge: Gauge,
     Counter: Counter,
-    Histogram: Histogram
+    Histogram: Histogram,
+    Distribution: Distribution
 };
diff --git a/lib/reporters.js b/lib/reporters.js
index a7ca740..7e47f70 100644
--- a/lib/reporters.js
+++ b/lib/reporters.js
@@ -57,14 +57,34 @@ DataDogReporter.prototype.report = function(series, onSuccess, onError) {
 
     if (debug.enabled) {
         // Only call stringify when debugging.
-        debug('Calling add_metrics with %s', JSON.stringify(series));
+        debug('Calling report with %s', JSON.stringify(series));
     }
 
-    callbackify(() => metricsApi.submitMetrics({
-        body: {
-            series
+    // Distributions must be submitted via a different method than other
+    // metrics, so split them up.
+    const metrics = [];
+    const distributions = [];
+    for (const metric of series) {
+        if (metric.type === 'distribution') {
+            distributions.push(metric);
+        } else {
+            metrics.push(metric);
         }
-    }))(callback);
+    }
+
+    let submissions = [];
+    if (metrics.length) {
+        submissions.push(metricsApi.submitMetrics({
+            body: { series: metrics }
+        }));
+    }
+    if (distributions.length) {
+        submissions.push(metricsApi.submitDistributionPoints({
+            body: { series: distributions }
+        }));
+    }
+    
+    callbackify(() => Promise.all(submissions))(callback);
 };
 
 module.exports = {


Test patch:
diff --git a/test/metrics_tests.js b/test/metrics_tests.js
index eb59911..8a90677 100644
--- a/test/metrics_tests.js
+++ b/test/metrics_tests.js
@@ -83,7 +83,7 @@ describe('Counter', function() {
         f[0].should.have.deep.property('points[0][1]', 1);
     });
 
-    it('should flush correctly', function() {
+    it('should flush correctly when given a timestamp', function() {
         var g = new metrics.Counter('the.key', ['mytag'], 'myhost');
         g.addPoint(1, 123000);
         var f = g.flush();
@@ -221,5 +221,68 @@ describe('Histogram', function() {
         f.should.have.deep.property('[1].points[0][1]', 85);
 
     });
+});
+
+describe('Distribution', function() {
+    it('should extend Metric', function() {
+        var g = new metrics.Distribution();
+        g.updateTimestamp.should.be.a('function');
+    });
 
+    it('should flush correctly', function() {
+        var g = new metrics.Distribution('the.key', ['mytag'], 'myhost');
+        g.addPoint(1);
+        var f = g.flush();
+        f.should.have.length(1);
+        f[0].should.have.deep.property('metric', 'the.key');
+        f[0].should.have.deep.property('tags[0]', 'mytag');
+        f[0].should.have.deep.property('host', 'myhost');
+        f[0].should.have.deep.property('type', 'distribution');
+        f[0].should.have.deep.property('points[0][0]', g.timestamp);
+        f[0].should.have.deep.property('points[0][1][0]', 1);
+        f[0].points[0][1].should.have.length(1);
+    });
+
+    it('should flush correctly when given timestamp', function() {
+        var g = new metrics.Distribution('the.key', ['mytag'], 'myhost');
+        g.addPoint(1, 123000);
+        var f = g.flush();
+        f.should.have.length(1);
+        f[0].should.have.deep.property('metric', 'the.key');
+        f[0].should.have.deep.property('tags[0]', 'mytag');
+        f[0].should.have.deep.property('host', 'myhost');
+        f[0].should.have.deep.property('type', 'distribution');
+        f[0].should.have.deep.property('points[0][0]', 123);
+        f[0].should.have.deep.property('points[0][1][0]', 1);
+        f[0].points[0][1].should.have.length(1);
+    });
+
+    it('should format multiple points from different times', function () {
+        var g = new metrics.Distribution('the.key', ['mytag'], 'myhost');
+        g.addPoint(1, 123000);
+        g.addPoint(2, 125000);
+        g.addPoint(3, 121000);
+
+        var f = g.flush();
+        f.should.have.length(1);
+        f[0].points.should.eql([
+            [123, [1]],
+            [125, [2]],
+            [121, [3]]
+        ]);
+    });
+
+    it('should format multiple points from the same time', function () {
+        var g = new metrics.Distribution('the.key', ['mytag'], 'myhost');
+        g.addPoint(1, 123000);
+        g.addPoint(2, 125000);
+        g.addPoint(3, 125000);
+
+        var f = g.flush();
+        f.should.have.length(1);
+        f[0].points.should.eql([
+            [123, [1]],
+            [125, [2, 3]]
+        ]);
+    });
 });