Skip to content

Commit e869004

Browse files
authored
fix(otel): honor DD_TRACE_OTEL_ENABLED=false and OTEL_SDK_DISABLED=false (#8219)
* fix(otel): honor DD_TRACE_OTEL_ENABLED=false and OTEL_SDK_DISABLED=false `otel-sdk-trace.js` and the `otel_enabled` telemetry tag in `opentracing/span.js` checked these env vars for raw truthiness, which made the string `'false'` itself truthy and mishandled the OTel spec's positive opt-in form (`OTEL_SDK_DISABLED=false`). Replace the truthiness check with explicit precedence: the Datadog opt-out (`DD_TRACE_OTEL_ENABLED=false`) wins over every OTel signal, then the OTel opt-out (`OTEL_SDK_DISABLED=true`) wins over the opt-in side, then either explicit opt-in enables it; otherwise stay disabled by default. Fixes: #4873 * refactor(span): read config off the parent tracer DatadogSpan was reading `DD_TRACE_OTEL_ENABLED`, the experimental flags, and `DD_TRACE_SPAN_LEAK_DEBUG` from `getValueFromEnvSources` at module load. Anything that updated those values through the Config singleton (remote config, programmatic options) never reached the span gates because the gates had captured the env-time value. Read them off `tracer._config` instead — the opentracing tracer already holds the singleton. Three coordinated pieces: 1. Convert `_parentTracer` to a `#parentTracer` private field. There are no external readers; `tracer()` is the public accessor. While in the file, fix three sites that were reading from a non-existent `this._tracer`. 2. Drop the comment block above the span-leak gate. It justified the `tracer?._config?.X` nullish chain that this refactor replaces. 3. The four spec files constructing `new DatadogSpan(...)` with `tracer = {}` (or `null`, or `{}` inline) now pass `tracer = { _config: getConfig() }`. The two `tracePropagationBehaviorExtract` overrides spread the real config so the constructor's other reads see real defaults. Drive-by fix: * Drop a long-dead commented-out `registry.register` line in spanleak.js, and the unused `operationName` argument span.js was passing to `addSpan(span)`.
1 parent f67ac4d commit e869004

8 files changed

Lines changed: 136 additions & 46 deletions

File tree

packages/datadog-instrumentations/src/otel-sdk-trace.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@
33
const shimmer = require('../../datadog-shimmer')
44
const tracer = require('../../dd-trace')
55
const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
6+
const { isFalse, isTrue } = require('../../dd-trace/src/util')
67
const { addHook } = require('./helpers/instrument')
78

8-
const otelSdkEnabled = getValueFromEnvSources('DD_TRACE_OTEL_ENABLED') ||
9-
getValueFromEnvSources('OTEL_SDK_DISABLED')
10-
? !getValueFromEnvSources('OTEL_SDK_DISABLED')
11-
: undefined
12-
13-
if (otelSdkEnabled) {
9+
if (isOtelSdkEnabled()) {
1410
addHook({
1511
name: '@opentelemetry/sdk-trace-node',
1612
file: 'build/src/NodeTracerProvider.js',
@@ -22,3 +18,12 @@ if (otelSdkEnabled) {
2218
return mod
2319
})
2420
}
21+
22+
function isOtelSdkEnabled () {
23+
// Datadog explicit opt-out wins over every OTel signal; check it first.
24+
const ddTraceOtelEnabled = getValueFromEnvSources('DD_TRACE_OTEL_ENABLED')
25+
if (isFalse(ddTraceOtelEnabled)) return false
26+
const otelSdkDisabled = getValueFromEnvSources('OTEL_SDK_DISABLED')
27+
if (isTrue(otelSdkDisabled)) return false
28+
return isTrue(ddTraceOtelEnabled) || isFalse(otelSdkDisabled)
29+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
5+
const { describe, it } = require('mocha')
6+
const proxyquire = require('proxyquire').noPreserveCache()
7+
const sinon = require('sinon')
8+
9+
describe('otel-sdk-trace', () => {
10+
/**
11+
* Re-load `otel-sdk-trace.js` with the supplied env values stubbed in via
12+
* `getValueFromEnvSources` and report what `addHook` saw.
13+
*
14+
* @param {{ ddTraceOtelEnabled?: string, otelSdkDisabled?: string }} env
15+
* @param {{ TracerProvider?: unknown }} [tracerStub]
16+
* @returns {{ addHook: sinon.SinonSpy, wrap: sinon.SinonSpy }}
17+
*/
18+
function loadWithEnv (env, tracerStub = {}) {
19+
const addHook = sinon.spy()
20+
const wrap = sinon.spy()
21+
const values = {
22+
DD_TRACE_OTEL_ENABLED: env.ddTraceOtelEnabled,
23+
OTEL_SDK_DISABLED: env.otelSdkDisabled,
24+
}
25+
proxyquire('../src/otel-sdk-trace', {
26+
'../../datadog-shimmer': { wrap },
27+
'../../dd-trace': tracerStub,
28+
'../../dd-trace/src/config/helper': {
29+
getValueFromEnvSources: (name) => values[name],
30+
},
31+
'./helpers/instrument': { addHook },
32+
})
33+
return { addHook, wrap }
34+
}
35+
36+
describe('gate precedence', () => {
37+
it('disables when DD_TRACE_OTEL_ENABLED is the explicit opt-out', () => {
38+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: 'false' }).addHook.called, false)
39+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: '0' }).addHook.called, false)
40+
})
41+
42+
it('keeps DD opt-out winning even when OTEL_SDK_DISABLED=false opts in', () => {
43+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: 'false', otelSdkDisabled: 'false' }).addHook.called, false)
44+
})
45+
46+
it('disables when OTEL_SDK_DISABLED is the explicit opt-out', () => {
47+
assert.equal(loadWithEnv({ otelSdkDisabled: 'true' }).addHook.called, false)
48+
assert.equal(loadWithEnv({ otelSdkDisabled: '1' }).addHook.called, false)
49+
})
50+
51+
it('keeps OTel opt-out winning even when DD_TRACE_OTEL_ENABLED=true opts in', () => {
52+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: 'true', otelSdkDisabled: 'true' }).addHook.called, false)
53+
})
54+
55+
it('enables when DD_TRACE_OTEL_ENABLED is the explicit opt-in', () => {
56+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: 'true' }).addHook.called, true)
57+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: '1' }).addHook.called, true)
58+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: 'true', otelSdkDisabled: 'false' }).addHook.called, true)
59+
})
60+
61+
it('enables when OTEL_SDK_DISABLED=false is the OTel positive opt-in', () => {
62+
assert.equal(loadWithEnv({ otelSdkDisabled: 'false' }).addHook.called, true)
63+
assert.equal(loadWithEnv({ otelSdkDisabled: '0' }).addHook.called, true)
64+
})
65+
66+
it('stays disabled by default when neither env var is set', () => {
67+
assert.equal(loadWithEnv({}).addHook.called, false)
68+
})
69+
70+
it('stays disabled for unrecognized values on either side', () => {
71+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: 'maybe', otelSdkDisabled: 'sure' }).addHook.called, false)
72+
assert.equal(loadWithEnv({ ddTraceOtelEnabled: '' }).addHook.called, false)
73+
})
74+
})
75+
76+
describe('hook registration', () => {
77+
it('wraps NodeTracerProvider with the dd-trace TracerProvider', () => {
78+
const tracerProvider = function FakeTracerProvider () {}
79+
const { addHook, wrap } = loadWithEnv({ ddTraceOtelEnabled: 'true' }, { TracerProvider: tracerProvider })
80+
81+
sinon.assert.calledOnce(addHook)
82+
const [hookOptions, transform] = addHook.firstCall.args
83+
assert.deepStrictEqual(hookOptions, {
84+
name: '@opentelemetry/sdk-trace-node',
85+
file: 'build/src/NodeTracerProvider.js',
86+
versions: ['*'],
87+
})
88+
89+
const mod = { NodeTracerProvider: function OriginalProvider () {} }
90+
assert.equal(transform(mod), mod)
91+
92+
sinon.assert.calledOnceWithExactly(wrap, mod, 'NodeTracerProvider', sinon.match.func)
93+
assert.equal(wrap.firstCall.args[2](), tracerProvider)
94+
})
95+
})
96+
})

