dd-trace 5.63.3 → 5.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -712,6 +712,16 @@ declare namespace tracer {
712
712
  * @default true
713
713
  */
714
714
  enabled?: boolean,
715
+
716
+ /** Whether to enable endpoint collection for API Security.
717
+ * @default true
718
+ */
719
+ endpointCollectionEnabled?: boolean,
720
+
721
+ /** Maximum number of endpoints that can be serialized per message.
722
+ * @default 300
723
+ */
724
+ endpointCollectionMessageLimit?: number,
715
725
  },
716
726
  /**
717
727
  * Configuration for RASP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.63.3",
3
+ "version": "5.65.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -98,7 +98,18 @@ function wrapWithTracer (fn) {
98
98
  return function () {
99
99
  const options = arguments[0]
100
100
 
101
- options.experimental_telemetry ??= { isEnabled: true, tracer: noopTracer }
101
+ const experimentalTelemetry = options.experimental_telemetry
102
+ if (experimentalTelemetry?.isEnabled === false) {
103
+ return fn.apply(this, arguments)
104
+ }
105
+
106
+ if (experimentalTelemetry == null) {
107
+ options.experimental_telemetry = { isEnabled: true, tracer: noopTracer }
108
+ } else {
109
+ experimentalTelemetry.isEnabled = true
110
+ experimentalTelemetry.tracer ??= noopTracer
111
+ }
112
+
102
113
  wrapTracer(options.experimental_telemetry.tracer)
103
114
 
104
115
  return fn.apply(this, arguments)
@@ -148,8 +148,8 @@ function getPlaywrightConfig (playwrightRunner) {
148
148
  }
149
149
  }
150
150
 
151
- function getRootDir (playwrightRunner) {
152
- const config = getPlaywrightConfig(playwrightRunner)
151
+ function getRootDir (playwrightRunner, configArg) {
152
+ const config = configArg?.config || getPlaywrightConfig(playwrightRunner)
153
153
  if (config.rootDir) {
154
154
  return config.rootDir
155
155
  }
@@ -162,8 +162,8 @@ function getRootDir (playwrightRunner) {
162
162
  return process.cwd()
163
163
  }
164
164
 
165
- function getProjectsFromRunner (runner) {
166
- const config = getPlaywrightConfig(runner)
165
+ function getProjectsFromRunner (runner, configArg) {
166
+ const config = configArg?.projects ? configArg : getPlaywrightConfig(runner)
167
167
  return config.projects?.map((project) => {
168
168
  if (project.project) {
169
169
  return project.project
@@ -509,11 +509,12 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
509
509
  return dispatcherExport
510
510
  }
511
511
 
512
- function runnerHook (runnerExport, playwrightVersion) {
513
- shimmer.wrap(runnerExport.Runner.prototype, 'runAllTests', runAllTests => async function () {
512
+ function runAllTestsWrapper (runAllTests, playwrightVersion) {
513
+ // Config parameter is only available from >=1.55.0
514
+ return async function (config) {
514
515
  let onDone
515
516
 
516
- rootDir = getRootDir(this)
517
+ rootDir = getRootDir(this, config)
517
518
 
518
519
  const processArgv = process.argv.slice(2).join(' ')
519
520
  const command = `playwright ${processArgv}`
@@ -586,7 +587,7 @@ function runnerHook (runnerExport, playwrightVersion) {
586
587
  }
587
588
  }
588
589
 
589
- const projects = getProjectsFromRunner(this)
590
+ const projects = getProjectsFromRunner(this, config)
590
591
 
591
592
  const shouldSetRetries = isFlakyTestRetriesEnabled &&
592
593
  flakyTestRetriesCount > 0 &&
@@ -651,6 +652,23 @@ function runnerHook (runnerExport, playwrightVersion) {
651
652
  // TODO: we can trick playwright into thinking the session passed by returning
652
653
  // 'passed' here. We might be able to use this for both EFD and Test Management tests.
653
654
  return runAllTestsReturn
655
+ }
656
+ }
657
+
658
+ function runnerHook (runnerExport, playwrightVersion) {
659
+ shimmer.wrap(
660
+ runnerExport.Runner.prototype,
661
+ 'runAllTests',
662
+ runAllTests => runAllTestsWrapper(runAllTests, playwrightVersion)
663
+ )
664
+ }
665
+
666
+ function runnerHookNew (runnerExport, playwrightVersion) {
667
+ runnerExport = shimmer.wrap(runnerExport, 'runAllTestsWithConfig', function (originalGetter) {
668
+ const originalFunction = originalGetter.call(this)
669
+ return function () {
670
+ return runAllTestsWrapper(originalFunction, playwrightVersion)
671
+ }
654
672
  })
655
673
 
656
674
  return runnerExport
@@ -694,6 +712,12 @@ addHook({
694
712
  versions: ['>=1.38.0']
695
713
  }, runnerHook)
696
714
 
715
+ addHook({
716
+ name: 'playwright',
717
+ file: 'lib/runner/testRunner.js',
718
+ versions: ['>=1.55.0']
719
+ }, runnerHookNew)
720
+
697
721
  addHook({
698
722
  name: 'playwright',
699
723
  file: 'lib/runner/dispatcher.js',
@@ -76,10 +76,11 @@ function buildMetadata () {
76
76
  }
77
77
 
78
78
  function getAzureAppMetadata () {
79
- // DD_AZURE_APP_SERVICES is an environment variable introduced by the .NET APM team and is set automatically for
80
- // anyone using the Datadog APM Extensions (.NET, Java, or Node) for Windows Azure App Services
81
- // See: https://github.com/DataDog/datadog-aas-extension/blob/01f94b5c28b7fa7a9ab264ca28bd4e03be603900/node/src/applicationHost.xdt#L20-L21
82
- if (getEnvironmentVariable('DD_AZURE_APP_SERVICES') !== undefined) {
79
+ // WEBSITE_SITE_NAME is the unique name of the website instance within Azure App Services. Its
80
+ // presence is used to determine if we are running in Azure App Service
81
+ // See equivalent in dd-trace-dotnet:
82
+ // https://github.com/DataDog/dd-trace-dotnet/blob/37030168b2996e549ba23231ae732874b53a37e6/tracer/src/Datadog.Trace/Util/EnvironmentHelpers.cs#L99-L155
83
+ if (getEnvironmentVariable('WEBSITE_SITE_NAME') !== undefined) {
83
84
  return buildMetadata()
84
85
  }
85
86
  }
@@ -323,12 +323,6 @@ class Config {
323
323
  }
324
324
  }
325
325
 
326
- if (typeof options.runtimeMetrics?.gc === 'boolean') {
327
- options.runtimeMetrics.gc = {
328
- enabled: options.runtimeMetrics.gc
329
- }
330
- }
331
-
332
326
  const DD_INSTRUMENTATION_INSTALL_ID = coalesce(
333
327
  getEnvironmentVariable('DD_INSTRUMENTATION_INSTALL_ID'),
334
328
  null
@@ -493,6 +487,8 @@ class Config {
493
487
  defaults.apmTracingEnabled = true
494
488
  defaults['appsec.apiSecurity.enabled'] = true
495
489
  defaults['appsec.apiSecurity.sampleDelay'] = 30
490
+ defaults['appsec.apiSecurity.endpointCollectionEnabled'] = true
491
+ defaults['appsec.apiSecurity.endpointCollectionMessageLimit'] = 300
496
492
  defaults['appsec.blockedTemplateGraphql'] = undefined
497
493
  defaults['appsec.blockedTemplateHtml'] = undefined
498
494
  defaults['appsec.blockedTemplateJson'] = undefined
@@ -690,6 +686,8 @@ class Config {
690
686
  DD_AGENT_HOST,
691
687
  DD_API_SECURITY_ENABLED,
692
688
  DD_API_SECURITY_SAMPLE_DELAY,
689
+ DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED,
690
+ DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT,
693
691
  DD_APM_TRACING_ENABLED,
694
692
  DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE,
695
693
  DD_APPSEC_COLLECT_ALL_HEADERS,
@@ -846,6 +844,10 @@ class Config {
846
844
  ))
847
845
  this._setBoolean(env, 'appsec.apiSecurity.enabled', DD_API_SECURITY_ENABLED && isTrue(DD_API_SECURITY_ENABLED))
848
846
  env['appsec.apiSecurity.sampleDelay'] = maybeFloat(DD_API_SECURITY_SAMPLE_DELAY)
847
+ this._setBoolean(env, 'appsec.apiSecurity.endpointCollectionEnabled',
848
+ DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED)
849
+ env['appsec.apiSecurity.endpointCollectionMessageLimit'] =
850
+ maybeInt(DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT)
849
851
  env['appsec.blockedTemplateGraphql'] = maybeFile(DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON)
850
852
  env['appsec.blockedTemplateHtml'] = maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML)
851
853
  this._envUnprocessed['appsec.blockedTemplateHtml'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML
@@ -1073,6 +1075,10 @@ class Config {
1073
1075
  options.experimental?.appsec?.standalone && !options.experimental.appsec.standalone.enabled
1074
1076
  ))
1075
1077
  this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec?.apiSecurity?.enabled)
1078
+ this._setBoolean(opts, 'appsec.apiSecurity.endpointCollectionEnabled',
1079
+ options.appsec?.apiSecurity?.endpointCollectionEnabled)
1080
+ opts['appsec.apiSecurity.endpointCollectionMessageLimit'] =
1081
+ maybeInt(options.appsec?.apiSecurity?.endpointCollectionMessageLimit)
1076
1082
  opts['appsec.blockedTemplateGraphql'] = maybeFile(options.appsec?.blockedTemplateGraphql)
1077
1083
  opts['appsec.blockedTemplateHtml'] = maybeFile(options.appsec?.blockedTemplateHtml)
1078
1084
  this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec?.blockedTemplateHtml
@@ -1190,7 +1196,7 @@ class Config {
1190
1196
  this._setBoolean(opts, 'reportHostname', options.reportHostname)
1191
1197
  this._setBoolean(opts, 'runtimeMetrics.enabled', options.runtimeMetrics?.enabled)
1192
1198
  this._setBoolean(opts, 'runtimeMetrics.eventLoop', options.runtimeMetrics?.eventLoop)
1193
- this._setBoolean(opts, 'runtimeMetrics.gc', options.runtimeMetrics?.gc?.enabled)
1199
+ this._setBoolean(opts, 'runtimeMetrics.gc', options.runtimeMetrics?.gc)
1194
1200
  this._setBoolean(opts, 'runtimeMetricsRuntimeId', options.runtimeMetricsRuntimeId)
1195
1201
  this._setArray(opts, 'sampler.spanSamplingRules', reformatSpanSamplingRules(options.spanSamplingRules))
1196
1202
  this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate))
@@ -17,7 +17,7 @@ const CONTEXT_PROPAGATION_KEY_BASE64 = 'dd-pathway-ctx-base64'
17
17
  const logKeys = [CONTEXT_PROPAGATION_KEY, CONTEXT_PROPAGATION_KEY_BASE64]
18
18
 
19
19
  function shaHash (checkpointString) {
20
- const hash = crypto.createHash('md5').update(checkpointString).digest('hex').slice(0, 16)
20
+ const hash = crypto.createHash('sha256').update(checkpointString).digest('hex').slice(0, 16)
21
21
  return Buffer.from(hash, 'hex')
22
22
  }
23
23
 
@@ -156,23 +156,19 @@ class DogStatsDClient {
156
156
  return socket
157
157
  }
158
158
 
159
- static generateClientConfig (config = {}) {
159
+ static generateClientConfig (config) {
160
160
  const tags = []
161
161
 
162
162
  if (config.tags) {
163
- Object.keys(config.tags)
164
- .filter(key => typeof config.tags[key] === 'string')
165
- .filter(key => {
166
- // Skip runtime-id unless enabled as cardinality may be too high
167
- if (key !== 'runtime-id') return true
168
- return config.runtimeMetricsRuntimeId
169
- })
170
- .forEach(key => {
163
+ for (const [key, value] of Object.entries(config.tags)) {
164
+ // Skip runtime-id unless enabled as cardinality may be too high
165
+ if (typeof value === 'string' && (key !== 'runtime-id' || config.runtimeMetricsRuntimeId)) {
171
166
  // https://docs.datadoghq.com/tagging/#defining-tags
172
- const value = config.tags[key].replaceAll(/[^a-z0-9_:./-]/ig, '_')
167
+ const valueStripped = value.replaceAll(/[^a-z0-9_:./-]/ig, '_')
173
168
 
174
- tags.push(`${key}:${value}`)
175
- })
169
+ tags.push(`${key}:${valueStripped}`)
170
+ }
171
+ }
176
172
  }
177
173
 
178
174
  const clientConfig = {
@@ -216,7 +212,7 @@ class MetricsAggregationClient {
216
212
  this._histograms = new Map()
217
213
  }
218
214
 
219
- // TODO: Aggerate with a histogram and send the buckets to the client.
215
+ // TODO: Aggregate with a histogram and send the buckets to the client.
220
216
  distribution (name, value, tags) {
221
217
  this._client.distribution(name, value, tags)
222
218
  }
@@ -352,9 +348,10 @@ class MetricsAggregationClient {
352
348
  * @implements {DogStatsD}
353
349
  */
