dd-trace 5.105.0 → 5.107.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.
Files changed (108) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +5 -7
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -2
  7. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  8. package/packages/datadog-instrumentations/src/connect.js +3 -2
  9. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  10. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +19 -0
  11. package/packages/datadog-instrumentations/src/cucumber.js +319 -152
  12. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  13. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  14. package/packages/datadog-instrumentations/src/express.js +24 -20
  15. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  16. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  18. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  19. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  20. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  21. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  22. package/packages/datadog-instrumentations/src/jest.js +143 -73
  23. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  25. package/packages/datadog-instrumentations/src/multer.js +3 -2
  26. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  27. package/packages/datadog-instrumentations/src/net.js +8 -6
  28. package/packages/datadog-instrumentations/src/openai.js +19 -7
  29. package/packages/datadog-instrumentations/src/pg.js +19 -0
  30. package/packages/datadog-instrumentations/src/router.js +12 -10
  31. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  32. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  33. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +1 -1
  34. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +218 -4
  35. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  36. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  37. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  38. package/packages/datadog-plugin-http/src/client.js +0 -3
  39. package/packages/datadog-plugin-http/src/server.js +11 -1
  40. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  41. package/packages/datadog-plugin-pg/src/index.js +10 -0
  42. package/packages/dd-trace/src/aiguard/index.js +34 -15
  43. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  44. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  46. package/packages/dd-trace/src/config/defaults.js +14 -0
  47. package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -1
  48. package/packages/dd-trace/src/config/helper.js +1 -0
  49. package/packages/dd-trace/src/config/index.js +5 -9
  50. package/packages/dd-trace/src/config/parsers.js +8 -0
  51. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  53. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  54. package/packages/dd-trace/src/debugger/config.js +1 -1
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  56. package/packages/dd-trace/src/debugger/index.js +1 -2
  57. package/packages/dd-trace/src/dogstatsd.js +2 -3
  58. package/packages/dd-trace/src/encode/0.4.js +49 -41
  59. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  60. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  61. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  62. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  63. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  64. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  65. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  66. package/packages/dd-trace/src/id.js +15 -0
  67. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +91 -5
  68. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +43 -21
  69. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  70. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  71. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  72. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  73. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  74. package/packages/dd-trace/src/llmobs/util.js +54 -0
  75. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  76. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  77. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  78. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  79. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  80. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  81. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  82. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +25 -5
  83. package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -10
  84. package/packages/dd-trace/src/opentracing/span.js +23 -18
  85. package/packages/dd-trace/src/opentracing/span_context.js +1 -3
  86. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  87. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  88. package/packages/dd-trace/src/priority_sampler.js +6 -5
  89. package/packages/dd-trace/src/profiling/config.js +11 -25
  90. package/packages/dd-trace/src/profiling/exporters/agent.js +11 -10
  91. package/packages/dd-trace/src/profiling/profiler.js +19 -9
  92. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -3
  93. package/packages/dd-trace/src/proxy.js +13 -10
  94. package/packages/dd-trace/src/remote_config/index.js +1 -2
  95. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  96. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  97. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  98. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  99. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  100. package/packages/dd-trace/src/span_format.js +33 -25
  101. package/packages/dd-trace/src/span_stats.js +1 -1
  102. package/packages/dd-trace/src/startup-log.js +1 -2
  103. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  104. package/packages/dd-trace/src/tracer.js +1 -1
  105. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  106. package/vendor/dist/shell-quote/index.js +1 -1
  107. package/packages/dd-trace/src/agent/url.js +0 -28
  108. package/scripts/preinstall.js +0 -34
@@ -40,6 +40,16 @@ function processInfo (infos, info, type) {
40
40
  }
41
41
  }
42
42
 
