dd-trace 2.14.0 → 2.15.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 (43) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ext/tags.d.ts +2 -1
  3. package/ext/tags.js +2 -1
  4. package/index.d.ts +43 -20
  5. package/package.json +4 -3
  6. package/packages/datadog-instrumentations/src/crypto.js +30 -0
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  8. package/packages/datadog-instrumentations/src/http/server.js +1 -1
  9. package/packages/datadog-instrumentations/src/net.js +13 -0
  10. package/packages/datadog-plugin-mongodb-core/src/index.js +19 -10
  11. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -0
  12. package/packages/dd-trace/src/appsec/iast/analyzers/index.js +20 -0
  13. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +48 -0
  14. package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +24 -0
  15. package/packages/dd-trace/src/appsec/iast/iast-context.js +50 -0
  16. package/packages/dd-trace/src/appsec/iast/index.js +59 -0
  17. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +94 -0
  18. package/packages/dd-trace/src/appsec/iast/path-line.js +70 -0
  19. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +113 -0
  20. package/packages/dd-trace/src/config.js +76 -10
  21. package/packages/dd-trace/src/constants.js +9 -1
  22. package/packages/dd-trace/src/encode/span-stats.js +155 -0
  23. package/packages/dd-trace/src/exporters/agent/index.js +14 -2
  24. package/packages/dd-trace/src/exporters/agent/writer.js +6 -3
  25. package/packages/dd-trace/src/exporters/common/request.js +6 -2
  26. package/packages/dd-trace/src/exporters/span-stats/index.js +20 -0
  27. package/packages/dd-trace/src/exporters/span-stats/writer.js +54 -0
  28. package/packages/dd-trace/src/format.js +2 -0
  29. package/packages/dd-trace/src/iitm.js +11 -0
  30. package/packages/dd-trace/src/opentracing/propagation/text_map.js +71 -0
  31. package/packages/dd-trace/src/opentracing/tracer.js +1 -1
  32. package/packages/dd-trace/src/plugin_manager.js +12 -2
  33. package/packages/dd-trace/src/plugins/log_plugin.js +16 -9
  34. package/packages/dd-trace/src/plugins/util/ip_blocklist.js +25 -0
  35. package/packages/dd-trace/src/plugins/util/web.js +99 -2
  36. package/packages/dd-trace/src/priority_sampler.js +36 -1
  37. package/packages/dd-trace/src/proxy.js +3 -0
  38. package/packages/dd-trace/src/ritm.js +10 -1
  39. package/packages/dd-trace/src/span_processor.js +7 -1
  40. package/packages/dd-trace/src/span_stats.js +210 -0
  41. package/packages/dd-trace/src/telemetry/dependencies.js +83 -0
  42. package/packages/dd-trace/src/{telemetry.js → telemetry/index.js} +10 -65
  43. package/packages/dd-trace/src/telemetry/send-data.js +35 -0
