dd-trace 2.44.0 → 2.45.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": "2.44.0",
3
+ "version": "2.45.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 =>
@@ -127,11 +128,6 @@ class Config {
127
128
  'agent'
128
129
  )
129
130
  const DD_PROFILING_SOURCE_MAP = process.env.DD_PROFILING_SOURCE_MAP
130
- const DD_LOGS_INJECTION = coalesce(
131
- options.logInjection,
132
- process.env.DD_LOGS_INJECTION,
133
- false
134
- )
135
131
  const DD_RUNTIME_METRICS_ENABLED = coalesce(
136
132
  options.runtimeMetrics, // TODO: remove when enabled by default
137
133
  process.env.DD_RUNTIME_METRICS_ENABLED,
@@ -236,7 +232,7 @@ class Config {
236
232
  !inServerlessEnvironment
237
233
  )
238
234
  const DD_TELEMETRY_HEARTBEAT_INTERVAL = process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL
239
- ? parseInt(process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL) * 1000
235
+ ? Math.floor(parseFloat(process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL) * 1000)
240
236
  : 60000
241
237
  const DD_OPENAI_SPAN_CHAR_LIMIT = process.env.DD_OPENAI_SPAN_CHAR_LIMIT
242
238
  ? parseInt(process.env.DD_OPENAI_SPAN_CHAR_LIMIT)
@@ -438,8 +434,8 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
438
434
  !inServerlessEnvironment
439
435
  )
440
436
  const DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS = coalesce(
441
- parseInt(remoteConfigOptions.pollInterval),
442
- parseInt(process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS),
437
+ parseFloat(remoteConfigOptions.pollInterval),
438
+ parseFloat(process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS),
443
439
  5 // seconds
444
440
  )
445
441
 
@@ -507,11 +503,6 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
507
503
  const ingestion = options.ingestion || {}
508
504
  const dogstatsd = coalesce(options.dogstatsd, {})
509
505
  const sampler = {
510
- sampleRate: coalesce(
511
- options.sampleRate,
512
- process.env.DD_TRACE_SAMPLE_RATE,
513
- ingestion.sampleRate
514
- ),
515
506
  rateLimit: coalesce(options.rateLimit, process.env.DD_TRACE_RATE_LIMIT, ingestion.rateLimit),
516
507
  rules: coalesce(
517
508
  options.samplingRules,
@@ -535,14 +526,13 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
535
526
  })
536
527
  }
537
528
 
538
- const defaultFlushInterval = inServerlessEnvironment ? 0 : 2000
529
+ const defaultFlushInterval = inAWSLambda ? 0 : 2000
539
530
 
540
531
  this.tracing = !isFalse(DD_TRACING_ENABLED)
541
532
  this.dbmPropagationMode = DD_DBM_PROPAGATION_MODE
542
533
  this.dsmEnabled = isTrue(DD_DATA_STREAMS_ENABLED)
543
534
  this.openAiLogsEnabled = DD_OPENAI_LOGS_ENABLED
544
535
  this.apiKey = DD_API_KEY
545
- this.logInjection = isTrue(DD_LOGS_INJECTION)
546
536
  this.env = DD_ENV
547
537
  this.url = DD_CIVISIBILITY_AGENTLESS_URL ? new URL(DD_CIVISIBILITY_AGENTLESS_URL)
548
538
  : getAgentUrl(DD_TRACE_AGENT_URL, options)
@@ -551,7 +541,6 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
551
541
  this.port = String(DD_TRACE_AGENT_PORT || (this.url && this.url.port))
552
542
  this.flushInterval = coalesce(parseInt(options.flushInterval, 10), defaultFlushInterval)
553
543
  this.flushMinSpans = DD_TRACE_PARTIAL_FLUSH_MIN_SPANS
554
- this.sampleRate = coalesce(Math.min(Math.max(sampler.sampleRate, 0), 1), 1)
555
544
  this.queryStringObfuscation = DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP
556
545
  this.clientIpEnabled = DD_TRACE_CLIENT_IP_ENABLED
557
546
  this.clientIpHeader = DD_TRACE_CLIENT_IP_HEADER
@@ -684,6 +673,139 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
684
673
  version: this.version,
685
674
  'runtime-id': uuid()
686
675
  })
