dd-trace 4.13.0 → 4.14.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.13.0",
3
+ "version": "4.14.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@datadog/native-appsec": "^3.2.0",
71
- "@datadog/native-iast-rewriter": "2.0.1",
71
+ "@datadog/native-iast-rewriter": "2.1.3",
72
72
  "@datadog/native-iast-taint-tracking": "1.5.0",
73
73
  "@datadog/native-metrics": "^2.0.0",
74
74
  "@datadog/pprof": "3.2.0",
@@ -15,15 +15,14 @@ const consumerStartCh = channel('apm:kafkajs:consume:start')
15
15
  const consumerFinishCh = channel('apm:kafkajs:consume:finish')
16
16
  const consumerErrorCh = channel('apm:kafkajs:consume:error')
17
17
 
18
- addHook({ name: 'kafkajs', versions: ['>=1.4'] }, (obj) => {
19
- class Kafka extends obj.Kafka {
18
+ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => {
19
+ class Kafka extends BaseKafka {
20
20
  constructor (options) {
21
21
  super(options)
22
22
  this._brokers = (options.brokers && typeof options.brokers !== 'function')
23
23
  ? options.brokers.join(',') : undefined
24
24
  }
25
25
  }
26
- obj.Kafka = Kafka
27
26
 
28
27
  shimmer.wrap(Kafka.prototype, 'producer', createProducer => function () {
29
28
  const producer = createProducer.apply(this, arguments)
@@ -117,5 +116,5 @@ addHook({ name: 'kafkajs', versions: ['>=1.4'] }, (obj) => {
117
116
  }
118
117
  return consumer
119
118
  })
120
- return obj
119
+ return Kafka
121
120
  })
@@ -441,6 +441,9 @@ addHook({
441
441
  file: 'lib/cli/run-helpers.js'
442
442
  }, (run) => {
443
443
  shimmer.wrap(run, 'runMocha', runMocha => async function () {
444
+ if (!testStartCh.hasSubscribers) {
445
+ return runMocha.apply(this, arguments)
446
+ }
444
447
  const mocha = arguments[0]
445
448
  /**
446
449
  * This attaches `run` to the global context, which we'll call after
@@ -25,6 +25,7 @@ const {
25
25
  } = require('../../dd-trace/src/plugins/util/test')
26
26
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
27
27
  const log = require('../../dd-trace/src/log')
28
+ const NoopTracer = require('../../dd-trace/src/noop/tracer')
28
29
 
29
30
  const TEST_FRAMEWORK_NAME = 'cypress'
30
31
 
@@ -119,10 +120,32 @@ function getSkippableTests (isSuitesSkippingEnabled, tracer, testConfiguration)
119
120
  })
120
121
  }
121
122
 
123
+ const noopTask = {
124
+ 'dd:testSuiteStart': () => {
125
+ return null
126
+ },
127
+ 'dd:beforeEach': () => {
128
+ return {}
129
+ },
130
+ 'dd:afterEach': () => {
131
+ return null
132
+ },
133
+ 'dd:addTags': () => {
134
+ return null
135
+ }
136
+ }
137
+
122
138
  module.exports = (on, config) => {
123
139
  let isTestsSkipped = false
124
140
  const skippedTests = []
125
141
  const tracer = require('../../dd-trace')
142
+
143
+ // The tracer was not init correctly for whatever reason (such as invalid DD_SITE)
144
+ if (tracer._tracer instanceof NoopTracer) {
145
+ // We still need to register these tasks or the support file will fail
146
+ return on('task', noopTask)
147
+ }
148
+
126
149
  const testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
127
150
 
128
151
  const {
@@ -328,12 +351,16 @@ module.exports = (on, config) => {
328
351
  }
329
352
 
330
353
  return new Promise(resolve => {
331
- if (tracer._tracer._exporter.flush) {
332
- tracer._tracer._exporter.flush(() => {
354
+ const exporter = tracer._tracer._exporter
355
+ if (!exporter) {
356
+ return resolve(null)
357
+ }
358
+ if (exporter.flush) {
359
+ exporter.flush(() => {
333
360
  resolve(null)
334
361
  })
335
- } else {
336
- tracer._tracer._exporter._writer.flush(() => {
362
+ } else if (exporter._writer) {
363
+ exporter._writer.flush(() => {
337
364
  resolve(null)
338
365
  })
339
366
  }
@@ -375,7 +402,7 @@ module.exports = (on, config) => {
375
402
  'dd:afterEach': ({ test, coverage }) => {
376
403
  const { state, error, isRUMActive, testSourceLine, testSuite, testName } = test
377
404
  if (activeSpan) {
378
- if (coverage && tracer._tracer._exporter.exportCoverage && isCodeCoverageEnabled) {
405
+ if (coverage && isCodeCoverageEnabled && tracer._tracer._exporter && tracer._tracer._exporter.exportCoverage) {
379
406
  const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
380
407
  const relativeCoverageFiles = coverageFiles.map(file => getTestSuitePath(file, rootDir))
381
408
  const { _traceId, _spanId } = testSuiteSpan.context()
@@ -36,10 +36,14 @@ class MongodbCorePlugin extends DatabasePlugin {
36
36
  }
37
37
  }
38
38
 
39
+ function sanitizeBigInt (data) {
40
+ return JSON.stringify(data, (_key, value) => typeof value === 'bigint' ? value.toString() : value)
41
+ }
42
+
39
43
  function getQuery (cmd) {
40
44
  if (!cmd || typeof cmd !== 'object' || Array.isArray(cmd)) return
41
- if (cmd.query) return JSON.stringify(limitDepth(cmd.query))
42
- if (cmd.filter) return JSON.stringify(limitDepth(cmd.filter))
45
+ if (cmd.query) return sanitizeBigInt(limitDepth(cmd.query))
46
+ if (cmd.filter) return sanitizeBigInt(limitDepth(cmd.filter))
43
47
  }
44
48
 
45
49
  function getResource (plugin, ns, query, operationName) {
@@ -6,6 +6,7 @@ const { addVulnerability } = require('../vulnerability-reporter')
6
6
  const { getIastContext } = require('../iast-context')
7
7
  const overheadController = require('../overhead-controller')
8
8
  const { SinkIastPlugin } = require('../iast-plugin')
9
+ const { getOriginalPathAndLineFromSourceMap } = require('../taint-tracking/rewriter')
9
10
 
10
11
  class Analyzer extends SinkIastPlugin {
11
12
  constructor (type) {
@@ -25,8 +26,9 @@ class Analyzer extends SinkIastPlugin {
25
26
  const evidence = this._getEvidence(value, context)
26
27
  const location = this._getLocation(value)
27
28
  if (!this._isExcluded(location)) {
29
+ const locationSourceMap = this._replaceLocationFromSourceMap(location)
28
30
  const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
29
- const vulnerability = this._createVulnerability(this._type, evidence, spanId, location)
31
+ const vulnerability = this._createVulnerability(this._type, evidence, spanId, locationSourceMap)
30
32
  addVulnerability(context, vulnerability)
31
33
  }
32
34
  }
@@ -47,6 +49,22 @@ class Analyzer extends SinkIastPlugin {
47
49
  return getFirstNonDDPathAndLine(this._getExcludedPaths())
48
50
  }
49
51
 
52
+ _replaceLocationFromSourceMap (location) {
53
+ if (location) {
54
+ const { path, line, column } = getOriginalPathAndLineFromSourceMap(location)
55
+ if (path) {
56
+ location.path = path
57
+ }
58
+ if (line) {
59
+ location.line = line
60
+ }
61
+ if (column) {
62
+ location.column = column
63
+ }
64
+ }
65
+ return location
66
+ }
67
+
50
68
  _getExcludedPaths () {}
51
69
 
52
70
  _isInvalidContext (store, iastContext) {
@@ -47,6 +47,7 @@ function getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPat
47
47
  return {
48
48
  path: path.relative(process.cwd(), filepath),
49
49
  line: callsite.getLineNumber(),
50
+ column: callsite.getColumnNumber(),
50
51
  isInternal: !path.isAbsolute(filepath)
51
52
  }
52
53
  }
@@ -10,12 +10,51 @@ const { getRewriteFunction } = require('./rewriter-telemetry')
10
10
 
11
11
  let rewriter
12
12
  let getPrepareStackTrace
13
+
14
+ let getRewriterOriginalPathAndLineFromSourceMap = function (path, line, column) {
15
+ return { path, line, column }
16
+ }
17
+
18
+ function isFlagPresent (flag) {
19
+ return process.env.NODE_OPTIONS?.includes(flag) ||
20
+ process.execArgv?.some(arg => arg.includes(flag))
21
+ }
22
+
23
+ function getGetOriginalPathAndLineFromSourceMapFunction (chainSourceMap, getOriginalPathAndLineFromSourceMap) {
24
+ if (chainSourceMap) {
25
+ return function (path, line, column) {
26
+ // if --enable-source-maps is present stacktraces of the rewritten files contain the original path, file and
27
+ // column because the sourcemap chaining is done during the rewriting process so we can skip it
28
+ if (isPrivateModule(path) && isNotLibraryFile(path)) {
29
+ return { path, line, column }
30
+ } else {
31
+ return getOriginalPathAndLineFromSourceMap(path, line, column)
32
+ }
33
+ }
34
+ } else {
35
+ return getOriginalPathAndLineFromSourceMap
36
+ }
37
+ }
38
+
13
39
  function getRewriter (telemetryVerbosity) {
14
40
  if (!rewriter) {
15
- const iastRewriter = require('@datadog/native-iast-rewriter')
16
- const Rewriter = iastRewriter.Rewriter
17
- getPrepareStackTrace = iastRewriter.getPrepareStackTrace
18
- rewriter = new Rewriter({ csiMethods, telemetryVerbosity: getName(telemetryVerbosity) })
41
+ try {
42
+ const iastRewriter = require('@datadog/native-iast-rewriter')
43
+ const Rewriter = iastRewriter.Rewriter
44
+ getPrepareStackTrace = iastRewriter.getPrepareStackTrace
45
+
46
+ const chainSourceMap = isFlagPresent('--enable-source-maps')
47
+ const getOriginalPathAndLineFromSourceMap = iastRewriter.getOriginalPathAndLineFromSourceMap
48
+ if (getOriginalPathAndLineFromSourceMap) {
49
+ getRewriterOriginalPathAndLineFromSourceMap =
50
+ getGetOriginalPathAndLineFromSourceMapFunction(chainSourceMap, getOriginalPathAndLineFromSourceMap)
51
+ }
52
+
53
+ rewriter = new Rewriter({ csiMethods, telemetryVerbosity: getName(telemetryVerbosity), chainSourceMap })
54
+ } catch (e) {
55
+ iastLog.error('Unable to initialize TaintTracking Rewriter')
56
+ .errorAndPublish(e)
57
+ }
19
58
  }
20
59
  return rewriter
21
60
  }
@@ -74,6 +113,10 @@ function disableRewriter () {
74
113
  Error.prepareStackTrace = originalPrepareStackTrace
75
114
  }
76
115
 
116
+ function getOriginalPathAndLineFromSourceMap ({ path, line, column }) {
117
+ return getRewriterOriginalPathAndLineFromSourceMap(path, line, column)
118
+ }
119
+
77
120
  module.exports = {
78
- enableRewriter, disableRewriter
121
+ enableRewriter, disableRewriter, getOriginalPathAndLineFromSourceMap
79
122
  }
@@ -76,7 +76,18 @@ class SensitiveHandler {
76
76
  while (nextSensitive != null && contains(nextTainted, nextSensitive)) {
77
77
  const redactionStart = nextSensitive.start - nextTainted.start
78
78
  const redactionEnd = nextSensitive.end - nextTainted.start
79
- this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
79
+ if (redactionStart === redactionEnd) {
80
+ this.writeRedactedValuePart(valueParts, 0)
81
+ } else {
82
+ this.redactSource(
83
+ sources,
84
+ redactedSources,
85
+ redactedSourcesContext,
86
+ sourceIndex,
87
+ redactionStart,
88
+ redactionEnd
89
+ )
90
+ }
80
91
  nextSensitive = sensitive.shift()
81
92
  }
82
93
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "2.2",
3
3
  "metadata": {
4
- "rules_version": "1.7.1"
4
+ "rules_version": "1.7.2"
5
5
  },
6
6
  "rules": [
7
7
  {
@@ -1743,7 +1743,10 @@
1743
1743
  "sys/hypervisor",
1744
1744
  "sys/kernel",
1745
1745
  "sys/module",
1746
- "sys/power"
1746
+ "sys/power",
1747
+ "windows\\win.ini",
1748
+ "default\\ntuser.dat",
1749
+ "/var/run/secrets/kubernetes.io/serviceaccount"
1747
1750
  ]
1748
1751
  },
1749
1752
  "operator": "phrase_match"
@@ -2312,7 +2315,8 @@
2312
2315
  }
2313
2316
  ],
2314
2317
  "transformers": [
2315
- "lowercase"
2318
+ "lowercase",
2319
+ "cmdLine"
2316
2320
  ]
2317
2321
  },
2318
2322
  {
@@ -2950,7 +2954,7 @@
2950
2954
  "address": "grpc.server.request.message"
2951
2955
  }
2952
2956
  ],
2953
- "regex": "[\\s\\\"'`;\\/0-9=\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]on(?:d(?:r(?:ag(?:en(?:ter|d)|leave|start|over)?|op)|urationchange|blclick)|s(?:e(?:ek(?:ing|ed)|arch|lect)|u(?:spend|bmit)|talled|croll|how)|m(?:ouse(?:(?:lea|mo)ve|o(?:ver|ut)|enter|down|up)|essage)|p(?:a(?:ge(?:hide|show)|(?:st|us)e)|lay(?:ing)?|rogress)|c(?:anplay(?:through)?|o(?:ntextmenu|py)|hange|lick|ut)|a(?:nimation(?:iteration|start|end)|(?:fterprin|bor)t)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|imeupdate)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?)|(?:(?:volume|hash)chang|o(?:ff|n)lin)e|b(?:efore(?:unload|print)|lur)|load(?:ed(?:meta)?data|start)?|r(?:es(?:ize|et)|atechange)|key(?:press|down|up)|w(?:aiting|heel)|in(?:valid|put)|e(?:nded|rror)|unload)[\\s\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]*?=[^=]",
2957
+ "regex": "\\bon(?:d(?:r(?:ag(?:en(?:ter|d)|leave|start|over)?|op)|urationchange|blclick)|s(?:e(?:ek(?:ing|ed)|arch|lect)|u(?:spend|bmit)|talled|croll|how)|m(?:ouse(?:(?:lea|mo)ve|o(?:ver|ut)|enter|down|up)|essage)|p(?:a(?:ge(?:hide|show)|(?:st|us)e)|lay(?:ing)?|rogress|aste|ointer(?:cancel|down|enter|leave|move|out|over|rawupdate|up))|c(?:anplay(?:through)?|o(?:ntextmenu|py)|hange|lick|ut)|a(?:nimation(?:iteration|start|end)|(?:fterprin|bor)t|uxclick|fterscriptexecute)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|imeupdate)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?|inish)|(?:(?:volume|hash)chang|o(?:ff|n)lin)e|b(?:efore(?:unload|print)|lur)|load(?:ed(?:meta)?data|start|end)?|r(?:es(?:ize|et)|atechange)|key(?:press|down|up)|w(?:aiting|heel)|in(?:valid|put)|e(?:nded|rror)|unload)[\\s\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]*?=[^=]",
2954
2958
  "options": {
2955
2959
  "min_length": 8
2956
2960
  }
@@ -4636,6 +4640,46 @@
4636
4640
  ],
4637
4641
  "transformers": []
4638
4642
  },
4643
+ {
4644
+ "id": "dog-913-008",
4645
+ "name": "Netsparker OOB domain",
4646
+ "tags": {
4647
+ "type": "commercial_scanner",
4648
+ "category": "attack_attempt",
4649
+ "tool_name": "Netsparker",
4650
+ "confidence": "0"
4651
+ },
4652
+ "conditions": [
4653
+ {
4654
+ "parameters": {
4655
+ "inputs": [
4656
+ {
4657
+ "address": "server.request.query"
4658
+ },
4659
+ {
4660
+ "address": "server.request.body"
4661
+ },
4662
+ {
4663
+ "address": "server.request.path_params"
4664
+ },
4665
+ {
4666
+ "address": "server.request.headers.no_cookies"
4667
+ },
4668
+ {
4669
+ "address": "grpc.server.request.message"
4670
+ }
4671
+ ],
4672
+ "regex": "\\b(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)r87(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)(?:me|com)\\b",
4673
+ "options": {
4674
+ "case_sensitive": false,
4675
+ "min_length": 7
4676
+ }
4677
+ },
4678
+ "operator": "match_regex"
4679
+ }
4680
+ ],
4681
+ "transformers": []
4682
+ },
4639
4683
  {
4640
4684
  "id": "dog-931-001",
4641
4685
  "name": "RFI: URL Payload to well known RFI target",
@@ -4699,6 +4743,56 @@
4699
4743
  ],
4700
4744
  "transformers": []
4701
4745
  },
4746
+ {
4747
+ "id": "dog-941-001",
4748
+ "name": "XSS in source property",
4749
+ "tags": {
4750
+ "type": "xss",
4751
+ "category": "attack_attempt",
4752
+ "confidence": "0"
4753
+ },
4754
+ "conditions": [
4755
+ {
4756
+ "parameters": {
4757
+ "inputs": [
4758
+ {
4759
+ "address": "server.request.headers.no_cookies",
4760
+ "key_path": [
4761
+ "user-agent"
4762
+ ]
4763
+ },
4764
+ {
4765
+ "address": "server.request.headers.no_cookies",
4766
+ "key_path": [
4767
+ "referer"
4768
+ ]
4769
+ },
4770
+ {
4771
+ "address": "server.request.query"
4772
+ },
4773
+ {
4774
+ "address": "server.request.body"
4775
+ },
4776
+ {
4777
+ "address": "server.request.path_params"
4778
+ },
4779
+ {
4780
+ "address": "grpc.server.request.message"
4781
+ }
4782
+ ],
4783
+ "regex": "<(?:iframe|esi:include)(?:(?:\\s|/)*\\w+=[\"'\\w]+)*(?:\\s|/)*src(?:doc)?=[\"']?(?:data:|javascript:|http:|//)[^\\s'\"]+['\"]?",
4784
+ "options": {
4785
+ "min_length": 14
4786
+ }
4787
+ },
4788
+ "operator": "match_regex"
4789
+ }
4790
+ ],
4791
+ "transformers": [
4792
+ "removeNulls",
4793
+ "urlDecodeUni"
4794
+ ]
4795
+ },
4702
4796
  {
4703
4797
  "id": "dog-942-001",
4704
4798
  "name": "Blind XSS callback domains",
@@ -5428,12 +5522,14 @@
5428
5522
  "address": "grpc.server.request.message"
5429
5523
  }
5430
5524
  ],
5431
- "regex": "(?i)[&|]\\s*cat\\s+\\/etc\\/[\\w\\.\\/]*passwd\\s*[&|]"
5525
+ "regex": "(?i)[&|]\\s*cat\\s*\\/etc\\/[\\w\\.\\/]*passwd\\s*[&|]"
5432
5526
  },
5433
5527
  "operator": "match_regex"
5434
5528
  }
5435
5529
  ],
5436
- "transformers": []
5530
+ "transformers": [
5531
+ "cmdLine"
5532
+ ]
5437
5533
  },
5438
5534
  {
5439
5535
  "id": "sqr-000-010",
@@ -7014,7 +7110,10 @@
7014
7110
  ]
7015
7111
  }
7016
7112
  ],
7017
- "regex": "mozilla/4\\.0 \\(compatible(; msie 6\\.0; win32)?\\)"
7113
+ "regex": "mozilla/4\\.0 \\(compatible(; msie (?:6\\.0; win32|4\\.0; Windows NT))?\\)",
7114
+ "options": {
7115
+ "case_sensitive": false
7116
+ }
7018
7117
  },
7019
7118
  "operator": "match_regex"
7020
7119
  }
@@ -7076,4 +7175,4 @@
7076
7175
  "transformers": []
7077
7176
  }
7078
7177
  ]
7079
- }
7178
+ }
@@ -5,6 +5,7 @@ const request = require('./exporters/common/request')
5
5
  const dgram = require('dgram')
6
6
  const isIP = require('net').isIP
7
7
  const log = require('./log')
8
+ const { URL, format } = require('url')
8
9
 
9
10
  const MAX_BUFFER_SIZE = 1024 // limit from the agent
10
11
 
@@ -13,9 +14,7 @@ const TYPE_GAUGE = 'g'
13
14
  const TYPE_DISTRIBUTION = 'd'
14
15
 
15
16
  class DogStatsDClient {
16
- constructor (options) {
17
- options = options || {}
18
-
17
+ constructor (options = {}) {
19
18
  if (options.metricsProxyUrl) {
20
19
  this._httpOptions = {
21
20
  url: options.metricsProxyUrl.toString(),
@@ -50,6 +49,8 @@ class DogStatsDClient {
50
49
  flush () {
51
50
  const queue = this._enqueue()
52
51
 
52
+ log.debug(`Flushing ${queue.length} metrics via ${this._httpOptions ? 'HTTP' : 'UDP'}`)
53
+
53
54
  if (this._queue.length === 0) return
54
55
 
55
56
  this._queue = []
@@ -141,6 +142,44 @@ class DogStatsDClient {
141
142
 
142
143
  return socket
143
144
  }
145
+
146
+ static generateClientConfig (config = {}) {
147
+ const tags = []
148
+
149
+ if (config.tags) {
150
+ Object.keys(config.tags)
151
+ .filter(key => typeof config.tags[key] === 'string')
152
+ .filter(key => {
153
+ // Skip runtime-id unless enabled as cardinality may be too high
154
+ if (key !== 'runtime-id') return true
155
+ return (config.experimental && config.experimental.runtimeId)
156
+ })
157
+ .forEach(key => {
158
+ // https://docs.datadoghq.com/tagging/#defining-tags
159
+ const value = config.tags[key].replace(/[^a-z0-9_:./-]/ig, '_')
160
+
161
+ tags.push(`${key}:${value}`)
162
+ })
163
+ }
164
+
165
+ const clientConfig = {
166
+ host: config.dogstatsd.hostname,
167
+ port: config.dogstatsd.port,
168
+ tags
169
+ }
170
+
171
+ if (config.url) {
172
+ clientConfig.metricsProxyUrl = config.url
173
+ } else if (config.port) {
174
+ clientConfig.metricsProxyUrl = new URL(format({
175
+ protocol: 'http:',
176
+ hostname: config.hostname || 'localhost',
177
+ port: config.port
178
+ }))
179
+ }
180
+
181
+ return clientConfig
182
+ }
144
183
  }
145
184
 
146
185
  class NoopDogStatsDClient {
@@ -155,8 +194,9 @@ class NoopDogStatsDClient {
155
194
 
156
195
  // This is a simplified user-facing proxy to the underlying DogStatsDClient instance
157
196
  class CustomMetrics {
158
- constructor (options) {
159
- this.dogstatsd = new DogStatsDClient(options)
197
+ constructor (config) {
198
+ const clientConfig = DogStatsDClient.generateClientConfig(config)
199
+ this.dogstatsd = new DogStatsDClient(clientConfig)
160
200
  }
161
201
 
162
202
  increment (stat, value = 1, tags) {
@@ -139,13 +139,13 @@ function removeInvalidMetadata (metadata) {
139
139
  return Object.keys(metadata).reduce((filteredTags, tag) => {
140
140
  if (tag === GIT_REPOSITORY_URL) {
141
141
  if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
142
- log.error('DD_GIT_REPOSITORY_URL must be a valid URL')
142
+ log.error(`Repository URL is not a valid repository URL: ${metadata[GIT_REPOSITORY_URL]}.`)
143
143
  return filteredTags
144
144
  }
145
145
  }
146
146
  if (tag === GIT_COMMIT_SHA) {
147
147
  if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
148
- log.error('DD_GIT_COMMIT_SHA must be a full-length git SHA')
148
+ log.error(`Git commit SHA must be a full-length git SHA: ${metadata[GIT_COMMIT_SHA]}.`)
149
149
  return filteredTags
150
150
  }
151
151
  }
@@ -6,9 +6,11 @@ const dc = require('../../../../diagnostics_channel')
6
6
  const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags')
7
7
  const { WEB } = require('../../../../../ext/types')
8
8
  const runtimeMetrics = require('../../runtime_metrics')
9
+ const telemetryMetrics = require('../../telemetry/metrics')
9
10
 
10
11
  const beforeCh = dc.channel('dd-trace:storage:before')
11
12
  const enterCh = dc.channel('dd-trace:storage:enter')
13
+ const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
12
14
 
13
15
  let kSampleCount
14
16
 
@@ -168,6 +170,16 @@ class NativeWallProfiler {
168
170
  }
169
171
  }
170
172
 
173
+ _reportV8bug (maybeBug) {
174
+ const tag = `v8_profiler_bug_workaround_enabled:${this._v8ProfilerBugWorkaroundEnabled}`
175
+ const metric = `v8_cpu_profiler${maybeBug ? '_maybe' : ''}_stuck_event_loop`
176
+ this._logger?.warn(`Wall profiler: ${maybeBug ? 'possible ' : ''}v8 profiler stuck event loop detected.`)
177
+ // report as runtime metric (can be removed in the future when telemetry is mature)
178
+ runtimeMetrics.increment(`runtime.node.profiler.${metric}`, tag, true)
179
+ // report as telemetry metric
180
+ profilerTelemetryMetrics.count(metric, [tag]).inc()
181
+ }
182
+
171
183
  _stop (restart) {
172
184
  if (!this._started) return
173
185
  if (this._codeHotspotsEnabled) {
@@ -178,12 +190,8 @@ class NativeWallProfiler {
178
190
  const profile = this._pprof.time.stop(restart, this._codeHotspotsEnabled ? generateLabels : undefined)
179
191
  if (restart) {
180
192
  const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
181
- if (v8BugDetected === 1) {
182
- this._logger?.warn('Wall profiler: possible v8 profiler stuck event loop detected.')
183
- runtimeMetrics.increment('runtime.node.profiler.v8_cpu_profiler_maybe_stuck_event_loop', undefined, true)
184
- } else if (v8BugDetected === 2) {
185
- this._logger?.warn('Wall profiler: v8 profiler stuck event loop detected.')
186
- runtimeMetrics.increment('runtime.node.profiler.v8_cpu_profiler_stuck_event_loop', undefined, true)
193
+ if (v8BugDetected !== 0) {
194
+ this._reportV8bug(v8BugDetected === 1)
187
195
  }
188
196
  }
189
197
  return profile
@@ -30,17 +30,7 @@ class Tracer extends NoopProxy {
30
30
 
31
31
  if (config.dogstatsd) {
32
32
  // Custom Metrics
33
- this.dogstatsd = new dogstatsd.CustomMetrics({
34
- host: config.dogstatsd.hostname,
35
- port: config.dogstatsd.port,
36
- tags: [
37
- // these are the Runtime Metrics default tags
38
- // Python also uses these as default Custom Metrics tags
39
- `service:${config.tags.service}`,
40
- `env:${config.tags.env}`,
41
- `version:${config.tags.version}`
42
- ]
43
- })
33
+ this.dogstatsd = new dogstatsd.CustomMetrics(config)
44
34
 
45
35
  setInterval(() => {
46
36
  this.dogstatsd.flush()
@@ -2,7 +2,6 @@
2
2
 
3
3
  // TODO: capture every second and flush every 10 seconds
4
4
 
5
- const { URL, format } = require('url')
6
5
  const v8 = require('v8')
7
6
  const os = require('os')
8
7
  const { DogStatsDClient } = require('./dogstatsd')
@@ -27,21 +26,7 @@ reset()
27
26
 
28
27
  module.exports = {
29
28
  start (config) {
30
- const tags = []
31
-
32
- Object.keys(config.tags)
33
- .filter(key => typeof config.tags[key] === 'string')
34
- .filter(key => {
35
- // Skip runtime-id unless enabled as cardinality may be too high
36
- if (key !== 'runtime-id') return true
37
- return (config.experimental && config.experimental.runtimeId)
38
- })
39
- .forEach(key => {
40
- // https://docs.datadoghq.com/tagging/#defining-tags
41
- const value = config.tags[key].replace(/[^a-z0-9_:./-]/ig, '_')
42
-
43
- tags.push(`${key}:${value}`)
44
- })
29
+ const clientConfig = DogStatsDClient.generateClientConfig(config)
45
30
 
46
31
  try {
47
32
  nativeMetrics = require('@datadog/native-metrics')
@@ -51,22 +36,6 @@ module.exports = {
51
36
  nativeMetrics = null
52
37
  }
53
38
 
54
- const clientConfig = {
55
- host: config.dogstatsd.hostname,
56
- port: config.dogstatsd.port,
57
- tags
58
- }
59
-
60
- if (config.url) {
61
- clientConfig.metricsProxyUrl = config.url
62
- } else if (config.port) {
63
- clientConfig.metricsProxyUrl = new URL(format({
64
- protocol: 'http:',
65
- hostname: config.hostname || 'localhost',
66
- port: config.port
67
- }))
68
- }
69
-
70
39
  client = new DogStatsDClient(clientConfig)
71
40
 
72
41
  time = process.hrtime()
@@ -15,6 +15,7 @@ const FILE_URI_START = `file://`
15
15
  const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart')
16
16
 
17
17
  let immediate, config, application, host
18
+ let isFirstModule = true
18
19
 
19
20
  function waitAndSend (config, application, host) {
20
21
  if (!immediate) {
@@ -36,7 +37,21 @@ function waitAndSend (config, application, host) {
36
37
  }
37
38
  }
38
39
 
40
+ function loadAllTheLoadedModules () {
41
+ if (require.cache) {
42
+ const filenames = Object.keys(require.cache)
43
+ filenames.forEach(filename => {
44
+ onModuleLoad({ filename })
45
+ })
46
+ }
47
+ }
48
+
39
49
  function onModuleLoad (data) {
50
+ if (isFirstModule) {
51
+ isFirstModule = false
52
+ loadAllTheLoadedModules()
53
+ }
54
+
40
55
  if (data) {
41
56
  let filename = data.filename
42
57
  if (filename && filename.startsWith(FILE_URI_START)) {
@@ -17,6 +17,7 @@ let pluginManager
17
17
  let application
18
18
  let host
19
19
  let interval
20
+ let heartbeatTimeout
20
21
  let heartbeatInterval
21
22
  const sentIntegrations = new Set()
22
23
 
@@ -110,6 +111,14 @@ function getTelemetryData () {
110
111
  return { config, application, host, heartbeatInterval }
111
112
  }
112
113
 
114
+ function heartbeat (config, application, host) {
115
+ heartbeatTimeout = setTimeout(() => {
116
+ sendData(config, application, host, 'app-heartbeat')
117
+ heartbeat(config, application, host)
118
+ }, heartbeatInterval).unref()
119
+ return heartbeatTimeout
120
+ }
121
+
113
122
  function start (aConfig, thePluginManager) {
114
123
  if (!aConfig.telemetry.enabled) {
115
124
  return
@@ -122,9 +131,9 @@ function start (aConfig, thePluginManager) {
122
131
 
123
132
  dependencies.start(config, application, host)
124
133
  sendData(config, application, host, 'app-started', appStarted())
134
+ heartbeat(config, application, host)
125
135
  interval = setInterval(() => {
126
136
  metricsManager.send(config, application, host)
127
- sendData(config, application, host, 'app-heartbeat')
128
137
  }, heartbeatInterval)
129
138
  interval.unref()
130
139
  process.on('beforeExit', onBeforeExit)
@@ -137,6 +146,7 @@ function stop () {
137
146
  return
138
147
  }
139
148
  clearInterval(interval)
149
+ clearTimeout(heartbeatTimeout)
140
150
  process.removeListener('beforeExit', onBeforeExit)
141
151
 
142
152
  telemetryStopChannel.publish(getTelemetryData())