dd-trace 4.20.0 → 4.21.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 (58) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/index.d.ts +24 -0
  3. package/package.json +6 -5
  4. package/packages/datadog-instrumentations/src/aerospike.js +47 -0
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/http/client.js +22 -0
  7. package/packages/datadog-instrumentations/src/jest.js +11 -5
  8. package/packages/datadog-instrumentations/src/kafkajs.js +27 -0
  9. package/packages/datadog-instrumentations/src/next.js +3 -1
  10. package/packages/datadog-instrumentations/src/restify.js +1 -1
  11. package/packages/datadog-plugin-aerospike/src/index.js +113 -0
  12. package/packages/datadog-plugin-http/src/client.js +19 -2
  13. package/packages/datadog-plugin-kafkajs/src/consumer.js +51 -0
  14. package/packages/datadog-plugin-kafkajs/src/producer.js +55 -0
  15. package/packages/datadog-plugin-next/src/index.js +7 -7
  16. package/packages/dd-trace/src/appsec/addresses.js +2 -1
  17. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  18. package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +105 -0
  19. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/constants.js +7 -0
  20. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +12 -19
  21. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/header-sensitive-analyzer.js +20 -0
  22. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +6 -10
  23. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +18 -25
  24. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +79 -85
  25. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +27 -36
  26. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +14 -11
  27. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  28. package/packages/dd-trace/src/appsec/index.js +13 -1
  29. package/packages/dd-trace/src/appsec/recommended.json +1395 -2
  30. package/packages/dd-trace/src/appsec/reporter.js +19 -0
  31. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +6 -3
  32. package/packages/dd-trace/src/appsec/waf/waf_manager.js +0 -1
  33. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +17 -1
  34. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +75 -56
  35. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +15 -9
  36. package/packages/dd-trace/src/config.js +31 -2
  37. package/packages/dd-trace/src/datastreams/processor.js +107 -12
  38. package/packages/dd-trace/src/noop/proxy.js +4 -0
  39. package/packages/dd-trace/src/opentracing/span.js +2 -0
  40. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -1
  41. package/packages/dd-trace/src/plugins/index.js +1 -0
  42. package/packages/dd-trace/src/plugins/util/git.js +2 -2
  43. package/packages/dd-trace/src/plugins/util/test.js +3 -2
  44. package/packages/dd-trace/src/profiler.js +5 -3
  45. package/packages/dd-trace/src/profiling/config.js +8 -0
  46. package/packages/dd-trace/src/profiling/profiler.js +10 -4
  47. package/packages/dd-trace/src/profiling/profilers/events.js +166 -73
  48. package/packages/dd-trace/src/proxy.js +21 -1
  49. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +5 -0
  50. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +4 -0
  51. package/packages/dd-trace/src/spanleak.js +98 -0
  52. package/packages/dd-trace/src/startup-log.js +7 -1
  53. package/packages/dd-trace/src/telemetry/dependencies.js +55 -9
  54. package/packages/dd-trace/src/telemetry/index.js +135 -43
  55. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  56. package/packages/dd-trace/src/telemetry/send-data.js +47 -5
  57. package/packages/dd-trace/src/tracer.js +4 -0
  58. package/scripts/install_plugin_modules.js +11 -3
@@ -397,8 +397,9 @@ function addIntelligentTestRunnerSpanTags (
397
397
  testModuleSpan.setTag(TEST_ITR_FORCED_RUN, 'true')
398
398
  }
399
399
 
