dd-trace 5.106.0 → 5.108.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 (105) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +9 -11
  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/body-parser.js +5 -2
  7. package/packages/datadog-instrumentations/src/connect.js +3 -2
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +7 -0
  10. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  11. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  12. package/packages/datadog-instrumentations/src/express.js +24 -20
  13. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  14. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  15. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  16. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  17. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  18. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  19. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  20. package/packages/datadog-instrumentations/src/jest.js +143 -73
  21. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  23. package/packages/datadog-instrumentations/src/multer.js +3 -2
  24. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  25. package/packages/datadog-instrumentations/src/net.js +8 -6
  26. package/packages/datadog-instrumentations/src/openai.js +19 -7
  27. package/packages/datadog-instrumentations/src/pg.js +19 -0
  28. package/packages/datadog-instrumentations/src/router.js +12 -10
  29. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  30. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  31. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  32. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  33. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  34. package/packages/datadog-plugin-http/src/client.js +0 -3
  35. package/packages/datadog-plugin-http/src/server.js +11 -1
  36. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  37. package/packages/datadog-plugin-pg/src/index.js +10 -0
  38. package/packages/dd-trace/src/aiguard/index.js +34 -15
  39. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  40. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +3 -2
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -1
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  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 +2 -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 +20 -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/llmobs/plugins/genai/index.js +4 -0
  67. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  68. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  69. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  70. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  71. package/packages/dd-trace/src/llmobs/util.js +54 -0
  72. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  73. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  74. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  75. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  76. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  77. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  78. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  79. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
  80. package/packages/dd-trace/src/opentracing/span.js +23 -18
  81. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  82. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  83. package/packages/dd-trace/src/priority_sampler.js +6 -5
  84. package/packages/dd-trace/src/profiling/config.js +3 -2
  85. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  86. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  87. package/packages/dd-trace/src/proxy.js +13 -10
  88. package/packages/dd-trace/src/remote_config/index.js +1 -2
  89. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  90. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  91. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  92. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  93. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  94. package/packages/dd-trace/src/span_format.js +33 -25
  95. package/packages/dd-trace/src/span_stats.js +1 -1
  96. package/packages/dd-trace/src/startup-log.js +1 -2
  97. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  98. package/packages/dd-trace/src/tracer.js +1 -1
  99. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  100. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  101. package/vendor/dist/protobufjs/index.js +1 -1
  102. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  103. package/vendor/dist/shell-quote/index.js +1 -1
  104. package/packages/dd-trace/src/agent/url.js +0 -28
  105. package/scripts/preinstall.js +0 -34
@@ -10,7 +10,6 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
10
10
  const { getIsAzureFunction } = require('../serverless')
11
11
  const { getAzureTagsFromMetadata, getAzureAppMetadata, getAzureFunctionMetadata } = require('../azure_metadata')
12
12
  const { getEnvironmentVariable } = require('../config/helper')
13
- const { getAgentUrl } = require('../agent/url')
14
13
  const { isACFActive } = require('../../../datadog-core/src/storage')
15
14
 
16
15
  const { AgentExporter } = require('./exporters/agent')
