Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 19 additions & 46 deletions packages/dd-trace/src/opentelemetry/context_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { trace, ROOT_CONTEXT, propagation } = require('@opentelemetry/api')
const { storage } = require('../../../datadog-core')
const { getAllBaggageItems, setBaggageItem, removeAllBaggageItems } = require('../baggage')

const tracer = require('../../')
const SpanContext = require('./span_context')
Expand All @@ -19,31 +20,26 @@ class ContextManager {

const storedSpan = store ? trace.getSpan(store) : null

// Convert DD baggage to OTel format
const baggages = getAllBaggageItems()
const hasBaggage = Object.keys(baggages).length > 0
let otelBaggages
if (hasBaggage) {
const entries = {}
for (const [key, value] of Object.entries(baggages)) {
entries[key] = { value }
}
otelBaggages = propagation.createBaggage(entries)
}

// If stored span wraps the active DD span, prefer the stored context
if (storedSpan && storedSpan._ddSpan === activeSpan) {
const baggages = JSON.parse(activeSpan.getAllBaggageItems())
if (Object.keys(baggages).length > 0) {
const entries = {}
for (const [key, value] of Object.entries(baggages)) {
entries[key] = { value }
}
const otelBaggages = propagation.createBaggage(entries)
return propagation.setBaggage(store, otelBaggages)
}
if (otelBaggages) return propagation.setBaggage(store, otelBaggages)
return store
}

if (!activeSpan) {
const storedBaggageItems = storedSpan?._spanContext?._ddContext?._baggageItems
if (storedBaggageItems) {
const baggages = storedBaggageItems
const entries = {}
for (const [key, value] of Object.entries(baggages)) {
entries[key] = { value }
}
const otelBaggages = propagation.createBaggage(entries)
return propagation.setBaggage(baseContext, otelBaggages)
}
if (otelBaggages) return propagation.setBaggage(baseContext, otelBaggages)
return baseContext
}

Expand All @@ -53,18 +49,6 @@ class ContextManager {
ddContext._otelSpanContext = new SpanContext(ddContext)
}

// Convert DD baggage to OTel format
const baggages = JSON.parse(activeSpan.getAllBaggageItems())
const hasBaggage = Object.keys(baggages).length > 0
let otelBaggages
if (hasBaggage) {
const entries = {}
for (const [key, value] of Object.entries(baggages)) {
entries[key] = { value }
}
otelBaggages = propagation.createBaggage(entries)
}

if (store && trace.getSpanContext(store) === ddContext._otelSpanContext) {
return otelBaggages ? propagation.setBaggage(store, otelBaggages) : store
}
Expand All @@ -86,22 +70,11 @@ class ContextManager {
if (baggages) {
baggageItems = baggages.getAllEntries()
}
if (span && span._ddSpan) {
// does otel always override datadog?
span._ddSpan.removeAllBaggageItems()
for (const baggage of baggageItems) {
span._ddSpan.setBaggageItem(baggage[0], baggage[1].value)
}
return ddScope.activate(span._ddSpan, run)
}
// span instanceof NonRecordingSpan
const ddContext = span?._spanContext?._ddContext
if (ddContext && ddContext._baggageItems) {
ddContext._baggageItems = {}
for (const baggage of baggageItems) {
ddContext._baggageItems[baggage[0]] = baggage[1].value
}
removeAllBaggageItems()
for (const baggage of baggageItems) {
setBaggageItem(baggage[0], baggage[1].value)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate about this? I am unsure what this does.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is calling the global baggage APIs to make sure the currently active baggage is in sync with baggage on the context. It essentially does the same thing as these operations but for global baggage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Otel baggage overrule DD baggage? Should it be merged? Do we have any documentation or RFCs around that or was there any discussion around that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. The code lets whichever comes later override, but I'm not sure if this is what's intended. @rachelyangdog @zacharycmontoya do you think we need to discuss this before proceeding?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so turns out dd baggage should always override otel baggage, updated the code

Copy link
Collaborator Author

@ida613 ida613 Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I support this approach. Legacy baggage won't be included in this support. We will only keep the Current OTel API Baggage in sync with the global Datadog baggage that's tracked independently of spans.

Thank you Zach! I can do this no problem, but currently in node, dd and otel baggages only get synced when we call active or with. In order to do what dotnet does (syncing on each individual operation), we would have to replace the otel baggage operations within dd-trace, which will involve a ton of work. Is it acceptable to only sync when calling with or active?

Did we already establish a precedence for this scenario or are we only now adding that? My preference would be to prefer global baggage, aka from the baggage HTTP header. How feasible would that be and do you see any issues with that preference?

Sounds good with me, it's not hard to do. I'll work on it :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think it should be fine to only update when calling active or with. Let me describe the interactions I expect, but if my understanding is wrong please point it out so we're operating off the same understanding.

DD API

Each time the user wants to read/write to the active global baggage, they call one of the Baggage API's here. Notably, when clearing or setting baggage, these API's will automatically update the current baggage before returning to the caller. As long as we instrument each time active or with is called in the OpenTelemetry context manager, we don't need to update any code here.

OTel API

Each time the user wants to read/write to the active global baggage, they will need to get the current baggage by calling getActiveBagage which gets the current context by calling active. Then, once the user has updated the baggage and wants to set it as current, the user must attach the baggage to a Context using the OTel API setBaggage and call the api.context.with API with the Context and a callback, so the baggage will become active.

So with this flow, we just need our active operation to copy over the Datadog global baggage to overwrite all the OpenTelemetry baggage in the context and our with operation to copy over the OpenTelemetry baggage to override all the Datadog baggage.

And actually, this is exactly what my .NET implementation does! 😄 It just turns out that the .NET OTel Baggage API's directly set the "current" OTel baggage API within each API call (Set/Remove) so we just have to do the active/with implementation in more places

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks Zach! that's my understanding too. I've updated the code accordingly

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zacharycmontoya turns out legacy baggage and the new baggage are extracted/injected and propagated independently, so there will be never be a key conflict. But the legacy baggage never interact with the otel drop in code, is this okay?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's okay 👍🏼

if (span && span._ddSpan) return ddScope.activate(span._ddSpan, run)
return run()
}

Expand Down
4 changes: 4 additions & 0 deletions packages/dd-trace/src/opentracing/span.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const log = require('../log')
const { storage } = require('../../../datadog-core')
const telemetryMetrics = require('../telemetry/metrics')
const { getValueFromEnvSources } = require('../config/helper')
const { setBaggageItem, removeBaggageItem, removeAllBaggageItems } = require('../baggage')
const SpanContext = require('./span_context')

const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
Expand Down Expand Up @@ -163,6 +164,7 @@ class DatadogSpan {

setBaggageItem (key, value) {
this._spanContext._baggageItems[key] = value
setBaggageItem(key, value)
return this
}

Expand All @@ -176,10 +178,12 @@ class DatadogSpan {

removeBaggageItem (key) {
delete this._spanContext._baggageItems[key]
removeBaggageItem(key)
}

removeAllBaggageItems () {
this._spanContext._baggageItems = {}
removeAllBaggageItems()
}

setTag (key, value) {
Expand Down
79 changes: 28 additions & 51 deletions packages/dd-trace/test/opentelemetry/context_manager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,12 @@ const assert = require('node:assert/strict')
const { describe, it, beforeEach } = require('mocha')
const { context, propagation, trace, ROOT_CONTEXT } = require('@opentelemetry/api')
const api = require('@opentelemetry/api')
const { getAllBaggageItems, setBaggageItem, removeBaggageItem } = require('../../src/baggage')

require('../setup/core')
const ContextManager = require('../../src/opentelemetry/context_manager')
const TracerProvider = require('../../src/opentelemetry/tracer_provider')
const tracer = require('../../').init()

function makeSpan (...args) {
const tracerProvider = new TracerProvider()
tracerProvider.register()
const tracer = tracerProvider.getTracer()
return tracer.startSpan(...args)
}
require('../../').init()

function getTracer () {
const tracerProvider = new TracerProvider()
Expand Down Expand Up @@ -138,63 +132,46 @@ describe('OTel Context Manager', () => {
}
const baggage = propagation.createBaggage(entries)
const contextWithBaggage = propagation.setBaggage(context.active(), baggage)
const span = makeSpan('otel-to-dd')
const contextWithSpan = trace.setSpan(contextWithBaggage, span)
api.context.with(contextWithSpan, () => {
assert.strictEqual(tracer.scope().active().getBaggageItem('foo'), 'bar')
api.context.with(contextWithBaggage, () => {
assert.deepStrictEqual(getAllBaggageItems(), { foo: 'bar' })
})
})

it('should propagate baggage from a datadog span to an otel span', () => {
const baggageKey = 'raccoon'
const baggageVal = 'chunky'
const ddSpan = tracer.startSpan('dd-to-otel')
ddSpan.setBaggageItem(baggageKey, baggageVal)
tracer.scope().activate(ddSpan, () => {
const baggages = propagation.getActiveBaggage().getAllEntries()
assert.strictEqual(baggages.length, 1)
const baggage = baggages[0]
assert.strictEqual(baggage[0], baggageKey)
assert.strictEqual(baggage[1].value, baggageVal)
})
setBaggageItem('raccoon', 'chunky')
assert.deepStrictEqual(propagation.getActiveBaggage().getAllEntries(),
[['raccoon', { value: 'chunky' }]]
)
})

it('should handle dd-otel baggage conflict', () => {
const ddSpan = tracer.startSpan('dd')
ddSpan.setBaggageItem('key1', 'dd1')
let contextWithUpdatedBaggages
tracer.scope().activate(ddSpan, () => {
let baggages = propagation.getBaggage(api.context.active())
baggages = baggages.setEntry('key1', { value: 'otel1' })
baggages = baggages.setEntry('key2', { value: 'otel2' })
contextWithUpdatedBaggages = propagation.setBaggage(api.context.active(), baggages)
})
assert.deepStrictEqual(JSON.parse(ddSpan.getAllBaggageItems()), { key1: 'dd1' })
setBaggageItem('key1', 'dd1')
let baggages = propagation.getActiveBaggage()
baggages = baggages.setEntry('key1', { value: 'otel1' })
baggages = baggages.setEntry('key2', { value: 'otel2' })
const contextWithUpdatedBaggages = propagation.setBaggage(context.active(), baggages)
assert.deepStrictEqual(getAllBaggageItems(), { key1: 'dd1' })
api.context.with(contextWithUpdatedBaggages, () => {
assert.deepStrictEqual(JSON.parse(ddSpan.getAllBaggageItems()), { key1: 'otel1', key2: 'otel2' })
ddSpan.setBaggageItem('key2', 'dd2')
assert.deepStrictEqual(propagation.getActiveBaggage().getAllEntries(),
[['key1', { value: 'otel1' }], ['key2', { value: 'dd2' }]]
)
assert.deepStrictEqual(getAllBaggageItems(), { key1: 'otel1', key2: 'otel2' })
})
setBaggageItem('key2', 'dd2')
assert.deepStrictEqual(propagation.getActiveBaggage().getAllEntries(),
[['key1', { value: 'otel1' }], ['key2', { value: 'dd2' }]]
)
})

it('should handle dd-otel baggage removal', () => {
const ddSpan = tracer.startSpan('dd')
ddSpan.setBaggageItem('key1', 'dd1')
ddSpan.setBaggageItem('key2', 'dd2')
let contextWithUpdatedBaggages
tracer.scope().activate(ddSpan, () => {
let baggages = propagation.getBaggage(api.context.active())
baggages = baggages.removeEntry('key1')
contextWithUpdatedBaggages = propagation.setBaggage(api.context.active(), baggages)
})
assert.deepStrictEqual(JSON.parse(ddSpan.getAllBaggageItems()), { key1: 'dd1', key2: 'dd2' })
setBaggageItem('key1', 'dd1')
setBaggageItem('key2', 'dd2')
let baggages = propagation.getActiveBaggage()
baggages = baggages.removeEntry('key1')
const contextWithUpdatedBaggages = propagation.setBaggage(context.active(), baggages)
assert.deepStrictEqual(getAllBaggageItems(), { key1: 'dd1', key2: 'dd2' })
api.context.with(contextWithUpdatedBaggages, () => {
assert.deepStrictEqual(JSON.parse(ddSpan.getAllBaggageItems()), { key2: 'dd2' })
ddSpan.removeBaggageItem('key2')
assert.deepStrictEqual(propagation.getActiveBaggage().getAllEntries(), [])
assert.deepStrictEqual(getAllBaggageItems(), { key2: 'dd2' })
})
removeBaggageItem('key2')
assert.deepStrictEqual(propagation.getActiveBaggage(), undefined)
})

it('should return active span', () => {
Expand Down
Loading