dd-trace 4.19.0 → 4.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "4.19.0",
3
+ "version": "4.20.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -36,7 +36,7 @@ function enable (_config) {
36
36
 
37
37
  setTemplates(_config)
38
38
 
39
- RuleManager.applyRules(_config.appsec.rules, _config.appsec)
39
+ RuleManager.loadRules(_config.appsec)
40
40
 
41
41
  remoteConfig.enableWafUpdate(_config.appsec)
42
42
 
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const fs = require('fs')
3
4
  const waf = require('./waf')
4
5
  const { ACKNOWLEDGED, ERROR } = require('./remote_config/apply_states')
5
6
  const blocking = require('./blocking')
@@ -13,13 +14,15 @@ let appliedExclusions = new Map()
13
14
  let appliedCustomRules = new Map()
14
15
  let appliedActions = new Map()
15
16
 
16
- function applyRules (rules, config) {
17
- defaultRules = rules
17
+ function loadRules (config) {
18
+ defaultRules = config.rules
19
+ ? JSON.parse(fs.readFileSync(config.rules))
20
+ : require('./recommended.json')
18
21
 
19
- waf.init(rules, config)
22
+ waf.init(defaultRules, config)
20
23
 
21
- if (rules.actions) {
22
- blocking.updateBlockingConfiguration(rules.actions.find(action => action.id === 'block'))
24
+ if (defaultRules.actions) {
25
+ blocking.updateBlockingConfiguration(defaultRules.actions.find(action => action.id === 'block'))
23
26
  }
24
27
  }
25
28
 
@@ -252,7 +255,7 @@ function clearAllRules () {
252
255
  }
253
256
 
254
257
  module.exports = {
255
- applyRules,
258
+ loadRules,
256
259
  updateWafFromRC,
257
260
  clearAllRules
258
261
  }
@@ -309,6 +309,10 @@ class Config {
309
309
  options.tracePropagationStyle,
310
310
  defaultPropagationStyle
311
311
  )
312
+ const DD_TRACE_PROPAGATION_EXTRACT_FIRST = coalesce(
313
+ process.env.DD_TRACE_PROPAGATION_EXTRACT_FIRST,
314
+ false
315
+ )
312
316
  const DD_TRACE_RUNTIME_ID_ENABLED = coalesce(
313
317
  options.experimental && options.experimental.runtimeId,
314
318
  process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED,
@@ -579,6 +583,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
579
583
  inject: DD_TRACE_PROPAGATION_STYLE_INJECT,
580
584
  extract: DD_TRACE_PROPAGATION_STYLE_EXTRACT
581
585
  }
586
+ this.tracePropagationExtractFirst = isTrue(DD_TRACE_PROPAGATION_EXTRACT_FIRST)
582
587
  this.experimental = {
583
588
  runtimeId: isTrue(DD_TRACE_RUNTIME_ID_ENABLED),
584
589
  exporter: DD_TRACE_EXPORTER,
@@ -610,7 +615,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
610
615
  this.tagsHeaderMaxLength = parseInt(DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH)
611
616
  this.appsec = {
612
617
  enabled: DD_APPSEC_ENABLED,
613
- rules: DD_APPSEC_RULES ? safeJsonParse(maybeFile(DD_APPSEC_RULES)) : require('./appsec/recommended.json'),
618
+ rules: DD_APPSEC_RULES,
614
619
  customRulesProvided: !!DD_APPSEC_RULES,
615
620
  rateLimit: DD_APPSEC_TRACE_RATE_LIMIT,
616
621
  wafTimeout: DD_APPSEC_WAF_TIMEOUT,
@@ -42,6 +42,18 @@ class Identifier {
42
42
  toJSON () {
43
43
  return this.toString()
44
44
  }
45
+
46
+ equals (other) {
47
+ const length = this._buffer.length
48
+ const otherLength = other._buffer.length
49
+
50
+ // Only compare the bytes available in both IDs.
51
+ for (let i = length, j = otherLength; i >= 0 && j >= 0; i--, j--) {
52
+ if (this._buffer[i] !== other._buffer[j]) return false
53
+ }
54
+
55
+ return true
56
+ }
45
57
  }
46
58
 
47
59
  // Create a buffer, using an optional hexadecimal value if provided.
@@ -236,11 +236,20 @@ class TextMapPropagator {
236
236
  _extractDatadogContext (carrier) {
237
237
  const spanContext = this._extractGenericContext(carrier, traceKey, spanKey, 10)
238
238
 
239
- if (spanContext) {
240
- this._extractOrigin(carrier, spanContext)
241
- this._extractBaggageItems(carrier, spanContext)
242
- this._extractSamplingPriority(carrier, spanContext)
243
- this._extractTags(carrier, spanContext)
239
+ if (!spanContext) return spanContext
240
+
241
+ this._extractOrigin(carrier, spanContext)
242
+ this._extractBaggageItems(carrier, spanContext)
243
+ this._extractSamplingPriority(carrier, spanContext)
244
+ this._extractTags(carrier, spanContext)
245
+
246
+ if (this._config.tracePropagationExtractFirst) return spanContext
247
+
248
+ const tc = this._extractTraceparentContext(carrier)
249
+
250
+ if (tc && spanContext._traceId.equals(tc._traceId)) {
251
+ spanContext._traceparent = tc._traceparent
252
+ spanContext._tracestate = tc._tracestate
244
253
  }
245
254
 
246
255
  return spanContext
@@ -131,10 +131,12 @@ class Config {
131
131
  : getProfilers({
132
132
  DD_PROFILING_HEAP_ENABLED,
133
133
  DD_PROFILING_WALLTIME_ENABLED,
134
- DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED,
135
134
  DD_PROFILING_PROFILERS
136
135
  })
137
136
 
137
+ this.timelineEnabled = isTrue(coalesce(options.timelineEnabled,
138
+ DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, false))
139
+
138
140
  this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
139
141
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
140
142
  DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, false))
@@ -147,8 +149,7 @@ class Config {
147
149
  module.exports = { Config }
148
150
 
149
151
  function getProfilers ({
150
- DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED,
151
- DD_PROFILING_PROFILERS, DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED
152
+ DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED, DD_PROFILING_PROFILERS
152
153
  }) {
153
154
  // First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to wall + space
154
155
  // Use a Set to avoid duplicates
@@ -172,11 +173,6 @@ function getProfilers ({
172
173
  }
173
174
  }
174
175
 
175
- // Events profiler is a profiler for timeline events that goes with the wall
176
- // profiler
177
- if (profilers.has('wall') && DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED) {
178
- profilers.add('events')
179
- }
180
176
  return [...profilers]
181
177
  }
182
178
 
@@ -238,8 +234,6 @@ function getProfiler (name, options) {
238
234
  return new WallProfiler(options)
239
235
  case 'space':
240
236
  return new SpaceProfiler(options)
241
- case 'events':
242
- return new EventsProfiler(options)
243
237
  default:
244
238
  options.logger.error(`Unknown profiler "${name}"`)
245
239
  }
@@ -257,6 +251,11 @@ function ensureProfilers (profilers, options) {
257
251
  }
258
252
  }
259
253
 
254
+ // Events profiler is a profiler for timeline events
255
+ if (options.timelineEnabled) {
256
+ profilers.push(new EventsProfiler(options))
257
+ }
258
+
260
259
  // Filter out any invalid profilers
261
260
  return profilers.filter(v => v)
262
261
  }
@@ -36,7 +36,7 @@ class EventsProfiler {
36
36
  this._observer = new PerformanceObserver(add.bind(this))
37
37
  }
38
38
  // Currently only support GC
39
- this._observer.observe({ type: 'gc' })
39
+ this._observer.observe({ entryTypes: ['gc'] })
40
40
  }
41
41
 
42
42
  stop () {
@@ -46,6 +46,11 @@ class EventsProfiler {
46
46
  }
47
47
 
48
48
  profile () {
49
+ if (this.entries.length === 0) {
50
+ // No events in the period; don't produce a profile
51
+ return null
52
+ }
53
+
49
54
  const stringTable = new StringTable()
50
55
  const timestampLabelKey = stringTable.dedup(END_TIMESTAMP)
51
56
  const kindLabelKey = stringTable.dedup('gc type')
@@ -15,7 +15,7 @@ const spanFinishCh = dc.channel('dd-trace:span:finish')
15
15
  const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
16
16
  const threadName = `${threadNamePrefix} Event Loop`
17
17
 
18
- const CachedWebTags = Symbol('NativeWallProfiler.CachedWebTags')
18
+ const MemoizedWebTags = Symbol('NativeWallProfiler.MemoizedWebTags')
19
19
 
20
20
  let kSampleCount
21
21
 
@@ -28,28 +28,6 @@ function getStartedSpans (context) {
28
28
  return context._trace.started
29
29
  }
30
30
 
31
- function generateLabels ({ context: { spanId, rootSpanId, webTags, endpoint }, timestamp }) {
32
- const labels = {
33
- [THREAD_NAME]: threadName,
34
- // Incoming timestamps are in microseconds, we emit nanos.
35
- [END_TIMESTAMP]: timestamp * 1000n
36
- }
37
- if (spanId) {
38
- labels['span id'] = spanId
39
- }
40
- if (rootSpanId) {
41
- labels['local root span id'] = rootSpanId
42
- }
43
- if (webTags && Object.keys(webTags).length !== 0) {
44
- labels['trace endpoint'] = endpointNameFromTags(webTags)
45
- } else if (endpoint) {
46
- // fallback to endpoint computed when sample was taken
47
- labels['trace endpoint'] = endpoint
48
- }
49
-
50
- return labels
51
- }
52
-
53
31
  function isWebServerSpan (tags) {
54
32
  return tags[SPAN_TYPE] === WEB
55
33
  }
@@ -61,6 +39,38 @@ function endpointNameFromTags (tags) {
61
39
  ].filter(v => v).join(' ')
62
40
  }
63
41
 
42
+ function getWebTags (startedSpans, i, span) {
43
+ // Are web tags for this span already memoized?
44
+ const memoizedWebTags = span[MemoizedWebTags]
45
+ if (memoizedWebTags !== undefined) {
46
+ return memoizedWebTags
47
+ }
48
+ // No, we'll have to memoize a new value
49
+ function memoize (tags) {
50
+ span[MemoizedWebTags] = tags
51
+ return tags
52
+ }
53
+ // Is this span itself a web span?
54
+ const context = span.context()
55
+ const tags = context._tags
56
+ if (isWebServerSpan(tags)) {
57
+ return memoize(tags)
58
+ }
59
+ // It isn't. Get parent's web tags (memoize them too recursively.)
60
+ // There might be several webspans, for example with next.js, http plugin creates the first span
61
+ // and then next.js plugin creates a child span, and this child span has the correct endpoint
62
+ // information. That's why we always use the tags of the closest ancestor web span.
63
+ const parentId = context._parentId
64
+ while (--i >= 0) {
65
+ const ispan = startedSpans[i]
66
+ if (ispan.context()._spanId === parentId) {
67
+ return memoize(getWebTags(startedSpans, i, ispan))
68
+ }
69
+ }
70
+ // Local root span with no web span
71
+ return memoize(null)
72
+ }
73
+
64
74
  class NativeWallProfiler {
65
75
  constructor (options = {}) {
66
76
  this.type = 'wall'
@@ -68,14 +78,30 @@ class NativeWallProfiler {
68
78
  this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
69
79
  this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
70
80
  this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
71
- this._withContexts = this._codeHotspotsEnabled || this._endpointCollectionEnabled
81
+ this._timelineEnabled = !!options.timelineEnabled
82
+ // We need to capture span data into the sample context for either code hotspots
83
+ // or endpoint collection.
84
+ this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
85
+ // We need to run the pprof wall profiler with sample contexts if we're either
86
+ // capturing span data or timeline is enabled (so we need sample timestamps, and for now
87
+ // timestamps require the sample contexts feature in the pprof wall profiler.)
88
+ this._withContexts = this._captureSpanData || this._timelineEnabled
72
89
  this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
73
90
  this._mapper = undefined
74
91
  this._pprof = undefined
75
92
 
76
- // Bind to this so the same value can be used to unsubscribe later
77
- this._enter = this._enter.bind(this)
78
- this._spanFinished = this._spanFinished.bind(this)
93
+ // Bind these to this so they can be used as callbacks
94
+ if (this._withContexts) {
95
+ if (this._captureSpanData) {
96
+ this._enter = this._enter.bind(this)
97
+ this._spanFinished = this._spanFinished.bind(this)
98
+ }
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
+ }
79
105
  this._logger = options.logger
80
106
  this._started = false
81
107
  }
@@ -113,17 +139,20 @@ class NativeWallProfiler {
113
139
  })
114
140
 
115
141
  if (this._withContexts) {
116
- this._profilerState = this._pprof.time.getState()
117
142
  this._currentContext = {}
118
143
  this._pprof.time.setContext(this._currentContext)
119
- this._lastSpan = undefined
120
- this._lastStartedSpans = undefined
121
- this._lastWebTags = undefined
122
- this._lastSampleCount = 0
123
144
 
124
- beforeCh.subscribe(this._enter)
125
- enterCh.subscribe(this._enter)
126
- spanFinishCh.subscribe(this._spanFinished)
145
+ if (this._captureSpanData) {
146
+ this._profilerState = this._pprof.time.getState()
147
+ this._lastSpan = undefined
148
+ this._lastStartedSpans = undefined
149
+ this._lastWebTags = undefined
150
+ this._lastSampleCount = 0
151
+
152
+ beforeCh.subscribe(this._enter)
153
+ enterCh.subscribe(this._enter)
154
+ spanFinishCh.subscribe(this._spanFinished)
155
+ }
127
156
  }
128
157
 
129
158
  this._started = true
@@ -149,33 +178,7 @@ class NativeWallProfiler {
149
178
  const startedSpans = getStartedSpans(context)
150
179
  this._lastStartedSpans = startedSpans
151
180
  if (this._endpointCollectionEnabled) {
152
- const cachedWebTags = span[CachedWebTags]
153
- if (cachedWebTags === undefined) {
154
- let found = false
155
- // Find the first webspan starting from the end:
156
- // There might be several webspans, for example with next.js, http plugin creates a first span
157
- // and then next.js plugin creates a child span, and this child span has the correct endpoint information.
158
- let nextSpanId = context._spanId
159
- for (let i = startedSpans.length - 1; i >= 0; i--) {
160
- const nextContext = startedSpans[i].context()
161
- if (nextContext._spanId === nextSpanId) {
162
- const tags = nextContext._tags
163
- if (isWebServerSpan(tags)) {
164
- this._lastWebTags = tags
165
- span[CachedWebTags] = tags
166
- found = true
167
- break
168
- }
169
- nextSpanId = nextContext._parentId
170
- }
171
- }
172
- if (!found) {
173
- this._lastWebTags = undefined
174
- span[CachedWebTags] = null // cache negative lookup result
175
- }
176
- } else {
177
- this._lastWebTags = cachedWebTags
178
- }
181
+ this._lastWebTags = getWebTags(startedSpans, startedSpans.length, span)
179
182
  }
180
183
  } else {
181
184
  this._lastStartedSpans = undefined
@@ -204,8 +207,8 @@ class NativeWallProfiler {
204
207
  }
205
208
 
206
209
  _spanFinished (span) {
207
- if (span[CachedWebTags]) {
208
- span[CachedWebTags] = undefined
210
+ if (span[MemoizedWebTags]) {
211
+ span[MemoizedWebTags] = undefined
209
212
  }
210
213
  }
211
214
 
@@ -221,12 +224,12 @@ class NativeWallProfiler {
221
224
 
222
225
  _stop (restart) {
223
226
  if (!this._started) return
224
- if (this._withContexts) {
227
+ if (this._captureSpanData) {
225
228
  // update last sample context if needed
226
229
  this._enter()
227
230
  this._lastSampleCount = 0
228
231
  }
229
- const profile = this._pprof.time.stop(restart, this._withContexts ? generateLabels : undefined)
232
+ const profile = this._pprof.time.stop(restart, this._generateLabels)
230
233
  if (restart) {
231
234
  const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
232
235
  if (v8BugDetected !== 0) {
@@ -236,6 +239,29 @@ class NativeWallProfiler {
236
239
  return profile
237
240
  }
238
241
 
242
+ _generateLabels ({ context: { spanId, rootSpanId, webTags, endpoint }, timestamp }) {
243
+ const labels = this._timelineEnabled ? {
244
+ [THREAD_NAME]: threadName,
245
+ // Incoming timestamps are in microseconds, we emit nanos.
246
+ [END_TIMESTAMP]: timestamp * 1000n
247
+ } : {}
248
+
249
+ if (spanId) {
250
+ labels['span id'] = spanId
251
+ }
252
+ if (rootSpanId) {
253
+ labels['local root span id'] = rootSpanId
254
+ }
255
+ if (webTags && Object.keys(webTags).length !== 0) {
256
+ labels['trace endpoint'] = endpointNameFromTags(webTags)
257
+ } else if (endpoint) {
258
+ // fallback to endpoint computed when sample was taken
259
+ labels['trace endpoint'] = endpoint
260
+ }
261
+
262
+ return labels
263
+ }
264
+
239
265
  profile () {
240
266
  return this._stop(true)
241
267
  }
@@ -248,7 +274,7 @@ class NativeWallProfiler {
248
274
  if (!this._started) return
249
275
 
250
276
  const profile = this._stop(false)
251
- if (this._withContexts) {
277
+ if (this._captureSpanData) {
252
278
  beforeCh.unsubscribe(this._enter)
253
279
  enterCh.unsubscribe(this._enter)
254
280
  spanFinishCh.unsubscribe(this._spanFinished)