676
+
677
+ this._applyDefaults()
678
+ this._applyEnvironment()
679
+ this._applyOptions(options)
680
+ this._applyRemote({})
681
+ this._merge()
682
+ }
683
+
684
+ // Supports only a subset of options for now.
685
+ configure (options, remote) {
686
+ if (remote) {
687
+ this._applyRemote(options)
688
+ } else {
689
+ this._applyOptions(options)
690
+ }
691
+
692
+ this._merge()
693
+ }
694
+
695
+ _applyDefaults () {
696
+ const defaults = this._defaults = {}
697
+
698
+ this._setUnit(defaults, 'sampleRate', undefined)
699
+ this._setBoolean(defaults, 'logInjection', false)
700
+ this._setArray(defaults, 'headerTags', [])
701
+ }
702
+
703
+ _applyEnvironment () {
704
+ const {
705
+ DD_TRACE_SAMPLE_RATE,
706
+ DD_LOGS_INJECTION,
707
+ DD_TRACE_HEADER_TAGS
708
+ } = process.env
709
+
710
+ const env = this._env = {}
711
+
712
+ this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE)
713
+ this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
714
+ this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
715
+ }
716
+
717
+ _applyOptions (options) {
718
+ const opts = this._options = this._options || {}
719
+
720
+ options = Object.assign({ ingestion: {} }, options, opts)
721
+
722
+ this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate))
723
+ this._setBoolean(opts, 'logInjection', options.logInjection)
724
+ this._setArray(opts, 'headerTags', options.headerTags)
725
+ }
726
+
727
+ _applyRemote (options) {
728
+ const opts = this._remote = this._remote || {}
729
+ const headerTags = options.tracing_header_tags
730
+ ? options.tracing_header_tags.map(tag => {
731
+ return tag.tag_name ? `${tag.header}:${tag.tag_name}` : tag.header
732
+ })
733
+ : undefined
734
+
735
+ this._setUnit(opts, 'sampleRate', options.tracing_sampling_rate)
736
+ this._setBoolean(opts, 'logInjection', options.log_injection_enabled)
737
+ this._setArray(opts, 'headerTags', headerTags)
738
+ }
739
+
740
+ _setBoolean (obj, name, value) {
741
+ if (value === undefined || value === null) {
742
+ this._setValue(obj, name, value)
743
+ } else if (isTrue(value)) {
744
+ this._setValue(obj, name, true)
745
+ } else if (isFalse(value)) {
746
+ this._setValue(obj, name, false)
747
+ }
748
+ }
749
+
750
+ _setUnit (obj, name, value) {
751
+ if (value === null || value === undefined) {
752
+ return this._setValue(obj, name, value)
753
+ }
754
+
755
+ value = parseFloat(value)
756
+
757
+ if (!isNaN(value)) {
758
+ // TODO: Ignore out of range values instead of normalizing them.
759
+ this._setValue(obj, name, Math.min(Math.max(value, 0), 1))
760
+ }
761
+ }
762
+
763
+ _setArray (obj, name, value) {
764
+ if (value === null || value === undefined) {
765
+ return this._setValue(obj, name, null)
766
+ }
767
+
768
+ if (typeof value === 'string') {
769
+ value = value && value.split(',')
770
+ }
771
+
772
+ if (Array.isArray(value)) {
773
+ this._setValue(obj, name, value)
774
+ }
775
+ }
776
+
777
+ _setValue (obj, name, value) {
778
+ obj[name] = value
779
+ }
780
+
781
+ // TODO: Report origin changes and errors to telemetry.
782
+ // TODO: Deeply merge configurations.
783
+ // TODO: Move change tracking to telemetry.
784
+ _merge () {
785
+ const containers = [this._remote, this._options, this._env, this._defaults]
786
+ const origins = ['remote_config', 'code', 'env_var', 'default']
787
+ const changes = []
788
+
789
+ for (const name in this._defaults) {
790
+ for (let i = 0; i < containers.length; i++) {
791
+ const container = containers[i]
792
+ const origin = origins[i]
793
+
794
+ if ((container[name] !== null && container[name] !== undefined) || container === this._defaults) {
795
+ if (this[name] === container[name] && this.hasOwnProperty(name)) break
796
+
797
+ const value = this[name] = container[name]
798
+
799
+ changes.push({ name, value, origin })
800
+
801
+ break
802
+ }
803
+ }
804
+ }
805
+
806
+ this.sampler.sampleRate = this.sampleRate
807
+
808
+ updateConfig(changes, this)
687
809
  }
688
810
  }
689
811
 
@@ -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) {