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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { parentPort, workerData } = require('node:worker_threads')
|
|
4
|
+
const port = /** @type {import('node:worker_threads').MessagePort} */ (parentPort)
|
|
5
|
+
|
|
6
|
+
// Bootstrap only the lifecycle registry, never the tracer or its preload entrypoints.
|
|
7
|
+
globalThis[Symbol.for('dd-trace')] = { beforeExitHandlers: new Set() }
|
|
8
|
+
|
|
9
|
+
const logWriter = require('../../log/writer')
|
|
10
|
+
const { configureWorkerTelemetry } = require('./flag-evaluation-telemetry')
|
|
11
|
+
const state = new Int32Array(workerData.state)
|
|
12
|
+
configureWorkerTelemetry(state)
|
|
13
|
+
const FlagEvaluationConsumer = require('./flag-evaluation-consumer')
|
|
14
|
+
|
|
15
|
+
/** @param {'debug' | 'info' | 'warn' | 'error'} level */
|
|
16
|
+
function forwardLog (level) {
|
|
17
|
+
/** @param {unknown} message */
|
|
18
|
+
return message => {
|
|
19
|
+
try {
|
|
20
|
+
port.postMessage({ type: 'log', level, message: message instanceof Error ? message.stack : String(message) })
|
|
21
|
+
} catch {
|
|
22
|
+
// Diagnostics are best-effort when the parent port is closing.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Use the parent's effective settings, not a second environment-only configuration.
|
|
28
|
+
// The custom logger itself stays in the parent isolate because functions cannot be cloned.
|
|
29
|
+
const { enabled = false, level } = workerData.logging ?? {}
|
|
30
|
+
logWriter.configure(enabled, level, {
|
|
31
|
+
debug: forwardLog('debug'),
|
|
32
|
+
info: forwardLog('info'),
|
|
33
|
+
warn: forwardLog('warn'),
|
|
34
|
+
error: forwardLog('error'),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** @param {import('./flag-evaluations').SerializedFlagEvaluationRoute} route */
|
|
38
|
+
function deserializeRoute (route) {
|
|
39
|
+
return {
|
|
40
|
+
url: new URL(route.url),
|
|
41
|
+
basePath: route.basePath,
|
|
42
|
+
headers: route.headers,
|
|
43
|
+
fallback: route.fallback && deserializeRoute(route.fallback),
|
|
44
|
+
onFallback: route.onFallback
|
|
45
|
+
? () => port.postMessage({ type: 'route', id: route.id, status: 'fallback' })
|
|
46
|
+
: undefined,
|
|
47
|
+
onUnavailable: route.onUnavailable
|
|
48
|
+
? () => port.postMessage({ type: 'route', id: route.id, status: 'unavailable' })
|
|
49
|
+
: undefined,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const route = deserializeRoute(workerData.route)
|
|
54
|
+
const consumer = new FlagEvaluationConsumer({ ...workerData.context, url: route.url }, route, {
|
|
55
|
+
onProcessed: count => Atomics.sub(state, 0, count),
|
|
56
|
+
onDelivered: count => Atomics.sub(state, 1, count),
|
|
57
|
+
onIdle: () => port.close(),
|
|
58
|
+
})
|
|
59
|
+
consumer.setEnabled(true, route)
|
|
60
|
+
port.on('message', message => {
|
|
61
|
+
switch (message.type) {
|
|
62
|
+
case 'batch':
|
|
63
|
+
for (const event of message.events) {
|
|
64
|
+
if (!consumer.enqueue(event)) Atomics.sub(state, 0, 1)
|
|
65
|
+
}
|
|
66
|
+
break
|
|
67
|
+
case 'enabled':
|
|
68
|
+
consumer.setEnabled(message.enabled, message.route && deserializeRoute(message.route))
|
|
69
|
+
break
|
|
70
|
+
case 'flush':
|
|
71
|
+
consumer.flush()
|
|
72
|
+
break
|
|
73
|
+
case 'close':
|
|
74
|
+
consumer.destroy()
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
})
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { join } = require('node:path')
|
|
4
|
+
|
|
5
|
+
const { channel } = require('dc-polyfill')
|
|
6
|
+
|
|
7
|
+
const { FLAG_EVALUATION_FLUSH_INTERVAL, FLAG_EVALUATION_QUEUE_CAP } = require('../constants/constants')
|
|
8
|
+
const { EVP_PROXY_PATH_V2 } = require('../../evp_proxy/constants')
|
|
9
|
+
const { getEnvironmentVariables } = require('../../config/helper')
|
|
10
|
+
const log = require('../../log')
|
|
11
|
+
const { normalizeFlagEvaluationEvent, normalizeTargetingKey } = require('./flag-evaluation-pii')
|
|
12
|
+
const {
|
|
13
|
+
collectWorkerTelemetry, createWorkerState, recordDropped, recordTargetingKeyOmitted,
|
|
14
|
+
} = require('./flag-evaluation-telemetry')
|
|
15
|
+
|
|
16
|
+
const telemetryAppClosingCh = channel('datadog:telemetry:app-closing')
|
|
17
|
+
const flushCh = channel('ffe:writers:flush')
|
|
18
|
+
|
|
19
|
+
// Share clone/wakeup costs across event-loop turns without delaying sparse evaluations indefinitely.
|
|
20
|
+
const BATCH_SIZE = 64
|
|
21
|
+
const CONSENT_BATCH_SIZE = 8
|
|
22
|
+
const BATCH_DELAY_MS = 20
|
|
23
|
+
const permissionFlags = new Set([
|
|
24
|
+
'--permission', '--experimental-permission', '--permission-audit',
|
|
25
|
+
'--allow-addons', '--allow-child-process', '--allow-fs-read', '--allow-fs-write',
|
|
26
|
+
'--allow-ffi', '--allow-inspector', '--allow-net', '--allow-openssl-store', '--allow-wasi', '--allow-worker',
|
|
27
|
+
])
|
|
28
|
+
const networkFlags = new Set([
|
|
29
|
+
'--dns-result-order', '--network-family-autoselection', '--enable-network-family-autoselection',
|
|
30
|
+
'--network-family-autoselection-attempt-timeout', '--max-http-header-size', '--insecure-http-parser',
|
|
31
|
+
// TLS version defaults are worker-local; other TLS/OpenSSL options may reject worker execArgv.
|
|
32
|
+
'--tls-min-v1.0', '--tls-min-v1.1', '--tls-min-v1.2', '--tls-min-v1.3',
|
|
33
|
+
'--tls-max-v1.2', '--tls-max-v1.3',
|
|
34
|
+
])
|
|
35
|
+
const flagsWithValues = new Set([
|
|
36
|
+
'--allow-fs-read', '--allow-fs-write', '--dns-result-order', '--network-family-autoselection-attempt-timeout',
|
|
37
|
+
'--max-http-header-size',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
/** @param {string} [nodeOptions] */
|
|
41
|
+
function getWorkerArgs (nodeOptions = '') {
|
|
42
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- Feature detection also supports Node 18.
|
|
43
|
+
const permissionEnabled = Boolean(process.permission)
|
|
44
|
+
const args = []
|
|
45
|
+
let arg = ''
|
|
46
|
+
let quoted = false
|
|
47
|
+
// Match Node's NODE_OPTIONS parsing: spaces delimit arguments; double quotes allow spaces,
|
|
48
|
+
// and backslashes escape the next character only inside double quotes (not shell quoting).
|
|
49
|
+
for (let i = 0; i < nodeOptions.length; i++) {
|
|
50
|
+
const char = nodeOptions[i]
|
|
51
|
+
if (char === '\\' && quoted) {
|
|
52
|
+
if (++i === nodeOptions.length) throw new Error('Invalid escape in NODE_OPTIONS')
|
|
53
|
+
arg += nodeOptions[i]
|
|
54
|
+
} else if (char === '"') {
|
|
55
|
+
quoted = !quoted
|
|
56
|
+
} else if (char === ' ' && !quoted) {
|
|
57
|
+
if (arg) args.push(arg)
|
|
58
|
+
arg = ''
|
|
59
|
+
} else {
|
|
60
|
+
arg += char
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (quoted) throw new Error('Unterminated quote in NODE_OPTIONS')
|
|
64
|
+
if (arg) args.push(arg)
|
|
65
|
+
// Node processes environment options first, then command-line overrides.
|
|
66
|
+
args.push(...process.execArgv)
|
|
67
|
+
const workerArgs = []
|
|
68
|
+
for (let i = 0; i < args.length; i++) {
|
|
69
|
+
const flag = args[i].split('=')[0].replaceAll('_', '-').replace(/^--no-/, '--')
|
|
70
|
+
if (!networkFlags.has(flag) && !(permissionEnabled && permissionFlags.has(flag))) continue
|
|
71
|
+
workerArgs.push(args[i])
|
|
72
|
+
if (flagsWithValues.has(flag) && !args[i].includes('=')) {
|
|
73
|
+
workerArgs.push(args[++i])
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return workerArgs
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @typedef {import('./flag-evaluation-consumer').FlagEvaluationRoute} FlagEvaluationRoute */
|
|
80
|
+
/** @typedef {import('./flag-evaluation-aggregation').FlagEvaluationEvent} FlagEvaluationEvent */
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {object} SerializedFlagEvaluationRoute
|
|
84
|
+
* @property {number} id
|
|
85
|
+
* @property {string} url
|
|
86
|
+
* @property {string} basePath
|
|
87
|
+
* @property {object} [headers]
|
|
88
|
+
* @property {boolean} onFallback
|
|
89
|
+
* @property {boolean} onUnavailable
|
|
90
|
+
* @property {SerializedFlagEvaluationRoute} [fallback]
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {FlagEvaluationRoute} route
|
|
95
|
+
* @param {number} id - Generation used to reject reports from obsolete routes
|
|
96
|
+
*/
|
|
97
|
+
function serializeRoute (route, id) {
|
|
98
|
+
// Live custom agents cannot cross isolates. The common request helper recreates env proxy agents in the worker.
|
|
99
|
+
if (route.agent) throw new Error('Custom EVP route agents are not supported in the flag evaluation worker')
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
url: route.url.href,
|
|
103
|
+
basePath: route.basePath,
|
|
104
|
+
headers: route.headers,
|
|
105
|
+
onFallback: typeof route.onFallback === 'function',
|
|
106
|
+
onUnavailable: typeof route.onUnavailable === 'function',
|
|
107
|
+
fallback: route.fallback ? serializeRoute(route.fallback, id) : undefined,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class FlagEvaluationsWriter {
|
|
112
|
+
#enabled = false
|
|
113
|
+
#discoveryPending = true
|
|
114
|
+
#flushPending = false
|
|
115
|
+
#closed = false
|
|
116
|
+
#failed = false
|
|
117
|
+
#worker
|
|
118
|
+
#state = createWorkerState()
|
|
119
|
+
#batch = []
|
|
120
|
+
#batchTimer
|
|
121
|
+
#periodic
|
|
122
|
+
#deadline
|
|
123
|
+
#destroyer
|
|
124
|
+
#onAppClosing
|
|
125
|
+
#onFlush
|
|
126
|
+
#route
|
|
127
|
+
#routeId = 0
|
|
128
|
+
#serializedRoute
|
|
129
|
+
#context
|
|
130
|
+
#logging
|
|
131
|
+
#failureReason = 'worker_failure'
|
|
132
|
+
|
|
133
|
+
/** @param {import('../../config/config-base')} config */
|
|
134
|
+
constructor (config) {
|
|
135
|
+
this.#route = { url: /** @type {URL} */ (config.url), basePath: EVP_PROXY_PATH_V2 }
|
|
136
|
+
this.#logging = { enabled: config.debug === true, level: config.logLevel }
|
|
137
|
+
this.#context = {
|
|
138
|
+
service: typeof config.service === 'string' ? config.service : '',
|
|
139
|
+
env: typeof config.env === 'string' ? config.env : undefined,
|
|
140
|
+
version: typeof config.version === 'string' ? config.version : undefined,
|
|
141
|
+
}
|
|
142
|
+
this.#destroyer = () => this.destroy()
|
|
143
|
+
// Telemetry registers its shutdown sender before providers. Collect existing worker metrics before that send.
|
|
144
|
+
// Metrics produced by the later final drain remain best-effort; this does not coordinate telemetry shutdown.
|
|
145
|
+
this.#onAppClosing = () => collectWorkerTelemetry(this.#state)
|
|
146
|
+
this.#onFlush = () => this.flush()
|
|
147
|
+
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(this.#destroyer)
|
|
148
|
+
telemetryAppClosingCh.subscribe(this.#onAppClosing)
|
|
149
|
+
flushCh.subscribe(this.#onFlush)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
hasCapacity () {
|
|
153
|
+
return this.getUnavailableReason() === undefined &&
|
|
154
|
+
Atomics.load(this.#state, 0) < FLAG_EVALUATION_QUEUE_CAP && Atomics.load(this.#state, 1) < 0x7F_FF_F0_00
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @returns {'closed' | 'worker_failure' | 'unavailable' | undefined} */
|
|
158
|
+
getUnavailableReason () {
|
|
159
|
+
if (this.#closed) return 'closed'
|
|
160
|
+
if (this.#failed) return 'worker_failure'
|
|
161
|
+
return this.#enabled || this.#discoveryPending ? undefined : 'unavailable'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {boolean} enabled
|
|
166
|
+
* @param {FlagEvaluationRoute} [route]
|
|
167
|
+
*/
|
|
168
|
+
setEnabled (enabled, route) {
|
|
169
|
+
if (this.#closed || this.#failed) return
|
|
170
|
+
const discoveryPending = this.#discoveryPending
|
|
171
|
+
this.#discoveryPending = false
|
|
172
|
+
this.#routeId++
|
|
173
|
+
if (route) this.#route = route
|
|
174
|
+
if (!enabled) {
|
|
175
|
+
this.#discardPartial('unavailable')
|
|
176
|
+
this.#post({ type: 'enabled', enabled: false })
|
|
177
|
+
this.#enabled = false
|
|
178
|
+
this.#flushPending = false
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
this.#serializedRoute = serializeRoute(this.#route, this.#routeId)
|
|
183
|
+
this.#post({ type: 'enabled', enabled: true, route: this.#serializedRoute })
|
|
184
|
+
this.#enabled = !this.#failed
|
|
185
|
+
if (discoveryPending && this.#enabled) this.#drainDiscoveryQueue()
|
|
186
|
+
} catch {
|
|
187
|
+
this.#fail()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @param {FlagEvaluationEvent} event */
|
|
192
|
+
enqueue (event) {
|
|
193
|
+
if (!this.hasCapacity()) {
|
|
194
|
+
recordDropped(this.getUnavailableReason() ?? 'queue_overflow')
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
const targetingKey = normalizeTargetingKey(event.targetingKey)
|
|
198
|
+
if (targetingKey === undefined && event.targetingKey !== undefined && event.targetingKey !== null) {
|
|
199
|
+
recordTargetingKeyOmitted()
|
|
200
|
+
}
|
|
201
|
+
const normalized = normalizeFlagEvaluationEvent(event, targetingKey)
|
|
202
|
+
const consent = normalized.observeFullEvaluationData
|
|
203
|
+
Atomics.add(this.#state, 0, 1)
|
|
204
|
+
Atomics.add(this.#state, 1, 1)
|
|
205
|
+
this.#batch.push(normalized)
|
|
206
|
+
// Discovery uses the same queue credits; no worker or timer is needed until a route is known.
|
|
207
|
+
if (this.#discoveryPending) return true
|
|
208
|
+
// A consented event flushes any batch of 8+, so a mixed batch contains at most 8 context snapshots.
|
|
209
|
+
if (this.#batch.length >= (consent ? CONSENT_BATCH_SIZE : BATCH_SIZE)) this.#sendBatch()
|
|
210
|
+
else if (this.#batchTimer === undefined) {
|
|
211
|
+
this.#batchTimer = setTimeout(() => this.#sendBatch(), BATCH_DELAY_MS)
|
|
212
|
+
this.#batchTimer.unref?.()
|
|
213
|
+
}
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
flush () {
|
|
218
|
+
if (this.#closed || this.#failed) return
|
|
219
|
+
if (this.#discoveryPending) {
|
|
220
|
+
this.#flushPending = true
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
this.#sendBatch()
|
|
224
|
+
this.#post({ type: 'flush' })
|
|
225
|
+
collectWorkerTelemetry(this.#state)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
destroy () {
|
|
229
|
+
if (this.#closed) return
|
|
230
|
+
if (this.#discoveryPending) this.#discardPartial('closed')
|
|
231
|
+
else this.#sendBatch()
|
|
232
|
+
this.#closed = true
|
|
233
|
+
this.#enabled = false
|
|
234
|
+
this.#cleanup()
|
|
235
|
+
if (!this.#worker || this.#failed) return
|
|
236
|
+
// Keep only a bounded final drain alive. An unresponsive worker can delay graceful
|
|
237
|
+
// exit by up to five seconds; unref'ing the deadline would not bypass the ref'd worker.
|
|
238
|
+
// Ordinary traffic and idle workers never keep the app alive.
|
|
239
|
+
this.#worker.ref()
|
|
240
|
+
this.#deadline = setTimeout(() => {
|
|
241
|
+
this.#failureReason = 'shutdown_timeout'
|
|
242
|
+
this.#fail()
|
|
243
|
+
}, 5000)
|
|
244
|
+
this.#post({ type: 'close' })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#sendBatch () {
|
|
248
|
+
this.#cancelBatchTimer()
|
|
249
|
+
if (this.#batch.length === 0) return
|
|
250
|
+
if (!this.#worker) {
|
|
251
|
+
this.#startWorker()
|
|
252
|
+
if (this.#failed) return
|
|
253
|
+
}
|
|
254
|
+
const events = this.#batch
|
|
255
|
+
this.#batch = []
|
|
256
|
+
this.#post({ type: 'batch', events })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#drainDiscoveryQueue () {
|
|
260
|
+
const events = this.#batch
|
|
261
|
+
this.#batch = []
|
|
262
|
+
// Keep the normal message limits when releasing a potentially queue-sized startup backlog.
|
|
263
|
+
for (const event of events) {
|
|
264
|
+
this.#batch.push(event)
|
|
265
|
+
if (this.#batch.length >= (event.observeFullEvaluationData ? CONSENT_BATCH_SIZE : BATCH_SIZE)) {
|
|
266
|
+
this.#sendBatch()
|
|
267
|
+
if (this.#failed) return
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.#sendBatch()
|
|
271
|
+
if (this.#flushPending) {
|
|
272
|
+
this.#flushPending = false
|
|
273
|
+
this.flush()
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Providers that never evaluate need no worker. A first batch during destroy still uses the bounded drain.
|
|
278
|
+
#startWorker () {
|
|
279
|
+
try {
|
|
280
|
+
const { Worker } = require('node:worker_threads')
|
|
281
|
+
// Intentionally use the tracer's supported-config filter (which retains non-DD/OTEL env).
|
|
282
|
+
// Strip application preloads, but retain permissions and supported HTTP/TLS/network settings.
|
|
283
|
+
const { NODE_OPTIONS, ...env } = getEnvironmentVariables()
|
|
284
|
+
const execArgv = getWorkerArgs(NODE_OPTIONS)
|
|
285
|
+
// PnP has no node_modules fallback: retain only its active resolver, not application preloads.
|
|
286
|
+
// eslint-disable-next-line n/no-missing-require -- Yarn supplies this virtual module only in PnP applications.
|
|
287
|
+
if (process.versions.pnp) execArgv.push('--require', require.resolve('pnpapi'))
|
|
288
|
+
this.#worker = new Worker(join(__dirname, 'flag-evaluation-worker.js'), {
|
|
289
|
+
name: 'dd-flag-evaluation',
|
|
290
|
+
execArgv,
|
|
291
|
+
env,
|
|
292
|
+
workerData: {
|
|
293
|
+
route: this.#serializedRoute, context: this.#context, state: this.#state.buffer, logging: this.#logging,
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
this.#worker.on('error', error => this.#fail(
|
|
297
|
+
error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND' ? 'missing_module' : 'worker_error'
|
|
298
|
+
))
|
|
299
|
+
this.#worker.on('messageerror', () => this.#fail('message_error'))
|
|
300
|
+
this.#worker.on('message', message => {
|
|
301
|
+
if (message?.type === 'log') this.#onLogMessage(message)
|
|
302
|
+
else this.#onRouteMessage(message)
|
|
303
|
+
})
|
|
304
|
+
this.#worker.once('exit', () => this.#exited())
|
|
305
|
+
this.#worker.unref?.()
|
|
306
|
+
this.#periodic = setInterval(() => collectWorkerTelemetry(this.#state), FLAG_EVALUATION_FLUSH_INTERVAL)
|
|
307
|
+
this.#periodic.unref?.()
|
|
308
|
+
} catch {
|
|
309
|
+
this.#fail('startup_error')
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** @param {{ level: string, message: string }} message */
|
|
314
|
+
#onLogMessage ({ level, message }) {
|
|
315
|
+
if (typeof message !== 'string') return
|
|
316
|
+
// Relay diagnostics without introducing new error-telemetry reports.
|
|
317
|
+
if (level === 'error') log.errorWithoutTelemetry('%s', message)
|
|
318
|
+
else if (level === 'debug' || level === 'info' || level === 'warn') log[level]('%s', message)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @param {{ type: 'route', id: number, status: 'fallback' | 'unavailable' }} message */
|
|
322
|
+
#onRouteMessage (message) {
|
|
323
|
+
if (this.#closed || this.#failed || !this.#enabled ||
|
|
324
|
+
message?.type !== 'route' || message.id !== this.#routeId) return
|
|
325
|
+
const callback = message.status === 'fallback'
|
|
326
|
+
? this.#route.onFallback
|
|
327
|
+
: message.status === 'unavailable' ? this.#route.onUnavailable : undefined
|
|
328
|
+
if (!callback) return
|
|
329
|
+
// Consume the transition once. The strategy may synchronously publish a replacement route.
|
|
330
|
+
this.#routeId++
|
|
331
|
+
try {
|
|
332
|
+
callback()
|
|
333
|
+
} catch {
|
|
334
|
+
this.#fail('route_error')
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** @param {object} message */
|
|
339
|
+
#post (message) {
|
|
340
|
+
if (!this.#worker || this.#failed) return
|
|
341
|
+
try {
|
|
342
|
+
this.#worker.postMessage(message)
|
|
343
|
+
} catch {
|
|
344
|
+
this.#fail()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#cancelBatchTimer () {
|
|
349
|
+
if (this.#batchTimer !== undefined) clearTimeout(this.#batchTimer)
|
|
350
|
+
this.#batchTimer = undefined
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @param {string} reason */
|
|
354
|
+
#discardPartial (reason) {
|
|
355
|
+
this.#cancelBatchTimer()
|
|
356
|
+
const count = this.#batch.length
|
|
357
|
+
this.#batch = []
|
|
358
|
+
Atomics.sub(this.#state, 0, count)
|
|
359
|
+
Atomics.sub(this.#state, 1, count)
|
|
360
|
+
recordDropped(reason, count)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#cleanup () {
|
|
364
|
+
clearInterval(this.#periodic)
|
|
365
|
+
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.delete(this.#destroyer)
|
|
366
|
+
telemetryAppClosingCh.unsubscribe(this.#onAppClosing)
|
|
367
|
+
flushCh.unsubscribe(this.#onFlush)
|
|
368
|
+
collectWorkerTelemetry(this.#state)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** @param {string} [reason] - Internal diagnostic only; never include raw error messages or paths. */
|
|
372
|
+
#fail (reason = 'worker_error') {
|
|
373
|
+
if (this.#failed) return
|
|
374
|
+
this.#failed = true
|
|
375
|
+
this.#enabled = false
|
|
376
|
+
this.#cancelBatchTimer()
|
|
377
|
+
this.#batch = []
|
|
378
|
+
this.#cleanup()
|
|
379
|
+
log.warn('Flag evaluation counts disabled after worker failure (%s)', reason)
|
|
380
|
+
if (this.#worker) this.#worker.terminate()
|
|
381
|
+
else this.#exited()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#exited () {
|
|
385
|
+
clearTimeout(this.#deadline)
|
|
386
|
+
this.#failed = true
|
|
387
|
+
this.#enabled = false
|
|
388
|
+
this.#worker = undefined
|
|
389
|
+
this.#cancelBatchTimer()
|
|
390
|
+
this.#batch = []
|
|
391
|
+
this.#cleanup()
|
|
392
|
+
// Only settle ownership once the worker cannot mutate counters anymore.
|
|
393
|
+
recordDropped(this.#failureReason, Atomics.exchange(this.#state, 1, 0))
|
|
394
|
+
Atomics.store(this.#state, 0, 0)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = FlagEvaluationsWriter
|
|
@@ -34,7 +34,7 @@ function warnExposureDeliveryUnavailable () {
|
|
|
34
34
|
/**
|
|
35
35
|
* Preserves the one-time EVP v2 capability check for the Remote Configuration source.
|
|
36
36
|
*
|
|
37
|
-
* @param {import('../../config')} config - Tracer configuration object
|
|
37
|
+
* @param {import('../../config/config-base')} config - Tracer configuration object
|
|
38
38
|
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
|
|
39
39
|
*/
|
|
40
40
|
function setAgentStrategy (config, setWriterEnabledValue) {
|
|
@@ -64,7 +64,7 @@ function setAgentStrategy (config, setWriterEnabledValue) {
|
|
|
64
64
|
* Local discovery is optional for delivery. A missing listener, discovery
|
|
65
65
|
* error, or incompatible receiver selects direct intake when credentials exist.
|
|
66
66
|
*
|
|
67
|
-
* @param {import('../../config')} config - Tracer configuration object
|
|
67
|
+
* @param {import('../../config/config-base')} config - Tracer configuration object
|
|
68
68
|
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
|
|
69
69
|
*/
|
|
70
70
|
function setAgentlessStrategy (config, setWriterEnabledValue) {
|
|
@@ -169,8 +169,9 @@ function setAgentlessStrategy (config, setWriterEnabledValue) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Applies one event-delivery strategy that can be shared by all Feature Flags writers.
|
|
171
171
|
*
|
|
172
|
-
* @param {import('../../config')} config - Tracer configuration object
|
|
173
|
-
* @param {
|
|
172
|
+
* @param {import('../../config/config-base')} config - Tracer configuration object
|
|
173
|
+
* @param {(enabled: boolean, route?: import('./flag-evaluations').FlagEvaluationRoute) => void} setWriterEnabledValue
|
|
174
|
+
* Callback to set the writer enabled state
|
|
174
175
|
*/
|
|
175
176
|
function setEventDeliveryStrategy (config, setWriterEnabledValue) {
|
|
176
177
|
if (config.featureFlags?.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE === 'agentless') {
|
|
@@ -93,6 +93,9 @@ class ContextManager {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
bind (context, target) {
|
|
96
|
+
// Per the OTel contract, only functions are wrapped; other targets are returned as-is.
|
|
97
|
+
if (typeof target !== 'function') return target
|
|
98
|
+
|
|
96
99
|
const self = this
|
|
97
100
|
return function (...args) {
|
|
98
101
|
return self.with(context, target, this, ...args)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const api = require('@opentelemetry/api')
|
|
4
4
|
const { AUTO_KEEP } = require('../../../../ext/priority')
|
|
5
|
+
const { updateOtelTraceState } = require('../otel-sampling')
|
|
5
6
|
const DatadogSpanContext = require('../opentracing/span_context')
|
|
7
|
+
const TraceState = require('../opentracing/propagation/tracestate')
|
|
6
8
|
const id = require('../id')
|
|
7
9
|
|
|
8
10
|
function newContext () {
|
|
@@ -37,8 +39,10 @@ class SpanContext {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
get traceState () {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
this._ddContext._ensureSamplingPriority()
|
|
43
|
+
const traceState = this._ddContext._tracestate?.clone() ?? new TraceState()
|
|
44
|
+
updateOtelTraceState(this._ddContext, traceState)
|
|
45
|
+
return api.createTraceState(traceState.toString())
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
|
|
4
4
|
const { getProtobufTypes } = require('../otlp/protobuf_loader')
|
|
5
|
+
const { AUTO_KEEP } = require('../../../../../ext/priority')
|
|
5
6
|
const { VERSION } = require('../../../../../version')
|
|
7
|
+
const { SAMPLING_PRIORITY_KEY } = require('../../constants')
|
|
6
8
|
const id = require('../../id')
|
|
7
9
|
const { eventTimeNano } = require('../../encode/tags-processors')
|
|
8
10
|
|
|
@@ -52,6 +54,7 @@ const TRACE_ID_128 = '_dd.p.tid'
|
|
|
52
54
|
* @property {number} start - Start time in nanoseconds since epoch
|
|
53
55
|
* @property {number} duration - Duration in nanoseconds
|
|
54
56
|
* @property {DDSpanEvent[]} [span_events] - Span events
|
|
57
|
+
* @property {string} [trace_state] - W3C tracestate for OTLP export
|
|
55
58
|
*/
|
|
56
59
|
|
|
57
60
|
// Map DD span.kind string values to OTLP SpanKind numeric values
|
|
@@ -161,11 +164,13 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
161
164
|
#transformSpan (span, traceIdHigh) {
|
|
162
165
|
const parentId = span.parent_id
|
|
163
166
|
const links = this.#extractLinks(span.meta?.['_dd.span_links'])
|
|
167
|
+
const samplingPriority = span.metrics?.[SAMPLING_PRIORITY_KEY]
|
|
164
168
|
|
|
165
169
|
return {
|
|
166
170
|
traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
|
|
167
171
|
spanId: this.#idToBytes(span.span_id, 8),
|
|
168
172
|
parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
|
|
173
|
+
traceState: span.trace_state,
|
|
169
174
|
name: span.resource,
|
|
170
175
|
kind: this.#mapSpanKind(span.meta?.['span.kind']),
|
|
171
176
|
startTimeUnixNano: span.start,
|
|
@@ -177,6 +182,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
177
182
|
links: links.length ? links : undefined,
|
|
178
183
|
droppedLinksCount: 0,
|
|
179
184
|
status: this.#mapStatus(span),
|
|
185
|
+
flags: typeof samplingPriority === 'number' ? (samplingPriority >= AUTO_KEEP ? 1 : 0) : undefined,
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
@@ -8,6 +8,7 @@ const tracer = require('../../')
|
|
|
8
8
|
|
|
9
9
|
const id = require('../id')
|
|
10
10
|
const log = require('../log')
|
|
11
|
+
const { normalizeOtelTraceState } = require('../otel-sampling')
|
|
11
12
|
const TraceState = require('../opentracing/propagation/tracestate')
|
|
12
13
|
const SpanContext = require('./span_context')
|
|
13
14
|
const Span = require('./span')
|
|
@@ -74,12 +75,12 @@ class Tracer {
|
|
|
74
75
|
_convertOtelContextToDatadog (traceId, spanId, traceFlag, ts, meta = {}) {
|
|
75
76
|
let origin = null
|
|
76
77
|
let samplingPriority = traceFlag
|
|
78
|
+
let samplingMechanism
|
|
79
|
+
const traceStateValue = typeof ts?.serialize === 'function' ? ts.serialize() : ts?.traceparent
|
|
80
|
+
const traceState = TraceState.fromString(traceStateValue)
|
|
81
|
+
normalizeOtelTraceState(traceState)
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (ts) {
|
|
81
|
-
// Use TraceState.fromString to parse the tracestate header
|
|
82
|
-
const traceState = TraceState.fromString(ts)
|
|
83
|
+
if (traceStateValue) {
|
|
83
84
|
let ddTraceStateData = null
|
|
84
85
|
|
|
85
86
|
// Extract Datadog specific trace state data
|
|
@@ -89,29 +90,26 @@ class Tracer {
|
|
|
89
90
|
})
|
|
90
91
|
|
|
91
92
|
if (ddTraceStateData) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Update meta and samplingPriority based on extracted values
|
|
100
|
-
Object.assign(meta, otherPropagatedTags)
|
|
101
|
-
// Guard against an undefined/empty `s:` field that would result in NaN.
|
|
102
|
-
const tracestateSamplingPriority = samplingPriorityTs ? Math.trunc(samplingPriorityTs) : undefined
|
|
93
|
+
const priority = Number.parseInt(ddTraceStateData.get('s'), 10)
|
|
94
|
+
const tracestateSamplingPriority = Number.isInteger(priority) ? priority : undefined
|
|
95
|
+
origin = ddTraceStateData.get('o')?.replaceAll('~', '=') ?? null
|
|
96
|
+
|
|
97
|
+
const mechanism = Math.abs(Number.parseInt(ddTraceStateData.get('t.dm'), 10))
|
|
98
|
+
if (Number.isInteger(mechanism)) samplingMechanism = mechanism
|
|
99
|
+
|
|
103
100
|
samplingPriority = getSamplingPriority(traceFlag, tracestateSamplingPriority, origin)
|
|
104
101
|
} else {
|
|
105
|
-
log.debug('No dd list member in tracestate from incoming request:',
|
|
102
|
+
log.debug('No dd list member in tracestate from incoming request:', traceStateValue)
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
const spanContext = new SpanContext({
|
|
110
|
-
traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16),
|
|
107
|
+
traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16), tracestate: traceState,
|
|
111
108
|
})
|
|
112
109
|
|
|
113
110
|
spanContext._ddContext._sampling = { priority: samplingPriority }
|
|
114
111
|
spanContext._ddContext._trace = { ...spanContext._ddContext._trace, origin }
|
|
112
|
+
if (samplingMechanism !== undefined) spanContext._ddContext._sampling.mechanism = samplingMechanism
|
|
115
113
|
return spanContext
|
|
116
114
|
}
|
|
117
115
|
|