@@ -54,7 +53,7 @@ class Config {
54
53
  this.pprofPrefix = options.DD_PROFILING_PPROF_PREFIX
55
54
  this.v8ProfilerBugWorkaroundEnabled = options.DD_PROFILING_V8_PROFILER_BUG_WORKAROUND
56
55
 
57
- this.url = getAgentUrl(options)
56
+ this.url = options.url
58
57
 
59
58
  this.libraryInjected = !!options.DD_INJECTION_ENABLED
60
59
 
@@ -87,6 +86,7 @@ class Config {
87
86
 
88
87
  this.timelineEnabled = options.DD_PROFILING_TIMELINE_ENABLED
89
88
  this.timelineSamplingEnabled = options.DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED
89
+ this.allocationProfilingEnabled = options.DD_PROFILING_ALLOCATION_ENABLED
90
90
  this.codeHotspotsEnabled = options.DD_PROFILING_CODEHOTSPOTS_ENABLED
91
91
  this.cpuProfilingEnabled = options.DD_PROFILING_CPU_ENABLED
92
92
  this.heapSamplingInterval = options.DD_PROFILING_HEAP_SAMPLING_INTERVAL
@@ -140,6 +140,7 @@ class Config {
140
140
 
141
141
  get systemInfoReport () {
142
142
  const report = {
143
+ allocationProfilingEnabled: this.allocationProfilingEnabled,
143
144
  asyncContextFrameEnabled: this.asyncContextFrameEnabled,
144
145
  codeHotspotsEnabled: this.codeHotspotsEnabled,
145
146
  cpuProfilingEnabled: this.cpuProfilingEnabled,
@@ -66,12 +66,11 @@ class GCDecorator {
66
66
  constructor (stringTable) {
67
67
  this.stringTable = stringTable
68
68
  this.reasonLabelKey = stringTable.dedup('gc reason')
69
+ this.kindLabelKey = stringTable.dedup('gc type')
69
70
  this.kindLabels = []
70
71
  this.reasonLabels = []
71
72
  this.flagObj = {}
72
73
 
73
- const kindLabelKey = stringTable.dedup('gc type')
74
-
75
74
  // Create labels for all GC performance flags and kinds of GC
76
75
  for (const [key, value] of Object.entries(constants)) {
77
76
  if (key.startsWith('NODE_PERFORMANCE_GC_FLAGS_')) {
@@ -79,20 +78,43 @@ class GCDecorator {
79
78
  } else if (key.startsWith('NODE_PERFORMANCE_GC_')) {
80
79
  // It's a constant for a kind of GC
81
80
  const kind = key.slice(20).toLowerCase()
82
- this.kindLabels[value] = labelFromStr(stringTable, kindLabelKey, kind)
81
+ this.kindLabels[value] = labelFromStr(stringTable, this.kindLabelKey, kind)
83
82
  }
84
83
  }
84
+
85
+ // V8's young-generation collector emits GC events with kind 2, but Node.js
86
+ // doesn't expose a matching NODE_PERFORMANCE_GC_* constant for it, so we map it
87
+ // explicitly. The collector was renamed from Minor Mark-Compact to Minor
88
+ // Mark-Sweep in the V8 version that shipped with Node 22. See equivalent
89
+ // mapping in runtime_metrics.js.
90
+ const minorMarkGCKind = 2
91
+ if (this.kindLabels[minorMarkGCKind] === undefined) {
92
+ const { NODE_MAJOR } = require('../../../../../version')
93
+ const minorGCLabel = NODE_MAJOR >= 22 ? 'minor_mark_sweep' : 'minor_mark_compact'
94
+ this.kindLabels[minorMarkGCKind] = labelFromStr(stringTable, this.kindLabelKey, minorGCLabel)
95
+ }
85
96
  }
86
97
 
87
98
  decorateSample (sampleInput, item) {
88
99
  const { kind, flags } = item.detail
89
- sampleInput.label.push(this.kindLabels[kind])
100
+ sampleInput.label.push(this.getKindLabel(kind))
90
101
  const reasonLabel = this.getReasonLabel(flags)
91
102
  if (reasonLabel) {
92
103
  sampleInput.label.push(reasonLabel)
93
104
  }
94
105
  }
95
106
 
107
+ getKindLabel (kind) {
108
+ let kindLabel = this.kindLabels[kind]
109
+ if (kindLabel === undefined) {
110
+ // Gracefully handle GC kinds we don't have a label for (e.g. a value
111
+ // introduced by a future Node.js/V8 version).
112
+ kindLabel = labelFromStr(this.stringTable, this.kindLabelKey, `unknown_${kind}`)
113
+ this.kindLabels[kind] = kindLabel
114
+ }
115
+ return kindLabel
116
+ }
117
+
96
118
  getReasonLabel (flags) {
97
119
  if (flags === 0) {
98
120
  return null
@@ -13,12 +13,14 @@ class NativeSpaceProfiler {
13
13
  #mapper
14
14
  #oomMonitoring
15
15
  #pprof
16
+ #allocationProfilingEnabled = false
16
17
  #samplingInterval = 512 * 1024
17
18
  #started = false
18
19
 
19
20
  constructor (options = {}) {
20
21
  // TODO: Remove default value. It is only used in testing.
21
22
  this.#samplingInterval = options.heapSamplingInterval || 512 * 1024
23
+ this.#allocationProfilingEnabled = options.allocationProfilingEnabled
22
24
  this.#oomMonitoring = options.oomMonitoring || {}
23
25
  }
24
26
 
@@ -31,7 +33,7 @@ class NativeSpaceProfiler {
31
33
 
32
34
  this.#mapper = mapper
33
35
  this.#pprof = require('@datadog/pprof')
34
- this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH)
36
+ this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH, this.#allocationProfilingEnabled)
35
37
  if (this.#oomMonitoring.enabled) {
36
38
  const strategies = this.#oomMonitoring.exportStrategies
37
39
  this.#pprof.heap.monitorOutOfMemory(
@@ -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
 
@@ -190,7 +190,7 @@ class SpanStatsProcessor {
190
190
  if (!this.enabled) return
191
191
  if (!span.metrics[TOP_LEVEL_KEY] && !span.metrics[MEASURED]) return
192
192
 
193
- const spanEndNs = span.startTime + span.duration
193
+ const spanEndNs = span.start + span.duration
194
194
  const bucketTime = spanEndNs - (spanEndNs % this.bucketSizeNs)
195
195
 
196
196
  this.buckets.forTime(bucketTime)