Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ public BaggageBuilder remove(@Nullable String key) {

@Override
public Baggage build() {
return new OtelBaggage(items);
return new OtelBaggage(new HashMap<>(items));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ public <V> Context with(ContextKey<V> key, V value) {
if (value instanceof OtelBaggage) {
Baggage baggage = ((OtelBaggage) value).asAgentBaggage();
return new OtelContext(delegate.with(baggage));
} else if (value instanceof io.opentelemetry.api.baggage.Baggage) {
Baggage baggage = Baggage.empty();
// transfer baggage to our internal container for propagation purposes
((io.opentelemetry.api.baggage.Baggage) value)
.forEach((k, b) -> baggage.addItem(k, b.getValue()));
Comment on lines +84 to +87
Copy link
Contributor

@PerfectSlayer PerfectSlayer Nov 18, 2025

Choose a reason for hiding this comment

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

🎯 suggestion: ‏That might be an over optimization but (follow me here), what about implementing a custom Map implementation that projects the Map<String, BaggageEntry> returned by (OTel) Baggage.asMap() into a Map<String, String>?

We can then use (DD) Baggage.create(Map) from it, meaning having zero collection allocation as OTel uses the ReadOnlyArrayMap projection and we can have ours to translate value to their string counter parts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One hiccup is that projection would be read-only - and our internal Baggage container has mutable operations (addItem / removeItem) that assume a mutable map.

So I'd prefer to do that as part of a refactoring to optimize the internal Baggage API, ideally making it inherently immutable once created/built.

return new OtelContext(delegate.with(baggage));
}
// fall-through and store as non-datadog baggage
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,43 +338,90 @@ class ContextTest extends InstrumentationSpecification {
otelBaggageFromContext.isEmpty()
otelBaggageFromContextOrNull == null

when:
def otelScope = Baggage.builder()
.put("foo", "otel_value_to_be_replaced")
.put("FOO","OTEL_UNTOUCHED")
.put("remove_me_key", "otel_remove_me_value")
.build()
.makeCurrent()
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ (For my own understanding). I assume that this makeCurrent() invokes our OTelContext instead of the OpenTelemetry Context implementation. How does the makeCurrent() know to use our implementation? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note this is Baggage.makeCurrent() :)

Baggage inherits this default method from ImplicitContextKeyed: https://github.com/open-telemetry/opentelemetry-java/blob/main/context/src/main/java/io/opentelemetry/context/ImplicitContextKeyed.java#L33

  default Scope makeCurrent() {
    return Context.current().with(this).makeCurrent();
  }

Notice how it's implemented by getting the current Context (which will be our OtelContext wrapper) to which it adds the new baggage via with (which we'll intercept via our OtelContext wrapper) before activating the resulting context.

Copy link
Contributor

Choose a reason for hiding this comment

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

Most of this makes sense. The part that I'm confused about is how our OtelContext wrapper gets magically used here instead of the OTel implementation of Context, since I don't see anywhere where we explicitly "set" OtelContext as the global implementation for io.opentelemetry.context.Context.

Hope the question makes sense!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh that makes sense. We instrument the calls made to OTel Context and specify to use our OtelContext instead. Thanks for the explanation! :)

otelBaggage = Baggage.current()
otelBaggageFromContext = Baggage.fromContext(Context.current())
otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current())

then: "OTel baggage must be current"
otelBaggage != null
otelBaggage.size() == 3
otelBaggage.getEntryValue("foo") == "otel_value_to_be_replaced"
otelBaggage.getEntryValue("FOO") == "OTEL_UNTOUCHED"
otelBaggage.getEntryValue("remove_me_key") == "otel_remove_me_value"
otelBaggage.asMap() == otelBaggageFromContext.asMap()
otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap()

when:
def ddContext = datadog.context.Context.current()
def ddBaggage = datadog.trace.bootstrap.instrumentation.api.Baggage.create([
"foo" : "value_to_be_replaced",
"FOO" : "UNTOUCHED",
"remove_me_key" : "remove_me_value"
])
def ddBaggage = datadog.trace.bootstrap.instrumentation.api.Baggage.fromContext(ddContext)
ddBaggage.addItem("new_foo", "dd_new_value")
ddBaggage.addItem("foo", "dd_overwrite_value")
ddBaggage.removeItem("remove_me_key")
def ddScope = ddContext.with(ddBaggage).attach()
otelBaggage = Baggage.current()
otelBaggageFromContext = Baggage.fromContext(Context.current())
otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current())

then: "baggage must contain Datadog changes"
otelBaggage != null
otelBaggage.size() == 3
otelBaggage.getEntryValue("foo") == "dd_overwrite_value"
otelBaggage.getEntryValue("FOO") == "OTEL_UNTOUCHED"
otelBaggage.getEntryValue("new_foo") == "dd_new_value"
otelBaggage.asMap() == otelBaggageFromContext.asMap()
otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap()

when:
ddScope.close()
otelScope.close()

then: "current baggage must be empty or null"
Baggage.current().isEmpty()

when:
ddContext = datadog.context.Context.current()
ddBaggage = datadog.trace.bootstrap.instrumentation.api.Baggage.create([
"foo" : "dd_value_to_be_replaced",
"FOO" : "DD_UNTOUCHED",
"remove_me_key" : "dd_remove_me_value"
])
ddScope = ddContext.with(ddBaggage).attach()
otelBaggage = Baggage.current()
otelBaggageFromContext = Baggage.fromContext(Context.current())
otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current())

then: "Datadog baggage must be current"
otelBaggage != null
otelBaggage.size() == 3
otelBaggage.getEntryValue("foo") == "value_to_be_replaced"
otelBaggage.getEntryValue("FOO") == "UNTOUCHED"
otelBaggage.getEntryValue("remove_me_key") == "remove_me_value"
otelBaggage.getEntryValue("foo") == "dd_value_to_be_replaced"
otelBaggage.getEntryValue("FOO") == "DD_UNTOUCHED"
otelBaggage.getEntryValue("remove_me_key") == "dd_remove_me_value"
otelBaggage.asMap() == otelBaggageFromContext.asMap()
otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap()

when:
def builder = otelBaggage.toBuilder()
builder.put("new_foo", "new_value")
builder.put("foo", "overwrite_value")
builder.put("new_foo", "otel_new_value")
builder.put("foo", "otel_overwrite_value")
builder.remove("remove_me_key")
def otelScope = builder.build().makeCurrent()
otelScope = builder.build().makeCurrent()
otelBaggage = Baggage.current()
otelBaggageFromContext = Baggage.fromContext(Context.current())
otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current())

then: "baggage must contain OTel changes"
otelBaggage != null
otelBaggage.size() == 3
otelBaggage.getEntryValue("foo") == "overwrite_value"
otelBaggage.getEntryValue("FOO") == "UNTOUCHED"
otelBaggage.getEntryValue("new_foo") == "new_value"
otelBaggage.getEntryValue("foo") == "otel_overwrite_value"
otelBaggage.getEntryValue("FOO") == "DD_UNTOUCHED"
otelBaggage.getEntryValue("new_foo") == "otel_new_value"
otelBaggage.asMap() == otelBaggageFromContext.asMap()
otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap()

Expand Down