packages/dd-trace/src/opentracing/span.js

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,16 @@ const runtimeMetrics = require('../runtime_metrics')
1111
const log = require('../log')
1212
const { storage } = require('../../../datadog-core')
1313
const telemetryMetrics = require('../telemetry/metrics')
14-
const { getValueFromEnvSources } = require('../config/helper')
15-
const { isTrue } = require('../util')
1614
const SpanContext = require('./span_context')
1715

1816
const dateNow = Date.now
1917

2018
const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
2119

22-
const DD_TRACE_EXPERIMENTAL_STATE_TRACKING = isTrue(getValueFromEnvSources('DD_TRACE_EXPERIMENTAL_STATE_TRACKING'))
23-
const DD_TRACE_EXPERIMENTAL_SPAN_COUNTS = isTrue(getValueFromEnvSources('DD_TRACE_EXPERIMENTAL_SPAN_COUNTS'))
24-
2520
const unfinishedRegistry = createRegistry('unfinished')
2621
const finishedRegistry = createRegistry('finished')
2722

28-
const OTEL_ENABLED = !!getValueFromEnvSources('DD_TRACE_OTEL_ENABLED')
23+
let OTEL_ENABLED = false
2924
const ALLOWED = new Set(['string', 'number', 'boolean'])
3025

