dd-trace 3.44.0 → 3.46.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 (36) hide show
  1. package/MIGRATING.md +15 -0
  2. package/README.md +11 -9
  3. package/package.json +8 -7
  4. package/packages/datadog-instrumentations/src/cucumber.js +3 -1
  5. package/packages/datadog-instrumentations/src/jest.js +3 -0
  6. package/packages/datadog-instrumentations/src/mocha.js +9 -2
  7. package/packages/datadog-plugin-cucumber/src/index.js +11 -7
  8. package/packages/datadog-plugin-cypress/src/plugin.js +60 -46
  9. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -0
  10. package/packages/datadog-plugin-jest/src/index.js +31 -5
  11. package/packages/datadog-plugin-jest/src/util.js +38 -16
  12. package/packages/datadog-plugin-mocha/src/index.js +11 -2
  13. package/packages/datadog-plugin-playwright/src/index.js +2 -0
  14. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  15. package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +3 -3
  16. package/packages/dd-trace/src/appsec/iast/analyzers/weak-randomness-analyzer.js +19 -0
  17. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
  18. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +12 -1
  19. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  20. package/packages/dd-trace/src/appsec/remote_config/manager.js +9 -8
  21. package/packages/dd-trace/src/appsec/reporter.js +2 -1
  22. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +4 -2
  23. package/packages/dd-trace/src/config.js +11 -7
  24. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +25 -2
  25. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  26. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  27. package/packages/dd-trace/src/profiling/config.js +32 -3
  28. package/packages/dd-trace/src/profiling/exporters/file.js +2 -1
  29. package/packages/dd-trace/src/profiling/profiler.js +18 -14
  30. package/packages/dd-trace/src/profiling/profilers/events.js +10 -4
  31. package/packages/dd-trace/src/profiling/profilers/shared.js +6 -0
  32. package/packages/dd-trace/src/profiling/profilers/space.js +17 -2
  33. package/packages/dd-trace/src/profiling/profilers/wall.js +34 -21
  34. package/packages/dd-trace/src/telemetry/index.js +8 -3
  35. package/packages/dd-trace/src/telemetry/send-data.js +35 -16
  36. package/scripts/st.js +105 -0
@@ -3,6 +3,7 @@
3
3
  const csiMethods = [
4
4
  { src: 'concat' },
5
5
  { src: 'plusOperator', operator: true },
6
+ { src: 'random' },
6
7
  { src: 'replace' },
7
8
  { src: 'slice' },
8
9
  { src: 'substr' },
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const dc = require('dc-polyfill')
3
4
  const TaintedUtils = require('@datadog/native-iast-taint-tracking')
4
5
  const { storage } = require('../../../../../datadog-core')
5
6
  const iastContextFunctions = require('../iast-context')
@@ -7,12 +8,15 @@ const iastLog = require('../iast-log')
7
8
  const { EXECUTED_PROPAGATION } = require('../telemetry/iast-metric')
8
9
  const { isDebugAllowed } = require('../telemetry/verbosity')
9
10
 
11
+ const mathRandomCallCh = dc.channel('datadog:random:call')
12
+
10
13
  function noop (res) { return res }
11
14
  // NOTE: methods of this object must be synchronized with csi-methods.js file definitions!
12
15
  // Otherwise you may end up rewriting a method and not providing its rewritten implementation
13
16
  const TaintTrackingNoop = {
14
17
  plusOperator: noop,
15
18
  concat: noop,
19
+ random: noop,
16
20
  replace: noop,
17
21
  slice: noop,
18
22
  substr: noop,
@@ -110,7 +114,14 @@ function csiMethodsOverrides (getContext) {
110
114
  getContext,
111
115
  String.prototype.trim,
112
116
  String.prototype.trimStart
113
- )
117
+ ),
118
+
119
+ random: function (res, fn) {
120
+ if (mathRandomCallCh.hasSubscribers) {
121
+ mathRandomCallCh.publish({ fn })
122
+ }
123
+ return res
124
+ }
114
125
  }
115
126
  }
116
127
 
