From 3f3bb0fefcc64e42d4f41a55cb0f613bf602f2c0 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Mon, 17 Nov 2025 12:56:51 +0000 Subject: [PATCH 1/2] Support using the OpenTelemetry API to interact with automatic W3C baggage Note: custom BaggageEntryMetadata is not supported --- .../shim/baggage/OtelBaggage.java | 96 +++++++++++++++++++ .../shim/baggage/OtelBaggageBuilder.java | 40 ++++++++ .../shim/context/OtelContext.java | 15 +++ .../OpenTelemetryInstrumentation.java | 3 + .../OpenTelemetryContextInstrumentation.java | 3 + ...elemetryContextStorageInstrumentation.java | 3 + .../context/ContextTest.groovy | 59 ++++++++++++ 7 files changed, 219 insertions(+) create mode 100644 dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java create mode 100644 dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggageBuilder.java diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java new file mode 100644 index 00000000000..e5edc85b451 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java @@ -0,0 +1,96 @@ +package datadog.opentelemetry.shim.baggage; + +import static java.util.stream.Collectors.toMap; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.baggage.BaggageEntry; +import io.opentelemetry.api.baggage.BaggageEntryMetadata; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +public class OtelBaggage implements Baggage { + private final datadog.trace.bootstrap.instrumentation.api.Baggage delegate; + + public OtelBaggage(datadog.trace.bootstrap.instrumentation.api.Baggage delegate) { + this.delegate = delegate; + } + + public OtelBaggage(Map items) { + this(datadog.trace.bootstrap.instrumentation.api.Baggage.create(items)); + } + + @Override + public int size() { + return delegate.asMap().size(); + } + + @Override + public void forEach(BiConsumer consumer) { + for (Map.Entry entry : delegate.asMap().entrySet()) { + consumer.accept(entry.getKey(), new ValueOnly(entry)); + } + } + + @Override + public Map asMap() { + return delegate.asMap().entrySet().stream().collect(toMap(Map.Entry::getKey, ValueOnly::new)); + } + + @Nullable + @Override + public String getEntryValue(String key) { + return delegate.asMap().get(key); + } + + @Nullable + @Override + public BaggageEntry getEntry(String key) { + String value = getEntryValue(key); + return value != null ? new ValueOnly(value) : null; + } + + @Override + public BaggageBuilder toBuilder() { + return new OtelBaggageBuilder(delegate.asMap()); + } + + public datadog.trace.bootstrap.instrumentation.api.Baggage asAgentBaggage() { + return delegate; + } + + static class ValueOnly implements BaggageEntry { + private final String value; + + ValueOnly(String value) { + this.value = value; + } + + ValueOnly(Map.Entry entry) { + this(entry.getValue()); + } + + @Override + public String getValue() { + return value; + } + + @Override + public BaggageEntryMetadata getMetadata() { + return BaggageEntryMetadata.empty(); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public final boolean equals(Object o) { + return (o instanceof ValueOnly) && value.equals(((ValueOnly) o).value); + } + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggageBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggageBuilder.java new file mode 100644 index 00000000000..430f7be3b01 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggageBuilder.java @@ -0,0 +1,40 @@ +package datadog.opentelemetry.shim.baggage; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.baggage.BaggageEntryMetadata; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +public class OtelBaggageBuilder implements BaggageBuilder { + private final Map items; + + public OtelBaggageBuilder(Map items) { + this.items = new HashMap<>(items); + } + + @Override + public BaggageBuilder put( + @Nullable String key, @Nullable String value, BaggageEntryMetadata ignore) { + if (key != null && value != null) { + items.put(key, value); + } + return this; + } + + @Override + public BaggageBuilder remove(@Nullable String key) { + if (key != null) { + items.remove(key); + } + return this; + } + + @Override + public Baggage build() { + return new OtelBaggage(items); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/context/OtelContext.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/context/OtelContext.java index d81e9d9c512..19e124db4f5 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/context/OtelContext.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/context/OtelContext.java @@ -1,8 +1,10 @@ package datadog.opentelemetry.shim.context; +import datadog.opentelemetry.shim.baggage.OtelBaggage; import datadog.opentelemetry.shim.trace.OtelSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper; +import datadog.trace.bootstrap.instrumentation.api.Baggage; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.context.Scope; @@ -18,6 +20,7 @@ public class OtelContext implements Context { /** Overridden root context. */ public static final Context ROOT = new OtelContext(datadog.context.Context.root()); + private static final String OTEL_CONTEXT_BAGGAGE_KEY = "opentelemetry-baggage-key"; private static final String OTEL_CONTEXT_SPAN_KEY = "opentelemetry-trace-span-key"; private static final String OTEL_CONTEXT_ROOT_SPAN_KEY = "opentelemetry-traces-local-root-span"; @@ -55,6 +58,12 @@ public V get(ContextKey key) { return (V) toOtelSpan(span.getLocalRootSpan()); } // fall-through and check for non-datadog span data + } else if (OTEL_CONTEXT_BAGGAGE_KEY.equals(key.toString())) { + Baggage baggage = Baggage.fromContext(delegate); + if (baggage != null) { + return (V) new OtelBaggage(baggage); + } + // fall-through and check for non-datadog baggage } return (V) delegate.get(delegateKey(key)); } @@ -67,6 +76,12 @@ public Context with(ContextKey key, V value) { return new OtelContext(delegate.with(span)); } // fall-through and store as non-datadog span data + } else if (OTEL_CONTEXT_BAGGAGE_KEY.equals(key.toString())) { + if (value instanceof OtelBaggage) { + Baggage baggage = ((OtelBaggage) value).asAgentBaggage(); + return new OtelContext(delegate.with(baggage)); + } + // fall-through and store as non-datadog baggage } return new OtelContext(delegate.with(delegateKey(key), value)); } diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java index 3047659c8bb..0820e06b57d 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java @@ -62,6 +62,9 @@ public String[] helperClassNames() { "datadog.opentelemetry.shim.context.propagation.AgentTextMapPropagator", "datadog.opentelemetry.shim.context.propagation.OtelContextPropagators", "datadog.opentelemetry.shim.context.propagation.TraceStateHelper", + "datadog.opentelemetry.shim.baggage.OtelBaggage", + "datadog.opentelemetry.shim.baggage.OtelBaggage$ValueOnly", + "datadog.opentelemetry.shim.baggage.OtelBaggageBuilder", "datadog.opentelemetry.shim.trace.OtelExtractedContext", "datadog.opentelemetry.shim.trace.OtelConventions", "datadog.opentelemetry.shim.trace.OtelConventions$1", diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextInstrumentation.java index 65de7ed322c..fe0714ce433 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextInstrumentation.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextInstrumentation.java @@ -56,6 +56,9 @@ public String[] helperClassNames() { return new String[] { "datadog.opentelemetry.shim.context.OtelContext", "datadog.opentelemetry.shim.context.OtelScope", + "datadog.opentelemetry.shim.baggage.OtelBaggage", + "datadog.opentelemetry.shim.baggage.OtelBaggage$ValueOnly", + "datadog.opentelemetry.shim.baggage.OtelBaggageBuilder", "datadog.opentelemetry.shim.trace.OtelExtractedContext", "datadog.opentelemetry.shim.trace.OtelConventions", "datadog.opentelemetry.shim.trace.OtelConventions$1", diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextStorageInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextStorageInstrumentation.java index 64ce6c572db..a5e6f5ce945 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextStorageInstrumentation.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/context/OpenTelemetryContextStorageInstrumentation.java @@ -57,6 +57,9 @@ public String[] helperClassNames() { return new String[] { "datadog.opentelemetry.shim.context.OtelContext", "datadog.opentelemetry.shim.context.OtelScope", + "datadog.opentelemetry.shim.baggage.OtelBaggage", + "datadog.opentelemetry.shim.baggage.OtelBaggage$ValueOnly", + "datadog.opentelemetry.shim.baggage.OtelBaggageBuilder", "datadog.opentelemetry.shim.trace.OtelExtractedContext", "datadog.opentelemetry.shim.trace.OtelConventions", "datadog.opentelemetry.shim.trace.OtelConventions$1", diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy index a9daee5c342..5cfa8de0c81 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy @@ -3,6 +3,7 @@ package opentelemetry14.context import datadog.trace.agent.test.InstrumentationSpecification import datadog.trace.api.DDSpanId import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.baggage.Baggage import io.opentelemetry.api.trace.Span import io.opentelemetry.context.Context import io.opentelemetry.context.ContextKey @@ -324,6 +325,64 @@ class ContextTest extends InstrumentationSpecification { context.get(CustomData.KEY) == null } + def "test Baggage.current/makeCurrent()"() { + when: + def otelBaggage = Baggage.current() + def otelBaggageFromContext = Baggage.fromContext(Context.current()) + def otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()) + + then: "current baggage must be empty or null" + otelBaggage != null + otelBaggage.isEmpty() + otelBaggageFromContext != null + otelBaggageFromContext.isEmpty() + otelBaggageFromContextOrNull == null + + 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 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.asMap() == otelBaggageFromContext.asMap() + otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap() + + when: + def builder = otelBaggage.toBuilder() + builder.put("new_foo", "new_value") + builder.put("foo", "overwrite_value") + builder.remove("remove_me_key") + def 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.asMap() == otelBaggageFromContext.asMap() + otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap() + + cleanup: + otelScope.close() + ddScope.close() + } + @Override void cleanup() { // Test for context leak From a460aa25e90e4a4b0058fcb420ba31c2d788e4c7 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Mon, 17 Nov 2025 15:04:42 +0000 Subject: [PATCH 2/2] Defensive hashCode+equals --- .../java/datadog/opentelemetry/shim/baggage/OtelBaggage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java index e5edc85b451..0d2fba36ad5 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/baggage/OtelBaggage.java @@ -7,6 +7,7 @@ import io.opentelemetry.api.baggage.BaggageEntry; import io.opentelemetry.api.baggage.BaggageEntryMetadata; import java.util.Map; +import java.util.Objects; import java.util.function.BiConsumer; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; @@ -85,12 +86,12 @@ public BaggageEntryMetadata getMetadata() { @Override public int hashCode() { - return value.hashCode(); + return Objects.hashCode(value); } @Override public final boolean equals(Object o) { - return (o instanceof ValueOnly) && value.equals(((ValueOnly) o).value); + return (o instanceof ValueOnly) && Objects.equals(value, ((ValueOnly) o).value); } } }