diff --git a/package.json b/package.json index 1503853e99f..f115723044f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.41.0", + "version": "5.41.1", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", diff --git a/packages/datadog-instrumentations/src/fetch.js b/packages/datadog-instrumentations/src/fetch.js index dbddfade768..8a1c855790e 100644 --- a/packages/datadog-instrumentations/src/fetch.js +++ b/packages/datadog-instrumentations/src/fetch.js @@ -17,7 +17,7 @@ if (globalThis.fetch) { const ch = tracingChannel('apm:fetch:request') const wrapFetch = createWrapFetch(globalThis.Request, ch, () => { - channel('dd-trace:instrumentation:load').publish({ name: 'fetch' }) + channel('dd-trace:instrumentation:load').publish({ name: 'global:fetch' }) }) fetch = wrapFetch(globalFetch) diff --git a/packages/datadog-plugin-fetch/test/index.spec.js b/packages/datadog-plugin-fetch/test/index.spec.js index bf18053952f..c956ebf8051 100644 --- a/packages/datadog-plugin-fetch/test/index.spec.js +++ b/packages/datadog-plugin-fetch/test/index.spec.js @@ -609,5 +609,64 @@ describe('Plugin', function () { }) }) }) + + describe('in serverless', () => { + beforeEach(() => { + process.env.DD_TRACE_EXPERIMENTAL_EXPORTER = 'agent' + process.env.AWS_LAMBDA_FUNCTION_NAME = 'test' + }) + + beforeEach(() => { + return agent.load('fetch') + .then(() => { + express = require('express') + fetch = globalThis.fetch + }) + }) + + beforeEach(() => { + delete process.env.DD_TRACE_EXPERIMENTAL_EXPORTER + delete process.env.AWS_LAMBDA_FUNCTION_NAME + }) + + withNamingSchema( + () => { + const app = express() + app.get('/user', (req, res) => { + res.status(200).send() + }) + + appListener = server(app, port => { + fetch(`http://localhost:${port}/user`) + }) + }, + rawExpectedSchema.client + ) + + it('should do automatic instrumentation', done => { + const app = express() + app.get('/user', (req, res) => { + res.status(200).send() + }) + appListener = server(app, port => { + agent + .use(traces => { + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) + expect(traces[0][0]).to.have.property('type', 'http') + expect(traces[0][0]).to.have.property('resource', 'GET') + expect(traces[0][0].meta).to.have.property('span.kind', 'client') + expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/user`) + expect(traces[0][0].meta).to.have.property('http.method', 'GET') + expect(traces[0][0].meta).to.have.property('http.status_code', '200') + expect(traces[0][0].meta).to.have.property('component', 'fetch') + expect(traces[0][0].meta).to.have.property('out.host', 'localhost') + }) + .then(done) + .catch(done) + + fetch(`http://localhost:${port}/user`) + }) + }) + }) }) }) diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index 5978a25e1ec..f97a95b75eb 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -6,6 +6,7 @@ const dgram = require('dgram') const isIP = require('net').isIP const log = require('./log') const { URL, format } = require('url') +const Histogram = require('./histogram') const MAX_BUFFER_SIZE = 1024 // limit from the agent @@ -193,6 +194,117 @@ class DogStatsDClient { } } +// TODO: Handle arrays of tags and tags translation. +class MetricsAggregationClient { + constructor (client) { + this._client = client + + this.reset() + } + + flush () { + this._captureCounters() + this._captureGauges() + this._captureHistograms() + + this._client.flush() + } + + reset () { + this._counters = {} + this._gauges = {} + this._histograms = {} + } + + distribution (name, value, tag) { + this._client.distribution(name, value, tag && [tag]) + } + + boolean (name, value, tag) { + this.gauge(name, value ? 1 : 0, tag) + } + + histogram (name, value, tag) { + this._histograms[name] = this._histograms[name] || new Map() + + if (!this._histograms[name].has(tag)) { + this._histograms[name].set(tag, new Histogram()) + } + + this._histograms[name].get(tag).record(value) + } + + count (name, count, tag, monotonic = false) { + if (typeof tag === 'boolean') { + monotonic = tag + tag = undefined + } + + const map = monotonic ? this._counters : this._gauges + + map[name] = map[name] || new Map() + + const value = map[name].get(tag) || 0 + + map[name].set(tag, value + count) + } + + gauge (name, value, tag) { + this._gauges[name] = this._gauges[name] || new Map() + this._gauges[name].set(tag, value) + } + + increment (name, count = 1, tag, monotonic) { + this.count(name, count, tag, monotonic) + } + + decrement (name, count = 1, tag) { + this.count(name, -count, tag) + } + + _captureGauges () { + Object.keys(this._gauges).forEach(name => { + this._gauges[name].forEach((value, tag) => { + this._client.gauge(name, value, tag && [tag]) + }) + }) + } + + _captureCounters () { + Object.keys(this._counters).forEach(name => { + this._counters[name].forEach((value, tag) => { + this._client.increment(name, value, tag && [tag]) + }) + }) + + this._counters = {} + } + + _captureHistograms () { + Object.keys(this._histograms).forEach(name => { + this._histograms[name].forEach((stats, tag) => { + const tags = tag && [tag] + + // Stats can contain garbage data when a value was never recorded. + if (stats.count === 0) { + stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0, reset: stats.reset } + } + + this._client.gauge(`${name}.min`, stats.min, tags) + this._client.gauge(`${name}.max`, stats.max, tags) + this._client.increment(`${name}.sum`, stats.sum, tags) + this._client.increment(`${name}.total`, stats.sum, tags) + this._client.gauge(`${name}.avg`, stats.avg, tags) + this._client.increment(`${name}.count`, stats.count, tags) + this._client.gauge(`${name}.median`, stats.median, tags) + this._client.gauge(`${name}.95percentile`, stats.p95, tags) + + stats.reset() + }) + }) + } +} + /** * This is a simplified user-facing proxy to the underlying DogStatsDClient instance * @@ -201,7 +313,7 @@ class DogStatsDClient { class CustomMetrics { constructor (config) { const clientConfig = DogStatsDClient.generateClientConfig(config) - this.dogstatsd = new DogStatsDClient(clientConfig) + this._client = new MetricsAggregationClient(new DogStatsDClient(clientConfig)) const flush = this.flush.bind(this) @@ -212,47 +324,43 @@ class CustomMetrics { } increment (stat, value = 1, tags) { - return this.dogstatsd.increment( - stat, - value, - CustomMetrics.tagTranslator(tags) - ) + for (const tag of this._normalizeTags(tags)) { + this._client.increment(stat, value, tag) + } } decrement (stat, value = 1, tags) { - return this.dogstatsd.decrement( - stat, - value, - CustomMetrics.tagTranslator(tags) - ) + for (const tag of this._normalizeTags(tags)) { + this._client.decrement(stat, value, tag) + } } gauge (stat, value, tags) { - return this.dogstatsd.gauge( - stat, - value, - CustomMetrics.tagTranslator(tags) - ) + for (const tag of this._normalizeTags(tags)) { + this._client.gauge(stat, value, tag) + } } distribution (stat, value, tags) { - return this.dogstatsd.distribution( - stat, - value, - CustomMetrics.tagTranslator(tags) - ) + for (const tag of this._normalizeTags(tags)) { + this._client.distribution(stat, value, tag) + } } histogram (stat, value, tags) { - return this.dogstatsd.histogram( - stat, - value, - CustomMetrics.tagTranslator(tags) - ) + for (const tag of this._normalizeTags(tags)) { + this._client.histogram(stat, value, tag) + } } flush () { - return this.dogstatsd.flush() + return this._client.flush() + } + + _normalizeTags (tags) { + tags = CustomMetrics.tagTranslator(tags) + + return tags.length === 0 ? [undefined] : tags } /** @@ -274,5 +382,6 @@ class CustomMetrics { module.exports = { DogStatsDClient, - CustomMetrics + CustomMetrics, + MetricsAggregationClient } diff --git a/packages/dd-trace/src/plugin_manager.js b/packages/dd-trace/src/plugin_manager.js index d6bc81e68d6..96b82a7f9de 100644 --- a/packages/dd-trace/src/plugin_manager.js +++ b/packages/dd-trace/src/plugin_manager.js @@ -103,10 +103,6 @@ module.exports = class PluginManager { this._tracerConfig = config this._tracer._nomenclature.configure(config) - if (!config._isInServerlessEnvironment?.()) { - maybeEnable(require('../../datadog-plugin-fetch/src')) - } - for (const name in pluginClasses) { this.loadPlugin(name) } diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 0104417b2fc..67c68e4595f 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -39,6 +39,7 @@ module.exports = { get express () { return require('../../../datadog-plugin-express/src') }, get fastify () { return require('../../../datadog-plugin-fastify/src') }, get 'find-my-way' () { return require('../../../datadog-plugin-find-my-way/src') }, + get 'global:fetch' () { return require('../../../datadog-plugin-fetch/src') }, get graphql () { return require('../../../datadog-plugin-graphql/src') }, get grpc () { return require('../../../datadog-plugin-grpc/src') }, get hapi () { return require('../../../datadog-plugin-hapi/src') }, diff --git a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js index 2672248df9e..4f92afea90c 100644 --- a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js @@ -4,7 +4,7 @@ const v8 = require('v8') const os = require('os') -const { DogStatsDClient } = require('../dogstatsd') +const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd') const log = require('../log') const Histogram = require('../histogram') const { performance, PerformanceObserver } = require('perf_hooks') @@ -25,9 +25,6 @@ let interval let client let time let cpuUsage -let gauges -let counters -let histograms let elu reset() @@ -49,7 +46,7 @@ const runtimeMetrics = module.exports = { nativeMetrics = null } - client = new DogStatsDClient(clientConfig) + client = new MetricsAggregationClient(new DogStatsDClient(clientConfig)) time = process.hrtime() @@ -98,50 +95,27 @@ const runtimeMetrics = module.exports = { }, boolean (name, value, tag) { - this.gauge(name, value ? 1 : 0, tag) + client && client.boolean(name, value, tag) }, histogram (name, value, tag) { - if (!client) return - - histograms[name] = histograms[name] || new Map() - - if (!histograms[name].has(tag)) { - histograms[name].set(tag, new Histogram()) - } - - histograms[name].get(tag).record(value) + client && client.histogram(name, value, tag) }, count (name, count, tag, monotonic = false) { - if (!client) return - if (typeof tag === 'boolean') { - monotonic = tag - tag = undefined - } - - const map = monotonic ? counters : gauges - - map[name] = map[name] || new Map() - - const value = map[name].get(tag) || 0 - - map[name].set(tag, value + count) + client && client.count(name, count, tag, monotonic) }, gauge (name, value, tag) { - if (!client) return - - gauges[name] = gauges[name] || new Map() - gauges[name].set(tag, value) + client && client.gauge(name, value, tag) }, increment (name, tag, monotonic) { - this.count(name, 1, tag, monotonic) + client && client.increment(name, 1, tag, monotonic) }, decrement (name, tag) { - this.count(name, -1, tag) + client && client.decrement(name, 1, tag) } } @@ -150,9 +124,6 @@ function reset () { client = null time = null cpuUsage = null - gauges = {} - counters = {} - histograms = {} nativeMetrics = null gcObserver && gcObserver.disconnect() gcObserver = null @@ -246,33 +217,6 @@ function captureGCMetrics () { gcProfiler.start() } -function captureGauges () { - Object.keys(gauges).forEach(name => { - gauges[name].forEach((value, tag) => { - client.gauge(name, value, tag && [tag]) - }) - }) -} - -function captureCounters () { - Object.keys(counters).forEach(name => { - counters[name].forEach((value, tag) => { - client.increment(name, value, tag && [tag]) - }) - }) - - counters = {} -} - -function captureHistograms () { - Object.keys(histograms).forEach(name => { - histograms[name].forEach((stats, tag) => { - histogram(name, stats, tag && [tag]) - stats.reset() - }) - }) -} - /** * Gathers and reports Event Loop Utilization (ELU) since last run * @@ -295,9 +239,6 @@ function captureCommonMetrics () { captureMemoryUsage() captureProcess() captureHeapStats() - captureGauges() - captureCounters() - captureHistograms() captureELU() captureGCMetrics() } @@ -339,21 +280,15 @@ function captureNativeMetrics () { } function histogram (name, stats, tags) { - tags = [].concat(tags) + tags = tags ? [].concat(tags) : [] - // Stats can contain garbage data when a value was never recorded. - if (stats.count === 0) { - stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0 } + if (tags.length > 0) { + for (const tag of tags) { + client.histogram(name, stats, tag) + } + } else { + client.histogram(name, stats) } - - client.gauge(`${name}.min`, stats.min, tags) - client.gauge(`${name}.max`, stats.max, tags) - client.increment(`${name}.sum`, stats.sum, tags) - client.increment(`${name}.total`, stats.sum, tags) - client.gauge(`${name}.avg`, stats.avg, tags) - client.increment(`${name}.count`, stats.count, tags) - client.gauge(`${name}.median`, stats.median, tags) - client.gauge(`${name}.95percentile`, stats.p95, tags) } function startGCObserver () { diff --git a/packages/dd-trace/test/custom-metrics.spec.js b/packages/dd-trace/test/custom-metrics.spec.js index 802fa01e3e7..7ef809b60ca 100644 --- a/packages/dd-trace/test/custom-metrics.spec.js +++ b/packages/dd-trace/test/custom-metrics.spec.js @@ -53,7 +53,7 @@ describe('Custom Metrics', () => { if (stdout) console.log(stdout) if (stderr) console.error(stderr) - expect(metricsData.split('#')[0]).to.equal('page.views.data:1|c|') + expect(metricsData.split('#')[0]).to.equal('page.views.data:1|g|') done() }) diff --git a/packages/dd-trace/test/dogstatsd.spec.js b/packages/dd-trace/test/dogstatsd.spec.js index 832c16b9b32..c34c80966d9 100644 --- a/packages/dd-trace/test/dogstatsd.spec.js +++ b/packages/dd-trace/test/dogstatsd.spec.js @@ -366,6 +366,7 @@ describe('dogstatsd', () => { it('.gauge()', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.gauge('test.avg', 10, { foo: 'bar' }) client.gauge('test.avg', 10, { foo: 'bar' }) client.flush() @@ -376,61 +377,76 @@ describe('dogstatsd', () => { it('.increment()', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.increment('test.count', 10) client.increment('test.count', 10) client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:10|c\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:20|g\n') }) it('.increment() with default', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.increment('test.count') client.increment('test.count') client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:1|c\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:2|g\n') }) it('.decrement()', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.decrement('test.count', 10) client.decrement('test.count', 10) client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-10|c\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-20|g\n') }) it('.decrement() with default', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.decrement('test.count') client.decrement('test.count') client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-1|c\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-2|g\n') }) it('.distribution()', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.distribution('test.dist', 10) client.distribution('test.dist', 10) client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.dist:10|d\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.dist:10|d\ntest.dist:10|d\n') }) it('.histogram()', () => { client = new CustomMetrics({ dogstatsd: {} }) + client.histogram('test.histogram', 10) client.histogram('test.histogram', 10) client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.histogram:10|h\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal([ + 'test.histogram.min:10|g', + 'test.histogram.max:10|g', + 'test.histogram.sum:20|c', + 'test.histogram.total:20|c', + 'test.histogram.avg:10|g', + 'test.histogram.count:2|c', + 'test.histogram.median:10.074696689511441|g', + 'test.histogram.95percentile:10.074696689511441|g' + ].join('\n') + '\n') }) it('should flush via interval', () => { diff --git a/scripts/release/helpers/terminal.js b/scripts/release/helpers/terminal.js index 9dd6317ae0f..bc2571aa53b 100644 --- a/scripts/release/helpers/terminal.js +++ b/scripts/release/helpers/terminal.js @@ -23,8 +23,8 @@ let timer let current // Output a command to the terminal and execute it. -function run (cmd, treatStderrAsFailure = true) { - capture(cmd, treatStderrAsFailure) +function run (cmd) { + capture(cmd) } // Ask a question in terminal and return the response. @@ -55,7 +55,7 @@ function checkpoint (question) { } // Run a command and capture its output to return it to the caller. -function capture (cmd, treatStderrAsFailure = true) { +function capture (cmd) { if (flags.debug) { log(`${GRAY}> ${cmd}${RESET}`) } @@ -76,25 +76,11 @@ function capture (cmd, treatStderrAsFailure = true) { if (flags.debug) { log(stdout) - if (stderr) { - if (flags['no-abort-on-error']) { - log(`${RED}${stderr}${RESET}`) - } else { - fatal( - stderr, - 'Aborting due to error! Use --no-abort-on-error to ignore and continue.' - ) - } - } - } else if (treatStderrAsFailure && stderr) { - if (flags['no-abort-on-error']) { - log(`${RED}${stderr}${RESET}`) - } else { - fatal( - stderr, - 'Aborting due to error! Use --no-abort-on-error to ignore and continue.' - ) - } + log(`${RED}${stderr}${RESET}`) + } + + if (result.status) { + throw new Error(stderr) } return stdout diff --git a/scripts/release/proposal.js b/scripts/release/proposal.js index c1ba876adb9..e213a444b67 100644 --- a/scripts/release/proposal.js +++ b/scripts/release/proposal.js @@ -82,7 +82,7 @@ try { try { // Pull latest changes in case the release was started by someone else. - run(`git remote show origin | grep v${newVersion} && git pull --ff-only`, false) + run(`git remote show origin | grep v${newVersion} && git pull --ff-only`) } catch (e) { // Either there is no remote to pull from or the local and remote branches // have diverged. In both cases we ignore the error and will just use our @@ -113,7 +113,7 @@ try { // Cherry pick all new commits to the proposal branch. try { - run(`echo "${proposalDiff}" | xargs git cherry-pick`, false) + run(`echo "${proposalDiff}" | xargs git cherry-pick`) pass() } catch (err) { @@ -147,7 +147,7 @@ try { // Create or edit the PR. This will also automatically output a link to the PR. try { - run(`gh pr create -d -B v${releaseLine}.x -t "v${newVersion} proposal" -F ${notesFile}`, false) + run(`gh pr create -d -B v${releaseLine}.x -t "v${newVersion} proposal" -F ${notesFile}`) } catch (e) { // PR already exists so update instead. // TODO: Keep existing non-release-notes PR description if there is one.