From 86135ac15bf59a614f408a000c7f985ecb74960b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 09:33:24 -0400 Subject: [PATCH 1/3] Expose the trace's global tags and span defaults on Metadata Serializers get the global tags (DD_TAGS, or their remote-config override) and the span defaults from the trace's config snapshot, so an OTLP export can write the global tags once as resource attributes instead of on every span. Both come from the snapshot the trace started with, so a config change mid-trace can't split one trace across two sets. getTags() is unchanged and still includes both. Test call sites pass empty maps; PayloadDispatcherImplTest's mocked PendingTrace now returns a real snapshot. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 3 +++ .../java/datadog/trace/core/Metadata.java | 23 +++++++++++++++++++ .../FileBasedPayloadDispatcherTest.java | 2 ++ .../writer/PayloadDispatcherImplTest.java | 9 ++++++++ .../trace/common/writer/TraceGenerator.java | 2 ++ .../java/datadog/trace/core/MetadataTest.java | 2 ++ .../traceAgentTest/java/TraceGenerator.java | 2 ++ 7 files changed, 43 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 1e236d3a120..24b652e7593 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1567,11 +1567,14 @@ void processTagsAndBaggage( : serializedPropagationTags.createTagMap(); } + CoreTracer.ConfigSnapshot traceConfig = traceCollector.getTraceConfig(); consumer.accept( new Metadata( threadId, threadName, unsafeTags, + traceConfig.globalTags, + traceConfig.spanDefaultTags, baggageItemsWithPropagationTags, getSamplingPriority(), measured, diff --git a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java index d10187d1861..3bf2a3fe9b5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java @@ -14,6 +14,8 @@ public final class Metadata { private final UTF8BytesString threadName; private final int httpStatusCode; private final TagMap tags; + private final TagMap globalTags; + private final TagMap spanDefaultTags; private final Map baggage; private final int samplingPriority; @@ -30,6 +32,8 @@ public Metadata( long threadId, UTF8BytesString threadName, TagMap tags, + TagMap globalTags, + TagMap spanDefaultTags, Map baggage, int samplingPriority, boolean measured, @@ -45,6 +49,8 @@ public Metadata( this.threadName = threadName; this.httpStatusCode = httpStatusCode; this.tags = tags; + this.globalTags = globalTags; + this.spanDefaultTags = spanDefaultTags; this.baggage = baggage; this.samplingPriority = samplingPriority; this.measured = measured; @@ -98,6 +104,23 @@ public TagMap getTags() { return this.tags; } + /** + * The trace's global tags ({@code DD_TAGS}, or their remote-config override). {@link #getTags()} + * already includes them, below the span defaults and the span's own tags; this lets a serializer + * that writes them once per payload, such as an OTLP resource, tell them apart. + */ + public TagMap getGlobalTags() { + return globalTags; + } + + /** + * The trace's span defaults ({@code DD_SPAN_TAGS} and tracer-defined tags such as {@code env}), + * without the global tags. {@link #getTags()} already includes them, below the span's own tags. + */ + public TagMap getSpanDefaultTags() { + return spanDefaultTags; + } + public Map getBaggage() { return baggage; } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java index 2704dff3d3e..f116a1589f0 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java @@ -307,6 +307,8 @@ private static CoreSpan mockSpan(CharSequence type, Map tags) Thread.currentThread().getId(), null, TagMap.fromMap(tags), + TagMap.EMPTY, + TagMap.EMPTY, Collections.emptyMap(), 0, false, diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java index cf756003e7c..44467412346 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java @@ -181,6 +181,12 @@ void testTraceAndSpanCountsAreResetAfterAccess() { } DDSpan realSpan() throws Exception { + // A real config snapshot, since serialization reads the trace's tag levels from it. Its type + // isn't visible from this package, so it's held as an Object and matched by name below. + CoreTracer tracer = CoreTracer.builder().writer(new ListWriter()).build(); + Object traceConfig = tracer.captureTraceConfig(); + tracer.close(); + // getTracer() and mapServiceName() are package-private in TraceCollector; use a custom // Answer to handle them at runtime without compile-time accessibility issues PendingTrace trace = @@ -193,6 +199,9 @@ DDSpan realSpan() throws Exception { // behavior) return mock(CoreTracer.class); } + if (returnType.getSimpleName().equals("ConfigSnapshot")) { + return traceConfig; + } if (returnType == String.class) { Object[] args = invocation.getArguments(); // mapServiceName(String) - return the argument unchanged diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java index c570b2ae7e4..c4ea0720df3 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java @@ -238,6 +238,8 @@ public PojoSpan( Thread.currentThread().getId(), UTF8BytesString.create(Thread.currentThread().getName()), TagMap.fromMap(tags), + TagMap.EMPTY, + TagMap.EMPTY, baggage, samplingPriority, measured, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/MetadataTest.java b/dd-trace-core/src/test/java/datadog/trace/core/MetadataTest.java index bbc5c607233..d844c42ea54 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/MetadataTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/MetadataTest.java @@ -54,6 +54,8 @@ private static Metadata metadataWithStatus(int status) { Thread.currentThread().getId(), UTF8BytesString.create("main"), TagMap.fromMap(emptyMap()), + TagMap.EMPTY, + TagMap.EMPTY, emptyMap(), 0, false, diff --git a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java index 0fe52affee4..1050191cff4 100644 --- a/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java +++ b/dd-trace-core/src/traceAgentTest/java/TraceGenerator.java @@ -175,6 +175,8 @@ static class PojoSpan implements CoreSpan { currentThread().getId(), UTF8BytesString.create(currentThread().getName()), fromMap(tags), + TagMap.EMPTY, + TagMap.EMPTY, baggage, UNSET, measured, From 39bba17e3684c84fc9df3713a94e4b31aa929d34 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 09:33:27 -0400 Subject: [PATCH 2/3] Test that Metadata carries the trace's tag levels Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/CoreTracerTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java index cfbef532a5e..b21d6154f9a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java @@ -6,7 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.mockito.ArgumentMatchers.any; @@ -766,6 +768,39 @@ void configSnapshotSeparatesGlobalTagsFromSpanDefaults() throws Exception { } } + /** + * Serializers get the global tags and span defaults of the snapshot the trace started with, so a + * config change mid-trace can't split one trace across two sets. + */ + @Test + void metadataCarriesTheTraceConfigTagLevels() { + Properties properties = new Properties(); + properties.setProperty(GeneralConfig.TAGS, "global_key:global_val"); + properties.setProperty(TracerConfig.SPAN_TAGS, "span_key:span_val"); + CoreTracer tracer = CoreTracer.builder().withProperties(properties).build(); + unclosedTracers.add(tracer); + + try { + ConfigSnapshot traceStartConfig = tracer.captureTraceConfig(); + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "my_span").start(); + + Properties changed = new Properties(); + changed.setProperty(GeneralConfig.TAGS, "other_key:other_val"); + tracer.rebuildTraceConfig(Config.get(changed)); + assertNotSame(traceStartConfig.globalTags, tracer.captureTraceConfig().globalTags); + + Metadata[] captured = {null}; + span.processTagsAndBaggage(metadata -> captured[0] = metadata, 0); + + assertSame(traceStartConfig.globalTags, captured[0].getGlobalTags()); + assertSame(traceStartConfig.spanDefaultTags, captured[0].getSpanDefaultTags()); + assertEquals(buildStringMap("global_key", "global_val"), captured[0].getGlobalTags()); + assertEquals(buildStringMap("span_key", "span_val"), captured[0].getSpanDefaultTags()); + } finally { + tracer.close(); + } + } + static final String ACTION_JSON = "'{\"action\": \"enable\", \"lib_config\":" + "{\"tracing_sampling_rate\": null," From 017f6e62d88d4b22feab05eff87370e862cad3ab Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 22:28:04 -0400 Subject: [PATCH 3/3] Name DD_TRACE_SPAN_TAGS and the override's env and version in Metadata Also restores the Properties import the Metadata test still needs, which #12937's switch to injectSysConfig dropped. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/core/Metadata.java | 12 +++++++----- .../test/java/datadog/trace/core/CoreTracerTest.java | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java index 3bf2a3fe9b5..8a5e51b511e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java @@ -105,17 +105,19 @@ public TagMap getTags() { } /** - * The trace's global tags ({@code DD_TAGS}, or their remote-config override). {@link #getTags()} - * already includes them, below the span defaults and the span's own tags; this lets a serializer - * that writes them once per payload, such as an OTLP resource, tell them apart. + * The trace's global tags ({@code DD_TAGS}, or their remote-config override, which keeps the + * configured {@code env} and {@code version}). {@link #getTags()} already includes them, below + * the span defaults and the span's own tags; this lets a serializer that writes them once per + * payload, such as an OTLP resource, tell them apart. */ public TagMap getGlobalTags() { return globalTags; } /** - * The trace's span defaults ({@code DD_SPAN_TAGS} and tracer-defined tags such as {@code env}), - * without the global tags. {@link #getTags()} already includes them, below the span's own tags. + * The trace's span defaults ({@code DD_TRACE_SPAN_TAGS} and tracer-defined tags such as {@code + * env}), without the global tags; only the tracer-defined tags while a remote-config override is + * active. {@link #getTags()} already includes them, below the span's own tags. */ public TagMap getSpanDefaultTags() { return spanDefaultTags; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java index b21d6154f9a..88239302feb 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest.java @@ -52,6 +52,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl;