dd-trace 4.46.0 → 4.47.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 (89) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +20 -8
  3. package/package.json +9 -3
  4. package/packages/datadog-instrumentations/src/cucumber.js +290 -53
  5. package/packages/datadog-instrumentations/src/jest.js +3 -1
  6. package/packages/datadog-instrumentations/src/kafkajs.js +67 -31
  7. package/packages/datadog-instrumentations/src/microgateway-core.js +3 -1
  8. package/packages/datadog-instrumentations/src/mocha/main.js +139 -54
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -15
  10. package/packages/datadog-instrumentations/src/mocha/worker.js +29 -1
  11. package/packages/datadog-instrumentations/src/openai.js +4 -2
  12. package/packages/datadog-instrumentations/src/pg.js +59 -4
  13. package/packages/datadog-instrumentations/src/vitest.js +184 -9
  14. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +33 -0
  16. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  17. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  19. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  20. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +36 -6
  21. package/packages/datadog-plugin-cypress/src/support.js +4 -1
  22. package/packages/datadog-plugin-http/src/client.js +1 -42
  23. package/packages/datadog-plugin-http2/src/client.js +1 -26
  24. package/packages/datadog-plugin-jest/src/index.js +17 -1
  25. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +20 -0
  26. package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -2
  27. package/packages/datadog-plugin-kafkajs/src/index.js +3 -1
  28. package/packages/datadog-plugin-mocha/src/index.js +18 -0
  29. package/packages/datadog-plugin-openai/src/index.js +27 -18
  30. package/packages/datadog-plugin-playwright/src/index.js +9 -0
  31. package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
  32. package/packages/datadog-plugin-vitest/src/index.js +68 -3
  33. package/packages/dd-trace/src/appsec/addresses.js +3 -1
  34. package/packages/dd-trace/src/appsec/channels.js +4 -2
  35. package/packages/dd-trace/src/appsec/rasp/index.js +103 -0
  36. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +86 -0
  37. package/packages/dd-trace/src/appsec/rasp/ssrf.js +37 -0
  38. package/packages/dd-trace/src/appsec/rasp/utils.js +63 -0
  39. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
  40. package/packages/dd-trace/src/appsec/remote_config/index.js +16 -7
  41. package/packages/dd-trace/src/appsec/remote_config/manager.js +89 -51
  42. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +33 -14
  43. package/packages/dd-trace/src/appsec/waf/waf_manager.js +2 -1
  44. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -0
  46. package/packages/dd-trace/src/config.js +61 -10
  47. package/packages/dd-trace/src/constants.js +11 -1
  48. package/packages/dd-trace/src/data_streams_context.js +3 -0
  49. package/packages/dd-trace/src/datastreams/fnv.js +23 -0
  50. package/packages/dd-trace/src/datastreams/pathway.js +12 -5
  51. package/packages/dd-trace/src/datastreams/processor.js +35 -0
  52. package/packages/dd-trace/src/datastreams/schemas/schema.js +8 -0
  53. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +125 -0
  54. package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +29 -0
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +24 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/index.js +57 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/inspector_promises_polyfill.js +23 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +164 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/send.js +28 -0
  60. package/packages/dd-trace/src/debugger/devtools_client/session.js +7 -0
  61. package/packages/dd-trace/src/debugger/devtools_client/state.js +47 -0
  62. package/packages/dd-trace/src/debugger/devtools_client/status.js +109 -0
  63. package/packages/dd-trace/src/debugger/index.js +92 -0
  64. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +29 -2
  65. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  66. package/packages/dd-trace/src/payload-tagging/config/aws.json +30 -0
  67. package/packages/dd-trace/src/payload-tagging/config/index.js +30 -0
  68. package/packages/dd-trace/src/payload-tagging/index.js +93 -0
  69. package/packages/dd-trace/src/payload-tagging/tagging.js +83 -0
  70. package/packages/dd-trace/src/plugin_manager.js +11 -10
  71. package/packages/dd-trace/src/plugins/ci_plugin.js +33 -8
  72. package/packages/dd-trace/src/plugins/util/env.js +5 -2
  73. package/packages/dd-trace/src/plugins/util/test.js +26 -2
  74. package/packages/dd-trace/src/profiling/config.js +5 -0
  75. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  76. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns.js +13 -0
  77. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookup.js +16 -0
  78. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookupservice.js +16 -0
  79. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_resolve.js +24 -0
  80. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_reverse.js +16 -0
  81. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +48 -0
  82. package/packages/dd-trace/src/profiling/profilers/event_plugins/net.js +24 -0
  83. package/packages/dd-trace/src/profiling/profilers/events.js +108 -32
  84. package/packages/dd-trace/src/profiling/profilers/shared.js +5 -0
  85. package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
  86. package/packages/dd-trace/src/profiling/ssi-heuristics.js +10 -2
  87. package/packages/dd-trace/src/proxy.js +10 -3
  88. package/packages/dd-trace/src/span_stats.js +4 -2
  89. package/packages/dd-trace/src/appsec/rasp.js +0 -176
