dd-trace 5.0.0-pre-e2df7ec → 5.0.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/README.md CHANGED
@@ -218,7 +218,7 @@ esbuild.build({
218
218
  outfile: 'out.js',
219
219
  plugins: [ddPlugin],
220
220
  platform: 'node', // allows built-in modules to be required
221
- target: ['node16']
221
+ target: ['node18']
222
222
  }).catch((err) => {
223
223
  console.error(err)
224
224
  process.exit(1)
package/index.d.ts CHANGED
@@ -83,8 +83,9 @@ export declare interface Tracer extends opentracing.Tracer {
83
83
  * unless there is already an active span or `childOf` option. Note that this
84
84
  * option is deprecated and has been removed in version 4.0.
85
85
  */
86
- trace<T> (name: string, fn: (span?: Span, fn?: (error?: Error) => any) => T): T;
87
- trace<T> (name: string, options: TraceOptions & SpanOptions, fn: (span?: Span, done?: (error?: Error) => string) => T): T;
86
+ trace<T> (name: string, fn: (span: Span) => T): T;
87
+ trace<T> (name: string, fn: (span: Span, done: (error?: Error) => void) => T): T;
88
+ trace<T> (name: string, options: TraceOptions & SpanOptions, fn: (span?: Span, done?: (error?: Error) => void) => T): T;
88
89
 
89
90
  /**
90
91
  * Wrap a function to automatically create a span activated on its
@@ -883,7 +884,7 @@ interface Analyzable {
883
884
  measured?: boolean | { [key: string]: boolean };
884
885
  }
885
886
 
886
- declare namespace plugins {
887
+ export declare namespace plugins {
887
888
  /** @hidden */
888
889
  interface Integration {
889
890
  /**
@@ -1380,7 +1381,8 @@ declare namespace plugins {
1380
1381
  */
1381
1382
  interface ioredis extends Instrumentation {
1382
1383
  /**
1383
- * List of commands that should be instrumented.
1384
+ * List of commands that should be instrumented. Commands must be in
1385
+ * lowercase for example 'xread'.
1384
1386
  *
1385
1387
  * @default /^.*$/
1386
1388
  */
@@ -1396,7 +1398,8 @@ declare namespace plugins {
1396
1398
 
1397
1399
  /**
1398
1400
  * List of commands that should not be instrumented. Takes precedence over
1399
- * allowlist if a command matches an entry in both.
1401
+ * allowlist if a command matches an entry in both. Commands must be in
1402
+ * lowercase for example 'xread'.
1400
1403
  *
1401
1404
  * @default []
1402
1405
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.0.0-pre-e2df7ec",
3
+ "version": "5.0.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -65,27 +65,27 @@
65
65
  },
66
66
  "homepage": "https://github.com/DataDog/dd-trace-js#readme",
67
67
  "engines": {
68
- "node": ">=16"
68
+ "node": ">=18"
69
69
  },
70
70
  "dependencies": {
71
71
  "@datadog/native-appsec": "6.0.0",
72
72
  "@datadog/native-iast-rewriter": "2.2.2",
73
73
  "@datadog/native-iast-taint-tracking": "1.6.4",
74
74
  "@datadog/native-metrics": "^2.0.0",
75
- "@datadog/pprof": "4.1.0",
75
+ "@datadog/pprof": "5.0.0",
76
76
  "@datadog/sketches-js": "^2.1.0",
77
77
  "@opentelemetry/api": "^1.0.0",
78
78
  "@opentelemetry/core": "^1.14.0",
79
79
  "crypto-randomuuid": "^1.0.0",
80
80
  "dc-polyfill": "^0.1.2",
81
81
  "ignore": "^5.2.4",
82
- "import-in-the-middle": "^1.4.2",
82
+ "import-in-the-middle": "^1.7.1",
83
83
  "int64-buffer": "^0.1.9",
84
84
  "ipaddr.js": "^2.1.0",
85
85
  "istanbul-lib-coverage": "3.2.0",
86
86
  "jest-docblock": "^29.7.0",
87
87
  "koalas": "^1.0.2",
88
- "limiter": "^1.1.4",
88
+ "limiter": "1.1.5",
89
89
  "lodash.kebabcase": "^4.1.1",
90
90
  "lodash.pick": "^4.4.0",
91
91
  "lodash.sortby": "^4.7.0",
@@ -99,12 +99,12 @@
99
99
  "path-to-regexp": "^0.1.2",
100
100
  "pprof-format": "^2.0.7",
101
101
  "protobufjs": "^7.2.5",
102
- "tlhunter-sorted-set": "^0.1.0",
103
102
  "retry": "^0.13.1",
104
- "semver": "^7.5.4"
103
+ "semver": "^7.5.4",
104
+ "tlhunter-sorted-set": "^0.1.0"
105
105
  },
106
106
  "devDependencies": {
107
- "@types/node": ">=16",
107
+ "@types/node": ">=18",
108
108
  "autocannon": "^4.5.2",
109
109
  "aws-sdk": "^2.1446.0",
110
110
  "axios": "^0.21.2",
@@ -252,9 +252,10 @@ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^3.0.7', '^3.1.
252
252
  return Cluster
253
253
  })
254
254
 
255
- // semver >=3.2.0
255
+ // semver >=3.2.2
256
+ // NOTE: <3.2.2 segfaults on cluster.close() https://issues.couchbase.com/browse/JSCBC-936
256
257
 
257
- addHook({ name: 'couchbase', file: 'dist/collection.js', versions: ['>=3.2.0'] }, collection => {
258
+ addHook({ name: 'couchbase', file: 'dist/collection.js', versions: ['>=3.2.2'] }, collection => {
258
259
  const Collection = collection.Collection
259
260
 
260
261
  wrapAllNames(['upsert', 'insert', 'replace'], name => {
@@ -264,7 +265,7 @@ addHook({ name: 'couchbase', file: 'dist/collection.js', versions: ['>=3.2.0'] }
264
265
  return collection
265
266
  })
266
267
 
267
- addHook({ name: 'couchbase', file: 'dist/bucket.js', versions: ['>=3.2.0'] }, bucket => {
268
+ addHook({ name: 'couchbase', file: 'dist/bucket.js', versions: ['>=3.2.2'] }, bucket => {
268
269
  const Bucket = bucket.Bucket
269
270
  shimmer.wrap(Bucket.prototype, 'collection', getCollection => {
270
271
  return function () {
@@ -278,7 +279,7 @@ addHook({ name: 'couchbase', file: 'dist/bucket.js', versions: ['>=3.2.0'] }, bu
278
279
  return bucket
279
280
  })
280
281
 
281
- addHook({ name: 'couchbase', file: 'dist/cluster.js', versions: ['3.2.0 - 3.2.1', '>=3.2.2'] }, (cluster) => {
282
+ addHook({ name: 'couchbase', file: 'dist/cluster.js', versions: ['>=3.2.2'] }, (cluster) => {
282
283
  const Cluster = cluster.Cluster
283
284
 
284
285
  shimmer.wrap(Cluster.prototype, 'query', wrapV3Query)
@@ -495,6 +495,8 @@ addHook({
495
495
  _ddTestModuleId,
496
496
  _ddTestSessionId,
497
497
  _ddTestCommand,
498
+ _ddForcedToRun,
499
+ _ddUnskippable,
498
500
  ...restOfTestEnvironmentOptions
499
501
  } = testEnvironmentOptions
500
502
 
@@ -28,6 +28,8 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
28
28
  finish (message) {
29
29
  const span = this.activeSpan
30
30
 
31
+ if (!span) return
32
+
31
33
  if (message.message._handled) {
32
34
  span.setTag('pubsub.ack', 1)
33
35
  }
@@ -38,6 +38,20 @@ class JestPlugin extends CiPlugin {
38
38
  return 'jest'
39
39
  }
40
40
 
41
+ // The lists are the same for every test suite, so we can cache them
42
+ getUnskippableSuites (unskippableSuitesList) {
43
+ if (!this.unskippableSuites) {
44
+ this.unskippableSuites = JSON.parse(unskippableSuitesList)
45
+ }
46
+ return this.unskippableSuites
47
+ }
48
+ getForcedToRunSuites (forcedToRunSuitesList) {
49
+ if (!this.forcedToRunSuites) {
50
+ this.forcedToRunSuites = JSON.parse(forcedToRunSuitesList)
51
+ }
52
+ return this.forcedToRunSuites
53
+ }
54
+
41
55
  constructor (...args) {
42
56
  super(...args)
43
57
 
@@ -128,11 +142,17 @@ class JestPlugin extends CiPlugin {
128
142
  const testSuiteMetadata = getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, 'jest')
129
143
 
130
144
  if (_ddUnskippable) {
131
- this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
132
- testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
145
+ const unskippableSuites = this.getUnskippableSuites(_ddUnskippable)
146
+ if (unskippableSuites[testSuite]) {
147
+ this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
148
+ testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
149
+ }
133
150
  if (_ddForcedToRun) {
134
- this.telemetry.count(TELEMETRY_ITR_FORCED_TO_RUN, { testLevel: 'suite' })
135
- testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
151
+ const forcedToRunSuites = this.getForcedToRunSuites(_ddForcedToRun)
152
+ if (forcedToRunSuites[testSuite]) {
153
+ this.telemetry.count(TELEMETRY_ITR_FORCED_TO_RUN, { testLevel: 'suite' })
154
+ testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
155
+ }
136
156
  }
137
157
  }
138
158
 
@@ -77,30 +77,52 @@ function isMarkedAsUnskippable (test) {
77
77
  }
78
78
 
79
79
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
80
- return originalTests.reduce((acc, test) => {
80
+ const unskippableSuites = {}
81
+ const forcedToRunSuites = {}
82
+
83
+ const skippedSuites = []
84
+ const suitesToRun = []
85
+
86
+ for (const test of originalTests) {
81
87
  const relativePath = getTestSuitePath(test.path, rootDir)
82
88
  const shouldBeSkipped = skippableSuites.includes(relativePath)
83
-
84
89
  if (isMarkedAsUnskippable(test)) {
85
- acc.suitesToRun.push(test)
86
- if (test?.context?.config?.testEnvironmentOptions) {
87
- test.context.config.testEnvironmentOptions['_ddUnskippable'] = true
88
- acc.hasUnskippableSuites = true
89
- if (shouldBeSkipped) {
90
- test.context.config.testEnvironmentOptions['_ddForcedToRun'] = true
91
- acc.hasForcedToRunSuites = true
92
- }
90
+ suitesToRun.push(test)
91
+ unskippableSuites[relativePath] = true
92
+ if (shouldBeSkipped) {
93
+ forcedToRunSuites[relativePath] = true
93
94
  }
94
- return acc
95
+ continue
95
96
  }
96
-
97
97
  if (shouldBeSkipped) {
98
- acc.skippedSuites.push(relativePath)
98
+ skippedSuites.push(relativePath)
99
99
  } else {
100
- acc.suitesToRun.push(test)
100
+ suitesToRun.push(test)
101
101
  }
102
- return acc
103
- }, { skippedSuites: [], suitesToRun: [], hasUnskippableSuites: false, hasForcedToRunSuites: false })
102
+ }
103
+
104
+ const hasUnskippableSuites = Object.keys(unskippableSuites).length > 0
105
+ const hasForcedToRunSuites = Object.keys(forcedToRunSuites).length > 0
106
+
107
+ if (originalTests.length) {
108
+ // The config object is shared by all tests, so we can just take the first one
109
+ const [test] = originalTests
110
+ if (test?.context?.config?.testEnvironmentOptions) {
111
+ if (hasUnskippableSuites) {
112
+ test.context.config.testEnvironmentOptions._ddUnskippable = JSON.stringify(unskippableSuites)
113
+ }
114
+ if (hasForcedToRunSuites) {
115
+ test.context.config.testEnvironmentOptions._ddForcedToRun = JSON.stringify(forcedToRunSuites)
116
+ }
117
+ }
118
+ }
119
+
120
+ return {
121
+ skippedSuites,
122
+ suitesToRun,
123
+ hasUnskippableSuites,
124
+ hasForcedToRunSuites
125
+ }
104
126
  }
105
127
 
106
128
  module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun, isMarkedAsUnskippable }
@@ -611,7 +611,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
611
611
  this.startupLogs = isTrue(DD_TRACE_STARTUP_LOGS)
612
612
  // Disabled for CI Visibility's agentless
613
613
  this.telemetry = {
614
- enabled: DD_TRACE_EXPORTER !== 'datadog' && isTrue(DD_INSTRUMENTATION_TELEMETRY_ENABLED),
614
+ enabled: isTrue(DD_INSTRUMENTATION_TELEMETRY_ENABLED),
615
615
  heartbeatInterval: DD_TELEMETRY_HEARTBEAT_INTERVAL,
616
616
  debug: isTrue(DD_TELEMETRY_DEBUG),
617
617
  logCollection: isTrue(DD_TELEMETRY_LOG_COLLECTION_ENABLED),
@@ -1,9 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const RateLimiter = require('./rate_limiter')
4
3
  const Sampler = require('./sampler')
5
- const ext = require('../../../ext')
6
4
  const { setSamplingRules } = require('./startup-log')
5
+ const SamplingRule = require('./sampling_rule')
7
6
 
8
7
  const {
9
8
  SAMPLING_MECHANISM_DEFAULT,
@@ -16,14 +15,21 @@ const {
16
15
  DECISION_MAKER_KEY
17
16
  } = require('./constants')
18
17
 
19
- const SERVICE_NAME = ext.tags.SERVICE_NAME
20
- const SAMPLING_PRIORITY = ext.tags.SAMPLING_PRIORITY
21
- const MANUAL_KEEP = ext.tags.MANUAL_KEEP
22
- const MANUAL_DROP = ext.tags.MANUAL_DROP
23
- const USER_REJECT = ext.priority.USER_REJECT
24
- const AUTO_REJECT = ext.priority.AUTO_REJECT
25
- const AUTO_KEEP = ext.priority.AUTO_KEEP
26
- const USER_KEEP = ext.priority.USER_KEEP
18
+ const {
19
+ tags: {
20
+ MANUAL_KEEP,
21
+ MANUAL_DROP,
22
+ SAMPLING_PRIORITY,
23
+ SERVICE_NAME
24
+ },
25
+ priority: {
26
+ AUTO_REJECT,
27
+ AUTO_KEEP,
28
+ USER_REJECT,
29
+ USER_KEEP
30
+ }
31
+ } = require('../../../ext')
32
+
27
33
  const DEFAULT_KEY = 'service:,env:'
28
34
 
29
35
  const defaultSampler = new Sampler(AUTO_KEEP)
@@ -36,8 +42,7 @@ class PrioritySampler {
36
42
 
37
43
  configure (env, { sampleRate, rateLimit = 100, rules = [] } = {}) {
38
44
  this._env = env
39
- this._rules = this._normalizeRules(rules, sampleRate)
40
- this._limiter = new RateLimiter(rateLimit)
45
+ this._rules = this._normalizeRules(rules, sampleRate, rateLimit)
41
46
 
42
47
  setSamplingRules(this._rules)
43
48
  }
@@ -104,7 +109,7 @@ class PrioritySampler {
104
109
 
105
110
  _getPriorityFromAuto (span) {
106
111
  const context = this._getContext(span)
107
- const rule = this._findRule(context)
112
+ const rule = this._findRule(span)
108
113
 
109
114
  return rule
110
115
  ? this._getPriorityByRule(context, rule)
@@ -131,15 +136,14 @@ class PrioritySampler {
131
136
  context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
132
137
  context._sampling.mechanism = SAMPLING_MECHANISM_RULE
133
138
 
134
- return rule.sampler.isSampled(context) && this._isSampledByRateLimit(context) ? USER_KEEP : USER_REJECT
135
- }
139
+ const sampled = rule.sample()
140
+ const priority = sampled ? USER_KEEP : USER_REJECT
136
141
 
137
- _isSampledByRateLimit (context) {
138
- const allowed = this._limiter.isAllowed()
139
-
140
- context._trace[SAMPLING_LIMIT_DECISION] = this._limiter.effectiveRate()
142
+ if (sampled) {
143
+ context._trace[SAMPLING_LIMIT_DECISION] = rule.effectiveRate
144
+ }
141
145
 
142
- return allowed
146
+ return priority
143
147
  }
144
148
 
145
149
  _getPriorityByAgent (context) {
@@ -172,33 +176,21 @@ class PrioritySampler {
172
176
  }
173
177
  }
174
178
 
175
- _normalizeRules (rules, sampleRate) {
179
+ _normalizeRules (rules, sampleRate, rateLimit) {
176
180
  rules = [].concat(rules || [])
177
181
 
178
182
  return rules
179
- .concat({ sampleRate })
183
+ .concat({ sampleRate, maxPerSecond: rateLimit })
180
184
  .map(rule => ({ ...rule, sampleRate: parseFloat(rule.sampleRate) }))
181
185
  .filter(rule => !isNaN(rule.sampleRate))
182
- .map(rule => ({ ...rule, sampler: new Sampler(rule.sampleRate) }))
186
+ .map(SamplingRule.from)
183
187
  }
184
188
 
185
- _findRule (context) {
186
- for (let i = 0, l = this._rules.length; i < l; i++) {
187
- if (this._matchRule(context, this._rules[i])) return this._rules[i]
189
+ _findRule (span) {
190
+ for (const rule of this._rules) {
191
+ if (rule.match(span)) return rule
188
192
  }
189
193
  }
190
-
191
- _matchRule (context, rule) {
192
- const name = context._name
193
- const service = context._tags['service.name']
194
-
195
- if (rule.name instanceof RegExp && !rule.name.test(name)) return false
196
- if (typeof rule.name === 'string' && rule.name !== name) return false
197
- if (rule.service instanceof RegExp && !rule.service.test(service)) return false
198
- if (typeof rule.service === 'string' && rule.service !== service) return false
199
-
200
- return true
201
- }
202
194
  }
203
195
 
204
196
  function hasOwn (object, prop) {
@@ -35,6 +35,7 @@ class Config {
35
35
  DD_PROFILING_HEAP_ENABLED,
36
36
  DD_PROFILING_V8_PROFILER_BUG_WORKAROUND,
37
37
  DD_PROFILING_WALLTIME_ENABLED,
38
+ DD_PROFILING_EXPERIMENTAL_CPU_ENABLED,
38
39
  DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED,
39
40
  DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE,
40
41
  DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT,
@@ -91,14 +92,24 @@ class Config {
91
92
  logger.warn(`${deprecatedEnvVarName} is deprecated. Use DD_PROFILING_${shortVarName} instead.`)
92
93
  }
93
94
  }
95
+ // Profiler sampling contexts are not available on Windows, so features
96
+ // depending on those (code hotspots and endpoint collection) need to default
97
+ // to false on Windows.
98
+ const samplingContextsAvailable = process.platform !== 'win32'
99
+ function checkOptionAllowed (option, description) {
100
+ if (option && !samplingContextsAvailable) {
101
+ throw new Error(`${description} not supported on ${process.platform}.`)
102
+ }
103
+ }
94
104
  this.flushInterval = flushInterval
95
105
  this.uploadTimeout = uploadTimeout
96
106
  this.sourceMap = sourceMap
97
107
  this.debugSourceMaps = isTrue(coalesce(options.debugSourceMaps, DD_PROFILING_DEBUG_SOURCE_MAPS, false))
98
108
  this.endpointCollectionEnabled = isTrue(coalesce(options.endpointCollection,
99
109
  DD_PROFILING_ENDPOINT_COLLECTION_ENABLED,
100
- DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED, false))
110
+ DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED, samplingContextsAvailable))
101
111
  logExperimentalVarDeprecation('ENDPOINT_COLLECTION_ENABLED')
112
+ checkOptionAllowed(this.endpointCollectionEnabled, 'Endpoint collection')
102
113
 
103
114
  this.pprofPrefix = pprofPrefix
104
115
  this.v8ProfilerBugWorkaroundEnabled = isTrue(coalesce(options.v8ProfilerBugWorkaround,
@@ -147,8 +158,12 @@ class Config {
147
158
 
148
159
  this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
149
160
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
150
- DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, false))
161
+ DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, samplingContextsAvailable))
151
162
  logExperimentalVarDeprecation('CODEHOTSPOTS_ENABLED')
163
+ checkOptionAllowed(this.codeHotspotsEnabled, 'Code hotspots')
164
+
165
+ this.cpuProfilingEnabled = isTrue(coalesce(options.cpuProfilingEnabled,
166
+ DD_PROFILING_EXPERIMENTAL_CPU_ENABLED, false))
152
167
 
153
168
  this.profilers = ensureProfilers(profilers, this)
154
169
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const { promisify } = require('util')
5
+ const { threadId } = require('worker_threads')
5
6
  const writeFile = promisify(fs.writeFile)
6
7
 
7
8
  function formatDateTime (t) {
@@ -19,7 +20,7 @@ class FileExporter {
19
20
  const types = Object.keys(profiles)
20
21
  const dateStr = formatDateTime(end)
21
22
  const tasks = types.map(type => {
22
- return writeFile(`${this._pprofPrefix}${type}_${dateStr}.pprof`, profiles[type])
23
+ return writeFile(`${this._pprofPrefix}${type}_worker_${threadId}_${dateStr}.pprof`, profiles[type])
23
24
  })
24
25
 
25
26
  return Promise.all(tasks)
@@ -3,6 +3,7 @@
3
3
  const { EventEmitter } = require('events')
4
4
  const { Config } = require('./config')
5
5
  const { snapshotKinds } = require('./constants')
6
+ const { threadNamePrefix } = require('./profilers/shared')
6
7
 
7
8
  function maybeSourceMap (sourceMap, SourceMapper, debug) {
8
9
  if (!sourceMap) return
@@ -68,7 +69,7 @@ class Profiler extends EventEmitter {
68
69
  mapper,
69
70
  nearOOMCallback: this._nearOOMExport.bind(this)
70
71
  })
71
- this._logger.debug(`Started ${profiler.type} profiler`)
72
+ this._logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`)
72
73
  }
73
74
 
74
75
  this._capture(this._timeoutInterval, start)
@@ -97,7 +98,7 @@ class Profiler extends EventEmitter {
97
98
 
98
99
  // collect and export current profiles
99
100
  // once collect returns, profilers can be safely stopped
100
- this._collect(snapshotKinds.ON_SHUTDOWN)
101
+ this._collect(snapshotKinds.ON_SHUTDOWN, false)
101
102
  this._stop()
102
103
  }
103
104
 
@@ -108,13 +109,11 @@ class Profiler extends EventEmitter {
108
109
 
109
110
  for (const profiler of this._config.profilers) {
110
111
  profiler.stop()
111
- this._logger.debug(`Stopped ${profiler.type} profiler`)
112
+ this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
112
113
  }
113
114
 
114
115
  clearTimeout(this._timer)
115
116
  this._timer = undefined
116
-
117
- return this
118
117
  }
119
118
 
120
119
  _capture (timeout, start) {
@@ -128,18 +127,21 @@ class Profiler extends EventEmitter {
128
127
  }
129
128
  }
130
129
 
131
- async _collect (snapshotKind) {
130
+ async _collect (snapshotKind, restart = true) {
132
131
  if (!this._enabled) return
133
132
 
134
- const start = this._lastStart
135
- const end = new Date()
133
+ const startDate = this._lastStart
134
+ const endDate = new Date()
136
135
  const profiles = []
137
136
  const encodedProfiles = {}
138
137
 
139
138
  try {
140
139
  // collect profiles synchronously so that profilers can be safely stopped asynchronously
141
140
  for (const profiler of this._config.profilers) {
142
- const profile = profiler.profile(start, end)
141
+ const profile = profiler.profile(restart, startDate, endDate)
142
+ if (!restart) {
143
+ this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
144
+ }
143
145
  if (!profile) continue
144
146
  profiles.push({ profiler, profile })
145
147
  }
@@ -155,8 +157,10 @@ class Profiler extends EventEmitter {
155
157
  })
156
158
  }
157
159
 
158
- this._capture(this._timeoutInterval, end)
159
- await this._submit(encodedProfiles, start, end, snapshotKind)
160
+ if (restart) {
161
+ this._capture(this._timeoutInterval, endDate)
162
+ }
163
+ await this._submit(encodedProfiles, startDate, endDate, snapshotKind)
160
164
  this._logger.debug('Submitted profiles')
161
165
  } catch (err) {
162
166
  this._logger.error(err)
@@ -196,10 +200,10 @@ class ServerlessProfiler extends Profiler {
196
200
  this._flushAfterIntervals = this._config.flushInterval / 1000
197
201
  }
198
202
 
199
- async _collect (snapshotKind) {
200
- if (this._profiledIntervals >= this._flushAfterIntervals) {
203
+ async _collect (snapshotKind, restart = true) {
204
+ if (this._profiledIntervals >= this._flushAfterIntervals || !restart) {
201
205
  this._profiledIntervals = 0
202
- await super._collect(snapshotKind)
206
+ await super._collect(snapshotKind, restart)
203
207
  } else {
204
208
  this._profiledIntervals += 1
205
209
  this._capture(this._timeoutInterval, new Date())
@@ -159,22 +159,24 @@ class EventsProfiler {
159
159
  }
160
160
 
161
161
  start () {
162
+ // if already started, do nothing
163
+ if (this._observer) return
164
+
162
165
  function add (items) {
163
166
  this.entries.push(...items.getEntries())
164
167
  }
165
- if (!this._observer) {
166
- this._observer = new PerformanceObserver(add.bind(this))
167
- }
168
+ this._observer = new PerformanceObserver(add.bind(this))
168
169
  this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
169
170
  }
170
171
 
171
172
  stop () {
172
173
  if (this._observer) {
173
174
  this._observer.disconnect()
175
+ this._observer = undefined
174
176
  }
175
177
  }
176
178
 
177
- profile (startDate, endDate) {
179
+ profile (restart, startDate, endDate) {
178
180
  if (this.entries.length === 0) {
179
181
  // No events in the period; don't produce a profile
180
182
  return null
@@ -243,6 +245,10 @@ class EventsProfiler {
243
245
  unit: stringTable.dedup(pprofValueUnit)
244
246
  })
245
247
 
248
+ if (!restart) {
249
+ this.stop()
250
+ }
251
+
246
252
  return new Profile({
247
253
  sampleType: [timeValueType],
248
254
  timeNanos: endDate.getTime() * MS_TO_NS,
@@ -29,11 +29,17 @@ function cacheThreadLabels () {
29
29
  }
30
30
  }
31
31
 
32
+ function getNonJSThreadsLabels () {
33
+ return { [THREAD_NAME_LABEL]: 'Non-JS threads', [THREAD_ID_LABEL]: 'NA', [OS_THREAD_ID_LABEL]: 'NA' }
34
+ }
35
+
32
36
  module.exports = {
33
37
  END_TIMESTAMP_LABEL,
34
38
  THREAD_NAME_LABEL,
35
39
  THREAD_ID_LABEL,
40
+ OS_THREAD_ID_LABEL,
36
41
  threadNamePrefix,
37
42
  eventLoopThreadName,
43
+ getNonJSThreadsLabels,
38
44
  getThreadLabels: cacheThreadLabels()
39
45
  }
@@ -14,9 +14,12 @@ class NativeSpaceProfiler {
14
14
  this._stackDepth = options.stackDepth || 64
15
15
  this._pprof = undefined
16
16
  this._oomMonitoring = options.oomMonitoring || {}
17
+ this._started = false
17
18
  }
18
19
 
19
20
  start ({ mapper, nearOOMCallback } = {}) {
21
+ if (this._started) return
22
+
20
23
  this._mapper = mapper
21
24
  this._pprof = require('@datadog/pprof')
22
25
  this._pprof.heap.start(this._samplingInterval, this._stackDepth)
@@ -31,10 +34,16 @@ class NativeSpaceProfiler {
31
34
  strategiesToCallbackMode(strategies, this._pprof.heap.CallbackMode)
32
35
  )
33
36
  }
37
+
38
+ this._started = true
34
39
  }
35
40
 
36
- profile () {
37
- return this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
41
+ profile (restart) {
42
+ const profile = this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
43
+ if (!restart) {
44
+ this.stop()
45
+ }
46
+ return profile
38
47
  }
39
48
 
40
49
  encode (profile) {
@@ -42,7 +51,13 @@ class NativeSpaceProfiler {
42
51
  }
43
52
 
44
53
  stop () {
54
+ if (!this._started) return
45
55
  this._pprof.heap.stop()
56
+ this._started = false
57
+ }
58
+
59
+ isStarted () {
60
+ return this._started
46
61
  }
47
62
  }
48
63
 
@@ -7,7 +7,7 @@ const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../
7
7
  const { WEB } = require('../../../../../ext/types')
8
8
  const runtimeMetrics = require('../../runtime_metrics')
9
9
  const telemetryMetrics = require('../../telemetry/metrics')
10
- const { END_TIMESTAMP_LABEL, getThreadLabels } = require('./shared')
10
+ const { END_TIMESTAMP_LABEL, getNonJSThreadsLabels, getThreadLabels } = require('./shared')
11
11
 
12
12
  const beforeCh = dc.channel('dd-trace:storage:before')
13
13
  const enterCh = dc.channel('dd-trace:storage:enter')
@@ -78,13 +78,15 @@ class NativeWallProfiler {
78
78
  this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
79
79
  this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
80
80
  this._timelineEnabled = !!options.timelineEnabled
81
+ this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
81
82
  // We need to capture span data into the sample context for either code hotspots
82
83
  // or endpoint collection.
83
84
  this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
84
85
  // We need to run the pprof wall profiler with sample contexts if we're either
85
86
  // capturing span data or timeline is enabled (so we need sample timestamps, and for now
86
- // timestamps require the sample contexts feature in the pprof wall profiler.)
87
- this._withContexts = this._captureSpanData || this._timelineEnabled
87
+ // timestamps require the sample contexts feature in the pprof wall profiler), or
88
+ // cpu profiling is enabled.
89
+ this._withContexts = this._captureSpanData || this._timelineEnabled || this._cpuProfilingEnabled
88
90
  this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
89
91
  this._mapper = undefined
90
92
  this._pprof = undefined
@@ -131,7 +133,8 @@ class NativeWallProfiler {
131
133
  sourceMapper: this._mapper,
132
134
  withContexts: this._withContexts,
133
135
  lineNumbers: false,
134
- workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled
136
+ workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
137
+ collectCpuTime: this._cpuProfilingEnabled
135
138
  })
136
139
 
137
140
  if (this._withContexts) {
@@ -220,22 +223,42 @@ class NativeWallProfiler {
220
223
 
221
224
  _stop (restart) {
222
225
  if (!this._started) return
226
+
223
227
  if (this._captureSpanData) {
224
228
  // update last sample context if needed
225
229
  this._enter()
226
230
  this._lastSampleCount = 0
227
231
  }
228
232
  const profile = this._pprof.time.stop(restart, this._generateLabels)
233
+
229
234
  if (restart) {
230
235
  const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
231
236
  if (v8BugDetected !== 0) {
232
237
  this._reportV8bug(v8BugDetected === 1)
233
238
  }
239
+ } else {
240
+ if (this._captureSpanData) {
241
+ beforeCh.unsubscribe(this._enter)
242
+ enterCh.unsubscribe(this._enter)
243
+ spanFinishCh.unsubscribe(this._spanFinished)
244
+ this._profilerState = undefined
245
+ this._lastSpan = undefined
246
+ this._lastStartedSpans = undefined
247
+ this._lastWebTags = undefined
248
+ }
249
+ this._started = false
234
250
  }
251
+
235
252
  return profile
236
253
  }
237
254
 
238
- _generateLabels (context) {
255
+ _generateLabels ({ node, context }) {
256
+ // check for special node that represents CPU time all non-JS threads.
257
+ // In that case only return a special thread name label since we cannot associate any timestamp/span/endpoint to it.
258
+ if (node.name === this._pprof.time.constants.NON_JS_THREADS_FUNCTION_NAME) {
259
+ return getNonJSThreadsLabels()
260
+ }
261
+
239
262
  if (context == null) {
240
263
  // generateLabels is also called for samples without context.
241
264
  // In that case just return thread labels.
@@ -267,8 +290,8 @@ class NativeWallProfiler {
267
290
  return labels
268
291
  }
269
292
 
270
- profile () {
271
- return this._stop(true)
293
+ profile (restart) {
294
+ return this._stop(restart)
272
295
  }
273
296
 
274
297
  encode (profile) {
@@ -276,21 +299,11 @@ class NativeWallProfiler {
276
299
  }
277
300
 
278
301
  stop () {
279
- if (!this._started) return
280
-
281
- const profile = this._stop(false)
282
- if (this._captureSpanData) {
283
- beforeCh.unsubscribe(this._enter)
284
- enterCh.unsubscribe(this._enter)
285
- spanFinishCh.unsubscribe(this._spanFinished)
286
- this._profilerState = undefined
287
- this._lastSpan = undefined
288
- this._lastStartedSpans = undefined
289
- this._lastWebTags = undefined
290
- }
302
+ this._stop(false)
303
+ }
291
304
 
292
- this._started = false
293
- return profile
305
+ isStarted () {
306
+ return this._started
294
307
  }
295
308
  }
296
309
 
@@ -0,0 +1,130 @@
1
+ 'use strict'
2
+
3
+ const { globMatch } = require('../src/util')
4
+ const RateLimiter = require('./rate_limiter')
5
+ const Sampler = require('./sampler')
6
+
7
+ class AlwaysMatcher {
8
+ match () {
9
+ return true
10
+ }
11
+ }
12
+
13
+ class GlobMatcher {
14
+ constructor (pattern, locator) {
15
+ this.pattern = pattern
16
+ this.locator = locator
17
+ }
18
+
19
+ match (span) {
20
+ const subject = this.locator(span)
21
+ if (!subject) return false
22
+ return globMatch(this.pattern, subject)
23
+ }
24
+ }
25
+
26
+ class RegExpMatcher {
27
+ constructor (pattern, locator) {
28
+ this.pattern = pattern
29
+ this.locator = locator
30
+ }
31
+
32
+ match (span) {
33
+ const subject = this.locator(span)
34
+ if (!subject) return false
35
+ return this.pattern.test(subject)
36
+ }
37
+ }
38
+
39
+ function matcher (pattern, locator) {
40
+ if (pattern instanceof RegExp) {
41
+ return new RegExpMatcher(pattern, locator)
42
+ }
43
+
44
+ if (typeof pattern === 'string' && pattern !== '*') {
45
+ return new GlobMatcher(pattern, locator)
46
+ }
47
+
48
+ return new AlwaysMatcher()
49
+ }
50
+
51
+ function makeTagLocator (tag) {
52
+ return (span) => span.context()._tags[tag]
53
+ }
54
+
55
+ function nameLocator (span) {
56
+ return span.context()._name
57
+ }
58
+
59
+ function serviceLocator (span) {
60
+ const { _tags: tags } = span.context()
61
+ return tags.service ||
62
+ tags['service.name'] ||
63
+ span.tracer()._service
64
+ }
65
+
66
+ class SamplingRule {
67
+ constructor ({ name, service, resource, tags, sampleRate = 1.0, maxPerSecond } = {}) {
68
+ this.matchers = []
69
+
70
+ if (name) {
71
+ this.matchers.push(matcher(name, nameLocator))
72
+ }
73
+ if (service) {
74
+ this.matchers.push(matcher(service, serviceLocator))
75
+ }
76
+ if (resource) {
77
+ this.matchers.push(matcher(resource, makeTagLocator('resource.name')))
78
+ }
79
+ for (const [key, value] of Object.entries(tags || {})) {
80
+ this.matchers.push(matcher(value, makeTagLocator(key)))
81
+ }
82
+
83
+ this._sampler = new Sampler(sampleRate)
84
+ this._limiter = undefined
85
+
86
+ if (Number.isFinite(maxPerSecond)) {
87
+ this._limiter = new RateLimiter(maxPerSecond)
88
+ }
89
+ }
90
+
91
+ static from (config) {
92
+ return new SamplingRule(config)
93
+ }
94
+
95
+ get sampleRate () {
96
+ return this._sampler.rate()
97
+ }
98
+
99
+ get effectiveRate () {
100
+ return this._limiter && this._limiter.effectiveRate()
101
+ }
102
+
103
+ get maxPerSecond () {
104
+ return this._limiter && this._limiter._rateLimit
105
+ }
106
+
107
+ match (span) {
108
+ for (const matcher of this.matchers) {
109
+ if (!matcher.match(span)) {
110
+ return false
111
+ }
112
+ }
113
+
114
+ return true
115
+ }
116
+
117
+ sample () {
118
+ if (!this._sampler.isSampled()) {
119
+ return false
120
+ }
121
+
122
+ if (this._limiter) {
123
+ return this._limiter.isAllowed()
124
+ }
125
+
126
+ return true
127
+ }
128
+ }
129
+
130
+ module.exports = SamplingRule
@@ -1,67 +1,16 @@
1
1
  'use strict'
2
- const { globMatch } = require('../src/util')
3
- const { USER_KEEP, AUTO_KEEP } = require('../../../ext').priority
4
- const RateLimiter = require('./rate_limiter')
5
- const Sampler = require('./sampler')
6
-
7
- class SpanSamplingRule {
8
- constructor ({ service, name, sampleRate = 1.0, maxPerSecond } = {}) {
9
- this.service = service
10
- this.name = name
11
-
12
- this._sampler = new Sampler(sampleRate)
13
- this._limiter = undefined
14
-
15
- if (Number.isFinite(maxPerSecond)) {
16
- this._limiter = new RateLimiter(maxPerSecond)
17
- }
18
- }
19
-
20
- get sampleRate () {
21
- return this._sampler.rate()
22
- }
23
-
24
- get maxPerSecond () {
25
- return this._limiter && this._limiter._rateLimit
26
- }
27
-
28
- static from (config) {
29
- return new SpanSamplingRule(config)
30
- }
31
-
32
- match (service, name) {
33
- if (this.service && !globMatch(this.service, service)) {
34
- return false
35
- }
36
-
37
- if (this.name && !globMatch(this.name, name)) {
38
- return false
39
- }
40
-
41
- return true
42
- }
43
-
44
- sample () {
45
- if (!this._sampler.isSampled()) {
46
- return false
47
- }
48
2
 
49
- if (this._limiter) {
50
- return this._limiter.isAllowed()
51
- }
52
-
53
- return true
54
- }
55
- }
3
+ const { USER_KEEP, AUTO_KEEP } = require('../../../ext').priority
4
+ const SamplingRule = require('./sampling_rule')
56
5
 
57
6
  class SpanSampler {
58
7
  constructor ({ spanSamplingRules = [] } = {}) {
59
- this._rules = spanSamplingRules.map(SpanSamplingRule.from)
8
+ this._rules = spanSamplingRules.map(SamplingRule.from)
60
9
  }
61
10
 
62
- findRule (service, name) {
11
+ findRule (context) {
63
12
  for (const rule of this._rules) {
64
- if (rule.match(service, name)) {
13
+ if (rule.match(context)) {
65
14
  return rule
66
15
  }
67
16
  }
@@ -73,14 +22,7 @@ class SpanSampler {
73
22
 
74
23
  const { started } = spanContext._trace
75
24
  for (const span of started) {
76
- const context = span.context()
77
- const tags = context._tags || {}
78
- const name = context._name
79
- const service = tags.service ||
80
- tags['service.name'] ||
81
- span.tracer()._service
82
-
83
- const rule = this.findRule(service, name)
25
+ const rule = this.findRule(span)
84
26
  if (rule && rule.sample()) {
85
27
  span.context()._spanSampling = {
86
28
  sampleRate: rule.sampleRate,
@@ -1,6 +1,7 @@
1
1
 
2
2
  const request = require('../exporters/common/request')
3
3
  const log = require('../log')
4
+
4
5
  let agentTelemetry = true
5
6
 
6
7
  function getHeaders (config, application, reqType) {
@@ -15,9 +16,22 @@ function getHeaders (config, application, reqType) {
15
16
  if (debug) {
16
17
  headers['dd-telemetry-debug-enabled'] = 'true'
17
18
  }
19
+ if (config.apiKey) {
20
+ headers['dd-api-key'] = config.apiKey
21
+ }
18
22
  return headers
19
23
  }
20
24
 
25
+ function getAgentlessTelemetryEndpoint (site) {
26
+ if (site === 'datad0g.com') { // staging
27
+ return 'https://all-http-intake.logs.datad0g.com'
28
+ }
29
+ if (site === 'datadoghq.eu') {
30
+ return 'https://instrumentation-telemetry-intake.eu1.datadoghq.com'
31
+ }
32
+ return `https://instrumentation-telemetry-intake.${site}`
33
+ }
34
+
21
35
  let seqId = 0
22
36
 
23
37
  function getPayload (payload) {
@@ -35,17 +49,33 @@ function sendData (config, application, host, reqType, payload = {}, cb = () =>
35
49
  const {
36
50
  hostname,
37
51
  port,
38
- url
52
+ experimental,
53
+ isCiVisibility
39
54
  } = config
40
55
 
56
+ let url = config.url
57
+
58
+ const isCiVisibilityAgentlessMode = isCiVisibility && experimental?.exporter === 'datadog'
59
+
60
+ if (isCiVisibilityAgentlessMode) {
61
+ try {
62
+ url = url || new URL(getAgentlessTelemetryEndpoint(config.site))
63
+ } catch (err) {
64
+ log.error(err)
65
+ // No point to do the request if the URL is invalid
66
+ return cb(err, { payload, reqType })
67
+ }
68
+ }
69
+
41
70
  const options = {
42
71
  url,
43
72
  hostname,
44
73
  port,
45
74
  method: 'POST',
46
- path: '/telemetry/proxy/api/v2/apmtelemetry',
75
+ path: isCiVisibilityAgentlessMode ? '/api/v2/apmtelemetry' : '/telemetry/proxy/api/v2/apmtelemetry',
47
76
  headers: getHeaders(config, application, reqType)
48
77
  }
78
+
49
79
  const data = JSON.stringify({
50
80
  api_version: 'v2',
51
81
  naming_schema_version: config.spanAttributeSchema ? config.spanAttributeSchema : '',
@@ -65,24 +95,13 @@ function sendData (config, application, host, reqType, payload = {}, cb = () =>
65
95
  agentTelemetry = false
66
96
  }
67
97
  // figure out which data center to send to
68
- let backendUrl
69
- const dataCenters = [
70
- 'datadoghq.com',
71
- 'us3.datadoghq.com',
72
- 'us5.datadoghq.com',
73
- 'ap1.datadoghq.com',
74
- 'eu1.datadoghq.com'
75
- ]
76
- if (config.site === 'datad0g.com') { // staging
77
- backendUrl = 'https://all-http-intake.logs.datad0g.com/api/v2/apmtelemetry'
78
- } else if (dataCenters.includes(config.site)) {
79
- backendUrl = 'https://instrumentation-telemetry-intake.' + config.site + '/api/v2/apmtelemetry'
80
- }
98
+ const backendUrl = getAgentlessTelemetryEndpoint(config.site)
81
99
  const backendHeader = { ...options.headers, 'DD-API-KEY': process.env.DD_API_KEY }
82
100
  const backendOptions = {
83
101
  ...options,
84
102
  url: backendUrl,
85
- headers: backendHeader
103
+ headers: backendHeader,
104
+ path: '/api/v2/apmtelemetry'
86
105
  }
87
106
  if (backendUrl) {
88
107
  request(data, backendOptions, (error) => { log.error(error) })