dd-trace 3.42.0 → 3.43.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 (32) hide show
  1. package/index.d.ts +5 -0
  2. package/package.json +3 -3
  3. package/packages/datadog-instrumentations/src/apollo-server-core.js +41 -0
  4. package/packages/datadog-instrumentations/src/apollo-server.js +83 -0
  5. package/packages/datadog-instrumentations/src/graphql.js +18 -4
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  7. package/packages/datadog-instrumentations/src/http/client.js +2 -14
  8. package/packages/datadog-instrumentations/src/next.js +15 -5
  9. package/packages/datadog-instrumentations/src/rhea.js +15 -9
  10. package/packages/datadog-plugin-graphql/src/resolve.js +26 -18
  11. package/packages/datadog-plugin-http/src/client.js +1 -1
  12. package/packages/datadog-plugin-next/src/index.js +32 -6
  13. package/packages/dd-trace/src/appsec/activation.js +29 -0
  14. package/packages/dd-trace/src/appsec/addresses.js +1 -0
  15. package/packages/dd-trace/src/appsec/api_security_sampler.js +48 -0
  16. package/packages/dd-trace/src/appsec/blocked_templates.js +4 -1
  17. package/packages/dd-trace/src/appsec/blocking.js +95 -43
  18. package/packages/dd-trace/src/appsec/channels.js +4 -1
  19. package/packages/dd-trace/src/appsec/graphql.js +146 -0
  20. package/packages/dd-trace/src/appsec/index.js +29 -40
  21. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  22. package/packages/dd-trace/src/appsec/remote_config/index.js +36 -15
  23. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
  24. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +25 -13
  25. package/packages/dd-trace/src/config.js +5 -0
  26. package/packages/dd-trace/src/plugins/util/user-provided-git.js +3 -2
  27. package/packages/dd-trace/src/profiling/profiler.js +7 -6
  28. package/packages/dd-trace/src/profiling/profilers/events.js +17 -12
  29. package/packages/dd-trace/src/profiling/profilers/shared.js +33 -3
  30. package/packages/dd-trace/src/profiling/profilers/space.js +2 -1
  31. package/packages/dd-trace/src/profiling/profilers/wall.js +17 -12
  32. package/packages/dd-trace/src/proxy.js +4 -0
@@ -18,30 +18,42 @@ class WAFContextWrapper {
18
18
  this.addressesToSkip = new Set()
19
19
  }
20
20
 
