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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { AUTO_KEEP } = require('../../../ext/priority')
|
|
4
|
+
const knuthHash = require('./knuth-hash')
|
|
5
|
+
const { SAMPLING_AGENT_DECISION, SAMPLING_RULE_DECISION } = require('./constants')
|
|
6
|
+
|
|
7
|
+
const MAX_THRESHOLD = 2n ** 56n
|
|
8
|
+
const MAX_ENCODABLE_THRESHOLD = MAX_THRESHOLD - 1n
|
|
9
|
+
const UINT64_MASK = 2n ** 64n - 1n
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} value
|
|
13
|
+
* @param {number} minLength
|
|
14
|
+
* @param {number} maxLength
|
|
15
|
+
* @returns {value is string}
|
|
16
|
+
*/
|
|
17
|
+
function isLowerHex (value, minLength, maxLength) {
|
|
18
|
+
if (typeof value !== 'string' || value.length < minLength || value.length > maxLength) return false
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < value.length; index++) {
|
|
21
|
+
const code = value.charCodeAt(index)
|
|
22
|
+
if ((code < 48 || code > 57) && (code < 97 || code > 102)) return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derives the OTel 56-bit random value from Datadog's sampling hash.
|
|
30
|
+
*
|
|
31
|
+
* @param {bigint} traceId
|
|
32
|
+
*/
|
|
33
|
+
function randomValueFor (traceId) {
|
|
34
|
+
return ((~knuthHash(traceId)) & UINT64_MASK) >> 8n
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts a sample rate to an OTel 56-bit rejection threshold.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} sampleRate
|
|
41
|
+
*/
|
|
42
|
+
function thresholdFor (sampleRate) {
|
|
43
|
+
if (sampleRate === 1) return 0n
|
|
44
|
+
if (sampleRate === 0) return MAX_ENCODABLE_THRESHOLD
|
|
45
|
+
|
|
46
|
+
const threshold = BigInt(Math.round((1 - sampleRate) * Number(MAX_THRESHOLD)))
|
|
47
|
+
if (threshold < 0n) return 0n
|
|
48
|
+
if (threshold > MAX_ENCODABLE_THRESHOLD) return MAX_ENCODABLE_THRESHOLD
|
|
49
|
+
return threshold
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats an OTel threshold with trailing zero nibbles removed.
|
|
54
|
+
*
|
|
55
|
+
* @param {bigint} threshold
|
|
56
|
+
*/
|
|
57
|
+
function formatThreshold (threshold) {
|
|
58
|
+
const value = threshold.toString(16).padStart(14, '0')
|
|
59
|
+
let end = value.length
|
|
60
|
+
while (end > 0 && value.charCodeAt(end - 1) === 48) end--
|
|
61
|
+
return end === 0 ? '0' : value.slice(0, end)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generates OTel sampling fields for a local probability decision.
|
|
66
|
+
*
|
|
67
|
+
* @param {import('./opentracing/span_context')} context
|
|
68
|
+
* @param {number} probabilityRate
|
|
69
|
+
* @returns {{ randomValue: string, threshold: string } | undefined}
|
|
70
|
+
*/
|
|
71
|
+
function generateFields (context, probabilityRate) {
|
|
72
|
+
const { priority } = context._sampling
|
|
73
|
+
if (priority === undefined || probabilityRate === undefined) return
|
|
74
|
+
|
|
75
|
+
const thresholdValue = thresholdFor(probabilityRate)
|
|
76
|
+
let randomValue = randomValueFor(context._traceId.toBigInt())
|
|
77
|
+
const kept = priority >= AUTO_KEEP
|
|
78
|
+
|
|
79
|
+
if (kept) {
|
|
80
|
+
if (randomValue < thresholdValue) {
|
|
81
|
+
randomValue = thresholdValue
|
|
82
|
+
}
|
|
83
|
+
} else if (randomValue >= thresholdValue) {
|
|
84
|
+
randomValue = thresholdValue > 0n ? thresholdValue - 1n : 0n
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
randomValue: randomValue.toString(16).padStart(14, '0'),
|
|
89
|
+
threshold: formatThreshold(thresholdValue),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the probability rate recorded by a committed Datadog sampling decision.
|
|
95
|
+
* Sampling probes deliberately leave these rate fields unset.
|
|
96
|
+
*
|
|
97
|
+
* @param {import('./opentracing/span_context')} context
|
|
98
|
+
* @returns {number | undefined}
|
|
99
|
+
*/
|
|
100
|
+
function getProbabilityRate (context) {
|
|
101
|
+
if (context._sampling.isProbabilityDecision === false) return
|
|
102
|
+
return (context._trace[SAMPLING_RULE_DECISION] ?? context._trace[SAMPLING_AGENT_DECISION]) || undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Removes malformed OTel sampling fields and returns the valid values.
|
|
107
|
+
*
|
|
108
|
+
* @param {import('./opentracing/propagation/tracestate')} state
|
|
109
|
+
* @returns {{ randomValue: string | undefined, threshold: string | undefined }}
|
|
110
|
+
*/
|
|
111
|
+
function sanitizeFields (state) {
|
|
112
|
+
let randomValue = state.get('rv')
|
|
113
|
+
let threshold = state.get('th')
|
|
114
|
+
|
|
115
|
+
if (!isLowerHex(randomValue, 14, 14)) {
|
|
116
|
+
state.delete('rv')
|
|
117
|
+
randomValue = undefined
|
|
118
|
+
}
|
|
119
|
+
if (!isLowerHex(threshold, 1, 14)) {
|
|
120
|
+
state.delete('th')
|
|
121
|
+
threshold = undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { randomValue, threshold }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sanitizes inherited OTel sampling fields without changing valid state.
|
|
129
|
+
*
|
|
130
|
+
* @param {import('./opentracing/propagation/tracestate')} traceState
|
|
131
|
+
*/
|
|
132
|
+
function normalizeOtelTraceState (traceState) {
|
|
133
|
+
if (traceState.get('ot') === undefined) return
|
|
134
|
+
traceState.forVendor('ot', sanitizeFields)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Updates the OTel tracestate member to represent the context's sampling decision.
|
|
139
|
+
*
|
|
140
|
+
* @param {import('./opentracing/span_context')} context
|
|
141
|
+
* @param {import('./opentracing/propagation/tracestate')} traceState
|
|
142
|
+
*/
|
|
143
|
+
function updateOtelTraceState (context, traceState) {
|
|
144
|
+
const otelMember = traceState.get('ot')
|
|
145
|
+
const probabilityRate = getProbabilityRate(context)
|
|
146
|
+
if (context._sampling.isProbabilityDecision === false) {
|
|
147
|
+
if (otelMember === undefined) return
|
|
148
|
+
} else if (probabilityRate === undefined) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
traceState.forVendor('ot', state => {
|
|
153
|
+
const { randomValue, threshold } = sanitizeFields(state)
|
|
154
|
+
|
|
155
|
+
if (context._sampling.isProbabilityDecision === false) {
|
|
156
|
+
state.delete('th')
|
|
157
|
+
} else if (randomValue === undefined && threshold === undefined) {
|
|
158
|
+
const generated = generateFields(context, probabilityRate)
|
|
159
|
+
if (generated) {
|
|
160
|
+
// Entries serialize in reverse insertion order, so add th before rv.
|
|
161
|
+
state.set('th', generated.threshold)
|
|
162
|
+
state.set('rv', generated.randomValue)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
normalizeOtelTraceState,
|
|
170
|
+
updateOtelTraceState,
|
|
171
|
+
}
|
|
@@ -15,7 +15,6 @@ const plugins = {
|
|
|
15
15
|
get '@supabase/postgrest-js' () { return require('../../../datadog-plugin-supabase/src') },
|
|
16
16
|
get '@supabase/realtime-js' () { return require('../../../datadog-plugin-supabase/src') },
|
|
17
17
|
get '@supabase/storage-js' () { return require('../../../datadog-plugin-supabase/src') },
|
|
18
|
-
get '@supabase/supabase-js' () { return require('../../../datadog-plugin-supabase/src') },
|
|
19
18
|
get 'durable-functions' () { return require('../../../datadog-plugin-azure-durable-functions/src') },
|
|
20
19
|
get '@azure/service-bus' () { return require('../../../datadog-plugin-azure-service-bus/src') },
|
|
21
20
|
get '@cucumber/cucumber' () { return require('../../../datadog-plugin-cucumber/src') },
|
|
@@ -2025,6 +2025,10 @@ function getModifiedFilesFromDiff (diff) {
|
|
|
2025
2025
|
if (lineMatch && currentFile) {
|
|
2026
2026
|
const start = Number(lineMatch.groups.start)
|
|
2027
2027
|
const count = lineMatch.groups.count ? Number(lineMatch.groups.count) : 1
|
|
2028
|
+
if (count === 0) {
|
|
2029
|
+
// Keep the surviving line before a deletion so line-based consumers can match its scope.
|
|
2030
|
+
result[currentFile].push(start)
|
|
2031
|
+
}
|
|
2028
2032
|
for (let j = 0; j < count; j++) {
|
|
2029
2033
|
result[currentFile].push(start + j)
|
|
2030
2034
|
}
|
|
@@ -26,8 +26,10 @@ const {
|
|
|
26
26
|
SAMPLING_MECHANISM_AGENT,
|
|
27
27
|
SAMPLING_MECHANISM_RULE,
|
|
28
28
|
SAMPLING_MECHANISM_MANUAL,
|
|
29
|
+
SAMPLING_MECHANISM_APPSEC,
|
|
29
30
|
SAMPLING_MECHANISM_REMOTE_USER,
|
|
30
31
|
SAMPLING_MECHANISM_REMOTE_DYNAMIC,
|
|
32
|
+
SAMPLING_MECHANISM_AI_GUARD,
|
|
31
33
|
SAMPLING_RULE_DECISION,
|
|
32
34
|
SAMPLING_LIMIT_DECISION,
|
|
33
35
|
SAMPLING_AGENT_DECISION,
|
|
@@ -39,6 +41,17 @@ const DEFAULT_KEY = 'service:,env:'
|
|
|
39
41
|
|
|
40
42
|
const defaultSampler = new Sampler(AUTO_KEEP)
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Returns whether a product has already force-kept the trace.
|
|
46
|
+
*
|
|
47
|
+
* @param {import('./opentracing/span_context')} context
|
|
48
|
+
*/
|
|
49
|
+
function isProductForceKeep (context) {
|
|
50
|
+
const { priority, mechanism } = context._sampling
|
|
51
|
+
return priority === USER_KEEP &&
|
|
52
|
+
(mechanism === SAMPLING_MECHANISM_APPSEC || mechanism === SAMPLING_MECHANISM_AI_GUARD)
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
/**
|
|
43
56
|
* PrioritySampler is responsible for determining whether a span should be sampled
|
|
44
57
|
* based on various rules, rate limits, and priorities. It supports manual and
|
|
@@ -114,10 +127,10 @@ class PrioritySampler {
|
|
|
114
127
|
const tag = this._getPriorityFromTags(context.getTags(), context)
|
|
115
128
|
|
|
116
129
|
if (this.validate(tag)) {
|
|
117
|
-
context._sampling.priority = tag
|
|
118
130
|
context._sampling.mechanism = SAMPLING_MECHANISM_MANUAL
|
|
131
|
+
this._recordDecision(context, tag)
|
|
119
132
|
} else if (auto) {
|
|
120
|
-
|
|
133
|
+
this.decideFromAuto(root)
|
|
121
134
|
} else {
|
|
122
135
|
return
|
|
123
136
|
}
|
|
@@ -125,6 +138,22 @@ class PrioritySampler {
|
|
|
125
138
|
this.#addDecisionMaker(root)
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Applies manual sampling tags from a supplied tag collection using their defined precedence.
|
|
143
|
+
*
|
|
144
|
+
* @param {DatadogSpan} span
|
|
145
|
+
* @param {Record<string, unknown>} tags
|
|
146
|
+
*/
|
|
147
|
+
setPriorityFromTags (span, tags) {
|
|
148
|
+
if (!span) return
|
|
149
|
+
|
|
150
|
+
const context = this._getContext(span)
|
|
151
|
+
if (isProductForceKeep(context)) return
|
|
152
|
+
|
|
153
|
+
const priority = this._getPriorityFromTags(tags, context)
|
|
154
|
+
if (this.validate(priority)) this.setPriority(span, priority)
|
|
155
|
+
}
|
|
156
|
+
|
|
128
157
|
/**
|
|
129
158
|
* Updates agent-provided sampling rates keyed by `service:,env:`.
|
|
130
159
|
*
|
|
@@ -179,14 +208,13 @@ class PrioritySampler {
|
|
|
179
208
|
return // noop span
|
|
180
209
|
}
|
|
181
210
|
|
|
182
|
-
context._sampling.priority = samplingPriority
|
|
183
|
-
|
|
184
211
|
const mechanism = product?.mechanism ?? SAMPLING_MECHANISM_MANUAL
|
|
185
212
|
context._sampling.mechanism = mechanism
|
|
213
|
+
this._recordDecision(context, samplingPriority)
|
|
186
214
|
|
|
187
215
|
log.trace(span, samplingPriority, mechanism)
|
|
188
216
|
|
|
189
|
-
this.#addDecisionMaker(root)
|
|
217
|
+
this.#addDecisionMaker(root, product === undefined)
|
|
190
218
|
}
|
|
191
219
|
|
|
192
220
|
/**
|
|
@@ -201,6 +229,23 @@ class PrioritySampler {
|
|
|
201
229
|
: /** @type {DatadogSpanContext} */ (span)
|
|
202
230
|
}
|
|
203
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Evaluates and records an automatic sampling decision.
|
|
234
|
+
*
|
|
235
|
+
* @param {DatadogSpan} span
|
|
236
|
+
* @returns {SamplingPriority}
|
|
237
|
+
*/
|
|
238
|
+
decideFromAuto (span) {
|
|
239
|
+
const context = this._getContext(span)
|
|
240
|
+
const rule = this.#findRule(span)
|
|
241
|
+
const priority = rule
|
|
242
|
+
? this.#getPriorityByRule(context, rule, true)
|
|
243
|
+
: this.#getPriorityByAgent(context, true)
|
|
244
|
+
|
|
245
|
+
context._sampling.priority = priority
|
|
246
|
+
return priority
|
|
247
|
+
}
|
|
248
|
+
|
|
204
249
|
/**
|
|
205
250
|
* Computes priority using rules and agent rates when no manual tag is present.
|
|
206
251
|
*
|
|
@@ -212,27 +257,25 @@ class PrioritySampler {
|
|
|
212
257
|
const rule = this.#findRule(span)
|
|
213
258
|
|
|
214
259
|
return rule
|
|
215
|
-
? this.#getPriorityByRule(context, rule)
|
|
216
|
-
: this.#getPriorityByAgent(context)
|
|
260
|
+
? this.#getPriorityByRule(context, rule, false)
|
|
261
|
+
: this.#getPriorityByAgent(context, false)
|
|
217
262
|
}
|
|
218
263
|
|
|
219
264
|
/**
|
|
220
|
-
*
|
|
221
|
-
* Included for compatibility with {@link import('./standalone/tracesource_priority_sampler')._getPriorityFromTags}
|
|
265
|
+
* Converts one sampling tag into a user priority.
|
|
222
266
|
*
|
|
223
|
-
* @param {
|
|
267
|
+
* @param {string} key
|
|
268
|
+
* @param {unknown} value
|
|
224
269
|
* @param {DatadogSpanContext} _context
|
|
225
270
|
* @returns {SamplingPriority|undefined}
|
|
226
271
|
*/
|
|
227
|
-
|
|
228
|
-
if (
|
|
272
|
+
_getPriorityFromTag (key, value, _context) {
|
|
273
|
+
if (key === MANUAL_KEEP && value !== false) {
|
|
229
274
|
return USER_KEEP
|
|
230
|
-
} else if (
|
|
275
|
+
} else if (key === MANUAL_DROP && value !== false) {
|
|
231
276
|
return USER_REJECT
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (rawPriority !== undefined) {
|
|
235
|
-
const priority = Math.trunc(rawPriority)
|
|
277
|
+
} else if (key === SAMPLING_PRIORITY && (typeof value === 'number' || typeof value === 'string')) {
|
|
278
|
+
const priority = Math.trunc(/** @type {number} */ (value))
|
|
236
279
|
|
|
237
280
|
if (priority === 1 || priority === 2) {
|
|
238
281
|
return USER_KEEP
|
|
@@ -242,30 +285,85 @@ class PrioritySampler {
|
|
|
242
285
|
}
|
|
243
286
|
}
|
|
244
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Computes priority from manual sampling tags if present.
|
|
290
|
+
* Calls the overridable single-tag conversion so specialized samplers retain their filtering rules.
|
|
291
|
+
*
|
|
292
|
+
* @param {Record<string, unknown>} tags
|
|
293
|
+
* @param {DatadogSpanContext} _context
|
|
294
|
+
* @returns {SamplingPriority|undefined}
|
|
295
|
+
*/
|
|
296
|
+
_getPriorityFromTags (tags, _context) {
|
|
297
|
+
if (!Object.hasOwn(tags, MANUAL_KEEP) && !Object.hasOwn(tags, MANUAL_DROP)) {
|
|
298
|
+
const priority = tags[SAMPLING_PRIORITY]
|
|
299
|
+
if (priority !== undefined) return this._getPriorityFromTag(SAMPLING_PRIORITY, priority, _context)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return this.#getPriorityFromManualTags(tags, _context)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Computes priority when at least one manual keep/drop tag is present.
|
|
308
|
+
* Value checks cover the common case; own-property checks preserve undefined-valued manual tags.
|
|
309
|
+
*
|
|
310
|
+
* @param {Record<string, unknown>} tags
|
|
311
|
+
* @param {DatadogSpanContext} context
|
|
312
|
+
* @returns {SamplingPriority|undefined}
|
|
313
|
+
*/
|
|
314
|
+
#getPriorityFromManualTags (tags, context) {
|
|
315
|
+
const manualKeep = tags[MANUAL_KEEP]
|
|
316
|
+
if (manualKeep !== undefined || Object.hasOwn(tags, MANUAL_KEEP)) {
|
|
317
|
+
const priority = this._getPriorityFromTag(MANUAL_KEEP, manualKeep, context)
|
|
318
|
+
if (priority !== undefined) return priority
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const manualDrop = tags[MANUAL_DROP]
|
|
322
|
+
if (manualDrop !== undefined || Object.hasOwn(tags, MANUAL_DROP)) {
|
|
323
|
+
const priority = this._getPriorityFromTag(MANUAL_DROP, manualDrop, context)
|
|
324
|
+
if (priority !== undefined) return priority
|
|
325
|
+
}
|
|
326
|
+
const priority = tags[SAMPLING_PRIORITY]
|
|
327
|
+
if (priority !== undefined) return this._getPriorityFromTag(SAMPLING_PRIORITY, priority, context)
|
|
328
|
+
}
|
|
329
|
+
|
|
245
330
|
/**
|
|
246
331
|
* Applies a matching rule and rate limit to compute the sampling priority.
|
|
247
332
|
*
|
|
248
333
|
* @param {DatadogSpanContext} context
|
|
249
334
|
* @param {import('./sampling_rule')} rule
|
|
335
|
+
* @param {boolean} recordDecision
|
|
250
336
|
* @returns {SamplingPriority}
|
|
251
337
|
*/
|
|
252
|
-
#getPriorityByRule (context, rule) {
|
|
253
|
-
context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
|
|
338
|
+
#getPriorityByRule (context, rule, recordDecision) {
|
|
339
|
+
if (recordDecision) context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate
|
|
254
340
|
context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rule.sampleRate)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
341
|
+
if (recordDecision) {
|
|
342
|
+
context._sampling.mechanism = SAMPLING_MECHANISM_RULE
|
|
343
|
+
if (rule.provenance === 'customer') {
|
|
344
|
+
context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER
|
|
345
|
+
} else if (rule.provenance === 'dynamic') {
|
|
346
|
+
context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC
|
|
347
|
+
}
|
|
260
348
|
}
|
|
261
349
|
|
|
262
350
|
if (rule.discard) {
|
|
263
351
|
context._sampling.discard = true
|
|
264
352
|
}
|
|
265
353
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
354
|
+
const sampled = rule.sample(context, true)
|
|
355
|
+
if (sampled !== true) {
|
|
356
|
+
if (recordDecision && sampled === undefined) this._recordDecisionMetadata(context)
|
|
357
|
+
return USER_REJECT
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!this._isSampledByRateLimit(context)) {
|
|
361
|
+
// The limiter makes only rejected traces non-probabilistic; allowed traces retain the rule probability.
|
|
362
|
+
if (recordDecision) this._recordDecisionMetadata(context)
|
|
363
|
+
return USER_REJECT
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return USER_KEEP
|
|
269
367
|
}
|
|
270
368
|
|
|
271
369
|
/**
|
|
@@ -287,39 +385,63 @@ class PrioritySampler {
|
|
|
287
385
|
* Computes priority using agent-provided sampling rates.
|
|
288
386
|
*
|
|
289
387
|
* @param {DatadogSpanContext} context
|
|
388
|
+
* @param {boolean} recordDecision
|
|
290
389
|
* @returns {SamplingPriority}
|
|
291
390
|
*/
|
|
292
|
-
#getPriorityByAgent (context) {
|
|
391
|
+
#getPriorityByAgent (context, recordDecision) {
|
|
293
392
|
const key = `service:${context.getTag(SERVICE_NAME)},env:${this._env}`
|
|
294
393
|
// TODO: Change underscored properties to private ones.
|
|
295
394
|
const sampler = this._samplers[key] || this._samplers[DEFAULT_KEY]
|
|
296
395
|
|
|
297
396
|
const rate = sampler.rate()
|
|
298
|
-
context._trace[SAMPLING_AGENT_DECISION] = rate
|
|
397
|
+
if (recordDecision) context._trace[SAMPLING_AGENT_DECISION] = rate
|
|
299
398
|
|
|
300
399
|
if (sampler === defaultSampler) {
|
|
301
|
-
context._sampling.mechanism = SAMPLING_MECHANISM_DEFAULT
|
|
400
|
+
if (recordDecision) context._sampling.mechanism = SAMPLING_MECHANISM_DEFAULT
|
|
302
401
|
} else {
|
|
303
402
|
context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rate)
|
|
304
|
-
context._sampling.mechanism = SAMPLING_MECHANISM_AGENT
|
|
403
|
+
if (recordDecision) context._sampling.mechanism = SAMPLING_MECHANISM_AGENT
|
|
305
404
|
}
|
|
306
405
|
|
|
307
406
|
return sampler.isSampled(context) ? AUTO_KEEP : AUTO_REJECT
|
|
308
407
|
}
|
|
309
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Records that a sampling decision does not represent a probability.
|
|
411
|
+
*
|
|
412
|
+
* @param {DatadogSpanContext} context
|
|
413
|
+
*/
|
|
414
|
+
_recordDecisionMetadata (context) {
|
|
415
|
+
context._sampling.isProbabilityDecision = false
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Records a local non-probability sampling decision.
|
|
420
|
+
*
|
|
421
|
+
* @param {DatadogSpanContext} context
|
|
422
|
+
* @param {SamplingPriority} priority
|
|
423
|
+
* @returns {SamplingPriority}
|
|
424
|
+
*/
|
|
425
|
+
_recordDecision (context, priority) {
|
|
426
|
+
context._sampling.priority = priority
|
|
427
|
+
this._recordDecisionMetadata(context)
|
|
428
|
+
return priority
|
|
429
|
+
}
|
|
430
|
+
|
|
310
431
|
/**
|
|
311
432
|
* Tags the trace with a decision maker when priority is keep, or removes it otherwise.
|
|
312
433
|
*
|
|
313
434
|
* @param {DatadogSpan} span
|
|
435
|
+
* @param {boolean} [overwrite]
|
|
314
436
|
*/
|
|
315
|
-
#addDecisionMaker (span) {
|
|
437
|
+
#addDecisionMaker (span, overwrite = false) {
|
|
316
438
|
const context = span.context()
|
|
317
439
|
const trace = context._trace
|
|
318
440
|
const priority = context._sampling.priority
|
|
319
441
|
const mechanism = context._sampling.mechanism
|
|
320
442
|
|
|
321
443
|
if (priority >= AUTO_KEEP) {
|
|
322
|
-
if (!trace.tags[DECISION_MAKER_KEY]) {
|
|
444
|
+
if (overwrite || !trace.tags[DECISION_MAKER_KEY]) {
|
|
323
445
|
trace.tags[DECISION_MAKER_KEY] = `-${mechanism}`
|
|
324
446
|
}
|
|
325
447
|
} else if (trace.tags[DECISION_MAKER_KEY] !== undefined) {
|
|
@@ -12,8 +12,21 @@ let activeSSIHeuristics
|
|
|
12
12
|
/** @type {import('./profiling') | undefined} */
|
|
13
13
|
let profilingModule
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Configuration published while the profiler was finishing a shutdown export, waiting for that
|
|
17
|
+
* export to settle before it is acted on.
|
|
18
|
+
*
|
|
19
|
+
* @type {import('./config/config-base') | undefined}
|
|
20
|
+
*/
|
|
21
|
+
let deferredConfig
|
|
22
|
+
|
|
15
23
|
function getProfilingModule () {
|
|
16
|
-
profilingModule
|
|
24
|
+
if (profilingModule === undefined) {
|
|
25
|
+
profilingModule = require('./profiling')
|
|
26
|
+
// The profiling layer never restarts itself; it only reports that a shutdown export has
|
|
27
|
+
// settled, which is when a configuration deferred during that window gets its decision.
|
|
28
|
+
profilingModule.profiler.on('stopped', applyDeferredConfig)
|
|
29
|
+
}
|
|
17
30
|
return profilingModule
|
|
18
31
|
}
|
|
19
32
|
|
|
@@ -25,6 +38,12 @@ function isStarted () {
|
|
|
25
38
|
return profilingModule !== undefined && profilingModule.profiler.enabled
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
// A stopped profiler still exports one final profile, and until that settles it reports neither
|
|
42
|
+
// the state a decision would be based on nor any willingness to start again.
|
|
43
|
+
function isStopping () {
|
|
44
|
+
return profilingModule !== undefined && profilingModule.profiler.isStopping()
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/** @type {typeof import('./profiling/ssi-heuristics') | undefined} */
|
|
29
48
|
let ssiHeuristicsModule
|
|
30
49
|
|
|
@@ -57,8 +76,11 @@ function start (config) {
|
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
function stop () {
|
|
60
|
-
//
|
|
61
|
-
//
|
|
79
|
+
// An explicit stop is the latest desired state, so it retracts a configuration still waiting
|
|
80
|
+
// for an in-flight shutdown to settle.
|
|
81
|
+
deferredConfig = undefined
|
|
82
|
+
|
|
83
|
+
// A stop command for a profiler that has never been loaded is already satisfied.
|
|
62
84
|
if (profilingModule === undefined) return
|
|
63
85
|
|
|
64
86
|
try {
|
|
@@ -93,7 +115,17 @@ function runWithLabels (labels, fn) {
|
|
|
93
115
|
return getProfilingModule().profiler.runWithLabels(labels, fn)
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
|
|
118
|
+
function applyDeferredConfig () {
|
|
119
|
+
const config = deferredConfig
|
|
120
|
+
if (config === undefined) return
|
|
121
|
+
deferredConfig = undefined
|
|
122
|
+
applyConfig(config)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {import('./config/config-base')} config - Tracer configuration
|
|
127
|
+
*/
|
|
128
|
+
function applyConfig (config) {
|
|
97
129
|
const enabled = config.profiling.DD_PROFILING_ENABLED
|
|
98
130
|
if (enabled === 'true') {
|
|
99
131
|
// A non-auto value means the SSI heuristics no longer get a say; disable them so a trigger that
|
|
@@ -118,7 +150,9 @@ configUpdateChannel.subscribe((config) => {
|
|
|
118
150
|
heuristics.start()
|
|
119
151
|
heuristics.onTriggered(() => {
|
|
120
152
|
// Explicit true/false publishes disable the heuristics, so reaching this callback guarantees
|
|
121
|
-
// the latest valid published value is still 'auto'.
|
|
153
|
+
// the latest valid published value is still 'auto'. A heuristic is only ever created for a
|
|
154
|
+
// profiler that is neither running nor stopping, and nothing can start that profiler behind
|
|
155
|
+
// an active heuristic's back, so this start can not collide with a shutdown in flight.
|
|
122
156
|
if (!isStarted()) start(config)
|
|
123
157
|
// The heuristic has made its decision, so release all of its listeners and timer before
|
|
124
158
|
// dropping the module-level reference. This does not stop the profiler that was just
|
|
@@ -132,6 +166,18 @@ configUpdateChannel.subscribe((config) => {
|
|
|
132
166
|
// like 'auto' or crash the customer application.
|
|
133
167
|
log.warn('Unexpected DD_PROFILING_ENABLED value: %o', enabled)
|
|
134
168
|
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
configUpdateChannel.subscribe((config) => {
|
|
172
|
+
// A shutdown export in flight makes every decision here unsafe: the profiler can not be
|
|
173
|
+
// started, and it does not yet report the state the auto branch would branch on. Defer instead,
|
|
174
|
+
// keeping only the latest configuration, exactly as a sequence of updates outside such a window
|
|
175
|
+
// would have left it.
|
|
176
|
+
if (isStopping()) {
|
|
177
|
+
deferredConfig = config
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
applyConfig(config)
|
|
135
181
|
})
|
|
136
182
|
|
|
137
183
|
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(stop)
|
|
@@ -61,7 +61,6 @@ class Profiler extends EventEmitter {
|
|
|
61
61
|
#exporters
|
|
62
62
|
#flushInterval
|
|
63
63
|
#lastStart
|
|
64
|
-
#pendingStart
|
|
65
64
|
#profileSeq = 0
|
|
66
65
|
#profilers
|
|
67
66
|
#spanFinishListener
|
|
@@ -87,6 +86,14 @@ class Profiler extends EventEmitter {
|
|
|
87
86
|
return this.#enabled
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
/**
|
|
90
|
+
* True while a stop()'s final collection is still encoding and exporting. The profiler no
|
|
91
|
+
* longer samples, but it also can not be started again until that settles and 'stopped' fires.
|
|
92
|
+
*/
|
|
93
|
+
isStopping () {
|
|
94
|
+
return this.#stopping !== undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
/**
|
|
91
98
|
* Declares the set of custom label keys that will be used with
|
|
92
99
|
* {@link runWithLabels}. This is used for profile upload metadata and
|
|
@@ -184,12 +191,12 @@ class Profiler extends EventEmitter {
|
|
|
184
191
|
if (this.#startFailed) return false
|
|
185
192
|
|
|
186
193
|
// A prior stop()'s shutdown collection may still be encoding/exporting via #tags,
|
|
187
|
-
// #exporters and #endpointCounts
|
|
188
|
-
//
|
|
189
|
-
//
|
|
194
|
+
// #exporters and #endpointCounts, which this start() would overwrite out from under it.
|
|
195
|
+
// Refuse rather than queue: the caller owns the decision of what to do once the shutdown
|
|
196
|
+
// settles, and by then its configuration may no longer be the one requested here. This is
|
|
197
|
+
// thrown outside the setup try/catch below so it does not count as a permanent start failure.
|
|
190
198
|
if (this.#stopping) {
|
|
191
|
-
|
|
192
|
-
return true
|
|
199
|
+
throw new Error('Cannot start the profiler while a shutdown collection is still in flight')
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
try {
|
|
@@ -294,11 +301,12 @@ class Profiler extends EventEmitter {
|
|
|
294
301
|
this._timeoutInterval = this.#flushInterval
|
|
295
302
|
}
|
|
296
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Stops the profiler after collecting and exporting one final profile. That export outlives
|
|
306
|
+
* this call: {@link isStopping} returns true and start() throws until it settles, at which
|
|
307
|
+
* point a 'stopped' event is emitted.
|
|
308
|
+
*/
|
|
297
309
|
stop () {
|
|
298
|
-
// A stop() always reflects the latest desired state, so it cancels any restart queued by a
|
|
299
|
-
// start() that arrived while a prior shutdown collection was still in flight.
|
|
300
|
-
this.#pendingStart = undefined
|
|
301
|
-
|
|
302
310
|
if (!this.enabled) return
|
|
303
311
|
|
|
304
312
|
// collect and export current profiles
|
|
@@ -311,16 +319,12 @@ class Profiler extends EventEmitter {
|
|
|
311
319
|
})
|
|
312
320
|
.finally(() => {
|
|
313
321
|
this.#stopping = undefined
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.start(config)
|
|
321
|
-
} catch (error) {
|
|
322
|
-
log.error(error)
|
|
323
|
-
}
|
|
322
|
+
// Nothing awaits this promise, so a listener throwing here would become an unhandled
|
|
323
|
+
// rejection instead of reaching whoever requested the stop.
|
|
324
|
+
try {
|
|
325
|
+
this.emit('stopped')
|
|
326
|
+
} catch (error) {
|
|
327
|
+
log.error(error)
|
|
324
328
|
}
|
|
325
329
|
})
|
|
326
330
|
this.#stop()
|