dd-trace 4.52.0 → 4.53.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/LICENSE-3rdparty.csv +8 -2
- package/ci/init.js +16 -0
- package/index.d.ts +31 -13
- package/init.js +4 -68
- package/loader-hook.mjs +4 -0
- package/package.json +16 -11
- package/packages/datadog-core/src/storage.js +39 -2
- package/packages/datadog-instrumentations/src/aerospike.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber.js +29 -3
- package/packages/datadog-instrumentations/src/express.js +38 -4
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +3 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +3 -4
- package/packages/datadog-instrumentations/src/http/client.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +27 -8
- package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
- package/packages/datadog-instrumentations/src/mysql2.js +13 -8
- package/packages/datadog-instrumentations/src/next.js +7 -4
- package/packages/datadog-instrumentations/src/passport-http.js +2 -14
- package/packages/datadog-instrumentations/src/passport-local.js +2 -14
- package/packages/datadog-instrumentations/src/passport-utils.js +43 -19
- package/packages/datadog-instrumentations/src/pg.js +6 -6
- package/packages/datadog-instrumentations/src/playwright.js +17 -4
- package/packages/datadog-instrumentations/src/router.js +97 -1
- package/packages/datadog-instrumentations/src/sequelize.js +9 -4
- package/packages/datadog-instrumentations/src/url.js +4 -0
- package/packages/datadog-instrumentations/src/vitest.js +27 -2
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +8 -3
- package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +154 -0
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +92 -0
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -1
- package/packages/datadog-plugin-cucumber/src/index.js +39 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +3 -3
- package/packages/datadog-plugin-grpc/src/client.js +2 -2
- package/packages/datadog-plugin-grpc/src/util.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +39 -4
- package/packages/datadog-plugin-mocha/src/index.js +36 -2
- package/packages/datadog-plugin-oracledb/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +34 -2
- package/packages/datadog-shimmer/src/shimmer.js +8 -4
- package/packages/dd-trace/src/appsec/addresses.js +3 -0
- package/packages/dd-trace/src/appsec/blocked_templates.js +1 -1
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +4 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js +10 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +4 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +4 -0
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +6 -19
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +3 -3
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +64 -3
- package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +2 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +32 -37
- package/packages/dd-trace/src/appsec/index.js +16 -10
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +1 -0
- package/packages/dd-trace/src/appsec/remote_config/index.js +25 -1
- package/packages/dd-trace/src/appsec/reporter.js +3 -1
- package/packages/dd-trace/src/appsec/sdk/track_event.js +32 -19
- package/packages/dd-trace/src/appsec/telemetry.js +10 -0
- package/packages/dd-trace/src/appsec/user_tracking.js +168 -0
- package/packages/dd-trace/src/azure_metadata.js +4 -4
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +5 -4
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +39 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +29 -9
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/config.js +24 -32
- package/packages/dd-trace/src/constants.js +1 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +3 -2
- package/packages/dd-trace/src/datastreams/processor.js +4 -6
- package/packages/dd-trace/src/datastreams/writer.js +6 -5
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +80 -0
- package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -1
- package/packages/dd-trace/src/debugger/devtools_client/defaults.js +6 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +63 -8
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +10 -67
- package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/status.js +4 -4
- package/packages/dd-trace/src/debugger/index.js +14 -10
- package/packages/dd-trace/src/dogstatsd.js +2 -2
- package/packages/dd-trace/src/encode/0.4.js +23 -78
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +0 -32
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +1 -2
- package/packages/dd-trace/src/encode/span-stats.js +0 -30
- package/packages/dd-trace/src/exporters/agent/writer.js +3 -3
- package/packages/dd-trace/src/exporters/common/request.js +1 -1
- package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -1
- package/packages/dd-trace/src/flare/index.js +1 -1
- package/packages/dd-trace/src/guardrails/index.js +64 -0
- package/packages/dd-trace/src/guardrails/log.js +32 -0
- package/packages/dd-trace/src/guardrails/telemetry.js +78 -0
- package/packages/dd-trace/src/guardrails/util.js +10 -0
- package/packages/dd-trace/src/lambda/runtime/ritm.js +2 -2
- package/packages/dd-trace/src/llmobs/storage.js +2 -3
- package/packages/dd-trace/src/llmobs/writers/base.js +2 -2
- package/packages/dd-trace/src/{encode → msgpack}/chunk.js +8 -5
- package/packages/dd-trace/src/msgpack/encoder.js +309 -0
- package/packages/dd-trace/src/msgpack/index.js +6 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +2 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -9
- package/packages/dd-trace/src/opentracing/span.js +1 -1
- package/packages/dd-trace/src/opentracing/tracer.js +2 -2
- package/packages/dd-trace/src/plugin_manager.js +4 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +47 -4
- package/packages/dd-trace/src/plugins/plugin.js +1 -1
- package/packages/dd-trace/src/plugins/tracing.js +1 -1
- package/packages/dd-trace/src/plugins/util/git.js +7 -7
- package/packages/dd-trace/src/plugins/util/test.js +36 -3
- package/packages/dd-trace/src/plugins/util/web.js +2 -2
- package/packages/dd-trace/src/profiling/config.js +3 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +9 -68
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +76 -0
- package/packages/dd-trace/src/profiling/exporters/file.js +8 -4
- package/packages/dd-trace/src/profiling/profiler.js +62 -10
- package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +22 -12
- package/packages/dd-trace/src/profiling/profilers/events.js +47 -8
- package/packages/dd-trace/src/profiling/profilers/wall.js +2 -17
- package/packages/dd-trace/src/profiling/webspan-utils.js +23 -0
- package/packages/dd-trace/src/proxy.js +7 -2
- package/packages/dd-trace/src/runtime_metrics.js +107 -4
- package/packages/dd-trace/src/serverless.js +1 -1
- package/packages/dd-trace/src/span_processor.js +10 -10
- package/packages/dd-trace/src/tagger.js +1 -1
- package/packages/dd-trace/src/telemetry/index.js +1 -0
- package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
- package/packages/dd-trace/src/telemetry/logs/log-collector.js +10 -2
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +5 -16
- package/packages/datadog-instrumentations/src/qs.js +0 -24
- package/packages/dd-trace/src/appsec/passport.js +0 -110
- package/packages/dd-trace/src/telemetry/init-telemetry.js +0 -75
|
@@ -106,6 +106,13 @@ const TEST_LEVEL_EVENT_TYPES = [
|
|
|
106
106
|
'test_session_end'
|
|
107
107
|
]
|
|
108
108
|
|
|
109
|
+
// Dynamic instrumentation - Test optimization integration tags
|
|
110
|
+
const DI_ERROR_DEBUG_INFO_CAPTURED = 'error.debug_info_captured'
|
|
111
|
+
// TODO: for the moment we'll only use a single snapshot id, so `0` is hardcoded
|
|
112
|
+
const DI_DEBUG_ERROR_SNAPSHOT_ID = '_dd.debug.error.0.snapshot_id'
|
|
113
|
+
const DI_DEBUG_ERROR_FILE = '_dd.debug.error.0.file'
|
|
114
|
+
const DI_DEBUG_ERROR_LINE = '_dd.debug.error.0.line'
|
|
115
|
+
|
|
109
116
|
module.exports = {
|
|
110
117
|
TEST_CODE_OWNERS,
|
|
111
118
|
TEST_SESSION_NAME,
|
|
@@ -181,7 +188,12 @@ module.exports = {
|
|
|
181
188
|
TEST_BROWSER_VERSION,
|
|
182
189
|
getTestSessionName,
|
|
183
190
|
TEST_LEVEL_EVENT_TYPES,
|
|
184
|
-
getNumFromKnownTests
|
|
191
|
+
getNumFromKnownTests,
|
|
192
|
+
getFileAndLineNumberFromError,
|
|
193
|
+
DI_ERROR_DEBUG_INFO_CAPTURED,
|
|
194
|
+
DI_DEBUG_ERROR_SNAPSHOT_ID,
|
|
195
|
+
DI_DEBUG_ERROR_FILE,
|
|
196
|
+
DI_DEBUG_ERROR_LINE
|
|
185
197
|
}
|
|
186
198
|
|
|
187
199
|
// Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
|
|
@@ -206,13 +218,13 @@ function removeInvalidMetadata (metadata) {
|
|
|
206
218
|
return Object.keys(metadata).reduce((filteredTags, tag) => {
|
|
207
219
|
if (tag === GIT_REPOSITORY_URL) {
|
|
208
220
|
if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
|
|
209
|
-
log.error(
|
|
221
|
+
log.error('Repository URL is not a valid repository URL: %s.', metadata[GIT_REPOSITORY_URL])
|
|
210
222
|
return filteredTags
|
|
211
223
|
}
|
|
212
224
|
}
|
|
213
225
|
if (tag === GIT_COMMIT_SHA) {
|
|
214
226
|
if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
|
|
215
|
-
log.error(
|
|
227
|
+
log.error('Git commit SHA must be a full-length git SHA: %s.', metadata[GIT_COMMIT_SHA])
|
|
216
228
|
return filteredTags
|
|
217
229
|
}
|
|
218
230
|
}
|
|
@@ -637,3 +649,24 @@ function getNumFromKnownTests (knownTests) {
|
|
|
637
649
|
|
|
638
650
|
return totalNumTests
|
|
639
651
|
}
|
|
652
|
+
|
|
653
|
+
function getFileAndLineNumberFromError (error) {
|
|
654
|
+
// Split the stack trace into individual lines
|
|
655
|
+
const stackLines = error.stack.split('\n')
|
|
656
|
+
|
|
657
|
+
// The top frame is usually the second line
|
|
658
|
+
const topFrame = stackLines[1]
|
|
659
|
+
|
|
660
|
+
// Regular expression to match the file path, line number, and column number
|
|
661
|
+
const regex = /\s*at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/
|
|
662
|
+
const match = topFrame.match(regex)
|
|
663
|
+
|
|
664
|
+
if (match) {
|
|
665
|
+
const filePath = match[1]
|
|
666
|
+
const lineNumber = Number(match[2])
|
|
667
|
+
const columnNumber = Number(match[3])
|
|
668
|
+
|
|
669
|
+
return [filePath, lineNumber, columnNumber]
|
|
670
|
+
}
|
|
671
|
+
return []
|
|
672
|
+
}
|
|
@@ -546,7 +546,7 @@ function getHeadersToRecord (config) {
|
|
|
546
546
|
.map(h => h.split(':'))
|
|
547
547
|
.map(([key, tag]) => [key.toLowerCase(), tag])
|
|
548
548
|
} catch (err) {
|
|
549
|
-
log.error(err)
|
|
549
|
+
log.error('Web plugin error getting headers', err)
|
|
550
550
|
}
|
|
551
551
|
} else if (config.hasOwnProperty('headers')) {
|
|
552
552
|
log.error('Expected `headers` to be an array of strings.')
|
|
@@ -595,7 +595,7 @@ function getQsObfuscator (config) {
|
|
|
595
595
|
try {
|
|
596
596
|
return new RegExp(obfuscator, 'gi')
|
|
597
597
|
} catch (err) {
|
|
598
|
-
log.error(err)
|
|
598
|
+
log.error('Web plugin error getting qs obfuscator', err)
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
|
|
@@ -21,6 +21,7 @@ class Config {
|
|
|
21
21
|
const {
|
|
22
22
|
DD_AGENT_HOST,
|
|
23
23
|
DD_ENV,
|
|
24
|
+
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
|
|
24
25
|
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
25
26
|
DD_PROFILING_CPU_ENABLED,
|
|
26
27
|
DD_PROFILING_DEBUG_SOURCE_MAPS,
|
|
@@ -175,6 +176,8 @@ class Config {
|
|
|
175
176
|
DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, samplingContextsAvailable))
|
|
176
177
|
logExperimentalVarDeprecation('TIMELINE_ENABLED')
|
|
177
178
|
checkOptionWithSamplingContextAllowed(this.timelineEnabled, 'Timeline view')
|
|
179
|
+
this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled,
|
|
180
|
+
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true))
|
|
178
181
|
|
|
179
182
|
this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
|
|
180
183
|
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
const retry = require('retry')
|
|
4
4
|
const { request: httpRequest } = require('http')
|
|
5
5
|
const { request: httpsRequest } = require('https')
|
|
6
|
+
const { EventSerializer } = require('./event_serializer')
|
|
6
7
|
|
|
7
8
|
// TODO: avoid using dd-trace internals. Make this a separate module?
|
|
8
9
|
const docker = require('../../exporters/common/docker')
|
|
9
10
|
const FormData = require('../../exporters/common/form-data')
|
|
10
11
|
const { storage } = require('../../../../datadog-core')
|
|
11
12
|
const version = require('../../../../../package.json').version
|
|
12
|
-
const os = require('os')
|
|
13
13
|
const { urlToHttpOptions } = require('url')
|
|
14
14
|
const perf = require('perf_hooks').performance
|
|
15
15
|
|
|
@@ -89,8 +89,10 @@ function computeRetries (uploadTimeout) {
|
|
|
89
89
|
return [tries, Math.floor(uploadTimeout)]
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
class AgentExporter {
|
|
93
|
-
constructor (
|
|
92
|
+
class AgentExporter extends EventSerializer {
|
|
93
|
+
constructor (config = {}) {
|
|
94
|
+
super(config)
|
|
95
|
+
const { url, logger, uploadTimeout } = config
|
|
94
96
|
this._url = url
|
|
95
97
|
this._logger = logger
|
|
96
98
|
|
|
@@ -98,74 +100,13 @@ class AgentExporter {
|
|
|
98
100
|
|
|
99
101
|
this._backoffTime = backoffTime
|
|
100
102
|
this._backoffTries = backoffTries
|
|
101
|
-
this._env = env
|
|
102
|
-
this._host = host
|
|
103
|
-
this._service = service
|
|
104
|
-
this._appVersion = version
|
|
105
|
-
this._libraryInjected = !!libraryInjected
|
|
106
|
-
this._activation = activation || 'unknown'
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
export (
|
|
105
|
+
export (exportSpec) {
|
|
106
|
+
const { profiles } = exportSpec
|
|
110
107
|
const fields = []
|
|
111
108
|
|
|
112
|
-
|
|
113
|
-
return `${type}.pprof`
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const event = JSON.stringify({
|
|
117
|
-
attachments: Object.keys(profiles).map(typeToFile),
|
|
118
|
-
start: start.toISOString(),
|
|
119
|
-
end: end.toISOString(),
|
|
120
|
-
family: 'node',
|
|
121
|
-
version: '4',
|
|
122
|
-
tags_profiler: [
|
|
123
|
-
'language:javascript',
|
|
124
|
-
'runtime:nodejs',
|
|
125
|
-
`runtime_arch:${process.arch}`,
|
|
126
|
-
`runtime_os:${process.platform}`,
|
|
127
|
-
`runtime_version:${process.version}`,
|
|
128
|
-
`process_id:${process.pid}`,
|
|
129
|
-
`profiler_version:${version}`,
|
|
130
|
-
'format:pprof',
|
|
131
|
-
...Object.entries(tags).map(([key, value]) => `${key}:${value}`)
|
|
132
|
-
].join(','),
|
|
133
|
-
info: {
|
|
134
|
-
application: {
|
|
135
|
-
env: this._env,
|
|
136
|
-
service: this._service,
|
|
137
|
-
start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(),
|
|
138
|
-
version: this._appVersion
|
|
139
|
-
},
|
|
140
|
-
platform: {
|
|
141
|
-
hostname: this._host,
|
|
142
|
-
kernel_name: os.type(),
|
|
143
|
-
kernel_release: os.release(),
|
|
144
|
-
kernel_version: os.version()
|
|
145
|
-
},
|
|
146
|
-
profiler: {
|
|
147
|
-
activation: this._activation,
|
|
148
|
-
ssi: {
|
|
149
|
-
mechanism: this._libraryInjected ? 'injected_agent' : 'none'
|
|
150
|
-
},
|
|
151
|
-
version
|
|
152
|
-
},
|
|
153
|
-
runtime: {
|
|
154
|
-
// Using `nodejs` for consistency with the existing `runtime` tag.
|
|
155
|
-
// Note that the event `family` property uses `node`, as that's what's
|
|
156
|
-
// proscribed by the Intake API, but that's an internal enum and is
|
|
157
|
-
// not customer visible.
|
|
158
|
-
engine: 'nodejs',
|
|
159
|
-
// strip off leading 'v'. This makes the format consistent with other
|
|
160
|
-
// runtimes (e.g. Ruby) but not with the existing `runtime_version` tag.
|
|
161
|
-
// We'll keep it like this as we want cross-engine consistency. We
|
|
162
|
-
// also aren't changing the format of the existing tag as we don't want
|
|
163
|
-
// to break it.
|
|
164
|
-
version: process.version.substring(1)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
|
|
109
|
+
const event = this.getEventJSON(exportSpec)
|
|
169
110
|
fields.push(['event', event, {
|
|
170
111
|
filename: 'event.json',
|
|
171
112
|
contentType: 'application/json'
|
|
@@ -181,7 +122,7 @@ class AgentExporter {
|
|
|
181
122
|
return `Adding ${type} profile to agent export: ` + bytes
|
|
182
123
|
})
|
|
183
124
|
|
|
184
|
-
const filename = typeToFile(type)
|
|
125
|
+
const filename = this.typeToFile(type)
|
|
185
126
|
fields.push([filename, buffer, {
|
|
186
127
|
filename,
|
|
187
128
|
contentType: 'application/octet-stream'
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const perf = require('perf_hooks').performance
|
|
3
|
+
const version = require('../../../../../package.json').version
|
|
4
|
+
|
|
5
|
+
class EventSerializer {
|
|
6
|
+
constructor ({ env, host, service, version, libraryInjected, activation } = {}) {
|
|
7
|
+
this._env = env
|
|
8
|
+
this._host = host
|
|
9
|
+
this._service = service
|
|
10
|
+
this._appVersion = version
|
|
11
|
+
this._libraryInjected = !!libraryInjected
|
|
12
|
+
this._activation = activation || 'unknown'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
typeToFile (type) {
|
|
16
|
+
return `${type}.pprof`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getEventJSON ({ profiles, start, end, tags = {}, endpointCounts }) {
|
|
20
|
+
return JSON.stringify({
|
|
21
|
+
attachments: Object.keys(profiles).map(t => this.typeToFile(t)),
|
|
22
|
+
start: start.toISOString(),
|
|
23
|
+
end: end.toISOString(),
|
|
24
|
+
family: 'node',
|
|
25
|
+
version: '4',
|
|
26
|
+
tags_profiler: [
|
|
27
|
+
'language:javascript',
|
|
28
|
+
'runtime:nodejs',
|
|
29
|
+
`runtime_arch:${process.arch}`,
|
|
30
|
+
`runtime_os:${process.platform}`,
|
|
31
|
+
`runtime_version:${process.version}`,
|
|
32
|
+
`process_id:${process.pid}`,
|
|
33
|
+
`profiler_version:${version}`,
|
|
34
|
+
'format:pprof',
|
|
35
|
+
...Object.entries(tags).map(([key, value]) => `${key}:${value}`)
|
|
36
|
+
].join(','),
|
|
37
|
+
endpoint_counts: endpointCounts,
|
|
38
|
+
info: {
|
|
39
|
+
application: {
|
|
40
|
+
env: this._env,
|
|
41
|
+
service: this._service,
|
|
42
|
+
start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(),
|
|
43
|
+
version: this._appVersion
|
|
44
|
+
},
|
|
45
|
+
platform: {
|
|
46
|
+
hostname: this._host,
|
|
47
|
+
kernel_name: os.type(),
|
|
48
|
+
kernel_release: os.release(),
|
|
49
|
+
kernel_version: os.version()
|
|
50
|
+
},
|
|
51
|
+
profiler: {
|
|
52
|
+
activation: this._activation,
|
|
53
|
+
ssi: {
|
|
54
|
+
mechanism: this._libraryInjected ? 'injected_agent' : 'none'
|
|
55
|
+
},
|
|
56
|
+
version
|
|
57
|
+
},
|
|
58
|
+
runtime: {
|
|
59
|
+
// Using `nodejs` for consistency with the existing `runtime` tag.
|
|
60
|
+
// Note that the event `family` property uses `node`, as that's what's
|
|
61
|
+
// proscribed by the Intake API, but that's an internal enum and is
|
|
62
|
+
// not customer visible.
|
|
63
|
+
engine: 'nodejs',
|
|
64
|
+
// strip off leading 'v'. This makes the format consistent with other
|
|
65
|
+
// runtimes (e.g. Ruby) but not with the existing `runtime_version` tag.
|
|
66
|
+
// We'll keep it like this as we want cross-engine consistency. We
|
|
67
|
+
// also aren't changing the format of the existing tag as we don't want
|
|
68
|
+
// to break it.
|
|
69
|
+
version: process.version.substring(1)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { EventSerializer }
|
|
@@ -4,6 +4,7 @@ const fs = require('fs')
|
|
|
4
4
|
const { promisify } = require('util')
|
|
5
5
|
const { threadId } = require('worker_threads')
|
|
6
6
|
const writeFile = promisify(fs.writeFile)
|
|
7
|
+
const { EventSerializer } = require('./event_serializer')
|
|
7
8
|
|
|
8
9
|
function formatDateTime (t) {
|
|
9
10
|
const pad = (n) => String(n).padStart(2, '0')
|
|
@@ -11,18 +12,21 @@ function formatDateTime (t) {
|
|
|
11
12
|
`T${pad(t.getUTCHours())}${pad(t.getUTCMinutes())}${pad(t.getUTCSeconds())}Z`
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
class FileExporter {
|
|
15
|
-
constructor (
|
|
15
|
+
class FileExporter extends EventSerializer {
|
|
16
|
+
constructor (config = {}) {
|
|
17
|
+
super(config)
|
|
18
|
+
const { pprofPrefix } = config
|
|
16
19
|
this._pprofPrefix = pprofPrefix || ''
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
export (
|
|
22
|
+
export (exportSpec) {
|
|
23
|
+
const { profiles, end } = exportSpec
|
|
20
24
|
const types = Object.keys(profiles)
|
|
21
25
|
const dateStr = formatDateTime(end)
|
|
22
26
|
const tasks = types.map(type => {
|
|
23
27
|
return writeFile(`${this._pprofPrefix}${type}_worker_${threadId}_${dateStr}.pprof`, profiles[type])
|
|
24
28
|
})
|
|
25
|
-
|
|
29
|
+
tasks.push(writeFile(`event_worker_${threadId}_${dateStr}.json`, this.getEventJSON(exportSpec)))
|
|
26
30
|
return Promise.all(tasks)
|
|
27
31
|
}
|
|
28
32
|
}
|
|
@@ -4,9 +4,11 @@ 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 { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('./webspan-utils')
|
|
7
8
|
const dc = require('dc-polyfill')
|
|
8
9
|
|
|
9
10
|
const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted')
|
|
11
|
+
const spanFinishedChannel = dc.channel('dd-trace:span:finish')
|
|
10
12
|
|
|
11
13
|
function maybeSourceMap (sourceMap, SourceMapper, debug) {
|
|
12
14
|
if (!sourceMap) return
|
|
@@ -21,6 +23,20 @@ function logError (logger, err) {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function findWebSpan (startedSpans, spanId) {
|
|
27
|
+
for (let i = startedSpans.length; --i >= 0;) {
|
|
28
|
+
const ispan = startedSpans[i]
|
|
29
|
+
const context = ispan.context()
|
|
30
|
+
if (context._spanId === spanId) {
|
|
31
|
+
if (isWebServerSpan(context._tags)) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
spanId = context._parentId
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
class Profiler extends EventEmitter {
|
|
25
41
|
constructor () {
|
|
26
42
|
super()
|
|
@@ -30,6 +46,7 @@ class Profiler extends EventEmitter {
|
|
|
30
46
|
this._timer = undefined
|
|
31
47
|
this._lastStart = undefined
|
|
32
48
|
this._timeoutInterval = undefined
|
|
49
|
+
this.endpointCounts = new Map()
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
start (options) {
|
|
@@ -82,6 +99,11 @@ class Profiler extends EventEmitter {
|
|
|
82
99
|
this._logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`)
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
if (config.endpointCollectionEnabled) {
|
|
103
|
+
this._spanFinishListener = this._onSpanFinish.bind(this)
|
|
104
|
+
spanFinishedChannel.subscribe(this._spanFinishListener)
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
this._capture(this._timeoutInterval, start)
|
|
86
108
|
return true
|
|
87
109
|
} catch (e) {
|
|
@@ -117,6 +139,11 @@ class Profiler extends EventEmitter {
|
|
|
117
139
|
|
|
118
140
|
this._enabled = false
|
|
119
141
|
|
|
142
|
+
if (this._spanFinishListener !== undefined) {
|
|
143
|
+
spanFinishedChannel.unsubscribe(this._spanFinishListener)
|
|
144
|
+
this._spanFinishListener = undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
for (const profiler of this._config.profilers) {
|
|
121
148
|
profiler.stop()
|
|
122
149
|
this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
|
|
@@ -137,6 +164,26 @@ class Profiler extends EventEmitter {
|
|
|
137
164
|
}
|
|
138
165
|
}
|
|
139
166
|
|
|
167
|
+
_onSpanFinish (span) {
|
|
168
|
+
const context = span.context()
|
|
169
|
+
const tags = context._tags
|
|
170
|
+
if (!isWebServerSpan(tags)) return
|
|
171
|
+
|
|
172
|
+
const endpointName = endpointNameFromTags(tags)
|
|
173
|
+
if (!endpointName) return
|
|
174
|
+
|
|
175
|
+
// Make sure this is the outermost web span, just in case so we don't overcount
|
|
176
|
+
if (findWebSpan(getStartedSpans(context), context._parentId)) return
|
|
177
|
+
|
|
178
|
+
let counter = this.endpointCounts.get(endpointName)
|
|
179
|
+
if (counter === undefined) {
|
|
180
|
+
counter = { count: 1 }
|
|
181
|
+
this.endpointCounts.set(endpointName, counter)
|
|
182
|
+
} else {
|
|
183
|
+
counter.count++
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
140
187
|
async _collect (snapshotKind, restart = true) {
|
|
141
188
|
if (!this._enabled) return
|
|
142
189
|
|
|
@@ -194,18 +241,23 @@ class Profiler extends EventEmitter {
|
|
|
194
241
|
|
|
195
242
|
_submit (profiles, start, end, snapshotKind) {
|
|
196
243
|
const { tags } = this._config
|
|
197
|
-
const tasks = []
|
|
198
244
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (this._logger) {
|
|
204
|
-
this._logger.warn(err)
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
tasks.push(task)
|
|
245
|
+
// Flatten endpoint counts
|
|
246
|
+
const endpointCounts = {}
|
|
247
|
+
for (const [endpoint, { count }] of this.endpointCounts) {
|
|
248
|
+
endpointCounts[endpoint] = count
|
|
208
249
|
}
|
|
250
|
+
this.endpointCounts.clear()
|
|
251
|
+
|
|
252
|
+
tags.snapshot = snapshotKind
|
|
253
|
+
const exportSpec = { profiles, start, end, tags, endpointCounts }
|
|
254
|
+
const tasks = this._config.exporters.map(exporter =>
|
|
255
|
+
exporter.export(exportSpec).catch(err => {
|
|
256
|
+
if (this._logger) {
|
|
257
|
+
this._logger.warn(err)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
)
|
|
209
261
|
|
|
210
262
|
return Promise.all(tasks)
|
|
211
263
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { storage } = require('../../../../../datadog-core')
|
|
2
2
|
const TracingPlugin = require('../../../plugins/tracing')
|
|
3
3
|
const { performance } = require('perf_hooks')
|
|
4
4
|
|
|
5
5
|
// We are leveraging the TracingPlugin class for its functionality to bind
|
|
6
6
|
// start/error/finish methods to the appropriate diagnostic channels.
|
|
7
7
|
class EventPlugin extends TracingPlugin {
|
|
8
|
-
constructor (eventHandler) {
|
|
8
|
+
constructor (eventHandler, eventFilter) {
|
|
9
9
|
super()
|
|
10
10
|
this.eventHandler = eventHandler
|
|
11
|
-
this.
|
|
11
|
+
this.eventFilter = eventFilter
|
|
12
|
+
this.store = storage('profiling')
|
|
12
13
|
this.entryType = this.constructor.entryType
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -20,27 +21,36 @@ class EventPlugin extends TracingPlugin {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
error () {
|
|
23
|
-
this.store.getStore()
|
|
24
|
+
const store = this.store.getStore()
|
|
25
|
+
if (store) {
|
|
26
|
+
store.error = true
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
finish () {
|
|
27
|
-
const
|
|
31
|
+
const store = this.store.getStore()
|
|
32
|
+
if (!store) return
|
|
33
|
+
|
|
34
|
+
const { startEvent, startTime, error } = store
|
|
28
35
|
if (error) {
|
|
29
36
|
return // don't emit perf events for failed operations
|
|
30
37
|
}
|
|
31
38
|
const duration = performance.now() - startTime
|
|
32
39
|
|
|
33
|
-
const context = this.activeSpan?.context()
|
|
34
|
-
const _ddSpanId = context?.toSpanId()
|
|
35
|
-
const _ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || _ddSpanId
|
|
36
|
-
|
|
37
40
|
const event = {
|
|
38
41
|
entryType: this.entryType,
|
|
39
42
|
startTime,
|
|
40
|
-
duration
|
|
41
|
-
_ddSpanId,
|
|
42
|
-
_ddRootSpanId
|
|
43
|
+
duration
|
|
43
44
|
}
|
|
45
|
+
|
|
46
|
+
if (!this.eventFilter(event)) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const context = this.activeSpan?.context()
|
|
51
|
+
event._ddSpanId = context?.toSpanId()
|
|
52
|
+
event._ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || event._ddSpanId
|
|
53
|
+
|
|
44
54
|
this.eventHandler(this.extendEvent(event, startEvent))
|
|
45
55
|
}
|
|
46
56
|
}
|
|
@@ -254,10 +254,10 @@ class NodeApiEventSource {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
class DatadogInstrumentationEventSource {
|
|
257
|
-
constructor (eventHandler) {
|
|
257
|
+
constructor (eventHandler, eventFilter) {
|
|
258
258
|
this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => {
|
|
259
259
|
const Plugin = require(`./event_plugins/${m}`)
|
|
260
|
-
return new Plugin(eventHandler)
|
|
260
|
+
return new Plugin(eventHandler, eventFilter)
|
|
261
261
|
})
|
|
262
262
|
|
|
263
263
|
this.started = false
|
|
@@ -292,29 +292,68 @@ class CompositeEventSource {
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
function createPossionProcessSamplingFilter (samplingIntervalMillis) {
|
|
296
|
+
let nextSamplingInstant = performance.now()
|
|
297
|
+
let currentSamplingInstant = 0
|
|
298
|
+
setNextSamplingInstant()
|
|
299
|
+
|
|
300
|
+
return event => {
|
|
301
|
+
const endTime = event.startTime + event.duration
|
|
302
|
+
while (endTime >= nextSamplingInstant) {
|
|
303
|
+
setNextSamplingInstant()
|
|
304
|
+
}
|
|
305
|
+
// An event is sampled if it started before, and ended on or after a sampling instant. The above
|
|
306
|
+
// while loop will ensure that the ending invariant is always true for the current sampling
|
|
307
|
+
// instant so we don't have to test for it below. Across calls, the invariant also holds as long
|
|
308
|
+
// as the events arrive in endTime order. This is true for events coming from
|
|
309
|
+
// DatadogInstrumentationEventSource; they will be ordered by endTime by virtue of this method
|
|
310
|
+
// being invoked synchronously with the plugins' finish() handler which evaluates
|
|
311
|
+
// performance.now(). OTOH, events coming from NodeAPIEventSource (GC in typical setup) might be
|
|
312
|
+
// somewhat delayed as they are queued by Node, so they can arrive out of order with regard to
|
|
313
|
+
// events coming from the non-queued source. By omitting the endTime check, we will pass through
|
|
314
|
+
// some short events that started and ended before the current sampling instant. OTOH, if we
|
|
315
|
+
// were to check for this.currentSamplingInstant <= endTime, we would discard some long events
|
|
316
|
+
// that also ended before the current sampling instant. We'd rather err on the side of including
|
|
317
|
+
// some short events than excluding some long events.
|
|
318
|
+
return event.startTime < currentSamplingInstant
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function setNextSamplingInstant () {
|
|
322
|
+
currentSamplingInstant = nextSamplingInstant
|
|
323
|
+
nextSamplingInstant -= Math.log(1 - Math.random()) * samplingIntervalMillis
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
/**
|
|
296
328
|
* This class generates pprof files with timeline events. It combines an event
|
|
297
|
-
* source with an event serializer.
|
|
329
|
+
* source with a sampling event filter and an event serializer.
|
|
298
330
|
*/
|
|
299
331
|
class EventsProfiler {
|
|
300
332
|
constructor (options = {}) {
|
|
301
333
|
this.type = 'events'
|
|
302
334
|
this.eventSerializer = new EventSerializer()
|
|
303
335
|
|
|
304
|
-
const eventHandler = event =>
|
|
305
|
-
|
|
336
|
+
const eventHandler = event => this.eventSerializer.addEvent(event)
|
|
337
|
+
const eventFilter = options.timelineSamplingEnabled
|
|
338
|
+
// options.samplingInterval comes in microseconds, we need millis
|
|
339
|
+
? createPossionProcessSamplingFilter((options.samplingInterval ?? 1e6 / 99) / 1000)
|
|
340
|
+
: _ => true
|
|
341
|
+
const filteringEventHandler = event => {
|
|
342
|
+
if (eventFilter(event)) {
|
|
343
|
+
eventHandler(event)
|
|
344
|
+
}
|
|
306
345
|
}
|
|
307
346
|
|
|
308
347
|
if (options.codeHotspotsEnabled) {
|
|
309
348
|
// Use Datadog instrumentation to collect events with span IDs. Still use
|
|
310
349
|
// Node API for GC events.
|
|
311
350
|
this.eventSource = new CompositeEventSource([
|
|
312
|
-
new DatadogInstrumentationEventSource(eventHandler),
|
|
313
|
-
new NodeApiEventSource(
|
|
351
|
+
new DatadogInstrumentationEventSource(eventHandler, eventFilter),
|
|
352
|
+
new NodeApiEventSource(filteringEventHandler, ['gc'])
|
|
314
353
|
])
|
|
315
354
|
} else {
|
|
316
355
|
// Use Node API instrumentation to collect events without span IDs
|
|
317
|
-
this.eventSource = new NodeApiEventSource(
|
|
356
|
+
this.eventSource = new NodeApiEventSource(filteringEventHandler)
|
|
318
357
|
}
|
|
319
358
|
}
|
|
320
359
|
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
const { storage } = require('../../../../datadog-core')
|
|
4
4
|
|
|
5
5
|
const dc = require('dc-polyfill')
|
|
6
|
-
const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags')
|
|
7
|
-
const { WEB } = require('../../../../../ext/types')
|
|
8
6
|
const runtimeMetrics = require('../../runtime_metrics')
|
|
9
7
|
const telemetryMetrics = require('../../telemetry/metrics')
|
|
10
8
|
const {
|
|
@@ -15,6 +13,8 @@ const {
|
|
|
15
13
|
getThreadLabels
|
|
16
14
|
} = require('./shared')
|
|
17
15
|
|
|
16
|
+
const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
|
|
17
|
+
|
|
18
18
|
const beforeCh = dc.channel('dd-trace:storage:before')
|
|
19
19
|
const enterCh = dc.channel('dd-trace:storage:enter')
|
|
20
20
|
const spanFinishCh = dc.channel('dd-trace:span:finish')
|
|
@@ -29,21 +29,6 @@ function getActiveSpan () {
|
|
|
29
29
|
return store && store.span
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function getStartedSpans (context) {
|
|
33
|
-
return context._trace.started
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isWebServerSpan (tags) {
|
|
37
|
-
return tags[SPAN_TYPE] === WEB
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function endpointNameFromTags (tags) {
|
|
41
|
-
return tags[RESOURCE_NAME] || [
|
|
42
|
-
tags[HTTP_METHOD],
|
|
43
|
-
tags[HTTP_ROUTE]
|
|
44
|
-
].filter(v => v).join(' ')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
32
|
let channelsActivated = false
|
|
48
33
|
function ensureChannelsActivated () {
|
|
49
34
|
if (channelsActivated) return
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../ext/tags')
|
|
2
|
+
const { WEB } = require('../../../../ext/types')
|
|
3
|
+
|
|
4
|
+
function isWebServerSpan (tags) {
|
|
5
|
+
return tags[SPAN_TYPE] === WEB
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function endpointNameFromTags (tags) {
|
|
9
|
+
return tags[RESOURCE_NAME] || [
|
|
10
|
+
tags[HTTP_METHOD],
|
|
11
|
+
tags[HTTP_ROUTE]
|
|
12
|
+
].filter(v => v).join(' ')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getStartedSpans (context) {
|
|
16
|
+
return context._trace.started
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
isWebServerSpan,
|
|
21
|
+
endpointNameFromTags,
|
|
22
|
+
getStartedSpans
|
|
23
|
+
}
|