43
+ // Route pprof through the central log module so logLevel applies.
44
+ const pprofLogger = {
45
+ trace: (...args) => log.trace(...args),
46
+ debug: (...args) => log.debug(...args),
47
+ info: (...args) => log.info(...args),
48
+ warn: (...args) => log.warn(...args),
49
+ error: (...args) => log.error(...args),
50
+ fatal: (...args) => log.error(...args),
51
+ }
52
+
43
53
  class Profiler extends EventEmitter {
44
54
  #compressionFn
45
55
  #compressionFnInitialized = false
@@ -49,7 +59,6 @@ class Profiler extends EventEmitter {
49
59
  #enabled = false
50
60
  #endpointCounts = new Map()
51
61
  #lastStart
52
- #logger
53
62
  #profileSeq = 0
54
63
  #spanFinishListener
55
64
  #timer
@@ -159,14 +168,13 @@ class Profiler extends EventEmitter {
159
168
  this.#enabled = true
160
169
 
161
170
  const config = this.#config = new Config(options)
162
- this.#logger = config.logger
163
171
 
164
172
  this._setInterval()
165
173
  // Log errors if the source map finder fails, but don't prevent the rest
166
174
  // of the profiler from running without source maps.
167
175
  let mapper
168
176
  const { setLogger, SourceMapper } = require('@datadog/pprof')
169
- setLogger(config.logger)
177
+ setLogger(pprofLogger)
170
178
 
171
179
  if (config.sourceMap) {
172
180
  mapper = new SourceMapper(config.debugSourceMaps)
@@ -174,7 +182,8 @@ class Profiler extends EventEmitter {
174
182
  .then(() => {
175
183
  if (config.debugSourceMaps) {
176
184
  const count = mapper.infoMap.size
177
- this.#logger.debug(() => {
185
+ // eslint-disable-next-line eslint-rules/eslint-log-printf-style
186
+ log.debug(() => {
178
187
  return count === 0
179
188
  ? 'Found no source maps'
180
189
  : `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
@@ -195,7 +204,7 @@ class Profiler extends EventEmitter {
195
204
  mapper,
196
205
  nearOOMCallback,
197
206
  })
198
- this.#logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`)
207
+ log.debug('Started %s profiler in %s thread', profiler.type, threadNamePrefix)
199
208
  }
200
209
 
201
210
  if (config.endpointCollectionEnabled) {
@@ -248,7 +257,7 @@ class Profiler extends EventEmitter {
248
257
 
249
258
  for (const profiler of this.#config.profilers) {
250
259
  profiler.stop()
251
- this.#logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
260
+ log.debug('Stopped %s profiler in %s thread', profiler.type, threadNamePrefix)
252
261
  }
253
262
 
254
263
  clearTimeout(this.#timer)
@@ -312,7 +321,7 @@ class Profiler extends EventEmitter {
312
321
  const info = profiler.getInfo()
313
322
  const profile = profiler.profile(restart, startDate, endDate)
314
323
  if (!restart) {
315
- this.#logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
324
+ log.debug('Stopped %s profiler in %s thread', profiler.type, threadNamePrefix)
316
325
  }
317
326
  if (!profile) continue
318
327
  profiles.push({ profiler, profile, info })
@@ -341,7 +350,8 @@ class Profiler extends EventEmitter {
341
350
  infos.hasMissingSourceMaps = true
342
351
  }
343
352
  processInfo(infos, info, profiler.type)
344
- this.#logger.debug(() => {
353
+ // eslint-disable-next-line eslint-rules/eslint-log-printf-style
354
+ log.debug(() => {
345
355
  const profileJson = JSON.stringify(profile, (_, value) => {
346
356
  return typeof value === 'bigint' ? value.toString() : value
347
357
  })
@@ -358,7 +368,7 @@ class Profiler extends EventEmitter {
358
368
  if (hasEncoded) {
359
369
  await this.#submit(encodedProfiles, infos, startDate, endDate, snapshotKind)
360
370
  profileSubmittedChannel.publish()
361
- this.#logger.debug('Submitted profiles')
371
+ log.debug('Submitted profiles')
362
372
  }
363
373
  } catch (error) {
364
374
  log.error(error)
@@ -3,6 +3,7 @@
3
3
  const dc = require('dc-polyfill')
4
4
 
5
5
  const { storage } = require('../../../../datadog-core')
6
+ const log = require('../../log')
6
7
  const runtimeMetrics = require('../../runtime_metrics')
7
8
  const telemetryMetrics = require('../../telemetry/metrics')
8
9
  const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
@@ -112,7 +113,6 @@ class NativeWallProfiler {
112
113
  #customLabelKeys
113
114
  #endpointCollectionEnabled = false
114
115
  #flushIntervalMillis = 0
115
- #logger
116
116
  #mapper
117
117
  #pprof
118
118
  #samplingIntervalMicros = 0
@@ -136,7 +136,6 @@ class NativeWallProfiler {
136
136
  this.#cpuProfilingEnabled = !!options.cpuProfilingEnabled
137
137
  this.#endpointCollectionEnabled = !!options.endpointCollectionEnabled
138
138
  this.#flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
139
- this.#logger = options.logger
140
139
  // TODO: Remove default value. It is only used in testing.
141
140
  this.#samplingIntervalMicros = (options.samplingInterval || 1e3 / 99) * 1000
142
141
  this.#telemetryHeartbeatIntervalMillis = options.heartbeatInterval || 60 * 1e3 // 60 seconds
@@ -348,7 +347,7 @@ class NativeWallProfiler {
348
347
  #reportV8bug (maybeBug) {
349
348
  const tag = `v8_profiler_bug_workaround_enabled:${this.#v8ProfilerBugWorkaroundEnabled}`
350
349
  const metric = `v8_cpu_profiler${maybeBug ? '_maybe' : ''}_stuck_event_loop`
351
- this.#logger?.warn(`Wall profiler: ${maybeBug ? 'possible ' : ''}v8 profiler stuck event loop detected.`)
350
+ log.warn('Wall profiler: %sv8 profiler stuck event loop detected.', maybeBug ? 'possible ' : '')
352
351
  // report as runtime metric (can be removed in the future when telemetry is mature)
353
352
  runtimeMetrics.increment(`runtime.node.profiler.${metric}`, tag, true)
354
353
  // report as telemetry metric
@@ -195,6 +195,19 @@ class Tracer extends NoopProxy {
195
195
  }
196
196
  }
197
197
 
198
+ // OTel logs/metrics pipelines must be initialized BEFORE runtimeMetrics.start so that
199
+ // when the OTLP runtime metrics module calls metrics.getMeterProvider(), it gets the
200
+ // real provider, otherwise instruments register on the noop provider and never export.
201
+ if (config.DD_LOGS_OTEL_ENABLED) {
202
+ const { initializeOpenTelemetryLogs } = require('./opentelemetry/logs')
203
+ initializeOpenTelemetryLogs(config)
204
+ }
205
+
206
+ if (config.DD_METRICS_OTEL_ENABLED) {
207
+ const { initializeOpenTelemetryMetrics } = require('./opentelemetry/metrics')
208
+ initializeOpenTelemetryMetrics(config)
209
+ }
210
+
198
211
  if (config.runtimeMetrics.enabled) {
199
212
  runtimeMetrics.start(config)
200
213
  }
@@ -224,16 +237,6 @@ class Tracer extends NoopProxy {
224
237
  }
225
238
  }
226
239
 
227
- if (config.DD_LOGS_OTEL_ENABLED) {
228
- const { initializeOpenTelemetryLogs } = require('./opentelemetry/logs')
229
- initializeOpenTelemetryLogs(config)
230
- }
231
-
232
- if (config.DD_METRICS_OTEL_ENABLED) {
233
- const { initializeOpenTelemetryMetrics } = require('./opentelemetry/metrics')
234
- initializeOpenTelemetryMetrics(config)
235
- }
236
-
237
240
  if (config.isTestDynamicInstrumentationEnabled) {
238
241
  const getDynamicInstrumentationClient = require('./ci-visibility/dynamic-instrumentation')
239
242
  // We instantiate the client but do not start the Worker here. The worker is started lazily
@@ -8,7 +8,6 @@ const { getExtraServices } = require('../service-naming/extra-services')
8
8
  const getGitMetadata = require('../git_metadata')
9
9
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
10
10
  const tagger = require('../tagger')
11
- const { getAgentUrl } = require('../agent/url')
12
11
  const processTags = require('../process-tags')
13
12
  const Scheduler = require('./scheduler')
14
13
  const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states')
@@ -32,7 +31,7 @@ class RemoteConfig {
32
31
  constructor (config) {
33
32
  const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000)
34
33
 
35
- this.url = getAgentUrl(config)
34
+ this.url = config.url
36
35
 
37
36
  tagger.add(config.tags, {
38
37
  '_dd.rc.client_id': clientId,
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd')
4
+ const processTags = require('../process-tags')
5
+
6
+ /**
7
+ * Builds the aggregating DogStatsD client used to emit DD-proprietary tracer
8
+ * metrics (runtime.node.*, datadog.tracer.*). Shared by both runtime-metrics
9
+ * paths (DogStatsD and OTLP) so their client construction can't drift apart.
10
+ *
11
+ * Process tags are applied here, not via config/generateClientConfig, so they only
12
+ * reach this bounded set of runtime metrics. Putting them on the global tags would
13
+ * also tag user-facing custom metrics, inflating their cardinality (and billing).
14
+ *
15
+ * @param {import('../config/config-base')} config - Tracer configuration
16
+ * @returns {MetricsAggregationClient}
17
+ */
18
+ function createMetricsClient (config) {
19
+ const clientConfig = DogStatsDClient.generateClientConfig(config)
20
+
21
+ if (config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED) {
22
+ for (const tag of processTags.tagsArray) {
23
+ clientConfig.tags.push(tag)
24
+ }
25
+ }
26
+
27
+ return new MetricsAggregationClient(new DogStatsDClient(clientConfig))
28
+ }
29
+
30
+ module.exports = { createMetricsClient }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../log')
4
+
3
5
  let runtimeMetrics
4
6
 
5
7
  const noop = runtimeMetrics = {
@@ -20,11 +22,19 @@ module.exports = {
20
22
  start (config) {
21
23
  if (!config?.runtimeMetrics.enabled) return
22
24
 
23
- runtimeMetrics = require('./runtime_metrics')
25
+ runtimeMetrics = config.DD_METRICS_OTEL_ENABLED
26
+ ? require('./otlp_runtime_metrics')
27
+ : require('./runtime_metrics')
24
28
 
25
29
  Object.setPrototypeOf(module.exports, runtimeMetrics)
26
30
 
27
- runtimeMetrics.start(config)
31
+ try {
32
+ runtimeMetrics.start(config)
33
+ } catch (err) {
34
+ // Unwind whatever managed to register so a partial init doesn't leak into the next start().
35
+ runtimeMetrics.stop()
36
+ log.error('Failed to start runtime metrics', err)
37
+ }
28
38
  },
29
39
 
30
40
  stop () {
@@ -0,0 +1,284 @@
1
+ 'use strict'
2
+
3
+ const v8 = require('node:v8')
4
+ const process = require('node:process')
5
+ const { performance, monitorEventLoopDelay, PerformanceObserver, constants } = require('node:perf_hooks')
6
+ const { metrics } = require('@opentelemetry/api')
7
+ const log = require('../log')
8
+ const { createMetricsClient } = require('./client')
9
+
10
+ const METER_NAME = 'datadog.runtime_metrics'
11
+
12
+ const ATTR_ELU_STATE_IDLE = { 'nodejs.eventloop.state': 'idle' }
13
+ const ATTR_ELU_STATE_ACTIVE = { 'nodejs.eventloop.state': 'active' }
14
+
15
+ // Pre-allocated `{ 'v8js.gc.type': <type> }` attribute objects so the observer
16
+ // doesn't allocate a new one per entry under GC pressure.
17
+ const ATTR_GC_TYPE_MINOR = { 'v8js.gc.type': 'minor' }
18
+ const ATTR_GC_TYPE_MAJOR = { 'v8js.gc.type': 'major' }
19
+ const ATTR_GC_TYPE_INCREMENTAL = { 'v8js.gc.type': 'incremental' }
20
+ const ATTR_GC_TYPE_WEAKCB = { 'v8js.gc.type': 'weakcb' }
21
+
22
+ // Kind 2 is V8's MinorMarkSweep (Node 20+) and not exposed via perf_hooks.constants.
23
+ const GC_ATTR_BY_KIND = new Map([
24
+ [constants.NODE_PERFORMANCE_GC_MINOR, ATTR_GC_TYPE_MINOR],
25
+ [2, ATTR_GC_TYPE_MINOR],
26
+ [constants.NODE_PERFORMANCE_GC_MAJOR, ATTR_GC_TYPE_MAJOR],
27
+ [constants.NODE_PERFORMANCE_GC_INCREMENTAL, ATTR_GC_TYPE_INCREMENTAL],
28
+ [constants.NODE_PERFORMANCE_GC_WEAKCB, ATTR_GC_TYPE_WEAKCB],
29
+ ])
30
+
31
+ let meter = null
32
+ let eventLoopHistogram = null
33
+ let gcObserver = null
34
+ let lastElu = null
35
+
36
+ // Cache `{ 'v8js.heap.space.name': <name> }` per V8 space name to avoid per-scrape allocations.
37
+ const HEAP_SPACE_ATTR_CACHE = new Map()
38
+ function getHeapSpaceAttr (name) {
39
+ let attr = HEAP_SPACE_ATTR_CACHE.get(name)
40
+ if (!attr) {
41
+ attr = { 'v8js.heap.space.name': name }
42
+ HEAP_SPACE_ATTR_CACHE.set(name, attr)
43
+ }
44
+ return attr
45
+ }
46
+
47
+ // getMeter() returns a cached meter, so without tracking what we registered we'd
48
+ // stack callbacks every time start() runs.
49
+ const registeredCallbacks = []
50
+ const registeredBatchCallbacks = []
51
+
52
+ // DD-proprietary tracer metrics (runtime.node.spans.*, datadog.tracer.*) have no OTel
53
+ // equivalent; keep a DogStatsD client so OTLP-path customers don't lose them.
54
+ let client = null
55
+ let flushInterval = null
56
+
57
+ module.exports = {
58
+ /**
59
+ * @param {import('../config/config-base')} config - Tracer configuration
60
+ */
61
+ start (config) {
62
+ this.stop()
63
+
64
+ client = createMetricsClient(config)
65
+ flushInterval = setInterval(() => {
66
+ client.flush()
67
+ }, config.DD_RUNTIME_METRICS_FLUSH_INTERVAL ?? 10_000)
68
+ flushInterval.unref?.()
69
+
70
+ meter = metrics.getMeterProvider().getMeter(METER_NAME)
71
+
72
+ const trackEventLoop = config.runtimeMetrics.eventLoop !== false
73
+ const trackGc = config.runtimeMetrics.gc !== false
74
+ if (trackEventLoop) {
75
+ eventLoopHistogram = monitorEventLoopDelay({ resolution: 4 })
76
+ eventLoopHistogram.enable()
77
+ }
78
+
79
+ const heapUsed = createHeapInstrument('v8js.memory.heap.used', 'V8 heap memory used.')
80
+ const heapLimit = createHeapInstrument('v8js.memory.heap.limit', 'V8 heap memory total available size.')
81
+ const heapSpaceAvailable = createHeapInstrument(
82
+ 'v8js.memory.heap.space.available_size', 'V8 heap space available size.')
83
+ const heapSpacePhysical = createHeapInstrument(
84
+ 'v8js.memory.heap.space.physical_size', 'V8 heap space physical size.')
85
+ const heapSpaceSize = createHeapInstrument(
86
+ 'v8js.memory.heap.space.size', 'Total heap memory size pre-allocated for a heap space.')
87
+
88
+ registerBatchCallback(
89
+ (result) => {
90
+ const stats = v8.getHeapStatistics()
91
+ result.observe(heapLimit, stats.heap_size_limit)
92
+
93
+ const spaces = v8.getHeapSpaceStatistics()
94
+ for (let i = 0; i < spaces.length; i++) {
95
+ const space = spaces[i]
96
+ const attr = getHeapSpaceAttr(space.space_name)
97
+ result.observe(heapUsed, space.space_used_size, attr)
98
+ result.observe(heapSpaceAvailable, space.space_available_size, attr)
99
+ result.observe(heapSpacePhysical, space.physical_space_size, attr)
100
+ result.observe(heapSpaceSize, space.space_size, attr)
101
+ }
102
+ },
103
+ [heapUsed, heapLimit, heapSpaceAvailable, heapSpacePhysical, heapSpaceSize]
104
+ )
105
+
106
+ const activeResource = meter.createObservableGauge('v8js.resource.active', {
107
+ unit: '{resource}',
108
+ description: 'Gauge of the active resources that are currently keeping the event loop alive.',
109
+ })
110
+ registerCallback((result) => {
111
+ const counts = new Map()
112
+ // Stable since Node 22.16; available on 18+ as experimental.
113
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
114
+ for (const resource of process.getActiveResourcesInfo()) {
115
+ counts.set(resource, (counts.get(resource) ?? 0) + 1)
116
+ }
117
+ for (const [type, count] of counts) {
118
+ result.observe(count, { 'v8js.resource.type': type })
119
+ }
120
+ }, activeResource)
121
+
122
+ // Spec wants nodejs.eventloop.delay.* in seconds; perf_hooks gives nanoseconds.
123
+ // Match @opentelemetry/instrumentation-runtime-node EventLoopDelayCollector: one batch
124
+ // callback, guard on sample count, emit, then reset so each interval is independent.
125
+ if (trackEventLoop) {
126
+ const delayMin = createDelayGauge('nodejs.eventloop.delay.min', 'Event loop minimum delay.')
127
+ const delayMax = createDelayGauge('nodejs.eventloop.delay.max', 'Event loop maximum delay.')
128
+ const delayMean = createDelayGauge('nodejs.eventloop.delay.mean', 'Event loop mean delay.')
129
+ const delayStddev = createDelayGauge('nodejs.eventloop.delay.stddev', 'Event loop standard deviation delay.')
130
+ const delayP50 = createDelayGauge('nodejs.eventloop.delay.p50', 'Event loop 50th percentile delay.')
131
+ const delayP90 = createDelayGauge('nodejs.eventloop.delay.p90', 'Event loop 90th percentile delay.')
132
+ const delayP99 = createDelayGauge('nodejs.eventloop.delay.p99', 'Event loop 99th percentile delay.')
133
+
134
+ registerBatchCallback((result) => {
135
+ const h = eventLoopHistogram
136
+ if (!h || h.count < 5) return
137
+ result.observe(delayMin, h.min / 1e9)
138
+ result.observe(delayMax, h.max / 1e9)
139
+ result.observe(delayMean, h.mean / 1e9)
140
+ result.observe(delayStddev, h.stddev / 1e9)
141
+ result.observe(delayP50, h.percentile(50) / 1e9)
142
+ result.observe(delayP90, h.percentile(90) / 1e9)
143
+ result.observe(delayP99, h.percentile(99) / 1e9)
144
+ h.reset()
145
+ }, [delayMin, delayMax, delayMean, delayStddev, delayP50, delayP90, delayP99])
146
+
147
+ if (performance.eventLoopUtilization) {
148
+ // Baseline so the first observation isn't 1.0.
149
+ lastElu = performance.eventLoopUtilization()
150
+
151
+ const eluTime = meter.createObservableCounter('nodejs.eventloop.time', {
152
+ unit: 's',
153
+ description: 'Cumulative duration of time the event loop has been in each state.',
154
+ })
155
+ registerCallback((result) => {
156
+ const elu = performance.eventLoopUtilization()
157
+ result.observe(elu.idle / 1000, ATTR_ELU_STATE_IDLE)
158
+ result.observe(elu.active / 1000, ATTR_ELU_STATE_ACTIVE)
159
+ }, eluTime)
160
+
161
+ const eluGauge = meter.createObservableGauge('nodejs.eventloop.utilization', {
162
+ unit: '1',
163
+ description: 'Event loop utilization.',
164
+ })
165
+ registerCallback((result) => {
166
+ const current = performance.eventLoopUtilization()
167
+ const idle = current.idle - lastElu.idle
168
+ const active = current.active - lastElu.active
169
+ lastElu = current
170
+ const total = idle + active
171
+ result.observe(total > 0 ? active / total : 0)
172
+ }, eluGauge)
173
+ }
174
+ }
175
+
176
+ if (trackGc) {
177
+ const gcHistogram = meter.createHistogram('v8js.gc.duration', {
178
+ unit: 's',
179
+ description: 'Garbage collection duration.',
180
+ })
181
+ gcObserver = new PerformanceObserver(list => {
182
+ const entries = list.getEntries()
183
+ for (let i = 0; i < entries.length; i++) {
184
+ const entry = entries[i]
185
+ const attr = GC_ATTR_BY_KIND.get(entry.detail?.kind ?? entry.kind)
186
+ if (attr === undefined) continue
187
+ gcHistogram.record(entry.duration / 1000, attr)
188
+ }
189
+ })
190
+ gcObserver.observe({ type: 'gc' })
191
+ }
192
+
193
+ log.debug('Started OTLP runtime metrics with OTel-native naming (v8js.*, nodejs.*)')
194
+ },
195
+
196
+ /**
197
+ * @returns {void}
198
+ */
199
+ stop () {
200
+ if (eventLoopHistogram) {
201
+ eventLoopHistogram.disable()
202
+ eventLoopHistogram = null
203
+ }
204
+ gcObserver?.disconnect()
205
+ gcObserver = null
206
+ for (let i = 0; i < registeredCallbacks.length; i++) {
207
+ const [callback, instrument] = registeredCallbacks[i]
208
+ instrument.removeCallback(callback)
209
+ }
210
+ registeredCallbacks.length = 0
211
+ if (meter) {
212
+ for (let i = 0; i < registeredBatchCallbacks.length; i++) {
213
+ const [callback, instruments] = registeredBatchCallbacks[i]
214
+ meter.removeBatchObservableCallback(callback, instruments)
215
+ }
216
+ }
217
+ registeredBatchCallbacks.length = 0
218
+ meter = null
219
+ lastElu = null
220
+ if (flushInterval) {
221
+ clearInterval(flushInterval)
222
+ flushInterval = null
223
+ }
224
+ client = null
225
+ },
226
+
227
+ // Tied to @datadog/native-metrics which the OTLP path doesn't enable; noop with expected shape.
228
+ track () { return { finish () {} } },
229
+
230
+ boolean (name, value, tag) {
231
+ client?.boolean(name, value, tag)
232
+ },
233
+ histogram (name, value, tag) {
234
+ client?.histogram(name, value, tag)
235
+ },
236
+ count (name, count, tag, monotonic = false) {
237
+ client?.count(name, count, tag, monotonic)
238
+ },
239
+ gauge (name, value, tag) {
240
+ client?.gauge(name, value, tag)
241
+ },
242
+ increment (name, tag, monotonic) {
243
+ this.count(name, 1, tag, monotonic)
244
+ },
245
+ decrement (name, tag) {
246
+ this.count(name, -1, tag)
247
+ },
248
+ }
249
+
250
+ /**
251
+ * @param {Function} callback
252
+ * @param {object} instrument
253
+ */
254
+ function registerCallback (callback, instrument) {
255
+ instrument.addCallback(callback)
256
+ registeredCallbacks.push([callback, instrument])
257
+ }
258
+
259
+ /**
260
+ * @param {Function} callback
261
+ * @param {Array} instruments
262
+ */
263
+ function registerBatchCallback (callback, instruments) {
264
+ meter.addBatchObservableCallback(callback, instruments)
265
+ registeredBatchCallbacks.push([callback, instruments])
266
+ }
267
+
268
+ /**
269
+ * @param {string} name
270
+ * @param {string} description
271
+ * @returns {object}
272
+ */
273
+ function createHeapInstrument (name, description) {
274
+ return meter.createObservableUpDownCounter(name, { unit: 'By', description })
275
+ }
276
+
277
+ /**
278
+ * @param {string} name
279
+ * @param {string} description
280
+ * @returns {object}
281
+ */
282
+ function createDelayGauge (name, description) {
283
+ return meter.createObservableGauge(name, { unit: 's', description })
284
+ }
@@ -6,11 +6,9 @@ const v8 = require('v8')
6
6
  const os = require('os')
7
7
  const process = require('process')
8
8
  const { performance, PerformanceObserver, monitorEventLoopDelay } = require('perf_hooks')
9
- const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd')
10
9
  const log = require('../log')
11
-
12
10
  const { NODE_MAJOR } = require('../../../../version')
13
- const processTags = require('../process-tags')
11
+ const { createMetricsClient } = require('./client')
14
12
 
15
13
  const eventLoopDelayResolution = 4
16
14
 
@@ -37,18 +35,11 @@ module.exports = {
37
35
  this.stop()
38
36
  // The agent expects a flush every ten seconds, so this is for tests only.
39
37
  const flushIntervalMs = config.DD_RUNTIME_METRICS_FLUSH_INTERVAL
40
- const clientConfig = DogStatsDClient.generateClientConfig(config)
41
-
42
- if (config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED) {
43
- for (const tag of processTags.tagsArray) {
44
- clientConfig.tags.push(tag)
45
- }
46
- }
47
38
 
48
39
  const trackEventLoop = config.runtimeMetrics.eventLoop !== false
49
40
  const trackGc = config.runtimeMetrics.gc !== false
50
41
 
51
- client = new MetricsAggregationClient(new DogStatsDClient(clientConfig))
42
+ client = createMetricsClient(config)
52
43
 
53
44
  if (trackGc) {
54
45
  startGCObserver()
@@ -26,7 +26,11 @@ function resolveServiceSource (span, tracerService) {
26
26
 
27
27
  if (currentService === tracerService) {
28
28
  if (existingSource === undefined) return
29
- spanContext.deleteTag(SVC_SRC_KEY)
29
+ // Clear by assigning undefined rather than deleting: `delete` on the plain
30
+ // `_tags` object drops it into dictionary (slow) mode, so the per-span
31
+ // extractTags scan that follows pays the slow path. The encode loop skips
32
+ // undefined values, so the emitted meta is unchanged.
33
+ spanContext.setTag(SVC_SRC_KEY, undefined)
30
34
  return
31
35
  }
32
36
 
@@ -48,9 +48,10 @@ const { IGNORE_OTEL_ERROR } = constants
48
48
  * @property {Array} links
49
49
  * @property {Array<SpanEvent> | undefined} span_events
50
50
  *
51
- * @typedef {object} SpanEvent
51
+ * @typedef {object} SpanEvent Raw span event as stored on the span; the encoder
52
+ * layer derives `time_unix_nano` from `startTime` via `eventTimeNano`.
52
53
  * @property {string} name
53
- * @property {number} time_unix_nano
54
+ * @property {number} startTime Milliseconds with sub-millisecond precision.
54
55
  * @property {Record<string, string>} [attributes]
55
56
  */
56
57
 
@@ -88,7 +89,6 @@ function formatSpan (span) {
88
89
  metrics: {},
89
90
  start: Math.round(span._startTime * 1e6),
90
91
  duration: Math.round(span._duration * 1e6),
91
- links: [],
92
92
  span_events: undefined,
93
93
  }
94
94
  }
@@ -112,24 +112,32 @@ function setSingleSpanIngestionTags (formattedSpan, options) {
112
112
  * @param {import('./opentracing/span')} span
113
113
  */
114
114
  function extractSpanLinks (formattedSpan, span) {
115
- if (!span._links?.length) {
115
+ const links = span._links
116
+ if (!links?.length) {
116
117
  return
117
118
  }
118
- const links = span._links.map(({ context, attributes }) => {
119
- const formattedLink = {
120
- trace_id: context.toTraceId(true),
121
- span_id: context.toSpanId(true),
119
+ // Build the `_dd.span_links` JSON directly. The trace / span ids are decimal
120
+ // strings (no escaping); attributes are pre-sanitized to a string map and
121
+ // `undefined` when empty, so they only need a presence check. Avoids the
122
+ // throwaway array of formatted-link objects the previous `map` allocated and
123
+ // the second walk `JSON.stringify` does over them.
124
+ let serialized = '['
125
+ for (let i = 0; i < links.length; i++) {
126
+ if (i > 0) serialized += ','
127
+ const { context, attributes } = links[i]
128
+ serialized += `{"trace_id":"${context.toTraceId(true)}","span_id":"${context.toSpanId(true)}"`
129
+ if (attributes !== undefined) {
130
+ serialized += `,"attributes":${JSON.stringify(attributes)}`
122
131
  }
123
-
124
- if (attributes && Object.keys(attributes).length > 0) {
125
- formattedLink.attributes = attributes
132
+ if (context?._sampling?.priority >= 0) {
133
+ serialized += `,"flags":${context._sampling.priority > 0 ? 1 : 0}`
126
134
  }
127
- if (context?._sampling?.priority >= 0) formattedLink.flags = context._sampling.priority > 0 ? 1 : 0
128
- if (context?._tracestate) formattedLink.tracestate = context._tracestate.toString()
129
-
130
- return formattedLink
131
- })
132
- let serialized = JSON.stringify(links)
135
+ if (context?._tracestate) {
136
+ serialized += `,"tracestate":${JSON.stringify(context._tracestate.toString())}`
137
+ }
138
+ serialized += '}'
139
+ }
140
+ serialized += ']'
133
141
  if (serialized.length > MAX_META_VALUE_LENGTH) {
134
142
  serialized = `${serialized.slice(0, MAX_META_VALUE_LENGTH)}...`
135
143
  }
@@ -137,6 +145,12 @@ function extractSpanLinks (formattedSpan, span) {
137
145
  }
138
146
 
139
147
  /**
148
+ * Hand the raw `_events` array to the encoder layer instead of copying it into
149
+ * reshaped `{ name, time_unix_nano, attributes }` objects. Each encoder derives
150
+ * `time_unix_nano` from `event.startTime` via `eventTimeNano` and drops empty
151
+ * attribute objects itself, so the per-event allocation here is pure waste on
152
+ * every event-bearing span.
153
+ *
140
154
  * @param {FormattedSpan} formattedSpan
141
155
  * @param {import('./opentracing/span')} span
142
156
  */
@@ -144,13 +158,7 @@ function extractSpanEvents (formattedSpan, span) {
144
158
  if (!span._events?.length) {
145
159
  return
146
160
  }
147
- formattedSpan.span_events = span._events.map(event => {
148
- return {
149
- name: event.name,
150
- time_unix_nano: Math.round(event.startTime * 1e6),
151
- attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined,
152
- }
153
- })
161
+ formattedSpan.span_events = span._events
154
162
  }
155
163
 
156
164
  function extractTags (formattedSpan, span) {
@@ -168,7 +176,7 @@ function extractTags (formattedSpan, span) {
168
176
  metrics[MEASURED] = 1
169
177
  }
170
178
 
171
- const tracerService = span.tracer()._service.toLowerCase()
179
+ const tracerService = span.tracer().serviceLower
172
180
  if (tags['service.name']?.toLowerCase() !== tracerService) {
173
181
  span.setTag(BASE_SERVICE, tracerService)
174
182