@@ -0,0 +1,83 @@
1
+ const { PAYLOAD_TAGGING_MAX_TAGS } = require('../constants')
2
+
3
+ const redactedKeys = [
4
+ 'authorization', 'x-authorization', 'password', 'token'
5
+ ]
6
+ const truncated = 'truncated'
7
+ const redacted = 'redacted'
8
+
9
+ function escapeKey (key) {
10
+ return key.replaceAll('.', '\\.')
11
+ }
12
+
13
+ /**
14
+ * Compute normalized payload tags from any given object.
15
+ *
16
+ * @param {object} object
17
+ * @param {import('./mask').Mask} mask
18
+ * @param {number} maxDepth
19
+ * @param {string} prefix
20
+ * @returns
21
+ */
22
+ function tagsFromObject (object, opts) {
23
+ const { maxDepth, prefix } = opts
24
+
25
+ let tagCount = 0
26
+ let abort = false
27
+ const result = {}
28
+
29
+ function tagRec (prefix, object, depth = 0) {
30
+ // Off by one: _dd.payload_tags_trimmed counts as 1 tag
31
+ if (abort) { return }
32
+
33
+ if (tagCount >= PAYLOAD_TAGGING_MAX_TAGS - 1) {
34
+ abort = true
35
+ result['_dd.payload_tags_incomplete'] = true
36
+ return
37
+ }
38
+
39
+ if (depth >= maxDepth && typeof object === 'object') {
40
+ tagCount += 1
41
+ result[prefix] = truncated
42
+ return
43
+ }
44
+
45
+ if (object === undefined) {
46
+ tagCount += 1
47
+ result[prefix] = 'undefined'
48
+ return
49
+ }
50
+
51
+ if (object === null) {
52
+ tagCount += 1
53
+ result[prefix] = 'null'
54
+ return
55
+ }
56
+
57
+ if (['number', 'boolean'].includes(typeof object) || Buffer.isBuffer(object)) {
58
+ tagCount += 1
59
+ result[prefix] = object.toString().substring(0, 5000)
60
+ return
61
+ }
62
+
63
+ if (typeof object === 'string') {
64
+ tagCount += 1
65
+ result[prefix] = object.substring(0, 5000)
66
+ }
67
+
68
+ if (typeof object === 'object') {
69
+ for (const [key, value] of Object.entries(object)) {
70
+ if (redactedKeys.includes(key.toLowerCase())) {
71
+ tagCount += 1
72
+ result[`${prefix}.${escapeKey(key)}`] = redacted
73
+ } else {
74
+ tagRec(`${prefix}.${escapeKey(key)}`, value, depth + 1)
75
+ }
76
+ }
77
+ }
78
+ }
79
+ tagRec(prefix, object)
80
+ return result
81
+ }
82
+
83
+ module.exports = { tagsFromObject }
@@ -136,10 +136,19 @@ module.exports = class PluginManager {
136
136
  dbmPropagationMode,
137
137
  dsmEnabled,
138
138
  clientIpEnabled,
139
- memcachedCommandEnabled
139
+ memcachedCommandEnabled,
140
+ ciVisibilityTestSessionName
140
141
  } = this._tracerConfig
