dd-trace 4.52.0 → 4.53.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 (144) hide show
  1. package/LICENSE-3rdparty.csv +8 -2
  2. package/ci/init.js +16 -0
  3. package/index.d.ts +31 -13
  4. package/init.js +4 -68
  5. package/loader-hook.mjs +4 -0
  6. package/package.json +16 -11
  7. package/packages/datadog-core/src/storage.js +39 -2
  8. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  9. package/packages/datadog-instrumentations/src/cucumber.js +29 -3
  10. package/packages/datadog-instrumentations/src/express.js +38 -4
  11. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +3 -3
  12. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -1
  13. package/packages/datadog-instrumentations/src/helpers/register.js +3 -4
  14. package/packages/datadog-instrumentations/src/http/client.js +1 -1
  15. package/packages/datadog-instrumentations/src/jest.js +27 -8
  16. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  17. package/packages/datadog-instrumentations/src/mysql2.js +13 -8
  18. package/packages/datadog-instrumentations/src/next.js +7 -4
  19. package/packages/datadog-instrumentations/src/passport-http.js +2 -14
  20. package/packages/datadog-instrumentations/src/passport-local.js +2 -14
  21. package/packages/datadog-instrumentations/src/passport-utils.js +43 -19
  22. package/packages/datadog-instrumentations/src/pg.js +6 -6
  23. package/packages/datadog-instrumentations/src/playwright.js +17 -4
  24. package/packages/datadog-instrumentations/src/router.js +97 -1
  25. package/packages/datadog-instrumentations/src/sequelize.js +9 -4
  26. package/packages/datadog-instrumentations/src/url.js +4 -0
  27. package/packages/datadog-instrumentations/src/vitest.js +27 -2
  28. package/packages/datadog-plugin-avsc/src/schema_iterator.js +8 -3
  29. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +154 -0
  30. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -1
  31. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  32. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +1 -1
  33. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -1
  34. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  35. package/packages/datadog-plugin-aws-sdk/src/util.js +92 -0
  36. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -1
  37. package/packages/datadog-plugin-cucumber/src/index.js +39 -4
  38. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +3 -3
  39. package/packages/datadog-plugin-grpc/src/client.js +2 -2
  40. package/packages/datadog-plugin-grpc/src/util.js +1 -1
  41. package/packages/datadog-plugin-jest/src/index.js +39 -4
  42. package/packages/datadog-plugin-mocha/src/index.js +36 -2
  43. package/packages/datadog-plugin-oracledb/src/index.js +1 -1
  44. package/packages/datadog-plugin-vitest/src/index.js +34 -2
  45. package/packages/datadog-shimmer/src/shimmer.js +8 -4
  46. package/packages/dd-trace/src/appsec/addresses.js +3 -0
  47. package/packages/dd-trace/src/appsec/blocked_templates.js +1 -1
  48. package/packages/dd-trace/src/appsec/channels.js +1 -0
  49. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +4 -0
  50. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
  51. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +1 -1
  52. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +1 -1
  53. package/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js +10 -3
  54. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +4 -0
  55. package/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +4 -0
  56. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +6 -19
  57. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +3 -3
  58. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +64 -3
  59. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +2 -1
  60. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +2 -2
  61. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  62. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +32 -37
  63. package/packages/dd-trace/src/appsec/index.js +16 -10
  64. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +1 -0
  65. package/packages/dd-trace/src/appsec/remote_config/index.js +25 -1
  66. package/packages/dd-trace/src/appsec/reporter.js +3 -1
  67. package/packages/dd-trace/src/appsec/sdk/track_event.js +32 -19
  68. package/packages/dd-trace/src/appsec/telemetry.js +10 -0
  69. package/packages/dd-trace/src/appsec/user_tracking.js +168 -0
  70. package/packages/dd-trace/src/azure_metadata.js +4 -4
  71. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +5 -4
  72. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +39 -3
  73. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  74. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  75. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  76. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  77. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +29 -9
  78. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  79. package/packages/dd-trace/src/config.js +24 -32
  80. package/packages/dd-trace/src/constants.js +1 -0
  81. package/packages/dd-trace/src/crashtracking/crashtracker.js +3 -2
  82. package/packages/dd-trace/src/datastreams/processor.js +4 -6
  83. package/packages/dd-trace/src/datastreams/writer.js +6 -5
  84. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +80 -0
  85. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -1
  86. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +6 -0
  87. package/packages/dd-trace/src/debugger/devtools_client/index.js +63 -8
  88. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +10 -67
  89. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  90. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -1
  91. package/packages/dd-trace/src/debugger/devtools_client/status.js +4 -4
  92. package/packages/dd-trace/src/debugger/index.js +14 -10
  93. package/packages/dd-trace/src/dogstatsd.js +2 -2
  94. package/packages/dd-trace/src/encode/0.4.js +23 -78
  95. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +0 -32
  96. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +1 -2
  97. package/packages/dd-trace/src/encode/span-stats.js +0 -30
  98. package/packages/dd-trace/src/exporters/agent/writer.js +3 -3
  99. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  100. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -1
  101. package/packages/dd-trace/src/flare/index.js +1 -1
  102. package/packages/dd-trace/src/guardrails/index.js +64 -0
  103. package/packages/dd-trace/src/guardrails/log.js +32 -0
  104. package/packages/dd-trace/src/guardrails/telemetry.js +78 -0
  105. package/packages/dd-trace/src/guardrails/util.js +10 -0
  106. package/packages/dd-trace/src/lambda/runtime/ritm.js +2 -2
  107. package/packages/dd-trace/src/llmobs/storage.js +2 -3
  108. package/packages/dd-trace/src/llmobs/writers/base.js +2 -2
  109. package/packages/dd-trace/src/{encode → msgpack}/chunk.js +8 -5
  110. package/packages/dd-trace/src/msgpack/encoder.js +309 -0
  111. package/packages/dd-trace/src/msgpack/index.js +6 -0
  112. package/packages/dd-trace/src/opentelemetry/context_manager.js +2 -2
  113. package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -9
  114. package/packages/dd-trace/src/opentracing/span.js +1 -1
  115. package/packages/dd-trace/src/opentracing/tracer.js +2 -2
  116. package/packages/dd-trace/src/plugin_manager.js +4 -2
  117. package/packages/dd-trace/src/plugins/ci_plugin.js +47 -4
  118. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  119. package/packages/dd-trace/src/plugins/tracing.js +1 -1
  120. package/packages/dd-trace/src/plugins/util/git.js +7 -7
  121. package/packages/dd-trace/src/plugins/util/test.js +36 -3
  122. package/packages/dd-trace/src/plugins/util/web.js +2 -2
  123. package/packages/dd-trace/src/profiling/config.js +3 -0
  124. package/packages/dd-trace/src/profiling/exporters/agent.js +9 -68
  125. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +76 -0
  126. package/packages/dd-trace/src/profiling/exporters/file.js +8 -4
  127. package/packages/dd-trace/src/profiling/profiler.js +62 -10
  128. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +22 -12
  129. package/packages/dd-trace/src/profiling/profilers/events.js +47 -8
  130. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -17
  131. package/packages/dd-trace/src/profiling/webspan-utils.js +23 -0
  132. package/packages/dd-trace/src/proxy.js +7 -2
  133. package/packages/dd-trace/src/runtime_metrics.js +107 -4
  134. package/packages/dd-trace/src/serverless.js +1 -1
  135. package/packages/dd-trace/src/span_processor.js +10 -10
  136. package/packages/dd-trace/src/tagger.js +1 -1
  137. package/packages/dd-trace/src/telemetry/index.js +1 -0
  138. package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
  139. package/packages/dd-trace/src/telemetry/logs/log-collector.js +10 -2
  140. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  141. package/packages/dd-trace/src/util.js +5 -16
  142. package/packages/datadog-instrumentations/src/qs.js +0 -24
  143. package/packages/dd-trace/src/appsec/passport.js +0 -110
  144. package/packages/dd-trace/src/telemetry/init-telemetry.js +0 -75
@@ -106,6 +106,13 @@ const TEST_LEVEL_EVENT_TYPES = [
106
106
  'test_session_end'
107
107
  ]
108
108
 
109
+ // Dynamic instrumentation - Test optimization integration tags
110
+ const DI_ERROR_DEBUG_INFO_CAPTURED = 'error.debug_info_captured'
111
+ // TODO: for the moment we'll only use a single snapshot id, so `0` is hardcoded
112
+ const DI_DEBUG_ERROR_SNAPSHOT_ID = '_dd.debug.error.0.snapshot_id'
113
+ const DI_DEBUG_ERROR_FILE = '_dd.debug.error.0.file'
114
+ const DI_DEBUG_ERROR_LINE = '_dd.debug.error.0.line'
115
+
109
116
  module.exports = {
110
117
  TEST_CODE_OWNERS,
111
118
  TEST_SESSION_NAME,
@@ -181,7 +188,12 @@ module.exports = {
181
188
  TEST_BROWSER_VERSION,
182
189
  getTestSessionName,
183
190
  TEST_LEVEL_EVENT_TYPES,
184
- getNumFromKnownTests
191
+ getNumFromKnownTests,
192
+ getFileAndLineNumberFromError,
193
+ DI_ERROR_DEBUG_INFO_CAPTURED,
194
+ DI_DEBUG_ERROR_SNAPSHOT_ID,
195
+ DI_DEBUG_ERROR_FILE,
196
+ DI_DEBUG_ERROR_LINE
185
197
  }
186
198
 
187
199
  // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
@@ -206,13 +218,13 @@ function removeInvalidMetadata (metadata) {
206
218
  return Object.keys(metadata).reduce((filteredTags, tag) => {
207
219
  if (tag === GIT_REPOSITORY_URL) {
208
220
  if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
209
- log.error(`Repository URL is not a valid repository URL: ${metadata[GIT_REPOSITORY_URL]}.`)
221
+ log.error('Repository URL is not a valid repository URL: %s.', metadata[GIT_REPOSITORY_URL])
210
222
  return filteredTags
211
223
  }
212
224
  }
213
225
  if (tag === GIT_COMMIT_SHA) {
214
226
  if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
215
- log.error(`Git commit SHA must be a full-length git SHA: ${metadata[GIT_COMMIT_SHA]}.`)
227
+ log.error('Git commit SHA must be a full-length git SHA: %s.', metadata[GIT_COMMIT_SHA])
216
228
  return filteredTags
217
229
  }
218
230
  }
@@ -637,3 +649,24 @@ function getNumFromKnownTests (knownTests) {
637
649
 
638
650
  return totalNumTests
639
651
  }
652
+
653
+ function getFileAndLineNumberFromError (error) {
654
+ // Split the stack trace into individual lines
655
+ const stackLines = error.stack.split('\n')
656
+
657
+ // The top frame is usually the second line
658
+ const topFrame = stackLines[1]
659
+
660
+ // Regular expression to match the file path, line number, and column number
661
+ const regex = /\s*at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/
662
+ const match = topFrame.match(regex)
663
+
664
+ if (match) {
665
+ const filePath = match[1]
666
+ const lineNumber = Number(match[2])
667
+ const columnNumber = Number(match[3])
668
+
669
+ return [filePath, lineNumber, columnNumber]
670
+ }
671
+ return []
672
+ }
@@ -546,7 +546,7 @@ function getHeadersToRecord (config) {
546
546
  .map(h => h.split(':'))
547
547
  .map(([key, tag]) => [key.toLowerCase(), tag])
548
548
  } catch (err) {
549
- log.error(err)
549
+ log.error('Web plugin error getting headers', err)
550
550
  }
551
551
  } else if (config.hasOwnProperty('headers')) {
552
552
  log.error('Expected `headers` to be an array of strings.')
@@ -595,7 +595,7 @@ function getQsObfuscator (config) {
595
595
  try {
596
596
  return new RegExp(obfuscator, 'gi')
597
597
  } catch (err) {
598
- log.error(err)
598
+ log.error('Web plugin error getting qs obfuscator', err)
599
599
  }
600
600
  }
601
601
 
@@ -21,6 +21,7 @@ class Config {
21
21
  const {
22
22
  DD_AGENT_HOST,
23
23
  DD_ENV,
24
+ DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
24
25
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
25
26
  DD_PROFILING_CPU_ENABLED,
26
27
  DD_PROFILING_DEBUG_SOURCE_MAPS,
@@ -175,6 +176,8 @@ class Config {
175
176
  DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, samplingContextsAvailable))
176
177
  logExperimentalVarDeprecation('TIMELINE_ENABLED')
177
178
  checkOptionWithSamplingContextAllowed(this.timelineEnabled, 'Timeline view')
179
+ this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled,
180
+ DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true))
178
181
 
179
182
  this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
180
183
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
@@ -3,13 +3,13 @@
3
3
  const retry = require('retry')
4
4
  const { request: httpRequest } = require('http')
5
5
  const { request: httpsRequest } = require('https')
6
+ const { EventSerializer } = require('./event_serializer')
6
7
 
7
8
  // TODO: avoid using dd-trace internals. Make this a separate module?
8
9
  const docker = require('../../exporters/common/docker')
9
10
  const FormData = require('../../exporters/common/form-data')
10
11
  const { storage } = require('../../../../datadog-core')
11
12
  const version = require('../../../../../package.json').version
12
- const os = require('os')
13
13
  const { urlToHttpOptions } = require('url')
14
14
  const perf = require('perf_hooks').performance
15
15
 
@@ -89,8 +89,10 @@ function computeRetries (uploadTimeout) {
89
89
  return [tries, Math.floor(uploadTimeout)]
90
90
  }
91
91
 
92
- class AgentExporter {
93
- constructor ({ url, logger, uploadTimeout, env, host, service, version, libraryInjected, activation } = {}) {
92
+ class AgentExporter extends EventSerializer {
93
+ constructor (config = {}) {
94
+ super(config)
95
+ const { url, logger, uploadTimeout } = config
94
96
  this._url = url
95
97
  this._logger = logger
96
98
 
@@ -98,74 +100,13 @@ class AgentExporter {
98
100
 
99
101
  this._backoffTime = backoffTime
100
102
  this._backoffTries = backoffTries
101
- this._env = env
102
- this._host = host
103
- this._service = service
104
- this._appVersion = version
105
- this._libraryInjected = !!libraryInjected
106
- this._activation = activation || 'unknown'
107
103
  }
108
104
 
109
- export ({ profiles, start, end, tags }) {
105
+ export (exportSpec) {
106
+ const { profiles } = exportSpec
110
107
  const fields = []
111
108
 
112
- function typeToFile (type) {
113
- return `${type}.pprof`
114
- }
115
-
116
- const event = JSON.stringify({
117
- attachments: Object.keys(profiles).map(typeToFile),
118
- start: start.toISOString(),
119
- end: end.toISOString(),
120
- family: 'node',
121
- version: '4',
122
- tags_profiler: [
123
- 'language:javascript',
124
- 'runtime:nodejs',
125
- `runtime_arch:${process.arch}`,
126
- `runtime_os:${process.platform}`,
127
- `runtime_version:${process.version}`,
128
- `process_id:${process.pid}`,
129
- `profiler_version:${version}`,
130
- 'format:pprof',
131
- ...Object.entries(tags).map(([key, value]) => `${key}:${value}`)
132
- ].join(','),
133
- info: {
134
- application: {
135
- env: this._env,
136
- service: this._service,
137
- start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(),
138
- version: this._appVersion
139
- },
140
- platform: {
141
- hostname: this._host,
142
- kernel_name: os.type(),
143
- kernel_release: os.release(),
144
- kernel_version: os.version()
145
- },
146
- profiler: {
147
- activation: this._activation,
148
- ssi: {
149
- mechanism: this._libraryInjected ? 'injected_agent' : 'none'
150
- },
151
- version
152
- },
153
- runtime: {
154
- // Using `nodejs` for consistency with the existing `runtime` tag.
155
- // Note that the event `family` property uses `node`, as that's what's
156
- // proscribed by the Intake API, but that's an internal enum and is
157
- // not customer visible.
158
- engine: 'nodejs',
159
- // strip off leading 'v'. This makes the format consistent with other
160
- // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag.
161
- // We'll keep it like this as we want cross-engine consistency. We
162
- // also aren't changing the format of the existing tag as we don't want
163
- // to break it.
164
- version: process.version.substring(1)
165
- }
166
- }
167
- })
168
-
109
+ const event = this.getEventJSON(exportSpec)
169
110
  fields.push(['event', event, {
170
111
  filename: 'event.json',
171
112
  contentType: 'application/json'
@@ -181,7 +122,7 @@ class AgentExporter {
181
122
  return `Adding ${type} profile to agent export: ` + bytes
182
123
  })
183
124
 
184
- const filename = typeToFile(type)
125
+ const filename = this.typeToFile(type)
185
126
  fields.push([filename, buffer, {
186
127
  filename,
187
128
  contentType: 'application/octet-stream'
@@ -0,0 +1,76 @@
1
+ const os = require('os')
2
+ const perf = require('perf_hooks').performance
3
+ const version = require('../../../../../package.json').version
4
+
5
+ class EventSerializer {
6
+ constructor ({ env, host, service, version, libraryInjected, activation } = {}) {
7
+ this._env = env
8
+ this._host = host
9
+ this._service = service
10
+ this._appVersion = version
11
+ this._libraryInjected = !!libraryInjected
12
+ this._activation = activation || 'unknown'
13
+ }
14
+
15
+ typeToFile (type) {
16
+ return `${type}.pprof`
17
+ }
18
+
19
+ getEventJSON ({ profiles, start, end, tags = {}, endpointCounts }) {
20
+ return JSON.stringify({
21
+ attachments: Object.keys(profiles).map(t => this.typeToFile(t)),
22
+ start: start.toISOString(),
23
+ end: end.toISOString(),
24
+ family: 'node',
25
+ version: '4',
26
+ tags_profiler: [
27
+ 'language:javascript',
28
+ 'runtime:nodejs',
29
+ `runtime_arch:${process.arch}`,
30
+ `runtime_os:${process.platform}`,
31
+ `runtime_version:${process.version}`,
32
+ `process_id:${process.pid}`,
33
+ `profiler_version:${version}`,
34
+ 'format:pprof',
35
+ ...Object.entries(tags).map(([key, value]) => `${key}:${value}`)
36
+ ].join(','),
37
+ endpoint_counts: endpointCounts,
38
+ info: {
39
+ application: {
40
+ env: this._env,
41
+ service: this._service,
42
+ start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(),
43
+ version: this._appVersion
44
+ },
45
+ platform: {
46
+ hostname: this._host,
47
+ kernel_name: os.type(),
48
+ kernel_release: os.release(),
49
+ kernel_version: os.version()
50
+ },
51
+ profiler: {
52
+ activation: this._activation,
53
+ ssi: {
54
+ mechanism: this._libraryInjected ? 'injected_agent' : 'none'
55
+ },
56
+ version
57
+ },
58
+ runtime: {
59
+ // Using `nodejs` for consistency with the existing `runtime` tag.
60
+ // Note that the event `family` property uses `node`, as that's what's
61
+ // proscribed by the Intake API, but that's an internal enum and is
62
+ // not customer visible.
63
+ engine: 'nodejs',
64
+ // strip off leading 'v'. This makes the format consistent with other
65
+ // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag.
66
+ // We'll keep it like this as we want cross-engine consistency. We
67
+ // also aren't changing the format of the existing tag as we don't want
68
+ // to break it.
69
+ version: process.version.substring(1)
70
+ }
71
+ }
72
+ })
73
+ }
74
+ }
75
+
76
+ module.exports = { EventSerializer }
@@ -4,6 +4,7 @@ const fs = require('fs')
4
4
  const { promisify } = require('util')
5
5
  const { threadId } = require('worker_threads')
6
6
  const writeFile = promisify(fs.writeFile)
7
+ const { EventSerializer } = require('./event_serializer')
7
8
 
8
9
  function formatDateTime (t) {
9
10
  const pad = (n) => String(n).padStart(2, '0')
@@ -11,18 +12,21 @@ function formatDateTime (t) {
11
12
  `T${pad(t.getUTCHours())}${pad(t.getUTCMinutes())}${pad(t.getUTCSeconds())}Z`
12
13
  }
13
14
 
14
- class FileExporter {
15
- constructor ({ pprofPrefix } = {}) {
15
+ class FileExporter extends EventSerializer {
16
+ constructor (config = {}) {
17
+ super(config)
18
+ const { pprofPrefix } = config
16
19
  this._pprofPrefix = pprofPrefix || ''
17
20
  }
18
21
 
19
- export ({ profiles, end }) {
22
+ export (exportSpec) {
23
+ const { profiles, end } = exportSpec
20
24
  const types = Object.keys(profiles)
21
25
  const dateStr = formatDateTime(end)
22
26
  const tasks = types.map(type => {
23
27
  return writeFile(`${this._pprofPrefix}${type}_worker_${threadId}_${dateStr}.pprof`, profiles[type])
24
28
  })
25
-
29
+ tasks.push(writeFile(`event_worker_${threadId}_${dateStr}.json`, this.getEventJSON(exportSpec)))
26
30
  return Promise.all(tasks)
27
31
  }
28
32
  }
@@ -4,9 +4,11 @@ const { EventEmitter } = require('events')
4
4
  const { Config } = require('./config')
5
5
  const { snapshotKinds } = require('./constants')
6
6
  const { threadNamePrefix } = require('./profilers/shared')
7
+ const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('./webspan-utils')
7
8
  const dc = require('dc-polyfill')
8
9
 
9
10
  const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted')
11
+ const spanFinishedChannel = dc.channel('dd-trace:span:finish')
10
12
 
11
13
  function maybeSourceMap (sourceMap, SourceMapper, debug) {
12
14
  if (!sourceMap) return
@@ -21,6 +23,20 @@ function logError (logger, err) {
21
23
  }
22
24
  }
23
25
 
26
+ function findWebSpan (startedSpans, spanId) {
27
+ for (let i = startedSpans.length; --i >= 0;) {
28
+ const ispan = startedSpans[i]
29
+ const context = ispan.context()
30
+ if (context._spanId === spanId) {
31
+ if (isWebServerSpan(context._tags)) {
32
+ return true
33
+ }
34
+ spanId = context._parentId
35
+ }
36
+ }
37
+ return false
38
+ }
39
+
24
40
  class Profiler extends EventEmitter {
25
41
  constructor () {
26
42
  super()
@@ -30,6 +46,7 @@ class Profiler extends EventEmitter {
30
46
  this._timer = undefined
31
47
  this._lastStart = undefined
32
48
  this._timeoutInterval = undefined
49
+ this.endpointCounts = new Map()
33
50
  }
34
51
 
35
52
  start (options) {
@@ -82,6 +99,11 @@ class Profiler extends EventEmitter {
82
99
  this._logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`)
83
100
  }
84
101
 
102
+ if (config.endpointCollectionEnabled) {
103
+ this._spanFinishListener = this._onSpanFinish.bind(this)
104
+ spanFinishedChannel.subscribe(this._spanFinishListener)
105
+ }
106
+
85
107
  this._capture(this._timeoutInterval, start)
86
108
  return true
87
109
  } catch (e) {
@@ -117,6 +139,11 @@ class Profiler extends EventEmitter {
117
139
 
118
140
  this._enabled = false
119
141
 
142
+ if (this._spanFinishListener !== undefined) {
143
+ spanFinishedChannel.unsubscribe(this._spanFinishListener)
144
+ this._spanFinishListener = undefined
145
+ }
146
+
120
147
  for (const profiler of this._config.profilers) {
121
148
  profiler.stop()
122
149
  this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
@@ -137,6 +164,26 @@ class Profiler extends EventEmitter {
137
164
  }
138
165
  }
139
166
 
167
+ _onSpanFinish (span) {
168
+ const context = span.context()
169
+ const tags = context._tags
170
+ if (!isWebServerSpan(tags)) return
171
+
172
+ const endpointName = endpointNameFromTags(tags)
173
+ if (!endpointName) return
174
+
175
+ // Make sure this is the outermost web span, just in case so we don't overcount
176
+ if (findWebSpan(getStartedSpans(context), context._parentId)) return
177
+
178
+ let counter = this.endpointCounts.get(endpointName)
179
+ if (counter === undefined) {
180
+ counter = { count: 1 }
181
+ this.endpointCounts.set(endpointName, counter)
182
+ } else {
183
+ counter.count++
184
+ }
185
+ }
186
+
140
187
  async _collect (snapshotKind, restart = true) {
141
188
  if (!this._enabled) return
142
189
 
@@ -194,18 +241,23 @@ class Profiler extends EventEmitter {
194
241
 
195
242
  _submit (profiles, start, end, snapshotKind) {
196
243
  const { tags } = this._config
197
- const tasks = []
198
244
 
199
- tags.snapshot = snapshotKind
200
- for (const exporter of this._config.exporters) {
201
- const task = exporter.export({ profiles, start, end, tags })
202
- .catch(err => {
203
- if (this._logger) {
204
- this._logger.warn(err)
205
- }
206
- })
207
- tasks.push(task)
245
+ // Flatten endpoint counts
246
+ const endpointCounts = {}
247
+ for (const [endpoint, { count }] of this.endpointCounts) {
248
+ endpointCounts[endpoint] = count
208
249
  }
250
+ this.endpointCounts.clear()
251
+
252
+ tags.snapshot = snapshotKind
253
+ const exportSpec = { profiles, start, end, tags, endpointCounts }
254
+ const tasks = this._config.exporters.map(exporter =>
255
+ exporter.export(exportSpec).catch(err => {
256
+ if (this._logger) {
257
+ this._logger.warn(err)
258
+ }
259
+ })
260
+ )
209
261
 
210
262
  return Promise.all(tasks)
211
263
  }
@@ -1,14 +1,15 @@
1
- const { AsyncLocalStorage } = require('async_hooks')
1
+ const { storage } = require('../../../../../datadog-core')
2
2
  const TracingPlugin = require('../../../plugins/tracing')
3
3
  const { performance } = require('perf_hooks')
4
4
 
5
5
  // We are leveraging the TracingPlugin class for its functionality to bind
6
6
  // start/error/finish methods to the appropriate diagnostic channels.
7
7
  class EventPlugin extends TracingPlugin {
8
- constructor (eventHandler) {
8
+ constructor (eventHandler, eventFilter) {
9
9
  super()
10
10
  this.eventHandler = eventHandler
11
- this.store = new AsyncLocalStorage()
11
+ this.eventFilter = eventFilter
12
+ this.store = storage('profiling')
12
13
  this.entryType = this.constructor.entryType
13
14
  }
14
15
 
@@ -20,27 +21,36 @@ class EventPlugin extends TracingPlugin {
20
21
  }
21
22
 
22
23
  error () {
23
- this.store.getStore().error = true
24
+ const store = this.store.getStore()
25
+ if (store) {
26
+ store.error = true
27
+ }
24
28
  }
25
29
 
26
30
  finish () {
27
- const { startEvent, startTime, error } = this.store.getStore()
31
+ const store = this.store.getStore()
32
+ if (!store) return
33
+
34
+ const { startEvent, startTime, error } = store
28
35
  if (error) {
29
36
  return // don't emit perf events for failed operations
30
37
  }
31
38
  const duration = performance.now() - startTime
32
39
 
33
- const context = this.activeSpan?.context()
34
- const _ddSpanId = context?.toSpanId()
35
- const _ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || _ddSpanId
36
-
37
40
  const event = {
38
41
  entryType: this.entryType,
39
42
  startTime,
40
- duration,
41
- _ddSpanId,
42
- _ddRootSpanId
43
+ duration
43
44
  }
45
+
46
+ if (!this.eventFilter(event)) {
47
+ return
48
+ }
49
+
50
+ const context = this.activeSpan?.context()
51
+ event._ddSpanId = context?.toSpanId()
52
+ event._ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || event._ddSpanId
53
+
44
54
  this.eventHandler(this.extendEvent(event, startEvent))
45
55
  }
46
56
  }
@@ -254,10 +254,10 @@ class NodeApiEventSource {
254
254
  }
255
255
 
256
256
  class DatadogInstrumentationEventSource {
257
- constructor (eventHandler) {
257
+ constructor (eventHandler, eventFilter) {
258
258
  this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => {
259
259
  const Plugin = require(`./event_plugins/${m}`)
260
- return new Plugin(eventHandler)
260
+ return new Plugin(eventHandler, eventFilter)
261
261
  })
262
262
 
263
263
  this.started = false
@@ -292,29 +292,68 @@ class CompositeEventSource {
292
292
  }
293
293
  }
294
294
 
295
+ function createPossionProcessSamplingFilter (samplingIntervalMillis) {
296
+ let nextSamplingInstant = performance.now()
297
+ let currentSamplingInstant = 0
298
+ setNextSamplingInstant()
299
+
300
+ return event => {
301
+ const endTime = event.startTime + event.duration
302
+ while (endTime >= nextSamplingInstant) {
303
+ setNextSamplingInstant()
304
+ }
305
+ // An event is sampled if it started before, and ended on or after a sampling instant. The above
306
+ // while loop will ensure that the ending invariant is always true for the current sampling
307
+ // instant so we don't have to test for it below. Across calls, the invariant also holds as long
308
+ // as the events arrive in endTime order. This is true for events coming from
309
+ // DatadogInstrumentationEventSource; they will be ordered by endTime by virtue of this method
310
+ // being invoked synchronously with the plugins' finish() handler which evaluates
311
+ // performance.now(). OTOH, events coming from NodeAPIEventSource (GC in typical setup) might be
312
+ // somewhat delayed as they are queued by Node, so they can arrive out of order with regard to
313
+ // events coming from the non-queued source. By omitting the endTime check, we will pass through
314
+ // some short events that started and ended before the current sampling instant. OTOH, if we
315
+ // were to check for this.currentSamplingInstant <= endTime, we would discard some long events
316
+ // that also ended before the current sampling instant. We'd rather err on the side of including
317
+ // some short events than excluding some long events.
318
+ return event.startTime < currentSamplingInstant
319
+ }
320
+
321
+ function setNextSamplingInstant () {
322
+ currentSamplingInstant = nextSamplingInstant
323
+ nextSamplingInstant -= Math.log(1 - Math.random()) * samplingIntervalMillis
324
+ }
325
+ }
326
+
295
327
  /**
296
328
  * This class generates pprof files with timeline events. It combines an event
297
- * source with an event serializer.
329
+ * source with a sampling event filter and an event serializer.
298
330
  */
299
331
  class EventsProfiler {
300
332
  constructor (options = {}) {
301
333
  this.type = 'events'
302
334
  this.eventSerializer = new EventSerializer()
303
335
 
304
- const eventHandler = event => {
305
- this.eventSerializer.addEvent(event)
336
+ const eventHandler = event => this.eventSerializer.addEvent(event)
337
+ const eventFilter = options.timelineSamplingEnabled
338
+ // options.samplingInterval comes in microseconds, we need millis
339
+ ? createPossionProcessSamplingFilter((options.samplingInterval ?? 1e6 / 99) / 1000)
340
+ : _ => true
341
+ const filteringEventHandler = event => {
342
+ if (eventFilter(event)) {
343
+ eventHandler(event)
344
+ }
306
345
  }
307
346
 
308
347
  if (options.codeHotspotsEnabled) {
309
348
  // Use Datadog instrumentation to collect events with span IDs. Still use
310
349
  // Node API for GC events.
311
350
  this.eventSource = new CompositeEventSource([
312
- new DatadogInstrumentationEventSource(eventHandler),
313
- new NodeApiEventSource(eventHandler, ['gc'])
351
+ new DatadogInstrumentationEventSource(eventHandler, eventFilter),
352
+ new NodeApiEventSource(filteringEventHandler, ['gc'])
314
353
  ])
315
354
  } else {
316
355
  // Use Node API instrumentation to collect events without span IDs
317
- this.eventSource = new NodeApiEventSource(eventHandler)
356
+ this.eventSource = new NodeApiEventSource(filteringEventHandler)
318
357
  }
319
358
  }
320
359
 
@@ -3,8 +3,6 @@
3
3
  const { storage } = require('../../../../datadog-core')
4
4
 
5
5
  const dc = require('dc-polyfill')
6
- const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags')
7
- const { WEB } = require('../../../../../ext/types')
8
6
  const runtimeMetrics = require('../../runtime_metrics')
9
7
  const telemetryMetrics = require('../../telemetry/metrics')
10
8
  const {
@@ -15,6 +13,8 @@ const {
15
13
  getThreadLabels
16
14
  } = require('./shared')
17
15
 
16
+ const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
17
+
18
18
  const beforeCh = dc.channel('dd-trace:storage:before')
19
19
  const enterCh = dc.channel('dd-trace:storage:enter')
20
20
  const spanFinishCh = dc.channel('dd-trace:span:finish')
@@ -29,21 +29,6 @@ function getActiveSpan () {
29
29
  return store && store.span
30
30
  }
31
31
 
32
- function getStartedSpans (context) {
33
- return context._trace.started
34
- }
35
-
36
- function isWebServerSpan (tags) {
37
- return tags[SPAN_TYPE] === WEB
38
- }
39
-
40
- function endpointNameFromTags (tags) {
41
- return tags[RESOURCE_NAME] || [
42
- tags[HTTP_METHOD],
43
- tags[HTTP_ROUTE]
44
- ].filter(v => v).join(' ')
45
- }
46
-
47
32
  let channelsActivated = false
48
33
  function ensureChannelsActivated () {
49
34
  if (channelsActivated) return
@@ -0,0 +1,23 @@
1
+ const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../ext/tags')
2
+ const { WEB } = require('../../../../ext/types')
3
+
4
+ function isWebServerSpan (tags) {
5
+ return tags[SPAN_TYPE] === WEB
6
+ }
7
+
8
+ function endpointNameFromTags (tags) {
9
+ return tags[RESOURCE_NAME] || [
10
+ tags[HTTP_METHOD],
11
+ tags[HTTP_ROUTE]
12
+ ].filter(v => v).join(' ')
13
+ }
14
+
15
+ function getStartedSpans (context) {
16
+ return context._trace.started
17
+ }
18
+
19
+ module.exports = {
20
+ isWebServerSpan,
21
+ endpointNameFromTags,
22
+ getStartedSpans
23
+ }