dd-trace 5.96.0 → 5.98.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 (173) hide show
  1. package/index.d.ts +60 -2
  2. package/package.json +9 -7
  3. package/packages/datadog-esbuild/index.js +20 -9
  4. package/packages/datadog-instrumentations/src/child_process.js +7 -17
  5. package/packages/datadog-instrumentations/src/crypto.js +1 -2
  6. package/packages/datadog-instrumentations/src/cucumber.js +69 -4
  7. package/packages/datadog-instrumentations/src/cypress-config.js +318 -0
  8. package/packages/datadog-instrumentations/src/cypress.js +86 -4
  9. package/packages/datadog-instrumentations/src/dns.js +1 -2
  10. package/packages/datadog-instrumentations/src/express.js +4 -4
  11. package/packages/datadog-instrumentations/src/fs.js +27 -29
  12. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  13. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +41 -13
  14. package/packages/datadog-instrumentations/src/helpers/hook.js +31 -6
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +12 -19
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -13
  17. package/packages/datadog-instrumentations/src/helpers/register.js +103 -142
  18. package/packages/datadog-instrumentations/src/http/client.js +2 -3
  19. package/packages/datadog-instrumentations/src/http/server.js +2 -5
  20. package/packages/datadog-instrumentations/src/http2/client.js +1 -3
  21. package/packages/datadog-instrumentations/src/http2/server.js +1 -3
  22. package/packages/datadog-instrumentations/src/jest.js +117 -16
  23. package/packages/datadog-instrumentations/src/limitd-client.js +1 -1
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +12 -1
  25. package/packages/datadog-instrumentations/src/net.js +2 -8
  26. package/packages/datadog-instrumentations/src/pino.js +1 -1
  27. package/packages/datadog-instrumentations/src/playwright.js +4 -1
  28. package/packages/datadog-instrumentations/src/prisma.js +1 -2
  29. package/packages/datadog-instrumentations/src/redis.js +12 -6
  30. package/packages/datadog-instrumentations/src/selenium.js +4 -1
  31. package/packages/datadog-instrumentations/src/sequelize.js +1 -1
  32. package/packages/datadog-instrumentations/src/url.js +1 -3
  33. package/packages/datadog-instrumentations/src/vitest.js +5 -1
  34. package/packages/datadog-instrumentations/src/vm.js +1 -3
  35. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -4
  36. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -0
  37. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -0
  38. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -0
  39. package/packages/datadog-plugin-aws-sdk/src/services/redshift.js +1 -0
  40. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +1 -0
  41. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -0
  42. package/packages/datadog-plugin-cucumber/src/index.js +13 -3
  43. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +166 -6
  44. package/packages/datadog-plugin-cypress/src/index.js +59 -2
  45. package/packages/datadog-plugin-fs/src/index.js +1 -1
  46. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -1
  47. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -7
  48. package/packages/datadog-plugin-graphql/src/resolve.js +1 -1
  49. package/packages/datadog-plugin-http/src/client.js +1 -1
  50. package/packages/datadog-plugin-http/src/server.js +10 -2
  51. package/packages/datadog-plugin-http2/src/client.js +1 -1
  52. package/packages/datadog-plugin-http2/src/server.js +10 -2
  53. package/packages/datadog-plugin-jest/src/index.js +4 -2
  54. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +31 -4
  55. package/packages/datadog-plugin-mocha/src/index.js +5 -2
  56. package/packages/datadog-plugin-mongodb-core/src/index.js +3 -3
  57. package/packages/datadog-plugin-mysql/src/index.js +1 -1
  58. package/packages/datadog-plugin-next/src/index.js +10 -16
  59. package/packages/datadog-plugin-openai/src/services.js +1 -0
  60. package/packages/datadog-plugin-pg/src/index.js +1 -1
  61. package/packages/datadog-plugin-tedious/src/index.js +1 -1
  62. package/packages/datadog-plugin-ws/src/close.js +1 -1
  63. package/packages/datadog-plugin-ws/src/receiver.js +1 -1
  64. package/packages/datadog-webpack/index.js +3 -3
  65. package/packages/dd-trace/index.js +12 -10
  66. package/packages/dd-trace/src/agent/url.js +2 -2
  67. package/packages/dd-trace/src/aiguard/sdk.js +26 -22
  68. package/packages/dd-trace/src/appsec/blocked_templates.js +4 -3
  69. package/packages/dd-trace/src/appsec/blocking.js +64 -33
  70. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  71. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +1 -1
  72. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  73. package/packages/dd-trace/src/appsec/remote_config.js +1 -0
  74. package/packages/dd-trace/src/appsec/sdk/index.js +4 -0
  75. package/packages/dd-trace/src/appsec/sdk/set_user.js +1 -1
  76. package/packages/dd-trace/src/appsec/sdk/track_event.js +5 -5
  77. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +2 -2
  78. package/packages/dd-trace/src/appsec/sdk/utils.js +4 -2
  79. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +6 -1
  80. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  81. package/packages/dd-trace/src/config/defaults.js +315 -146
  82. package/packages/dd-trace/src/config/generated-config-types.d.ts +9 -1
  83. package/packages/dd-trace/src/config/helper.js +59 -10
  84. package/packages/dd-trace/src/config/index.js +587 -1496
  85. package/packages/dd-trace/src/config/parsers.js +256 -0
  86. package/packages/dd-trace/src/config/remote_config.js +59 -2
  87. package/packages/dd-trace/src/config/supported-configurations.json +406 -432
  88. package/packages/dd-trace/src/constants.js +1 -0
  89. package/packages/dd-trace/src/crashtracking/crashtracker.js +7 -1
  90. package/packages/dd-trace/src/crashtracking/index.js +1 -7
  91. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +5 -2
  92. package/packages/dd-trace/src/debugger/index.js +1 -1
  93. package/packages/dd-trace/src/dogstatsd.js +12 -9
  94. package/packages/dd-trace/src/encode/0.4.js +8 -7
  95. package/packages/dd-trace/src/encode/span-stats.js +4 -1
  96. package/packages/dd-trace/src/exporters/agent/writer.js +7 -1
  97. package/packages/dd-trace/src/exporters/common/request.js +9 -0
  98. package/packages/dd-trace/src/exporters/common/writer.js +12 -2
  99. package/packages/dd-trace/src/heap_snapshots.js +3 -0
  100. package/packages/dd-trace/src/index.js +5 -2
  101. package/packages/dd-trace/src/lambda/runtime/ritm.js +6 -6
  102. package/packages/dd-trace/src/llmobs/index.js +4 -1
  103. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -1
  104. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +60 -12
  105. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +4 -2
  106. package/packages/dd-trace/src/llmobs/sdk.js +12 -8
  107. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  108. package/packages/dd-trace/src/llmobs/tagger.js +9 -6
  109. package/packages/dd-trace/src/llmobs/writers/base.js +2 -0
  110. package/packages/dd-trace/src/llmobs/writers/util.js +3 -0
  111. package/packages/dd-trace/src/log/index.js +20 -59
  112. package/packages/dd-trace/src/log/writer.js +7 -19
  113. package/packages/dd-trace/src/noop/proxy.js +8 -0
  114. package/packages/dd-trace/src/openfeature/remote_config.js +6 -1
  115. package/packages/dd-trace/src/opentelemetry/context_manager.js +6 -4
  116. package/packages/dd-trace/src/opentelemetry/logs/index.js +1 -1
  117. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  118. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +17 -2
  119. package/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js +14 -2
  120. package/packages/dd-trace/src/opentelemetry/otlp/trace.proto +358 -0
  121. package/packages/dd-trace/src/opentelemetry/otlp/trace_service.proto +78 -0
  122. package/packages/dd-trace/src/opentelemetry/trace/index.js +75 -0
  123. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +66 -0
  124. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +332 -0
  125. package/packages/dd-trace/src/opentracing/propagation/text_map.js +9 -4
  126. package/packages/dd-trace/src/opentracing/tracer.js +9 -4
  127. package/packages/dd-trace/src/payload-tagging/config/index.js +6 -5
  128. package/packages/dd-trace/src/plugin_manager.js +8 -6
  129. package/packages/dd-trace/src/plugins/ci_plugin.js +4 -0
  130. package/packages/dd-trace/src/plugins/log_plugin.js +3 -0
  131. package/packages/dd-trace/src/plugins/plugin.js +11 -13
  132. package/packages/dd-trace/src/plugins/storage.js +2 -2
  133. package/packages/dd-trace/src/plugins/tracing.js +22 -5
  134. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  135. package/packages/dd-trace/src/plugins/util/web.js +6 -88
  136. package/packages/dd-trace/src/process-tags/index.js +3 -0
  137. package/packages/dd-trace/src/profiler.js +27 -2
  138. package/packages/dd-trace/src/profiling/config.js +73 -241
  139. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -4
  140. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +6 -2
  141. package/packages/dd-trace/src/profiling/profiler.js +78 -109
  142. package/packages/dd-trace/src/profiling/profilers/events.js +2 -3
  143. package/packages/dd-trace/src/profiling/profilers/wall.js +89 -6
  144. package/packages/dd-trace/src/profiling/ssi-heuristics.js +4 -1
  145. package/packages/dd-trace/src/propagation-hash/index.js +2 -1
  146. package/packages/dd-trace/src/proxy.js +40 -6
  147. package/packages/dd-trace/src/remote_config/index.js +3 -0
  148. package/packages/dd-trace/src/require-package-json.js +8 -4
  149. package/packages/dd-trace/src/ritm.js +58 -26
  150. package/packages/dd-trace/src/runtime_metrics/index.js +3 -0
  151. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +18 -11
  152. package/packages/dd-trace/src/sampler.js +1 -1
  153. package/packages/dd-trace/src/service-naming/index.js +1 -1
  154. package/packages/dd-trace/src/service-naming/schemas/definition.js +4 -1
  155. package/packages/dd-trace/src/service-naming/schemas/util.js +15 -1
  156. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +24 -1
  157. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +60 -0
  158. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +17 -1
  159. package/packages/dd-trace/src/service-naming/schemas/v0/websocket.js +5 -0
  160. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +17 -0
  161. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +11 -1
  162. package/packages/dd-trace/src/service-naming/schemas/v1/websocket.js +6 -0
  163. package/packages/dd-trace/src/span_stats.js +5 -1
  164. package/packages/dd-trace/src/standalone/index.js +3 -0
  165. package/packages/dd-trace/src/telemetry/index.js +2 -3
  166. package/packages/dd-trace/src/telemetry/send-data.js +5 -19
  167. package/packages/dd-trace/src/telemetry/session-propagation.js +19 -44
  168. package/packages/dd-trace/src/telemetry/telemetry.js +28 -171
  169. package/packages/dd-trace/src/tracer.js +2 -2
  170. package/packages/dd-trace/src/util.js +0 -9
  171. package/vendor/dist/@apm-js-collab/code-transformer/index.js +28 -6
  172. package/vendor/dist/protobufjs/index.js +1 -1
  173. package/packages/dd-trace/src/log/utils.js +0 -16