141
142
 
142
- const sharedConfig = {}
143
+ const sharedConfig = {
144
+ dbmPropagationMode,
145
+ dsmEnabled,
146
+ memcachedCommandEnabled,
147
+ site,
148
+ url,
149
+ headers: headerTags || [],
150
+ ciVisibilityTestSessionName
151
+ }
143
152
 
144
153
  if (logInjection !== undefined) {
145
154
  sharedConfig.logInjection = logInjection
@@ -149,10 +158,6 @@ module.exports = class PluginManager {
149
158
  sharedConfig.queryStringObfuscation = queryStringObfuscation
150
159
  }
151
160
 
152
- sharedConfig.dbmPropagationMode = dbmPropagationMode
153
- sharedConfig.dsmEnabled = dsmEnabled
154
- sharedConfig.memcachedCommandEnabled = memcachedCommandEnabled
155
-
156
161
  if (serviceMapping && serviceMapping[name]) {
157
162
  sharedConfig.service = serviceMapping[name]
158
163
  }
@@ -161,10 +166,6 @@ module.exports = class PluginManager {
161
166
  sharedConfig.clientIpEnabled = clientIpEnabled
162
167
  }
163
168
 
164
- sharedConfig.site = site
165
- sharedConfig.url = url
166
- sharedConfig.headers = headerTags || []
167
-
168
169
  return sharedConfig
169
170
  }
170
171
  }