400
- // If suites have been skipped we don't want to report the total coverage, as it will be wrong
401
- if (testCodeCoverageLinesTotal !== undefined && !isSuitesSkipped) {
400
+ // This will not be reported unless the user has manually added code coverage.
401
+ // This is always the case for Mocha and Cucumber, but not for Jest.
402
+ if (testCodeCoverageLinesTotal !== undefined) {
402
403
  testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
403
404
  testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
404
405
  }
@@ -8,7 +8,7 @@ process.once('beforeExit', () => { profiler.stop() })
8
8
 
9
9
  module.exports = {
10
10
  start: config => {
11
- const { service, version, env, url, hostname, port, tags } = config
11
+ const { service, version, env, url, hostname, port, tags, repositoryUrl, commitSHA } = config
12
12
  const { enabled, sourceMap, exporters } = config.profiling
13
13
  const logger = {
14
14
  debug: (message) => log.debug(message),
@@ -17,7 +17,7 @@ module.exports = {
17
17
  error: (message) => log.error(message)
18
18
  }
19
19
 
20
- profiler.start({
20
+ return profiler.start({
21
21
  enabled,
22
22
  service,
23
23
  version,
@@ -28,7 +28,9 @@ module.exports = {
28
28
  url,
29
29
  hostname,
30
30
  port,
31
- tags
31
+ tags,
32
+ repositoryUrl,
33
+ commitSHA
32
34
  })
33
35
  },
34
36
 
@@ -11,6 +11,7 @@ const WallProfiler = require('./profilers/wall')
11
11
  const SpaceProfiler = require('./profilers/space')
12
12
  const EventsProfiler = require('./profilers/events')
13
13
  const { oomExportStrategies, snapshotKinds } = require('./constants')
14
+ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
14
15
  const { tagger } = require('./tagger')
15
16
  const { isFalse, isTrue } = require('../util')
16
17
 
@@ -72,6 +73,13 @@ class Config {
72
73
  tagger.parse(options.tags),
73
74
  tagger.parse({ env, host, service, version, functionname })
74
75
  )
76
+
77
+ // Add source code integration tags if available
78
+ if (options.repositoryUrl && options.commitSHA) {
79
+ this.tags[GIT_REPOSITORY_URL] = options.repositoryUrl
80
+ this.tags[GIT_COMMIT_SHA] = options.commitSHA
81
+ }
82
+
75
83
  this.logger = ensureLogger(options.logger)
76
84
  const logger = this.logger
77
85
  function logExperimentalVarDeprecation (shortVarName) {
@@ -23,15 +23,19 @@ class Profiler extends EventEmitter {
23
23
  }
24
24
 
25
25
  start (options) {
26
- this._start(options).catch((err) => { if (options.logger) options.logger.error(err) })
27
- return this
26
+ return this._start(options).catch((err) => {
27
+ if (options.logger) {
28
+ options.logger.error(err)
29
+ }
30
+ return false
31
+ })
28
32
  }
29
33
 
30
34
  async _start (options) {
31
- if (this._enabled) return
35
+ if (this._enabled) return true
32
36
 
33
37
  const config = this._config = new Config(options)
34
- if (!config.enabled) return
38
+ if (!config.enabled) return false
35
39
 
36
40
  this._logger = config.logger
37
41
  this._enabled = true
@@ -67,9 +71,11 @@ class Profiler extends EventEmitter {
67
71
  }
68
72
 
69
73
  this._capture(this._timeoutInterval)
74
+ return true
70
75
  } catch (e) {
71
76
  this._logger.error(e)
72
77
  this._stop()
78
+ return false
73
79
  }
74
80
  }
75
81
 
@@ -1,5 +1,5 @@
1
1
  const { performance, constants, PerformanceObserver } = require('node:perf_hooks')
2
- const { END_TIMESTAMP, THREAD_NAME, threadNamePrefix } = require('./shared')
2
+ const { END_TIMESTAMP } = 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/')
@@ -14,7 +14,137 @@ const MS_TO_NS = 1000000
14
14
  // perf_hooks events, the emitted pprof file uses the type "timeline".
15
15
  const pprofValueType = 'timeline'
16
16
  const pprofValueUnit = 'nanoseconds'
17
- const threadName = `${threadNamePrefix} GC`
17
+
18
+ function labelFromStr (stringTable, key, valStr) {
19
+ return new Label({ key, str: stringTable.dedup(valStr) })
20
+ }
21
+
22
+ function labelFromStrStr (stringTable, keyStr, valStr) {
23
+ return labelFromStr(stringTable, stringTable.dedup(keyStr), valStr)
24
+ }
25
+
26
+ class GCDecorator {
27
+ constructor (stringTable) {
28
+ this.stringTable = stringTable
29
+ this.reasonLabelKey = stringTable.dedup('gc reason')
30
+ this.kindLabels = []
31
+ this.reasonLabels = []
32
+ this.flagObj = {}
33
+
34
+ const kindLabelKey = stringTable.dedup('gc type')
35
+
36
+ // Create labels for all GC performance flags and kinds of GC
37
+ for (const [key, value] of Object.entries(constants)) {
38
+ if (key.startsWith('NODE_PERFORMANCE_GC_FLAGS_')) {
39
+ this.flagObj[key.substring(26).toLowerCase()] = value
40
+ } else if (key.startsWith('NODE_PERFORMANCE_GC_')) {
41
+ // It's a constant for a kind of GC
42
+ const kind = key.substring(20).toLowerCase()
43
+ this.kindLabels[value] = labelFromStr(stringTable, kindLabelKey, kind)
44
+ }
45
+ }
46
+ }
47
+
48
+ decorateSample (sampleInput, item) {
49
+ const { kind, flags } = node16 ? item.detail : item
50
+ sampleInput.label.push(this.kindLabels[kind])
51
+ const reasonLabel = this.getReasonLabel(flags)
52
+ if (reasonLabel) {
53
+ sampleInput.label.push(reasonLabel)
54
+ }
55
+ }
56
+
57
+ getReasonLabel (flags) {
58
+ if (flags === 0) {
59
+ return null
60
+ }
61
+ let reasonLabel = this.reasonLabels[flags]
62
+ if (!reasonLabel) {
63
+ const reasons = []
64
+ for (const [key, value] of Object.entries(this.flagObj)) {
65
+ if (value & flags) {
66
+ reasons.push(key)
67
+ }
68
+ }
69
+ const reasonStr = reasons.join(',')
70
+ reasonLabel = labelFromStr(this.stringTable, this.reasonLabelKey, reasonStr)
71
+ this.reasonLabels[flags] = reasonLabel
72
+ }
73
+ return reasonLabel
74
+ }
75
+ }
76
+
77
+ class DNSDecorator {
78
+ constructor (stringTable) {
79
+ this.stringTable = stringTable
80
+ this.operationNameLabelKey = stringTable.dedup('operation')
81
+ this.hostLabelKey = stringTable.dedup('host')
82
+ this.addressLabelKey = stringTable.dedup('address')
83
+ this.portLabelKey = stringTable.dedup('port')
84
+ }
85
+
86
+ decorateSample (sampleInput, item) {
87
+ const labels = sampleInput.label
88
+ const stringTable = this.stringTable
89
+ function addLabel (labelNameKey, labelValue) {
90
+ labels.push(labelFromStr(stringTable, labelNameKey, labelValue))
91
+ }
92
+ const op = item.name
93
+ addLabel(this.operationNameLabelKey, item.name)
94
+ const detail = item.detail
95
+ switch (op) {
96
+ case 'lookup':
97
+ addLabel(this.hostLabelKey, detail.hostname)
98
+ break
99
+ case 'lookupService':
100
+ addLabel(this.addressLabelKey, detail.host)
101
+ labels.push(new Label({ key: this.portLabelKey, num: detail.port }))
102
+ break
103
+ case 'getHostByAddr':
104
+ addLabel(this.addressLabelKey, detail.host)
105
+ break
106
+ default:
107
+ if (op.startsWith('query')) {
108
+ addLabel(this.hostLabelKey, detail.host)
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ class NetDecorator {
115
+ constructor (stringTable) {
116
+ this.stringTable = stringTable
117
+ this.operationNameLabelKey = stringTable.dedup('operation')
118
+ this.hostLabelKey = stringTable.dedup('host')
119
+ this.portLabelKey = stringTable.dedup('port')
120
+ }
121
+
122
+ decorateSample (sampleInput, item) {
123
+ const labels = sampleInput.label
124
+ const stringTable = this.stringTable
125
+ function addLabel (labelNameKey, labelValue) {
126
+ labels.push(labelFromStr(stringTable, labelNameKey, labelValue))
127
+ }
128
+ const op = item.name
129
+ addLabel(this.operationNameLabelKey, op)
130
+ if (op === 'connect') {
131
+ const detail = item.detail
132
+ addLabel(this.hostLabelKey, detail.host)
133
+ labels.push(new Label({ key: this.portLabelKey, num: detail.port }))
134
+ }
135
+ }
136
+ }
137
+
138
+ // Keys correspond to PerformanceEntry.entryType, values are constructor
139
+ // functions for type-specific decorators.
140
+ const decoratorTypes = {
141
+ gc: GCDecorator
142
+ }
143
+ // Needs at least node 16 for DNS and Net
144
+ if (node16) {
145
+ decoratorTypes.dns = DNSDecorator
146
+ decoratorTypes.net = NetDecorator
147
+ }
18
148
 
19
149
  /**
20
150
  * This class generates pprof files with timeline events sourced from Node.js
@@ -35,8 +165,7 @@ class EventsProfiler {
35
165
  if (!this._observer) {
36
166
  this._observer = new PerformanceObserver(add.bind(this))
37
167
  }
38
- // Currently only support GC
39
- this._observer.observe({ entryTypes: ['gc'] })
168
+ this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
40
169
  }
41
170
 
42
171
  stop () {
@@ -52,91 +181,55 @@ class EventsProfiler {
52
181
  }
53
182
 
54
183
  const stringTable = new StringTable()
55
- const timestampLabelKey = stringTable.dedup(END_TIMESTAMP)
56
- const kindLabelKey = stringTable.dedup('gc type')
57
- const reasonLabelKey = stringTable.dedup('gc reason')
58
- const kindLabels = []
59
- const reasonLabels = []
60
184
  const locations = []
61
185
  const functions = []
62
- const locationsPerKind = []
63
- const flagObj = {}
64
186
 
65
- function labelFromStr (key, valStr) {
66
- return new Label({ key, str: stringTable.dedup(valStr) })
67
- }
187
+ // A synthetic single-frame location to serve as the location for timeline
188
+ // samples. We need these as the profiling backend (mimicking official pprof
189
+ // tool's behavior) ignores these.
190
+ const locationId = (() => {
191
+ const fn = new Function({ id: functions.length + 1, name: stringTable.dedup('') })
192
+ functions.push(fn)
193
+ const line = new Line({ functionId: fn.id })
194
+ const location = new Location({ id: locations.length + 1, line: [line] })
195
+ locations.push(location)
196
+ return [location.id]
197
+ })()
68
198
 
69
- function labelFromStrStr (keyStr, valStr) {
70
- return labelFromStr(stringTable.dedup(keyStr), valStr)
71
- }
72
-
73
- // Create labels for all GC performance flags and kinds of GC
74
- for (const [key, value] of Object.entries(constants)) {
75
- if (key.startsWith('NODE_PERFORMANCE_GC_FLAGS_')) {
76
- flagObj[key.substring(26).toLowerCase()] = value
77
- } else if (key.startsWith('NODE_PERFORMANCE_GC_')) {
78
- // It's a constant for a kind of GC
79
- const kind = key.substring(20).toLowerCase()
80
- kindLabels[value] = labelFromStr(kindLabelKey, kind)
81
- // Construct a single-frame "location" too
82
- const fn = new Function({ id: functions.length + 1, name: stringTable.dedup(`${kind} GC`) })
83
- functions.push(fn)
84
- const line = new Line({ functionId: fn.id })
85
- const location = new Location({ id: locations.length + 1, line: [line] })
86
- locations.push(location)
87
- locationsPerKind[value] = [location.id]
88
- }
89
- }
90
-
91
- const gcEventLabel = labelFromStrStr('event', 'gc')
92
- const threadLabel = labelFromStrStr(THREAD_NAME, threadName)
93
-
94
- function getReasonLabel (flags) {
95
- if (flags === 0) {
96
- return null
97
- }
98
- let reasonLabel = reasonLabels[flags]
99
- if (!reasonLabel) {
100
- const reasons = []
101
- for (const [key, value] of Object.entries(flagObj)) {
102
- if (value & flags) {
103
- reasons.push(key)
104
- }
105
- }
106
- const reasonStr = reasons.join(',')
107
- reasonLabel = labelFromStr(reasonLabelKey, reasonStr)
108
- reasonLabels[flags] = reasonLabel
109
- }
110
- return reasonLabel
199
+ const decorators = {}
200
+ for (const [eventType, DecoratorCtor] of Object.entries(decoratorTypes)) {
201
+ const decorator = new DecoratorCtor(stringTable)
202
+ decorator.eventTypeLabel = labelFromStrStr(stringTable, 'event', eventType)
203
+ decorators[eventType] = decorator
111
204
  }
205
+ const timestampLabelKey = stringTable.dedup(END_TIMESTAMP)
112
206
 
113
207
  let durationFrom = Number.POSITIVE_INFINITY
114
208
  let durationTo = 0
115
209
  const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
116
210
 
117
211
  const samples = this.entries.map((item) => {
212
+ const decorator = decorators[item.entryType]
213
+ if (!decorator) {
214
+ // Shouldn't happen but it's better to not rely on observer only getting
215
+ // requested event types.
216
+ return null
217
+ }
118
218
  const { startTime, duration } = item
119
- const { kind, flags } = node16 ? item.detail : item
120
219
  const endTime = startTime + duration
121
220
  if (durationFrom > startTime) durationFrom = startTime
122
221
  if (durationTo < endTime) durationTo = endTime
123
- const labels = [
124
- gcEventLabel,
125
- threadLabel,
126
- new Label({ key: timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) }),
127
- kindLabels[kind]
128
- ]
129
- const reasonLabel = getReasonLabel(flags)
130
- if (reasonLabel) {
131
- labels.push(reasonLabel)
132
- }
133
- const sample = new Sample({
222
+ const sampleInput = {
134
223
  value: [Math.round(duration * MS_TO_NS)],
135
- label: labels,
136
- locationId: locationsPerKind[kind]
137
- })
138
- return sample
139
- })
224
+ locationId,
225
+ label: [
226
+ decorator.eventTypeLabel,
227
+ new Label({ key: timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
228
+ ]
229
+ }
230
+ decorator.decorateSample(sampleInput, item)
231
+ return new Sample(sampleInput)
232
+ }).filter(v => v)
140
233
 
141
234
  this.entries = []
142
235
 
@@ -10,6 +10,7 @@ const PluginManager = require('./plugin_manager')
10
10
  const remoteConfig = require('./appsec/remote_config')
11
11
  const AppsecSdk = require('./appsec/sdk')
12
12
  const dogstatsd = require('./dogstatsd')
13
+ const spanleak = require('./spanleak')
13
14
 
14
15
  class Tracer extends NoopProxy {
15
16
  constructor () {
@@ -37,6 +38,15 @@ class Tracer extends NoopProxy {
37
38
  }, 10 * 1000).unref()
38
39
  }
39
40
 
41
+ if (config.spanLeakDebug > 0) {
42
+ if (config.spanLeakDebug === spanleak.MODES.LOG) {
43
+ spanleak.enableLogging()
44
+ } else if (config.spanLeakDebug === spanleak.MODES.GC_AND_LOG) {
45
+ spanleak.enableGarbageCollection()
46
+ }
47
+ spanleak.startScrubber()
48
+ }
49
+
40
50
  if (config.remoteConfig.enabled && !config.isCiVisibility) {
41
51
  const rc = remoteConfig.enable(config)
42
52
 
@@ -62,11 +72,14 @@ class Tracer extends NoopProxy {
62
72
  // do not stop tracer initialization if the profiler fails to be imported
63
73
  try {
64
74
  const profiler = require('./profiler')
65
- profiler.start(config)
75
+ this._profilerStarted = profiler.start(config)
66
76
  } catch (e) {
67
77
  log.error(e)
68
78
  }
69
79
  }
80
+ if (!this._profilerStarted) {
81
+ this._profilerStarted = Promise.resolve(false)
82
+ }
70
83
 
71
84
  if (config.runtimeMetrics) {
72
85
  runtimeMetrics.start(config)
@@ -104,6 +117,13 @@ class Tracer extends NoopProxy {
104
117
  return this
105
118
  }
106
119
 
120
+ profilerStarted () {
121
+ if (!this._profilerStarted) {
122
+ throw new Error('profilerStarted() must be called after init()')
123
+ }
124
+ return this._profilerStarted
125
+ }
126
+
107
127
  use () {
108
128
  this._pluginManager.configurePlugin(...arguments)
109
129
  return this
@@ -37,6 +37,11 @@ const redisConfig = {
37
37
 
38
38
  const storage = {
39
39
  client: {
40
+ aerospike: {
41
+ opName: () => 'aerospike.command',
42
+ serviceName: ({ tracerService, pluginConfig }) =>
43
+ pluginConfig.service || `${tracerService}-aerospike`
44
+ },
40
45
  'cassandra-driver': {
41
46
  opName: () => 'cassandra.query',
42
47
  serviceName: ({ tracerService, pluginConfig, system }) =>
@@ -22,6 +22,10 @@ function withFunction ({ tracerService, pluginConfig, params }) {
22
22
 
23
23
  const storage = {
24
24
  client: {
25
+ aerospike: {
26
+ opName: () => 'aerospike.command',
27
+ serviceName: configWithFallback
28
+ },
25
29
  'cassandra-driver': {
26
30
  opName: () => 'cassandra.query',
27
31
  serviceName: configWithFallback
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable no-console */
4
+
5
+ const SortedSet = require('tlhunter-sorted-set')
6
+
7
+ const INTERVAL = 1000 // look for expired spans every 1s
8
+ const LIFETIME = 60 * 1000 // all spans have a max lifetime of 1m
9
+
10
+ const MODES = {
11
+ DISABLED: 0,
12
+ // METRICS_ONLY
13
+ LOG: 1,
14
+ GC_AND_LOG: 2
15
+ // GC
16
+ }
17
+
18
+ module.exports.MODES = MODES
19
+
20
+ const spans = new SortedSet()
21
+
22
+ // TODO: should these also be delivered as runtime metrics?
23
+
24
+ // const registry = new FinalizationRegistry(name => {
25
+ // spans.del(span) // there is no span
26
+ // })
27
+
28
+ let interval
29
+ let mode = MODES.DISABLED
30
+
31
+ module.exports.disable = function () {
32
+ mode = MODES.DISABLED
33
+ }
34
+
35
+ module.exports.enableLogging = function () {
36
+ mode = MODES.LOG
37
+ }
38
+
39
+ module.exports.enableGarbageCollection = function () {
40
+ mode = MODES.GC_AND_LOG
41
+ }
42
+
43
+ module.exports.startScrubber = function () {
44
+ if (!isEnabled()) return
45
+
46
+ interval = setInterval(() => {
47
+ const now = Date.now()
48
+ const expired = spans.rangeByScore(0, now)
49
+
50
+ if (!expired.length) return
51
+
52
+ const gc = isGarbageCollecting()
53
+
54
+ const expirationsByType = Object.create(null) // { [spanType]: count }
55
+
56
+ for (const wrapped of expired) {
57
+ spans.del(wrapped)
58
+ const span = wrapped.deref()
59
+
60
+ if (!span) continue // span has already been garbage collected
61
+
62
+ // TODO: Should we also do things like record the route to help users debug leaks?
63
+ if (!expirationsByType[span._name]) expirationsByType[span._name] = 0
64
+ expirationsByType[span._name]++
65
+
66
+ if (!gc) continue // everything after this point is related to manual GC
67
+
68
+ // TODO: what else can we do to alleviate memory usage
69
+ span.context()._tags = Object.create(null)
70
+ }
71
+
72
+ console.log('expired spans:' +
73
+ Object.keys(expirationsByType).reduce((a, c) => `${a} ${c}: ${expirationsByType[c]}`, ''))
74
+ }, INTERVAL)
75
+ }
76
+
77
+ module.exports.stopScrubber = function () {
78
+ clearInterval(interval)
79
+ }
80
+
81
+ module.exports.addSpan = function (span) {
82
+ if (!isEnabled()) return
83
+
84
+ const now = Date.now()
85
+ const expiration = now + LIFETIME
86
+ // eslint-disable-next-line no-undef
87
+ const wrapped = new WeakRef(span)
88
+ spans.add(wrapped, expiration)
89
+ // registry.register(span, span._name)
90
+ }
91
+
92
+ function isEnabled () {
93
+ return mode > MODES.DISABLED
94
+ }
95
+
96
+ function isGarbageCollecting () {
97
+ return mode >= MODES.GC_AND_LOG
98
+ }
@@ -6,6 +6,7 @@ const os = require('os')
6
6
  const { inspect } = require('util')
7
7
  const tracerVersion = require('../../../package.json').version
8
8
 
9
+ const errors = {}
9
10
  let config
10
11
  let pluginManager
11
12
  let samplingRules = []
@@ -89,6 +90,10 @@ function startupLog ({ agentError } = {}) {
89
90
  info('DATADOG TRACER CONFIGURATION - ' + out)
90
91
  if (agentError) {
91
92
  warn('DATADOG TRACER DIAGNOSTIC - Agent Error: ' + agentError.message)
93
+ errors.agentError = {
94
+ code: agentError.code ? agentError.code : '',
95
+ message: `Agent Error:${agentError.message}`
96
+ }
92
97
  }
93
98
 
94
99
  config = undefined
@@ -112,5 +117,6 @@ module.exports = {
112
117
  startupLog,
113
118
  setStartupLogConfig,
114
119
  setStartupLogPluginManager,
115
- setSamplingRules
120
+ setSamplingRules,
121
+ errors
116
122
  }
@@ -6,6 +6,7 @@ const requirePackageJson = require('../require-package-json')
6
6
  const { sendData } = require('./send-data')
7
7
  const dc = require('dc-polyfill')
8
8
  const { fileURLToPath } = require('url')
9
+ const { isTrue } = require('../../src/util')
9
10
 
10
11
  const savedDependenciesToSend = new Set()
11
12
  const detectedDependencyKeys = new Set()
@@ -14,20 +15,57 @@ const detectedDependencyVersions = new Set()
14
15
  const FILE_URI_START = `file://`
15
16
  const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart')
16
17
 
17
- let immediate, config, application, host
18
+ let immediate, config, application, host, initialLoad
18
19
  let isFirstModule = true
20
+ let getRetryData
21
+ let updateRetryData
19
22
 
23
+ function createBatchPayload (payload) {
24
+ const batchPayload = []
25
+ payload.map(item => {
26
+ batchPayload.push({
27
+ request_type: item.reqType,
28
+ payload: item.payload
29
+ })
30
+ })
31
+
32
+ return batchPayload
33
+ }
20
34
  function waitAndSend (config, application, host) {
21
35
  if (!immediate) {
22
36
  immediate = setImmediate(() => {
23
37
  immediate = null
24
38
  if (savedDependenciesToSend.size > 0) {
25
- const dependencies = Array.from(savedDependenciesToSend.values()).splice(0, 1000).map(pair => {
26
- savedDependenciesToSend.delete(pair)
27
- const [name, version] = pair.split(' ')
28
- return { name, version }
29
- })
30
- sendData(config, application, host, 'app-dependencies-loaded', { dependencies })
39
+ const dependencies = Array.from(savedDependenciesToSend.values())
40
+ // if a depencdency is from the initial load, *always* send the event
41
+ // Otherwise, only send if dependencyCollection is enabled
42
+ .filter(dep => {
43
+ const initialLoadModule = isTrue(dep.split(' ')[2])
44
+ const sendModule = initialLoadModule || (config.telemetry?.dependencyCollection)
45
+
46
+ if (!sendModule) savedDependenciesToSend.delete(dep) // we'll never send it
47
+ return sendModule
48
+ })
49
+ .splice(0, 2000) // v2 documentation specifies up to 2000 dependencies can be sent at once
50
+ .map(pair => {
51
+ savedDependenciesToSend.delete(pair)
52
+ const [name, version] = pair.split(' ')
53
+ return { name, version }
54
+ })
55
+ let currPayload
56
+ const retryData = getRetryData()
57
+ if (retryData) {
58
+ currPayload = { reqType: 'app-dependencies-loaded', payload: { dependencies } }
59
+ } else {
60
+ if (!dependencies.length) return // no retry data and no dependencies, nothing to send
61
+ currPayload = { dependencies }
62
+ }
63
+
64
+ const payload = retryData ? createBatchPayload([currPayload, retryData]) : currPayload
65
+ const reqType = retryData ? 'message-batch' : 'app-dependencies-loaded'
66
+
67
+ sendData(config, application, host, reqType, payload, updateRetryData)
68
+
31
69
  if (savedDependenciesToSend.size > 0) {
32
70
  waitAndSend(config, application, host)
33
71
  }
@@ -76,7 +114,7 @@ function onModuleLoad (data) {
76
114
  const dependencyAndVersion = `${name} ${version}`
77
115
 
78
116
  if (!detectedDependencyVersions.has(dependencyAndVersion)) {
79
- savedDependenciesToSend.add(dependencyAndVersion)
117
+ savedDependenciesToSend.add(`${dependencyAndVersion} ${initialLoad}`)
80
118
  detectedDependencyVersions.add(dependencyAndVersion)
81
119
 
82
120
  waitAndSend(config, application, host)
@@ -89,11 +127,19 @@ function onModuleLoad (data) {
89
127
  }
90
128
  }
91
129
  }
92
- function start (_config, _application, _host) {
130
+ function start (_config = {}, _application, _host, getRetryDataFunction, updateRetryDatafunction) {
93
131
  config = _config
94
132
  application = _application
95
133
  host = _host
134
+ initialLoad = true
135
+ getRetryData = getRetryDataFunction
136
+ updateRetryData = updateRetryDatafunction
96
137
  moduleLoadStartChannel.subscribe(onModuleLoad)
138
+
139
+ // try and capture intially loaded modules in the first tick
140
+ // since, ideally, the tracer (and this module) should be loaded first,
141
+ // this should capture any first-tick dependencies
142
+ queueMicrotask(() => { initialLoad = false })
97
143
  }
98
144
 
99
145
  function isDependency (filename, request) {