@@ -14,7 +14,7 @@ class EventSerializer {
14
14
  this._host = host
15
15
  this._service = service
16
16
  this._appVersion = version
17
- this._libraryInjected = !!libraryInjected
17
+ this._libraryInjected = libraryInjected
18
18
  this._activation = activation || 'unknown'
19
19
  }
20
20
 
@@ -22,7 +22,7 @@ class EventSerializer {
22
22
  return `${type}.pprof`
23
23
  }
24
24
 
25
- getEventJSON ({ profiles, infos, start, end, tags = {}, endpointCounts }) {
25
+ getEventJSON ({ profiles, infos, start, end, tags = {}, endpointCounts, customAttributes }) {
26
26
  const event = {
27
27
  attachments: Object.keys(profiles).map(t => this.typeToFile(t)),
28
28
  start: start.toISOString(),
@@ -80,6 +80,10 @@ class EventSerializer {
80
80
  },
81
81
  }
82
82
 
83
+ if (customAttributes) {
84
+ event.custom_attributes = customAttributes
85
+ }
86
+
83
87
  if (processTags.serialized) {
84
88
  event[processTags.PROFILING_FIELD_NAME] = processTags.serialized
85
89
  }
@@ -12,12 +12,6 @@ const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('./we
12
12
  const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted')
13
13
  const spanFinishedChannel = dc.channel('dd-trace:span:finish')
14
14
 
15
- function logError (logger, ...args) {
16
- if (logger) {
17
- logger.error(...args)
18
- }
19
- }
20
-
21
15
  function findWebSpan (startedSpans, spanId) {
22
16
  for (let i = startedSpans.length; --i >= 0;) {
23
17
  const ispan = startedSpans[i]
@@ -51,6 +45,7 @@ class Profiler extends EventEmitter {
51
45
  #compressionFnInitialized = false
52
46
  #compressionOptions
53
47
  #config
48
+ #customLabelKeys = new Set()
54
49
  #enabled = false
55
50
  #endpointCounts = new Map()
56
51
  #lastStart
@@ -70,73 +65,47 @@ class Profiler extends EventEmitter {
70
65
  return this.#config?.flushInterval
71
66
  }
72
67
 
73
- start (config) {
74
- const {
75
- service,
76
- version,
77
- env,
78
- url,
79
- hostname,
80
- port,
81
- tags,
82
- repositoryUrl,
83
- commitSHA,
84
- injectionEnabled,
85
- reportHostname,
86
- } = config
87
- const { enabled, sourceMap, exporters } = config.profiling
88
- const { heartbeatInterval } = config.telemetry
89
-
90
- // TODO: Unify with main logger and rewrite template strings to use printf formatting.
91
- const logger = {
92
- debug (message) { log.debug(message) },
93
- info (message) { log.info(message) },
94
- warn (message) { log.warn(message) },
95
- error (...args) { log.error(...args) },
96
- }
68
+ get enabled () {
69
+ return this.#enabled
70
+ }
97
71
 
98
- const libraryInjected = injectionEnabled.length > 0
99
- let activation
100
- if (enabled === 'auto') {
101
- activation = 'auto'
102
- } else if (enabled === 'true') {
103
- activation = 'manual'
104
- } // else activation = undefined
105
-
106
- const options = {
107
- service,
108
- version,
109
- env,
110
- logger,
111
- sourceMap,
112
- exporters,
113
- url,
114
- hostname,
115
- port,
116
- tags,
117
- repositoryUrl,
118
- commitSHA,
119
- libraryInjected,
120
- activation,
121
- heartbeatInterval,
122
- reportHostname,
72
+ /**
73
+ * Declares the set of custom label keys that will be used with
74
+ * {@link runWithLabels}. This is used for profile upload metadata and
75
+ * for pprof serialization optimization (low-cardinality deduplication).
76
+ *
77
+ * @param {Iterable<string>} keys - Custom label key names
78
+ */
79
+ setCustomLabelKeys (keys) {
80
+ this.#customLabelKeys.clear()
81
+ for (const key of keys) {
82
+ this.#customLabelKeys.add(key)
123
83
  }
124
-
125
- try {
126
- return this._start(options)
127
- } catch (err) {
128
- logError(logger, 'Error starting profiler. For troubleshooting tips, see ' +
129
- '<https://dtdg.co/nodejs-profiler-troubleshooting>', err)
130
- return false
84
+ if (this.#config) {
85
+ for (const profiler of this.#config.profilers) {
86
+ profiler.setCustomLabelKeys?.(this.#customLabelKeys)
87
+ }
131
88
  }
132
89
  }
133
90
 
134
- get enabled () {
135
- return this.#enabled
136
- }
137
-
138
- #logError (err) {
139
- logError(this.#logger, err)
91
+ /**
92
+ * Runs a function with custom profiling labels attached to wall profiler samples.
93
+ *
94
+ * @param {Record<string, string | number>} labels - Custom labels to attach
95
+ * @param {function(): T} fn - Function to execute with the labels
96
+ * @returns {T} The return value of fn
97
+ * @template T
98
+ */
99
+ runWithLabels (labels, fn) {
100
+ if (!this.#enabled || !this.#config) {
101
+ return fn()
102
+ }
103
+ for (const profiler of this.#config.profilers) {
104
+ if (profiler.runWithLabels) {
105
+ return profiler.runWithLabels(labels, fn)
106
+ }
107
+ }
108
+ return fn()
140
109
  }
141
110
 
142
111
  #getCompressionFn () {
@@ -175,48 +144,46 @@ class Profiler extends EventEmitter {
175
144
  }
176
145
  break
177
146
  }
178
- } catch (err) {
179
- this.#logError(err)
147
+ } catch (error) {
148
+ log.error(error)
180
149
  }
181
150
  }
182
151
  return this.#compressionFn
183
152
  }
184
153
 
185
- _start (options) {
154
+ /**
155
+ * @param {import('../config/config-base')} options - Tracer configuration
156
+ */
157
+ start (options) {
186
158
  if (this.enabled) return true
159
+ this.#enabled = true
187
160
 
188
161
  const config = this.#config = new Config(options)
189
-
190
162
  this.#logger = config.logger
191
- this.#enabled = true
192
- this._setInterval()
193
163
 
164
+ this._setInterval()
194
165
  // Log errors if the source map finder fails, but don't prevent the rest
195
166
  // of the profiler from running without source maps.
196
167
  let mapper
197
- try {
198
- const { setLogger, SourceMapper } = require('@datadog/pprof')
199
- setLogger(config.logger)
200
-
201
- if (config.sourceMap) {
202
- mapper = new SourceMapper(config.debugSourceMaps)
203
- mapper.loadDirectory(process.cwd())
204
- .then(() => {
205
- if (config.debugSourceMaps) {
206
- const count = mapper.infoMap.size
207
- this.#logger.debug(() => {
208
- return count === 0
209
- ? 'Found no source maps'
210
- : `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
211
- })
212
- }
213
- })
214
- .catch((err) => {
215
- this.#logError(err)
216
- })
217
- }
218
- } catch (err) {
219
- this.#logError(err)
168
+ const { setLogger, SourceMapper } = require('@datadog/pprof')
169
+ setLogger(config.logger)
170
+
171
+ if (config.sourceMap) {
172
+ mapper = new SourceMapper(config.debugSourceMaps)
173
+ mapper.loadDirectory(process.cwd())
174
+ .then(() => {
175
+ if (config.debugSourceMaps) {
176
+ const count = mapper.infoMap.size
177
+ this.#logger.debug(() => {
178
+ return count === 0
179
+ ? 'Found no source maps'
180
+ : `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
181
+ })
182
+ }
183
+ })
184
+ .catch((error) => {
185
+ log.error(error)
186
+ })
220
187
  }
221
188
 
222
189
  try {
@@ -237,12 +204,13 @@ class Profiler extends EventEmitter {
237
204
  }
238
205
 
239
206
  this._capture(this._timeoutInterval, start)
240
- return true
241
- } catch (e) {
242
- this.#logError(e)
207
+ } catch (error) {
208
+ log.error(error)
243
209
  this.#stop()
244
210
  return false
245
211
  }
212
+
213
+ return true
246
214
  }
247
215
 
248
216
  #nearOOMExport (profileType, encodedProfile, info) {
@@ -380,10 +348,10 @@ class Profiler extends EventEmitter {
380
348
  return `Collected ${profiler.type} profile: ` + profileJson
381
349
  })
382
350
  hasEncoded = true
383
- } catch (err) {
351
+ } catch (error) {
384
352
  // If encoding one of the profile types fails, we should still try to
385
353
  // encode and submit the other profile types.
386
- this.#logError(err)
354
+ log.error(error)
387
355
  }
388
356
  }))
389
357
 
@@ -392,8 +360,8 @@ class Profiler extends EventEmitter {
392
360
  profileSubmittedChannel.publish()
393
361
  this.#logger.debug('Submitted profiles')
394
362
  }
395
- } catch (err) {
396
- this.#logError(err)
363
+ } catch (error) {
364
+ log.error(error)
397
365
  this.#stop()
398
366
  }
399
367
  }
