dd-trace 6.19.0 → 6.20.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 +163 -22
- package/package.json +2 -2
- package/packages/datadog-esbuild/index.js +9 -6
- package/packages/datadog-instrumentations/src/ai.js +26 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
- package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
- package/packages/dd-trace/src/config/remote_config.js +12 -0
- package/packages/dd-trace/src/config/supported-configurations.json +21 -0
- package/packages/dd-trace/src/debugger/constants.js +13 -4
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
- package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
- package/packages/dd-trace/src/debugger/index.js +60 -28
- package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
- package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
- package/packages/dd-trace/src/debugger/redaction.js +139 -0
- package/packages/dd-trace/src/knuth-hash.js +17 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
- package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
- package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
- package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
- package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +2 -2
- package/packages/dd-trace/src/llmobs/sdk.js +19 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +42 -26
- package/packages/dd-trace/src/llmobs/tagger.js +35 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
- package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
- package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
- package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
- package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
- package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
- package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
- package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
- package/packages/dd-trace/src/opentracing/span.js +14 -9
- package/packages/dd-trace/src/opentracing/tracer.js +10 -3
- package/packages/dd-trace/src/otel-sampling.js +171 -0
- package/packages/dd-trace/src/plugins/index.js +0 -1
- package/packages/dd-trace/src/priority_sampler.js +155 -33
- package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
- package/packages/dd-trace/src/sampler.js +2 -5
- package/packages/dd-trace/src/sampling_rule.js +4 -8
- package/packages/dd-trace/src/span_format.js +6 -0
- package/packages/dd-trace/src/span_processor.js +26 -2
- package/packages/dd-trace/src/standalone/index.js +5 -4
- package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
- package/packages/dd-trace/src/telemetry/metrics.js +4 -3
- package/packages/datadog-instrumentations/src/postgres.js +0 -7
- package/packages/datadog-instrumentations/src/supabase.js +0 -15
|
@@ -1,128 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const config = require('../config')
|
|
4
|
-
|
|
5
|
-
const excludedIdentifiers = new Set(
|
|
6
|
-
config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS
|
|
7
|
-
.map((name) => normalizeName(name))
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
const REDACTED_IDENTIFIERS = new Set(
|
|
11
|
-
[
|
|
12
|
-
'2fa',
|
|
13
|
-
'_csrf',
|
|
14
|
-
'_csrf_token',
|
|
15
|
-
'_session',
|
|
16
|
-
'_xsrf',
|
|
17
|
-
'access_token',
|
|
18
|
-
'aiohttp_session',
|
|
19
|
-
'api_key',
|
|
20
|
-
'apisecret',
|
|
21
|
-
'apisignature',
|
|
22
|
-
'applicationkey',
|
|
23
|
-
'appkey',
|
|
24
|
-
'auth',
|
|
25
|
-
'authtoken',
|
|
26
|
-
'authorization',
|
|
27
|
-
'cc_number',
|
|
28
|
-
'certificatepin',
|
|
29
|
-
'cipher',
|
|
30
|
-
'client_secret',
|
|
31
|
-
'clientid',
|
|
32
|
-
'connect.sid',
|
|
33
|
-
'connectionstring',
|
|
34
|
-
'cookie',
|
|
35
|
-
'credentials',
|
|
36
|
-
'creditcard',
|
|
37
|
-
'csrf',
|
|
38
|
-
'csrf_token',
|
|
39
|
-
'cvv',
|
|
40
|
-
'databaseurl',
|
|
41
|
-
'db_url',
|
|
42
|
-
'encryption_key',
|
|
43
|
-
'encryptionkeyid',
|
|
44
|
-
'geo_location',
|
|
45
|
-
'gpg_key',
|
|
46
|
-
'ip_address',
|
|
47
|
-
'jti',
|
|
48
|
-
'jwt',
|
|
49
|
-
'license_key',
|
|
50
|
-
'masterkey',
|
|
51
|
-
'mysql_pwd',
|
|
52
|
-
'nonce',
|
|
53
|
-
'oauth',
|
|
54
|
-
'oauthtoken',
|
|
55
|
-
'otp',
|
|
56
|
-
'passhash',
|
|
57
|
-
'passwd',
|
|
58
|
-
'password',
|
|
59
|
-
'passwordb',
|
|
60
|
-
'pem_file',
|
|
61
|
-
'pgp_key',
|
|
62
|
-
'PHPSESSID',
|
|
63
|
-
'pin',
|
|
64
|
-
'pincode',
|
|
65
|
-
'pkcs8',
|
|
66
|
-
'private_key',
|
|
67
|
-
'publickey',
|
|
68
|
-
'pwd',
|
|
69
|
-
'recaptcha_key',
|
|
70
|
-
'refresh_token',
|
|
71
|
-
'routingnumber',
|
|
72
|
-
'salt',
|
|
73
|
-
'secret',
|
|
74
|
-
'secretKey',
|
|
75
|
-
'secrettoken',
|
|
76
|
-
'securitycode',
|
|
77
|
-
'security_answer',
|
|
78
|
-
'security_question',
|
|
79
|
-
'serviceaccountcredentials',
|
|
80
|
-
'session',
|
|
81
|
-
'sessionid',
|
|
82
|
-
'sessionkey',
|
|
83
|
-
'set_cookie',
|
|
84
|
-
'signature',
|
|
85
|
-
'signaturekey',
|
|
86
|
-
'ssh_key',
|
|
87
|
-
'ssn',
|
|
88
|
-
'symfony',
|
|
89
|
-
'token',
|
|
90
|
-
'transactionid',
|
|
91
|
-
'twilio_token',
|
|
92
|
-
'user_session',
|
|
93
|
-
'voterid',
|
|
94
|
-
'x-auth-token',
|
|
95
|
-
'x_api_key',
|
|
96
|
-
'x_csrftoken',
|
|
97
|
-
'x_forwarded_for',
|
|
98
|
-
'x_real_ip',
|
|
99
|
-
'XSRF-TOKEN',
|
|
100
|
-
...config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS,
|
|
101
|
-
]
|
|
102
|
-
.map(normalizeIdentifier)
|
|
103
|
-
.filter(isIncludedIdentifier)
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* @param {string} name
|
|
108
|
-
*/
|
|
109
|
-
function normalizeIdentifier (name) {
|
|
110
|
-
return normalizeName(name)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @param {string} name
|
|
115
|
-
*/
|
|
116
|
-
function isIncludedIdentifier (name) {
|
|
117
|
-
return !excludedIdentifiers.has(name)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function normalizeName (name, isSymbol) {
|
|
121
|
-
if (isSymbol) name = name.slice(7, -1) // Remove `Symbol(` and `)`
|
|
122
|
-
return name.toLowerCase().replaceAll(/[-_@$.]/g, '')
|
|
123
|
-
}
|
|
4
|
+
const { createIsRedactedIdentifier } = require('../../redaction')
|
|
124
5
|
|
|
125
6
|
module.exports = {
|
|
126
|
-
|
|
127
|
-
normalizeName,
|
|
7
|
+
isRedactedIdentifier: createIsRedactedIdentifier(config.dynamicInstrumentation),
|
|
128
8
|
}
|
|
@@ -32,8 +32,6 @@ const SKIPPED_REASON_NAMES = ['rateLimitGlobal', 'rateLimitProbe', 'evaluationTi
|
|
|
32
32
|
/** Complete events dropped before transport (`events.dropped`) */
|
|
33
33
|
const DROPPED_REASON = Object.freeze({
|
|
34
34
|
QUEUE_FULL: 0,
|
|
35
|
-
// The spec allows either trimming oversized payloads or dropping them whole. This tracer trims, and reports
|
|
36
|
-
// `capture.incomplete{payloadTooLarge}` instead, so this reason exists for spec parity but is never emitted.
|
|
37
35
|
PAYLOAD_TOO_LARGE: 1,
|
|
38
36
|
})
|
|
39
37
|
const DROPPED_REASON_NAMES = ['queueFull', 'payloadTooLarge']
|
|
@@ -13,11 +13,12 @@ const {
|
|
|
13
13
|
DEBUGGER_DIAGNOSTICS_V1,
|
|
14
14
|
DEBUGGER_INPUT_DIRECT,
|
|
15
15
|
DEBUGGER_INPUT_V2,
|
|
16
|
-
GUARDRAIL_METRICS_FLUSH_INTERVAL_MS,
|
|
17
16
|
INSPECT_SEGMENT_GLOBAL_PROPERTY,
|
|
17
|
+
METRICS_FLUSH_INTERVAL_MS,
|
|
18
18
|
} = require('./constants')
|
|
19
19
|
const { GuardrailMetrics, TELEMETRY_NAMESPACE } = require('./guardrail-metrics')
|
|
20
|
-
const {
|
|
20
|
+
const { PauseDurationHistogram } = require('./pause-duration-histogram')
|
|
21
|
+
const { configureProbeSampler, installProbeSampler, uninstallProbeSampler } = require('./probe_sampler')
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* @typedef {ReturnType<import('../config')>} Config
|
|
@@ -40,7 +41,9 @@ let rc = null
|
|
|
40
41
|
let inputPath = null
|
|
41
42
|
/** @type {GuardrailMetrics | null} */
|
|
42
43
|
let guardrailMetrics = null
|
|
43
|
-
|
|
44
|
+
/** @type {PauseDurationHistogram | null} */
|
|
45
|
+
let pauseDurations = null
|
|
46
|
+
let metricsFlushTimer = null
|
|
44
47
|
|
|
45
48
|
// eslint-disable-next-line eslint-rules/eslint-process-env
|
|
46
49
|
const { NODE_OPTIONS, ...env } = process.env
|
|
@@ -86,15 +89,20 @@ function start (config, rcInstance) {
|
|
|
86
89
|
|
|
87
90
|
const debuggerGlobals = globalThis[Symbol.for('dd-trace')]
|
|
88
91
|
debuggerGlobals.utilTypes = types
|
|
89
|
-
|
|
92
|
+
const createInspectSegment = require('./inspect-segment')
|
|
93
|
+
const { createIsRedactedIdentifier } = require('./redaction')
|
|
94
|
+
debuggerGlobals[INSPECT_SEGMENT_GLOBAL_PROPERTY] =
|
|
95
|
+
createInspectSegment(createIsRedactedIdentifier(config.dynamicInstrumentation))
|
|
90
96
|
|
|
91
97
|
const guardrailMetricsBuffer = GuardrailMetrics.createBuffer()
|
|
92
98
|
guardrailMetrics = new GuardrailMetrics(guardrailMetricsBuffer)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
const pauseDurationBuffer = PauseDurationHistogram.createBuffer()
|
|
100
|
+
pauseDurations = new PauseDurationHistogram(pauseDurationBuffer)
|
|
101
|
+
metricsFlushTimer = setInterval(flushMetrics, METRICS_FLUSH_INTERVAL_MS)
|
|
102
|
+
metricsFlushTimer.unref?.()
|
|
103
|
+
dc.subscribe(TELEMETRY_APP_CLOSING_CHANNEL, flushMetrics)
|
|
96
104
|
|
|
97
|
-
const probeSamplerBuffer = installProbeSampler(guardrailMetrics)
|
|
105
|
+
const probeSamplerBuffer = installProbeSampler(guardrailMetrics, config)
|
|
98
106
|
|
|
99
107
|
readProbeFile(config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE, (probes) => {
|
|
100
108
|
const action = 'apply'
|
|
@@ -108,7 +116,8 @@ function start (config, rcInstance) {
|
|
|
108
116
|
probeChannel.port2.postMessage({ action, probe, ackId })
|
|
109
117
|
})
|
|
110
118
|
|
|
111
|
-
probeChannel.port2.on('message', ({ ackId, error }) => {
|
|
119
|
+
probeChannel.port2.on('message', ({ ackId, error, reason }) => {
|
|
120
|
+
if (error && reason !== undefined) logWorkerError(error, reason)
|
|
112
121
|
const ack = rcAckCallbacks.get(ackId)
|
|
113
122
|
if (ack === undefined) {
|
|
114
123
|
// This should never happen, but just in case something changes in the future, we should guard against it
|
|
@@ -143,6 +152,7 @@ function start (config, rcInstance) {
|
|
|
143
152
|
configPort: configChannel.port1,
|
|
144
153
|
probeSamplerBuffer,
|
|
145
154
|
guardrailMetricsBuffer,
|
|
155
|
+
pauseDurationBuffer,
|
|
146
156
|
},
|
|
147
157
|
transferList: [probeChannel.port1, logChannel.port1, configChannel.port1],
|
|
148
158
|
}
|
|
@@ -155,17 +165,14 @@ function start (config, rcInstance) {
|
|
|
155
165
|
)
|
|
156
166
|
})
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
.distribution('execution.pause.duration')
|
|
160
|
-
worker.on('message', (/** @type {{ type: string, durationMs: number }} */ { type, durationMs }) => {
|
|
161
|
-
if (type === 'thread-paused') threadPausedMetric.track(durationMs)
|
|
162
|
-
})
|
|
163
|
-
worker.on('error', (err) => log.error('[debugger] worker thread error', err))
|
|
168
|
+
worker.on('error', (err) => logWorkerError(err))
|
|
164
169
|
worker.on('messageerror', (err) => log.error('[debugger] received "messageerror" from worker', err))
|
|
165
170
|
|
|
166
171
|
worker.once('exit', (code) => {
|
|
167
172
|
const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`)
|
|
168
|
-
|
|
173
|
+
// Telemetry omits printf arguments, so the numeric exit code must be part of the message.
|
|
174
|
+
// eslint-disable-next-line eslint-rules/eslint-log-printf-style
|
|
175
|
+
log.error(() => `[debugger] worker thread exited unexpectedly exit_code=${code}`, error)
|
|
169
176
|
cleanup(error) // Be nice, clean up now that the worker thread encountered an issue and we can't continue
|
|
170
177
|
})
|
|
171
178
|
|
|
@@ -179,6 +186,24 @@ function start (config, rcInstance) {
|
|
|
179
186
|
})
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Failure metadata belongs in the telemetry message; exception messages remain in the redacted cause.
|
|
191
|
+
*
|
|
192
|
+
* @param {Error & { code?: unknown, reason?: unknown }} error - The worker failure
|
|
193
|
+
* @param {unknown} [reason] - Explicit reason preserved across a probe acknowledgement's structured clone
|
|
194
|
+
*/
|
|
195
|
+
function logWorkerError (error, reason = error.reason) {
|
|
196
|
+
// Telemetry omits printf arguments, so the failure metadata must be part of the message.
|
|
197
|
+
// eslint-disable-next-line eslint-rules/eslint-log-printf-style
|
|
198
|
+
log.error(() => `[debugger] worker thread error name=${
|
|
199
|
+
typeof error.name === 'string' ? error.name : 'unknown'
|
|
200
|
+
} code=${
|
|
201
|
+
typeof error.code === 'string' ? error.code : 'unknown'
|
|
202
|
+
} reason=${
|
|
203
|
+
typeof reason === 'string' ? reason : 'unknown'
|
|
204
|
+
}`, error)
|
|
205
|
+
}
|
|
206
|
+
|
|
182
207
|
/**
|
|
183
208
|
* Reconfigure the Debugger worker with updated settings.
|
|
184
209
|
* Sends the new configuration to the worker thread via the config channel.
|
|
@@ -193,6 +218,7 @@ function configure (config) {
|
|
|
193
218
|
log.error('[debugger] Invalid DD_SITE for agentless Dynamic Instrumentation: %s', config.site)
|
|
194
219
|
return
|
|
195
220
|
}
|
|
221
|
+
configureProbeSampler(config)
|
|
196
222
|
configChannel.port2.postMessage(debuggerConfig)
|
|
197
223
|
}
|
|
198
224
|
|
|
@@ -234,18 +260,19 @@ function cleanup (error) {
|
|
|
234
260
|
configChannel = null
|
|
235
261
|
inputPath = null
|
|
236
262
|
|
|
237
|
-
if (
|
|
238
|
-
clearInterval(
|
|
239
|
-
|
|
263
|
+
if (metricsFlushTimer !== null) {
|
|
264
|
+
clearInterval(metricsFlushTimer)
|
|
265
|
+
metricsFlushTimer = null
|
|
240
266
|
}
|
|
241
267
|
if (guardrailMetrics !== null) {
|
|
242
|
-
dc.unsubscribe(TELEMETRY_APP_CLOSING_CHANNEL,
|
|
243
|
-
// Report what the worker
|
|
244
|
-
// worker asynchronously, so anything it
|
|
245
|
-
// concerns events still sitting in the worker's upload buffer, which die with the worker anyway,
|
|
246
|
-
// deferring the drain until the worker has exited.
|
|
247
|
-
|
|
268
|
+
dc.unsubscribe(TELEMETRY_APP_CLOSING_CHANNEL, flushMetrics)
|
|
269
|
+
// Report what the worker recorded up until it was stopped. Known limitation: `Worker#terminate()` interrupts the
|
|
270
|
+
// worker asynchronously, so anything it records between this drain and its actual termination is lost. That only
|
|
271
|
+
// concerns events still sitting in the worker's upload buffer, which die with the worker anyway, and a pause that
|
|
272
|
+
// might still be in progress, so it isn't worth deferring the drain until the worker has exited.
|
|
273
|
+
flushMetrics()
|
|
248
274
|
guardrailMetrics = null
|
|
275
|
+
pauseDurations = null
|
|
249
276
|
}
|
|
250
277
|
|
|
251
278
|
// Call any pending ack callbacks
|
|
@@ -261,14 +288,19 @@ function cleanup (error) {
|
|
|
261
288
|
}
|
|
262
289
|
|
|
263
290
|
/**
|
|
264
|
-
* Convert the guardrail counters accumulated by the probe sampler and the worker
|
|
291
|
+
* Convert the guardrail counters accumulated by the probe sampler and the worker, and the pause durations recorded by
|
|
292
|
+
* the worker, into telemetry metrics.
|
|
265
293
|
*/
|
|
266
|
-
function
|
|
267
|
-
if (guardrailMetrics === null) return
|
|
294
|
+
function flushMetrics () {
|
|
295
|
+
if (guardrailMetrics === null || pauseDurations === null) return
|
|
268
296
|
const namespace = telemetryMetrics.manager.namespace(TELEMETRY_NAMESPACE)
|
|
269
297
|
guardrailMetrics.drain((metric, tags, count) => {
|
|
270
298
|
namespace.count(metric, tags).inc(count)
|
|
271
299
|
})
|
|
300
|
+
const pauseDurationMetric = namespace.distribution('execution.pause.duration')
|
|
301
|
+
pauseDurations.drain((durationMs, count) => {
|
|
302
|
+
pauseDurationMetric.track(durationMs, count)
|
|
303
|
+
})
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
/**
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
const { inspect, types } = require('node:util')
|
|
4
4
|
|
|
5
5
|
const { NODE_MAJOR } = require('../../../../version')
|
|
6
|
+
const { REDACTED_PLACEHOLDER } = require('./redaction')
|
|
6
7
|
|
|
7
8
|
/** @typedef {NonNullable<ReturnType<typeof globalThis.Object.getOwnPropertyDescriptor>>} PropertyDescriptor */
|
|
8
9
|
/** @typedef {Map<unknown, unknown> | Set<unknown>} Collection */
|
|
10
|
+
/** @typedef {(name: string) => boolean} IsRedactedIdentifier */
|
|
9
11
|
|
|
10
12
|
const mapEntries = Map.prototype.entries
|
|
13
|
+
const mapKeys = Map.prototype.keys
|
|
11
14
|
const mapSet = Map.prototype.set
|
|
12
15
|
const mapSizeGetter = Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
|
|
13
16
|
const setAdd = Set.prototype.add
|
|
@@ -25,23 +28,34 @@ const segmentInspectOptions = {
|
|
|
25
28
|
maxStringLength: 8 * 1024,
|
|
26
29
|
breakLength: Infinity,
|
|
27
30
|
}
|
|
31
|
+
const redactedDescriptor = { value: REDACTED_PLACEHOLDER, enumerable: true }
|
|
28
32
|
|
|
29
|
-
module.exports =
|
|
33
|
+
module.exports = createInspectSegment
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {IsRedactedIdentifier} isRedactedIdentifier - Whether the value of a property or Map entry with the given key
|
|
37
|
+
* must be redacted.
|
|
38
|
+
*/
|
|
39
|
+
function createInspectSegment (isRedactedIdentifier) {
|
|
40
|
+
return (/** @type {unknown} */ value) => inspectSegment(value, isRedactedIdentifier)
|
|
41
|
+
}
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
44
|
* Inspect a dynamic-instrumentation template value without invoking user code.
|
|
33
45
|
* Unlike collections, `util.inspect` has no option for limiting the number of object properties, so this function
|
|
34
|
-
* truncates objects before inspecting them.
|
|
46
|
+
* truncates objects before inspecting them. It also replaces the values of redacted properties and Map entries, the
|
|
47
|
+
* same way they are redacted from snapshots.
|
|
35
48
|
*
|
|
36
49
|
* @param {unknown} value
|
|
50
|
+
* @param {IsRedactedIdentifier} isRedactedIdentifier
|
|
37
51
|
*/
|
|
38
|
-
function inspectSegment (value) {
|
|
52
|
+
function inspectSegment (value, isRedactedIdentifier) {
|
|
39
53
|
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
40
54
|
return inspect(value, segmentInspectOptions)
|
|
41
55
|
}
|
|
42
56
|
if (types.isProxy(value)) return '[Proxy]'
|
|
43
|
-
if (types.isMap(value)) return inspectCollection(value, true)
|
|
44
|
-
if (types.isSet(value)) return inspectCollection(value, false)
|
|
57
|
+
if (types.isMap(value)) return inspectCollection(value, true, isRedactedIdentifier)
|
|
58
|
+
if (types.isSet(value)) return inspectCollection(value, false, isRedactedIdentifier)
|
|
45
59
|
if (
|
|
46
60
|
Array.isArray(value) ||
|
|
47
61
|
types.isTypedArray(value) ||
|
|
@@ -72,11 +86,18 @@ function inspectSegment (value) {
|
|
|
72
86
|
if (inspectionCanRunUserCode(value)) {
|
|
73
87
|
return '[Value omitted: inspection may execute user code]'
|
|
74
88
|
}
|
|
75
|
-
return inspect(value, segmentInspectOptions)
|
|
89
|
+
if (!hasRedactedKey(keys, isRedactedIdentifier)) return inspect(value, segmentInspectOptions)
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
// Inspect a plain object holding the rendered properties instead, with the values of redacted properties replaced.
|
|
93
|
+
// The copy doesn't keep the prototype, as built-ins like `URL` can't be inspected without their internal slots.
|
|
94
|
+
const copy = {}
|
|
95
|
+
const copiedCount = Math.min(propertyCount, maxProperties)
|
|
96
|
+
for (let i = 0; i < copiedCount; i++) {
|
|
97
|
+
if (isRedactedKey(keys[i], isRedactedIdentifier)) {
|
|
98
|
+
Object.defineProperty(copy, keys[i], redactedDescriptor)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
80
101
|
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, keys[i]))
|
|
81
102
|
if (
|
|
82
103
|
(keys[i] === Symbol.toStringTag && descriptor.get !== undefined) ||
|
|
@@ -84,28 +105,34 @@ function inspectSegment (value) {
|
|
|
84
105
|
) {
|
|
85
106
|
return '[Value omitted: inspection may execute user code]'
|
|
86
107
|
}
|
|
87
|
-
if (descriptor.value === value) descriptor.value =
|
|
88
|
-
Object.defineProperty(
|
|
108
|
+
if (descriptor.value === value) descriptor.value = copy
|
|
109
|
+
Object.defineProperty(copy, keys[i], descriptor)
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
const inspected = inspect(copy, segmentInspectOptions)
|
|
113
|
+
if (propertyCount <= maxProperties) return inspected
|
|
114
|
+
|
|
91
115
|
const omitted = propertyCount - maxProperties
|
|
92
|
-
const inspected = inspect(truncated, segmentInspectOptions)
|
|
93
116
|
return `${inspected.slice(0, -2)}, ... ${omitted} more ${omitted === 1 ? 'property' : 'properties'} }`
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
/**
|
|
97
|
-
* Inspect a Map or Set while bounding the number of entries on Node.js 18, where `util.inspect` does not
|
|
120
|
+
* Inspect a Map or Set while bounding the number of entries on Node.js 18, where `util.inspect` does not, and replacing
|
|
121
|
+
* the values of redacted Map entries.
|
|
98
122
|
*
|
|
99
123
|
* @param {Collection} value
|
|
100
124
|
* @param {boolean} isMap
|
|
125
|
+
* @param {IsRedactedIdentifier} isRedactedIdentifier
|
|
101
126
|
*/
|
|
102
|
-
function inspectCollection (value, isMap) {
|
|
103
|
-
if (NODE_MAJOR !== 18) return inspect(value, segmentInspectOptions)
|
|
104
|
-
|
|
127
|
+
function inspectCollection (value, isMap, isRedactedIdentifier) {
|
|
105
128
|
const size = (isMap ? mapSizeGetter : setSizeGetter).call(value)
|
|
106
|
-
|
|
129
|
+
const truncate = NODE_MAJOR === 18 && size > maxCollectionEntries
|
|
130
|
+
if (!truncate && !(isMap && mapHasRedactedKey(/** @type {Map<unknown, unknown>} */ (value), isRedactedIdentifier))) {
|
|
131
|
+
return inspect(value, segmentInspectOptions)
|
|
132
|
+
}
|
|
107
133
|
|
|
108
|
-
|
|
134
|
+
// Only the entries that are rendered are copied
|
|
135
|
+
const copy = isMap ? new Map() : new Set()
|
|
109
136
|
const iterator = (isMap ? mapEntries : setValues).call(value)
|
|
110
137
|
const iteratorNext = isMap ? mapIteratorNext : setIteratorNext
|
|
111
138
|
|
|
@@ -115,22 +142,64 @@ function inspectCollection (value, isMap) {
|
|
|
115
142
|
|
|
116
143
|
if (isMap) {
|
|
117
144
|
const entry = result.value
|
|
118
|
-
const key = entry[0] === value ?
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
const key = entry[0] === value ? copy : entry[0]
|
|
146
|
+
let entryValue = entry[1] === value ? copy : entry[1]
|
|
147
|
+
if (isRedactedKey(entry[0], isRedactedIdentifier)) entryValue = REDACTED_PLACEHOLDER
|
|
148
|
+
mapSet.call(copy, key, entryValue)
|
|
121
149
|
} else {
|
|
122
|
-
const entryValue = result.value === value ?
|
|
123
|
-
setAdd.call(
|
|
150
|
+
const entryValue = result.value === value ? copy : result.value
|
|
151
|
+
setAdd.call(copy, entryValue)
|
|
124
152
|
}
|
|
125
153
|
}
|
|
126
154
|
|
|
155
|
+
const inspected = inspect(copy, segmentInspectOptions)
|
|
156
|
+
if (size <= maxCollectionEntries) return inspected
|
|
157
|
+
|
|
127
158
|
const type = isMap ? 'Map' : 'Set'
|
|
128
|
-
const inspected = inspect(truncated, segmentInspectOptions)
|
|
129
159
|
const normalized = inspected.replace(`${type}(${maxCollectionEntries})`, `${type}(${size})`)
|
|
130
160
|
const remaining = size - maxCollectionEntries
|
|
131
161
|
return `${normalized.slice(0, -2)}, ... ${remaining} more item${remaining === 1 ? '' : 's'} }`
|
|
132
162
|
}
|
|
133
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Determine whether any of the Map entries rendered by `util.inspect` has a redacted key.
|
|
166
|
+
*
|
|
167
|
+
* @param {Map<unknown, unknown>} map
|
|
168
|
+
* @param {IsRedactedIdentifier} isRedactedIdentifier
|
|
169
|
+
*/
|
|
170
|
+
function mapHasRedactedKey (map, isRedactedIdentifier) {
|
|
171
|
+
const iterator = mapKeys.call(map)
|
|
172
|
+
for (let i = 0; i < maxCollectionEntries; i++) {
|
|
173
|
+
const result = mapIteratorNext.call(iterator)
|
|
174
|
+
if (result.done) return false
|
|
175
|
+
if (isRedactedKey(result.value, isRedactedIdentifier)) return true
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {(string | symbol)[]} keys
|
|
182
|
+
* @param {IsRedactedIdentifier} isRedactedIdentifier
|
|
183
|
+
*/
|
|
184
|
+
function hasRedactedKey (keys, isRedactedIdentifier) {
|
|
185
|
+
for (let i = 0; i < keys.length; i++) {
|
|
186
|
+
if (isRedactedKey(keys[i], isRedactedIdentifier)) return true
|
|
187
|
+
}
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Determine whether a property or Map key is redacted. Like in snapshots, only string and symbol keys can be redacted.
|
|
193
|
+
*
|
|
194
|
+
* @param {unknown} key
|
|
195
|
+
* @param {IsRedactedIdentifier} isRedactedIdentifier
|
|
196
|
+
*/
|
|
197
|
+
function isRedactedKey (key, isRedactedIdentifier) {
|
|
198
|
+
if (typeof key === 'string') return isRedactedIdentifier(key)
|
|
199
|
+
if (typeof key === 'symbol') return isRedactedIdentifier(key.description ?? '')
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
|
|
134
203
|
/**
|
|
135
204
|
* Determine whether inspecting a value could invoke a proxy trap or toStringTag getter.
|
|
136
205
|
*
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Durations of the pauses caused by probes, shared between the debugger worker thread and the main thread.
|
|
7
|
+
*
|
|
8
|
+
* The worker records the duration of every pause, while the main thread converts them into a telemetry distribution.
|
|
9
|
+
* The main thread can't process a message while the application keeps it busy, for example in a synchronous loop that
|
|
10
|
+
* hits a probe on every iteration, so a message per pause would queue up without bound. Instead the worker counts each
|
|
11
|
+
* duration in a fixed set of buckets in a shared buffer using atomics, and the main thread periodically drains the
|
|
12
|
+
* buckets. Memory use is therefore constant no matter how many pauses happen between two drains.
|
|
13
|
+
*
|
|
14
|
+
* The buckets are the keys of the mapping used by the DDSketch backing telemetry distributions. Each drained bucket is
|
|
15
|
+
* reported as the value the sketch maps back to the same key, so the resulting distribution is as accurate as tracking
|
|
16
|
+
* each duration individually. Durations outside of the covered range are counted in the closest bucket.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Created the same way as the sketch of telemetry distributions, so both map durations to the same keys
|
|
20
|
+
const { mapping } = new LogCollapsingLowestDenseDDSketch()
|
|
21
|
+
|
|
22
|
+
const MIN_KEY = mapping.key(0.001) // 1 µs
|
|
23
|
+
const MAX_KEY = mapping.key(60 * 60 * 1000) // 1 hour
|
|
24
|
+
const BUCKET_COUNT = MAX_KEY - MIN_KEY + 1
|
|
25
|
+
|
|
26
|
+
class PauseDurationHistogram {
|
|
27
|
+
/** @type {Uint32Array} */
|
|
28
|
+
#counts
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {SharedArrayBuffer} buffer - A buffer created with {@link PauseDurationHistogram.createBuffer}
|
|
32
|
+
*/
|
|
33
|
+
constructor (buffer) {
|
|
34
|
+
this.#counts = new Uint32Array(buffer)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create the shared buffer backing the histogram.
|
|
39
|
+
*
|
|
40
|
+
* @returns {SharedArrayBuffer}
|
|
41
|
+
*/
|
|
42
|
+
static createBuffer () {
|
|
43
|
+
return new SharedArrayBuffer(BUCKET_COUNT * Uint32Array.BYTES_PER_ELEMENT)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Record the duration of a single pause.
|
|
48
|
+
*
|
|
49
|
+
* @param {number} durationMs - The pause duration, in milliseconds
|
|
50
|
+
*/
|
|
51
|
+
record (durationMs) {
|
|
52
|
+
const key = Math.min(Math.max(mapping.key(durationMs), MIN_KEY), MAX_KEY)
|
|
53
|
+
Atomics.add(this.#counts, key - MIN_KEY, 1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reset all buckets and report the ones that were non-empty.
|
|
58
|
+
*
|
|
59
|
+
* @param {(durationMs: number, count: number) => void} report - Called once per non-empty bucket with the duration
|
|
60
|
+
* representing the bucket and the number of pauses counted in it
|
|
61
|
+
*/
|
|
62
|
+
drain (report) {
|
|
63
|
+
for (let i = 0; i < BUCKET_COUNT; i++) {
|
|
64
|
+
const count = Atomics.exchange(this.#counts, i, 0)
|
|
65
|
+
if (count !== 0) {
|
|
66
|
+
report(mapping.value(MIN_KEY + i), count)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
PauseDurationHistogram,
|
|
74
|
+
}
|