@@ -0,0 +1,113 @@
1
+ const { MANUAL_KEEP } = require('../../../../../ext/tags')
2
+ const VULNERABILITIES_KEY = 'vulnerabilities'
3
+ const IAST_JSON_TAG_KEY = '_dd.iast.json'
4
+
5
+ function createVulnerability (type, evidence, spanId, location) {
6
+ if (type && evidence && spanId) {
7
+ return {
8
+ type,
9
+ evidence,
10
+ location: {
11
+ spanId,
12
+ ...location
13
+ },
14
+ hash: createHash(type, location)
15
+ }
16
+ }
17
+ return null
18
+ }
19
+
20
+ function createHash (type, location) {
21
+ let hashSource
22
+ if (location) {
23
+ hashSource = `${type}:${location.path}:${location.line}`
24
+ } else {
25
+ hashSource = type
26
+ }
27
+ let hash = 0
28
+ let offset = 0
29
+ const size = hashSource.length
30
+ for (let i = 0; i < size; i++) {
31
+ hash = ((hash << 5) - hash) + hashSource.charCodeAt(offset++)
32
+ }
33
+ return hash
34
+ }
35
+
36
+ function addVulnerability (iastContext, vulnerability) {
37
+ if (iastContext && vulnerability && vulnerability.evidence && vulnerability.type &&
38
+ vulnerability.location && vulnerability.location.spanId) {
39
+ iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || []
40
+ iastContext[VULNERABILITIES_KEY].push(vulnerability)
41
+ }
42
+ }
43
+
44
+ function isValidVulnerability (vulnerability) {
45
+ return vulnerability && vulnerability.type &&
46
+ vulnerability.evidence && vulnerability.evidence.value &&
47
+ vulnerability.location && vulnerability.location.spanId
48
+ }
49
+
50
+ function jsonVulnerabilityFromVulnerability (vulnerability) {
51
+ const jsonVulnerability = {
52
+ type: vulnerability.type,
53
+ hash: vulnerability.hash,
54
+ evidence: {
55
+ value: vulnerability.evidence.value
56
+ },
57
+ location: {
58
+ spanId: vulnerability.location.spanId
59
+ }
60
+ }
61
+ if (vulnerability.location.path) {
62
+ jsonVulnerability.location.path = vulnerability.location.path
63
+ }
64
+ if (vulnerability.location.line) {
65
+ jsonVulnerability.location.line = vulnerability.location.line
66
+ }
67
+ return jsonVulnerability
68
+ }
69
+
70
+ function sendVulnerabilities (iastContext) {
71
+ if (iastContext && iastContext.rootSpan && iastContext[VULNERABILITIES_KEY] &&
72
+ iastContext[VULNERABILITIES_KEY].length && iastContext.rootSpan.addTags) {
73
+ const span = iastContext.rootSpan
74
+ const allVulnerabilities = iastContext[VULNERABILITIES_KEY]
75
+ // TODO support sources and ranges
76
+ const jsonToSend = {
77
+ vulnerabilities: []
78
+ }
79
+
80
+ deduplicateVulnerabilities(allVulnerabilities).forEach((vulnerability) => {
81
+ if (isValidVulnerability(vulnerability)) {
82
+ jsonToSend.vulnerabilities.push(jsonVulnerabilityFromVulnerability(vulnerability))
83
+ }
84
+ })
85
+
86
+ if (jsonToSend.vulnerabilities.length > 0) {
87
+ const tags = {}
88
+ // TODO: Store this outside of the span and set the tag in the exporter.
89
+ tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend)
90
+ tags[MANUAL_KEEP] = 'true'
91
+ span.addTags(tags)
92
+ }
93
+ }
94
+ return IAST_JSON_TAG_KEY
95
+ }
96
+
97
+ function deduplicateVulnerabilities (vulnerabilities) {
98
+ const uniqueVulnerabilities = new Set()
99
+ return vulnerabilities.filter((vulnerability) => {
100
+ const key = `${vulnerability.type}${vulnerability.hash}`
101
+ if (!uniqueVulnerabilities.has(key)) {
102
+ uniqueVulnerabilities.add(key)
103
+ return true
104
+ }
105
+ return false
106
+ })
107
+ }
108
+
109
+ module.exports = {
110
+ createVulnerability,
111
+ addVulnerability,
112
+ sendVulnerabilities
113
+ }
@@ -13,6 +13,14 @@ const uuid = require('crypto-randomuuid')
13
13
  const fromEntries = Object.fromEntries || (entries =>
14
14
  entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}))
15
15
 
16
+ function safeJsonParse (input) {
17
+ try {
18
+ return JSON.parse(input)
19
+ } catch (err) {
20
+ return undefined
21
+ }
22
+ }
23
+
16
24
  class Config {
17
25
  constructor (options) {
18
26
  options = options || {}
@@ -115,6 +123,14 @@ class Config {
115
123
  parseInt(process.env.DD_TRACE_PARTIAL_FLUSH_MIN_SPANS),
116
124
  1000
117
125
  )
126
+ const DD_TRACE_CLIENT_IP_HEADER_DISABLED = coalesce(
127
+ process.env.DD_TRACE_CLIENT_IP_HEADER_DISABLED,
128
+ false
129
+ )
130
+ const DD_TRACE_CLIENT_IP_HEADER = coalesce(
131
+ process.env.DD_TRACE_CLIENT_IP_HEADER,
132
+ null
133
+ )
118
134
  const DD_TRACE_B3_ENABLED = coalesce(
119
135
  options.experimental && options.experimental.b3,
120
136
  process.env.DD_TRACE_EXPERIMENTAL_B3_ENABLED,
@@ -140,6 +156,17 @@ class Config {
140
156
  false
141
157
  )
142
158
 
159
+ const DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH = coalesce(
160
+ process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH,
161
+ '512'
162
+ )
163
+
164
+ const DD_TRACE_STATS_COMPUTATION_ENABLED = coalesce(
165
+ options.stats,
166
+ process.env.DD_TRACE_STATS_COMPUTATION_ENABLED,
167
+ false
168
+ )
169
+
143
170
  let appsec = options.appsec || (options.experimental && options.experimental.appsec)
144
171
 
145
172
  const DD_APPSEC_ENABLED = coalesce(
@@ -180,24 +207,51 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
180
207
  |[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}`
181
208
  )
182
209
 
210
+ const iastOptions = options.experimental && options.experimental.iast
211
+ const DD_IAST_ENABLED = coalesce(
212
+ iastOptions &&
213
+ (iastOptions === true || iastOptions.enabled === true),
214
+ process.env.DD_IAST_ENABLED,
215
+ false
216
+ )
217
+
218
+ const defaultIastRequestSampling = 30
219
+ const iastRequestSampling = coalesce(
220
+ parseInt(iastOptions && iastOptions.requestSampling),
221
+ parseInt(process.env.DD_IAST_REQUEST_SAMPLING),
222
+ defaultIastRequestSampling
223
+ )
224
+ const DD_IAST_REQUEST_SAMPLING = iastRequestSampling < 0 ||
225
+ iastRequestSampling > 100 ? defaultIastRequestSampling : iastRequestSampling
226
+
227
+ const DD_IAST_MAX_CONCURRENT_REQUESTS = coalesce(
228
+ parseInt(iastOptions && iastOptions.maxConcurrentRequests),
229
+ parseInt(process.env.DD_IAST_MAX_CONCURRENT_REQUESTS),
230
+ 2
231
+ )
232
+
233
+ const DD_IAST_MAX_CONTEXT_OPERATIONS = coalesce(
234
+ parseInt(iastOptions && iastOptions.maxContextOperations),
235
+ parseInt(process.env.DD_IAST_MAX_CONTEXT_OPERATIONS),
236
+ 2
237
+ )
238
+
183
239
  const DD_CIVISIBILITY_GIT_UPLOAD_ENABLED = coalesce(
184
240
  process.env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED,
185
241
  false
186
242
  )
187
243
 
188
- const sampler = (options.experimental && options.experimental.sampler) || {}
189
244
  const ingestion = options.ingestion || {}
190
245
  const dogstatsd = coalesce(options.dogstatsd, {})
191
-
192
- Object.assign(sampler, {
246
+ const sampler = {
193
247
  sampleRate: coalesce(
194
248
  options.sampleRate,
195
- ingestion.sampleRate,
196
- sampler.sampleRate,
197
- process.env.DD_TRACE_SAMPLE_RATE
249
+ process.env.DD_TRACE_SAMPLE_RATE,
250
+ ingestion.sampleRate
198
251
  ),
199
- rateLimit: coalesce(ingestion.rateLimit, sampler.rateLimit, process.env.DD_TRACE_RATE_LIMIT)
200
- })
252
+ rateLimit: coalesce(options.rateLimit, process.env.DD_TRACE_RATE_LIMIT, ingestion.rateLimit),
253
+ rules: coalesce(options.samplingRules, safeJsonParse(process.env.DD_TRACE_SAMPLING_RULES), [])
254
+ }
201
255
 
202
256
  const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined
203
257
  const defaultFlushInterval = inAWSLambda ? 0 : 2000
@@ -214,6 +268,8 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
214
268
  this.flushInterval = coalesce(parseInt(options.flushInterval, 10), defaultFlushInterval)
215
269
  this.flushMinSpans = DD_TRACE_PARTIAL_FLUSH_MIN_SPANS
216
270
  this.sampleRate = coalesce(Math.min(Math.max(sampler.sampleRate, 0), 1), 1)
271
+ this.clientIpHeaderDisabled = isTrue(DD_TRACE_CLIENT_IP_HEADER_DISABLED)
272
+ this.clientIpHeader = DD_TRACE_CLIENT_IP_HEADER
217
273
  this.logger = options.logger
218
274
  this.plugins = !!coalesce(options.plugins, true)
219
275
  this.service = DD_SERVICE
@@ -231,9 +287,9 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
231
287
  traceparent: isTrue(DD_TRACE_TRACEPARENT_ENABLED),
232
288
  runtimeId: isTrue(DD_TRACE_RUNTIME_ID_ENABLED),
233
289
  exporter: DD_TRACE_EXPORTER,
234
- enableGetRumData: isTrue(DD_TRACE_GET_RUM_DATA_ENABLED),
235
- sampler
290
+ enableGetRumData: isTrue(DD_TRACE_GET_RUM_DATA_ENABLED)
236
291
  }
292
+ this.sampler = sampler
237
293
  this.reportHostname = isTrue(coalesce(options.reportHostname, process.env.DD_TRACE_REPORT_HOSTNAME, false))
238
294
  this.scope = process.env.DD_TRACE_SCOPE
239
295
  this.logLevel = coalesce(
@@ -251,6 +307,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
251
307
  // Disabled for CI Visibility's agentless
252
308
  this.telemetryEnabled = DD_TRACE_EXPORTER !== 'datadog' && isTrue(DD_TRACE_TELEMETRY_ENABLED)
253
309
  this.protocolVersion = DD_TRACE_AGENT_PROTOCOL_VERSION
310
+ this.tagsHeaderMaxLength = parseInt(DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH)
254
311
  this.appsec = {
255
312
  enabled: isTrue(DD_APPSEC_ENABLED),
256
313
  rules: DD_APPSEC_RULES,
@@ -259,8 +316,17 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
259
316
  obfuscatorKeyRegex: DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP,
260
317
  obfuscatorValueRegex: DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP
261
318
  }
319
+ this.iast = {
320
+ enabled: isTrue(DD_IAST_ENABLED),
321
+ requestSampling: DD_IAST_REQUEST_SAMPLING,
322
+ maxConcurrentRequests: DD_IAST_MAX_CONCURRENT_REQUESTS,
323
+ maxContextOperations: DD_IAST_MAX_CONTEXT_OPERATIONS
324
+ }
262
325
  this.isGitUploadEnabled = isTrue(DD_CIVISIBILITY_GIT_UPLOAD_ENABLED)
263
326
  this.isIntelligentTestRunnerEnabled = isTrue(DD_CIVISIBILITY_ITR_ENABLED)
327
+ this.stats = {
328
+ enabled: isTrue(DD_TRACE_STATS_COMPUTATION_ENABLED)
329
+ }
264
330
 
265
331
  tagger.add(this.tags, {
266
332
  service: this.service,
@@ -5,8 +5,16 @@ module.exports = {
5
5
  ANALYTICS_KEY: '_dd1.sr.eausr',
6
6
  ORIGIN_KEY: '_dd.origin',
7
7
  HOSTNAME_KEY: '_dd.hostname',
8
+ TOP_LEVEL_KEY: '_dd.top_level',
8
9
  SAMPLING_RULE_DECISION: '_dd.rule_psr',
9
10
  SAMPLING_LIMIT_DECISION: '_dd.limit_psr',
10
11
  SAMPLING_AGENT_DECISION: '_dd.agent_psr',
11
- DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent'
12
+ SAMPLING_MECHANISM_DEFAULT: 0,
13
+ SAMPLING_MECHANISM_AGENT: 1,
14
+ SAMPLING_MECHANISM_RULE: 3,
15
+ SAMPLING_MECHANISM_MANUAL: 4,
16
+ SAMPLING_MECHANISM_APPSEC: 5,
17
+ SAMPLING_MECHANISM_SPAN: 8,
18
+ DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent',
19
+ DECISION_MAKER_KEY: '_dd.p.dm'
12
20
  }
@@ -0,0 +1,155 @@
1
+ 'use strict'
2
+
3
+ const { AgentEncoder } = require('./0.4')
4
+
5
+ const {
6
+ MAX_NAME_LENGTH,
7
+ MAX_SERVICE_LENGTH,
8
+ MAX_RESOURCE_NAME_LENGTH,
9
+ MAX_TYPE_LENGTH,
10
+ DEFAULT_SPAN_NAME,
11
+ DEFAULT_SERVICE_NAME
12
+ } = require('./tags-processors')
13
+
14
+ function truncate (value, maxLength, suffix = '') {
15
+ if (!value) {
16
+ return value
17
+ }
18
+ if (value.length > maxLength) {
19
+ return `${value.slice(0, maxLength)}${suffix}`
20
+ }
21
+ return value
22
+ }
23
+
24
+ class SpanStatsEncoder extends AgentEncoder {
25
+ _encodeBool (bytes, value) {
26
+ this._encodeByte(bytes, value ? 0xc3 : 0xc2)
27
+ }
28
+
29
+ makePayload () {
30
+ const traceSize = this._traceBytes.length
31
+ const buffer = Buffer.allocUnsafe(traceSize)
32
+ this._traceBytes.copy(buffer, 0, traceSize)
33
+ this._reset()
34
+ return buffer
35
+ }
36
+
37
+ _encodeMapPrefix (bytes, length) {
38
+ const offset = bytes.length
39
+
40
+ bytes.reserve(1)
41
+ bytes.length += 1
42
+
43
+ bytes.buffer[offset] = 0x80 + length
44
+ }
45
+
46
+ _encodeBuffer (bytes, buffer) {
47
+ const length = buffer.length
48
+ const offset = bytes.length
49
+
50
+ bytes.reserve(5)
51
+ bytes.length += 5
52
+
53
+ bytes.buffer[offset] = 0xc6
54
+ bytes.buffer[offset + 1] = length >> 24
55
+ bytes.buffer[offset + 2] = length >> 16
56
+ bytes.buffer[offset + 3] = length >> 8
57
+ bytes.buffer[offset + 4] = length
58
+
59
+ buffer.copy(bytes.buffer, offset + 5)
60
+ bytes.length += length
61
+ }
62
+
63
+ _encodeStat (bytes, stat) {
64
+ this._encodeMapPrefix(bytes, 12)
65
+
66
+ this._encodeString(bytes, 'Service')
67
+ const service = stat.Service || DEFAULT_SERVICE_NAME
68
+ this._encodeString(bytes, truncate(service, MAX_SERVICE_LENGTH))
69
+
70
+ this._encodeString(bytes, 'Name')
71
+ const name = stat.Name || DEFAULT_SPAN_NAME
72
+ this._encodeString(bytes, truncate(name, MAX_NAME_LENGTH))
73
+
74
+ this._encodeString(bytes, 'Resource')
75
+ this._encodeString(bytes, truncate(stat.Resource, MAX_RESOURCE_NAME_LENGTH, '...'))
76
+
77
+ this._encodeString(bytes, 'HTTPStatusCode')
78
+ this._encodeInteger(bytes, stat.HTTPStatusCode)
79
+
80
+ this._encodeString(bytes, 'Type')
81
+ this._encodeString(bytes, truncate(stat.Type, MAX_TYPE_LENGTH))
82
+
83
+ this._encodeString(bytes, 'Hits')
84
+ this._encodeLong(bytes, stat.Hits)
85
+
86
+ this._encodeString(bytes, 'Errors')
87
+ this._encodeLong(bytes, stat.Errors)
88
+
89
+ this._encodeString(bytes, 'Duration')
90
+ this._encodeLong(bytes, stat.Duration)
91
+
92
+ this._encodeString(bytes, 'OkSummary')
93
+ this._encodeBuffer(bytes, stat.OkSummary)
94
+
95
+ this._encodeString(bytes, 'ErrorSummary')
96
+ this._encodeBuffer(bytes, stat.ErrorSummary)
97
+
98
+ this._encodeString(bytes, 'Synthetics')
99
+ this._encodeBool(bytes, stat.Synthetics)
100
+
101
+ this._encodeString(bytes, 'TopLevelHits')
102
+ this._encodeLong(bytes, stat.TopLevelHits)
103
+ }
104
+
105
+ _encodeBucket (bytes, bucket) {
106
+ this._encodeMapPrefix(bytes, 3)
107
+
108
+ this._encodeString(bytes, 'Start')
109
+ this._encodeLong(bytes, bucket.Start)
110
+
111
+ this._encodeString(bytes, 'Duration')
112
+ this._encodeLong(bytes, bucket.Duration)
113
+
114
+ this._encodeString(bytes, 'Stats')
115
+ this._encodeArrayPrefix(bytes, bucket.Stats)
116
+ for (const stat of bucket.Stats) {
117
+ this._encodeStat(bytes, stat)
118
+ }
119
+ }
120
+
121
+ _encode (bytes, stats) {
122
+ this._encodeMapPrefix(bytes, 8)
123
+
124
+ this._encodeString(bytes, 'Hostname')
125
+ this._encodeString(bytes, stats.Hostname)
126
+
127
+ this._encodeString(bytes, 'Env')
128
+ this._encodeString(bytes, stats.Env)
129
+
130
+ this._encodeString(bytes, 'Version')
131
+ this._encodeString(bytes, stats.Version)
132
+
133
+ this._encodeString(bytes, 'Stats')
134
+ this._encodeArrayPrefix(bytes, stats.Stats)
135
+ for (const bucket of stats.Stats) {
136
+ this._encodeBucket(bytes, bucket)
137
+ }
138
+
139
+ this._encodeString(bytes, 'Lang')
140
+ this._encodeString(bytes, stats.Lang)
141
+
142
+ this._encodeString(bytes, 'TracerVersion')
143
+ this._encodeString(bytes, stats.TracerVersion)
144
+
145
+ this._encodeString(bytes, 'RuntimeID')
146
+ this._encodeString(bytes, stats.RuntimeID)
147
+
148
+ this._encodeString(bytes, 'Sequence')
149
+ this._encodeLong(bytes, stats.Sequence)
150
+ }
151
+ }
152
+
153
+ module.exports = {
154
+ SpanStatsEncoder
155
+ }
@@ -7,9 +7,21 @@ const Writer = require('./writer')
7
7
  class AgentExporter {
8
8
  constructor (config, prioritySampler) {
9
9
  this._config = config
10
- const { url, hostname, port, lookup, protocolVersion } = config
10
+ const { url, hostname, port, lookup, protocolVersion, stats = {} } = config
11
11
  this._url = url || new URL(`http://${hostname || 'localhost'}:${port}`)
12
- this._writer = new Writer({ url: this._url, prioritySampler, lookup, protocolVersion })
12
+
13
+ const headers = {}
14
+ if (stats.enabled) {
15
+ headers['Datadog-Client-Computed-Stats'] = 'yes'
16
+ }
17
+
18
+ this._writer = new Writer({
19
+ url: this._url,
20
+ prioritySampler,
21
+ lookup,
22
+ protocolVersion,
23
+ headers
24
+ })
13
25
 
14
26
  this._timer = undefined
15
27
  process.once('beforeExit', () => this._writer.flush())
@@ -10,7 +10,7 @@ const BaseWriter = require('../common/writer')
10
10
  const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
11
11
 
12
12
  class Writer extends BaseWriter {
13
- constructor ({ prioritySampler, lookup, protocolVersion }) {
13
+ constructor ({ prioritySampler, lookup, protocolVersion, headers }) {
14
14
  super(...arguments)
15
15
  const AgentEncoder = getEncoder(protocolVersion)
16
16
 
@@ -18,12 +18,14 @@ class Writer extends BaseWriter {
18
18
  this._lookup = lookup
19
19
  this._protocolVersion = protocolVersion
20
20
  this._encoder = new AgentEncoder(this)
21
+ this._headers = headers
21
22
  }
22
23
 
23
24
  _sendPayload (data, count, done) {
24
25
  metrics.increment(`${METRIC_PREFIX}.requests`, true)
25
26
 
26
- makeRequest(this._protocolVersion, data, count, this._url, this._lookup, true, (err, res, status) => {
27
+ const { _headers, _lookup, _protocolVersion, _url } = this
28
+ makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, true, (err, res, status) => {
27
29
  if (status) {
28
30
  metrics.increment(`${METRIC_PREFIX}.responses`, true)
29
31
  metrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
@@ -73,11 +75,12 @@ function getEncoder (protocolVersion) {
73
75
  }
74
76
  }
75
77
 
76
- function makeRequest (version, data, count, url, lookup, needsStartupLog, cb) {
78
+ function makeRequest (version, data, count, url, headers, lookup, needsStartupLog, cb) {
77
79
  const options = {
78
80
  path: `/v${version}/traces`,
79
81
  method: 'PUT',
80
82
  headers: {
83
+ ...headers,
81
84
  'Content-Type': 'application/msgpack',
82
85
  'Datadog-Meta-Tracer-Version': tracerVersion,
83
86
  'X-Datadog-Trace-Count': String(count)
@@ -84,7 +84,7 @@ function request (data, options, callback) {
84
84
  req.setTimeout(timeout, req.abort)
85
85
 
86
86
  if (isReadable) {
87
- data.pipe(req)
87
+ data.pipe(req) // TODO: Validate whether this is actually retriable.
88
88
  } else {
89
89
  dataArray.forEach(buffer => req.write(buffer))
90
90
  req.end()
@@ -93,7 +93,11 @@ function request (data, options, callback) {
93
93
  storage.enterWith(store)
94
94
  }
95
95
 
96
- makeRequest(() => makeRequest(callback))
96
+ // TODO: Figure out why setTimeout is needed to avoid losing the async context
97
+ // in the retry request before socket.connect() is called.
98
+ // TODO: Test that this doesn't trace itself on retry when the diagnostics
99
+ // channel events are available in the agent exporter.
100
+ makeRequest(() => setTimeout(() => makeRequest(callback)))
97
101
  }
98
102
 
99
103
  function byteLength (data) {
@@ -0,0 +1,20 @@
1
+ const { URL } = require('url')
2
+
3
+ const { Writer } = require('./writer')
4
+
5
+ class SpanStatsExporter {
6
+ constructor (config) {
7
+ const { hostname = '127.0.0.1', port = 8126, tags, url } = config
8
+ this._url = url || new URL(`http://${hostname || 'localhost'}:${port}`)
9
+ this._writer = new Writer({ url: this._url, tags })
10
+ }
11
+
12
+ export (payload) {
13
+ this._writer.append(payload)
14
+ this._writer.flush()
15
+ }
16
+ }
17
+
18
+ module.exports = {
19
+ SpanStatsExporter
20
+ }
@@ -0,0 +1,54 @@
1
+
2
+ const { SpanStatsEncoder } = require('../../encode/span-stats')
3
+
4
+ const pkg = require('../../../../../package.json')
5
+
6
+ const BaseWriter = require('../common/writer')
7
+ const request = require('../common/request')
8
+ const log = require('../../log')
9
+
10
+ class Writer extends BaseWriter {
11
+ constructor ({ url }) {
12
+ super(...arguments)
13
+ this._url = url
14
+ this._encoder = new SpanStatsEncoder(this)
15
+ }
16
+
17
+ _sendPayload (data, _, done) {
18
+ makeRequest(data, this._url, (err, res) => {
19
+ if (err) {
20
+ log.error(err)
21
+ done()
22
+ return
23
+ }
24
+ log.debug(`Response from the intake: ${res}`)
25
+ done()
26
+ })
27
+ }
28
+ }
29
+
30
+ function makeRequest (data, url, cb) {
31
+ const options = {
32
+ path: '/v0.6/stats',
33
+ method: 'PUT',
34
+ headers: {
35
+ 'Datadog-Meta-Lang': 'javascript',
36
+ 'Datadog-Meta-Tracer-Version': pkg.version,
37
+ 'Content-Type': 'application/msgpack'
38
+ }
39
+ }
40
+
41
+ options.protocol = url.protocol
42
+ options.hostname = url.hostname
43
+ options.port = url.port
44
+
45
+ log.debug(() => `Request to the intake: ${JSON.stringify(options)}`)
46
+
47
+ request(data, options, (err, res) => {
48
+ cb(err, res)
49
+ })
50
+ }
51
+
52
+ module.exports = {
53
+ Writer
54
+ }
@@ -12,6 +12,7 @@ const SAMPLING_AGENT_DECISION = constants.SAMPLING_AGENT_DECISION
12
12
  const MEASURED = tags.MEASURED
13
13
  const ORIGIN_KEY = constants.ORIGIN_KEY
14
14
  const HOSTNAME_KEY = constants.HOSTNAME_KEY
15
+ const TOP_LEVEL_KEY = constants.TOP_LEVEL_KEY
15
16
 
16
17
  const map = {
17
18
  'service.name': 'service',
@@ -110,6 +111,7 @@ function extractRootTags (trace, span) {
110
111
  addTag({}, trace.metrics, SAMPLING_RULE_DECISION, context._trace[SAMPLING_RULE_DECISION])
111
112
  addTag({}, trace.metrics, SAMPLING_LIMIT_DECISION, context._trace[SAMPLING_LIMIT_DECISION])
112
113
  addTag({}, trace.metrics, SAMPLING_AGENT_DECISION, context._trace[SAMPLING_AGENT_DECISION])
114
+ addTag({}, trace.metrics, TOP_LEVEL_KEY, 1)
113
115
  }
114
116
 
115
117
  function extractChunkTags (trace, span) {
@@ -2,8 +2,19 @@
2
2
 
3
3
  const semver = require('semver')
4
4
  const logger = require('./log')
5
+ const { addHook } = require('import-in-the-middle')
6
+ const dc = require('diagnostics_channel')
5
7
 
6
8
  if (semver.satisfies(process.versions.node, '^12.20.0 || >=14.13.1')) {
9
+ const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart')
10
+ addHook((name, namespace) => {
11
+ if (moduleLoadStartChannel.hasSubscribers) {
12
+ moduleLoadStartChannel.publish({
13
+ filename: name,
14
+ module: namespace
15
+ })
16
+ }
17
+ })
7
18
  module.exports = require('import-in-the-middle')
8
19
  } else {
9
20
  logger.warn('ESM is not fully supported by this version of Node.js, ' +