dd-trace 5.15.0 → 5.17.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.
package/index.d.ts CHANGED
@@ -197,6 +197,7 @@ interface Plugins {
197
197
  "selenium": tracer.plugins.selenium;
198
198
  "sharedb": tracer.plugins.sharedb;
199
199
  "tedious": tracer.plugins.tedious;
200
+ "undici": tracer.plugins.undici;
200
201
  "winston": tracer.plugins.winston;
201
202
  }
202
203
 
@@ -1800,6 +1801,12 @@ declare namespace tracer {
1800
1801
  */
1801
1802
  interface tedious extends Instrumentation {}
1802
1803
 
1804
+ /**
1805
+ * This plugin automatically instruments the
1806
+ * [undici](https://github.com/nodejs/undici) module.
1807
+ */
1808
+ interface undici extends HttpClient {}
1809
+
1803
1810
  /**
1804
1811
  * This plugin patches the [winston](https://github.com/winstonjs/winston)
1805
1812
  * to automatically inject trace identifiers in log records when the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.15.0",
3
+ "version": "5.17.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -109,6 +109,7 @@ module.exports = {
109
109
  sequelize: () => require('../sequelize'),
110
110
  sharedb: () => require('../sharedb'),
111
111
  tedious: () => require('../tedious'),
112
+ undici: () => require('../undici'),
112
113
  when: () => require('../when'),
113
114
  winston: () => require('../winston')
114
115
  }
@@ -10,6 +10,7 @@ const startServerCh = channel('apm:http:server:request:start')
10
10
  const exitServerCh = channel('apm:http:server:request:exit')
11
11
  const errorServerCh = channel('apm:http:server:request:error')
12
12
  const finishServerCh = channel('apm:http:server:request:finish')
13
+ const startWriteHeadCh = channel('apm:http:server:response:writeHead:start')
13
14
  const finishSetHeaderCh = channel('datadog:http:server:response:set-header:finish')
14
15
 
15
16
  const requestFinishedSet = new WeakSet()
@@ -20,6 +21,9 @@ const httpsNames = ['https', 'node:https']
20
21
  addHook({ name: httpNames }, http => {
21
22
  shimmer.wrap(http.ServerResponse.prototype, 'emit', wrapResponseEmit)
22
23
  shimmer.wrap(http.Server.prototype, 'emit', wrapEmit)
24
+ shimmer.wrap(http.ServerResponse.prototype, 'writeHead', wrapWriteHead)
25
+ shimmer.wrap(http.ServerResponse.prototype, 'write', wrapWrite)
26
+ shimmer.wrap(http.ServerResponse.prototype, 'end', wrapEnd)
23
27
  return http
24
28
  })
25
29
 
@@ -86,3 +90,97 @@ function wrapSetHeader (res) {
86
90
  }
87
91
  })
88
92
  }
93
+
94
+ function wrapWriteHead (writeHead) {
95
+ return function wrappedWriteHead (statusCode, reason, obj) {
96
+ if (!startWriteHeadCh.hasSubscribers) {
97
+ return writeHead.apply(this, arguments)
98
+ }
99
+
100
+ const abortController = new AbortController()
101
+
102
+ if (typeof reason !== 'string') {
103
+ obj ??= reason
104
+ }
105
+
106
+ // support writeHead(200, ['key1', 'val1', 'key2', 'val2'])
107
+ if (Array.isArray(obj)) {
108
+ const headers = {}
109
+
110
+ for (let i = 0; i < obj.length; i += 2) {
111
+ headers[obj[i]] = obj[i + 1]
112
+ }
113
+
114
+ obj = headers
115
+ }
116
+
117
+ // this doesn't support explicit duplicate headers, but it's an edge case
118
+ const responseHeaders = Object.assign(this.getHeaders(), obj)
119
+
120
+ startWriteHeadCh.publish({
121
+ req: this.req,
122
+ res: this,
123
+ abortController,
124
+ statusCode,
125
+ responseHeaders
126
+ })
127
+
128
+ if (abortController.signal.aborted) {
129
+ return this
130
+ }
131
+
132
+ return writeHead.apply(this, arguments)
133
+ }
134
+ }
135
+
136
+ function wrapWrite (write) {
137
+ return function wrappedWrite () {
138
+ if (!startWriteHeadCh.hasSubscribers) {
139
+ return write.apply(this, arguments)
140
+ }
141
+
142
+ const abortController = new AbortController()
143
+
144
+ const responseHeaders = this.getHeaders()
145
+
146
+ startWriteHeadCh.publish({
147
+ req: this.req,
148
+ res: this,
149
+ abortController,
150
+ statusCode: this.statusCode,
151
+ responseHeaders
152
+ })
153
+
154
+ if (abortController.signal.aborted) {
155
+ return true
156
+ }
157
+
158
+ return write.apply(this, arguments)
159
+ }
160
+ }
161
+
162
+ function wrapEnd (end) {
163
+ return function wrappedEnd () {
164
+ if (!startWriteHeadCh.hasSubscribers) {
165
+ return end.apply(this, arguments)
166
+ }
167
+
168
+ const abortController = new AbortController()
169
+
170
+ const responseHeaders = this.getHeaders()
171
+
172
+ startWriteHeadCh.publish({
173
+ req: this.req,
174
+ res: this,
175
+ abortController,
176
+ statusCode: this.statusCode,
177
+ responseHeaders
178
+ })
179
+
180
+ if (abortController.signal.aborted) {
181
+ return this
182
+ }
183
+
184
+ return end.apply(this, arguments)
185
+ }
186
+ }
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ addHook
5
+ } = require('./helpers/instrument')
6
+ const shimmer = require('../../datadog-shimmer')
7
+
8
+ const tracingChannel = require('dc-polyfill').tracingChannel
9
+ const ch = tracingChannel('apm:undici:fetch')
10
+
11
+ const { createWrapFetch } = require('./helpers/fetch')
12
+
13
+ addHook({
14
+ name: 'undici',
15
+ versions: ['^4.4.1', '5', '>=6.0.0']
16
+ }, undici => {
17
+ return shimmer.wrap(undici, 'fetch', createWrapFetch(undici.Request, ch))
18
+ })
@@ -80,6 +80,10 @@ class GraphQLResolvePlugin extends TracingPlugin {
80
80
  // this will disable resolve subscribers if `config.depth` is set to 0
81
81
  super.configure(config.depth === 0 ? false : config)
82
82
  }
83
+
84
+ finish (finishTime) {
85
+ this.activeSpan.finish(finishTime)
86
+ }
83
87
  }
84
88
 
85
89
  // helpers
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const FetchPlugin = require('../../datadog-plugin-fetch/src/index.js')
4
+
5
+ class UndiciPlugin extends FetchPlugin {
6
+ static get id () { return 'undici' }
7
+ static get prefix () {
8
+ return 'tracing:apm:undici:fetch'
9
+ }
10
+ }
11
+
12
+ module.exports = UndiciPlugin
@@ -111,6 +111,10 @@ function block (req, res, rootSpan, abortController, actionParameters) {
111
111
 
112
112
  const { body, headers, statusCode } = getBlockingData(req, null, rootSpan, actionParameters)
113
113
 
114
+ for (const headerName of res.getHeaderNames()) {
115
+ res.removeHeader(headerName)
116
+ }
117
+
114
118
  res.writeHead(statusCode, headers).end(body)
115
119
 
116
120
  abortController?.abort()
@@ -18,5 +18,6 @@ module.exports = {
18
18
  nextBodyParsed: dc.channel('apm:next:body-parsed'),
19
19
  nextQueryParsed: dc.channel('apm:next:query-parsed'),
20
20
  responseBody: dc.channel('datadog:express:response:json:start'),
21
+ responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
21
22
  httpClientRequestStart: dc.channel('apm:http:client:request:start')
22
23
  }
@@ -12,7 +12,8 @@ const {
12
12
  queryParser,
13
13
  nextBodyParsed,
14
14
  nextQueryParsed,
15
- responseBody
15
+ responseBody,
16
+ responseWriteHead
16
17
  } = require('./channels')
17
18
  const waf = require('./waf')
18
19
  const addresses = require('./addresses')
@@ -60,6 +61,7 @@ function enable (_config) {
60
61
  queryParser.subscribe(onRequestQueryParsed)
61
62
  cookieParser.subscribe(onRequestCookieParser)
62
63
  responseBody.subscribe(onResponseBody)
64
+ responseWriteHead.subscribe(onResponseWriteHead)
63
65
 
64
66
  if (_config.appsec.eventTracking.enabled) {
65
67
  passportVerify.subscribe(onPassportVerify)
@@ -110,14 +112,7 @@ function incomingHttpStartTranslator ({ req, res, abortController }) {
110
112
  }
111
113
 
112
114
  function incomingHttpEndTranslator ({ req, res }) {
113
- // TODO: this doesn't support headers sent with res.writeHead()
114
- const responseHeaders = Object.assign({}, res.getHeaders())
115
- delete responseHeaders['set-cookie']
116
-
117
- const persistent = {
118
- [addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + res.statusCode,
119
- [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
120
- }
115
+ const persistent = {}
121
116
 
122
117
  // we need to keep this to support other body parsers
123
118
  // TODO: no need to analyze it if it was already done by the body-parser hook
@@ -139,7 +134,9 @@ function incomingHttpEndTranslator ({ req, res }) {
139
134
  persistent[addresses.HTTP_INCOMING_QUERY] = req.query
140
135
  }
141
136
 
142
- waf.run({ persistent }, req)
137
+ if (Object.keys(persistent).length) {
138
+ waf.run({ persistent }, req)
139
+ }
143
140
 
144
141
  waf.disposeContext(req)
145
142
 
@@ -225,12 +222,48 @@ function onPassportVerify ({ credentials, user }) {
225
222
  passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
226
223
  }
227
224
 
225
+ const responseAnalyzedSet = new WeakSet()
226
+ const responseBlockedSet = new WeakSet()
227
+
228
+ function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
229
+ // avoid "write after end" error
230
+ if (responseBlockedSet.has(res)) {
231
+ abortController?.abort()
232
+ return
233
+ }
234
+
235
+ // avoid double waf call
236
+ if (responseAnalyzedSet.has(res)) {
237
+ return
238
+ }
239
+
240
+ const rootSpan = web.root(req)
241
+ if (!rootSpan) return
242
+
243
+ responseHeaders = Object.assign({}, responseHeaders)
244
+ delete responseHeaders['set-cookie']
245
+
246
+ const results = waf.run({
247
+ persistent: {
248
+ [addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + statusCode,
249
+ [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
250
+ }
251
+ }, req)
252
+
253
+ responseAnalyzedSet.add(res)
254
+
255
+ handleResults(results, req, res, rootSpan, abortController)
256
+ }
257
+
228
258
  function handleResults (actions, req, res, rootSpan, abortController) {
229
259
  if (!actions || !req || !res || !rootSpan || !abortController) return
230
260
 
231
261
  const blockingAction = getBlockingAction(actions)
232
262
  if (blockingAction) {
233
263
  block(req, res, rootSpan, abortController, blockingAction)
264
+ if (!abortController.signal || abortController.signal.aborted) {
265
+ responseBlockedSet.add(res)
266
+ }
234
267
  }
235
268
  }
236
269
 
@@ -256,6 +289,7 @@ function disable () {
256
289
  if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
257
290
  if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
258
291
  if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
292
+ if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
259
293
  }
260
294
 
261
295
  module.exports = {
@@ -6,6 +6,7 @@ module.exports = {
6
6
  ASM_DD_RULES: 1n << 3n,
7
7
  ASM_EXCLUSIONS: 1n << 4n,
8
8
  ASM_REQUEST_BLOCKING: 1n << 5n,
9
+ ASM_RESPONSE_BLOCKING: 1n << 6n,
9
10
  ASM_USER_BLOCKING: 1n << 7n,
10
11
  ASM_CUSTOM_RULES: 1n << 8n,
11
12
  ASM_CUSTOM_BLOCKING_RESPONSE: 1n << 9n,
@@ -71,6 +71,7 @@ function enableWafUpdate (appsecConfig) {
71
71
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, true)
72
72
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, true)
73
73
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, true)
74
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, true)
74
75
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true)
75
76
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true)
76
77
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true)
@@ -92,6 +93,7 @@ function disableWafUpdate () {
92
93
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, false)
93
94
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, false)
94
95
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, false)
96
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, false)
95
97
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false)
96
98
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false)
97
99
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false)
@@ -607,6 +607,7 @@ class Config {
607
607
 
608
608
  const tags = {}
609
609
  const env = this._env = {}
610
+ this._envUnprocessed = {}
610
611
 
611
612
  tagger.add(tags, OTEL_RESOURCE_ATTRIBUTES, true)
612
613
  tagger.add(tags, DD_TAGS)
@@ -614,16 +615,20 @@ class Config {
614
615
  tagger.add(tags, DD_TRACE_GLOBAL_TAGS)
615
616
 
616
617
  this._setValue(env, 'appsec.blockedTemplateHtml', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML))
618
+ this._envUnprocessed['appsec.blockedTemplateHtml'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML
617
619
  this._setValue(env, 'appsec.blockedTemplateJson', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON))
620
+ this._envUnprocessed['appsec.blockedTemplateJson'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON
618
621
  this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED)
619
622
  this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP)
620
623
  this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP)
621
624
  this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED)
622
625
  this._setValue(env, 'appsec.rateLimit', maybeInt(DD_APPSEC_TRACE_RATE_LIMIT))
626
+ this._envUnprocessed['appsec.rateLimit'] = DD_APPSEC_TRACE_RATE_LIMIT
623
627
  this._setString(env, 'appsec.rules', DD_APPSEC_RULES)
624
628
  // DD_APPSEC_SCA_ENABLED is never used locally, but only sent to the backend
625
629
  this._setBoolean(env, 'appsec.sca.enabled', DD_APPSEC_SCA_ENABLED)
626
630
  this._setValue(env, 'appsec.wafTimeout', maybeInt(DD_APPSEC_WAF_TIMEOUT))
631
+ this._envUnprocessed['appsec.wafTimeout'] = DD_APPSEC_WAF_TIMEOUT
627
632
  this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED)
628
633
  this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER)
629
634
  this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE)
@@ -636,13 +641,16 @@ class Config {
636
641
  this._setBoolean(env, 'experimental.runtimeId', DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED)
637
642
  if (AWS_LAMBDA_FUNCTION_NAME) this._setValue(env, 'flushInterval', 0)
638
643
  this._setValue(env, 'flushMinSpans', maybeInt(DD_TRACE_PARTIAL_FLUSH_MIN_SPANS))
644
+ this._envUnprocessed.flushMinSpans = DD_TRACE_PARTIAL_FLUSH_MIN_SPANS
639
645
  this._setBoolean(env, 'gitMetadataEnabled', DD_TRACE_GIT_METADATA_ENABLED)
640
646
  this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
641
647
  this._setString(env, 'hostname', coalesce(DD_AGENT_HOST, DD_TRACE_AGENT_HOSTNAME))
642
648
  this._setBoolean(env, 'iast.deduplicationEnabled', DD_IAST_DEDUPLICATION_ENABLED)
643
649
  this._setBoolean(env, 'iast.enabled', DD_IAST_ENABLED)
644
650
  this._setValue(env, 'iast.maxConcurrentRequests', maybeInt(DD_IAST_MAX_CONCURRENT_REQUESTS))
651
+ this._envUnprocessed['iast.maxConcurrentRequests'] = DD_IAST_MAX_CONCURRENT_REQUESTS
645
652
  this._setValue(env, 'iast.maxContextOperations', maybeInt(DD_IAST_MAX_CONTEXT_OPERATIONS))
653
+ this._envUnprocessed['iast.maxContextOperations'] = DD_IAST_MAX_CONTEXT_OPERATIONS
646
654
  this._setBoolean(env, 'iast.redactionEnabled', DD_IAST_REDACTION_ENABLED && !isFalse(DD_IAST_REDACTION_ENABLED))
647
655
  this._setString(env, 'iast.redactionNamePattern', DD_IAST_REDACTION_NAME_PATTERN)
648
656
  this._setString(env, 'iast.redactionValuePattern', DD_IAST_REDACTION_VALUE_PATTERN)
@@ -650,15 +658,18 @@ class Config {
650
658
  if (iastRequestSampling > -1 && iastRequestSampling < 101) {
651
659
  this._setValue(env, 'iast.requestSampling', iastRequestSampling)
652
660
  }
661
+ this._envUnprocessed['iast.requestSampling'] = DD_IAST_REQUEST_SAMPLING
653
662
  this._setString(env, 'iast.telemetryVerbosity', DD_IAST_TELEMETRY_VERBOSITY)
654
663
  this._setBoolean(env, 'isGCPFunction', getIsGCPFunction())
655
664
  this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
656
665
  this._setBoolean(env, 'openAiLogsEnabled', DD_OPENAI_LOGS_ENABLED)
657
666
  this._setValue(env, 'openaiSpanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT))
667
+ this._envUnprocessed.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT
658
668
  if (DD_TRACE_PEER_SERVICE_MAPPING) {
659
669
  this._setValue(env, 'peerServiceMapping', fromEntries(
660
- process.env.DD_TRACE_PEER_SERVICE_MAPPING.split(',').map(x => x.trim().split(':'))
670
+ DD_TRACE_PEER_SERVICE_MAPPING.split(',').map(x => x.trim().split(':'))
661
671
  ))
672
+ this._envUnprocessed.peerServiceMapping = DD_TRACE_PEER_SERVICE_MAPPING
662
673
  }
663
674
  this._setString(env, 'port', DD_TRACE_AGENT_PORT)
664
675
  this._setBoolean(env, 'profiling.enabled', coalesce(DD_EXPERIMENTAL_PROFILING_ENABLED, DD_PROFILING_ENABLED))
@@ -682,6 +693,7 @@ class Config {
682
693
  !this._isInServerlessEnvironment()
683
694
  ))
684
695
  this._setValue(env, 'remoteConfig.pollInterval', maybeFloat(DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS))
696
+ this._envUnprocessed['remoteConfig.pollInterval'] = DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS
685
697
  this._setBoolean(env, 'reportHostname', DD_TRACE_REPORT_HOSTNAME)
686
698
  // only used to explicitly set runtimeMetrics to false
687
699
  const otelSetRuntimeMetrics = String(OTEL_METRICS_EXPORTER).toLowerCase() === 'none'
@@ -699,12 +711,14 @@ class Config {
699
711
  }
700
712
  this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE || OTEL_TRACES_SAMPLER_MAPPING[OTEL_TRACES_SAMPLER])
701
713
  this._setValue(env, 'sampler.rateLimit', DD_TRACE_RATE_LIMIT)
702
- this._setSamplingRule(env, 'sampler.rules', safeJsonParse(DD_TRACE_SAMPLING_RULES)) // example
714
+ this._setSamplingRule(env, 'sampler.rules', safeJsonParse(DD_TRACE_SAMPLING_RULES))
715
+ this._envUnprocessed['sampler.rules'] = DD_TRACE_SAMPLING_RULES
703
716
  this._setString(env, 'scope', DD_TRACE_SCOPE)
704
717
  this._setString(env, 'service', DD_SERVICE || DD_SERVICE_NAME || tags.service || OTEL_SERVICE_NAME)
705
718
  this._setString(env, 'site', DD_SITE)
706
719
  if (DD_TRACE_SPAN_ATTRIBUTE_SCHEMA) {
707
720
  this._setString(env, 'spanAttributeSchema', validateNamingVersion(DD_TRACE_SPAN_ATTRIBUTE_SCHEMA))
721
+ this._envUnprocessed.spanAttributeSchema = DD_TRACE_SPAN_ATTRIBUTE_SCHEMA
708
722
  }
709
723
  this._setBoolean(env, 'spanRemoveIntegrationFromService', DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED)
710
724
  this._setBoolean(env, 'startupLogs', DD_TRACE_STARTUP_LOGS)
@@ -719,6 +733,7 @@ class Config {
719
733
  this._setBoolean(env, 'telemetry.debug', DD_TELEMETRY_DEBUG)
720
734
  this._setBoolean(env, 'telemetry.dependencyCollection', DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED)
721
735
  this._setValue(env, 'telemetry.heartbeatInterval', maybeInt(Math.floor(DD_TELEMETRY_HEARTBEAT_INTERVAL * 1000)))
736
+ this._envUnprocessed['telemetry.heartbeatInterval'] = DD_TELEMETRY_HEARTBEAT_INTERVAL * 1000
722
737
  const hasTelemetryLogsUsingFeatures =
723
738
  env['iast.enabled'] || env['profiling.enabled'] || env['profiling.heuristicsEnabled']
724
739
  ? true
@@ -735,20 +750,25 @@ class Config {
735
750
  _applyOptions (options) {
736
751
  const opts = this._options = this._options || {}
737
752
  const tags = {}
753
+ this._optsUnprocessed = {}
738
754
 
739
755
  options = this.options = Object.assign({ ingestion: {} }, options, opts)
740
756
 
741
757
  tagger.add(tags, options.tags)
742
758
 
743
759
  this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec.blockedTemplateHtml))
760
+ this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec.blockedTemplateHtml
744
761
  this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec.blockedTemplateJson))
762
+ this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec.blockedTemplateJson
745
763
  this._setBoolean(opts, 'appsec.enabled', options.appsec.enabled)
746
764
  this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex)
747
765
  this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex)
748
766
  this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec.rasp?.enabled)
749
767
  this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec.rateLimit))
768
+ this._optsUnprocessed['appsec.rateLimit'] = options.appsec.rateLimit
750
769
  this._setString(opts, 'appsec.rules', options.appsec.rules)
751
770
  this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec.wafTimeout))
771
+ this._optsUnprocessed['appsec.wafTimeout'] = options.appsec.wafTimeout
752
772
  this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled)
753
773
  this._setString(opts, 'clientIpHeader', options.clientIpHeader)
754
774
  this._setString(opts, 'dbmPropagationMode', options.dbmPropagationMode)
@@ -763,22 +783,26 @@ class Config {
763
783
  this._setString(opts, 'experimental.exporter', options.experimental && options.experimental.exporter)
764
784
  this._setBoolean(opts, 'experimental.runtimeId', options.experimental && options.experimental.runtimeId)
765
785
  this._setValue(opts, 'flushInterval', maybeInt(options.flushInterval))
786
+ this._optsUnprocessed.flushInterval = options.flushInterval
766
787
  this._setValue(opts, 'flushMinSpans', maybeInt(options.flushMinSpans))
788
+ this._optsUnprocessed.flushMinSpans = options.flushMinSpans
767
789
  this._setArray(opts, 'headerTags', options.headerTags)
768
790
  this._setString(opts, 'hostname', options.hostname)
769
791
  this._setBoolean(opts, 'iast.deduplicationEnabled', options.iastOptions && options.iastOptions.deduplicationEnabled)
770
792
  this._setBoolean(opts, 'iast.enabled',
771
793
  options.iastOptions && (options.iastOptions === true || options.iastOptions.enabled === true))
772
- const iastRequestSampling = maybeInt(options.iastOptions?.requestSampling)
773
794
  this._setValue(opts, 'iast.maxConcurrentRequests',
774
795
  maybeInt(options.iastOptions?.maxConcurrentRequests))
775
- this._setValue(opts, 'iast.maxContextOperations',
776
- maybeInt(options.iastOptions && options.iastOptions.maxContextOperations))
777
- this._setBoolean(opts, 'iast.redactionEnabled', options.iastOptions && options.iastOptions.redactionEnabled)
796
+ this._optsUnprocessed['iast.maxConcurrentRequests'] = options.iastOptions?.maxConcurrentRequests
797
+ this._setValue(opts, 'iast.maxContextOperations', maybeInt(options.iastOptions?.maxContextOperations))
798
+ this._optsUnprocessed['iast.maxContextOperations'] = options.iastOptions?.maxContextOperations
799
+ this._setBoolean(opts, 'iast.redactionEnabled', options.iastOptions?.redactionEnabled)
778
800
  this._setString(opts, 'iast.redactionNamePattern', options.iastOptions?.redactionNamePattern)
779
801
  this._setString(opts, 'iast.redactionValuePattern', options.iastOptions?.redactionValuePattern)
802
+ const iastRequestSampling = maybeInt(options.iastOptions?.requestSampling)
780
803
  if (iastRequestSampling > -1 && iastRequestSampling < 101) {
781
804
  this._setValue(opts, 'iast.requestSampling', iastRequestSampling)
805
+ this._optsUnprocessed['iast.requestSampling'] = options.iastOptions?.requestSampling
782
806
  }
783
807
  this._setString(opts, 'iast.telemetryVerbosity', options.iastOptions && options.iastOptions.telemetryVerbosity)
784
808
  this._setBoolean(opts, 'isCiVisibility', options.isCiVisibility)
@@ -792,6 +816,7 @@ class Config {
792
816
  this._setString(opts, 'protocolVersion', options.protocolVersion)
793
817
  if (options.remoteConfig) {
794
818
  this._setValue(opts, 'remoteConfig.pollInterval', maybeFloat(options.remoteConfig.pollInterval))
819
+ this._optsUnprocessed['remoteConfig.pollInterval'] = options.remoteConfig.pollInterval
795
820
  }
796
821
  this._setBoolean(opts, 'reportHostname', options.reportHostname)
797
822
  this._setBoolean(opts, 'runtimeMetrics', options.runtimeMetrics)
@@ -803,6 +828,7 @@ class Config {
803
828
  this._setString(opts, 'site', options.site)
804
829
  if (options.spanAttributeSchema) {
805
830
  this._setString(opts, 'spanAttributeSchema', validateNamingVersion(options.spanAttributeSchema))
831
+ this._optsUnprocessed.spanAttributeSchema = options.spanAttributeSchema
806
832
  }
807
833
  this._setBoolean(opts, 'spanRemoveIntegrationFromService', options.spanRemoveIntegrationFromService)
808
834
  this._setBoolean(opts, 'startupLogs', options.startupLogs)
@@ -931,6 +957,7 @@ class Config {
931
957
 
932
958
  _applyRemote (options) {
933
959
  const opts = this._remote = this._remote || {}
960
+ this._remoteUnprocessed = {}
934
961
  const tags = {}
935
962
  const headerTags = options.tracing_header_tags
936
963
  ? options.tracing_header_tags.map(tag => {
@@ -947,7 +974,8 @@ class Config {
947
974
  this._setTags(opts, 'tags', tags)
948
975
  this._setBoolean(opts, 'tracing', options.tracing_enabled)
949
976
  // ignore tags for now since rc sampling rule tags format is not supported
950
- this._setSamplingRule(opts, 'sampler.rules', this._ignoreTags(options.trace_sample_rules))
977
+ this._setSamplingRule(opts, 'sampler.rules', this._ignoreTags(options.tracing_sampling_rules))
978
+ this._remoteUnprocessed['sampler.rules'] = options.tracing_sampling_rules
951
979
  }
952
980
 
953
981
  _ignoreTags (samplingRules) {
@@ -1040,18 +1068,21 @@ class Config {
1040
1068
  _merge () {
1041
1069
  const containers = [this._remote, this._options, this._env, this._calculated, this._defaults]
1042
1070
  const origins = ['remote_config', 'code', 'env_var', 'calculated', 'default']
1071
+ const unprocessedValues = [this._remoteUnprocessed, this._optsUnprocessed, this._envUnprocessed, {}, {}]
1043
1072
  const changes = []
1044
1073
 
1045
1074
  for (const name in this._defaults) {
1046
1075
  for (let i = 0; i < containers.length; i++) {
1047
1076
  const container = containers[i]
1048
1077
  const origin = origins[i]
1078
+ const unprocessed = unprocessedValues[i]
1049
1079
 
1050
1080
  if ((container[name] !== null && container[name] !== undefined) || container === this._defaults) {
1051
1081
  if (get(this, name) === container[name] && has(this, name)) break
1052
1082
 
1053
- const value = container[name]
1083
+ let value = container[name]
1054
1084
  set(this, name, value)
1085
+ value = unprocessed[name] || value
1055
1086
 
1056
1087
  changes.push({ name, value, origin })
1057
1088
 
@@ -1069,6 +1100,7 @@ function maybeInt (number) {
1069
1100
  const parsed = parseInt(number)
1070
1101
  return isNaN(parsed) ? undefined : parsed
1071
1102
  }
1103
+
1072
1104
  function maybeFloat (number) {
1073
1105
  const parsed = parseFloat(number)
1074
1106
  return isNaN(parsed) ? undefined : parsed
@@ -15,6 +15,8 @@ module.exports = {
15
15
  SAMPLING_MECHANISM_MANUAL: 4,
16
16
  SAMPLING_MECHANISM_APPSEC: 5,
17
17
  SAMPLING_MECHANISM_SPAN: 8,
18
+ SAMPLING_MECHANISM_REMOTE_USER: 11,
19
+ SAMPLING_MECHANISM_REMOTE_DYNAMIC: 12,
18
20
  SPAN_SAMPLING_MECHANISM: '_dd.span_sampling.mechanism',
19
21
  SPAN_SAMPLING_RULE_RATE: '_dd.span_sampling.rule_rate',
20
22
  SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second',
@@ -34,6 +34,7 @@ function format (span) {
34
34
  const formatted = formatSpan(span)
35
35
 
36
36
  extractSpanLinks(formatted, span)
37
+ extractSpanEvents(formatted, span)
37
38
  extractRootTags(formatted, span)
38
39
  extractChunkTags(formatted, span)
39
40
  extractTags(formatted, span)
@@ -88,6 +89,22 @@ function extractSpanLinks (trace, span) {
88
89
  if (links.length > 0) { trace.meta['_dd.span_links'] = JSON.stringify(links) }
89
90
  }
90
91
 
92
+ function extractSpanEvents (trace, span) {
93
+ const events = []
94
+ if (span._events) {
95
+ for (const event of span._events) {
96
+ const formattedEvent = {
97
+ name: event.name,
98
+ time_unix_nano: Math.round(event.startTime * 1e6),
99
+ attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
100
+ }
101
+
102
+ events.push(formattedEvent)
103
+ }
104
+ }
105
+ if (events.length > 0) { trace.meta.events = JSON.stringify(events) }
106
+ }
107
+
91
108
  function extractTags (trace, span) {
92
109
  const context = span.context()
93
110
  const origin = context._trace.origin
@@ -134,7 +151,10 @@ function extractTags (trace, span) {
134
151
  case ERROR_STACK:
135
152
  // HACK: remove when implemented in the backend
136
153
  if (context._name !== 'fs.operation') {
137
- trace.error = 1
154
+ // HACK: to ensure otel.recordException does not influence trace.error
155
+ if (tags.setTraceError) {
156
+ trace.error = 1
157
+ }
138
158
  } else {
139
159
  break
140
160
  }
@@ -142,7 +162,6 @@ function extractTags (trace, span) {
142
162
  addTag(trace.meta, trace.metrics, tag, tags[tag])
143
163
  }
144
164
  }
145
-
146
165
  setSingleSpanIngestionTags(trace, context._spanSampling)
147
166
 
148
167
  addTag(trace.meta, trace.metrics, 'language', 'javascript')
@@ -20,6 +20,20 @@ function hrTimeToMilliseconds (time) {
20
20
  return time[0] * 1e3 + time[1] / 1e6
21
21
  }
22
22
 
23
+ function isTimeInput (startTime) {
24
+ if (typeof startTime === 'number') {
25
+ return true
26
+ }
27
+ if (startTime instanceof Date) {
28
+ return true
29
+ }
30
+ if (Array.isArray(startTime) && startTime.length === 2 &&
31
+ typeof startTime[0] === 'number' && typeof startTime[1] === 'number') {
32
+ return true
33
+ }
34
+ return false
35
+ }
36
+
23
37
  const spanKindNames = {
24
38
  [api.SpanKind.INTERNAL]: kinds.INTERNAL,
25
39
  [api.SpanKind.SERVER]: kinds.SERVER,
@@ -179,17 +193,20 @@ class Span {
179
193
  }
180
194
 
181
195
  setAttribute (key, value) {
196
+ if (key === 'http.response.status_code') {
197
+ this._ddSpan.setTag('http.status_code', value.toString())
198
+ }
199
+
182
200
  this._ddSpan.setTag(key, value)
183
201
  return this
184
202
  }
185
203
 
186
204
  setAttributes (attributes) {
187
- this._ddSpan.addTags(attributes)
188
- return this
189
- }
205
+ if ('http.response.status_code' in attributes) {
206
+ attributes['http.status_code'] = attributes['http.response.status_code'].toString()
207
+ }
190
208
 
191
- addEvent (name, attributesOrStartTime, startTime) {
192
- api.diag.warn('Events not supported')
209
+ this._ddSpan.addTags(attributes)
193
210
  return this
194
211
  }
195
212
 
@@ -236,12 +253,29 @@ class Span {
236
253
  return this.ended === false
237
254
  }
238
255
 
239
- recordException (exception) {
256
+ addEvent (name, attributesOrStartTime, startTime) {
257
+ startTime = attributesOrStartTime && isTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime
258
+ const hrStartTime = timeInputToHrTime(startTime || (performance.now() + timeOrigin))
259
+ startTime = hrTimeToMilliseconds(hrStartTime)
260
+
261
+ this._ddSpan.addEvent(name, attributesOrStartTime, startTime)
262
+ return this
263
+ }
264
+
265
+ recordException (exception, timeInput) {
266
+ // HACK: identifier is added so that trace.error remains unchanged after a call to otel.recordException
240
267
  this._ddSpan.addTags({
241
268
  [ERROR_TYPE]: exception.name,
242
269
  [ERROR_MESSAGE]: exception.message,
243
- [ERROR_STACK]: exception.stack
270
+ [ERROR_STACK]: exception.stack,
271
+ doNotSetTraceError: true
244
272
  })
273
+ const attributes = {}
274
+ if (exception.message) attributes['exception.message'] = exception.message
275
+ if (exception.type) attributes['exception.type'] = exception.type
276
+ if (exception.escaped) attributes['exception.escaped'] = exception.escaped
277
+ if (exception.stack) attributes['exception.stacktrace'] = exception.stack
278
+ this.addEvent(exception.name, attributes, timeInput)
245
279
  }
246
280
 
247
281
  get duration () {
@@ -67,6 +67,8 @@ class DatadogSpan {
67
67
  this._store = storage.getStore()
68
68
  this._duration = undefined
69
69
 
70
+ this._events = []
71
+
70
72
  // For internal use only. You probably want `context()._name`.
71
73
  // This name property is not updated when the span name changes.
72
74
  // This is necessary for span count metrics.
@@ -163,6 +165,19 @@ class DatadogSpan {
163
165
  })
164
166
  }
165
167
 
168
+ addEvent (name, attributesOrStartTime, startTime) {
169
+ const event = { name }
170
+ if (attributesOrStartTime) {
171
+ if (typeof attributesOrStartTime === 'object') {
172
+ event.attributes = this._sanitizeEventAttributes(attributesOrStartTime)
173
+ } else {
174
+ startTime = attributesOrStartTime
175
+ }
176
+ }
177
+ event.startTime = startTime || this._getTime()
178
+ this._events.push(event)
179
+ }
180
+
166
181
  finish (finishTime) {
167
182
  if (this._duration !== undefined) {
168
183
  return
@@ -221,7 +236,30 @@ class DatadogSpan {
221
236
  const [key, value] = entry
222
237
  addArrayOrScalarAttributes(key, value)
223
238
  })
239
+ return sanitizedAttributes
240
+ }
241
+
242
+ _sanitizeEventAttributes (attributes = {}) {
243
+ const sanitizedAttributes = {}
224
244
 
245
+ for (const key in attributes) {
246
+ const value = attributes[key]
247
+ if (Array.isArray(value)) {
248
+ const newArray = []
249
+ for (const subkey in value) {
250
+ if (ALLOWED.includes(typeof value[subkey])) {
251
+ newArray.push(value[subkey])
252
+ } else {
253
+ log.warn('Dropping span event attribute. It is not of an allowed type')
254
+ }
255
+ }
256
+ sanitizedAttributes[key] = newArray
257
+ } else if (ALLOWED.includes(typeof value)) {
258
+ sanitizedAttributes[key] = value
259
+ } else {
260
+ log.warn('Dropping span event attribute. It is not of an allowed type')
261
+ }
262
+ }
225
263
  return sanitizedAttributes
226
264
  }
227
265
 
@@ -81,5 +81,6 @@ module.exports = {
81
81
  get 'selenium-webdriver' () { return require('../../../datadog-plugin-selenium/src') },
82
82
  get sharedb () { return require('../../../datadog-plugin-sharedb/src') },
83
83
  get tedious () { return require('../../../datadog-plugin-tedious/src') },
84
+ get undici () { return require('../../../datadog-plugin-undici/src') },
84
85
  get winston () { return require('../../../datadog-plugin-winston/src') }
85
86
  }
@@ -10,6 +10,8 @@ const {
10
10
  SAMPLING_MECHANISM_AGENT,
11
11
  SAMPLING_MECHANISM_RULE,
12
12
  SAMPLING_MECHANISM_MANUAL,
13
+ SAMPLING_MECHANISM_REMOTE_USER,
14
+ SAMPLING_MECHANISM_REMOTE_DYNAMIC,
13
15
  SAMPLING_RULE_DECISION,
14
16
  SAMPLING_LIMIT_DECISION,
15
17
  SAMPLING_AGENT_DECISION,
@@ -41,9 +43,9 @@ class PrioritySampler {
41
43
  this.update({})
42
44
  }
43
45
 
44
- configure (env, { sampleRate, rateLimit = 100, rules = [] } = {}) {
46
+ configure (env, { sampleRate, provenance = undefined, rateLimit = 100, rules = [] } = {}) {
45
47
  this._env = env
46
- this._rules = this._normalizeRules(rules, sampleRate, rateLimit)
48
+ this._rules = this._normalizeRules(rules, sampleRate, rateLimit, provenance)
47
49
  this._limiter = new RateLimiter(rateLimit)
48
50
 
49
51
  setSamplingRules(this._rules)
@@ -137,6 +139,8 @@ class PrioritySampler {
137
139
  _getPriorityByRule (context, rule) {
138
140
  context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
139
141
  context._sampling.mechanism = SAMPLING_MECHANISM_RULE
142
+ if (rule.provenance === 'customer') context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER
143
+ if (rule.provenance === 'dynamic') context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC
140
144
 
141
145
  return rule.sample() && this._isSampledByRateLimit(context)
142
146
  ? USER_KEEP
@@ -181,11 +185,11 @@ class PrioritySampler {
181
185
  }
182
186
  }
183
187
 
184
- _normalizeRules (rules, sampleRate, rateLimit) {
188
+ _normalizeRules (rules, sampleRate, rateLimit, provenance) {
185
189
  rules = [].concat(rules || [])
186
190
 
187
191
  return rules
188
- .concat({ sampleRate, maxPerSecond: rateLimit })
192
+ .concat({ sampleRate, maxPerSecond: rateLimit, provenance })
189
193
  .map(rule => ({ ...rule, sampleRate: parseFloat(rule.sampleRate) }))
190
194
  .filter(rule => !isNaN(rule.sampleRate))
191
195
  .map(SamplingRule.from)
@@ -64,7 +64,7 @@ function serviceLocator (span) {
64
64
  }
65
65
 
66
66
  class SamplingRule {
67
- constructor ({ name, service, resource, tags, sampleRate = 1.0, maxPerSecond } = {}) {
67
+ constructor ({ name, service, resource, tags, sampleRate = 1.0, provenance = undefined, maxPerSecond } = {}) {
68
68
  this.matchers = []
69
69
 
70
70
  if (name) {
@@ -82,6 +82,7 @@ class SamplingRule {
82
82
 
83
83
  this._sampler = new Sampler(sampleRate)
84
84
  this._limiter = undefined
85
+ this.provenance = provenance
85
86
 
86
87
  if (Number.isFinite(maxPerSecond)) {
87
88
  this._limiter = new RateLimiter(maxPerSecond)
@@ -30,6 +30,10 @@ const web = {
30
30
  lambda: {
31
31
  opName: () => 'aws.request',
32
32
  serviceName: awsServiceV0
33
+ },
34
+ undici: {
35
+ opName: () => 'undici.request',
36
+ serviceName: httpPluginClientService
33
37
  }
34
38
  },
35
39
  server: {
@@ -29,6 +29,10 @@ const web = {
29
29
  lambda: {
30
30
  opName: () => 'aws.lambda.invoke',
31
31
  serviceName: identityService
32
+ },
33
+ undici: {
34
+ opName: () => 'undici.request',
35
+ serviceName: httpPluginClientService
32
36
  }
33
37
  },
34
38
  server: {
@@ -1,6 +1,10 @@
1
1
  'use strict'
2
2
 
3
+ const constants = require('./constants')
3
4
  const log = require('./log')
5
+ const ERROR_MESSAGE = constants.ERROR_MESSAGE
6
+ const ERROR_STACK = constants.ERROR_STACK
7
+ const ERROR_TYPE = constants.ERROR_TYPE
4
8
 
5
9
  const otelTagMap = {
6
10
  'deployment.environment': 'env',
@@ -14,7 +18,6 @@ function add (carrier, keyValuePairs, parseOtelTags = false) {
14
18
  if (Array.isArray(keyValuePairs)) {
15
19
  return keyValuePairs.forEach(tags => add(carrier, tags))
16
20
  }
17
-
18
21
  try {
19
22
  if (typeof keyValuePairs === 'string') {
20
23
  const segments = keyValuePairs.split(',')
@@ -32,6 +35,12 @@ function add (carrier, keyValuePairs, parseOtelTags = false) {
32
35
  carrier[key.trim()] = value.trim()
33
36
  }
34
37
  } else {
38
+ // HACK: to ensure otel.recordException does not influence trace.error
39
+ if (ERROR_MESSAGE in keyValuePairs || ERROR_STACK in keyValuePairs || ERROR_TYPE in keyValuePairs) {
40
+ if (!('doNotSetTraceError' in keyValuePairs)) {
41
+ carrier.setTraceError = true
42
+ }
43
+ }
35
44
  Object.assign(carrier, keyValuePairs)
36
45
  }
37
46
  } catch (e) {