dd-trace 3.42.0 → 3.44.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 (68) hide show
  1. package/index.d.ts +5 -0
  2. package/package.json +4 -4
  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/child-process.js +4 -5
  6. package/packages/datadog-instrumentations/src/couchbase.js +5 -4
  7. package/packages/datadog-instrumentations/src/crypto.js +2 -1
  8. package/packages/datadog-instrumentations/src/dns.js +2 -1
  9. package/packages/datadog-instrumentations/src/graphql.js +18 -4
  10. package/packages/datadog-instrumentations/src/helpers/hooks.js +9 -2
  11. package/packages/datadog-instrumentations/src/helpers/instrument.js +8 -3
  12. package/packages/datadog-instrumentations/src/helpers/register.js +18 -2
  13. package/packages/datadog-instrumentations/src/http/client.js +4 -16
  14. package/packages/datadog-instrumentations/src/http/server.js +7 -4
  15. package/packages/datadog-instrumentations/src/http2/client.js +3 -1
  16. package/packages/datadog-instrumentations/src/http2/server.js +3 -1
  17. package/packages/datadog-instrumentations/src/jest.js +1 -1
  18. package/packages/datadog-instrumentations/src/net.js +10 -2
  19. package/packages/datadog-instrumentations/src/next.js +15 -5
  20. package/packages/datadog-instrumentations/src/rhea.js +15 -9
  21. package/packages/datadog-plugin-cucumber/src/index.js +34 -2
  22. package/packages/datadog-plugin-cypress/src/plugin.js +60 -8
  23. package/packages/datadog-plugin-graphql/src/resolve.js +26 -18
  24. package/packages/datadog-plugin-http/src/client.js +1 -1
  25. package/packages/datadog-plugin-jest/src/index.js +38 -4
  26. package/packages/datadog-plugin-mocha/src/index.js +32 -1
  27. package/packages/datadog-plugin-next/src/index.js +32 -6
  28. package/packages/datadog-plugin-playwright/src/index.js +17 -1
  29. package/packages/dd-trace/src/appsec/activation.js +29 -0
  30. package/packages/dd-trace/src/appsec/addresses.js +1 -0
  31. package/packages/dd-trace/src/appsec/api_security_sampler.js +48 -0
  32. package/packages/dd-trace/src/appsec/blocked_templates.js +4 -1
  33. package/packages/dd-trace/src/appsec/blocking.js +95 -43
  34. package/packages/dd-trace/src/appsec/channels.js +4 -1
  35. package/packages/dd-trace/src/appsec/graphql.js +146 -0
  36. package/packages/dd-trace/src/appsec/index.js +29 -40
  37. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +6 -1
  38. package/packages/dd-trace/src/appsec/remote_config/index.js +40 -15
  39. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
  40. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +25 -13
  41. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +30 -1
  42. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +30 -1
  43. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +36 -4
  44. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +18 -1
  45. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +26 -1
  46. package/packages/dd-trace/src/ci-visibility/telemetry.js +130 -0
  47. package/packages/dd-trace/src/config.js +104 -58
  48. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +14 -1
  49. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +14 -0
  50. package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +4 -0
  51. package/packages/dd-trace/src/exporters/common/form-data.js +4 -0
  52. package/packages/dd-trace/src/opentracing/tracer.js +2 -2
  53. package/packages/dd-trace/src/plugins/ci_plugin.js +44 -8
  54. package/packages/dd-trace/src/plugins/index.js +5 -0
  55. package/packages/dd-trace/src/plugins/util/exec.js +23 -2
  56. package/packages/dd-trace/src/plugins/util/git.js +94 -19
  57. package/packages/dd-trace/src/plugins/util/user-provided-git.js +3 -2
  58. package/packages/dd-trace/src/priority_sampler.js +30 -38
  59. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -0
  60. package/packages/dd-trace/src/profiling/profiler.js +7 -6
  61. package/packages/dd-trace/src/profiling/profilers/events.js +18 -13
  62. package/packages/dd-trace/src/profiling/profilers/shared.js +34 -4
  63. package/packages/dd-trace/src/profiling/profilers/space.js +2 -1
  64. package/packages/dd-trace/src/profiling/profilers/wall.js +17 -12
  65. package/packages/dd-trace/src/proxy.js +4 -0
  66. package/packages/dd-trace/src/sampling_rule.js +130 -0
  67. package/packages/dd-trace/src/span_sampler.js +6 -64
  68. package/packages/dd-trace/src/telemetry/index.js +43 -5