3126
const integrationCounters = {
@@ -55,15 +50,19 @@ function getIntegrationCounter (event, integration) {
5550
}
5651

5752
class DatadogSpan {
53+
#parentTracer
54+
5855
constructor (tracer, processor, prioritySampler, fields, debug) {
56+
OTEL_ENABLED = tracer._config.DD_TRACE_OTEL_ENABLED
57+
5958
const operationName = fields.operationName
6059
const parent = fields.parent || null
6160
// TODO(BridgeAR): Investigate why this is causing a performance regression
6261
// eslint-disable-next-line prefer-object-spread
6362
const tags = Object.assign({}, fields.tags)
6463
const hostname = fields.hostname
6564

66-
this._parentTracer = tracer
65+
this.#parentTracer = tracer
6766
this._debug = debug
6867
this._processor = processor
6968
this._prioritySampler = prioritySampler
@@ -94,7 +93,7 @@ class DatadogSpan {
9493
attributes: this._sanitizeAttributes(link.attributes),
9594
})) ?? []
9695

97-
if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
96+
if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
9897
runtimeMetrics.increment('runtime.node.spans.unfinished')
9998
runtimeMetrics.increment('runtime.node.spans.unfinished.by.name', `span_name:${operationName}`)
10099

@@ -104,16 +103,8 @@ class DatadogSpan {
104103
unfinishedRegistry.register(this, operationName, this)
105104
}
106105

107-
// Nullish operator is used here because both `tracer` and `tracer._config`
108-
// can be null and there are tests passing invalid values to the `Span`
109-
// constructor which still succeed today. Part of the problem is that `Span`
110-
// stores only the tracer and not the config, so anything that needs the
111-
// config has to read it from the tracer stored on the span, including
112-
// even `Span` itself in this case.
113-
//
114-
// TODO: Refactor Tracer/Span + tests to avoid having to do nullish checks.
115-
if (tracer?._config?.DD_TRACE_SPAN_LEAK_DEBUG > 0) {
116-
require('../spanleak').addSpan(this, operationName)
106+
if (tracer._config.DD_TRACE_SPAN_LEAK_DEBUG > 0) {
107+
require('../spanleak').addSpan(this)
117108
}
118109

119110
if (startCh.hasSubscribers) {
@@ -124,7 +115,7 @@ class DatadogSpan {
124115
[util.inspect.custom] () {
125116
return {
126117
...this,
127-
_parentTracer: `[${this._parentTracer.constructor.name}]`,
118+
parentTracer: `[${this.#parentTracer.constructor.name}]`,
128119
_prioritySampler: `[${this._prioritySampler.constructor.name}]`,
129120
_processor: `[${this._processor.constructor.name}]`,
130121
}
@@ -156,7 +147,7 @@ class DatadogSpan {
156147
}
157148

158149
tracer () {
159-
return this._parentTracer
150+
return this.#parentTracer
160151
}
161152

162153
setOperationName (name) {
@@ -254,14 +245,14 @@ class DatadogSpan {
254245
return
255246
}
256247

257-
if (DD_TRACE_EXPERIMENTAL_STATE_TRACKING && !this._spanContext._tags['service.name']) {
248+
if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_STATE_TRACKING && !this._spanContext._tags['service.name']) {
258249
log.error('Finishing invalid span: %s', this)
259250
}
260251

261252
getIntegrationCounter('spans_finished', this._integrationName).inc()
262253
this._spanContext._tags['_dd.integration'] = this._integrationName
263254

264-
if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
255+
if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
265256
runtimeMetrics.decrement('runtime.node.spans.unfinished')
266257
runtimeMetrics.decrement('runtime.node.spans.unfinished.by.name', `span_name:${this._name}`)
267258
runtimeMetrics.increment('runtime.node.spans.finished')
@@ -336,7 +327,7 @@ class DatadogSpan {
336327
let startTime
337328

338329
let baggage = {}
339-
const propagationBehavior = this._parentTracer?._config?.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
330+
const propagationBehavior = this.#parentTracer._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
340331
if (parent && parent._isRemote && propagationBehavior !== 'continue') {
341332
baggage = parent._baggageItems
342333
parent = null

packages/dd-trace/src/spanleak.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ module.exports.addSpan = function (span) {
8585
const expiration = now + LIFETIME
8686
const wrapped = new WeakRef(span)
8787
spans.add(wrapped, expiration)
88-
// registry.register(span, span._name)
8988
}
9089

9190
function isEnabled () {

packages/dd-trace/test/llmobs/util.spec.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const assert = require('node:assert/strict')
44

55
const { before, describe, it } = require('mocha')
6+
7+
const getConfig = require('../../src/config')
68
const {
79
encodeUnicode,
810
getFunctionArguments,
@@ -169,29 +171,31 @@ describe('util', () => {
169171

170172
describe('spanHasError', () => {
171173
let Span
174+
let tracer
172175
let ps
173176

174177
before(() => {
175178
Span = require('../../src/opentracing/span')
179+
tracer = { _config: getConfig() }
176180
ps = {
177181
sample () {},
178182
}
179183
})
180184

181185
it('returns false when there is no error', () => {
182-
const span = new Span(null, null, ps, {})
186+
const span = new Span(tracer, null, ps, {})
183187
assert.strictEqual(spanHasError(span), false)
184188
})
185189

186190
it('returns true if the span has an "error" tag', () => {
187-
const span = new Span(null, null, ps, {})
191+
const span = new Span(tracer, null, ps, {})
188192
span.setTag('error', true)
189193
assert.strictEqual(spanHasError(span), true)
190194
})
191195

192196
it('returns true if the span has the error properties as tags', () => {
193197
const err = new Error('boom')
194-
const span = new Span(null, null, ps, {})
198+
const span = new Span(tracer, null, ps, {})
195199

196200
span.setTag('error.type', err.name)
197201
span.setTag('error.msg', err.message)

packages/dd-trace/test/opentracing/span.spec.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('Span', () => {
3333
id.onFirstCall().returns('123')
3434
id.onSecondCall().returns('456')
3535

36-
tracer = {}
36+
tracer = { _config: getConfig() }
3737

3838
processor = {
3939
process: sinon.stub(),
@@ -499,21 +499,13 @@ describe('Span', () => {
499499
})
500500

501501
it('should not propagate baggage items when Trace_Propagation_Behavior_Extract is set to ignore', () => {
502-
tracer = {
503-
_config: {
504-
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: 'ignore',
505-
},
506-
}
502+
tracer = { _config: { ...getConfig(), DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: 'ignore' } }
507503
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation', parent })
508504
assert.deepStrictEqual(span._spanContext._baggageItems, {})
509505
})
510506

511507
it('should propagate baggage items when Trace_Propagation_Behavior_Extract is set to restart', () => {
512-
tracer = {
513-
_config: {
514-
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: 'restart',
515-
},
516-
}
508+
tracer = { _config: { ...getConfig(), DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: 'restart' } }
517509
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation', parent })
518510
assert.deepStrictEqual(span._spanContext._baggageItems, { foo: 'bar' })
519511
})

packages/dd-trace/test/standalone/index.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const proxyquire = require('proxyquire')
88
const { channel } = require('dc-polyfill')
99

1010
require('../setup/core')
11+
const getConfig = require('../../src/config')
1112
const standalone = require('../../src/standalone')
1213
const DatadogSpan = require('../../src/opentracing/span')
1314

@@ -40,7 +41,7 @@ describe('Disabled APM Tracing or Standalone', () => {
4041
},
4142
}
4243

43-
tracer = {}
44+
tracer = { _config: getConfig() }
4445
processor = {}
4546
prioritySampler = {}
4647
})

packages/dd-trace/test/standalone/tracesource_priority_sampler.spec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const proxyquire = require('proxyquire')
88

99
require('../setup/core')
1010
const { USER_KEEP, AUTO_KEEP } = require('../../../../ext/priority')
11+
const getConfig = require('../../src/config')
1112
const DatadogSpan = require('../../src/opentracing/span')
1213
const TraceSourcePrioritySampler = require('../../src/standalone/tracesource_priority_sampler')
1314
const { TRACE_SOURCE_PROPAGATION_KEY } = require('../../src/constants')
@@ -36,7 +37,8 @@ describe('Disabled APM Tracing or Standalone - TraceSourcePrioritySampler', () =
3637

3738
describe('sample', () => {
3839
it('should provide the context when invoking _getPriorityFromTags', () => {
39-
const span = new DatadogSpan({}, {}, prioritySampler, {
40+
const tracer = { _config: getConfig() }
41+
const span = new DatadogSpan(tracer, {}, prioritySampler, {
4042
operationName: 'operation',
4143
})
4244

0 commit comments

Comments
 (0)