@@ -14,5 +14,6 @@ module.exports = {
14
14
  UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
15
15
  WEAK_CIPHER: 'WEAK_CIPHER',
16
16
  WEAK_HASH: 'WEAK_HASH',
17
+ WEAK_RANDOMNESS: 'WEAK_RANDOMNESS',
17
18
  XCONTENTTYPE_HEADER_MISSING: 'XCONTENTTYPE_HEADER_MISSING'
18
19
  }
@@ -25,7 +25,8 @@ class RemoteConfigManager extends EventEmitter {
25
25
  super()
26
26
 
27
27
  const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000)
28
- const url = config.url || new URL(format({
28
+
29
+ this.url = config.url || new URL(format({
29
30
  protocol: 'http:',
30
31
  hostname: config.hostname || 'localhost',
31
32
  port: config.port
@@ -33,12 +34,6 @@ class RemoteConfigManager extends EventEmitter {
33
34
 
34
35
  this.scheduler = new Scheduler((cb) => this.poll(cb), pollInterval)
35
36
 
36
- this.requestOptions = {
37
- url,
38
- method: 'POST',
39
- path: '/v0.7/config'
40
- }
41
-
42
37
  this.state = {
43
38
  client: {
44
39
  state: { // updated by `parseConfig()`
@@ -122,7 +117,13 @@ class RemoteConfigManager extends EventEmitter {
122
117
  }
123
118
 
124
119
  poll (cb) {
125
- request(this.getPayload(), this.requestOptions, (err, data, statusCode) => {
120
+ const options = {
121
+ url: this.url,
122
+ method: 'POST',
123
+ path: '/v0.7/config'
124
+ }
125
+
126
+ request(this.getPayload(), options, (err, data, statusCode) => {
126
127
  // 404 means RC is disabled, ignore it
127
128
  if (statusCode === 404) return cb()
128
129
 
@@ -29,9 +29,10 @@ const REQUEST_HEADERS_MAP = mapHeaderAndTags([
29
29
  'accept-encoding',
30
30
  'accept-language',
31
31
  'host',
32
- 'user-agent',
33
32
  'forwarded',
33
+ 'user-agent',
34
34
  'via',
35
+ 'x-amzn-trace-id',
35
36
 
36
37
  ...ipHeaderList,
37
38
  ...contentHeaderList
@@ -83,7 +83,8 @@ function getSkippableSuites ({
83
83
  } else {
84
84
  let skippableSuites = []
85
85
  try {
86
- skippableSuites = JSON.parse(res)
86
+ const parsedResponse = JSON.parse(res)
87
+ skippableSuites = parsedResponse
87
88
  .data
88
89
  .filter(({ type }) => type === testLevel)
89
90
  .map(({ attributes: { suite, name } }) => {
@@ -92,6 +93,7 @@ function getSkippableSuites ({
92
93
  }
93
94
  return { suite, name }
94
95
  })
96
+ const { meta: { correlation_id: correlationId } } = parsedResponse
95
97
  incrementCountMetric(
96
98
  testLevel === 'test'
97
99
  ? TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS : TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES,
@@ -100,7 +102,7 @@ function getSkippableSuites ({
100
102
  )
101
103
  distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length)
102
104
  log.debug(() => `Number of received skippable ${testLevel}s: ${skippableSuites.length}`)
103
- done(null, skippableSuites)
105
+ done(null, skippableSuites, correlationId)
104
106
  } catch (err) {
105
107
  done(err)
106
108
  }
@@ -611,7 +611,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
611
611
  this.startupLogs = isTrue(DD_TRACE_STARTUP_LOGS)
612
612
  // Disabled for CI Visibility's agentless
613
613
  this.telemetry = {
614
- enabled: DD_TRACE_EXPORTER !== 'datadog' && isTrue(DD_INSTRUMENTATION_TELEMETRY_ENABLED),
614
+ enabled: isTrue(DD_INSTRUMENTATION_TELEMETRY_ENABLED),
615
615
  heartbeatInterval: DD_TELEMETRY_HEARTBEAT_INTERVAL,
616
616
  debug: isTrue(DD_TELEMETRY_DEBUG),
617
617
  logCollection: isTrue(DD_TELEMETRY_LOG_COLLECTION_ENABLED),
@@ -795,9 +795,9 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
795
795
  tagger.add(tags, DD_TRACE_TAGS)
796
796
  tagger.add(tags, DD_TRACE_GLOBAL_TAGS)
797
797
 
798
- this._setValue(env, 'service', DD_SERVICE || DD_SERVICE_NAME || tags.service)
799
- this._setValue(env, 'env', DD_ENV || tags.env)
800
- this._setValue(env, 'version', DD_VERSION || tags.version)
798
+ this._setString(env, 'service', DD_SERVICE || DD_SERVICE_NAME || tags.service)
799
+ this._setString(env, 'env', DD_ENV || tags.env)
800
+ this._setString(env, 'version', DD_VERSION || tags.version)
801
801
  this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE)
802
802
  this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
803
803
  this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
@@ -812,9 +812,9 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
812
812
 
813
813
  tagger.add(tags, options.tags)
814
814
 
815
- this._setValue(opts, 'service', options.service || tags.service)
816
- this._setValue(opts, 'env', options.env || tags.env)
817
- this._setValue(opts, 'version', options.version || tags.version)
815
+ this._setString(opts, 'service', options.service || tags.service)
816
+ this._setString(opts, 'env', options.env || tags.env)
817
+ this._setString(opts, 'version', options.version || tags.version)
818
818
  this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate))
819
819
  this._setBoolean(opts, 'logInjection', options.logInjection)
820
820
  this._setArray(opts, 'headerTags', options.headerTags)
@@ -875,6 +875,10 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
875
875
  }
876
876
  }
877
877
 
878
+ _setString (obj, name, value) {
879
+ obj[name] = value || undefined // unset for empty strings
880
+ }
881
+
878
882
  _setTags (obj, name, value) {
879
883
  if (!value || Object.keys(value).length === 0) {
880
884
  return this._setValue(obj, name, null)
@@ -2,7 +2,8 @@
2
2
  const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
3
  const { AgentEncoder } = require('./0.4')
4
4
  const { version: ddTraceVersion } = require('../../../../package.json')
5
- const id = require('../../../dd-trace/src/id')
5
+ const { ITR_CORRELATION_ID } = require('../../src/plugins/util/test')
6
+ const id = require('../../src/id')
6
7
  const {
7
8
  distributionMetric,
8
9
  TELEMETRY_ENDPOINT_PAYLOAD_SERIALIZATION_MS,
@@ -45,7 +46,13 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
45
46
  }
46
47
 
47
48
  _encodeTestSuite (bytes, content) {
48
- this._encodeMapPrefix(bytes, TEST_SUITE_KEYS_LENGTH)
49
+ let keysLength = TEST_SUITE_KEYS_LENGTH
50
+ const itrCorrelationId = content.meta[ITR_CORRELATION_ID]
51
+ if (itrCorrelationId) {
52
+ keysLength++
53
+ }
54
+
55
+ this._encodeMapPrefix(bytes, keysLength)
49
56
  this._encodeString(bytes, 'type')
50
57
  this._encodeString(bytes, content.type)
51
58
 
@@ -58,6 +65,12 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
58
65
  this._encodeString(bytes, 'test_suite_id')
59
66
  this._encodeId(bytes, content.span_id)
60
67
 
68
+ if (itrCorrelationId) {
69
+ this._encodeString(bytes, ITR_CORRELATION_ID)
70
+ this._encodeString(bytes, itrCorrelationId)
71
+ delete content.meta[ITR_CORRELATION_ID]
72
+ }
73
+
61
74
  this._encodeString(bytes, 'error')
62
75
  this._encodeNumber(bytes, content.error)
63
76
  this._encodeString(bytes, 'name')
@@ -144,6 +157,10 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
144
157
  if (content.meta.test_suite_id) {
145
158
  totalKeysLength = totalKeysLength + 1
146
159
  }
160
+ const itrCorrelationId = content.meta[ITR_CORRELATION_ID]
161
+ if (itrCorrelationId) {
162
+ totalKeysLength = totalKeysLength + 1
163
+ }
147
164
  this._encodeMapPrefix(bytes, totalKeysLength)
148
165
  if (content.type) {
149
166
  this._encodeString(bytes, 'type')
@@ -194,6 +211,12 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
194
211
  delete content.meta.test_suite_id
195
212
  }
196
213
 
214
+ if (itrCorrelationId) {
215
+ this._encodeString(bytes, ITR_CORRELATION_ID)
216
+ this._encodeString(bytes, itrCorrelationId)
217
+ delete content.meta[ITR_CORRELATION_ID]
218
+ }
219
+
197
220
  this._encodeString(bytes, 'meta')
198
221
  this._encodeMap(bytes, content.meta)
199
222
  this._encodeString(bytes, 'metrics')
@@ -15,7 +15,8 @@ const {
15
15
  TEST_MODULE,
16
16
  getTestSuiteCommonTags,
17
17
  TEST_STATUS,
18
- TEST_SKIPPED_BY_ITR
18
+ TEST_SKIPPED_BY_ITR,
19
+ ITR_CORRELATION_ID
19
20
  } = require('./util/test')
20
21
  const Plugin = require('./plugin')
21
22
  const { COMPONENT } = require('../constants')
@@ -53,11 +54,13 @@ module.exports = class CiPlugin extends Plugin {
53
54
  if (!this.tracer._exporter || !this.tracer._exporter.getSkippableSuites) {
54
55
  return onDone({ err: new Error('CI Visibility was not initialized correctly') })
55
56
  }
56
- this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites) => {
57
+ this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => {
57
58
  if (err) {
58
59
  log.error(`Skippable suites could not be fetched. ${err.message}`)
60
+ } else {
61
+ this.itrCorrelationId = itrCorrelationId
59
62
  }
60
- onDone({ err, skippableSuites })
63
+ onDone({ err, skippableSuites, itrCorrelationId })
61
64
  })
62
65
  })
63
66
 
@@ -95,6 +98,9 @@ module.exports = class CiPlugin extends Plugin {
95
98
  const testCommand = this.testSessionSpan.context()._tags[TEST_COMMAND]
96
99
  skippedSuites.forEach((testSuite) => {
97
100
  const testSuiteMetadata = getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, this.constructor.id)
101
+ if (this.itrCorrelationId) {
102
+ testSuiteMetadata[ITR_CORRELATION_ID] = this.itrCorrelationId
103
+ }
98
104
 
99
105
  this.tracer.startSpan(`${this.constructor.id}.test_suite`, {
100
106
  childOf: this.testModuleSpan,
@@ -60,6 +60,7 @@ const TEST_ITR_SKIPPING_COUNT = 'test.itr.tests_skipping.count'
60
60
  const TEST_CODE_COVERAGE_ENABLED = 'test.code_coverage.enabled'
61
61
  const TEST_ITR_UNSKIPPABLE = 'test.itr.unskippable'
62
62
  const TEST_ITR_FORCED_RUN = 'test.itr.forced_run'
63
+ const ITR_CORRELATION_ID = 'itr_correlation_id'
63
64
 
64
65
  const TEST_CODE_COVERAGE_LINES_PCT = 'test.code_coverage.lines_pct'
65
66
 
@@ -111,6 +112,7 @@ module.exports = {
111
112
  TEST_CODE_COVERAGE_LINES_PCT,
112
113
  TEST_ITR_UNSKIPPABLE,
113
114
  TEST_ITR_FORCED_RUN,
115
+ ITR_CORRELATION_ID,
114
116
  addIntelligentTestRunnerSpanTags,
115
117
  getCoveredFilenamesFromCoverage,
116
118
  resetCoverage,
@@ -35,10 +35,12 @@ class Config {
35
35
  DD_PROFILING_HEAP_ENABLED,
36
36
  DD_PROFILING_V8_PROFILER_BUG_WORKAROUND,
37
37
  DD_PROFILING_WALLTIME_ENABLED,
38
+ DD_PROFILING_EXPERIMENTAL_CPU_ENABLED,
38
39
  DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED,
39
40
  DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE,
40
41
  DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT,
41
42
  DD_PROFILING_EXPERIMENTAL_OOM_EXPORT_STRATEGIES,
43
+ DD_PROFILING_TIMELINE_ENABLED,
42
44
  DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED,
43
45
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
44
46
  DD_PROFILING_ENDPOINT_COLLECTION_ENABLED,
@@ -91,14 +93,28 @@ class Config {
91
93
  logger.warn(`${deprecatedEnvVarName} is deprecated. Use DD_PROFILING_${shortVarName} instead.`)
92
94
  }
93
95
  }
96
+ // Profiler sampling contexts are not available on Windows, so features
97
+ // depending on those (code hotspots and endpoint collection) need to default
98
+ // to false on Windows.
99
+ const samplingContextsAvailable = process.platform !== 'win32'
100
+ function checkOptionAllowed (option, description, condition) {
101
+ if (option && !condition) {
102
+ throw new Error(`${description} not supported on ${process.platform}.`)
103
+ }
104
+ }
105
+ function checkOptionWithSamplingContextAllowed (option, description) {
106
+ checkOptionAllowed(option, description, samplingContextsAvailable)
107
+ }
108
+
94
109
  this.flushInterval = flushInterval
95
110
  this.uploadTimeout = uploadTimeout
96
111
  this.sourceMap = sourceMap
97
112
  this.debugSourceMaps = isTrue(coalesce(options.debugSourceMaps, DD_PROFILING_DEBUG_SOURCE_MAPS, false))
98
113
  this.endpointCollectionEnabled = isTrue(coalesce(options.endpointCollection,
99
114
  DD_PROFILING_ENDPOINT_COLLECTION_ENABLED,
100
- DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED, false))
115
+ DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED, samplingContextsAvailable))
101
116
  logExperimentalVarDeprecation('ENDPOINT_COLLECTION_ENABLED')
117
+ checkOptionWithSamplingContextAllowed(this.endpointCollectionEnabled, 'Endpoint collection')
102
118
 
103
119
  this.pprofPrefix = pprofPrefix
104
120
  this.v8ProfilerBugWorkaroundEnabled = isTrue(coalesce(options.v8ProfilerBugWorkaround,
@@ -115,8 +131,13 @@ class Config {
115
131
  new AgentExporter(this)
116
132
  ], this)
117
133
 
134
+ // OOM monitoring does not work well on Windows, so it is disabled by default.
135
+ const oomMonitoringSupported = process.platform !== 'win32'
136
+
118
137
  const oomMonitoringEnabled = isTrue(coalesce(options.oomMonitoring,
119
- DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, true))
138
+ DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, oomMonitoringSupported))
139
+ checkOptionAllowed(oomMonitoringEnabled, 'OOM monitoring', oomMonitoringSupported)
140
+
120
141
  const heapLimitExtensionSize = coalesce(options.oomHeapLimitExtensionSize,
121
142
  Number(DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE), 0)
122
143
  const maxHeapExtensionCount = coalesce(options.oomMaxHeapExtensionCount,
@@ -143,12 +164,20 @@ class Config {
143
164
  })
144
165
 
145
166
  this.timelineEnabled = isTrue(coalesce(options.timelineEnabled,
167
+ DD_PROFILING_TIMELINE_ENABLED,
146
168
  DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, false))
169
+ logExperimentalVarDeprecation('TIMELINE_ENABLED')
170
+ checkOptionWithSamplingContextAllowed(this.timelineEnabled, 'Timeline view')
147
171
 
148
172
  this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
149
173
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
150
- DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, false))
174
+ DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, samplingContextsAvailable))
151
175
  logExperimentalVarDeprecation('CODEHOTSPOTS_ENABLED')
176
+ checkOptionWithSamplingContextAllowed(this.codeHotspotsEnabled, 'Code hotspots')
177
+
178
+ this.cpuProfilingEnabled = isTrue(coalesce(options.cpuProfilingEnabled,
179
+ DD_PROFILING_EXPERIMENTAL_CPU_ENABLED, false))
180
+ checkOptionWithSamplingContextAllowed(this.cpuProfilingEnabled, 'CPU profiling')
152
181
 
153
182
  this.profilers = ensureProfilers(profilers, this)
154
183
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const { promisify } = require('util')
5
+ const { threadId } = require('worker_threads')
5
6
  const writeFile = promisify(fs.writeFile)
6
7
 
7
8
  function formatDateTime (t) {
@@ -19,7 +20,7 @@ class FileExporter {
19
20
  const types = Object.keys(profiles)
20
21
  const dateStr = formatDateTime(end)
21
22
  const tasks = types.map(type => {
22
- return writeFile(`${this._pprofPrefix}${type}_${dateStr}.pprof`, profiles[type])
23
+ return writeFile(`${this._pprofPrefix}${type}_worker_${threadId}_${dateStr}.pprof`, profiles[type])
23
24
  })
24
25
 
25
26
  return Promise.all(tasks)
@@ -3,6 +3,7 @@
3
3
  const { EventEmitter } = require('events')
4
4
  const { Config } = require('./config')
5
5
  const { snapshotKinds } = require('./constants')
6
+ const { threadNamePrefix } = require('./profilers/shared')
6
7
 
7
8
  function maybeSourceMap (sourceMap, SourceMapper, debug) {
8
9
  if (!sourceMap) return
@@ -68,7 +69,7 @@ class Profiler extends EventEmitter {
68
69
  mapper,
69
70
  nearOOMCallback: this._nearOOMExport.bind(this)
70
71
  })
71
- this._logger.debug(`Started ${profiler.type} profiler`)
72
+ this._logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`)
72
73
  }
73
74
 
74
75
  this._capture(this._timeoutInterval, start)
@@ -97,7 +98,7 @@ class Profiler extends EventEmitter {
97
98
 
98
99
  // collect and export current profiles
99
100
  // once collect returns, profilers can be safely stopped
100
- this._collect(snapshotKinds.ON_SHUTDOWN)
101
+ this._collect(snapshotKinds.ON_SHUTDOWN, false)
101
102
  this._stop()
102
103
  }
103
104
 
@@ -108,13 +109,11 @@ class Profiler extends EventEmitter {
108
109
 
109
110
  for (const profiler of this._config.profilers) {
110
111
  profiler.stop()
111
- this._logger.debug(`Stopped ${profiler.type} profiler`)
112
+ this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
112
113
  }
113
114
 
114
115
  clearTimeout(this._timer)
115
116
  this._timer = undefined
116
-
117
- return this
118
117
  }
119
118
 
120
119
  _capture (timeout, start) {
@@ -128,18 +127,21 @@ class Profiler extends EventEmitter {
128
127
  }
129
128
  }
130
129
 
131
- async _collect (snapshotKind) {
130
+ async _collect (snapshotKind, restart = true) {
132
131
  if (!this._enabled) return
133
132
 
134
- const start = this._lastStart
135
- const end = new Date()
133
+ const startDate = this._lastStart
134
+ const endDate = new Date()
136
135
  const profiles = []
137
136
  const encodedProfiles = {}
138
137
 
139
138
  try {
140
139
  // collect profiles synchronously so that profilers can be safely stopped asynchronously
141
140
  for (const profiler of this._config.profilers) {
142
- const profile = profiler.profile(start, end)
141
+ const profile = profiler.profile(restart, startDate, endDate)
142
+ if (!restart) {
143
+ this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
144
+ }
143
145
  if (!profile) continue
144
146
  profiles.push({ profiler, profile })
145
147
  }
@@ -155,8 +157,10 @@ class Profiler extends EventEmitter {
155
157
  })
156
158
  }
157
159
 
158
- this._capture(this._timeoutInterval, end)
159
- await this._submit(encodedProfiles, start, end, snapshotKind)
160
+ if (restart) {
161
+ this._capture(this._timeoutInterval, endDate)
162
+ }
163
+ await this._submit(encodedProfiles, startDate, endDate, snapshotKind)
160
164
  this._logger.debug('Submitted profiles')
161
165
  } catch (err) {
162
166
  this._logger.error(err)
@@ -196,10 +200,10 @@ class ServerlessProfiler extends Profiler {
196
200
  this._flushAfterIntervals = this._config.flushInterval / 1000
197
201
  }
198
202
 
199
- async _collect (snapshotKind) {
200
- if (this._profiledIntervals >= this._flushAfterIntervals) {
203
+ async _collect (snapshotKind, restart = true) {
204
+ if (this._profiledIntervals >= this._flushAfterIntervals || !restart) {
201
205
  this._profiledIntervals = 0
202
- await super._collect(snapshotKind)
206
+ await super._collect(snapshotKind, restart)
203
207
  } else {
204
208
  this._profiledIntervals += 1
205
209
  this._capture(this._timeoutInterval, new Date())
@@ -159,22 +159,24 @@ class EventsProfiler {
159
159
  }
160
160
 
161
161
  start () {
162
+ // if already started, do nothing
163
+ if (this._observer) return
164
+
162
165
  function add (items) {
163
166
  this.entries.push(...items.getEntries())
164
167
  }
165
- if (!this._observer) {
166
- this._observer = new PerformanceObserver(add.bind(this))
167
- }
168
+ this._observer = new PerformanceObserver(add.bind(this))
168
169
  this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
169
170
  }
170
171
 
171
172
  stop () {
172
173
  if (this._observer) {
173
174
  this._observer.disconnect()
175
+ this._observer = undefined
174
176
  }
175
177
  }
176
178
 
177
- profile (startDate, endDate) {
179
+ profile (restart, startDate, endDate) {
178
180
  if (this.entries.length === 0) {
179
181
  // No events in the period; don't produce a profile
180
182
  return null
@@ -243,6 +245,10 @@ class EventsProfiler {
243
245
  unit: stringTable.dedup(pprofValueUnit)
244
246
  })
245
247
 
248
+ if (!restart) {
249
+ this.stop()
250
+ }
251
+
246
252
  return new Profile({
247
253
  sampleType: [timeValueType],
248
254
  timeNanos: endDate.getTime() * MS_TO_NS,
@@ -29,11 +29,17 @@ function cacheThreadLabels () {
29
29
  }
30
30
  }
31
31
 
32
+ function getNonJSThreadsLabels () {
33
+ return { [THREAD_NAME_LABEL]: 'Non-JS threads', [THREAD_ID_LABEL]: 'NA', [OS_THREAD_ID_LABEL]: 'NA' }
34
+ }
35
+
32
36
  module.exports = {
33
37
  END_TIMESTAMP_LABEL,
34
38
  THREAD_NAME_LABEL,
35
39
  THREAD_ID_LABEL,
40
+ OS_THREAD_ID_LABEL,
36
41
  threadNamePrefix,
37
42
  eventLoopThreadName,
43
+ getNonJSThreadsLabels,
38
44
  getThreadLabels: cacheThreadLabels()
39
45
  }
@@ -14,9 +14,12 @@ class NativeSpaceProfiler {
14
14
  this._stackDepth = options.stackDepth || 64
15
15
  this._pprof = undefined
16
16
  this._oomMonitoring = options.oomMonitoring || {}
17
+ this._started = false
17
18
  }
18
19
 
19
20
  start ({ mapper, nearOOMCallback } = {}) {
21
+ if (this._started) return
22
+
20
23
  this._mapper = mapper
21
24
  this._pprof = require('@datadog/pprof')
22
25
  this._pprof.heap.start(this._samplingInterval, this._stackDepth)
@@ -31,10 +34,16 @@ class NativeSpaceProfiler {
31
34
  strategiesToCallbackMode(strategies, this._pprof.heap.CallbackMode)
32
35
  )
33
36
  }
37
+
38
+ this._started = true
34
39
  }
35
40
 
36
- profile () {
37
- return this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
41
+ profile (restart) {
42
+ const profile = this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
43
+ if (!restart) {
44
+ this.stop()
45
+ }
46
+ return profile
38
47
  }
39
48
 
40
49
  encode (profile) {
@@ -42,7 +51,13 @@ class NativeSpaceProfiler {
42
51
  }
43
52
 
44
53
  stop () {
54
+ if (!this._started) return
45
55
  this._pprof.heap.stop()
56
+ this._started = false
57
+ }
58
+
59
+ isStarted () {
60
+ return this._started
46
61
  }
47
62
  }
48
63