@@ -1,10 +1,9 @@
1
- const { execFileSync } = require('child_process')
1
+ const cp = require('child_process')
2
2
  const os = require('os')
3
3
  const path = require('path')
4
4
  const fs = require('fs')
5
5
 
6
6
  const log = require('../../log')
7
- const { sanitizedExec } = require('./exec')
8
7
  const {
9
8
  GIT_COMMIT_SHA,
10
9
  GIT_BRANCH,
@@ -19,10 +18,46 @@ const {
19
18
  GIT_COMMIT_AUTHOR_NAME,
20
19
  CI_WORKSPACE_PATH
21
20
  } = require('./tags')
21
+ const {
22
+ incrementCountMetric,
23
+ distributionMetric,
24
+ TELEMETRY_GIT_COMMAND,
25
+ TELEMETRY_GIT_COMMAND_MS,
26
+ TELEMETRY_GIT_COMMAND_ERRORS
27
+ } = require('../../ci-visibility/telemetry')
22
28
  const { filterSensitiveInfoFromRepository } = require('./url')
23
29
 
24
30
  const GIT_REV_LIST_MAX_BUFFER = 8 * 1024 * 1024 // 8MB
25
31
 
32
+ function sanitizedExec (
33
+ cmd,
34
+ flags,
35
+ operationMetric,
36
+ durationMetric,
37
+ errorMetric
38
+ ) {
39
+ let startTime
40
+ if (operationMetric) {
41
+ incrementCountMetric(operationMetric.name, operationMetric.tags)
42
+ }
43
+ if (durationMetric) {
44
+ startTime = Date.now()
45
+ }
46
+ try {
47
+ const result = cp.execFileSync(cmd, flags, { stdio: 'pipe' }).toString().replace(/(\r\n|\n|\r)/gm, '')
48
+ if (durationMetric) {
49
+ distributionMetric(durationMetric.name, durationMetric.tags, Date.now() - startTime)
50
+ }
51
+ return result
52
+ } catch (e) {
53
+ if (errorMetric) {
54
+ incrementCountMetric(errorMetric.name, { ...errorMetric.tags, exitCode: e.status })
55
+ }
56
+ log.error(e)
57
+ return ''
58
+ }
59
+ }
60
+
26
61
  function isDirectory (path) {
27
62
  try {
28
63
  const stats = fs.statSync(path)
@@ -33,7 +68,13 @@ function isDirectory (path) {
33
68
  }
34
69
 
35
70
  function isShallowRepository () {
36
- return sanitizedExec('git', ['rev-parse', '--is-shallow-repository']) === 'true'
71
+ return sanitizedExec(
72
+ 'git',
73
+ ['rev-parse', '--is-shallow-repository'],
74
+ { name: TELEMETRY_GIT_COMMAND, tags: { command: 'check_shallow' } },
75
+ { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'check_shallow' } },
76
+ { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'check_shallow' } }
77
+ ) === 'true'
37
78
  }
38
79
 
39
80
  function getGitVersion () {
@@ -72,50 +113,76 @@ function unshallowRepository () {
72
113
  defaultRemoteName
73
114
  ]
74
115
 
116
+ incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'unshallow' })
117
+ const start = Date.now()
75
118
  try {
76
- execFileSync('git', [
119
+ cp.execFileSync('git', [
77
120
  ...baseGitOptions,
78
121
  revParseHead
79
122
  ], { stdio: 'pipe' })
80
- } catch (e) {
123
+ } catch (err) {
81
124
  // If the local HEAD is a commit that has not been pushed to the remote, the above command will fail.
82
- log.error(e)
125
+ log.error(err)
126
+ incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', exitCode: err.status })
83
127
  const upstreamRemote = sanitizedExec('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
84
128
  try {
85
- execFileSync('git', [
129
+ cp.execFileSync('git', [
86
130
  ...baseGitOptions,
87
131
  upstreamRemote
88
132
  ], { stdio: 'pipe' })
89
- } catch (e) {
133
+ } catch (err) {
90
134
  // If the CI is working on a detached HEAD or branch tracking hasn’t been set up, the above command will fail.
91
- log.error(e)
135
+ log.error(err)
136
+ incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', exitCode: err.status })
92
137
  // We use sanitizedExec here because if this last option fails, we'll give up.
93
- sanitizedExec('git', baseGitOptions)
138
+ sanitizedExec(
139
+ 'git',
140
+ baseGitOptions,
141
+ null,
142
+ null,
143
+ { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'unshallow' } } // we log the error in sanitizedExec
144
+ )
94
145
  }
95
146
  }
147
+ distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'unshallow' }, Date.now() - start)
96
148
  }
97
149
 
98
150
  function getRepositoryUrl () {
99
- return sanitizedExec('git', ['config', '--get', 'remote.origin.url'])
151
+ return sanitizedExec(
152
+ 'git',
153
+ ['config', '--get', 'remote.origin.url'],
154
+ { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_repository' } },
155
+ { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_repository' } },
156
+ { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'get_repository' } }
157
+ )
100
158
  }
101
159
 
102
160
  function getLatestCommits () {
161
+ incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'get_local_commits' })
162
+ const startTime = Date.now()
103
163
  try {
104
- return execFileSync('git', ['log', '--format=%H', '-n 1000', '--since="1 month ago"'], { stdio: 'pipe' })
164
+ const result = cp.execFileSync('git', ['log', '--format=%H', '-n 1000', '--since="1 month ago"'], { stdio: 'pipe' })
105
165
  .toString()
106
166
  .split('\n')
107
167
  .filter(commit => commit)
168
+ distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_local_commits' }, Date.now() - startTime)
169
+ return result
108
170
  } catch (err) {
109
171
  log.error(`Get latest commits failed: ${err.message}`)
172
+ incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_local_commits', errorType: err.status })
110
173
  return []
111
174
  }
112
175
  }
