From 9bf7b00f19944e1a7177f3b9aa31d52947df2357 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 23 Jun 2023 13:10:27 -0700 Subject: [PATCH] Telemetry metrics for OTel (#3259) --- integration-tests/helpers.js | 50 ++ integration-tests/opentelemetry.spec.js | 56 +- integration-tests/opentelemetry/basic.js | 5 + packages/dd-trace/src/opentelemetry/span.js | 1 + packages/dd-trace/src/opentracing/span.js | 32 ++ packages/dd-trace/src/opentracing/tracer.js | 3 +- packages/dd-trace/src/plugins/tracing.js | 3 +- packages/dd-trace/src/telemetry/index.js | 3 + packages/dd-trace/src/telemetry/metrics.js | 225 ++++++++ .../dd-trace/test/opentracing/tracer.spec.js | 9 +- .../dd-trace/test/telemetry/metrics.spec.js | 531 ++++++++++++++++++ 11 files changed, 911 insertions(+), 7 deletions(-) create mode 100644 packages/dd-trace/src/telemetry/metrics.js create mode 100644 packages/dd-trace/test/telemetry/metrics.spec.js diff --git a/integration-tests/helpers.js b/integration-tests/helpers.js index 08caa4e7dae..acbb2630f26 100644 --- a/integration-tests/helpers.js +++ b/integration-tests/helpers.js @@ -27,6 +27,7 @@ class FakeAgent extends EventEmitter { async start () { const app = express() app.use(bodyParser.raw({ limit: Infinity, type: 'application/msgpack' })) + app.use(bodyParser.json({ limit: Infinity, type: 'application/json' })) app.put('/v0.4/traces', (req, res) => { if (req.body.length === 0) return res.status(200).send() res.status(200).send({ rate_by_service: { 'service:,env:': 1 } }) @@ -43,6 +44,13 @@ class FakeAgent extends EventEmitter { files: req.files }) }) + app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => { + res.status(200).send() + this.emit('telemetry', { + headers: req.headers, + payload: req.body + }) + }) return new Promise((resolve, reject) => { const timeoutObj = setTimeout(() => { @@ -103,6 +111,48 @@ class FakeAgent extends EventEmitter { return resultPromise } + + assertTelemetryReceived (fn, timeout, requestType, expectedMessageCount = 1) { + timeout = timeout || 5000 + let resultResolve + let resultReject + let msgCount = 0 + const errors = [] + + const timeoutObj = setTimeout(() => { + resultReject([...errors, new Error('timeout')]) + }, timeout) + + const resultPromise = new Promise((resolve, reject) => { + resultResolve = () => { + clearTimeout(timeoutObj) + resolve() + } + resultReject = (e) => { + clearTimeout(timeoutObj) + reject(e) + } + }) + + const messageHandler = msg => { + if (msg.payload.request_type !== requestType) return + msgCount += 1 + try { + fn(msg) + if (msgCount === expectedMessageCount) { + resultResolve() + } + } catch (e) { + errors.push(e) + } + if (msgCount === expectedMessageCount) { + this.removeListener('telemetry', messageHandler) + } + } + this.on('telemetry', messageHandler) + + return resultPromise + } } function spawnProc (filename, options = {}) { diff --git a/integration-tests/opentelemetry.spec.js b/integration-tests/opentelemetry.spec.js index 0b7d01ceba3..57750934fc1 100644 --- a/integration-tests/opentelemetry.spec.js +++ b/integration-tests/opentelemetry.spec.js @@ -6,9 +6,13 @@ const { join } = require('path') const { assert } = require('chai') const { satisfies } = require('semver') -function check (agent, proc, timeout, onMessage = () => { }) { +function check (agent, proc, timeout, onMessage = () => { }, isMetrics) { + const messageReceiver = isMetrics + ? agent.assertTelemetryReceived(onMessage, timeout, 'generate-metrics') + : agent.assertMessageReceived(onMessage, timeout) + return Promise.all([ - agent.assertMessageReceived(onMessage, timeout), + messageReceiver, new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Process timed out')) @@ -38,6 +42,11 @@ function eachEqual (spans, expected, fn) { return spans.every((span, i) => fn(span) === expected[i]) } +function nearNow (ts, now = Date.now(), range = 1000) { + const delta = Math.abs(now - ts) + return delta < range && delta >= 0 +} + describe('opentelemetry', () => { let agent let proc @@ -84,6 +93,49 @@ describe('opentelemetry', () => { }) }) + it('should capture telemetry', () => { + proc = fork(join(cwd, 'opentelemetry/basic.js'), { + cwd, + env: { + DD_TRACE_AGENT_PORT: agent.port, + DD_TRACE_OTEL_ENABLED: 1, + DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, + TIMEOUT: 1500 + } + }) + + return check(agent, proc, timeout, ({ payload }) => { + assert.strictEqual(payload.request_type, 'generate-metrics') + + const metrics = payload.payload + assert.strictEqual(metrics.namespace, 'tracers') + + const spanCreated = metrics.series.find(({ metric }) => metric === 'span_created') + const spanFinished = metrics.series.find(({ metric }) => metric === 'span_finished') + + // Validate common fields between start and finish + for (const series of [spanCreated, spanFinished]) { + assert.ok(series) + + assert.strictEqual(series.points.length, 1) + assert.strictEqual(series.points[0].length, 2) + + const [ts, value] = series.points[0] + assert.ok(nearNow(ts, Date.now() / 1e3)) + assert.strictEqual(value, 1) + + assert.strictEqual(series.type, 'count') + assert.strictEqual(series.common, true) + assert.deepStrictEqual(series.tags, [ + 'integration_name:otel', + 'otel_enabled:true', + 'lib_language:nodejs', + `version:${process.version}` + ]) + } + }, true) + }) + it('should work within existing datadog-traced http request', async () => { proc = fork(join(cwd, 'opentelemetry/server.js'), { cwd, diff --git a/integration-tests/opentelemetry/basic.js b/integration-tests/opentelemetry/basic.js index 024c4395e3d..f3397103084 100644 --- a/integration-tests/opentelemetry/basic.js +++ b/integration-tests/opentelemetry/basic.js @@ -1,5 +1,7 @@ 'use strict' +const TIMEOUT = Number(process.env.TIMEOUT || 0) + const tracer = require('dd-trace').init() const { TracerProvider } = tracer @@ -16,5 +18,8 @@ const otelTracer = ot.trace.getTracer( otelTracer.startActiveSpan('otel-sub', otelSpan => { setImmediate(() => { otelSpan.end() + + // Allow the process to be held open to gather telemetry metrics + setTimeout(() => {}, TIMEOUT) }) }) diff --git a/packages/dd-trace/src/opentelemetry/span.js b/packages/dd-trace/src/opentelemetry/span.js index ddfe391298b..f2c3e277c6a 100644 --- a/packages/dd-trace/src/opentelemetry/span.js +++ b/packages/dd-trace/src/opentelemetry/span.js @@ -38,6 +38,7 @@ class Span { context: spanContext._ddContext, startTime, hostname: _tracer._hostname, + integrationName: 'otel', tags: { 'service.name': _tracer._service } diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index 7da3b6ff089..ca77ad11232 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -11,6 +11,9 @@ const tagger = require('../tagger') const metrics = require('../metrics') const log = require('../log') const { storage } = require('../../../datadog-core') +const telemetryMetrics = require('../telemetry/metrics') + +const tracerMetrics = telemetryMetrics.manager.namespace('tracers') const { DD_TRACE_EXPERIMENTAL_STATE_TRACKING, @@ -20,6 +23,30 @@ const { const unfinishedRegistry = createRegistry('unfinished') const finishedRegistry = createRegistry('finished') +const OTEL_ENABLED = !!process.env.DD_TRACE_OTEL_ENABLED + +const integrationCounters = { + span_created: {}, + span_finished: {} +} + +function getIntegrationCounter (event, integration) { + const counters = integrationCounters[event] + + if (integration in counters) { + return counters[integration] + } + + const counter = tracerMetrics.count(event, [ + `integration_name:${integration.toLowerCase()}`, + `otel_enabled:${OTEL_ENABLED}` + ]) + + integrationCounters[event][integration] = counter + + return counter +} + class DatadogSpan { constructor (tracer, processor, prioritySampler, fields, debug) { const operationName = fields.operationName @@ -38,6 +65,9 @@ class DatadogSpan { // This name property is not updated when the span name changes. // This is necessary for span count metrics. this._name = operationName + this._integrationName = fields.integrationName || 'opentracing' + + getIntegrationCounter('span_created', this._integrationName).inc() this._spanContext = this._createContext(parent, fields) this._spanContext._name = operationName @@ -126,6 +156,8 @@ class DatadogSpan { } } + getIntegrationCounter('span_finished', this._integrationName).inc() + if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) { metrics.decrement('runtime.node.spans.unfinished') metrics.decrement('runtime.node.spans.unfinished.by.name', `span_name:${this._name}`) diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index 8ff74151145..1cb3ad959ed 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -61,7 +61,8 @@ class DatadogTracer { tags, startTime: options.startTime, hostname: this._hostname, - traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled + traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled, + integrationName: options.integrationName }, this._debug) span.addTags(this._tags) diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index c0ad2e3d0f5..30704f308da 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -91,7 +91,8 @@ class TracingPlugin extends Plugin { 'span.type': type, ...meta, ...metrics - } + }, + integrationName: type }) analyticsSampler.sample(span, this.config.measured) diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index 20705f49efc..ce54f140dec 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -6,6 +6,8 @@ const os = require('os') const dependencies = require('./dependencies') const { sendData } = require('./send-data') +const { manager: metricsManager } = require('./metrics') + const telemetryStartChannel = dc.channel('datadog:telemetry:start') const telemetryStopChannel = dc.channel('datadog:telemetry:stop') @@ -121,6 +123,7 @@ function start (aConfig, thePluginManager) { dependencies.start(config, application, host) sendData(config, application, host, 'app-started', appStarted()) interval = setInterval(() => { + metricsManager.send(config, application, host) sendData(config, application, host, 'app-heartbeat') }, heartbeatInterval) interval.unref() diff --git a/packages/dd-trace/src/telemetry/metrics.js b/packages/dd-trace/src/telemetry/metrics.js new file mode 100644 index 00000000000..9669bf9ba82 --- /dev/null +++ b/packages/dd-trace/src/telemetry/metrics.js @@ -0,0 +1,225 @@ +'use strict' + +const { version } = require('../../../../package.json') + +const { sendData } = require('./send-data') + +function getId (type, namespace, name, tags) { + return `${type}:${namespace}.${name}:${tagArray(tags).sort().join(',')}` +} + +function tagArray (tags = {}) { + if (Array.isArray(tags)) return tags + const list = [] + for (const [key, value] of Object.entries(tags)) { + list.push(`${key}:${value}`.toLowerCase()) + } + return list +} + +function now () { + return Date.now() / 1e3 +} + +function mapToJsonArray (map) { + return Array.from(map.values()).map(v => v.toJSON()) +} + +class Metric { + constructor (namespace, metric, common, tags) { + this.namespace = namespace.toString() + this.metric = common ? metric : `nodejs.${metric}` + this.tags = tagArray(tags) + if (common) { + this.tags.push('lib_language:nodejs') + this.tags.push(`version:${process.version}`) + } else { + this.tags.push(`lib_version:${version}`) + } + this.common = common + + this.points = [] + } + + toString () { + const { namespace, metric } = this + return `${namespace}.${metric}` + } + + reset () { + this.points = [] + } + + track () { + throw new Error('not implemented') + } + + toJSON () { + const { metric, points, interval, type, tags, common } = this + return { + metric, + points, + interval, + type, + tags, + common + } + } +} + +class CountMetric extends Metric { + get type () { + return 'count' + } + + inc (value) { + return this.track(value) + } + + dec (value = -1) { + return this.track(value) + } + + track (value = 1) { + if (this.points.length) { + this.points[0][1] += value + } else { + this.points.push([now(), value]) + } + } +} + +class GaugeMetric extends Metric { + get type () { + return 'gauge' + } + + mark (value) { + return this.track(value) + } + + track (value = 1) { + this.points.push([now(), value]) + } +} + +class RateMetric extends Metric { + constructor (namespace, metric, common, tags, interval) { + super(namespace, metric, common, tags) + + this.interval = interval + this.rate = 0 + } + + get type () { + return 'rate' + } + + reset () { + super.reset() + this.rate = 0 + } + + track (value = 1) { + this.rate += value + const rate = this.interval ? (this.rate / this.interval) : 0.0 + this.points = [[now(), rate]] + } +} + +const metricsTypes = { + count: CountMetric, + gauge: GaugeMetric, + rate: RateMetric +} + +class Namespace extends Map { + constructor (namespace) { + super() + this.namespace = namespace + } + + reset () { + for (const metric of this.values()) { + metric.reset() + } + } + + toString () { + return `dd.instrumentation_telemetry_data.${this.namespace}` + } + + getMetric (type, name, tags, interval) { + const metricId = getId(type, this, name, tags) + + let metric = this.get(metricId) + if (metric) return metric + + const Factory = metricsTypes[type] + if (!Factory) { + throw new Error(`Unknown metric type ${type}`) + } + + metric = new Factory(this, name, true, tags, interval) + this.set(metricId, metric) + + return metric + } + + count (name, tags) { + return this.getMetric('count', name, tags) + } + + gauge (name, tags) { + return this.getMetric('gauge', name, tags) + } + + rate (name, interval, tags) { + return this.getMetric('rate', name, tags, interval) + } + + toJSON () { + const { namespace } = this + return { + namespace, + series: mapToJsonArray(this) + } + } +} + +class NamespaceManager extends Map { + namespace (name) { + let ns = this.get(name) + if (ns) return ns + + ns = new Namespace(name) + this.set(name, ns) + return ns + } + + toJSON () { + return mapToJsonArray(this) + } + + send (config, application, host) { + for (const namespace of this.values()) { + sendData(config, application, host, 'generate-metrics', namespace.toJSON()) + + // TODO: This could also be clear() but then it'd have to rebuild all + // metric instances on every send. This may be desirable if we want tags + // with high cardinality and variability over time. + namespace.reset() + } + } +} + +const manager = new NamespaceManager() + +module.exports = { + CountMetric, + GaugeMetric, + RateMetric, + Namespace, + NamespaceManager, + manager +} diff --git a/packages/dd-trace/test/opentracing/tracer.spec.js b/packages/dd-trace/test/opentracing/tracer.spec.js index d2b96e5729d..0c3fb37fbf3 100644 --- a/packages/dd-trace/test/opentracing/tracer.spec.js +++ b/packages/dd-trace/test/opentracing/tracer.spec.js @@ -119,7 +119,8 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, - traceId128BitGenerationEnabled: undefined + traceId128BitGenerationEnabled: undefined, + integrationName: undefined }, true) expect(span.addTags).to.have.been.calledWith({ @@ -176,7 +177,8 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: os.hostname(), - traceId128BitGenerationEnabled: undefined + traceId128BitGenerationEnabled: undefined, + integrationName: undefined }) expect(testSpan).to.equal(span) @@ -246,7 +248,8 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, - traceId128BitGenerationEnabled: true + traceId128BitGenerationEnabled: true, + integrationName: undefined }) expect(testSpan).to.equal(span) diff --git a/packages/dd-trace/test/telemetry/metrics.spec.js b/packages/dd-trace/test/telemetry/metrics.spec.js new file mode 100644 index 00000000000..81c9faf7616 --- /dev/null +++ b/packages/dd-trace/test/telemetry/metrics.spec.js @@ -0,0 +1,531 @@ +'use strict' + +require('../setup/tap') + +const proxyquire = require('proxyquire') + +describe('metrics', () => { + let metrics + let sendData + let now + + beforeEach(() => { + now = Date.now() + sinon.stub(Date, 'now').returns(now) + + sendData = sinon.stub() + metrics = proxyquire('../../src/telemetry/metrics', { + './send-data': { + sendData + } + }) + }) + + afterEach(() => { + Date.now.restore() + }) + + describe('NamespaceManager', () => { + it('should export singleton manager', () => { + expect(metrics.manager).to.be.instanceOf(metrics.NamespaceManager) + }) + + it('should make namespaces', () => { + const manager = new metrics.NamespaceManager() + const ns = manager.namespace('test') + expect(ns).to.be.instanceOf(metrics.Namespace) + expect(ns.namespace).to.equal('test') + expect(ns.toString()).to.equal('dd.instrumentation_telemetry_data.test') + }) + + it('should reuse namespace instances with the same name', () => { + const manager = new metrics.NamespaceManager() + const ns = manager.namespace('test') + expect(manager.namespace('test')).to.equal(ns) + }) + + it('should convert to json', () => { + const manager = new metrics.NamespaceManager() + + const test1 = manager.namespace('test1') + const test2 = manager.namespace('test2') + + test1.count('metric1', { bar: 'baz' }).inc() + test2.count('metric2', { bux: 'bax' }).inc() + + expect(manager.toJSON()).to.deep.equal([ + { + namespace: 'test1', + series: [ + { + metric: 'metric1', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }, + { + namespace: 'test2', + series: [ + { + metric: 'metric2', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + } + ]) + }) + + it('should send data', () => { + const manager = new metrics.NamespaceManager() + + const test1 = manager.namespace('test1') + const test2 = manager.namespace('test2') + + test1.count('metric1', { bar: 'baz' }).inc() + test2.count('metric2', { bux: 'bax' }).inc() + + const config = { + hostname: 'localhost', + port: 12345, + tags: { + 'runtime-id': 'abc123' + } + } + const application = { + language_name: 'nodejs', + tracer_version: '1.2.3' + } + const host = {} + + manager.send(config, application, host) + + expect(sendData).to.have.been + .calledWith(config, application, host, 'generate-metrics', { + namespace: 'test1', + series: [ + { + metric: 'metric1', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }) + expect(sendData).to.have.been + .calledWith(config, application, host, 'generate-metrics', { + namespace: 'test2', + series: [ + { + metric: 'metric2', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }) + }) + }) + + describe('Namespace', () => { + it('should store namespace name', () => { + const ns = new metrics.Namespace('name') + expect(ns).to.have.property('namespace', 'name') + }) + + it('should convert to string', () => { + const ns = new metrics.Namespace('name') + expect(ns.toString()).to.equal('dd.instrumentation_telemetry_data.name') + }) + + it('should get count metric', () => { + const ns = new metrics.Namespace('name') + expect(ns.count('name')).to.be.instanceOf(metrics.CountMetric) + }) + + it('should get gauge metric', () => { + const ns = new metrics.Namespace('name') + expect(ns.gauge('name')).to.be.instanceOf(metrics.GaugeMetric) + }) + + it('should get rate metric', () => { + const ns = new metrics.Namespace('name') + expect(ns.rate('name')).to.be.instanceOf(metrics.RateMetric) + }) + + it('should get metric by type', () => { + const ns = new metrics.Namespace('name') + expect(ns.getMetric('count', 'name')).to.be.instanceOf(metrics.CountMetric) + expect(ns.getMetric('gauge', 'name')).to.be.instanceOf(metrics.GaugeMetric) + expect(ns.getMetric('rate', 'name')).to.be.instanceOf(metrics.RateMetric) + + expect(() => ns.getMetric('non-existent', 'name')) + .to.throw(Error, 'Unknown metric type non-existent') + }) + + it('should have unique metrics per unique tag set', () => { + const ns = new metrics.Namespace('test') + ns.count('foo', { bar: 'baz' }).inc() + ns.count('foo', { bar: 'baz' }).inc() // not unique + ns.count('foo', { bux: 'bax' }).inc() + expect(ns).to.have.lengthOf(2) + }) + + it('should reset metrics', () => { + const ns = new metrics.Namespace('test') + const metric = ns.count('foo', { bar: 'baz' }) + metric.inc() + + metric.reset = sinon.spy(metric.reset) + + expect(metric.points).to.have.lengthOf(1) + ns.reset() + expect(metric.points).to.have.lengthOf(0) + + expect(metric.reset).to.have.been.called + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('test') + ns.count('foo', { bar: 'baz' }).inc() + ns.count('foo', { bux: 'bax' }).inc() + + expect(ns.toJSON()).to.deep.equal({ + namespace: 'test', + series: [ + { + metric: 'foo', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }, + { + metric: 'foo', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }) + }) + }) + + describe('CountMetric', () => { + it('should expose input data', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name', { + foo: 'bar', + baz: 'buz' + }) + + expect(metric.type).to.equal('count') + expect(metric).to.deep.equal({ + namespace: 'dd.instrumentation_telemetry_data.tracers', + metric: 'name', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true, + points: [] + }) + }) + + it('should increment', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.track = sinon.spy(metric.track) + + metric.inc() + + expect(metric.track).to.be.called + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1] + ]) + + metric.inc() + + expect(metric.points).to.deep.equal([ + [now / 1e3, 2] + ]) + }) + + it('should decrement', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.inc() + metric.inc() + + metric.track = sinon.spy(metric.track) + + metric.dec() + + expect(metric.track).to.be.calledWith(-1) + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1] + ]) + }) + + it('should retain timestamp of first change', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.inc() + + Date.now.restore() + const newNow = Date.now() + sinon.stub(Date, 'now').returns(newNow) + + metric.inc() + + expect(metric.points).to.deep.equal([ + [now / 1e3, 2] + ]) + }) + + it('should reset state', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.inc() + metric.reset() + + expect(metric.points).to.deep.equal([]) + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name', { + foo: 'bar', + baz: 'buz' + }) + + metric.inc() + + expect(metric.toJSON()).to.deep.equal({ + metric: 'name', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }) + }) + }) + + describe('GaugeMetric', () => { + it('should expose input data', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name', { + foo: 'bar', + baz: 'buz' + }) + + expect(metric.type).to.equal('gauge') + expect(metric).to.deep.equal({ + namespace: 'dd.instrumentation_telemetry_data.tracers', + metric: 'name', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true, + points: [] + }) + }) + + it('should mark', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name') + + metric.track = sinon.spy(metric.track) + + metric.mark(1) + + expect(metric.track).to.be.called + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1] + ]) + + Date.now.restore() + const newNow = Date.now() + sinon.stub(Date, 'now').returns(newNow) + + metric.mark(2) + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1], + [newNow / 1e3, 2] + ]) + }) + + it('should reset state', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name') + + metric.mark(1) + metric.reset() + + expect(metric.points).to.deep.equal([]) + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name', { + foo: 'bar', + baz: 'buz' + }) + + metric.mark(1) + + Date.now.restore() + const newNow = Date.now() + sinon.stub(Date, 'now').returns(newNow) + + metric.mark(2) + + expect(metric.toJSON()).to.deep.equal({ + metric: 'name', + points: [ + [now / 1e3, 1], + [newNow / 1e3, 2] + ], + interval: undefined, + type: 'gauge', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }) + }) + }) + + describe('RateMetric', () => { + it('should expose input data', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000, { + foo: 'bar', + baz: 'buz' + }) + + expect(metric.type).to.equal('rate') + expect(metric).to.deep.equal({ + namespace: 'dd.instrumentation_telemetry_data.tracers', + metric: 'name', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true, + points: [], + interval: 1000, + rate: 0 + }) + }) + + it('should track', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000) + + metric.track(100) + + expect(metric.points).to.deep.equal([ + [now / 1e3, 0.1] + ]) + }) + + it('should reset state', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000) + + metric.track(1) + metric.reset() + + expect(metric.points).to.deep.equal([]) + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000, { + foo: 'bar', + baz: 'buz' + }) + + metric.track(123) + + expect(metric.toJSON()).to.deep.equal({ + metric: 'name', + points: [ + [now / 1e3, 0.123] + ], + interval: 1000, + type: 'rate', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }) + }) + }) +})