dd-trace 4.33.0 → 4.35.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/index.d.ts +15 -0
- package/package.json +3 -2
- package/packages/datadog-instrumentations/src/fetch.js +6 -45
- package/packages/datadog-instrumentations/src/helpers/fetch.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -1
- package/packages/datadog-instrumentations/src/jest.js +161 -14
- package/packages/datadog-instrumentations/src/kafkajs.js +4 -7
- package/packages/datadog-instrumentations/src/mongoose.js +2 -1
- package/packages/datadog-instrumentations/src/oracledb.js +1 -1
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +6 -1
- package/packages/datadog-instrumentations/src/selenium.js +69 -0
- package/packages/datadog-plugin-cucumber/src/index.js +2 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +2 -2
- package/packages/datadog-plugin-cypress/src/support.js +19 -3
- package/packages/datadog-plugin-fetch/src/index.js +17 -11
- package/packages/datadog-plugin-jest/src/index.js +7 -2
- package/packages/datadog-plugin-mocha/src/index.js +4 -5
- package/packages/datadog-plugin-openai/src/services.js +2 -1
- package/packages/datadog-plugin-playwright/src/index.js +2 -2
- package/packages/datadog-plugin-selenium/src/index.js +71 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +70 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +14 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-rule-type.js +6 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-analyzer.js +5 -50
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +742 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +539 -66
- package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +6 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/reporter.js +11 -10
- package/packages/dd-trace/src/appsec/telemetry.js +36 -7
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +9 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/config.js +97 -10
- package/packages/dd-trace/src/dogstatsd.js +13 -11
- package/packages/dd-trace/src/index.js +5 -1
- package/packages/dd-trace/src/noop/dogstatsd.js +11 -0
- package/packages/dd-trace/src/noop/proxy.js +3 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +10 -4
- package/packages/dd-trace/src/opentracing/span.js +2 -0
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/git.js +33 -11
- package/packages/dd-trace/src/plugins/util/test.js +34 -3
- package/packages/dd-trace/src/profiling/config.js +8 -4
- package/packages/dd-trace/src/profiling/exporters/agent.js +5 -3
- package/packages/dd-trace/src/profiling/profiler.js +4 -0
- package/packages/dd-trace/src/profiling/ssi-telemetry-mock-profiler.js +33 -0
- package/packages/dd-trace/src/profiling/ssi-telemetry.js +167 -0
- package/packages/dd-trace/src/proxy.js +7 -1
- package/packages/dd-trace/src/tagger.js +13 -3
- package/packages/dd-trace/src/telemetry/index.js +5 -4
- package/packages/dd-trace/src/telemetry/metrics.js +2 -2
|
@@ -28,7 +28,7 @@ const {
|
|
|
28
28
|
const { filterSensitiveInfoFromRepository } = require('./url')
|
|
29
29
|
const { storage } = require('../../../../datadog-core')
|
|
30
30
|
|
|
31
|
-
const GIT_REV_LIST_MAX_BUFFER =
|
|
31
|
+
const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB
|
|
32
32
|
|
|
33
33
|
function sanitizedExec (
|
|
34
34
|
cmd,
|
|
@@ -53,11 +53,15 @@ function sanitizedExec (
|
|
|
53
53
|
distributionMetric(durationMetric.name, durationMetric.tags, Date.now() - startTime)
|
|
54
54
|
}
|
|
55
55
|
return result
|
|
56
|
-
} catch (
|
|
56
|
+
} catch (err) {
|
|
57
57
|
if (errorMetric) {
|
|
58
|
-
incrementCountMetric(errorMetric.name, {
|
|
58
|
+
incrementCountMetric(errorMetric.name, {
|
|
59
|
+
...errorMetric.tags,
|
|
60
|
+
errorType: err.code,
|
|
61
|
+
exitCode: err.status || err.errno
|
|
62
|
+
})
|
|
59
63
|
}
|
|
60
|
-
log.error(
|
|
64
|
+
log.error(err)
|
|
61
65
|
return ''
|
|
62
66
|
} finally {
|
|
63
67
|
storage.enterWith(store)
|
|
@@ -129,7 +133,10 @@ function unshallowRepository () {
|
|
|
129
133
|
} catch (err) {
|
|
130
134
|
// If the local HEAD is a commit that has not been pushed to the remote, the above command will fail.
|
|
131
135
|
log.error(err)
|
|
132
|
-
incrementCountMetric(
|
|
136
|
+
incrementCountMetric(
|
|
137
|
+
TELEMETRY_GIT_COMMAND_ERRORS,
|
|
138
|
+
{ command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
|
|
139
|
+
)
|
|
133
140
|
const upstreamRemote = sanitizedExec('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
|
|
134
141
|
try {
|
|
135
142
|
cp.execFileSync('git', [
|
|
@@ -139,7 +146,10 @@ function unshallowRepository () {
|
|
|
139
146
|
} catch (err) {
|
|
140
147
|
// If the CI is working on a detached HEAD or branch tracking hasn’t been set up, the above command will fail.
|
|
141
148
|
log.error(err)
|
|
142
|
-
incrementCountMetric(
|
|
149
|
+
incrementCountMetric(
|
|
150
|
+
TELEMETRY_GIT_COMMAND_ERRORS,
|
|
151
|
+
{ command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
|
|
152
|
+
)
|
|
143
153
|
// We use sanitizedExec here because if this last option fails, we'll give up.
|
|
144
154
|
sanitizedExec(
|
|
145
155
|
'git',
|
|
@@ -175,13 +185,16 @@ function getLatestCommits () {
|
|
|
175
185
|
return result
|
|
176
186
|
} catch (err) {
|
|
177
187
|
log.error(`Get latest commits failed: ${err.message}`)
|
|
178
|
-
incrementCountMetric(
|
|
188
|
+
incrementCountMetric(
|
|
189
|
+
TELEMETRY_GIT_COMMAND_ERRORS,
|
|
190
|
+
{ command: 'get_local_commits', errorType: err.status }
|
|
191
|
+
)
|
|
179
192
|
return []
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
function getCommitsRevList (commitsToExclude, commitsToInclude) {
|
|
184
|
-
let result =
|
|
197
|
+
let result = null
|
|
185
198
|
|
|
186
199
|
const commitsToExcludeString = commitsToExclude.map(commit => `^${commit}`)
|
|
187
200
|
|
|
@@ -205,7 +218,10 @@ function getCommitsRevList (commitsToExclude, commitsToInclude) {
|
|
|
205
218
|
.filter(commit => commit)
|
|
206
219
|
} catch (err) {
|
|
207
220
|
log.error(`Get commits to upload failed: ${err.message}`)
|
|
208
|
-
incrementCountMetric(
|
|
221
|
+
incrementCountMetric(
|
|
222
|
+
TELEMETRY_GIT_COMMAND_ERRORS,
|
|
223
|
+
{ command: 'get_objects', errorType: err.code, exitCode: err.status || err.errno } // err.status might be null
|
|
224
|
+
)
|
|
209
225
|
}
|
|
210
226
|
distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_objects' }, Date.now() - startTime)
|
|
211
227
|
return result
|
|
@@ -245,7 +261,10 @@ function generatePackFilesForCommits (commitsToUpload) {
|
|
|
245
261
|
result = execGitPackObjects(temporaryPath)
|
|
246
262
|
} catch (err) {
|
|
247
263
|
log.error(err)
|
|
248
|
-
incrementCountMetric(
|
|
264
|
+
incrementCountMetric(
|
|
265
|
+
TELEMETRY_GIT_COMMAND_ERRORS,
|
|
266
|
+
{ command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code }
|
|
267
|
+
)
|
|
249
268
|
/**
|
|
250
269
|
* The generation of pack files in the temporary folder (from `os.tmpdir()`)
|
|
251
270
|
* sometimes fails in certain CI setups with the error message
|
|
@@ -262,7 +281,10 @@ function generatePackFilesForCommits (commitsToUpload) {
|
|
|
262
281
|
result = execGitPackObjects(cwdPath)
|
|
263
282
|
} catch (err) {
|
|
264
283
|
log.error(err)
|
|
265
|
-
incrementCountMetric(
|
|
284
|
+
incrementCountMetric(
|
|
285
|
+
TELEMETRY_GIT_COMMAND_ERRORS,
|
|
286
|
+
{ command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code }
|
|
287
|
+
)
|
|
266
288
|
}
|
|
267
289
|
}
|
|
268
290
|
distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'pack_objects' }, Date.now() - startTime)
|
|
@@ -53,7 +53,8 @@ const TEST_CONFIGURATION_BROWSER_NAME = 'test.configuration.browser_name'
|
|
|
53
53
|
// Early flake detection
|
|
54
54
|
const TEST_IS_NEW = 'test.is_new'
|
|
55
55
|
const TEST_IS_RETRY = 'test.is_retry'
|
|
56
|
-
const
|
|
56
|
+
const TEST_EARLY_FLAKE_ENABLED = 'test.early_flake.enabled'
|
|
57
|
+
const TEST_EARLY_FLAKE_ABORT_REASON = 'test.early_flake.abort_reason'
|
|
57
58
|
|
|
58
59
|
const CI_APP_ORIGIN = 'ciapp-test'
|
|
59
60
|
|
|
@@ -71,6 +72,12 @@ const ITR_CORRELATION_ID = 'itr_correlation_id'
|
|
|
71
72
|
|
|
72
73
|
const TEST_CODE_COVERAGE_LINES_PCT = 'test.code_coverage.lines_pct'
|
|
73
74
|
|
|
75
|
+
// selenium tags
|
|
76
|
+
const TEST_BROWSER_DRIVER = 'test.browser.driver'
|
|
77
|
+
const TEST_BROWSER_DRIVER_VERSION = 'test.browser.driver_version'
|
|
78
|
+
const TEST_BROWSER_NAME = 'test.browser.name'
|
|
79
|
+
const TEST_BROWSER_VERSION = 'test.browser.version'
|
|
80
|
+
|
|
74
81
|
// jest worker variables
|
|
75
82
|
const JEST_WORKER_TRACE_PAYLOAD_CODE = 60
|
|
76
83
|
const JEST_WORKER_COVERAGE_PAYLOAD_CODE = 61
|
|
@@ -102,7 +109,8 @@ module.exports = {
|
|
|
102
109
|
TEST_CONFIGURATION_BROWSER_NAME,
|
|
103
110
|
TEST_IS_NEW,
|
|
104
111
|
TEST_IS_RETRY,
|
|
105
|
-
|
|
112
|
+
TEST_EARLY_FLAKE_ENABLED,
|
|
113
|
+
TEST_EARLY_FLAKE_ABORT_REASON,
|
|
106
114
|
getTestEnvironmentMetadata,
|
|
107
115
|
getTestParametersString,
|
|
108
116
|
finishAllTraceSpans,
|
|
@@ -141,7 +149,12 @@ module.exports = {
|
|
|
141
149
|
EFD_STRING,
|
|
142
150
|
EFD_TEST_NAME_REGEX,
|
|
143
151
|
removeEfdStringFromTestName,
|
|
144
|
-
addEfdStringToTestName
|
|
152
|
+
addEfdStringToTestName,
|
|
153
|
+
getIsFaultyEarlyFlakeDetection,
|
|
154
|
+
TEST_BROWSER_DRIVER,
|
|
155
|
+
TEST_BROWSER_DRIVER_VERSION,
|
|
156
|
+
TEST_BROWSER_NAME,
|
|
157
|
+
TEST_BROWSER_VERSION
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
// Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
|
|
@@ -571,3 +584,21 @@ function addEfdStringToTestName (testName, numAttempt) {
|
|
|
571
584
|
function removeEfdStringFromTestName (testName) {
|
|
572
585
|
return testName.replace(EFD_TEST_NAME_REGEX, '')
|
|
573
586
|
}
|
|
587
|
+
|
|
588
|
+
function getIsFaultyEarlyFlakeDetection (projectSuites, testsBySuiteName, faultyThresholdPercentage) {
|
|
589
|
+
let newSuites = 0
|
|
590
|
+
for (const suite of projectSuites) {
|
|
591
|
+
if (!testsBySuiteName[suite]) {
|
|
592
|
+
newSuites++
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const newSuitesPercentage = (newSuites / projectSuites.length) * 100
|
|
596
|
+
|
|
597
|
+
// The faulty threshold represents a percentage, but we also want to consider
|
|
598
|
+
// smaller projects, where big variations in the % are more likely.
|
|
599
|
+
// This is why we also check the absolute number of new suites.
|
|
600
|
+
return (
|
|
601
|
+
newSuites > faultyThresholdPercentage &&
|
|
602
|
+
newSuitesPercentage > faultyThresholdPercentage
|
|
603
|
+
)
|
|
604
|
+
}
|
|
@@ -21,6 +21,7 @@ class Config {
|
|
|
21
21
|
DD_AGENT_HOST,
|
|
22
22
|
DD_ENV,
|
|
23
23
|
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
24
|
+
DD_PROFILING_CPU_ENABLED,
|
|
24
25
|
DD_PROFILING_DEBUG_SOURCE_MAPS,
|
|
25
26
|
DD_PROFILING_ENABLED,
|
|
26
27
|
DD_PROFILING_ENDPOINT_COLLECTION_ENABLED,
|
|
@@ -165,7 +166,7 @@ class Config {
|
|
|
165
166
|
|
|
166
167
|
this.timelineEnabled = isTrue(coalesce(options.timelineEnabled,
|
|
167
168
|
DD_PROFILING_TIMELINE_ENABLED,
|
|
168
|
-
DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED,
|
|
169
|
+
DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, samplingContextsAvailable))
|
|
169
170
|
logExperimentalVarDeprecation('TIMELINE_ENABLED')
|
|
170
171
|
checkOptionWithSamplingContextAllowed(this.timelineEnabled, 'Timeline view')
|
|
171
172
|
|
|
@@ -176,7 +177,9 @@ class Config {
|
|
|
176
177
|
checkOptionWithSamplingContextAllowed(this.codeHotspotsEnabled, 'Code hotspots')
|
|
177
178
|
|
|
178
179
|
this.cpuProfilingEnabled = isTrue(coalesce(options.cpuProfilingEnabled,
|
|
179
|
-
|
|
180
|
+
DD_PROFILING_CPU_ENABLED,
|
|
181
|
+
DD_PROFILING_EXPERIMENTAL_CPU_ENABLED, samplingContextsAvailable))
|
|
182
|
+
logExperimentalVarDeprecation('CPU_ENABLED')
|
|
180
183
|
checkOptionWithSamplingContextAllowed(this.cpuProfilingEnabled, 'CPU profiling')
|
|
181
184
|
|
|
182
185
|
this.profilers = ensureProfilers(profilers, this)
|
|
@@ -288,8 +291,9 @@ function ensureProfilers (profilers, options) {
|
|
|
288
291
|
}
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
// Events profiler is a profiler
|
|
292
|
-
if
|
|
294
|
+
// Events profiler is a profiler that produces timeline events. It is only
|
|
295
|
+
// added if timeline is enabled and there's a wall profiler.
|
|
296
|
+
if (options.timelineEnabled && profilers.some(p => p instanceof WallProfiler)) {
|
|
293
297
|
profilers.push(new EventsProfiler(options))
|
|
294
298
|
}
|
|
295
299
|
|
|
@@ -10,6 +10,7 @@ const FormData = require('../../exporters/common/form-data')
|
|
|
10
10
|
const { storage } = require('../../../../datadog-core')
|
|
11
11
|
const version = require('../../../../../package.json').version
|
|
12
12
|
const os = require('os')
|
|
13
|
+
const { urlToHttpOptions } = require('url')
|
|
13
14
|
const perf = require('perf_hooks').performance
|
|
14
15
|
|
|
15
16
|
const containerId = docker.id()
|
|
@@ -177,9 +178,10 @@ class AgentExporter {
|
|
|
177
178
|
if (this._url.protocol === 'unix:') {
|
|
178
179
|
options.socketPath = this._url.pathname
|
|
179
180
|
} else {
|
|
180
|
-
|
|
181
|
-
options.
|
|
182
|
-
options.
|
|
181
|
+
const httpOptions = urlToHttpOptions(this._url)
|
|
182
|
+
options.protocol = httpOptions.protocol
|
|
183
|
+
options.hostname = httpOptions.hostname
|
|
184
|
+
options.port = httpOptions.port
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
this._logger.debug(() => {
|
|
@@ -4,6 +4,9 @@ const { EventEmitter } = require('events')
|
|
|
4
4
|
const { Config } = require('./config')
|
|
5
5
|
const { snapshotKinds } = require('./constants')
|
|
6
6
|
const { threadNamePrefix } = require('./profilers/shared')
|
|
7
|
+
const dc = require('dc-polyfill')
|
|
8
|
+
|
|
9
|
+
const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted')
|
|
7
10
|
|
|
8
11
|
function maybeSourceMap (sourceMap, SourceMapper, debug) {
|
|
9
12
|
if (!sourceMap) return
|
|
@@ -161,6 +164,7 @@ class Profiler extends EventEmitter {
|
|
|
161
164
|
this._capture(this._timeoutInterval, endDate)
|
|
162
165
|
}
|
|
163
166
|
await this._submit(encodedProfiles, startDate, endDate, snapshotKind)
|
|
167
|
+
profileSubmittedChannel.publish()
|
|
164
168
|
this._logger.debug('Submitted profiles')
|
|
165
169
|
} catch (err) {
|
|
166
170
|
this._logger.error(err)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const dc = require('dc-polyfill')
|
|
4
|
+
const coalesce = require('koalas')
|
|
5
|
+
const profileSubmittedChannel = dc.channel('datadog:profiling:mock-profile-submitted')
|
|
6
|
+
const { DD_PROFILING_UPLOAD_PERIOD } = process.env
|
|
7
|
+
|
|
8
|
+
let timerId
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
start: config => {
|
|
12
|
+
// Copied from packages/dd-trace/src/profiler.js
|
|
13
|
+
const flushInterval = coalesce(config.interval, Number(DD_PROFILING_UPLOAD_PERIOD) * 1000, 65 * 1000)
|
|
14
|
+
|
|
15
|
+
function scheduleProfileSubmit () {
|
|
16
|
+
timerId = setTimeout(emitProfileSubmit, flushInterval)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function emitProfileSubmit () {
|
|
20
|
+
profileSubmittedChannel.publish()
|
|
21
|
+
scheduleProfileSubmit()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
scheduleProfileSubmit()
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
stop: () => {
|
|
28
|
+
if (timerId !== undefined) {
|
|
29
|
+
clearTimeout(timerId)
|
|
30
|
+
timerId = undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const telemetryMetrics = require('../telemetry/metrics')
|
|
4
|
+
const profilersNamespace = telemetryMetrics.manager.namespace('profilers')
|
|
5
|
+
const performance = require('perf_hooks').performance
|
|
6
|
+
const dc = require('dc-polyfill')
|
|
7
|
+
const { isTrue, isFalse } = require('../util')
|
|
8
|
+
|
|
9
|
+
// If the process lived for less than 30 seconds, it's considered short-lived
|
|
10
|
+
const DEFAULT_SHORT_LIVED_THRESHOLD = 30000
|
|
11
|
+
|
|
12
|
+
const EnablementChoice = {
|
|
13
|
+
MANUALLY_ENABLED: Symbol('SSITelemetry.EnablementChoice.MANUALLY_ENABLED'),
|
|
14
|
+
SSI_ENABLED: Symbol('SSITelemetry.EnablementChoice.SSI_ENABLED'),
|
|
15
|
+
SSI_NOT_ENABLED: Symbol('SSITelemetry.EnablementChoice.SSI_NOT_ENABLED'),
|
|
16
|
+
DISABLED: Symbol('SSITelemetry.EnablementChoice.MANUALLY_DISABLED')
|
|
17
|
+
}
|
|
18
|
+
Object.freeze(EnablementChoice)
|
|
19
|
+
|
|
20
|
+
function getEnablementChoiceFromEnv () {
|
|
21
|
+
const { DD_PROFILING_ENABLED, DD_INJECTION_ENABLED } = process.env
|
|
22
|
+
if (DD_INJECTION_ENABLED === undefined || isFalse(DD_PROFILING_ENABLED)) {
|
|
23
|
+
return EnablementChoice.DISABLED
|
|
24
|
+
} else if (DD_INJECTION_ENABLED.split(',').includes('profiling')) {
|
|
25
|
+
return EnablementChoice.SSI_ENABLED
|
|
26
|
+
} else if (isTrue(DD_PROFILING_ENABLED)) {
|
|
27
|
+
return EnablementChoice.MANUALLY_ENABLED
|
|
28
|
+
} else {
|
|
29
|
+
return EnablementChoice.SSI_NOT_ENABLED
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function enablementChoiceToTagValue (enablementChoice) {
|
|
34
|
+
switch (enablementChoice) {
|
|
35
|
+
case EnablementChoice.MANUALLY_ENABLED:
|
|
36
|
+
return 'manually_enabled'
|
|
37
|
+
case EnablementChoice.SSI_ENABLED:
|
|
38
|
+
return 'ssi_enabled'
|
|
39
|
+
case EnablementChoice.SSI_NOT_ENABLED:
|
|
40
|
+
return 'not_enabled'
|
|
41
|
+
case EnablementChoice.MANUALLY_DISABLED:
|
|
42
|
+
// Can't emit this one as a tag
|
|
43
|
+
throw new Error('Invalid enablement choice')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* This class emits telemetry metrics about the profiler behavior under SSI. It will only emit metrics
|
|
49
|
+
* when the application closes, and will emit the following metrics:
|
|
50
|
+
* - `number_of_profiles`: The number of profiles that were submitted
|
|
51
|
+
* - `number_of_runtime_id`: The number of runtime IDs in the app (always 1 for Node.js)
|
|
52
|
+
* It will also add tags describing the state of heuristics triggers, the enablement choice, and whether
|
|
53
|
+
* actual profiles were sent (as opposed to mock profiles). There is a mock profiler that is activated
|
|
54
|
+
* when the profiler is not enabled, and it will emit mock profile submission events at the same cadence
|
|
55
|
+
* the profiler would, providing insight into how many profiles would've been emitted if SSI enabled
|
|
56
|
+
* profiling. Note that telemetry is per tracer instance, and each worker thread will have its own instance.
|
|
57
|
+
*/
|
|
58
|
+
class SSITelemetry {
|
|
59
|
+
constructor ({
|
|
60
|
+
enablementChoice = getEnablementChoiceFromEnv(),
|
|
61
|
+
shortLivedThreshold = DEFAULT_SHORT_LIVED_THRESHOLD
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (!Object.values(EnablementChoice).includes(enablementChoice)) {
|
|
64
|
+
throw new Error('Invalid enablement choice')
|
|
65
|
+
}
|
|
66
|
+
if (typeof shortLivedThreshold !== 'number' || shortLivedThreshold <= 0) {
|
|
67
|
+
throw new Error('Short-lived threshold must be a positive number')
|
|
68
|
+
}
|
|
69
|
+
this.enablementChoice = enablementChoice
|
|
70
|
+
this.shortLivedThreshold = shortLivedThreshold
|
|
71
|
+
|
|
72
|
+
this.hasSentProfiles = false
|
|
73
|
+
this.noSpan = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
enabled () {
|
|
77
|
+
return this.enablementChoice !== EnablementChoice.DISABLED
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
start () {
|
|
81
|
+
if (this.enabled()) {
|
|
82
|
+
// Used to determine short-livedness of the process. We could use the process start time as the
|
|
83
|
+
// reference point, but the tracer initialization point is more relevant, as we couldn't be
|
|
84
|
+
// collecting profiles earlier anyway. The difference is not particularly significant if the
|
|
85
|
+
// tracer is initialized early in the process lifetime.
|
|
86
|
+
this.startTime = performance.now()
|
|
87
|
+
|
|
88
|
+
this._onSpanCreated = this._onSpanCreated.bind(this)
|
|
89
|
+
this._onProfileSubmitted = this._onProfileSubmitted.bind(this)
|
|
90
|
+
this._onMockProfileSubmitted = this._onMockProfileSubmitted.bind(this)
|
|
91
|
+
this._onAppClosing = this._onAppClosing.bind(this)
|
|
92
|
+
|
|
93
|
+
dc.subscribe('dd-trace:span:start', this._onSpanCreated)
|
|
94
|
+
dc.subscribe('datadog:profiling:profile-submitted', this._onProfileSubmitted)
|
|
95
|
+
dc.subscribe('datadog:profiling:mock-profile-submitted', this._onMockProfileSubmitted)
|
|
96
|
+
dc.subscribe('datadog:telemetry:app-closing', this._onAppClosing)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_onSpanCreated () {
|
|
101
|
+
this.noSpan = false
|
|
102
|
+
dc.unsubscribe('dd-trace:span:start', this._onSpanCreated)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_onProfileSubmitted () {
|
|
106
|
+
this.hasSentProfiles = true
|
|
107
|
+
this._incProfileCount()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_onMockProfileSubmitted () {
|
|
111
|
+
this._incProfileCount()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_incProfileCount () {
|
|
115
|
+
this._ensureProfileMetrics()
|
|
116
|
+
this._profileCount.inc()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_ensureProfileMetrics () {
|
|
120
|
+
const decision = []
|
|
121
|
+
if (this.noSpan) {
|
|
122
|
+
decision.push('no_span')
|
|
123
|
+
}
|
|
124
|
+
if (performance.now() - this.startTime < this.shortLivedThreshold) {
|
|
125
|
+
decision.push('short_lived')
|
|
126
|
+
}
|
|
127
|
+
if (decision.length === 0) {
|
|
128
|
+
decision.push('triggered')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tags = [
|
|
132
|
+
'installation:ssi',
|
|
133
|
+
`enablement_choice:${enablementChoiceToTagValue(this.enablementChoice)}`,
|
|
134
|
+
`has_sent_profiles:${this.hasSentProfiles}`,
|
|
135
|
+
`heuristic_hypothetical_decision:${decision.join('_')}`
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
this._profileCount = profilersNamespace.count('ssi_heuristic.number_of_profiles', tags)
|
|
139
|
+
this._runtimeIdCount = profilersNamespace.count('ssi_heuristic.number_of_runtime_id', tags)
|
|
140
|
+
|
|
141
|
+
if (!this._emittedRuntimeId && decision[0] === 'triggered') {
|
|
142
|
+
// Tags won't change anymore, so we can emit the runtime ID metric now
|
|
143
|
+
this._emittedRuntimeId = true
|
|
144
|
+
this._runtimeIdCount.inc()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_onAppClosing () {
|
|
149
|
+
this._ensureProfileMetrics()
|
|
150
|
+
// Last ditch effort to emit a runtime ID count metric
|
|
151
|
+
if (!this._emittedRuntimeId) {
|
|
152
|
+
this._emittedRuntimeId = true
|
|
153
|
+
this._runtimeIdCount.inc()
|
|
154
|
+
}
|
|
155
|
+
// So we have the metrics in the final state
|
|
156
|
+
this._profileCount.inc(0)
|
|
157
|
+
|
|
158
|
+
dc.unsubscribe('datadog:profiling:profile-submitted', this._onProfileSubmitted)
|
|
159
|
+
dc.unsubscribe('datadog:profiling:mock-profile-submitted', this._onMockProfileSubmitted)
|
|
160
|
+
dc.unsubscribe('datadog:telemetry:app-closing', this._onAppClosing)
|
|
161
|
+
if (this.noSpan) {
|
|
162
|
+
dc.unsubscribe('dd-trace:span:start', this._onSpanCreated)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { SSITelemetry, EnablementChoice }
|
|
@@ -11,7 +11,9 @@ const PluginManager = require('./plugin_manager')
|
|
|
11
11
|
const remoteConfig = require('./appsec/remote_config')
|
|
12
12
|
const AppsecSdk = require('./appsec/sdk')
|
|
13
13
|
const dogstatsd = require('./dogstatsd')
|
|
14
|
+
const NoopDogStatsDClient = require('./noop/dogstatsd')
|
|
14
15
|
const spanleak = require('./spanleak')
|
|
16
|
+
const { SSITelemetry } = require('./profiling/ssi-telemetry')
|
|
15
17
|
|
|
16
18
|
class Tracer extends NoopProxy {
|
|
17
19
|
constructor () {
|
|
@@ -20,7 +22,7 @@ class Tracer extends NoopProxy {
|
|
|
20
22
|
this._initialized = false
|
|
21
23
|
this._nomenclature = nomenclature
|
|
22
24
|
this._pluginManager = new PluginManager(this)
|
|
23
|
-
this.dogstatsd = new
|
|
25
|
+
this.dogstatsd = new NoopDogStatsDClient()
|
|
24
26
|
this._tracingInitialized = false
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -72,6 +74,8 @@ class Tracer extends NoopProxy {
|
|
|
72
74
|
require('./serverless').maybeStartServerlessMiniAgent(config)
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
const ssiTelemetry = new SSITelemetry()
|
|
78
|
+
ssiTelemetry.start()
|
|
75
79
|
if (config.profiling.enabled) {
|
|
76
80
|
// do not stop tracer initialization if the profiler fails to be imported
|
|
77
81
|
try {
|
|
@@ -80,6 +84,8 @@ class Tracer extends NoopProxy {
|
|
|
80
84
|
} catch (e) {
|
|
81
85
|
log.error(e)
|
|
82
86
|
}
|
|
87
|
+
} else if (ssiTelemetry.enabled()) {
|
|
88
|
+
require('./profiling/ssi-telemetry-mock-profiler').start(config)
|
|
83
89
|
}
|
|
84
90
|
if (!this._profilerStarted) {
|
|
85
91
|
this._profilerStarted = Promise.resolve(false)
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('./log')
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const otelTagMap = {
|
|
6
|
+
'deployment.environment': 'env',
|
|
7
|
+
'service.name': 'service',
|
|
8
|
+
'service.version': 'version'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function add (carrier, keyValuePairs, parseOtelTags = false) {
|
|
6
12
|
if (!carrier || !keyValuePairs) return
|
|
7
13
|
|
|
8
14
|
if (Array.isArray(keyValuePairs)) {
|
|
@@ -13,12 +19,16 @@ function add (carrier, keyValuePairs) {
|
|
|
13
19
|
if (typeof keyValuePairs === 'string') {
|
|
14
20
|
const segments = keyValuePairs.split(',')
|
|
15
21
|
for (const segment of segments) {
|
|
16
|
-
const separatorIndex = segment.indexOf(':')
|
|
22
|
+
const separatorIndex = parseOtelTags ? segment.indexOf('=') : segment.indexOf(':')
|
|
17
23
|
if (separatorIndex === -1) continue
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
let key = segment.slice(0, separatorIndex)
|
|
20
26
|
const value = segment.slice(separatorIndex + 1)
|
|
21
27
|
|
|
28
|
+
if (parseOtelTags && key in otelTagMap) {
|
|
29
|
+
key = otelTagMap[key]
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
carrier[key.trim()] = value.trim()
|
|
23
33
|
}
|
|
24
34
|
} else {
|
|
@@ -10,6 +10,7 @@ const logs = require('./logs')
|
|
|
10
10
|
|
|
11
11
|
const telemetryStartChannel = dc.channel('datadog:telemetry:start')
|
|
12
12
|
const telemetryStopChannel = dc.channel('datadog:telemetry:stop')
|
|
13
|
+
const telemetryAppClosingChannel = dc.channel('datadog:telemetry:app-closing')
|
|
13
14
|
|
|
14
15
|
let config
|
|
15
16
|
let pluginManager
|
|
@@ -129,12 +130,12 @@ function appClosing () {
|
|
|
129
130
|
if (!config?.telemetry?.enabled) {
|
|
130
131
|
return
|
|
131
132
|
}
|
|
133
|
+
// Give chance to listeners to update metrics before shutting down.
|
|
134
|
+
telemetryAppClosingChannel.publish()
|
|
132
135
|
const { reqType, payload } = createPayload('app-closing')
|
|
133
136
|
sendData(config, application, host, reqType, payload)
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
metricsManager.send(config, application, host)
|
|
137
|
-
}
|
|
137
|
+
// We flush before shutting down.
|
|
138
|
+
metricsManager.send(config, application, host)
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
function onBeforeExit () {
|