dd-trace 4.18.0 → 4.22.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 +3 -2
- package/README.md +3 -3
- package/ext/kinds.d.ts +1 -0
- package/ext/kinds.js +2 -1
- package/ext/tags.d.ts +2 -1
- package/ext/tags.js +6 -1
- package/index.d.ts +29 -0
- package/package.json +11 -10
- package/packages/datadog-core/src/storage/async_resource.js +1 -1
- package/packages/datadog-esbuild/index.js +1 -20
- package/packages/datadog-instrumentations/src/aerospike.js +47 -0
- package/packages/datadog-instrumentations/src/apollo-server-core.js +41 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +83 -0
- package/packages/datadog-instrumentations/src/graphql.js +18 -4
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +1 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
- package/packages/datadog-instrumentations/src/http/client.js +10 -0
- package/packages/datadog-instrumentations/src/jest.js +11 -5
- package/packages/datadog-instrumentations/src/kafkajs.js +27 -0
- package/packages/datadog-instrumentations/src/next.js +18 -6
- package/packages/datadog-instrumentations/src/restify.js +14 -1
- package/packages/datadog-instrumentations/src/rhea.js +15 -9
- package/packages/datadog-plugin-aerospike/src/index.js +113 -0
- package/packages/datadog-plugin-graphql/src/resolve.js +26 -18
- package/packages/datadog-plugin-http/src/client.js +19 -2
- package/packages/datadog-plugin-kafkajs/src/consumer.js +59 -6
- package/packages/datadog-plugin-kafkajs/src/producer.js +64 -6
- package/packages/datadog-plugin-next/src/index.js +40 -14
- package/packages/dd-trace/src/appsec/activation.js +29 -0
- package/packages/dd-trace/src/appsec/addresses.js +3 -1
- package/packages/dd-trace/src/appsec/api_security_sampler.js +48 -0
- package/packages/dd-trace/src/appsec/blocked_templates.js +4 -1
- package/packages/dd-trace/src/appsec/blocking.js +95 -43
- package/packages/dd-trace/src/appsec/channels.js +5 -2
- package/packages/dd-trace/src/appsec/graphql.js +146 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +105 -0
- package/packages/dd-trace/src/appsec/iast/iast-log.js +1 -1
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
- package/packages/dd-trace/src/appsec/iast/index.js +1 -1
- package/packages/dd-trace/src/appsec/iast/path-line.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/constants.js +7 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +12 -19
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/header-sensitive-analyzer.js +20 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +6 -10
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +18 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +79 -85
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +27 -36
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +14 -11
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +33 -32
- package/packages/dd-trace/src/appsec/recommended.json +1737 -120
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +36 -15
- package/packages/dd-trace/src/appsec/reporter.js +50 -34
- package/packages/dd-trace/src/appsec/rule_manager.js +9 -6
- package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +28 -13
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +0 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +17 -1
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +75 -56
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +22 -6
- package/packages/dd-trace/src/config.js +48 -7
- package/packages/dd-trace/src/datastreams/processor.js +166 -26
- package/packages/dd-trace/src/format.js +6 -1
- package/packages/dd-trace/src/id.js +12 -0
- package/packages/dd-trace/src/iitm.js +1 -1
- package/packages/dd-trace/src/log/channels.js +1 -1
- package/packages/dd-trace/src/noop/proxy.js +4 -0
- package/packages/dd-trace/src/opentelemetry/span.js +95 -2
- package/packages/dd-trace/src/opentelemetry/tracer.js +9 -10
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +14 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -0
- package/packages/dd-trace/src/opentracing/span_context.js +5 -2
- package/packages/dd-trace/src/plugin_manager.js +1 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +2 -1
- package/packages/dd-trace/src/plugins/database.js +1 -1
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/plugin.js +1 -1
- package/packages/dd-trace/src/plugins/util/ci.js +6 -19
- package/packages/dd-trace/src/plugins/util/git.js +4 -3
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +7 -6
- package/packages/dd-trace/src/plugins/util/test.js +3 -2
- package/packages/dd-trace/src/plugins/util/url.js +26 -0
- package/packages/dd-trace/src/plugins/util/user-provided-git.js +4 -16
- package/packages/dd-trace/src/profiler.js +5 -3
- package/packages/dd-trace/src/profiling/config.js +26 -2
- package/packages/dd-trace/src/profiling/profiler.js +17 -10
- package/packages/dd-trace/src/profiling/profilers/events.js +264 -0
- package/packages/dd-trace/src/profiling/profilers/shared.js +39 -0
- package/packages/dd-trace/src/profiling/profilers/space.js +2 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +121 -58
- package/packages/dd-trace/src/proxy.js +25 -1
- package/packages/dd-trace/src/ritm.js +1 -1
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +5 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +4 -0
- package/packages/dd-trace/src/span_processor.js +4 -0
- package/packages/dd-trace/src/spanleak.js +98 -0
- package/packages/dd-trace/src/startup-log.js +7 -1
- package/packages/dd-trace/src/telemetry/dependencies.js +56 -10
- package/packages/dd-trace/src/telemetry/index.js +136 -44
- package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
- package/packages/dd-trace/src/telemetry/send-data.js +47 -5
- package/packages/dd-trace/src/tracer.js +8 -2
- package/scripts/install_plugin_modules.js +11 -3
- package/packages/diagnostics_channel/index.js +0 -3
- package/packages/diagnostics_channel/src/index.js +0 -121
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const { performance, constants, PerformanceObserver } = require('node:perf_hooks')
|
|
2
|
+
const { END_TIMESTAMP_LABEL } = require('./shared')
|
|
3
|
+
const semver = require('semver')
|
|
4
|
+
const { Function, Label, Line, Location, Profile, Sample, StringTable, ValueType } = require('pprof-format')
|
|
5
|
+
const pprof = require('@datadog/pprof/')
|
|
6
|
+
|
|
7
|
+
// Format of perf_hooks events changed with Node 16, we need to be mindful of it.
|
|
8
|
+
const node16 = semver.gte(process.version, '16.0.0')
|
|
9
|
+
|
|
10
|
+
// perf_hooks uses millis, with fractional part representing nanos. We emit nanos into the pprof file.
|
|
11
|
+
const MS_TO_NS = 1000000
|
|
12
|
+
|
|
13
|
+
// While this is an "events profiler", meaning it emits a pprof file based on events observed as
|
|
14
|
+
// perf_hooks events, the emitted pprof file uses the type "timeline".
|
|
15
|
+
const pprofValueType = 'timeline'
|
|
16
|
+
const pprofValueUnit = 'nanoseconds'
|
|
17
|
+
|
|
18
|
+
function labelFromStr (stringTable, key, valStr) {
|
|
19
|
+
return new Label({ key, str: stringTable.dedup(valStr) })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function labelFromStrStr (stringTable, keyStr, valStr) {
|
|
23
|
+
return labelFromStr(stringTable, stringTable.dedup(keyStr), valStr)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class GCDecorator {
|
|
27
|
+
constructor (stringTable) {
|
|
28
|
+
this.stringTable = stringTable
|
|
29
|
+
this.reasonLabelKey = stringTable.dedup('gc reason')
|
|
30
|
+
this.kindLabels = []
|
|
31
|
+
this.reasonLabels = []
|
|
32
|
+
this.flagObj = {}
|
|
33
|
+
|
|
34
|
+
const kindLabelKey = stringTable.dedup('gc type')
|
|
35
|
+
|
|
36
|
+
// Create labels for all GC performance flags and kinds of GC
|
|
37
|
+
for (const [key, value] of Object.entries(constants)) {
|
|
38
|
+
if (key.startsWith('NODE_PERFORMANCE_GC_FLAGS_')) {
|
|
39
|
+
this.flagObj[key.substring(26).toLowerCase()] = value
|
|
40
|
+
} else if (key.startsWith('NODE_PERFORMANCE_GC_')) {
|
|
41
|
+
// It's a constant for a kind of GC
|
|
42
|
+
const kind = key.substring(20).toLowerCase()
|
|
43
|
+
this.kindLabels[value] = labelFromStr(stringTable, kindLabelKey, kind)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
decorateSample (sampleInput, item) {
|
|
49
|
+
const { kind, flags } = node16 ? item.detail : item
|
|
50
|
+
sampleInput.label.push(this.kindLabels[kind])
|
|
51
|
+
const reasonLabel = this.getReasonLabel(flags)
|
|
52
|
+
if (reasonLabel) {
|
|
53
|
+
sampleInput.label.push(reasonLabel)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getReasonLabel (flags) {
|
|
58
|
+
if (flags === 0) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
let reasonLabel = this.reasonLabels[flags]
|
|
62
|
+
if (!reasonLabel) {
|
|
63
|
+
const reasons = []
|
|
64
|
+
for (const [key, value] of Object.entries(this.flagObj)) {
|
|
65
|
+
if (value & flags) {
|
|
66
|
+
reasons.push(key)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const reasonStr = reasons.join(',')
|
|
70
|
+
reasonLabel = labelFromStr(this.stringTable, this.reasonLabelKey, reasonStr)
|
|
71
|
+
this.reasonLabels[flags] = reasonLabel
|
|
72
|
+
}
|
|
73
|
+
return reasonLabel
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class DNSDecorator {
|
|
78
|
+
constructor (stringTable) {
|
|
79
|
+
this.stringTable = stringTable
|
|
80
|
+
this.operationNameLabelKey = stringTable.dedup('operation')
|
|
81
|
+
this.hostLabelKey = stringTable.dedup('host')
|
|
82
|
+
this.addressLabelKey = stringTable.dedup('address')
|
|
83
|
+
this.portLabelKey = stringTable.dedup('port')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
decorateSample (sampleInput, item) {
|
|
87
|
+
const labels = sampleInput.label
|
|
88
|
+
const stringTable = this.stringTable
|
|
89
|
+
function addLabel (labelNameKey, labelValue) {
|
|
90
|
+
labels.push(labelFromStr(stringTable, labelNameKey, labelValue))
|
|
91
|
+
}
|
|
92
|
+
const op = item.name
|
|
93
|
+
addLabel(this.operationNameLabelKey, item.name)
|
|
94
|
+
const detail = item.detail
|
|
95
|
+
switch (op) {
|
|
96
|
+
case 'lookup':
|
|
97
|
+
addLabel(this.hostLabelKey, detail.hostname)
|
|
98
|
+
break
|
|
99
|
+
case 'lookupService':
|
|
100
|
+
addLabel(this.addressLabelKey, detail.host)
|
|
101
|
+
labels.push(new Label({ key: this.portLabelKey, num: detail.port }))
|
|
102
|
+
break
|
|
103
|
+
case 'getHostByAddr':
|
|
104
|
+
addLabel(this.addressLabelKey, detail.host)
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
if (op.startsWith('query')) {
|
|
108
|
+
addLabel(this.hostLabelKey, detail.host)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class NetDecorator {
|
|
115
|
+
constructor (stringTable) {
|
|
116
|
+
this.stringTable = stringTable
|
|
117
|
+
this.operationNameLabelKey = stringTable.dedup('operation')
|
|
118
|
+
this.hostLabelKey = stringTable.dedup('host')
|
|
119
|
+
this.portLabelKey = stringTable.dedup('port')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
decorateSample (sampleInput, item) {
|
|
123
|
+
const labels = sampleInput.label
|
|
124
|
+
const stringTable = this.stringTable
|
|
125
|
+
function addLabel (labelNameKey, labelValue) {
|
|
126
|
+
labels.push(labelFromStr(stringTable, labelNameKey, labelValue))
|
|
127
|
+
}
|
|
128
|
+
const op = item.name
|
|
129
|
+
addLabel(this.operationNameLabelKey, op)
|
|
130
|
+
if (op === 'connect') {
|
|
131
|
+
const detail = item.detail
|
|
132
|
+
addLabel(this.hostLabelKey, detail.host)
|
|
133
|
+
labels.push(new Label({ key: this.portLabelKey, num: detail.port }))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Keys correspond to PerformanceEntry.entryType, values are constructor
|
|
139
|
+
// functions for type-specific decorators.
|
|
140
|
+
const decoratorTypes = {
|
|
141
|
+
gc: GCDecorator
|
|
142
|
+
}
|
|
143
|
+
// Needs at least node 16 for DNS and Net
|
|
144
|
+
if (node16) {
|
|
145
|
+
decoratorTypes.dns = DNSDecorator
|
|
146
|
+
decoratorTypes.net = NetDecorator
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* This class generates pprof files with timeline events sourced from Node.js
|
|
151
|
+
* performance measurement APIs.
|
|
152
|
+
*/
|
|
153
|
+
class EventsProfiler {
|
|
154
|
+
constructor (options = {}) {
|
|
155
|
+
this.type = 'events'
|
|
156
|
+
this._flushIntervalNanos = (options.flushInterval || 60000) * 1e6 // 60 sec
|
|
157
|
+
this._observer = undefined
|
|
158
|
+
this.entries = []
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
start () {
|
|
162
|
+
function add (items) {
|
|
163
|
+
this.entries.push(...items.getEntries())
|
|
164
|
+
}
|
|
165
|
+
if (!this._observer) {
|
|
166
|
+
this._observer = new PerformanceObserver(add.bind(this))
|
|
167
|
+
}
|
|
168
|
+
this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
stop () {
|
|
172
|
+
if (this._observer) {
|
|
173
|
+
this._observer.disconnect()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
profile (startDate, endDate) {
|
|
178
|
+
if (this.entries.length === 0) {
|
|
179
|
+
// No events in the period; don't produce a profile
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const stringTable = new StringTable()
|
|
184
|
+
const locations = []
|
|
185
|
+
const functions = []
|
|
186
|
+
|
|
187
|
+
// A synthetic single-frame location to serve as the location for timeline
|
|
188
|
+
// samples. We need these as the profiling backend (mimicking official pprof
|
|
189
|
+
// tool's behavior) ignores these.
|
|
190
|
+
const locationId = (() => {
|
|
191
|
+
const fn = new Function({ id: functions.length + 1, name: stringTable.dedup('') })
|
|
192
|
+
functions.push(fn)
|
|
193
|
+
const line = new Line({ functionId: fn.id })
|
|
194
|
+
const location = new Location({ id: locations.length + 1, line: [line] })
|
|
195
|
+
locations.push(location)
|
|
196
|
+
return [location.id]
|
|
197
|
+
})()
|
|
198
|
+
|
|
199
|
+
const decorators = {}
|
|
200
|
+
for (const [eventType, DecoratorCtor] of Object.entries(decoratorTypes)) {
|
|
201
|
+
const decorator = new DecoratorCtor(stringTable)
|
|
202
|
+
decorator.eventTypeLabel = labelFromStrStr(stringTable, 'event', eventType)
|
|
203
|
+
decorators[eventType] = decorator
|
|
204
|
+
}
|
|
205
|
+
const timestampLabelKey = stringTable.dedup(END_TIMESTAMP_LABEL)
|
|
206
|
+
|
|
207
|
+
const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
|
|
208
|
+
const lateEntries = []
|
|
209
|
+
const perfEndDate = endDate.getTime() - performance.timeOrigin
|
|
210
|
+
const samples = this.entries.map((item) => {
|
|
211
|
+
const decorator = decorators[item.entryType]
|
|
212
|
+
if (!decorator) {
|
|
213
|
+
// Shouldn't happen but it's better to not rely on observer only getting
|
|
214
|
+
// requested event types.
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
const { startTime, duration } = item
|
|
218
|
+
if (startTime >= perfEndDate) {
|
|
219
|
+
// An event past the current recording end date; save it for the next
|
|
220
|
+
// profile. Not supposed to happen as long as there's no async activity
|
|
221
|
+
// between capture of the endDate value in profiler.js _collect() and
|
|
222
|
+
// here, but better be safe than sorry.
|
|
223
|
+
lateEntries.push(item)
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
const endTime = startTime + duration
|
|
227
|
+
const sampleInput = {
|
|
228
|
+
value: [Math.round(duration * MS_TO_NS)],
|
|
229
|
+
locationId,
|
|
230
|
+
label: [
|
|
231
|
+
decorator.eventTypeLabel,
|
|
232
|
+
new Label({ key: timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
decorator.decorateSample(sampleInput, item)
|
|
236
|
+
return new Sample(sampleInput)
|
|
237
|
+
}).filter(v => v)
|
|
238
|
+
|
|
239
|
+
this.entries = lateEntries
|
|
240
|
+
|
|
241
|
+
const timeValueType = new ValueType({
|
|
242
|
+
type: stringTable.dedup(pprofValueType),
|
|
243
|
+
unit: stringTable.dedup(pprofValueUnit)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
return new Profile({
|
|
247
|
+
sampleType: [timeValueType],
|
|
248
|
+
timeNanos: endDate.getTime() * MS_TO_NS,
|
|
249
|
+
periodType: timeValueType,
|
|
250
|
+
period: 1,
|
|
251
|
+
durationNanos: (endDate.getTime() - startDate.getTime()) * MS_TO_NS,
|
|
252
|
+
sample: samples,
|
|
253
|
+
location: locations,
|
|
254
|
+
function: functions,
|
|
255
|
+
stringTable: stringTable
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
encode (profile) {
|
|
260
|
+
return pprof.encode(profile)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = EventsProfiler
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isMainThread, threadId } = require('node:worker_threads')
|
|
4
|
+
|
|
5
|
+
const END_TIMESTAMP_LABEL = 'end_timestamp_ns'
|
|
6
|
+
const THREAD_NAME_LABEL = 'thread name'
|
|
7
|
+
const OS_THREAD_ID_LABEL = 'os thread id'
|
|
8
|
+
const THREAD_ID_LABEL = 'thread id'
|
|
9
|
+
const threadNamePrefix = isMainThread ? 'Main' : `Worker #${threadId}`
|
|
10
|
+
const eventLoopThreadName = `${threadNamePrefix} Event Loop`
|
|
11
|
+
|
|
12
|
+
function getThreadLabels () {
|
|
13
|
+
const pprof = require('@datadog/pprof')
|
|
14
|
+
const nativeThreadId = pprof.getNativeThreadId()
|
|
15
|
+
return {
|
|
16
|
+
[THREAD_NAME_LABEL]: eventLoopThreadName,
|
|
17
|
+
[THREAD_ID_LABEL]: `${threadId}`,
|
|
18
|
+
[OS_THREAD_ID_LABEL]: `${nativeThreadId}`
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cacheThreadLabels () {
|
|
23
|
+
let labels
|
|
24
|
+
return () => {
|
|
25
|
+
if (!labels) {
|
|
26
|
+
labels = getThreadLabels()
|
|
27
|
+
}
|
|
28
|
+
return labels
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
END_TIMESTAMP_LABEL,
|
|
34
|
+
THREAD_NAME_LABEL,
|
|
35
|
+
THREAD_ID_LABEL,
|
|
36
|
+
threadNamePrefix,
|
|
37
|
+
eventLoopThreadName,
|
|
38
|
+
getThreadLabels: cacheThreadLabels()
|
|
39
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { oomExportStrategies } = require('../constants')
|
|
4
|
+
const { getThreadLabels } = require('./shared')
|
|
4
5
|
|
|
5
6
|
function strategiesToCallbackMode (strategies, callbackMode) {
|
|
6
7
|
return strategies.includes(oomExportStrategies.ASYNC_CALLBACK) ? callbackMode.Async : 0
|
|
@@ -33,7 +34,7 @@ class NativeSpaceProfiler {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
profile () {
|
|
36
|
-
return this._pprof.heap.profile(undefined, this._mapper)
|
|
37
|
+
return this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
encode (profile) {
|
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const { storage } = require('../../../../datadog-core')
|
|
4
4
|
|
|
5
|
-
const dc = require('
|
|
5
|
+
const dc = require('dc-polyfill')
|
|
6
6
|
const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags')
|
|
7
7
|
const { WEB } = require('../../../../../ext/types')
|
|
8
8
|
const runtimeMetrics = require('../../runtime_metrics')
|
|
9
9
|
const telemetryMetrics = require('../../telemetry/metrics')
|
|
10
|
+
const { END_TIMESTAMP_LABEL, getThreadLabels } = require('./shared')
|
|
10
11
|
|
|
11
12
|
const beforeCh = dc.channel('dd-trace:storage:before')
|
|
12
13
|
const enterCh = dc.channel('dd-trace:storage:enter')
|
|
14
|
+
const spanFinishCh = dc.channel('dd-trace:span:finish')
|
|
13
15
|
const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
|
|
14
16
|
|
|
15
|
-
const
|
|
16
|
-
const { isMainThread, threadId } = require('node:worker_threads')
|
|
17
|
-
const name = isMainThread ? 'Main' : `Worker #${threadId}`
|
|
18
|
-
return `${name} Event Loop`
|
|
19
|
-
})()
|
|
17
|
+
const MemoizedWebTags = Symbol('NativeWallProfiler.MemoizedWebTags')
|
|
20
18
|
|
|
21
19
|
let kSampleCount
|
|
22
20
|
|
|
@@ -29,30 +27,6 @@ function getStartedSpans (context) {
|
|
|
29
27
|
return context._trace.started
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
function generateLabels ({ context: { spanId, rootSpanId, webTags, endpoint }, timestamp }) {
|
|
33
|
-
const labels = { 'thread name': threadName }
|
|
34
|
-
if (spanId) {
|
|
35
|
-
labels['span id'] = spanId
|
|
36
|
-
}
|
|
37
|
-
if (rootSpanId) {
|
|
38
|
-
labels['local root span id'] = rootSpanId
|
|
39
|
-
}
|
|
40
|
-
if (webTags && Object.keys(webTags).length !== 0) {
|
|
41
|
-
labels['trace endpoint'] = endpointNameFromTags(webTags)
|
|
42
|
-
} else if (endpoint) {
|
|
43
|
-
// fallback to endpoint computed when sample was taken
|
|
44
|
-
labels['trace endpoint'] = endpoint
|
|
45
|
-
}
|
|
46
|
-
// Incoming timestamps are in microseconds, we emit nanos.
|
|
47
|
-
labels['end_timestamp_ns'] = timestamp * 1000n
|
|
48
|
-
|
|
49
|
-
return labels
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function getSpanContextTags (span) {
|
|
53
|
-
return span.context()._tags
|
|
54
|
-
}
|
|
55
|
-
|
|
56
30
|
function isWebServerSpan (tags) {
|
|
57
31
|
return tags[SPAN_TYPE] === WEB
|
|
58
32
|
}
|
|
@@ -64,6 +38,38 @@ function endpointNameFromTags (tags) {
|
|
|
64
38
|
].filter(v => v).join(' ')
|
|
65
39
|
}
|
|
66
40
|
|
|
41
|
+
function getWebTags (startedSpans, i, span) {
|
|
42
|
+
// Are web tags for this span already memoized?
|
|
43
|
+
const memoizedWebTags = span[MemoizedWebTags]
|
|
44
|
+
if (memoizedWebTags !== undefined) {
|
|
45
|
+
return memoizedWebTags
|
|
46
|
+
}
|
|
47
|
+
// No, we'll have to memoize a new value
|
|
48
|
+
function memoize (tags) {
|
|
49
|
+
span[MemoizedWebTags] = tags
|
|
50
|
+
return tags
|
|
51
|
+
}
|
|
52
|
+
// Is this span itself a web span?
|
|
53
|
+
const context = span.context()
|
|
54
|
+
const tags = context._tags
|
|
55
|
+
if (isWebServerSpan(tags)) {
|
|
56
|
+
return memoize(tags)
|
|
57
|
+
}
|
|
58
|
+
// It isn't. Get parent's web tags (memoize them too recursively.)
|
|
59
|
+
// There might be several webspans, for example with next.js, http plugin creates the first span
|
|
60
|
+
// and then next.js plugin creates a child span, and this child span has the correct endpoint
|
|
61
|
+
// information. That's why we always use the tags of the closest ancestor web span.
|
|
62
|
+
const parentId = context._parentId
|
|
63
|
+
while (--i >= 0) {
|
|
64
|
+
const ispan = startedSpans[i]
|
|
65
|
+
if (ispan.context()._spanId === parentId) {
|
|
66
|
+
return memoize(getWebTags(startedSpans, i, ispan))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Local root span with no web span
|
|
70
|
+
return memoize(null)
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
class NativeWallProfiler {
|
|
68
74
|
constructor (options = {}) {
|
|
69
75
|
this.type = 'wall'
|
|
@@ -71,13 +77,27 @@ class NativeWallProfiler {
|
|
|
71
77
|
this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
|
|
72
78
|
this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
|
|
73
79
|
this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
|
|
74
|
-
this.
|
|
80
|
+
this._timelineEnabled = !!options.timelineEnabled
|
|
81
|
+
// We need to capture span data into the sample context for either code hotspots
|
|
82
|
+
// or endpoint collection.
|
|
83
|
+
this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
|
|
84
|
+
// We need to run the pprof wall profiler with sample contexts if we're either
|
|
85
|
+
// 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
|
|
75
88
|
this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
|
|
76
89
|
this._mapper = undefined
|
|
77
90
|
this._pprof = undefined
|
|
78
91
|
|
|
79
|
-
// Bind to this so
|
|
80
|
-
|
|
92
|
+
// Bind these to this so they can be used as callbacks
|
|
93
|
+
if (this._withContexts) {
|
|
94
|
+
if (this._captureSpanData) {
|
|
95
|
+
this._enter = this._enter.bind(this)
|
|
96
|
+
this._spanFinished = this._spanFinished.bind(this)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this._generateLabels = this._generateLabels.bind(this)
|
|
100
|
+
|
|
81
101
|
this._logger = options.logger
|
|
82
102
|
this._started = false
|
|
83
103
|
}
|
|
@@ -115,15 +135,20 @@ class NativeWallProfiler {
|
|
|
115
135
|
})
|
|
116
136
|
|
|
117
137
|
if (this._withContexts) {
|
|
118
|
-
this._profilerState = this._pprof.time.getState()
|
|
119
138
|
this._currentContext = {}
|
|
120
139
|
this._pprof.time.setContext(this._currentContext)
|
|
121
|
-
this._lastSpan = undefined
|
|
122
|
-
this._lastStartedSpans = undefined
|
|
123
|
-
this._lastSampleCount = 0
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
if (this._captureSpanData) {
|
|
142
|
+
this._profilerState = this._pprof.time.getState()
|
|
143
|
+
this._lastSpan = undefined
|
|
144
|
+
this._lastStartedSpans = undefined
|
|
145
|
+
this._lastWebTags = undefined
|
|
146
|
+
this._lastSampleCount = 0
|
|
147
|
+
|
|
148
|
+
beforeCh.subscribe(this._enter)
|
|
149
|
+
enterCh.subscribe(this._enter)
|
|
150
|
+
spanFinishCh.subscribe(this._spanFinished)
|
|
151
|
+
}
|
|
127
152
|
}
|
|
128
153
|
|
|
129
154
|
this._started = true
|
|
@@ -144,11 +169,17 @@ class NativeWallProfiler {
|
|
|
144
169
|
|
|
145
170
|
const span = getActiveSpan()
|
|
146
171
|
if (span) {
|
|
172
|
+
const context = span.context()
|
|
147
173
|
this._lastSpan = span
|
|
148
|
-
|
|
174
|
+
const startedSpans = getStartedSpans(context)
|
|
175
|
+
this._lastStartedSpans = startedSpans
|
|
176
|
+
if (this._endpointCollectionEnabled) {
|
|
177
|
+
this._lastWebTags = getWebTags(startedSpans, startedSpans.length, span)
|
|
178
|
+
}
|
|
149
179
|
} else {
|
|
150
180
|
this._lastStartedSpans = undefined
|
|
151
181
|
this._lastSpan = undefined
|
|
182
|
+
this._lastWebTags = undefined
|
|
152
183
|
}
|
|
153
184
|
}
|
|
154
185
|
|
|
@@ -163,21 +194,17 @@ class NativeWallProfiler {
|
|
|
163
194
|
context.rootSpanId = rootSpan.context().toSpanId()
|
|
164
195
|
}
|
|
165
196
|
}
|
|
166
|
-
if (this.
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
context.endpoint = endpointNameFromTags(tags)
|
|
178
|
-
break
|
|
179
|
-
}
|
|
180
|
-
}
|
|
197
|
+
if (this._lastWebTags) {
|
|
198
|
+
context.webTags = this._lastWebTags
|
|
199
|
+
// endpoint may not be determined yet, but keep it as fallback
|
|
200
|
+
// if tags are not available anymore during serialization
|
|
201
|
+
context.endpoint = endpointNameFromTags(this._lastWebTags)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_spanFinished (span) {
|
|
206
|
+
if (span[MemoizedWebTags]) {
|
|
207
|
+
span[MemoizedWebTags] = undefined
|
|
181
208
|
}
|
|
182
209
|
}
|
|
183
210
|
|
|
@@ -193,12 +220,12 @@ class NativeWallProfiler {
|
|
|
193
220
|
|
|
194
221
|
_stop (restart) {
|
|
195
222
|
if (!this._started) return
|
|
196
|
-
if (this.
|
|
223
|
+
if (this._captureSpanData) {
|
|
197
224
|
// update last sample context if needed
|
|
198
225
|
this._enter()
|
|
199
226
|
this._lastSampleCount = 0
|
|
200
227
|
}
|
|
201
|
-
const profile = this._pprof.time.stop(restart, this.
|
|
228
|
+
const profile = this._pprof.time.stop(restart, this._generateLabels)
|
|
202
229
|
if (restart) {
|
|
203
230
|
const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
|
|
204
231
|
if (v8BugDetected !== 0) {
|
|
@@ -208,6 +235,38 @@ class NativeWallProfiler {
|
|
|
208
235
|
return profile
|
|
209
236
|
}
|
|
210
237
|
|
|
238
|
+
_generateLabels (context) {
|
|
239
|
+
if (context == null) {
|
|
240
|
+
// generateLabels is also called for samples without context.
|
|
241
|
+
// In that case just return thread labels.
|
|
242
|
+
return getThreadLabels()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const labels = { ...getThreadLabels() }
|
|
246
|
+
|
|
247
|
+
const { context: { spanId, rootSpanId, webTags, endpoint }, timestamp } = context
|
|
248
|
+
|
|
249
|
+
if (this._timelineEnabled) {
|
|
250
|
+
// Incoming timestamps are in microseconds, we emit nanos.
|
|
251
|
+
labels[END_TIMESTAMP_LABEL] = timestamp * 1000n
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (spanId) {
|
|
255
|
+
labels['span id'] = spanId
|
|
256
|
+
}
|
|
257
|
+
if (rootSpanId) {
|
|
258
|
+
labels['local root span id'] = rootSpanId
|
|
259
|
+
}
|
|
260
|
+
if (webTags && Object.keys(webTags).length !== 0) {
|
|
261
|
+
labels['trace endpoint'] = endpointNameFromTags(webTags)
|
|
262
|
+
} else if (endpoint) {
|
|
263
|
+
// fallback to endpoint computed when sample was taken
|
|
264
|
+
labels['trace endpoint'] = endpoint
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return labels
|
|
268
|
+
}
|
|
269
|
+
|
|
211
270
|
profile () {
|
|
212
271
|
return this._stop(true)
|
|
213
272
|
}
|
|
@@ -220,10 +279,14 @@ class NativeWallProfiler {
|
|
|
220
279
|
if (!this._started) return
|
|
221
280
|
|
|
222
281
|
const profile = this._stop(false)
|
|
223
|
-
if (this.
|
|
282
|
+
if (this._captureSpanData) {
|
|
224
283
|
beforeCh.unsubscribe(this._enter)
|
|
225
284
|
enterCh.unsubscribe(this._enter)
|
|
285
|
+
spanFinishCh.unsubscribe(this._spanFinished)
|
|
226
286
|
this._profilerState = undefined
|
|
287
|
+
this._lastSpan = undefined
|
|
288
|
+
this._lastStartedSpans = undefined
|
|
289
|
+
this._lastWebTags = undefined
|
|
227
290
|
}
|
|
228
291
|
|
|
229
292
|
this._started = false
|
|
@@ -10,6 +10,7 @@ const PluginManager = require('./plugin_manager')
|
|
|
10
10
|
const remoteConfig = require('./appsec/remote_config')
|
|
11
11
|
const AppsecSdk = require('./appsec/sdk')
|
|
12
12
|
const dogstatsd = require('./dogstatsd')
|
|
13
|
+
const spanleak = require('./spanleak')
|
|
13
14
|
|
|
14
15
|
class Tracer extends NoopProxy {
|
|
15
16
|
constructor () {
|
|
@@ -35,6 +36,19 @@ class Tracer extends NoopProxy {
|
|
|
35
36
|
setInterval(() => {
|
|
36
37
|
this.dogstatsd.flush()
|
|
37
38
|
}, 10 * 1000).unref()
|
|
39
|
+
|
|
40
|
+
process.once('beforeExit', () => {
|
|
41
|
+
this.dogstatsd.flush()
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (config.spanLeakDebug > 0) {
|
|
46
|
+
if (config.spanLeakDebug === spanleak.MODES.LOG) {
|
|
47
|
+
spanleak.enableLogging()
|
|
48
|
+
} else if (config.spanLeakDebug === spanleak.MODES.GC_AND_LOG) {
|
|
49
|
+
spanleak.enableGarbageCollection()
|
|
50
|
+
}
|
|
51
|
+
spanleak.startScrubber()
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
if (config.remoteConfig.enabled && !config.isCiVisibility) {
|
|
@@ -62,11 +76,14 @@ class Tracer extends NoopProxy {
|
|
|
62
76
|
// do not stop tracer initialization if the profiler fails to be imported
|
|
63
77
|
try {
|
|
64
78
|
const profiler = require('./profiler')
|
|
65
|
-
profiler.start(config)
|
|
79
|
+
this._profilerStarted = profiler.start(config)
|
|
66
80
|
} catch (e) {
|
|
67
81
|
log.error(e)
|
|
68
82
|
}
|
|
69
83
|
}
|
|
84
|
+
if (!this._profilerStarted) {
|
|
85
|
+
this._profilerStarted = Promise.resolve(false)
|
|
86
|
+
}
|
|
70
87
|
|
|
71
88
|
if (config.runtimeMetrics) {
|
|
72
89
|
runtimeMetrics.start(config)
|
|
@@ -104,6 +121,13 @@ class Tracer extends NoopProxy {
|
|
|
104
121
|
return this
|
|
105
122
|
}
|
|
106
123
|
|
|
124
|
+
profilerStarted () {
|
|
125
|
+
if (!this._profilerStarted) {
|
|
126
|
+
throw new Error('profilerStarted() must be called after init()')
|
|
127
|
+
}
|
|
128
|
+
return this._profilerStarted
|
|
129
|
+
}
|
|
130
|
+
|
|
107
131
|
use () {
|
|
108
132
|
this._pluginManager.configurePlugin(...arguments)
|
|
109
133
|
return this
|
|
@@ -37,6 +37,11 @@ const redisConfig = {
|
|
|
37
37
|
|
|
38
38
|
const storage = {
|
|
39
39
|
client: {
|
|
40
|
+
aerospike: {
|
|
41
|
+
opName: () => 'aerospike.command',
|
|
42
|
+
serviceName: ({ tracerService, pluginConfig }) =>
|
|
43
|
+
pluginConfig.service || `${tracerService}-aerospike`
|
|
44
|
+
},
|
|
40
45
|
'cassandra-driver': {
|
|
41
46
|
opName: () => 'cassandra.query',
|
|
42
47
|
serviceName: ({ tracerService, pluginConfig, system }) =>
|
|
@@ -22,6 +22,10 @@ function withFunction ({ tracerService, pluginConfig, params }) {
|
|
|
22
22
|
|
|
23
23
|
const storage = {
|
|
24
24
|
client: {
|
|
25
|
+
aerospike: {
|
|
26
|
+
opName: () => 'aerospike.command',
|
|
27
|
+
serviceName: configWithFallback
|
|
28
|
+
},
|
|
25
29
|
'cassandra-driver': {
|
|
26
30
|
opName: () => 'cassandra.query',
|
|
27
31
|
serviceName: configWithFallback
|