113
176
 
114
177
  function getCommitsRevList (commitsToExclude, commitsToInclude) {
178
+ let result = []
179
+
115
180
  const commitsToExcludeString = commitsToExclude.map(commit => `^${commit}`)
116
181
 
182
+ incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'get_objects' })
183
+ const startTime = Date.now()
117
184
  try {
118
- return execFileSync(
185
+ result = cp.execFileSync(
119
186
  'git',
120
187
  [
121
188
  'rev-list',
@@ -132,11 +199,14 @@ function getCommitsRevList (commitsToExclude, commitsToInclude) {
132
199
  .filter(commit => commit)
133
200
  } catch (err) {
134
201
  log.error(`Get commits to upload failed: ${err.message}`)
135
- return []
202
+ incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_objects', errorType: err.status })
136
203
  }
204
+ distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_objects' }, Date.now() - startTime)
205
+ return result
137
206
  }
138
207
 
139
208
  function generatePackFilesForCommits (commitsToUpload) {
209
+ let result = []
140
210
  const tmpFolder = os.tmpdir()
141
211
 
142
212
  if (!isDirectory(tmpFolder)) {
@@ -148,10 +218,12 @@ function generatePackFilesForCommits (commitsToUpload) {
148
218
  const temporaryPath = path.join(tmpFolder, randomPrefix)
149
219
  const cwdPath = path.join(process.cwd(), randomPrefix)
150
220
 
221
+ incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'pack_objects' })
222
+ const startTime = Date.now()
151
223
  // Generates pack files to upload and
152
224
  // returns the ordered list of packfiles' paths
153
225
  function execGitPackObjects (targetPath) {
154
- return execFileSync(
226
+ return cp.execFileSync(
155
227
  'git',
156
228
  [
157
229
  'pack-objects',
@@ -164,9 +236,10 @@ function generatePackFilesForCommits (commitsToUpload) {
164
236
  }
165
237
 
166
238
  try {
167
- return execGitPackObjects(temporaryPath)
239
+ result = execGitPackObjects(temporaryPath)
168
240
  } catch (err) {
169
241
  log.error(err)
242
+ incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', errorType: err.status })
170
243
  /**
171
244
  * The generation of pack files in the temporary folder (from `os.tmpdir()`)
172
245
  * sometimes fails in certain CI setups with the error message
@@ -180,13 +253,15 @@ function generatePackFilesForCommits (commitsToUpload) {
180
253
  * TODO: fix issue and remove workaround.
181
254
  */
182
255
  try {
183
- return execGitPackObjects(cwdPath)
256
+ result = execGitPackObjects(cwdPath)
184
257
  } catch (err) {
185
258
  log.error(err)
259
+ incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', errorType: err.status })
186
260
  }
187
-
188
- return []
189
261
  }
262
+ distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'pack_objects' }, Date.now() - startTime)
263
+
264
+ return result
190
265
  }
191
266
 
192
267
  // If there is ciMetadata, it takes precedence.
@@ -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) {
@@ -1,9 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const RateLimiter = require('./rate_limiter')
4
3
  const Sampler = require('./sampler')
5
- const ext = require('../../../ext')
6
4
  const { setSamplingRules } = require('./startup-log')
5
+ const SamplingRule = require('./sampling_rule')
7
6
 
8
7
  const {
9
8
  SAMPLING_MECHANISM_DEFAULT,
@@ -16,14 +15,21 @@ const {
16
15
  DECISION_MAKER_KEY
17
16
  } = require('./constants')
18
17
 
19
- const SERVICE_NAME = ext.tags.SERVICE_NAME
20
- const SAMPLING_PRIORITY = ext.tags.SAMPLING_PRIORITY
21
- const MANUAL_KEEP = ext.tags.MANUAL_KEEP
22
- const MANUAL_DROP = ext.tags.MANUAL_DROP
23
- const USER_REJECT = ext.priority.USER_REJECT
24
- const AUTO_REJECT = ext.priority.AUTO_REJECT
25
- const AUTO_KEEP = ext.priority.AUTO_KEEP
26
- const USER_KEEP = ext.priority.USER_KEEP
18
+ const {
19
+ tags: {
20
+ MANUAL_KEEP,
21
+ MANUAL_DROP,
22
+ SAMPLING_PRIORITY,
23
+ SERVICE_NAME
24
+ },
25
+ priority: {
26
+ AUTO_REJECT,
27
+ AUTO_KEEP,
28
+ USER_REJECT,
29
+ USER_KEEP
30
+ }
31
+ } = require('../../../ext')
32
+
27
33
  const DEFAULT_KEY = 'service:,env:'
28
34
 
29
35
  const defaultSampler = new Sampler(AUTO_KEEP)
@@ -36,8 +42,7 @@ class PrioritySampler {
36
42
 
37
43
  configure (env, { sampleRate, rateLimit = 100, rules = [] } = {}) {
38
44
  this._env = env
39
- this._rules = this._normalizeRules(rules, sampleRate)
40
- this._limiter = new RateLimiter(rateLimit)
45
+ this._rules = this._normalizeRules(rules, sampleRate, rateLimit)
41
46
 
42
47
  setSamplingRules(this._rules)
43
48
  }
@@ -104,7 +109,7 @@ class PrioritySampler {
104
109
 
105
110
  _getPriorityFromAuto (span) {
106
111
  const context = this._getContext(span)
107
- const rule = this._findRule(context)
112
+ const rule = this._findRule(span)
108
113
 
109
114
  return rule
110
115
  ? this._getPriorityByRule(context, rule)
@@ -131,15 +136,14 @@ class PrioritySampler {
131
136
  context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
132
137
  context._sampling.mechanism = SAMPLING_MECHANISM_RULE
133
138
 
134
- return rule.sampler.isSampled(context) && this._isSampledByRateLimit(context) ? USER_KEEP : USER_REJECT
135
- }
139
+ const sampled = rule.sample()
140
+ const priority = sampled ? USER_KEEP : USER_REJECT
136
141
 
137
- _isSampledByRateLimit (context) {
138
- const allowed = this._limiter.isAllowed()
139
-
140
- context._trace[SAMPLING_LIMIT_DECISION] = this._limiter.effectiveRate()
142
+ if (sampled) {
143
+ context._trace[SAMPLING_LIMIT_DECISION] = rule.effectiveRate
144
+ }
141
145
 
142
- return allowed
146
+ return priority
143
147
  }
144
148
 
145
149
  _getPriorityByAgent (context) {
@@ -172,33 +176,21 @@ class PrioritySampler {
172
176
  }
173
177
  }
174
178
 
175
- _normalizeRules (rules, sampleRate) {
179
+ _normalizeRules (rules, sampleRate, rateLimit) {
176
180
  rules = [].concat(rules || [])
177
181
 
178
182
  return rules
179
- .concat({ sampleRate })
183
+ .concat({ sampleRate, maxPerSecond: rateLimit })
180
184
  .map(rule => ({ ...rule, sampleRate: parseFloat(rule.sampleRate) }))
181
185
  .filter(rule => !isNaN(rule.sampleRate))
182
- .map(rule => ({ ...rule, sampler: new Sampler(rule.sampleRate) }))
186
+ .map(SamplingRule.from)
183
187
  }
184
188
 
185
- _findRule (context) {
186
- for (let i = 0, l = this._rules.length; i < l; i++) {
187
- if (this._matchRule(context, this._rules[i])) return this._rules[i]
189
+ _findRule (span) {
190
+ for (const rule of this._rules) {
191
+ if (rule.match(span)) return rule
188
192
  }
189
193
  }
190
-
191
- _matchRule (context, rule) {
192
- const name = context._name
193
- const service = context._tags['service.name']
194
-
195
- if (rule.name instanceof RegExp && !rule.name.test(name)) return false
196
- if (typeof rule.name === 'string' && rule.name !== name) return false
197
- if (rule.service instanceof RegExp && !rule.service.test(service)) return false
198
- if (typeof rule.service === 'string' && rule.service !== service) return false
199
-
200
- return true
201
- }
202
194
  }
203
195
 
204
196
  function hasOwn (object, prop) {
@@ -75,6 +75,7 @@ class AgentExporter {
75
75
  ['tags[]', 'language:javascript'],
76
76
  ['tags[]', 'runtime:nodejs'],
77
77
  ['tags[]', `runtime_version:${process.version}`],
78
+ ['tags[]', `process_id:${process.pid}`],
78
79
  ['tags[]', `profiler_version:${version}`],
79
80
  ['tags[]', 'format:pprof'],
80
81
  ...Object.entries(tags).map(([key, value]) => ['tags[]', `${key}:${value}`])
@@ -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
- const { performance, constants, PerformanceObserver } = require('node:perf_hooks')
2
- const { END_TIMESTAMP } = require('./shared')
1
+ const { performance, constants, PerformanceObserver } = require('perf_hooks')
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,
@@ -1,9 +1,39 @@
1
1
  'use strict'
2
2
 
3
- const { isMainThread, threadId } = require('node:worker_threads')
3
+ const { isMainThread, threadId } = require('worker_threads')
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
+ }
4
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) {