@@ -1,5 +1,6 @@
1
1
  const {
2
2
  getTestEnvironmentMetadata,
3
+ getTestSessionName,
3
4
  getCodeOwnersFileEntries,
4
5
  getTestParentSpan,
5
6
  getTestCommonTags,
@@ -13,11 +14,14 @@ const {
13
14
  TEST_SESSION_ID,
14
15
  TEST_COMMAND,
15
16
  TEST_MODULE,
17
+ TEST_SESSION_NAME,
16
18
  getTestSuiteCommonTags,
17
19
  TEST_STATUS,
18
20
  TEST_SKIPPED_BY_ITR,
19
21
  ITR_CORRELATION_ID,
20
- TEST_SOURCE_FILE
22
+ TEST_SOURCE_FILE,
23
+ TEST_LEVEL_EVENT_TYPES,
24
+ TEST_SUITE
21
25
  } = require('./util/test')
22
26
  const Plugin = require('./plugin')
23
27
  const { COMPONENT } = require('../constants')
@@ -75,6 +79,19 @@ module.exports = class CiPlugin extends Plugin {
75
79
  // only for playwright
76
80
  this.rootDir = rootDir
77
81
 
82
+ const testSessionName = getTestSessionName(this.config, this.command, this.testEnvironmentMetadata)
83
+
84
+ const metadataTags = {}
85
+ for (const testLevel of TEST_LEVEL_EVENT_TYPES) {
86
+ metadataTags[testLevel] = {
87
+ [TEST_SESSION_NAME]: testSessionName
88
+ }
89
+ }
90
+ // tracer might not be initialized correctly
91
+ if (this.tracer._exporter.setMetadataTags) {
92
+ this.tracer._exporter.setMetadataTags(metadataTags)
93
+ }
94
+
78
95
  this.testSessionSpan = this.tracer.startSpan(`${this.constructor.id}.test_session`, {
79
96
  childOf,
80
97
  tags: {
@@ -97,6 +114,7 @@ module.exports = class CiPlugin extends Plugin {
97
114
  if (this.constructor.id === 'vitest') {
98
115
  process.env.DD_CIVISIBILITY_TEST_SESSION_ID = this.testSessionSpan.context().toTraceId()
99
116
  process.env.DD_CIVISIBILITY_TEST_MODULE_ID = this.testModuleSpan.context().toSpanId()
117
+ process.env.DD_CIVISIBILITY_TEST_COMMAND = this.command
100
118
  }
101
119
 
102
120
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module')
@@ -194,6 +212,19 @@ module.exports = class CiPlugin extends Plugin {
194
212
  }
195
213
  }
196
214
 
215
+ getCodeOwners (tags) {
216
+ const {
217
+ [TEST_SOURCE_FILE]: testSourceFile,
218
+ [TEST_SUITE]: testSuite
219
+ } = tags
220
+ // We'll try with the test source file if available (it could be different from the test suite)
221
+ let codeOwners = getCodeOwnersForFilename(testSourceFile, this.codeOwnersEntries)
222
+ if (!codeOwners) {
223
+ codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries)
224
+ }
225
+ return codeOwners
226
+ }
227
+
197
228
  startTestSpan (testName, testSuite, testSuiteSpan, extraTags = {}) {
198
229
  const childOf = getTestParentSpan(this.tracer)
199
230
 
@@ -208,13 +239,7 @@ module.exports = class CiPlugin extends Plugin {
208
239
  ...extraTags
209
240
  }
210
241
 
211
- const { [TEST_SOURCE_FILE]: testSourceFile } = extraTags
212
- // We'll try with the test source file if available (it could be different from the test suite)
213
- let codeOwners = getCodeOwnersForFilename(testSourceFile, this.codeOwnersEntries)
214
- if (!codeOwners) {
215
- codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries)
216
- }
217
-
242
+ const codeOwners = this.getCodeOwners(testTags)
218
243
  if (codeOwners) {
219
244
  testTags[TEST_CODE_OWNERS] = codeOwners
220
245
  }
@@ -5,6 +5,7 @@ const OS_VERSION = 'os.version'
5
5
  const OS_ARCHITECTURE = 'os.architecture'
6
6
  const RUNTIME_NAME = 'runtime.name'
7
7
  const RUNTIME_VERSION = 'runtime.version'
8
+ const DD_HOST_CPU_COUNT = '_dd.host.vcpu_count'
8
9
 
9
10
  function getRuntimeAndOSMetadata () {
10
11
  return {
@@ -12,7 +13,8 @@ function getRuntimeAndOSMetadata () {
12
13
  [OS_ARCHITECTURE]: process.arch,
13
14
  [OS_PLATFORM]: process.platform,
14
15
  [RUNTIME_NAME]: 'node',
15
- [OS_VERSION]: os.release()
16
+ [OS_VERSION]: os.release(),
17
+ [DD_HOST_CPU_COUNT]: os.cpus().length
16
18
  }
17
19
  }
18
20
 
@@ -22,5 +24,6 @@ module.exports = {
22
24
  OS_VERSION,
23
25
  OS_ARCHITECTURE,
24
26
  RUNTIME_NAME,
25
- RUNTIME_VERSION
27
+ RUNTIME_VERSION,
28
+ DD_HOST_CPU_COUNT
26
29
  }
@@ -19,7 +19,8 @@ const {
19
19
  GIT_COMMIT_AUTHOR_NAME,
20
20
  GIT_COMMIT_MESSAGE,
21
21
  CI_WORKSPACE_PATH,
22
- CI_PIPELINE_URL
22
+ CI_PIPELINE_URL,
23
+ CI_JOB_NAME
23
24
  } = require('./tags')
24
25
  const id = require('../../id')
25
26
 
@@ -28,6 +29,9 @@ const { SAMPLING_RULE_DECISION } = require('../../constants')
28
29
  const { AUTO_KEEP } = require('../../../../../ext/priority')
29
30
  const { version: ddTraceVersion } = require('../../../../../package.json')
30
31
 
32
+ // session tags
33
+ const TEST_SESSION_NAME = 'test_session.name'
34
+
31
35
  const TEST_FRAMEWORK = 'test.framework'
32
36
  const TEST_FRAMEWORK_VERSION = 'test.framework_version'
33
37
  const TEST_TYPE = 'test.type'
@@ -95,8 +99,16 @@ const MOCHA_WORKER_TRACE_PAYLOAD_CODE = 80
95
99
  const EFD_STRING = "Retried by Datadog's Early Flake Detection"
96
100
  const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + ' \\(#\\d+\\): ', 'g')
97
101
 
102
+ const TEST_LEVEL_EVENT_TYPES = [
103
+ 'test',
104
+ 'test_suite_end',
105
+ 'test_module_end',
106
+ 'test_session_end'
107
+ ]
108
+
98
109
  module.exports = {
99
110
  TEST_CODE_OWNERS,
111
+ TEST_SESSION_NAME,
100
112
  TEST_FRAMEWORK,
101
113
  TEST_FRAMEWORK_VERSION,
102
114
  JEST_TEST_RUNNER,
@@ -167,7 +179,9 @@ module.exports = {
167
179
  TEST_BROWSER_DRIVER,
168
180
  TEST_BROWSER_DRIVER_VERSION,
169
181
  TEST_BROWSER_NAME,
170
- TEST_BROWSER_VERSION
182
+ TEST_BROWSER_VERSION,
183
+ getTestSessionName,
184
+ TEST_LEVEL_EVENT_TYPES
171
185
  }
172
186
 
173
187
  // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
@@ -615,3 +629,13 @@ function getIsFaultyEarlyFlakeDetection (projectSuites, testsBySuiteName, faulty
615
629
  newSuitesPercentage > faultyThresholdPercentage
616
630
  )
617
631
  }
632
+
633
+ function getTestSessionName (config, testCommand, envTags) {
634
+ if (config.ciVisibilityTestSessionName) {
635
+ return config.ciVisibilityTestSessionName
636
+ }
637
+ if (envTags[CI_JOB_NAME]) {
638
+ return `${envTags[CI_JOB_NAME]}-${testCommand}`
639
+ }
640
+ return testCommand
641
+ }
@@ -97,6 +97,11 @@ class Config {
97
97
  const samplingContextsAvailable = process.platform !== 'win32'
98
98
  function checkOptionAllowed (option, description, condition) {
99
99
  if (option && !condition) {
100
+ // injection hardening: all of these can only happen if user explicitly
101
+ // sets an environment variable to its non-default value on the platform.
102
+ // In practical terms, it'd require someone explicitly turning on OOM
103
+ // monitoring, code hotspots, endpoint profiling, or CPU profiling on
104
+ // Windows, where it is not supported.
100
105
  throw new Error(`${description} not supported on ${process.platform}.`)
101
106
  }
102
107
  }
@@ -199,7 +199,7 @@ class AgentExporter {
199
199
  this._logger.error(`Error from the agent: ${err.message}`)
200
200
  return
201
201
  } else if (err) {
202
- reject(new Error('Profiler agent export back-off period expired'))
202
+ reject(err)
203
203
  return
204
204
  }
205
205
 
@@ -0,0 +1,13 @@
1
+ const EventPlugin = require('./event')
2
+
3
+ class DNSPlugin extends EventPlugin {
4
+ static get id () {
5
+ return 'dns'
6
+ }
7
+
8
+ static get entryType () {
9
+ return 'dns'
10
+ }
11
+ }
12
+
13
+ module.exports = DNSPlugin
@@ -0,0 +1,16 @@
1
+ const DNSPlugin = require('./dns')
2
+
3
+ class DNSLookupPlugin extends DNSPlugin {
4
+ static get operation () {
5
+ return 'lookup'
6
+ }
7
+
8
+ extendEvent (event, startEvent) {
9
+ event.name = 'lookup'
10
+ event.detail = { hostname: startEvent[0] }
11
+
12
+ return event
13
+ }
14
+ }
15
+
16
+ module.exports = DNSLookupPlugin
@@ -0,0 +1,16 @@
1
+ const DNSPlugin = require('./dns')
2
+
3
+ class DNSLookupServicePlugin extends DNSPlugin {
4
+ static get operation () {
5
+ return 'lookup_service'
6
+ }
7
+
8
+ extendEvent (event, startEvent) {
9
+ event.name = 'lookupService'
10
+ event.detail = { host: startEvent[0], port: startEvent[1] }
11
+
12
+ return event
13
+ }
14
+ }
15
+
16
+ module.exports = DNSLookupServicePlugin
@@ -0,0 +1,24 @@
1
+ const DNSPlugin = require('./dns')
2
+
3
+ const queryNames = new Map()
4
+
5
+ class DNSResolvePlugin extends DNSPlugin {
6
+ static get operation () {
7
+ return 'resolve'
8
+ }
9
+
10
+ extendEvent (event, startEvent) {
11
+ const rrtype = startEvent[1]
12
+ let name = queryNames.get(rrtype)
13
+ if (!name) {
14
+ name = `query${rrtype}`
15
+ queryNames.set(rrtype, name)
16
+ }
17
+ event.name = name
18
+ event.detail = { host: startEvent[0] }
19
+
20
+ return event
21
+ }
22
+ }
23
+
24
+ module.exports = DNSResolvePlugin
@@ -0,0 +1,16 @@
1
+ const DNSPlugin = require('./dns')
2
+
3
+ class DNSReversePlugin extends DNSPlugin {
4
+ static get operation () {
5
+ return 'reverse'
6
+ }
7
+
8
+ extendEvent (event, startEvent) {
9
+ event.name = 'getHostByAddr'
10
+ event.detail = { host: startEvent[0] }
11
+
12
+ return event
13
+ }
14
+ }
15
+
16
+ module.exports = DNSReversePlugin
@@ -0,0 +1,48 @@
1
+ const { AsyncLocalStorage } = require('async_hooks')
2
+ const TracingPlugin = require('../../../plugins/tracing')
3
+ const { performance } = require('perf_hooks')
4
+
5
+ // We are leveraging the TracingPlugin class for its functionality to bind
6
+ // start/error/finish methods to the appropriate diagnostic channels.
7
+ class EventPlugin extends TracingPlugin {
8
+ constructor (eventHandler) {
9
+ super()
10
+ this.eventHandler = eventHandler
11
+ this.store = new AsyncLocalStorage()
12
+ this.entryType = this.constructor.entryType
13
+ }
14
+
15
+ start (startEvent) {
16
+ this.store.enterWith({
17
+ startEvent,
18
+ startTime: performance.now()
19
+ })
20
+ }
21
+
22
+ error () {
23
+ this.store.getStore().error = true
24
+ }
25
+
26
+ finish () {
27
+ const { startEvent, startTime, error } = this.store.getStore()
28
+ if (error) {
29
+ return // don't emit perf events for failed operations
30
+ }
31
+ const duration = performance.now() - startTime
32
+
33
+ const context = this.activeSpan?.context()
34
+ const _ddSpanId = context?.toSpanId()
35
+ const _ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || _ddSpanId
36
+
37
+ const event = {
38
+ entryType: this.entryType,
39
+ startTime,
40
+ duration,
41
+ _ddSpanId,
42
+ _ddRootSpanId
43
+ }
44
+ this.eventHandler(this.extendEvent(event, startEvent))
45
+ }
46
+ }
47
+
48
+ module.exports = EventPlugin
@@ -0,0 +1,24 @@
1
+ const EventPlugin = require('./event')
2
+
3
+ class NetPlugin extends EventPlugin {
4
+ static get id () {
5
+ return 'net'
6
+ }
7
+
8
+ static get operation () {
9
+ return 'tcp'
10
+ }
11
+
12
+ static get entryType () {
13
+ return 'net'
14
+ }
15
+
16
+ extendEvent (event, { options }) {
17
+ event.name = 'connect'
18
+ event.detail = options
19
+
20
+ return event
21
+ }
22
+ }
23
+
24
+ module.exports = NetPlugin
@@ -1,12 +1,8 @@
1
1
  const { performance, constants, PerformanceObserver } = require('perf_hooks')
2
- const { END_TIMESTAMP_LABEL } = require('./shared')
3
- const semver = require('semver')
2
+ const { END_TIMESTAMP_LABEL, SPAN_ID_LABEL, LOCAL_ROOT_SPAN_ID_LABEL } = require('./shared')
4
3
  const { Function, Label, Line, Location, Profile, Sample, StringTable, ValueType } = require('pprof-format')
5
4
  const pprof = require('@datadog/pprof/')
6
5
 
7
- // Format of perf_hooks events changed with Node 16, we need to be mindful of it.
8
- const node16 = semver.gte(process.version, '16.0.0')
9
-
10
6
  // perf_hooks uses millis, with fractional part representing nanos. We emit nanos into the pprof file.
11
7
  const MS_TO_NS = 1000000
12
8
 
@@ -48,7 +44,7 @@ class GCDecorator {
48
44
  }
49
45
 
50
46
  decorateSample (sampleInput, item) {
51
- const { kind, flags } = node16 ? item.detail : item
47
+ const { kind, flags } = item.detail
52
48
  sampleInput.label.push(this.kindLabels[kind])
53
49
  const reasonLabel = this.getReasonLabel(flags)
54
50
  if (reasonLabel) {
@@ -140,12 +136,9 @@ class NetDecorator {
140
136
  // Keys correspond to PerformanceEntry.entryType, values are constructor
141
137
  // functions for type-specific decorators.
142
138
  const decoratorTypes = {
143
- gc: GCDecorator
144
- }
145
- // Needs at least node 16 for DNS and Net
146
- if (node16) {
147
- decoratorTypes.dns = DNSDecorator
148
- decoratorTypes.net = NetDecorator
139
+ gc: GCDecorator,
140
+ dns: DNSDecorator,
141
+ net: NetDecorator
149
142
  }
150
143
 
151
144
  // Translates performance entries into pprof samples.
@@ -168,10 +161,12 @@ class EventSerializer {
168
161
  this.locationId = [location.id]
169
162
 
170
163
  this.timestampLabelKey = this.stringTable.dedup(END_TIMESTAMP_LABEL)
164
+ this.spanIdKey = this.stringTable.dedup(SPAN_ID_LABEL)
165
+ this.rootSpanIdKey = this.stringTable.dedup(LOCAL_ROOT_SPAN_ID_LABEL)
171
166
  }
172
167
 
173
168
  addEvent (item) {
174
- const { entryType, startTime, duration } = item
169
+ const { entryType, startTime, duration, _ddSpanId, _ddRootSpanId } = item
175
170
  let decorator = this.decorators[entryType]
176
171
  if (!decorator) {
177
172
  const DecoratorCtor = decoratorTypes[entryType]
@@ -186,13 +181,21 @@ class EventSerializer {
186
181
  }
187
182
  }
188
183
  const endTime = startTime + duration
184
+ const label = [
185
+ decorator.eventTypeLabel,
186
+ new Label({ key: this.timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
187
+ ]
188
+ if (_ddSpanId) {
189
+ label.push(labelFromStr(this.stringTable, this.spanIdKey, _ddSpanId))
190
+ }
191
+ if (_ddRootSpanId) {
192
+ label.push(labelFromStr(this.stringTable, this.rootSpanIdKey, _ddRootSpanId))
193
+ }
194
+
189
195
  const sampleInput = {
190
196
  value: [Math.round(duration * MS_TO_NS)],
191
197
  locationId: this.locationId,
192
- label: [
193
- decorator.eventTypeLabel,
194
- new Label({ key: this.timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
195
- ]
198
+ label
196
199
  }
197
200
  decorator.decorateSample(sampleInput, item)
198
201
  this.samples.push(new Sample(sampleInput))
@@ -219,36 +222,109 @@ class EventSerializer {
219
222
  }
220
223
 
221
224
  /**
222
- * This class generates pprof files with timeline events sourced from Node.js
223
- * performance measurement APIs.
225
+ * Class that sources timeline events through Node.js performance measurement APIs.
224
226
  */
225
- class EventsProfiler {
226
- constructor (options = {}) {
227
- this.type = 'events'
228
- this._flushIntervalNanos = (options.flushInterval || 60000) * 1e6 // 60 sec
229
- this._observer = undefined
230
- this.eventSerializer = new EventSerializer()
227
+ class NodeApiEventSource {
228
+ constructor (eventHandler, entryTypes) {
229
+ this.eventHandler = eventHandler
230
+ this.observer = undefined
231
+ this.entryTypes = entryTypes || Object.keys(decoratorTypes)
231
232
  }
232
233
 
233
234
  start () {
234
235
  // if already started, do nothing
235
- if (this._observer) return
236
+ if (this.observer) return
236
237
 
237
238
  function add (items) {
238
239
  for (const item of items.getEntries()) {
239
- this.eventSerializer.addEvent(item)
240
+ this.eventHandler(item)
240
241
  }
241
242
  }
242
- this._observer = new PerformanceObserver(add.bind(this))
243
- this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
243
+
244
+ this.observer = new PerformanceObserver(add.bind(this))
245
+ this.observer.observe({ entryTypes: this.entryTypes })
246
+ }
247
+
248
+ stop () {
249
+ if (this.observer) {
250
+ this.observer.disconnect()
251
+ this.observer = undefined
252
+ }
253
+ }
254
+ }
255
+
256
+ class DatadogInstrumentationEventSource {
257
+ constructor (eventHandler) {
258
+ this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => {
259
+ const Plugin = require(`./event_plugins/${m}`)
260
+ return new Plugin(eventHandler)
261
+ })
262
+
263
+ this.started = false
264
+ }
265
+
266
+ start () {
267
+ if (!this.started) {
268
+ this.plugins.forEach(p => p.configure({ enabled: true }))
269
+ this.started = true
270
+ }
244
271
  }
245
272
 
246
273
  stop () {
247
- if (this._observer) {
248
- this._observer.disconnect()
249
- this._observer = undefined
274
+ if (this.started) {
275
+ this.plugins.forEach(p => p.configure({ enabled: false }))
276
+ this.started = false
250
277
  }
251
278
  }
279
+ }
280
+
281
+ class CompositeEventSource {
282
+ constructor (sources) {
283
+ this.sources = sources
284
+ }
285
+
286
+ start () {
287
+ this.sources.forEach(s => s.start())
288
+ }
289
+
290
+ stop () {
291
+ this.sources.forEach(s => s.stop())
292
+ }
293
+ }
294
+
295
+ /**
296
+ * This class generates pprof files with timeline events. It combines an event
297
+ * source with an event serializer.
298
+ */
299
+ class EventsProfiler {
300
+ constructor (options = {}) {
301
+ this.type = 'events'
302
+ this.eventSerializer = new EventSerializer()
303
+
304
+ const eventHandler = event => {
305
+ this.eventSerializer.addEvent(event)
306
+ }
307
+
308
+ if (options.codeHotspotsEnabled) {
309
+ // Use Datadog instrumentation to collect events with span IDs. Still use
310
+ // Node API for GC events.
311
+ this.eventSource = new CompositeEventSource([
312
+ new DatadogInstrumentationEventSource(eventHandler),
313
+ new NodeApiEventSource(eventHandler, ['gc'])
314
+ ])
315
+ } else {
316
+ // Use Node API instrumentation to collect events without span IDs
317
+ this.eventSource = new NodeApiEventSource(eventHandler)
318
+ }
319
+ }
320
+
321
+ start () {
322
+ this.eventSource.start()
323
+ }
324
+
325
+ stop () {
326
+ this.eventSource.stop()
327
+ }
252
328
 
253
329
  profile (restart, startDate, endDate) {
254
330
  if (!restart) {