dd-trace 4.10.0 → 4.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -30,6 +30,8 @@ function createWrapEmit (ctx) {
30
30
  function createWrapRequest (authority, options) {
31
31
  return function wrapRequest (request) {
32
32
  return function (headers) {
33
+ if (!startChannel.hasSubscribers) return request.apply(this, arguments)
34
+
33
35
  const ctx = { headers, authority, options }
34
36
 
35
37
  return startChannel.runStores(ctx, () => {
@@ -121,11 +121,11 @@ function addResponseHeaders (res, span, config) {
121
121
  ? Object.fromEntries(res.headers.entries())
122
122
  : res.headers
123
123
 
124
- config.headers.forEach(key => {
124
+ config.headers.forEach(([key, tag]) => {
125
125
  const value = headers[key]
126
126
 
127
127
  if (value) {
128
- span.setTag(`${HTTP_RESPONSE_HEADERS}.${key}`, value)
128
+ span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, value)
129
129
  }
130
130
  })
131
131
  }
@@ -135,11 +135,11 @@ function addRequestHeaders (req, span, config) {
135
135
  ? Object.fromEntries(req.headers.entries())
136
136
  : req.headers || req.getHeaders()
137
137
 
138
- config.headers.forEach(key => {
139
- const value = headers[key]
138
+ config.headers.forEach(([key, tag]) => {
139
+ const value = Array.isArray(headers[key]) ? headers[key].toString() : headers[key]
140
140
 
141
141
  if (value) {
142
- span.setTag(`${HTTP_REQUEST_HEADERS}.${key}`, Array.isArray(value) ? value.toString() : value)
142
+ span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, value)
143
143
  }
144
144
  })
145
145
  }
@@ -182,7 +182,8 @@ function getHeaders (config) {
182
182
 
183
183
  return config.headers
184
184
  .filter(key => typeof key === 'string')
185
- .map(key => key.toLowerCase())
185
+ .map(h => h.split(':'))
186
+ .map(([key, tag]) => [key.toLowerCase(), tag])
186
187
  }
187
188
 
188
189
  function getHooks (config) {
@@ -10,7 +10,7 @@ class PGPlugin extends DatabasePlugin {
10
10
 
11
11
  start ({ params = {}, query, processId }) {
12
12
  const service = this.serviceName({ pluginConfig: this.config, params })
13
- const originalStatement = query.text
13
+ const originalStatement = this.maybeTruncate(query.text)
14
14
 
15
15
  this.startSpan(this.operationName(), {
16
16
  service,
@@ -29,6 +29,8 @@ function enable (config) {
29
29
  }
30
30
  })
31
31
  }
32
+
33
+ return rc
32
34
  }
33
35
 
34
36
  function enableWafUpdate (appsecConfig) {
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { URL, format } = require('url')
3
4
  const uuid = require('crypto-randomuuid')
4
5
  const { EventEmitter } = require('events')
5
6
  const Scheduler = require('./scheduler')
@@ -22,13 +23,17 @@ class RemoteConfigManager extends EventEmitter {
22
23
  constructor (config) {
23
24
  super()
24
25
 
25
- const pollInterval = config.remoteConfig.pollInterval * 1000
26
+ const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000)
27
+ const url = config.url || new URL(format({
28
+ protocol: 'http:',
29
+ hostname: config.hostname || 'localhost',
30
+ port: config.port
31
+ }))
32
+
26
33
  this.scheduler = new Scheduler((cb) => this.poll(cb), pollInterval)
27
34
 
28
35
  this.requestOptions = {
29
- url: config.url,
30
- hostname: config.hostname,
31
- port: config.port,
36
+ url,
32
37
  method: 'POST',
33
38
  path: '/v0.7/config'
34
39
  }
@@ -11,6 +11,7 @@ const tagger = require('./tagger')
11
11
  const { isTrue, isFalse } = require('./util')
12
12
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('./plugins/util/tags')
13
13
  const { getGitMetadataFromGitProperties } = require('./git_properties')
14
+ const { updateConfig } = require('./telemetry')
14
15
  const { getIsGCPFunction, getIsAzureFunctionConsumptionPlan } = require('./serverless')
15
16
 
16
17
  const fromEntries = Object.fromEntries || (entries =>
@@ -130,11 +131,6 @@ class Config {
130
131
  'agent'
131
132
  )
132
133
  const DD_PROFILING_SOURCE_MAP = process.env.DD_PROFILING_SOURCE_MAP
133
- const DD_LOGS_INJECTION = coalesce(
134
- options.logInjection,
135
- process.env.DD_LOGS_INJECTION,
136
- false
137
- )
138
134
  const DD_RUNTIME_METRICS_ENABLED = coalesce(
139
135
  options.runtimeMetrics, // TODO: remove when enabled by default
140
136
  process.env.DD_RUNTIME_METRICS_ENABLED,
@@ -239,7 +235,7 @@ class Config {
239
235
  !inServerlessEnvironment
240
236
  )
241
237
  const DD_TELEMETRY_HEARTBEAT_INTERVAL = process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL
242
- ? parseInt(process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL) * 1000
238
+ ? Math.floor(parseFloat(process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL) * 1000)
243
239
  : 60000
244
240
  const DD_OPENAI_SPAN_CHAR_LIMIT = process.env.DD_OPENAI_SPAN_CHAR_LIMIT
245
241
  ? parseInt(process.env.DD_OPENAI_SPAN_CHAR_LIMIT)
@@ -441,8 +437,8 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
441
437
  !inServerlessEnvironment
442
438
  )
443
439
  const DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS = coalesce(
444
- parseInt(remoteConfigOptions.pollInterval),
445
- parseInt(process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS),
440
+ parseFloat(remoteConfigOptions.pollInterval),
441
+ parseFloat(process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS),
446
442
  5 // seconds
447
443
  )
448
444
 
@@ -510,11 +506,6 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
510
506
  const ingestion = options.ingestion || {}
511
507
  const dogstatsd = coalesce(options.dogstatsd, {})
512
508
  const sampler = {
513
- sampleRate: coalesce(
514
- options.sampleRate,
515
- process.env.DD_TRACE_SAMPLE_RATE,
516
- ingestion.sampleRate
517
- ),
518
509
  rateLimit: coalesce(options.rateLimit, process.env.DD_TRACE_RATE_LIMIT, ingestion.rateLimit),
519
510
  rules: coalesce(
520
511
  options.samplingRules,
@@ -538,14 +529,13 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
538
529
  })
539
530
  }
540
531
 
541
- const defaultFlushInterval = inServerlessEnvironment ? 0 : 2000
532
+ const defaultFlushInterval = inAWSLambda ? 0 : 2000
542
533
 
543
534
  this.tracing = !isFalse(DD_TRACING_ENABLED)
544
535
  this.dbmPropagationMode = DD_DBM_PROPAGATION_MODE
545
536
  this.dsmEnabled = isTrue(DD_DATA_STREAMS_ENABLED)
546
537
  this.openAiLogsEnabled = DD_OPENAI_LOGS_ENABLED
547
538
  this.apiKey = DD_API_KEY
548
- this.logInjection = isTrue(DD_LOGS_INJECTION)
549
539
  this.env = DD_ENV
550
540
  this.url = DD_CIVISIBILITY_AGENTLESS_URL ? new URL(DD_CIVISIBILITY_AGENTLESS_URL)
551
541
  : getAgentUrl(DD_TRACE_AGENT_URL, options)
@@ -554,7 +544,6 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
554
544
  this.port = String(DD_TRACE_AGENT_PORT || (this.url && this.url.port))
555
545
  this.flushInterval = coalesce(parseInt(options.flushInterval, 10), defaultFlushInterval)
556
546
  this.flushMinSpans = DD_TRACE_PARTIAL_FLUSH_MIN_SPANS
557
- this.sampleRate = coalesce(Math.min(Math.max(sampler.sampleRate, 0), 1), 1)
558
547
  this.queryStringObfuscation = DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP
559
548
  this.clientIpEnabled = DD_TRACE_CLIENT_IP_ENABLED
560
549
  this.clientIpHeader = DD_TRACE_CLIENT_IP_HEADER
@@ -687,6 +676,139 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
687
676
  version: this.version,
688
677
  'runtime-id': uuid()
689
678
  })
679
+
680
+ this._applyDefaults()
681
+ this._applyEnvironment()
682
+ this._applyOptions(options)
683
+ this._applyRemote({})
684
+ this._merge()
685
+ }
686
+
687
+ // Supports only a subset of options for now.
688
+ configure (options, remote) {
689
+ if (remote) {
690
+ this._applyRemote(options)
691
+ } else {
692
+ this._applyOptions(options)
693
+ }
694
+
695
+ this._merge()
696
+ }
697
+
698
+ _applyDefaults () {
699
+ const defaults = this._defaults = {}
700
+
701
+ this._setUnit(defaults, 'sampleRate', undefined)
702
+ this._setBoolean(defaults, 'logInjection', false)
703
+ this._setArray(defaults, 'headerTags', [])
704
+ }
705
+
706
+ _applyEnvironment () {
707
+ const {
708
+ DD_TRACE_SAMPLE_RATE,
709
+ DD_LOGS_INJECTION,
710
+ DD_TRACE_HEADER_TAGS
711
+ } = process.env
712
+
713
+ const env = this._env = {}
714
+
715
+ this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE)
716
+ this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
717
+ this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
718
+ }
719
+
720
+ _applyOptions (options) {
721
+ const opts = this._options = this._options || {}
722
+
723
+ options = Object.assign({ ingestion: {} }, options, opts)
724
+
725
+ this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate))
726
+ this._setBoolean(opts, 'logInjection', options.logInjection)
727
+ this._setArray(opts, 'headerTags', options.headerTags)
728
+ }
729
+
730
+ _applyRemote (options) {
731
+ const opts = this._remote = this._remote || {}
732
+ const headerTags = options.tracing_header_tags
733
+ ? options.tracing_header_tags.map(tag => {
734
+ return tag.tag_name ? `${tag.header}:${tag.tag_name}` : tag.header
735
+ })
736
+ : undefined
737
+
738
+ this._setUnit(opts, 'sampleRate', options.tracing_sampling_rate)
739
+ this._setBoolean(opts, 'logInjection', options.log_injection_enabled)
740
+ this._setArray(opts, 'headerTags', headerTags)
741
+ }
742
+
743
+ _setBoolean (obj, name, value) {
744
+ if (value === undefined || value === null) {
745
+ this._setValue(obj, name, value)
746
+ } else if (isTrue(value)) {
747
+ this._setValue(obj, name, true)
748
+ } else if (isFalse(value)) {
749
+ this._setValue(obj, name, false)
750
+ }
751
+ }
752
+
753
+ _setUnit (obj, name, value) {
754
+ if (value === null || value === undefined) {
755
+ return this._setValue(obj, name, value)
756
+ }
757
+
758
+ value = parseFloat(value)
759
+
760
+ if (!isNaN(value)) {
761
+ // TODO: Ignore out of range values instead of normalizing them.
762
+ this._setValue(obj, name, Math.min(Math.max(value, 0), 1))
763
+ }
764
+ }
765
+
766
+ _setArray (obj, name, value) {
767
+ if (value === null || value === undefined) {
768
+ return this._setValue(obj, name, null)
769
+ }
770
+
771
+ if (typeof value === 'string') {
772
+ value = value && value.split(',')
773
+ }
774
+
775
+ if (Array.isArray(value)) {
776
+ this._setValue(obj, name, value)
777
+ }
778
+ }
779
+
780
+ _setValue (obj, name, value) {
781
+ obj[name] = value
782
+ }
783
+
784
+ // TODO: Report origin changes and errors to telemetry.
785
+ // TODO: Deeply merge configurations.
786
+ // TODO: Move change tracking to telemetry.
787
+ _merge () {
788
+ const containers = [this._remote, this._options, this._env, this._defaults]
789
+ const origins = ['remote_config', 'code', 'env_var', 'default']
790
+ const changes = []
791
+
792
+ for (const name in this._defaults) {
793
+ for (let i = 0; i < containers.length; i++) {
794
+ const container = containers[i]
795
+ const origin = origins[i]
796
+
797
+ if ((container[name] !== null && container[name] !== undefined) || container === this._defaults) {
798
+ if (this[name] === container[name] && this.hasOwnProperty(name)) break
799
+
800
+ const value = this[name] = container[name]
801
+
802
+ changes.push({ name, value, origin })
803
+
804
+ break
805
+ }
806
+ }
807
+ }
808
+
809
+ this.sampler.sampleRate = this.sampleRate
810
+
811
+ updateConfig(changes, this)
690
812
  }
691
813
  }
692
814
 
@@ -9,6 +9,8 @@ class NoopTracer {
9
9
  this._span = new Span(this)
10
10
  }
11
11
 
12
+ configure (options) {}
13
+
12
14
  trace (name, options, fn) {
13
15
  return fn(this._span, () => {})
14
16
  }
@@ -134,6 +134,7 @@ module.exports = class PluginManager {
134
134
  queryStringObfuscation,
135
135
  site,
136
136
  url,
137
+ headerTags,
137
138
  dbmPropagationMode,
138
139
  dsmEnabled,
139
140
  clientIpEnabled
@@ -162,6 +163,7 @@ module.exports = class PluginManager {
162
163
 
163
164
  sharedConfig.site = site
164
165
  sharedConfig.url = url
166
+ sharedConfig.headers = headerTags || []
165
167
 
166
168
  return sharedConfig
167
169
  }
@@ -55,6 +55,18 @@ class DatabasePlugin extends StoragePlugin {
55
55
  return `/*${servicePropagation},traceparent='${traceparent}'*/ ${query}`
56
56
  }
57
57
  }
58
+
59
+ maybeTruncate (query) {
60
+ const maxLength = typeof this.config.truncate === 'number'
61
+ ? this.config.truncate
62
+ : 5000 // same as what the agent does
63
+
64
+ if (this.config.truncate && query && query.length > maxLength) {
65
+ query = `${query.slice(0, maxLength - 3)}...`
66
+ }
67
+
68
+ return query
69
+ }
58
70
  }
59
71
 
60
72
  module.exports = DatabasePlugin
@@ -477,16 +477,16 @@ function addResourceTag (context) {
477
477
  function addHeaders (context) {
478
478
  const { req, res, config, span } = context
479
479
 
480
- config.headers.forEach(key => {
480
+ config.headers.forEach(([key, tag]) => {
481
481
  const reqHeader = req.headers[key]
482
482
  const resHeader = res.getHeader(key)
483
483
 
484
484
  if (reqHeader) {
485
- span.setTag(`${HTTP_REQUEST_HEADERS}.${key}`, reqHeader)
485
+ span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader)
486
486
  }
487
487
 
488
488
  if (resHeader) {
489
- span.setTag(`${HTTP_RESPONSE_HEADERS}.${key}`, resHeader)
489
+ span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader)
490
490
  }
491
491
  })
492
492
  }
@@ -512,7 +512,9 @@ function getProtocol (req) {
512
512
  function getHeadersToRecord (config) {
513
513
  if (Array.isArray(config.headers)) {
514
514
  try {
515
- return config.headers.map(key => key.toLowerCase())
515
+ return config.headers
516
+ .map(h => h.split(':'))
517
+ .map(([key, tag]) => [key.toLowerCase(), tag])
516
518
  } catch (err) {
517
519
  log.error(err)
518
520
  }
@@ -29,14 +29,17 @@ const DEFAULT_KEY = 'service:,env:'
29
29
  const defaultSampler = new Sampler(AUTO_KEEP)
30
30
 
31
31
  class PrioritySampler {
32
- constructor (env, { sampleRate, rateLimit = 100, rules = [] } = {}) {
32
+ constructor (env, config) {
33
+ this.configure(env, config)
34
+ this.update({})
35
+ }
36
+
37
+ configure (env, { sampleRate, rateLimit = 100, rules = [] } = {}) {
33
38
  this._env = env
34
39
  this._rules = this._normalizeRules(rules, sampleRate)
35
40
  this._limiter = new RateLimiter(rateLimit)
36
41
 
37
42
  setSamplingRules(this._rules)
38
-
39
- this.update({})
40
43
  }
41
44
 
42
45
  isSampled (span) {
@@ -24,10 +24,23 @@ class Tracer extends NoopProxy {
24
24
  this._initialized = true
25
25
 
26
26
  try {
27
- const config = new Config(options) // TODO: support dynamic config
27
+ const config = new Config(options) // TODO: support dynamic code config
28
28
 
29
29
  if (config.remoteConfig.enabled && !config.isCiVisibility) {
30
- remoteConfig.enable(config)
30
+ const rc = remoteConfig.enable(config)
31
+
32
+ rc.on('APM_TRACING', (action, conf) => {
33
+ if (action === 'unapply') {
34
+ config.configure({}, true)
35
+ } else {
36
+ config.configure(conf.lib_config, true)
37
+ }
38
+
39
+ if (config.tracing) {
40
+ this._tracer.configure(config)
41
+ this._pluginManager.configure(config)
42
+ }
43
+ })
31
44
  }
32
45
 
33
46
  if (config.isGCPFunction || config.isAzureFunctionConsumptionPlan) {
@@ -49,6 +62,9 @@ class Tracer extends NoopProxy {
49
62
  }
50
63
 
51
64
  if (config.tracing) {
65
+ // TODO: This should probably not require tracing to be enabled.
66
+ telemetry.start(config, this._pluginManager)
67
+
52
68
  // dirty require for now so zero appsec code is executed unless explicitly enabled
53
69
  if (config.appsec.enabled) {
54
70
  require('./appsec').enable(config)
@@ -63,7 +79,6 @@ class Tracer extends NoopProxy {
63
79
 
64
80
  this._pluginManager.configure(config)
65
81
  setStartupLogPluginManager(this._pluginManager)
66
- telemetry.start(config, this._pluginManager)
67
82
 
68
83
  if (config.isManualApiEnabled) {
69
84
  const TestApiManualPlugin = require('./ci-visibility/test-api-manual/test-api-manual-plugin')
@@ -62,7 +62,7 @@ function startupLog ({ agentError } = {}) {
62
62
  out.agent_error = agentError.message
63
63
  }
64
64
  out.debug = !!config.debug
65
- out.sample_rate = config.sampleRate
65
+ out.sample_rate = config.sampler.sampleRate
66
66
  out.sampling_rules = samplingRules
67
67
  out.tags = config.tags
68
68
  if (config.tags && config.tags.version) {
@@ -66,7 +66,7 @@ function onBeforeExit () {
66
66
  sendData(config, application, host, 'app-closing')
67
67
  }
68
68
 
69
- function createAppObject () {
69
+ function createAppObject (config) {
70
70
  return {
71
71
  service_name: config.service,
72
72
  env: config.env,
@@ -116,7 +116,7 @@ function start (aConfig, thePluginManager) {
116
116
  }
117
117
  config = aConfig
118
118
  pluginManager = thePluginManager
119
- application = createAppObject()
119
+ application = createAppObject(config)
120
120
  host = createHostObject()
121
121
  heartbeatInterval = config.telemetry.heartbeatInterval
122
122
 
@@ -155,8 +155,36 @@ function updateIntegrations () {
155
155
  sendData(config, application, host, 'app-integrations-change', { integrations })
156
156
  }
157
157
 
158
+ function updateConfig (changes, config) {
159
+ if (!config.telemetry.enabled) return
160
+ if (changes.length === 0) return
161
+
162
+ // Hack to make system tests happy until we ship telemetry v2
163
+ if (process.env.DD_INTERNAL_TELEMETRY_V2_ENABLED !== '1') return
164
+
165
+ const application = createAppObject(config)
166
+ const host = createHostObject()
167
+
168
+ const names = {
169
+ sampleRate: 'DD_TRACE_SAMPLE_RATE',
170
+ logInjection: 'DD_LOG_INJECTION',
171
+ headerTags: 'DD_TRACE_HEADER_TAGS'
172
+ }
173
+
174
+ const configuration = changes.map(change => ({
175
+ name: names[change.name],
176
+ value: Array.isArray(change.value) ? change.value.join(',') : change.value,
177
+ origin: change.origin
178
+ }))
179
+
180
+ sendData(config, application, host, 'app-client-configuration-change', {
181
+ configuration
182
+ })
183
+ }
184
+
158
185
  module.exports = {
159
186
  start,
160
187
  stop,
161
- updateIntegrations
188
+ updateIntegrations,
189
+ updateConfig
162
190
  }
@@ -25,6 +25,10 @@ class DatadogTracer extends Tracer {
25
25
  setStartupLogConfig(config)
26
26
  }
27
27
 
28
+ configure ({ env, sampler }) {
29
+ this._prioritySampler.configure(env, sampler)
30
+ }
31
+
28
32
  // todo[piochelepiotr] These two methods are not related to the tracer, but to data streams monitoring.
29
33
  // They should be moved outside of the tracer in the future.
30
34
  setCheckpoint (edgeTags) {