@@ -410,12 +378,13 @@ class Profiler extends EventEmitter {
410
378
 
411
379
  tags.snapshot = snapshotKind
412
380
  tags.profile_seq = this.#profileSeq++
413
- const exportSpec = { profiles, infos, start, end, tags, endpointCounts }
381
+ const customAttributes = this.#customLabelKeys.size > 0
382
+ ? [...this.#customLabelKeys]
383
+ : undefined
384
+ const exportSpec = { profiles, infos, start, end, tags, endpointCounts, customAttributes }
414
385
  const tasks = this.#config.exporters.map(exporter =>
415
- exporter.export(exportSpec).catch(err => {
416
- if (this.#logger) {
417
- this.#logger.warn(err)
418
- }
386
+ exporter.export(exportSpec).catch(error => {
387
+ log.warn(error)
419
388
  })
420
389
  )
421
390
 
@@ -51,8 +51,7 @@ function labelFromStrStr (stringTable, keyStr, valStr) {
51
51
  }
52
52
 
53
53
  function getMaxSamples (options) {
54
- const flushInterval = options.flushInterval || 65 * 1e3 // 65 seconds
55
- const maxCpuSamples = flushInterval / options.samplingInterval
54
+ const maxCpuSamples = options.flushInterval / options.samplingInterval
56
55
 
57
56
  // The lesser of max parallelism and libuv thread pool size, plus one so we can detect
58
57
  // oversubscription on libuv thread pool, plus another one for GC.
@@ -403,7 +402,7 @@ class EventsProfiler {
403
402
 
404
403
  get type () { return 'events' }
405
404
 
406
- constructor (options = {}) {
405
+ constructor (options) {
407
406
  this.#maxSamples = getMaxSamples(options)
408
407
  this.#timelineSamplingEnabled = !!options.timelineSamplingEnabled
409
408
  this.#eventSerializer = new EventSerializer(this.#maxSamples)
@@ -108,6 +108,8 @@ class NativeWallProfiler {
108
108
  #captureSpanData = false
109
109
  #codeHotspotsEnabled = false
110
110
  #cpuProfilingEnabled = false
111
+ #customLabelsActive = false
112
+ #customLabelKeys
111
113
  #endpointCollectionEnabled = false
112
114
  #flushIntervalMillis = 0
113
115
  #logger
@@ -245,7 +247,24 @@ class NativeWallProfiler {
245
247
  // context -- we simply can't tell which one it might've been across all
246
248
  // possible async context frames.
247
249
  if (this.#asyncContextFrameEnabled) {
248
- this.#pprof.time.setContext(sampleContext)
250
+ if (this.#customLabelsActive) {
251
+ // Custom labels may be active in this async context. The current CPED
252
+ // context could be a 2-element array [profilingContext, customLabels].
253
+ // Replace the profiling context while preserving the custom labels.
254
+ // This flag is monotonic (once set, stays true) because async
255
+ // continuations from runWithLabels can fire at any time after the
256
+ // synchronous runWithLabels call has returned.
257
+ const current = this.#pprof.time.getContext()
258
+ if (Array.isArray(current)) {
259
+ if (current[0] !== sampleContext) {
260
+ this.#pprof.time.setContext([sampleContext, current[1]])
261
+ }
262
+ } else if (current !== sampleContext) {
263
+ this.#pprof.time.setContext(sampleContext)
264
+ }
265
+ } else {
266
+ this.#pprof.time.setContext(sampleContext)
267
+ }
249
268
  } else {
250
269
  const sampleCount = this._profilerState[kSampleCount]
251
270
  if (sampleCount !== this._lastSampleCount) {
@@ -344,6 +363,13 @@ class NativeWallProfiler {
344
363
  const lowCardinalityLabels = Object.keys(getThreadLabels())
345
364
  lowCardinalityLabels.push(TRACE_ENDPOINT_LABEL)
346
365
 
366
+ // Custom labels are expected to be low-cardinality (e.g. customer tier, region)
367
+ if (this.#customLabelKeys) {
368
+ for (const key of this.#customLabelKeys) {
369
+ lowCardinalityLabels.push(key)
370
+ }
371
+ }
372
+
347
373
  const profile = this.#pprof.time.stop(restart, this.#boundGenerateLabels, lowCardinalityLabels)
348
374
 
349
375
  if (restart) {
@@ -383,7 +409,29 @@ class NativeWallProfiler {
383
409
  return getThreadLabels()
384
410
  }
385
411
 
386
- const labels = { ...getThreadLabels() }
412
+ // Native profiler doesn't set context.context for some samples, such as idle samples or when
413
+ // the context was otherwise unavailable when the sample was taken. Note that with ACF, we don't
414
+ // use the "ref" indirection.
415
+ let ref
416
+ let customLabels
417
+ const cctx = context.context
418
+ if (this.#asyncContextFrameEnabled) {
419
+ // When custom labels are active with ACF, context.context is a 2-element array:
420
+ // [profilingContext, customLabels]. Otherwise it's a plain object.
421
+ if (Array.isArray(cctx)) {
422
+ [ref, customLabels] = cctx
423
+ } else {
424
+ ref = cctx
425
+ }
426
+ } else {
427
+ ref = cctx?.ref
428
+ }
429
+
430
+ // Custom labels are spread first so that internal labels always take
431
+ // precedence and overwrite them.
432
+ const labels = customLabels === undefined
433
+ ? { ...getThreadLabels() }
434
+ : { ...customLabels, ...getThreadLabels() }
387
435
 
388
436
  if (this.#timelineEnabled) {
389
437
  // Incoming timestamps are in microseconds, we emit nanos.
@@ -395,10 +443,6 @@ class NativeWallProfiler {
395
443
  labels['async id'] = asyncId
396
444
  }
397
445
 
398
- // Native profiler doesn't set context.context for some samples, such as idle samples or when
399
- // the context was otherwise unavailable when the sample was taken. Note that with async context
400
- // frame, we don't use the "ref" indirection.
401
- const ref = this.#asyncContextFrameEnabled ? context.context : context.context?.ref
402
446
  if (typeof ref !== 'object') {
403
447
  return labels
404
448
  }
@@ -421,6 +465,45 @@ class NativeWallProfiler {
421
465
  return labels
422
466
  }
423
467
 
468
+ /**
469
+ * Sets the custom label keys used for pprof low-cardinality deduplication.
470
+ * Called once by the top-level Profiler when keys are declared.
471
+ *
472
+ * @param {Iterable<string>} keys
473
+ */
474
+ setCustomLabelKeys (keys) {
475
+ this.#customLabelKeys = keys
476
+ }
477
+
478
+ /**
479
+ * Runs a function with custom profiling labels attached to all wall profiler
480
+ * samples taken during its execution. Labels are key-value pairs that appear
481
+ * in the pprof output and can be used to filter flame graphs in the Datadog UI.
482
+ *
483
+ * Requires AsyncContextFrame (ACF) to be enabled. Supports nesting: inner
484
+ * calls merge labels with outer calls, with inner values taking precedence.
485
+ *
486
+ * @param {Record<string, string | number>} labels - Custom labels to attach
487
+ * @param {function(): T} fn - Function to execute with the labels
488
+ * @returns {T} The return value of fn
489
+ * @template T
490
+ */
491
+ runWithLabels (labels, fn) {
492
+ if (!this.#asyncContextFrameEnabled || !this.#withContexts) {
493
+ return fn()
494
+ }
495
+
496
+ // Read current context; merge custom labels if already in a runWithLabels scope
497
+ const current = this.#pprof.time.getContext()
498
+ const isCurrentArray = Array.isArray(current)
499
+ const customLabels = isCurrentArray ? { ...current[1], ...labels } : labels
500
+
501
+ const profilingContext = (isCurrentArray ? current[0] : current) ?? {}
502
+
503
+ this.#customLabelsActive = true
504
+ return this.#pprof.time.runWithContext([profilingContext, customLabels], fn)
505
+ }
506
+
424
507
  profile (restart) {
425
508
  return this.#stop(restart)
426
509
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const dc = require('dc-polyfill')
3
+ const dc = /** @type {typeof import('diagnostics_channel')} */ (require('dc-polyfill'))
4
4
  const log = require('../log')
5
5
 
6
6
  // If the process lives for at least 30 seconds, it's considered long-lived
@@ -10,6 +10,9 @@ const DEFAULT_LONG_LIVED_THRESHOLD = 30_000
10
10
  * This class embodies the SSI profiler-triggering heuristics under SSI.
11
11
  */
12
12
  class SSIHeuristics {
13
+ /**
14
+ * @param {import('../config/config-base')} config - Tracer configuration
15
+ */
13
16
  constructor (config) {
14
17
  const longLivedThreshold = config.profiling.longLivedThreshold || DEFAULT_LONG_LIVED_THRESHOLD
15
18
  if (typeof longLivedThreshold !== 'number' || longLivedThreshold <= 0) {
@@ -17,11 +17,12 @@ class PropagationHashManager {
17
17
  _cachedHash = null
18
18
  _cachedHashString = null
19
19
  _cachedHashBase64 = null
20
+ /** @type {import('../config/config-base') | null} */
20
21
  _config = null
21
22
 
22
23
  /**
23
24
  * Configure the propagation hash manager with tracer config
24
- * @param {object} config - Tracer configuration
25
+ * @param {import('../config/config-base')} config - Tracer configuration
25
26
  */
26
27
  configure (config) {
27
28
  this._config = config
@@ -27,9 +27,12 @@ class LazyModule {
27
27
  this.provider = provider
28
28
  }
29
29
 
30
- enable (...args) {
30
+ /**
31
+ * @param {import('./config/config-base')} config - Tracer configuration
32
+ */
33
+ enable (config, ...args) {
31
34
  this.module = this.provider()
32
- this.module.enable(...args)
35
+ this.module.enable(config, ...args)
33
36
  }
34
37
 
35
38
  disable () {
@@ -238,25 +241,32 @@ class Tracer extends NoopProxy {
238
241
  getDynamicInstrumentationClient(config)
239
242
  }
240
243
  } catch (e) {
241
- log.error('Error initialising tracer', e)
244
+ log.error('Error initializing tracer', e)
245
+ // TODO: Should we stop everything started so far?
242
246
  }
243
247
 
244
248
  return this
245
249
  }
246
250
 
251
+ /**
252
+ * @param {import('./config/config-base')} config - Tracer configuration
253
+ */
247
254
  _startProfiler (config) {
248
255
  // do not stop tracer initialization if the profiler fails to be imported
249
256
  try {
250
257
  return require('./profiler').start(config)
251
- } catch (e) {
258
+ } catch (error) {
252
259
  log.error(
253
260
  'Error starting profiler. For troubleshooting tips, see <https://dtdg.co/nodejs-profiler-troubleshooting>',
254
- e
261
+ error
255
262
  )
256
263
  return false
257
264
  }
258
265
  }
259
266
 
267
+ /**
268
+ * @param {import('./config/config-base')} config - Tracer configuration
269
+ */
260
270
  #updateTracing (config) {
261
271
  if (config.tracing !== false) {
262
272
  if (config.appsec.enabled) {
@@ -269,7 +279,12 @@ class Tracer extends NoopProxy {
269
279
  const prioritySampler = config.apmTracingEnabled === false
270
280
  ? require('./standalone').configure(config)
271
281
  : undefined
272
- this._tracer = new DatadogTracer(config, prioritySampler)
282
+ let otlpExporter
283
+ if (config.otelTracesEnabled) {
284
+ const { buildResourceAttributes, createOtlpTraceExporter } = require('./opentelemetry/trace')
285
+ otlpExporter = createOtlpTraceExporter(config, buildResourceAttributes(config))
286
+ }
287
+ this._tracer = new DatadogTracer(config, prioritySampler, otlpExporter)
273
288
  this.dataStreamsCheckpointer = this._tracer.dataStreamsCheckpointer
274
289
  lazyProxy(this, 'appsec', () => require('./appsec/sdk'), this._tracer, config)
275
290
  lazyProxy(this, 'llmobs', () => require('./llmobs/sdk'), this._tracer, this._modules.llmobs, config)
@@ -330,6 +345,25 @@ class Tracer extends NoopProxy {
330
345
  }
331
346
  }
332
347
 
348
+ /**
349
+ * @override
350
+ */
351
+ get profiling () {
352
+ // Lazily require the profiler module and cache the result. If profiling
353
+ // is not enabled, runWithLabels still works as a passthrough (just calls fn()).
354
+ const profilerModule = require('./profiler')
355
+ const profiling = {
356
+ setCustomLabelKeys (keys) {
357
+ profilerModule.setCustomLabelKeys(keys)
358
+ },
359
+ runWithLabels (labels, fn) {
360
+ return profilerModule.runWithLabels(labels, fn)
361
+ },
362
+ }
363
+ Reflect.defineProperty(this, 'profiling', { value: profiling, configurable: true, enumerable: true })
364
+ return profiling
365
+ }
366
+
333
367
  /**
334
368
  * @override
335
369
  */
@@ -25,6 +25,9 @@ class RemoteConfig {
25
25
  #products = new Set()
26
26
  #batchHandlers = new Map()
27
27
 
28
+ /**
29
+ * @param {import('../config/config-base')} config - Tracer configuration
30
+ */
28
31
  constructor (config) {
29
32
  const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000)
30
33
 
@@ -21,10 +21,14 @@ function requirePackageJson (name, module) {
21
21
  }
22
22
  for (const modulePath of module.paths) {
23
23
  const candidate = path.join(modulePath, name, 'package.json')
24
- try {
25
- return JSON.parse(fs.readFileSync(candidate, 'utf8'))
26
- } catch {
27
- continue
24
+ // fs.existsSync is faster than fs.readFileSync due to not throwing an error if the file does not exist.
25
+ // The race condition should also not matter here as the time window is very small.
26
+ if (fs.existsSync(candidate)) {
27
+ try {
28
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'))
29
+ } catch {
30
+ continue
31
+ }
28
32
  }
29
33
  }
30
34
  throw new Error(`could not find ${name}/package.json`)