21
- run (params) {
21
+ run ({ persistent, ephemeral }) {
22
+ const payload = {}
23
+ let payloadHasData = false
22
24
  const inputs = {}
23
- let someInputAdded = false
24
25
  const newAddressesToSkip = new Set(this.addressesToSkip)
25
26
 
26
- // TODO: possible optimizaion: only send params that haven't already been sent with same value to this wafContext
27
- for (const key of Object.keys(params)) {
28
- // TODO: requiredAddresses is no longer used due to processor addresses are not included in the list. Check on
29
- // future versions when the actual addresses are included in the 'loaded' section inside diagnostics.
30
- if (!this.addressesToSkip.has(key)) {
31
- inputs[key] = params[key]
32
- if (preventDuplicateAddresses.has(key)) {
33
- newAddressesToSkip.add(key)
27
+ if (persistent && typeof persistent === 'object') {
28
+ // TODO: possible optimization: only send params that haven't already been sent with same value to this wafContext
29
+ for (const key of Object.keys(persistent)) {
30
+ // TODO: requiredAddresses is no longer used due to processor addresses are not included in the list. Check on
31
+ // future versions when the actual addresses are included in the 'loaded' section inside diagnostics.
32
+ if (!this.addressesToSkip.has(key)) {
33
+ inputs[key] = persistent[key]
34
+ if (preventDuplicateAddresses.has(key)) {
35
+ newAddressesToSkip.add(key)
36
+ }
34
37
  }
35
- someInputAdded = true
36
38
  }
37
39
  }
38
40
 
39
- if (!someInputAdded) return
41
+ if (Object.keys(inputs).length) {
42
+ payload['persistent'] = inputs
43
+ payloadHasData = true
44
+ }
45
+
46
+ if (ephemeral && Object.keys(ephemeral).length) {
47
+ payload['ephemeral'] = ephemeral
48
+ payloadHasData = true
49
+ }
50
+
51
+ if (!payloadHasData) return
40
52
 
41
53
  try {
42
54
  const start = process.hrtime.bigint()
43
55
 
44
- const result = this.ddwafContext.run(inputs, this.wafTimeout)
56
+ const result = this.ddwafContext.run(payload, this.wafTimeout)
45
57
 
46
58
  const end = process.hrtime.bigint()
47
59
 
@@ -435,6 +435,10 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
435
435
  maybeFile(appsec.blockedTemplateJson),
436
436
  maybeFile(process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON)
437
437
  )
438
+ const DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON = coalesce(
439
+ maybeFile(appsec.blockedTemplateGraphql),
440
+ maybeFile(process.env.DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON)
441
+ )
438
442
  const DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = coalesce(
439
443
  appsec.eventTracking && appsec.eventTracking.mode,
440
444
  process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING,
@@ -644,6 +648,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
644
648
  obfuscatorValueRegex: DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP,
645
649
  blockedTemplateHtml: DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML,
646
650
  blockedTemplateJson: DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON,
651
+ blockedTemplateGraphql: DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON,
647
652
  eventTracking: {
648
653
  enabled: ['extended', 'safe'].includes(DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING),
649
654
  mode: DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING
@@ -27,10 +27,11 @@ function removeEmptyValues (tags) {
27
27
  }, {})
28
28
  }
29
29
 
30
- // The regex is extracted from
30
+ // The regex is inspired by
31
31
  // https://github.com/jonschlinkert/is-git-url/blob/396965ffabf2f46656c8af4c47bef1d69f09292e/index.js#L9C15-L9C87
32
+ // The `.git` suffix is optional in this version
32
33
  function validateGitRepositoryUrl (repoUrl) {
33
- return /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/.test(repoUrl)
34
+ return /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\/?|#[-\d\w._]+?)$/.test(repoUrl)
34
35
  }
35
36
 
36
37
  function validateGitCommitSha (gitCommitSha) {
@@ -61,6 +61,7 @@ class Profiler extends EventEmitter {
61
61
  }
62
62
 
63
63
  try {
64
+ const start = new Date()
64
65
  for (const profiler of config.profilers) {
65
66
  // TODO: move this out of Profiler when restoring sourcemap support
66
67
  profiler.start({
@@ -70,7 +71,7 @@ class Profiler extends EventEmitter {
70
71
  this._logger.debug(`Started ${profiler.type} profiler`)
71
72
  }
72
73
 
73
- this._capture(this._timeoutInterval)
74
+ this._capture(this._timeoutInterval, start)
74
75
  return true
75
76
  } catch (e) {
76
77
  this._logger.error(e)
@@ -116,9 +117,9 @@ class Profiler extends EventEmitter {
116
117
  return this
117
118
  }
118
119
 
119
- _capture (timeout) {
120
+ _capture (timeout, start) {
120
121
  if (!this._enabled) return
121
- this._lastStart = new Date()
122
+ this._lastStart = start
122
123
  if (!this._timer || timeout !== this._timeoutInterval) {
123
124
  this._timer = setTimeout(() => this._collect(snapshotKinds.PERIODIC), timeout)
124
125
  this._timer.unref()
@@ -138,7 +139,7 @@ class Profiler extends EventEmitter {
138
139
  try {
139
140
  // collect profiles synchronously so that profilers can be safely stopped asynchronously
140
141
  for (const profiler of this._config.profilers) {
141
- const profile = profiler.profile()
142
+ const profile = profiler.profile(start, end)
142
143
  if (!profile) continue
143
144
  profiles.push({ profiler, profile })
144
145
  }
@@ -154,7 +155,7 @@ class Profiler extends EventEmitter {
154
155
  })
155
156
  }
156
157
 
157
- this._capture(this._timeoutInterval)
158
+ this._capture(this._timeoutInterval, end)
158
159
  await this._submit(encodedProfiles, start, end, snapshotKind)
159
160
  this._logger.debug('Submitted profiles')
160
161
  } catch (err) {
@@ -201,7 +202,7 @@ class ServerlessProfiler extends Profiler {
201
202
  await super._collect(snapshotKind)
202
203
  } else {
203
204
  this._profiledIntervals += 1
204
- this._capture(this._timeoutInterval)
205
+ this._capture(this._timeoutInterval, new Date())
205
206
  // Don't submit profile until 65 (flushAfterIntervals) intervals have elapsed
206
207
  }
207
208
  }
@@ -1,5 +1,5 @@
1
1
  const { performance, constants, PerformanceObserver } = require('node:perf_hooks')
2
- const { END_TIMESTAMP } = require('./shared')
2
+ const { END_TIMESTAMP_LABEL } = require('./shared')
3
3
  const semver = require('semver')
4
4
  const { Function, Label, Line, Location, Profile, Sample, StringTable, ValueType } = require('pprof-format')
5
5
  const pprof = require('@datadog/pprof/')
@@ -174,7 +174,7 @@ class EventsProfiler {
174
174
  }
175
175
  }
176
176
 
177
- profile () {
177
+ profile (startDate, endDate) {
178
178
  if (this.entries.length === 0) {
179
179
  // No events in the period; don't produce a profile
180
180
  return null
@@ -202,12 +202,11 @@ class EventsProfiler {
202
202
  decorator.eventTypeLabel = labelFromStrStr(stringTable, 'event', eventType)
203
203
  decorators[eventType] = decorator
204
204
  }
205
- const timestampLabelKey = stringTable.dedup(END_TIMESTAMP)
205
+ const timestampLabelKey = stringTable.dedup(END_TIMESTAMP_LABEL)
206
206
 
207
- let durationFrom = Number.POSITIVE_INFINITY
208
- let durationTo = 0
209
207
  const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
210
-
208
+ const lateEntries = []
209
+ const perfEndDate = endDate.getTime() - performance.timeOrigin
211
210
  const samples = this.entries.map((item) => {
212
211
  const decorator = decorators[item.entryType]
213
212
  if (!decorator) {
@@ -216,9 +215,15 @@ class EventsProfiler {
216
215
  return null
217
216
  }
218
217
  const { startTime, duration } = item
218
+ if (startTime >= perfEndDate) {
219
+ // An event past the current recording end date; save it for the next
220
+ // profile. Not supposed to happen as long as there's no async activity
221
+ // between capture of the endDate value in profiler.js _collect() and
222
+ // here, but better be safe than sorry.
223
+ lateEntries.push(item)
224
+ return null
225
+ }
219
226
  const endTime = startTime + duration
220
- if (durationFrom > startTime) durationFrom = startTime
221
- if (durationTo < endTime) durationTo = endTime
222
227
  const sampleInput = {
223
228
  value: [Math.round(duration * MS_TO_NS)],
224
229
  locationId,
@@ -231,7 +236,7 @@ class EventsProfiler {
231
236
  return new Sample(sampleInput)
232
237
  }).filter(v => v)
233
238
 
234
- this.entries = []
239
+ this.entries = lateEntries
235
240
 
236
241
  const timeValueType = new ValueType({
237
242
  type: stringTable.dedup(pprofValueType),
@@ -240,10 +245,10 @@ class EventsProfiler {
240
245
 
241
246
  return new Profile({
242
247
  sampleType: [timeValueType],
243
- timeNanos: dateOffset + BigInt(Math.round(durationFrom * MS_TO_NS)),
248
+ timeNanos: endDate.getTime() * MS_TO_NS,
244
249
  periodType: timeValueType,
245
- period: this._flushIntervalNanos,
246
- durationNanos: Math.max(0, Math.round((durationTo - durationFrom) * MS_TO_NS)),
250
+ period: 1,
251
+ durationNanos: (endDate.getTime() - startDate.getTime()) * MS_TO_NS,
247
252
  sample: samples,
248
253
  location: locations,
249
254
  function: functions,
@@ -2,8 +2,38 @@
2
2
 
3
3
  const { isMainThread, threadId } = require('node:worker_threads')
4
4
 
5
+ const END_TIMESTAMP_LABEL = 'end_timestamp_ns'
6
+ const THREAD_NAME_LABEL = 'thread name'
7
+ const OS_THREAD_ID_LABEL = 'os thread id'
8
+ const THREAD_ID_LABEL = 'thread id'
9
+ const threadNamePrefix = isMainThread ? 'Main' : `Worker #${threadId}`
10
+ const eventLoopThreadName = `${threadNamePrefix} Event Loop`
11
+
12
+ function getThreadLabels () {
13
+ const pprof = require('@datadog/pprof')
14
+ const nativeThreadId = pprof.getNativeThreadId()
15
+ return {
16
+ [THREAD_NAME_LABEL]: eventLoopThreadName,
17
+ [THREAD_ID_LABEL]: `${threadId}`,
18
+ [OS_THREAD_ID_LABEL]: `${nativeThreadId}`
19
+ }
20
+ }
21
+
22
+ function cacheThreadLabels () {
23
+ let labels
24
+ return () => {
25
+ if (!labels) {
26
+ labels = getThreadLabels()
27
+ }
28
+ return labels
29
+ }
30
+ }
31
+
5
32
  module.exports = {
6
- END_TIMESTAMP: 'end_timestamp_ns',
7
- THREAD_NAME: 'thread name',
8
- threadNamePrefix: isMainThread ? 'Main' : `Worker #${threadId}`
33
+ END_TIMESTAMP_LABEL,
34
+ THREAD_NAME_LABEL,
35
+ THREAD_ID_LABEL,
36
+ threadNamePrefix,
37
+ eventLoopThreadName,
38
+ getThreadLabels: cacheThreadLabels()
9
39
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { oomExportStrategies } = require('../constants')
4
+ const { getThreadLabels } = require('./shared')
4
5
 
5
6
  function strategiesToCallbackMode (strategies, callbackMode) {
6
7
  return strategies.includes(oomExportStrategies.ASYNC_CALLBACK) ? callbackMode.Async : 0
@@ -33,7 +34,7 @@ class NativeSpaceProfiler {
33
34
  }
34
35
 
35
36
  profile () {
36
- return this._pprof.heap.profile(undefined, this._mapper)
37
+ return this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
37
38
  }
38
39
 
39
40
  encode (profile) {
@@ -7,13 +7,12 @@ const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../
7
7
  const { WEB } = require('../../../../../ext/types')
8
8
  const runtimeMetrics = require('../../runtime_metrics')
9
9
  const telemetryMetrics = require('../../telemetry/metrics')
10
- const { END_TIMESTAMP, THREAD_NAME, threadNamePrefix } = require('./shared')
10
+ const { END_TIMESTAMP_LABEL, getThreadLabels } = require('./shared')
11
11
 
12
12
  const beforeCh = dc.channel('dd-trace:storage:before')
13
13
  const enterCh = dc.channel('dd-trace:storage:enter')
14
14
  const spanFinishCh = dc.channel('dd-trace:span:finish')
15
15
  const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
16
- const threadName = `${threadNamePrefix} Event Loop`
17
16
 
18
17
  const MemoizedWebTags = Symbol('NativeWallProfiler.MemoizedWebTags')
19
18
 
@@ -96,12 +95,9 @@ class NativeWallProfiler {
96
95
  this._enter = this._enter.bind(this)
97
96
  this._spanFinished = this._spanFinished.bind(this)
98
97
  }
99
- this._generateLabels = this._generateLabels.bind(this)
100
- } else {
101
- // Explicitly assigning, to express the intent that this is meant to be
102
- // undefined when passed to pprof.time.stop() when not using sample contexts.
103
- this._generateLabels = undefined
104
98
  }
99
+ this._generateLabels = this._generateLabels.bind(this)
100
+
105
101
  this._logger = options.logger
106
102
  this._started = false
107
103
  }
@@ -239,12 +235,21 @@ class NativeWallProfiler {
239
235
  return profile
240
236
  }
241
237
 
242
- _generateLabels ({ context: { spanId, rootSpanId, webTags, endpoint }, timestamp }) {
243
- const labels = this._timelineEnabled ? {
244
- [THREAD_NAME]: threadName,
238
+ _generateLabels (context) {
239
+ if (context == null) {
240
+ // generateLabels is also called for samples without context.
241
+ // In that case just return thread labels.
242
+ return getThreadLabels()
243
+ }
244
+
245
+ const labels = { ...getThreadLabels() }
246
+
247
+ const { context: { spanId, rootSpanId, webTags, endpoint }, timestamp } = context
248
+
249
+ if (this._timelineEnabled) {
245
250
  // Incoming timestamps are in microseconds, we emit nanos.
246
- [END_TIMESTAMP]: timestamp * 1000n
247
- } : {}
251
+ labels[END_TIMESTAMP_LABEL] = timestamp * 1000n
252
+ }
248
253
 
249
254
  if (spanId) {
250
255
  labels['span id'] = spanId
@@ -36,6 +36,10 @@ class Tracer extends NoopProxy {
36
36
  setInterval(() => {
37
37
  this.dogstatsd.flush()
38
38
  }, 10 * 1000).unref()
39
+
40
+ process.once('beforeExit', () => {
41
+ this.dogstatsd.flush()
42
+ })
39
43
  }
40
44
 
41
45
  if (config.spanLeakDebug > 0) {