354
350
  class CustomMetrics {
351
+ #client
355
352
  constructor (config) {
356
353
  const clientConfig = DogStatsDClient.generateClientConfig(config)
357
- this._client = new MetricsAggregationClient(new DogStatsDClient(clientConfig))
354
+ this.#client = new MetricsAggregationClient(new DogStatsDClient(clientConfig))
358
355
 
359
356
  const flush = this.flush.bind(this)
360
357
 
@@ -365,27 +362,27 @@ class CustomMetrics {
365
362
  }
366
363
 
367
364
  increment (stat, value = 1, tags) {
368
- this._client.increment(stat, value, CustomMetrics.tagTranslator(tags))
365
+ this.#client.increment(stat, value, CustomMetrics.tagTranslator(tags))
369
366
  }
370
367
 
371
368
  decrement (stat, value = 1, tags) {
372
- this._client.decrement(stat, value, CustomMetrics.tagTranslator(tags))
369
+ this.#client.decrement(stat, value, CustomMetrics.tagTranslator(tags))
373
370
  }
374
371
 
375
372
  gauge (stat, value, tags) {
376
- this._client.gauge(stat, value, CustomMetrics.tagTranslator(tags))
373
+ this.#client.gauge(stat, value, CustomMetrics.tagTranslator(tags))
377
374
  }
378
375
 
379
376
  distribution (stat, value, tags) {
380
- this._client.distribution(stat, value, CustomMetrics.tagTranslator(tags))
377
+ this.#client.distribution(stat, value, CustomMetrics.tagTranslator(tags))
381
378
  }
382
379
 
383
380
  histogram (stat, value, tags) {
384
- this._client.histogram(stat, value, CustomMetrics.tagTranslator(tags))
381
+ this.#client.histogram(stat, value, CustomMetrics.tagTranslator(tags))
385
382
  }
386
383
 
387
384
  flush () {
388
- return this._client.flush()
385
+ return this.#client.flush()
389
386
  }
390
387
 
391
388
  /**
@@ -86,7 +86,7 @@ class DatadogTracer {
86
86
  }
87
87
 
88
88
  try {
89
- if (format !== 'text_map_dsm') {
89
+ if (format !== 'text_map_dsm' && format !== formats.LOG) {
90
90
  this._prioritySampler.sample(context)
91
91
  }
92
92
  this._propagators[format].inject(context, carrier)
@@ -4,72 +4,90 @@
4
4
 
5
5
  const v8 = require('v8')
6
6
  const os = require('os')
7
+ const process = require('process')
7
8
  const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd')
8
9
  const log = require('../log')
9
- const { performance, PerformanceObserver } = require('perf_hooks')
10
+ const { performance, PerformanceObserver, monitorEventLoopDelay } = require('perf_hooks')
10
11
  const { getEnvironmentVariable } = require('../config-helper')
11
12
 
12
- const { NODE_MAJOR, NODE_MINOR } = require('../../../../version')
13
+ const { NODE_MAJOR } = require('../../../../version')
14
+ // TODO: This environment variable may not be changed, since the agent expects a flush every ten seconds.
15
+ // It is only a variable for testing. Think about alternatives.
13
16
  const DD_RUNTIME_METRICS_FLUSH_INTERVAL = getEnvironmentVariable('DD_RUNTIME_METRICS_FLUSH_INTERVAL') ?? '10000'
14
17
  const INTERVAL = Number.parseInt(DD_RUNTIME_METRICS_FLUSH_INTERVAL, 10)
15
18
 
16
- // Node >=16 has PerformanceObserver with `gc` type, but <16.7 had a critical bug.
17
- // See: https://github.com/nodejs/node/issues/39548
18
- const hasGCObserver = NODE_MAJOR >= 18 || (NODE_MAJOR === 16 && NODE_MINOR >= 7)
19
+ const eventLoopDelayResolution = 4
19
20
 
20
21
  let nativeMetrics = null
21
22
  let gcObserver = null
22
-
23
- let interval
24
- let client
25
- let time
26
- let cpuUsage
27
- let elu
28
-
29
- reset()
30
-
31
- const runtimeMetrics = module.exports = {
23
+ let interval = null
24
+ let client = null
25
+ let lastTime = 0n
26
+ let lastCpuUsage = null
27
+ let eventLoopDelayObserver = null
28
+
29
+ // !!!!!!!!!!!
30
+ // IMPORTANT
31
+ // !!!!!!!!!!!
32
+ //
33
+ // ALL metrics that relate to time are handled in nanoseconds in the backend.
34
+ // https://github.com/DataDog/dogweb/blob/prod/integration/node/node_metadata.csv
35
+
36
+ module.exports = {
32
37
  start (config) {
38
+ this.stop()
33
39
  const clientConfig = DogStatsDClient.generateClientConfig(config)
34
- const watchers = []
35
40
 
36
- if (config.runtimeMetrics.gc !== false) {
37
- if (hasGCObserver) {
38
- startGCObserver()
39
- } else {
40
- watchers.push('gc')
41
- }
42
- }
41
+ const trackEventLoop = config.runtimeMetrics.eventLoop !== false
42
+ const trackGc = config.runtimeMetrics.gc !== false
43
43
 
44
- if (config.runtimeMetrics.eventLoop !== false) {
45
- watchers.push('loop')
44
+ if (trackGc) {
45
+ startGCObserver()
46
46
  }
47
47
 
48
+ // Using no-gc prevents the native gc metrics from being tracked. Not
49
+ // passing any options means all metrics are tracked.
50
+ // TODO: This is a workaround. We should find a better solution.
51
+ const watchers = trackEventLoop ? ['loop'] : ['no-gc']
52
+
48
53
  try {
49
54
  nativeMetrics = require('@datadog/native-metrics')
50
55
  nativeMetrics.start(...watchers)
51
- } catch (e) {
52
- log.error('Error starting native metrics', e)
56
+ } catch (error) {
57
+ log.error('Error starting native metrics', error)
53
58
  nativeMetrics = null
54
59
  }
55
60
 
56
61
  client = new MetricsAggregationClient(new DogStatsDClient(clientConfig))
57
62
 
58
- time = process.hrtime()
63
+ lastTime = performance.now()
59
64
 
60
65
  if (nativeMetrics) {
61
66
  interval = setInterval(() => {
62
- captureCommonMetrics()
63
- captureNativeMetrics()
67
+ captureNativeMetrics(trackEventLoop, trackGc)
68
+ captureCommonMetrics(trackEventLoop)
64
69
  client.flush()
65
70
  }, INTERVAL)
66
71
  } else {
67
- cpuUsage = process.cpuUsage()
72
+ lastCpuUsage = process.cpuUsage()
73
+
74
+ if (trackEventLoop) {
75
+ eventLoopDelayObserver = monitorEventLoopDelay({ resolution: eventLoopDelayResolution })
76
+ eventLoopDelayObserver.enable()
77
+ }
68
78
 
69
79
  interval = setInterval(() => {
70
- captureCommonMetrics()
71
80
  captureCpuUsage()
81
+ captureCommonMetrics(trackEventLoop)
72
82
  captureHeapSpace()
83
+ if (trackEventLoop) {
84
+ // Experimental: The Node.js implementation deviates from the native metrics.
85
+ // We normalize the metrics to the same format but the Node.js values
86
+ // are that way lower than they should be, while they are still nearer
87
+ // to the native ones that way.
88
+ // We use these only as fallback values.
89
+ captureEventLoopDelay()
90
+ }
73
91
  client.flush()
74
92
  }, INTERVAL)
75
93
  }
@@ -78,12 +96,21 @@ const runtimeMetrics = module.exports = {
78
96
  },
79
97
 
80
98
  stop () {
81
- if (nativeMetrics) {
82
- nativeMetrics.stop()
83
- }
99
+ nativeMetrics?.stop()
100
+ nativeMetrics = null
84
101
 
85
102
  clearInterval(interval)
86
- reset()
103
+ interval = null
104
+
105
+ client = null
106
+ lastTime = 0n
107
+ lastCpuUsage = null
108
+
109
+ gcObserver?.disconnect()
110
+ gcObserver = null
111
+
112
+ eventLoopDelayObserver?.disable()
113
+ eventLoopDelayObserver = null
87
114
  },
88
115
 
89
116
  track (span) {
@@ -99,19 +126,19 @@ const runtimeMetrics = module.exports = {
99
126
  },
100
127
 
101
128
  boolean (name, value, tag) {
102
- client && client.boolean(name, value, tag)
129
+ client?.boolean(name, value, tag)
103
130
  },
104
131
 
105
132
  histogram (name, value, tag) {
106
- client && client.histogram(name, value, tag)
133
+ client?.histogram(name, value, tag)
107
134
  },
108
135
 
109
136
  count (name, count, tag, monotonic = false) {
110
- client && client.count(name, count, tag, monotonic)
137
+ client?.count(name, count, tag, monotonic)
111
138
  },
112
139
 
113
140
  gauge (name, value, tag) {
114
- client && client.gauge(name, value, tag)
141
+ client?.gauge(name, value, tag)
115
142
  },
116
143
 
117
144
  increment (name, tag, monotonic) {
@@ -123,30 +150,20 @@ const runtimeMetrics = module.exports = {
123
150
  }
124
151
  }
125
152
 
126
- function reset () {
127
- interval = null
128
- client = null
129
- time = null
130
- cpuUsage = null
131
- nativeMetrics = null
132
- gcObserver && gcObserver.disconnect()
133
- gcObserver = null
134
- }
135
-
136
153
  function captureCpuUsage () {
137
- if (!process.cpuUsage) return
138
-
139
- const elapsedTime = process.hrtime(time)
140
- const elapsedUsage = process.cpuUsage(cpuUsage)
141
-
142
- time = process.hrtime()
143
- cpuUsage = process.cpuUsage()
144
-
145
- const elapsedMs = elapsedTime[0] * 1000 + elapsedTime[1] / 1_000_000
146
- const userPercent = 100 * elapsedUsage.user / 1000 / elapsedMs
147
- const systemPercent = 100 * elapsedUsage.system / 1000 / elapsedMs
154
+ const currentCpuUsage = process.cpuUsage()
155
+ const elapsedUsageUser = currentCpuUsage.user - lastCpuUsage.user
156
+ const elapsedUsageSystem = currentCpuUsage.system - lastCpuUsage.system
157
+
158
+ const currentTime = performance.now() // Milliseconds with decimal places
159
+ const elapsedUsDividedBy100 = (currentTime - lastTime) * 10
160
+ const userPercent = elapsedUsageUser / elapsedUsDividedBy100
161
+ const systemPercent = elapsedUsageSystem / elapsedUsDividedBy100
148
162
  const totalPercent = userPercent + systemPercent
149
163
 
164
+ lastTime = currentTime
165
+ lastCpuUsage = currentCpuUsage
166
+
150
167
  client.gauge('runtime.node.cpu.system', systemPercent.toFixed(2))
151
168
  client.gauge('runtime.node.cpu.user', userPercent.toFixed(2))
152
169
  client.gauge('runtime.node.cpu.total', totalPercent.toFixed(2))
@@ -160,12 +177,44 @@ function captureMemoryUsage () {
160
177
  client.gauge('runtime.node.mem.rss', stats.rss)
161
178
  client.gauge('runtime.node.mem.total', os.totalmem())
162
179
  client.gauge('runtime.node.mem.free', os.freemem())
180
+ client.gauge('runtime.node.mem.external', stats.external)
181
+ // TODO: Add arrayBuffers to the metrics. That also requires the
182
+ // node_metadata.csv to be updated for the website.
183
+ //
184
+ // client.gauge('runtime.node.mem.arrayBuffers', stats.arrayBuffers)
185
+ }
163
186
 
164
- stats.external && client.gauge('runtime.node.mem.external', stats.external)
187
+ function captureUptime () {
188
+ // WARNING: lastTime must be updated in the same interval before this function is called!
189
+ // This is a faster `process.uptime()`.
190
+ client.gauge('runtime.node.process.uptime', Math.round((lastTime + 499) / 1000))
165
191
  }
166
192
 
167
- function captureProcess () {
168
- client.gauge('runtime.node.process.uptime', Math.round(process.uptime()))
193
+ function captureEventLoopDelay () {
194
+ eventLoopDelayObserver.disable()
195
+
196
+ if (eventLoopDelayObserver.count !== 0) {
197
+ const minimum = eventLoopDelayResolution * 1e6
198
+ const avg = Math.max(eventLoopDelayObserver.mean - minimum, 0)
199
+ const sum = Math.round(avg * eventLoopDelayObserver.count)
200
+
201
+ if (sum !== 0) {
202
+ // Normalize the metrics to the same format as the native metrics.
203
+ const stats = {
204
+ min: Math.max(eventLoopDelayObserver.min - minimum, 0),
205
+ max: Math.max(eventLoopDelayObserver.max - minimum, 0),
206
+ sum,
207
+ total: sum,
208
+ avg,
209
+ count: eventLoopDelayObserver.count,
210
+ p95: Math.max(eventLoopDelayObserver.percentile(95) - minimum, 0)
211
+ }
212
+
213
+ histogram('runtime.node.event_loop.delay', stats)
214
+ }
215
+ }
216
+ eventLoopDelayObserver = monitorEventLoopDelay({ resolution: eventLoopDelayResolution })
217
+ eventLoopDelayObserver.enable()
169
218
  }
170
219
 
171
220
  function captureHeapStats () {
@@ -176,14 +225,17 @@ function captureHeapStats () {
176
225
  client.gauge('runtime.node.heap.total_physical_size', stats.total_physical_size)
177
226
  client.gauge('runtime.node.heap.total_available_size', stats.total_available_size)
178
227
  client.gauge('runtime.node.heap.heap_size_limit', stats.heap_size_limit)
179
-
180
- stats.malloced_memory && client.gauge('runtime.node.heap.malloced_memory', stats.malloced_memory)
181
- stats.peak_malloced_memory && client.gauge('runtime.node.heap.peak_malloced_memory', stats.peak_malloced_memory)
228
+ client.gauge('runtime.node.heap.malloced_memory', stats.malloced_memory)
229
+ client.gauge('runtime.node.heap.peak_malloced_memory', stats.peak_malloced_memory)
230
+ // TODO: Add number_of_native_contexts and number_of_detached_contexts to the
231
+ // metrics. Those metrics allow to identify memory leaks. Adding them also
232
+ // requires the node_metadata.csv to be updated for the website.
233
+ //
234
+ // client.gauge('runtime.node.heap.number_of_native_contexts', stats.number_of_native_contexts)
235
+ // client.gauge('runtime.node.heap.number_of_detached_contexts', stats.number_of_detached_contexts)
182
236
  }
183
237
 
184
238
  function captureHeapSpace () {
185
- if (!v8.getHeapSpaceStatistics) return
186
-
187
239
  const stats = v8.getHeapSpaceStatistics()
188
240
 
189
241
  for (let i = 0, l = stats.length; i < l; i++) {
@@ -197,55 +249,63 @@ function captureHeapSpace () {
197
249
  }
198
250
 
199
251
  /**
200
- * Gathers and reports Event Loop Utilization (ELU) since last run
252
+ * Gathers and reports Event Loop Utilization (ELU) since last run, or from the
253
+ * start of the process on first run.
201
254
  *
202
255
  * ELU is a measure of how busy the event loop is, like running JavaScript or
203
256
  * waiting on *Sync functions. The value is between 0 (idle) and 1 (exhausted).
204
- *
205
- * performance.eventLoopUtilization available in Node.js >= v14.10, >= v12.19, >= v16
206
257
  */
207
- let captureELU = () => {}
208
- if ('eventLoopUtilization' in performance) {
209
- captureELU = () => {
210
- // if elu is undefined (first run) the measurement is from start of process
211
- elu = performance.eventLoopUtilization(elu)
258
+ let lastElu = { idle: 0, active: 0 }
259
+ function captureELU () {
260
+ const elu = performance.eventLoopUtilization()
212
261
 
213
- client.gauge('runtime.node.event_loop.utilization', elu.utilization)
214
- }
262
+ const idle = elu.idle - lastElu.idle
263
+ const active = elu.active - lastElu.active
264
+ const utilization = active / (idle + active)
265
+
266
+ lastElu = elu
267
+
268
+ client.gauge('runtime.node.event_loop.utilization', utilization)
215
269
  }
216
270
 
217
- function captureCommonMetrics () {
271
+ function captureCommonMetrics (trackEventLoop) {
218
272
  captureMemoryUsage()
219
- captureProcess()
273
+ captureUptime()
220
274
  captureHeapStats()
221
- captureELU()
275
+ if (trackEventLoop) {
276
+ captureELU()
277
+ }
222
278
  }
223
279
 
224
- function captureNativeMetrics () {
280
+ function captureNativeMetrics (trackEventLoop, trackGc) {
225
281
  const stats = nativeMetrics.stats()
226
282
  const spaces = stats.heap.spaces
227
- const elapsedTime = process.hrtime(time)
228
283
 
229
- time = process.hrtime()
284
+ const currentTime = performance.now() // Milliseconds with decimal places
285
+ const elapsedUsDividedBy100 = (currentTime - lastTime) * 10
286
+ lastTime = currentTime
230
287
 
231
- const elapsedUs = elapsedTime[0] * 1e6 + elapsedTime[1] / 1e3
232
- const userPercent = 100 * stats.cpu.user / elapsedUs
233
- const systemPercent = 100 * stats.cpu.system / elapsedUs
288
+ const userPercent = stats.cpu.user / elapsedUsDividedBy100
289
+ const systemPercent = stats.cpu.system / elapsedUsDividedBy100
234
290
  const totalPercent = userPercent + systemPercent
235
291
 
236
292
  client.gauge('runtime.node.cpu.system', systemPercent.toFixed(2))
237
293
  client.gauge('runtime.node.cpu.user', userPercent.toFixed(2))
238
294
  client.gauge('runtime.node.cpu.total', totalPercent.toFixed(2))
239
295
 
240
- histogram('runtime.node.event_loop.delay', stats.eventLoop)
296
+ if (trackEventLoop) {
297
+ histogram('runtime.node.event_loop.delay', stats.eventLoop)
298
+ }
241
299
 
242
- Object.keys(stats.gc).forEach(type => {
243
- if (type === 'all') {
244
- histogram('runtime.node.gc.pause', stats.gc[type])
245
- } else {
246
- histogram('runtime.node.gc.pause.by.type', stats.gc[type], `gc_type:${type}`)
300
+ if (trackGc) {
301
+ for (const [type, value] of Object.entries(stats.gc)) {
302
+ if (type === 'all') {
303
+ histogram('runtime.node.gc.pause', value)
304
+ } else {
305
+ histogram('runtime.node.gc.pause.by.type', value, `gc_type:${type}`)
306
+ }
247
307
  }
248
- })
308
+ }
249
309
 
250
310
  for (let i = 0, l = spaces.length; i < l; i++) {
251
311
  const tag = `heap_space:${spaces[i].space_name}`
@@ -258,13 +318,19 @@ function captureNativeMetrics () {
258
318
  }
259
319
 
260
320
  function histogram (name, stats, tag) {
321
+ if (stats.count === 0) {
322
+ return
323
+ }
261
324
  client.gauge(`${name}.min`, stats.min, tag)
262
325
  client.gauge(`${name}.max`, stats.max, tag)
263
326
  client.increment(`${name}.sum`, stats.sum, tag)
264
327
  client.increment(`${name}.total`, stats.sum, tag)
265
328
  client.gauge(`${name}.avg`, stats.avg, tag)
266
329
  client.increment(`${name}.count`, stats.count, tag)
267
- client.gauge(`${name}.median`, stats.median, tag)
330
+ if (stats.median !== undefined) {
331
+ // TODO: Consider adding the median to the Node.js histogram/adding stddev to native metrics.
332
+ client.gauge(`${name}.median`, stats.median, tag)
333
+ }
268
334
  client.gauge(`${name}.95percentile`, stats.p95, tag)
269
335
  }
270
336
 
@@ -274,43 +340,27 @@ function startGCObserver () {
274
340
  gcObserver = new PerformanceObserver(list => {
275
341
  for (const entry of list.getEntries()) {
276
342
  const type = gcType(entry.detail?.kind || entry.kind)
343
+ const duration = entry.duration * 1_000_000
277
344
 
278
- runtimeMetrics.histogram('runtime.node.gc.pause.by.type', entry.duration, `gc_type:${type}`)
279
- runtimeMetrics.histogram('runtime.node.gc.pause', entry.duration)
345
+ // These are individual metrics for each type of GC.
346
+ client.histogram('runtime.node.gc.pause.by.type', duration, `gc_type:${type}`)
347
+ client.histogram('runtime.node.gc.pause', duration)
280
348
  }
281
349
  })
282
350
 
283
351
  gcObserver.observe({ type: 'gc' })
284
352
  }
285
353
 
354
+ const minorGCType = NODE_MAJOR >= 22 ? 'minor_mark_sweep' : 'minor_mark_compact'
355
+
286
356
  function gcType (kind) {
287
- if (NODE_MAJOR >= 22) {
288
- switch (kind) {
289
- case 1: return 'scavenge'
290
- case 2: return 'minor_mark_sweep'
291
- case 4: return 'mark_sweep_compact' // Deprecated, might be removed soon.
292
- case 8: return 'incremental_marking'
293
- case 16: return 'process_weak_callbacks'
294
- case 31: return 'all'
295
- }
296
- } else if (NODE_MAJOR >= 18) {
297
- switch (kind) {
298
- case 1: return 'scavenge'
299
- case 2: return 'minor_mark_compact'
300
- case 4: return 'mark_sweep_compact'
301
- case 8: return 'incremental_marking'
302
- case 16: return 'process_weak_callbacks'
303
- case 31: return 'all'
304
- }
305
- } else {
306
- switch (kind) {
307
- case 1: return 'scavenge'
308
- case 2: return 'mark_sweep_compact'
309
- case 4: return 'incremental_marking'
310
- case 8: return 'process_weak_callbacks'
311
- case 15: return 'all'
312
- }
357
+ switch (kind) {
358
+ case 1: return 'scavenge'
359
+ case 2: return minorGCType
360
+ case 4: return 'mark_sweep_compact' // Deprecated, might be removed soon.
361
+ case 8: return 'incremental_marking'
362
+ case 16: return 'process_weak_callbacks'
363
+ case 31: return 'all'
364
+ default: return 'unknown'
313
365
  }
314
-
315
- return 'unknown'
316
366
  }
@@ -8,6 +8,8 @@
8
8
  "DD_API_KEY": ["A"],
9
9
  "DD_API_SECURITY_ENABLED": ["A"],
10
10
  "DD_API_SECURITY_SAMPLE_DELAY": ["A"],
11
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED": ["A"],
12
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT": ["A"],
11
13
  "DD_APM_FLUSH_DEADLINE_MILLISECONDS": ["A"],
12
14
  "DD_APM_TRACING_ENABLED": ["A"],
13
15
  "DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": ["A"],
@@ -29,7 +31,6 @@
29
31
  "DD_APPSEC_STACK_TRACE_ENABLED": ["A"],
30
32
  "DD_APPSEC_TRACE_RATE_LIMIT": ["A"],
31
33
  "DD_APPSEC_WAF_TIMEOUT": ["A"],
32
- "DD_AZURE_APP_SERVICES": ["A"],
33
34
  "DD_CIVISIBILITY_AGENTLESS_ENABLED": ["A"],
34
35
  "DD_CIVISIBILITY_AGENTLESS_URL": ["A"],
35
36
  "DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER": ["A"],
@@ -0,0 +1,124 @@
1
+ 'use strict'
2
+
3
+ const dc = require('dc-polyfill')
4
+ const { sendData } = require('./send-data')
5
+
6
+ const fastifyRouteCh = dc.channel('apm:fastify:route:added')
7
+
8
+ let config
9
+ let application
10
+ let host
11
+ let getRetryData
12
+ let updateRetryData
13
+
14
+ /**
15
+ * Keep track of endpoints that still need to be sent.
16
+ * Map key is `${METHOD} ${PATH}`, value is { method, path }
17
+ */
18
+ const pendingEndpoints = new Map()
19
+ let flushScheduled = false
20
+ let isFirstPayload = true
21
+
22
+ function endpointKey (method, path) {
23
+ return `${method.toUpperCase()} ${path}`
24
+ }
25
+
26
+ function scheduleFlush () {
27
+ if (flushScheduled) return
28
+ flushScheduled = true
29
+ setImmediate(flushAndSend).unref()
30
+ }
31
+
32
+ function recordEndpoint (method, path) {
33
+ const key = endpointKey(method, path)
34
+ if (pendingEndpoints.has(key)) return
35
+
36
+ pendingEndpoints.set(key, { method: method.toUpperCase(), path })
37
+ scheduleFlush()
38
+ }
39
+
40
+ function onFastifyRoute (routeData) {
41
+ const routeOptions = routeData?.routeOptions
42
+ if (!routeOptions?.path) return
43
+
44
+ const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method]
45
+
46
+ for (const method of methods) {
47
+ recordEndpoint(method, routeOptions.path)
48
+ }
49
+ }
50
+
51
+ function buildEndpointObjects (endpoints) {
52
+ return endpoints.map(({ method, path }) => {
53
+ return {
54
+ type: 'REST',
55
+ method,
56
+ path,
57
+ operation_name: 'http.request',
58
+ resource_name: endpointKey(method, path)
59
+ }
60
+ })
61
+ }
62
+
63
+ function flushAndSend () {
64
+ flushScheduled = false
65
+ if (pendingEndpoints.size === 0) return
66
+
67
+ const batchEndpoints = []
68
+ for (const [key, endpoint] of pendingEndpoints) {
69
+ batchEndpoints.push(endpoint)
70
+ pendingEndpoints.delete(key)
71
+ if (batchEndpoints.length >= config.appsec?.apiSecurity?.endpointCollectionMessageLimit) break
72
+ }
73
+
74
+ const payloadObj = {
75
+ is_first: isFirstPayload,
76
+ endpoints: buildEndpointObjects(batchEndpoints)
77
+ }
78
+
79
+ let reqType = 'app-endpoints'
80
+ let payload = payloadObj
81
+
82
+ const retryData = getRetryData()
83
+ if (retryData) {
84
+ payload = [
85
+ { request_type: 'app-endpoints', payload: payloadObj },
86
+ { request_type: retryData.reqType, payload: retryData.payload }
87
+ ]
88
+ reqType = 'message-batch'
89
+ }
90
+
91
+ sendData(config, application, host, reqType, payload, updateRetryData)
92
+
93
+ if (isFirstPayload) {
94
+ isFirstPayload = false
95
+ }
96
+
97
+ // If more endpoints accumulated while sending, schedule another flush.
98
+ if (pendingEndpoints.size) scheduleFlush()
99
+ }
100
+
101
+ function start (_config = {}, _application, _host, getRetryDataFunction, updateRetryDataFunction) {
102
+ if (!_config?.appsec?.apiSecurity?.endpointCollectionEnabled) return
103
+
104
+ config = _config
105
+ application = _application
106
+ host = _host
107
+ getRetryData = getRetryDataFunction
108
+ updateRetryData = updateRetryDataFunction
109
+
110
+ fastifyRouteCh.subscribe(onFastifyRoute)
111
+ }
112
+
113
+ function stop () {
114
+ fastifyRouteCh.unsubscribe(onFastifyRoute)
115
+
116
+ pendingEndpoints.clear()
117
+ flushScheduled = false
118
+ config = application = host = getRetryData = updateRetryData = null
119
+ }
120
+
121
+ module.exports = {
122
+ start,
123
+ stop
124
+ }
@@ -3,6 +3,7 @@ const tracerVersion = require('../../../../package.json').version
3
3
  const dc = require('dc-polyfill')
4
4
  const os = require('os')
5
5
  const dependencies = require('./dependencies')
6
+ const endpoints = require('./endpoints')
6
7
  const { sendData } = require('./send-data')
7
8
  const { errors } = require('../startup-log')
8
9
  const { manager: metricsManager } = require('./metrics')
@@ -254,6 +255,7 @@ function start (aConfig, thePluginManager) {
254
255
 
255
256
  dependencies.start(config, application, host, getRetryData, updateRetryData)
256
257
  telemetryLogger.start(config)
258
+ endpoints.start(config, application, host, getRetryData, updateRetryData)
257
259
 
258
260
  sendData(config, application, host, 'app-started', appStarted(config))
259
261
 
@@ -280,6 +282,7 @@ function stop () {
280
282
 
281
283
  telemetryStopChannel.publish(getTelemetryData())
282
284
 
285
+ endpoints.stop()
283
286
  config = undefined
284
287
  }
285
288