dd-trace 6.18.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/LICENSE-3rdparty.csv +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +166 -24
- package/package.json +7 -7
- package/packages/datadog-esbuild/index.js +9 -6
- package/packages/datadog-instrumentations/src/ai.js +26 -0
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +40 -4
- 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-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/playwright.js +26 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +19 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
- package/packages/dd-trace/src/config/remote_config.js +12 -0
- package/packages/dd-trace/src/config/supported-configurations.json +26 -5
- package/packages/dd-trace/src/constants.js +2 -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 +33 -8
- 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/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
- package/packages/dd-trace/src/llmobs/sdk.js +19 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
- package/packages/dd-trace/src/llmobs/tagger.js +37 -36
- 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/index.js +5 -0
- 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/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/priority_sampler.js +155 -33
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
- package/packages/dd-trace/src/ritm.js +12 -9
- 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 +32 -3
- 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/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/postgres.js +0 -7
- package/packages/datadog-instrumentations/src/supabase.js +0 -15
|
@@ -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
|
+
}
|
|
@@ -20,28 +20,62 @@ const ddTraceGlobal = /** @type {Record<symbol, SharedArrayBuffer | object | und
|
|
|
20
20
|
/** @type {Record<symbol, unknown>} */ (globalThis)[Symbol.for(DD_TRACE_SYMBOL)]
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} ProbeThrottle
|
|
25
|
+
* @property {bigint} untilNs - The probe is skipped at entry until this point in time
|
|
26
|
+
* @property {boolean} timedOut - Whether the throttle was caused by an evaluation exceeding its time budget
|
|
27
|
+
* @property {string | undefined} error - A condition error not yet handed over to the worker
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} ProbeSamplerConfig
|
|
32
|
+
* @property {{ DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS: number }} dynamicInstrumentation
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
let evaluationTimeoutNs = 0n
|
|
36
|
+
|
|
23
37
|
module.exports = {
|
|
38
|
+
configureProbeSampler,
|
|
24
39
|
installProbeSampler,
|
|
25
40
|
uninstallProbeSampler,
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Apply the evaluation time budget from the tracer configuration.
|
|
45
|
+
*
|
|
46
|
+
* @param {ProbeSamplerConfig} config - The tracer configuration.
|
|
47
|
+
*/
|
|
48
|
+
function configureProbeSampler (config) {
|
|
49
|
+
evaluationTimeoutNs = BigInt(config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS) *
|
|
50
|
+
1_000_000n
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
/**
|
|
29
54
|
* Install the runtime sampler in the debuggee context.
|
|
30
55
|
*
|
|
31
56
|
* @param {import('./guardrail-metrics').GuardrailMetrics} guardrailMetrics - Counters for skipped probe hits.
|
|
57
|
+
* @param {ProbeSamplerConfig} config - The tracer configuration.
|
|
32
58
|
* @returns {SharedArrayBuffer} The shared sampler buffer to pass to the debugger worker.
|
|
33
59
|
*/
|
|
34
|
-
function installProbeSampler (guardrailMetrics) {
|
|
60
|
+
function installProbeSampler (guardrailMetrics, config) {
|
|
61
|
+
configureProbeSampler(config)
|
|
62
|
+
|
|
35
63
|
const buffer = createProbeSamplerBuffer()
|
|
36
64
|
|
|
37
65
|
const lastCaptureNsByProbeId = new Map()
|
|
38
66
|
/**
|
|
39
|
-
* Probes
|
|
40
|
-
*
|
|
67
|
+
* Probes that are skipped at entry because a recent evaluation failed or exceeded its time budget. One error result
|
|
68
|
+
* is reported per throttle window, so the probe stays visible without repeatedly paying for the evaluation.
|
|
41
69
|
*
|
|
42
|
-
* @type {Map<string,
|
|
70
|
+
* @type {Map<string, ProbeThrottle>}
|
|
71
|
+
*/
|
|
72
|
+
const throttleByProbeId = new Map()
|
|
73
|
+
/**
|
|
74
|
+
* When the condition currently being evaluated started. V8 doesn't evaluate breakpoint conditions re-entrantly, and
|
|
75
|
+
* the conditions of probes at the same location are evaluated one after another, so one start time is enough. It is
|
|
76
|
+
* kept here rather than in the compiled condition so the condition can't shadow a variable of the paused frame.
|
|
43
77
|
*/
|
|
44
|
-
|
|
78
|
+
let conditionStartNs = 0n
|
|
45
79
|
const sampledProbeIndexes = new Int32Array(buffer)
|
|
46
80
|
Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 0)
|
|
47
81
|
Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 0)
|
|
@@ -52,7 +86,7 @@ function installProbeSampler (guardrailMetrics) {
|
|
|
52
86
|
|
|
53
87
|
ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)] = {
|
|
54
88
|
/**
|
|
55
|
-
* Decide if a probe should be sampled and store sampled probe indexes for the debugger worker.
|
|
89
|
+
* Decide if a probe without a condition should be sampled and store sampled probe indexes for the debugger worker.
|
|
56
90
|
*
|
|
57
91
|
* @param {number} probeIndex - The worker-side probe sampling index.
|
|
58
92
|
* @param {string} probeId - The probe id.
|
|
@@ -61,55 +95,46 @@ function installProbeSampler (guardrailMetrics) {
|
|
|
61
95
|
*/
|
|
62
96
|
makeSampleDecision (probeIndex, probeId, nsBetweenSampling, isSnapshotProducingProbe) {
|
|
63
97
|
const now = process.hrtime.bigint()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
SKIPPED_REASON.RATE_LIMIT_PROBE,
|
|
68
|
-
isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
|
|
69
|
-
)
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let shouldResetGlobalSnapshotRateWindow = false
|
|
74
|
-
if (isSnapshotProducingProbe === true) {
|
|
75
|
-
if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
|
|
76
|
-
shouldResetGlobalSnapshotRateWindow = true
|
|
77
|
-
} else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
|
|
78
|
-
guardrailMetrics.eventSkipped(SKIPPED_REASON.RATE_LIMIT_GLOBAL, EVENT_TYPE.SNAPSHOT)
|
|
79
|
-
return false
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!storeSampledProbeIndex(probeIndex)) return false
|
|
84
|
-
|
|
85
|
-
if (isSnapshotProducingProbe === true) {
|
|
86
|
-
if (shouldResetGlobalSnapshotRateWindow === true) {
|
|
87
|
-
snapshotsSampledWithinTheLastSecond = 1
|
|
88
|
-
globalSnapshotSamplingRateWindowStart = now
|
|
89
|
-
} else {
|
|
90
|
-
snapshotsSampledWithinTheLastSecond++
|
|
91
|
-
}
|
|
92
|
-
}
|
|
98
|
+
if (isThrottled(probeId, now, isSnapshotProducingProbe)) return false
|
|
99
|
+
return sample(probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe)
|
|
100
|
+
},
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Decide if a probe's condition should be evaluated, or skipped because a recent evaluation error throttled it. If
|
|
104
|
+
* it should be evaluated, this starts timing the evaluation against the evaluation time budget.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} probeId - The probe id.
|
|
107
|
+
* @param {boolean} isSnapshotProducingProbe - Whether this probe produces snapshots.
|
|
108
|
+
*/
|
|
109
|
+
shouldEvaluateCondition (probeId, isSnapshotProducingProbe) {
|
|
110
|
+
const now = process.hrtime.bigint()
|
|
111
|
+
if (isThrottled(probeId, now, isSnapshotProducingProbe)) return false
|
|
112
|
+
conditionStartNs = now
|
|
95
113
|
return true
|
|
96
114
|
},
|
|
97
115
|
|
|
98
116
|
/**
|
|
99
|
-
* Decide if a probe
|
|
117
|
+
* Decide if a probe should be sampled now that its condition has been evaluated. A condition that exceeded the
|
|
118
|
+
* evaluation time budget is reported like a condition error instead, regardless of its result.
|
|
100
119
|
*
|
|
120
|
+
* @param {number} probeIndex - The worker-side probe sampling index.
|
|
101
121
|
* @param {string} probeId - The probe id.
|
|
122
|
+
* @param {boolean} matched - Whether the condition evaluated to `true`.
|
|
123
|
+
* @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
|
|
124
|
+
* @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
|
|
102
125
|
*/
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
conditionEvaluated (probeIndex, probeId, matched, nsBetweenSampling, isSnapshotProducingProbe) {
|
|
127
|
+
const now = process.hrtime.bigint()
|
|
128
|
+
const elapsedNs = now - conditionStartNs
|
|
129
|
+
if (elapsedNs > evaluationTimeoutNs) {
|
|
130
|
+
return recordConditionError(probeIndex, probeId, now, describeTimeout(elapsedNs), true)
|
|
131
|
+
}
|
|
132
|
+
return matched === true && sample(probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe)
|
|
109
133
|
},
|
|
110
134
|
|
|
111
135
|
/**
|
|
112
|
-
* Record that a probe's condition threw, throttle the probe, and request a pause if the error can be reported.
|
|
136
|
+
* Record that a probe's condition threw, throttle the probe, and request a pause if the error can be reported. A
|
|
137
|
+
* condition that exceeded the evaluation time budget before throwing is reported as a timeout instead.
|
|
113
138
|
*
|
|
114
139
|
* Error results bypass the per-probe and global rate limits: they are rate limited by the throttle instead, which
|
|
115
140
|
* allows one error result per probe per window. If the shared buffer is full, the error is dropped but the throttle
|
|
@@ -120,12 +145,12 @@ function installProbeSampler (guardrailMetrics) {
|
|
|
120
145
|
* @param {unknown} error - The value thrown by the condition.
|
|
121
146
|
*/
|
|
122
147
|
conditionError (probeIndex, probeId, error) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
return
|
|
148
|
+
const now = process.hrtime.bigint()
|
|
149
|
+
const elapsedNs = now - conditionStartNs
|
|
150
|
+
if (elapsedNs > evaluationTimeoutNs) {
|
|
151
|
+
return recordConditionError(probeIndex, probeId, now, describeTimeout(elapsedNs), true)
|
|
152
|
+
}
|
|
153
|
+
return recordConditionError(probeIndex, probeId, now, describeError(error), false)
|
|
129
154
|
},
|
|
130
155
|
|
|
131
156
|
/**
|
|
@@ -135,13 +160,28 @@ function installProbeSampler (guardrailMetrics) {
|
|
|
135
160
|
* @returns {string | undefined} The error description, if any.
|
|
136
161
|
*/
|
|
137
162
|
takeConditionError (probeId) {
|
|
138
|
-
const state =
|
|
163
|
+
const state = throttleByProbeId.get(probeId)
|
|
139
164
|
if (state === undefined) return
|
|
140
165
|
const { error } = state
|
|
141
166
|
state.error = undefined
|
|
142
167
|
return error
|
|
143
168
|
},
|
|
144
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Throttle a probe whose evaluation exceeded its time budget in the worker, e.g. while evaluating its template.
|
|
172
|
+
* Called by the worker after the result has been reported.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} probeId - The probe id.
|
|
175
|
+
*/
|
|
176
|
+
evaluationTimedOut (probeId) {
|
|
177
|
+
throttleByProbeId.set(probeId, {
|
|
178
|
+
untilNs: process.hrtime.bigint() + CONDITION_ERROR_THROTTLE_NS,
|
|
179
|
+
timedOut: true,
|
|
180
|
+
// A hit that raced this request may have recorded a condition error that its pause has yet to report
|
|
181
|
+
error: throttleByProbeId.get(probeId)?.error,
|
|
182
|
+
})
|
|
183
|
+
},
|
|
184
|
+
|
|
145
185
|
/**
|
|
146
186
|
* Remove cached sampling state for a probe.
|
|
147
187
|
*
|
|
@@ -149,10 +189,100 @@ function installProbeSampler (guardrailMetrics) {
|
|
|
149
189
|
*/
|
|
150
190
|
remove (probeId) {
|
|
151
191
|
lastCaptureNsByProbeId.delete(probeId)
|
|
152
|
-
|
|
192
|
+
throttleByProbeId.delete(probeId)
|
|
153
193
|
},
|
|
154
194
|
}
|
|
155
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Check if a probe is skipped at entry because of a recent evaluation error, recording the skip when the error was
|
|
198
|
+
* an exceeded time budget.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} probeId - The probe id.
|
|
201
|
+
* @param {bigint | undefined} now - The current time, if already available.
|
|
202
|
+
* @param {boolean} isSnapshotProducingProbe - Whether this probe produces snapshots.
|
|
203
|
+
*/
|
|
204
|
+
function isThrottled (probeId, now, isSnapshotProducingProbe) {
|
|
205
|
+
const state = throttleByProbeId.get(probeId)
|
|
206
|
+
if (state === undefined) return false
|
|
207
|
+
now ??= process.hrtime.bigint()
|
|
208
|
+
if (now >= state.untilNs) {
|
|
209
|
+
throttleByProbeId.delete(probeId)
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
if (state.timedOut) {
|
|
213
|
+
guardrailMetrics.eventSkipped(
|
|
214
|
+
SKIPPED_REASON.EVALUATION_TIMEOUT,
|
|
215
|
+
isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
return true
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Apply the per-probe and global rate limits and store the sampled probe index for the debugger worker.
|
|
223
|
+
*
|
|
224
|
+
* @param {number} probeIndex - The worker-side probe sampling index.
|
|
225
|
+
* @param {string} probeId - The probe id.
|
|
226
|
+
* @param {bigint} now - The current time.
|
|
227
|
+
* @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
|
|
228
|
+
* @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
|
|
229
|
+
*/
|
|
230
|
+
function sample (probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe) {
|
|
231
|
+
const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
|
|
232
|
+
if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) {
|
|
233
|
+
guardrailMetrics.eventSkipped(
|
|
234
|
+
SKIPPED_REASON.RATE_LIMIT_PROBE,
|
|
235
|
+
isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
|
|
236
|
+
)
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let shouldResetGlobalSnapshotRateWindow = false
|
|
241
|
+
if (isSnapshotProducingProbe === true) {
|
|
242
|
+
if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
|
|
243
|
+
shouldResetGlobalSnapshotRateWindow = true
|
|
244
|
+
} else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
|
|
245
|
+
guardrailMetrics.eventSkipped(SKIPPED_REASON.RATE_LIMIT_GLOBAL, EVENT_TYPE.SNAPSHOT)
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!storeSampledProbeIndex(probeIndex)) return false
|
|
251
|
+
|
|
252
|
+
if (isSnapshotProducingProbe === true) {
|
|
253
|
+
if (shouldResetGlobalSnapshotRateWindow === true) {
|
|
254
|
+
snapshotsSampledWithinTheLastSecond = 1
|
|
255
|
+
globalSnapshotSamplingRateWindowStart = now
|
|
256
|
+
} else {
|
|
257
|
+
snapshotsSampledWithinTheLastSecond++
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lastCaptureNsByProbeId.set(probeId, now)
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Throttle a probe whose condition failed and request a pause so the error can be reported. Error results bypass the
|
|
267
|
+
* per-probe and global rate limits: they are rate limited by the throttle instead, which allows one error result per
|
|
268
|
+
* probe per window.
|
|
269
|
+
*
|
|
270
|
+
* @param {number} probeIndex - The worker-side probe sampling index.
|
|
271
|
+
* @param {string} probeId - The probe id.
|
|
272
|
+
* @param {bigint} now - The current time.
|
|
273
|
+
* @param {string} error - The error description.
|
|
274
|
+
* @param {boolean} timedOut - Whether the error is an exceeded time budget.
|
|
275
|
+
*/
|
|
276
|
+
function recordConditionError (probeIndex, probeId, now, error, timedOut) {
|
|
277
|
+
const stored = storeSampledProbeIndex(probeIndex | CONDITION_ERROR_FLAG)
|
|
278
|
+
throttleByProbeId.set(probeId, {
|
|
279
|
+
untilNs: now + CONDITION_ERROR_THROTTLE_NS,
|
|
280
|
+
timedOut,
|
|
281
|
+
error: stored ? error : undefined,
|
|
282
|
+
})
|
|
283
|
+
return stored
|
|
284
|
+
}
|
|
285
|
+
|
|
156
286
|
/**
|
|
157
287
|
* Hand a sampled probe index over to the worker for the upcoming pause.
|
|
158
288
|
*
|
|
@@ -224,6 +354,16 @@ function getErrorStringProperty (error, property) {
|
|
|
224
354
|
}
|
|
225
355
|
}
|
|
226
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Describe a condition evaluation that exceeded its time budget.
|
|
359
|
+
*
|
|
360
|
+
* @param {bigint} elapsedNs - The time the evaluation took.
|
|
361
|
+
*/
|
|
362
|
+
function describeTimeout (elapsedNs) {
|
|
363
|
+
return `Condition evaluation exceeded its time budget of ${evaluationTimeoutNs / 1_000_000n}ms ` +
|
|
364
|
+
`(took ${(Number(elapsedNs) / 1_000_000).toFixed(1)}ms)`
|
|
365
|
+
}
|
|
366
|
+
|
|
227
367
|
/**
|
|
228
368
|
* Create the shared buffer used to hand sampled probe indexes from breakpoint conditions to the debugger worker.
|
|
229
369
|
*
|