From 3cc1df768ba79fd058ed39ff985df5873590b047 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Tue, 4 Aug 2026 15:45:10 -0400 Subject: [PATCH 01/15] Fix OTLP trace-metrics attribute compliance gaps Brings the traces.span.sdk.metrics.duration OTLP export in line with the RFC's attribute spec: emit datadog.process_tags as one arrayValue resource attribute instead of split per-key attributes, add the missing datadog.is_trace_root data-point attribute, canonicalize span.kind to the OTel Span Metrics Connector's uppercase convention, and emit status.code unconditionally with STATUS_CODE_OK/ERROR values instead of only on error. Co-Authored-By: Claude Sonnet 5 --- .../otlp/common/OtlpResourceAttributes.java | 28 +++--- .../core/otlp/common/OtlpResourceJson.java | 17 +++- .../core/otlp/common/OtlpResourceProto.java | 17 +++- .../otlp/metrics/OtlpStatsMetricWriter.java | 50 ++++++---- .../otlp/common/OtlpResourceJsonTest.java | 92 ++++++++++++++----- .../otlp/common/OtlpResourceProtoTest.java | 89 ++++++++++++++---- .../metrics/OtlpStatsMetricWriterTest.java | 88 ++++++++++++++++-- 7 files changed, 294 insertions(+), 87 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java index 1a56ad2216e..ebc5b732b13 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java @@ -36,9 +36,12 @@ private OtlpResourceAttributes() {} "telemetry.sdk.version", "telemetry.sdk.language")); - /** Visits each resource attribute key/value pair with {@code visitor}. */ + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code + * List}. + */ static void visitResourceAttributes( - Config config, Map extraAttributes, BiConsumer visitor) { + Config config, Map extraAttributes, BiConsumer visitor) { String serviceName = config.getServiceName(); String env = config.getEnv(); String version = config.getVersion(); @@ -73,34 +76,31 @@ static void visitResourceAttributes( extraAttributes.forEach(visitor); } + private static final String PROCESS_TAGS_KEY = DATADOG_PREFIX + "process_tags"; + /** * Builds the extra resource attributes for the OTLP trace export: the {@code _dd.stats_computed} * marker when the SDK is computing OTLP span metrics, so a downstream Agent does not recompute * them from the exported spans. */ - static Map traceResourceAttributes(Config config) { - Map attributes = new LinkedHashMap<>(); + static Map traceResourceAttributes(Config config) { + Map attributes = new LinkedHashMap<>(); if (config.isOtelTracesSpanMetricsEnabled()) { attributes.put(STATS_COMPUTED_KEY, "true"); } return attributes; } - static Map datadogResourceAttributes(Config config) { - Map attributes = new LinkedHashMap<>(); + static Map datadogResourceAttributes(Config config) { + Map attributes = new LinkedHashMap<>(); String runtimeId = config.getRuntimeId(); if (runtimeId != null && !runtimeId.isEmpty()) { attributes.put(DATADOG_PREFIX + "runtime_id", runtimeId); } - // Process tags arrive as "key:value" pairs; emit each as datadog. = value. + // Mirrors SerializingMetricWriter's v0.6 ProcessTags shape; keep both in sync if that changes. List processTags = ProcessTags.getTagsAsStringList(); - if (processTags != null) { - for (String tag : processTags) { - int colon = tag.indexOf(':'); - if (colon > 0) { - attributes.put(DATADOG_PREFIX + tag.substring(0, colon), tag.substring(colon + 1)); - } - } + if (processTags != null && !processTags.isEmpty()) { + attributes.put(PROCESS_TAGS_KEY, processTags); } return attributes; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index 4731bcd592a..0db64e8da9b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -1,6 +1,7 @@ package datadog.trace.core.otlp.common; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; import static datadog.trace.core.otlp.common.OtlpCommonJson.writeAttribute; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; @@ -9,6 +10,7 @@ import datadog.json.JsonWriter; import datadog.trace.api.Config; import java.util.Collections; +import java.util.List; import java.util.Map; /** Provides a canned JSON fragment for OpenTelemetry's "resource.proto" JSON encoding. */ @@ -35,7 +37,7 @@ private OtlpResourceJson() {} public static final String TRACE_RESOURCE_FRAGMENT = buildResourceFragment(Config.get(), traceResourceAttributes(Config.get())); - static String buildResourceFragment(Config config, Map extraAttributes) { + static String buildResourceFragment(Config config, Map extraAttributes) { try (JsonWriter writer = new JsonWriter()) { writer.beginObject(); writer.name("attributes").beginArray(); @@ -49,7 +51,16 @@ static String buildResourceFragment(Config config, Map extraAttr } } - private static void writeResourceAttribute(JsonWriter writer, String key, String value) { - writeAttribute(writer, STRING_ATTRIBUTE, key, value); + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code + * List}. + */ + @SuppressWarnings("unchecked") + private static void writeResourceAttribute(JsonWriter writer, String key, Object value) { + if (value instanceof List) { + writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, (List) value); + } else { + writeAttribute(writer, STRING_ATTRIBUTE, key, value); + } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index 91a6cf7193c..67536777673 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -1,5 +1,6 @@ package datadog.trace.core.otlp.common; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.core.otlp.common.OtlpCommonProto.LEN_WIRE_TYPE; import static datadog.trace.core.otlp.common.OtlpCommonProto.writeAttribute; @@ -12,6 +13,7 @@ import datadog.communication.serialization.StreamingBuffer; import datadog.trace.api.Config; import java.util.Collections; +import java.util.List; import java.util.Map; /** Provides a canned message for OpenTelemetry's "resource.proto" wire protocol. */ @@ -38,7 +40,7 @@ private OtlpResourceProto() {} public static final byte[] TRACE_RESOURCE_MESSAGE = buildResourceMessage(Config.get(), traceResourceAttributes(Config.get())); - static byte[] buildResourceMessage(Config config, Map extraAttributes) { + static byte[] buildResourceMessage(Config config, Map extraAttributes) { GrowableBuffer buf = new GrowableBuffer(512); visitResourceAttributes( @@ -52,8 +54,17 @@ static byte[] buildResourceMessage(Config config, Map extraAttri return resourceMessage; } - private static void writeResourceAttribute(StreamingBuffer buf, String key, String value) { + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code + * List}. + */ + @SuppressWarnings("unchecked") + private static void writeResourceAttribute(StreamingBuffer buf, String key, Object value) { writeTag(buf, 1, LEN_WIRE_TYPE); - writeAttribute(buf, STRING_ATTRIBUTE, key, value); + if (value instanceof List) { + writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, (List) value); + } else { + writeAttribute(buf, STRING_ATTRIBUTE, key, value); + } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index d7b7f56ac94..4f8742ced17 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -1,6 +1,7 @@ package datadog.trace.core.otlp.metrics; import static datadog.trace.bootstrap.otel.metrics.OtelInstrumentType.HISTOGRAM; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; @@ -9,6 +10,7 @@ import datadog.trace.api.config.OtlpConfig; import datadog.trace.api.telemetry.OtlpTelemetry; import datadog.trace.api.time.SystemTimeSource; +import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; import datadog.trace.bootstrap.otel.metrics.OtelInstrumentDescriptor; @@ -49,13 +51,21 @@ public final class OtlpStatsMetricWriter implements MetricWriter { private static final String HTTP_ROUTE = "http.route"; private static final String RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code"; private static final String STATUS_CODE = "status.code"; - private static final String STATUS_CODE_ERROR = "ERROR"; + private static final String STATUS_CODE_OK = "STATUS_CODE_OK"; + private static final String STATUS_CODE_ERROR = "STATUS_CODE_ERROR"; private static final String DATADOG_OPERATION_NAME = "datadog.operation.name"; private static final String DATADOG_SPAN_TYPE = "datadog.span.type"; private static final String DATADOG_SPAN_TOP_LEVEL = "datadog.span.top_level"; + private static final String DATADOG_IS_TRACE_ROOT = "datadog.is_trace_root"; private static final String DATADOG_ORIGIN = "datadog.origin"; private static final String SYNTHETICS_ORIGIN = "synthetics"; + private static final String SPAN_KIND_SERVER = "SPAN_KIND_SERVER"; + private static final String SPAN_KIND_CLIENT = "SPAN_KIND_CLIENT"; + private static final String SPAN_KIND_PRODUCER = "SPAN_KIND_PRODUCER"; + private static final String SPAN_KIND_CONSUMER = "SPAN_KIND_CONSUMER"; + private static final String SPAN_KIND_INTERNAL = "SPAN_KIND_INTERNAL"; + @Nullable private final OtlpSender sender; private final boolean otelSemanticsMode; @@ -205,13 +215,9 @@ private void emit(OtlpMetricsVisitor visitor) { private void emitDataPointAttributes( OtlpMetricVisitor metric, AggregateEntry entry, boolean error, boolean allTopLevel) { - if (error) { - emitStringAttribute(metric, STATUS_CODE, STATUS_CODE_ERROR); - } - // OTel semconv attrs are emitted in both modes + emitStringAttribute(metric, STATUS_CODE, error ? STATUS_CODE_ERROR : STATUS_CODE_OK); emitStringAttribute(metric, SPAN_NAME, entry.getResource()); - emitStringAttribute(metric, SPAN_KIND, entry.getSpanKind()); - // service.name on the point only when the span's service differs from the resource's default + emitStringAttribute(metric, SPAN_KIND, canonicalSpanKind(entry.getSpanKind())); UTF8BytesString service = entry.getService(); if (service != null && service.length() > 0 && !service.toString().equals(defaultService)) { emitStringAttribute(metric, SERVICE_NAME, service); @@ -228,10 +234,7 @@ private void emitDataPointAttributes( if (entry.hasGrpcStatusCode()) { emitStringAttribute(metric, RPC_RESPONSE_STATUS_CODE, entry.getGrpcStatusCode()); } - // Additional metric tags: user-configured span-derived dimensions, carried as packed - // "key:value" UTF8 strings in schema order. Emitted in both modes as plain OTLP string - // attributes keyed by the tag name. NOTE: the attribute-key representation (raw tag name vs a - // datadog.* namespace) is an open cross-team question with the OTLP/agent side -- see the PR. + // additional_metric_tags support is still evolving/TBD across most tracer SDKs. for (UTF8BytesString additionalTag : entry.getAdditionalTags()) { emitAdditionalTag(metric, additionalTag); } @@ -240,17 +243,27 @@ private void emitDataPointAttributes( emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); emitLongAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel ? 1L : 0L); + emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); if (entry.isSynthetics()) { emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); } } } - // Splits a packed "key:value" additional-tag string at the first ':' (keys cannot contain ':', - // values may) and emits it as an OTLP string attribute. Skips only malformed slots with no ':' or - // an empty key. An empty value ("key:") is emitted as key="": the aggregation path treats an - // explicitly-empty tag as a distinct dimension from an absent one, so dropping it here would - // export two separately-aggregated rows with identical OTLP attribute sets. + private static String canonicalSpanKind(CharSequence spanKind) { + if (Tags.SPAN_KIND_SERVER.contentEquals(spanKind)) { + return SPAN_KIND_SERVER; + } else if (Tags.SPAN_KIND_CLIENT.contentEquals(spanKind)) { + return SPAN_KIND_CLIENT; + } else if (Tags.SPAN_KIND_PRODUCER.contentEquals(spanKind)) { + return SPAN_KIND_PRODUCER; + } else if (Tags.SPAN_KIND_CONSUMER.contentEquals(spanKind)) { + return SPAN_KIND_CONSUMER; + } else { + return SPAN_KIND_INTERNAL; + } + } + private static void emitAdditionalTag(OtlpMetricVisitor metric, UTF8BytesString additionalTag) { String packed = additionalTag.toString(); int separator = packed.indexOf(':'); @@ -261,7 +274,6 @@ private static void emitAdditionalTag(OtlpMetricVisitor metric, UTF8BytesString STRING_ATTRIBUTE, packed.substring(0, separator), packed.substring(separator + 1)); } - // accepts both String literals and UTF8BytesString (both CharSequence); skips null values private static void emitStringAttribute( OtlpMetricVisitor metric, String key, @Nullable CharSequence value) { if (value != null) { @@ -272,4 +284,8 @@ private static void emitStringAttribute( private static void emitLongAttribute(OtlpMetricVisitor metric, String key, long value) { metric.visitAttribute(LONG_ATTRIBUTE, key, value); } + + private static void emitBooleanAttribute(OtlpMetricVisitor metric, String key, boolean value) { + metric.visitAttribute(BOOLEAN_ATTRIBUTE, key, value); + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java index 84ef6a9c3ae..1b9908676e2 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java @@ -2,6 +2,7 @@ import static datadog.communication.ddagent.TracerVersion.TRACER_VERSION; import static datadog.trace.api.config.GeneralConfig.ENV; +import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED; import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; import static datadog.trace.api.config.GeneralConfig.TAGS; import static datadog.trace.api.config.GeneralConfig.VERSION; @@ -15,13 +16,16 @@ import datadog.json.JsonMapper; import datadog.trace.api.Config; +import datadog.trace.api.ProcessTags; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -43,8 +47,8 @@ private static Properties props(String... keyValues) { return props; } - private static Map attrs(String... keyValues) { - Map map = new LinkedHashMap<>(); + private static Map attrs(String... keyValues) { + Map map = new LinkedHashMap<>(); for (int i = 0; i < keyValues.length; i += 2) { map.put(keyValues[i], keyValues[i + 1]); } @@ -54,6 +58,11 @@ private static Map attrs(String... keyValues) { return map; } + @AfterEach + void resetProcessTags() { + ProcessTags.reset(Config.get()); + } + static Stream resourceFragmentCases() { return Stream.of( Arguments.of( @@ -129,28 +138,24 @@ static Stream resourceFragmentCases() { @ParameterizedTest(name = "{0}") @MethodSource("resourceFragmentCases") void testBuildResourceFragment( - String caseName, Properties properties, Map expectedAttributes) + String caseName, Properties properties, Map expectedAttributes) throws IOException { Config config = Config.get(properties); String fragment = OtlpResourceJson.buildResourceFragment(config, Collections.emptyMap()); - Map actualAttributes = parseResourceAttributes(fragment); + Map actualAttributes = parseResourceAttributes(fragment); assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName); } - /** - * The datadog-attrs variant carries {@code datadog.runtime_id}; the plain variant omits it. - * (Process tags are emitted only when the experimental process-tag propagation is enabled, so - * they aren't asserted here.) - */ + /** The datadog-attrs variant carries {@code datadog.runtime_id}; the plain variant omits it. */ @Test void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { Config config = Config.get(props(SERVICE_NAME, "my-service")); - Map withDatadog = + Map withDatadog = parseResourceAttributes( OtlpResourceJson.buildResourceFragment(config, datadogResourceAttributes(config))); - Map plain = + Map plain = parseResourceAttributes( OtlpResourceJson.buildResourceFragment(config, Collections.emptyMap())); @@ -164,17 +169,35 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { assertFalse(plain.containsKey("datadog.runtime_id"), "plain variant omits datadog.runtime_id"); } + @Test + void datadogResourceAttributesVariantCarriesProcessTagsAsOneArrayValue() throws IOException { + Config config = + Config.get( + props(SERVICE_NAME, "my-service", EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true")); + ProcessTags.reset(config); + ProcessTags.addTag("entrypoint.name", "app"); + ProcessTags.addTag("entrypoint.type", "web"); + + Map withDatadog = + parseResourceAttributes( + OtlpResourceJson.buildResourceFragment(config, datadogResourceAttributes(config))); + + Object processTags = withDatadog.get("datadog.process_tags"); + assertTrue(processTags instanceof List, "datadog.process_tags is a single arrayValue"); + assertEquals(ProcessTags.getTagsAsStringList(), processTags); + } + @Test void statsComputedVariantCarriesMarker() throws IOException { Config withMetrics = Config.get(props(SERVICE_NAME, "my-service", OTEL_TRACES_SPAN_METRICS_ENABLED, "true")); Config withoutMetrics = Config.get(props(SERVICE_NAME, "my-service")); - Map withMarker = + Map withMarker = parseResourceAttributes( OtlpResourceJson.buildResourceFragment( withMetrics, traceResourceAttributes(withMetrics))); - Map without = + Map without = parseResourceAttributes( OtlpResourceJson.buildResourceFragment( withoutMetrics, traceResourceAttributes(withoutMetrics))); @@ -200,27 +223,41 @@ void cannedFragmentsMatchTheirProtoCounterparts() throws IOException { // ── parsing helpers ─────────────────────────────────────────────────────── @SuppressWarnings("unchecked") - private static Map parseResourceAttributes(String fragment) throws IOException { + private static Map parseResourceAttributes(String fragment) throws IOException { Map resource = JsonMapper.fromJsonToMap(fragment); List attributes = (List) resource.get("attributes"); - Map result = new LinkedHashMap<>(); + Map result = new LinkedHashMap<>(); for (Object attribute : attributes) { Map keyValue = (Map) attribute; Map value = (Map) keyValue.get("value"); - result.put((String) keyValue.get("key"), (String) value.get("stringValue")); + result.put((String) keyValue.get("key"), readAnyValue(value)); } return result; } - private static Map parseResourceAttributesFromProto(byte[] bytes) + @SuppressWarnings("unchecked") + private static Object readAnyValue(Map value) { + if (value.containsKey("arrayValue")) { + Map arrayValue = (Map) value.get("arrayValue"); + List elements = (List) arrayValue.get("values"); + List strings = new ArrayList<>(); + for (Object element : elements) { + strings.add((String) readAnyValue((Map) element)); + } + return strings; + } + return value.get("stringValue"); + } + + private static Map parseResourceAttributesFromProto(byte[] bytes) throws IOException { com.google.protobuf.CodedInputStream outer = com.google.protobuf.CodedInputStream.newInstance(bytes); outer.readTag(); com.google.protobuf.CodedInputStream resource = outer.readBytes().newCodedInput(); - Map attributes = new LinkedHashMap<>(); + Map attributes = new LinkedHashMap<>(); while (!resource.isAtEnd()) { resource.readTag(); com.google.protobuf.CodedInputStream kv = resource.readBytes().newCodedInput(); @@ -228,10 +265,23 @@ private static Map parseResourceAttributesFromProto(byte[] bytes String key = kv.readString(); kv.readTag(); com.google.protobuf.CodedInputStream av = kv.readBytes().newCodedInput(); - av.readTag(); - String value = av.readString(); - attributes.put(key, value); + attributes.put(key, readAnyValueFromProto(av)); } return attributes; } + + private static Object readAnyValueFromProto(com.google.protobuf.CodedInputStream av) + throws IOException { + int tag = av.readTag(); + if (com.google.protobuf.WireFormat.getTagFieldNumber(tag) == 5) { // array_value + com.google.protobuf.CodedInputStream arrayValue = av.readBytes().newCodedInput(); + List values = new ArrayList<>(); + while (!arrayValue.isAtEnd()) { + arrayValue.readTag(); // ArrayValue.values (field 1, repeated AnyValue) + values.add((String) readAnyValueFromProto(arrayValue.readBytes().newCodedInput())); + } + return values; + } + return av.readString(); // string_value (field 1) + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java index d9bfe5d9616..659c2b88786 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java @@ -2,6 +2,7 @@ import static datadog.communication.ddagent.TracerVersion.TRACER_VERSION; import static datadog.trace.api.config.GeneralConfig.ENV; +import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED; import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; import static datadog.trace.api.config.GeneralConfig.TAGS; import static datadog.trace.api.config.GeneralConfig.VERSION; @@ -16,12 +17,16 @@ import com.google.protobuf.CodedInputStream; import com.google.protobuf.WireFormat; import datadog.trace.api.Config; +import datadog.trace.api.ProcessTags; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -55,8 +60,8 @@ private static Properties props(String... keyValues) { return props; } - private static Map attrs(String... keyValues) { - Map map = new LinkedHashMap<>(); + private static Map attrs(String... keyValues) { + Map map = new LinkedHashMap<>(); for (int i = 0; i < keyValues.length; i += 2) { map.put(keyValues[i], keyValues[i + 1]); } @@ -66,6 +71,11 @@ private static Map attrs(String... keyValues) { return map; } + @AfterEach + void resetProcessTags() { + ProcessTags.reset(Config.get()); + } + static Stream resourceMessageCases() { return Stream.of( // service not set: should use the auto-detected name @@ -150,28 +160,27 @@ static Stream resourceMessageCases() { @ParameterizedTest(name = "{0}") @MethodSource("resourceMessageCases") void testBuildResourceMessage( - String caseName, Properties properties, Map expectedAttributes) + String caseName, Properties properties, Map expectedAttributes) throws IOException { Config config = Config.get(properties); byte[] bytes = OtlpResourceProto.buildResourceMessage(config, Collections.emptyMap()); - Map actualAttributes = parseResourceAttributes(bytes); + Map actualAttributes = parseResourceAttributes(bytes); assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName); } /** * The datadog-attrs variant ({@code buildResourceMessage(config, datadogResourceAttributes)}) - * carries {@code datadog.runtime_id}; the plain variant omits it. (Process tags are emitted only - * when the experimental process-tag propagation is enabled, so they aren't asserted here.) + * carries {@code datadog.runtime_id}; the plain variant omits it. */ @Test void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { Config config = Config.get(props(SERVICE_NAME, "my-service")); - Map withDatadog = + Map withDatadog = parseResourceAttributes( OtlpResourceProto.buildResourceMessage(config, datadogResourceAttributes(config))); - Map plain = + Map plain = parseResourceAttributes( OtlpResourceProto.buildResourceMessage(config, Collections.emptyMap())); @@ -185,17 +194,35 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { assertFalse(plain.containsKey("datadog.runtime_id"), "plain variant omits datadog.runtime_id"); } + @Test + void datadogResourceAttributesVariantCarriesProcessTagsAsOneArrayValue() throws IOException { + Config config = + Config.get( + props(SERVICE_NAME, "my-service", EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true")); + ProcessTags.reset(config); + ProcessTags.addTag("entrypoint.name", "app"); + ProcessTags.addTag("entrypoint.type", "web"); + + Map withDatadog = + parseResourceAttributes( + OtlpResourceProto.buildResourceMessage(config, datadogResourceAttributes(config))); + + Object processTags = withDatadog.get("datadog.process_tags"); + assertTrue(processTags instanceof List, "datadog.process_tags is a single arrayValue"); + assertEquals(ProcessTags.getTagsAsStringList(), processTags); + } + @Test void statsComputedVariantCarriesMarker() throws IOException { Config withMetrics = Config.get(props(SERVICE_NAME, "my-service", OTEL_TRACES_SPAN_METRICS_ENABLED, "true")); Config withoutMetrics = Config.get(props(SERVICE_NAME, "my-service")); - Map withMarker = + Map withMarker = parseResourceAttributes( OtlpResourceProto.buildResourceMessage( withMetrics, traceResourceAttributes(withMetrics))); - Map without = + Map without = parseResourceAttributes( OtlpResourceProto.buildResourceMessage( withoutMetrics, traceResourceAttributes(withoutMetrics))); @@ -214,9 +241,10 @@ void statsComputedVariantCarriesMarker() throws IOException { *

{@code buildResourceMessage} returns a length-prefixed message with an outer tag (field 1, * LEN wire type) followed by the Resource body size and body. Read the outer tag, then iterate * over all {@code Resource.attributes} (field 1, LEN wire type). Each attribute is a {@code - * KeyValue} whose {@code value} is an {@code AnyValue} containing a {@code string_value}. + * KeyValue} whose {@code value} is an {@code AnyValue} containing either a {@code string_value} + * (field 1) or, for {@code datadog.process_tags}, an {@code array_value} (field 5). */ - private static Map parseResourceAttributes(byte[] bytes) throws IOException { + private static Map parseResourceAttributes(byte[] bytes) throws IOException { // Read the outer tag (field 1, LEN wire type) that wraps the Resource body CodedInputStream outer = CodedInputStream.newInstance(bytes); int outerTag = outer.readTag(); @@ -224,7 +252,7 @@ private static Map parseResourceAttributes(byte[] bytes) throws assertEquals(WireFormat.WIRETYPE_LENGTH_DELIMITED, WireFormat.getTagWireType(outerTag)); CodedInputStream resource = outer.readBytes().newCodedInput(); - Map attributes = new LinkedHashMap<>(); + Map attributes = new LinkedHashMap<>(); while (!resource.isAtEnd()) { // Each attribute is Resource.attributes (field 1, LEN wire type) int tag = resource.readTag(); @@ -236,13 +264,7 @@ private static Map parseResourceAttributes(byte[] bytes) throws String key = readKeyField(kv); CodedInputStream av = readAnyValueField(kv); - - // Read AnyValue.string_value (field 1, LEN) - int avTag = av.readTag(); - assertEquals(1, WireFormat.getTagFieldNumber(avTag), "AnyValue.string_value is field 1"); - assertEquals(WireFormat.WIRETYPE_LENGTH_DELIMITED, WireFormat.getTagWireType(avTag)); - String value = av.readString(); - assertTrue(av.isAtEnd(), "no extra fields in AnyValue"); + Object value = readAnyValueBody(av); assertTrue(kv.isAtEnd(), "no extra fields in KeyValue"); attributes.put(key, value); @@ -268,4 +290,31 @@ private static CodedInputStream readAnyValueField(CodedInputStream kv) throws IO assertEquals(WireFormat.WIRETYPE_LENGTH_DELIMITED, WireFormat.getTagWireType(tag)); return kv.readBytes().newCodedInput(); } + + /** Reads {@code AnyValue.string_value} (field 1) or {@code AnyValue.array_value} (field 5). */ + private static Object readAnyValueBody(CodedInputStream av) throws IOException { + int avTag = av.readTag(); + int field = WireFormat.getTagFieldNumber(avTag); + assertEquals(WireFormat.WIRETYPE_LENGTH_DELIMITED, WireFormat.getTagWireType(avTag)); + Object value; + if (field == 5) { + value = readArrayValue(av.readBytes().newCodedInput()); + } else { + assertEquals(1, field, "AnyValue.string_value is field 1"); + value = av.readString(); + } + assertTrue(av.isAtEnd(), "no extra fields in AnyValue"); + return value; + } + + /** Reads {@code ArrayValue.values} (field 1, repeated {@code AnyValue}) into a string list. */ + private static List readArrayValue(CodedInputStream arrayValue) throws IOException { + List values = new ArrayList<>(); + while (!arrayValue.isAtEnd()) { + int tag = arrayValue.readTag(); + assertEquals(1, WireFormat.getTagFieldNumber(tag), "ArrayValue.values is field 1"); + values.add((String) readAnyValueBody(arrayValue.readBytes().newCodedInput())); + } + return values; + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index 3d5eee87b60..0547ae93261 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -295,9 +295,15 @@ private static Object readAnyValue(CodedInputStream any) throws IOException { case 1: // string_value value = any.readString(); break; + case 2: // bool_value + value = any.readBool(); + break; case 3: // int_value value = any.readInt64(); break; + case 5: // array_value + value = readArrayValue(any.readBytes().newCodedInput()); + break; default: any.skipField(tag); } @@ -305,6 +311,22 @@ private static Object readAnyValue(CodedInputStream any) throws IOException { return value; } + /** + * Decodes an {@code ArrayValue.values} (field 1, repeated {@code AnyValue}) into a string list. + */ + private static List readArrayValue(CodedInputStream arrayValue) throws IOException { + List values = new ArrayList<>(); + while (!arrayValue.isAtEnd()) { + int tag = arrayValue.readTag(); + if (WireFormat.getTagFieldNumber(tag) == 1) { + values.add((String) readAnyValue(arrayValue.readBytes().newCodedInput())); + } else { + arrayValue.skipField(tag); + } + } + return values; + } + // ── writer driver ───────────────────────────────────────────────────────── /** @@ -338,7 +360,7 @@ void okOnlyEntryProducesExactlyOneDataPoint() throws IOException { assertEquals(BUCKET_START, dp.start, "start_time_unix_nano == startBucket start"); assertEquals(BUCKET_START + BUCKET_DURATION, dp.end, "time_unix_nano == start + duration"); assertEquals(3L, dp.count); - assertFalse(dp.attributes.containsKey("status.code"), "ok point carries no status.code"); + assertEquals("STATUS_CODE_OK", dp.attributes.get("status.code"), "ok point → STATUS_CODE_OK"); } @Test @@ -356,7 +378,7 @@ void okPlusErrorEntryProducesTwoDataPointsWithErrorStatus() throws IOException { DataPoint errorPoint = null; DataPoint okPoint = null; for (DataPoint dp : metric.dataPoints) { - if ("ERROR".equals(dp.attributes.get("status.code"))) { + if ("STATUS_CODE_ERROR".equals(dp.attributes.get("status.code"))) { errorPoint = dp; errorCount = dp.count; } else { @@ -364,8 +386,9 @@ void okPlusErrorEntryProducesTwoDataPointsWithErrorStatus() throws IOException { okCount = dp.count; } } - assertNotNull(errorPoint, "one data point must carry status.code=ERROR"); - assertNotNull(okPoint, "one data point must omit status.code"); + assertNotNull(errorPoint, "one data point must carry status.code=STATUS_CODE_ERROR"); + assertNotNull(okPoint, "one data point must carry status.code=STATUS_CODE_OK"); + assertEquals("STATUS_CODE_OK", okPoint.attributes.get("status.code")); assertEquals(e.getOkLatencies().getCount(), (double) okCount, 1e-9); assertEquals(e.getErrorLatencies().getCount(), (double) errorCount, 1e-9); } @@ -387,8 +410,8 @@ void errorSeriesDoesNotLingerAfterClearWhenBucketHasOnlyOkHits() throws IOExcept assertEquals(2, bucket1.dataPoints.size(), "bucket with an error → ok+error data points"); assertTrue( bucket1.dataPoints.stream() - .anyMatch(dp -> "ERROR".equals(dp.attributes.get("status.code"))), - "bucket 1 must carry a status.code=ERROR point"); + .anyMatch(dp -> "STATUS_CODE_ERROR".equals(dp.attributes.get("status.code"))), + "bucket 1 must carry a status.code=STATUS_CODE_ERROR point"); // Bucket 2: same entry, reset then only OK hits. errorLatencies survives clear() (cleared, not // nulled), so a non-null-but-empty histogram must NOT emit a phantom zero-count error series. @@ -400,9 +423,10 @@ void errorSeriesDoesNotLingerAfterClearWhenBucketHasOnlyOkHits() throws IOExcept writer.finishBucket(); DecodedMetric bucket2 = decode(sender.lastPayload); assertEquals(1, bucket2.dataPoints.size(), "ok-only bucket → exactly one data point"); - assertFalse( - bucket2.dataPoints.get(0).attributes.containsKey("status.code"), - "recovered entry must not emit a lingering status.code=ERROR series"); + assertEquals( + "STATUS_CODE_OK", + bucket2.dataPoints.get(0).attributes.get("status.code"), + "recovered entry must not emit a lingering status.code=STATUS_CODE_ERROR series"); } @Test @@ -625,6 +649,52 @@ void defaultModeCarriesDatadogAttributes() throws IOException { // datadog.origin presence/absence is covered by defaultModeEmitsSyntheticOrigin } + @ParameterizedTest + @CsvSource({"true", "false"}) + void defaultModeEmitsIsTraceRoot(boolean traceRoot) throws IOException { + AggregateEntry e = + AggregateEntryTestUtils.of( + "GET /users", "web", "servlet.request", null, "web", 0, false, traceRoot, "server", + null, null, null, null); + AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); + + Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + assertEquals(traceRoot, attrs.get("datadog.is_trace_root")); + } + + @Test + void otelSemanticsModeOmitsIsTraceRoot() throws IOException { + AggregateEntry e = + AggregateEntryTestUtils.of( + "GET /users", "web", "servlet.request", null, "web", 0, false, true, "server", null, + null, null, null); + AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); + + Map attrs = writeAndDecode(true, e).dataPoints.get(0).attributes; + assertFalse(attrs.containsKey("datadog.is_trace_root")); + } + + @ParameterizedTest + @CsvSource({ + "server, SPAN_KIND_SERVER", + "client, SPAN_KIND_CLIENT", + "producer, SPAN_KIND_PRODUCER", + "consumer, SPAN_KIND_CONSUMER", + "broker, SPAN_KIND_INTERNAL", + "'', SPAN_KIND_INTERNAL", + }) + void spanKindIsCanonicalizedToUppercaseEnumName(String spanKind, String expected) + throws IOException { + AggregateEntry e = + AggregateEntryTestUtils.of( + "GET /users", "web", "servlet.request", null, "web", 0, false, true, spanKind, null, + null, null, null); + AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); + + Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + assertEquals(expected, attrs.get("span.kind")); + } + /** * In default mode a synthetic entry emits {@code datadog.origin = "synthetics"}; a non-synthetic * entry omits the attribute. Origin has collapsed to a boolean {@code synthetic} flag upstream, From eb11a698ed1cee7905d8ef74a5529ef584f79bdd Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Tue, 4 Aug 2026 16:05:00 -0400 Subject: [PATCH 02/15] Fix spotless formatting violations Corrects a reversed static-import order in OtlpResourceJson.java and reformats a few lines flagged by google-java-format. Co-Authored-By: Claude Sonnet 5 --- .../otlp/common/OtlpResourceAttributes.java | 5 +-- .../core/otlp/common/OtlpResourceJson.java | 7 ++-- .../core/otlp/common/OtlpResourceProto.java | 5 +-- .../metrics/OtlpStatsMetricWriterTest.java | 35 ++++++++++++++----- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java index ebc5b732b13..67a71dea6ac 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java @@ -36,10 +36,7 @@ private OtlpResourceAttributes() {} "telemetry.sdk.version", "telemetry.sdk.language")); - /** - * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code - * List}. - */ + /** {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ static void visitResourceAttributes( Config config, Map extraAttributes, BiConsumer visitor) { String serviceName = config.getServiceName(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index 0db64e8da9b..55e8cb39fcf 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -1,7 +1,7 @@ package datadog.trace.core.otlp.common; -import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.core.otlp.common.OtlpCommonJson.writeAttribute; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; @@ -51,10 +51,7 @@ static String buildResourceFragment(Config config, Map extraAttr } } - /** - * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code - * List}. - */ + /** {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ @SuppressWarnings("unchecked") private static void writeResourceAttribute(JsonWriter writer, String key, Object value) { if (value instanceof List) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index 67536777673..2b617d33159 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -54,10 +54,7 @@ static byte[] buildResourceMessage(Config config, Map extraAttri return resourceMessage; } - /** - * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code - * List}. - */ + /** {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ @SuppressWarnings("unchecked") private static void writeResourceAttribute(StreamingBuffer buf, String key, Object value) { writeTag(buf, 1, LEN_WIRE_TYPE); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index 0547ae93261..e83c9e14f48 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -654,8 +654,19 @@ void defaultModeCarriesDatadogAttributes() throws IOException { void defaultModeEmitsIsTraceRoot(boolean traceRoot) throws IOException { AggregateEntry e = AggregateEntryTestUtils.of( - "GET /users", "web", "servlet.request", null, "web", 0, false, traceRoot, "server", - null, null, null, null); + "GET /users", + "web", + "servlet.request", + null, + "web", + 0, + false, + traceRoot, + "server", + null, + null, + null, + null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; @@ -664,10 +675,7 @@ void defaultModeEmitsIsTraceRoot(boolean traceRoot) throws IOException { @Test void otelSemanticsModeOmitsIsTraceRoot() throws IOException { - AggregateEntry e = - AggregateEntryTestUtils.of( - "GET /users", "web", "servlet.request", null, "web", 0, false, true, "server", null, - null, null, null); + AggregateEntry e = entry("GET /users", false, 0, null, null, null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); Map attrs = writeAndDecode(true, e).dataPoints.get(0).attributes; @@ -687,8 +695,19 @@ void spanKindIsCanonicalizedToUppercaseEnumName(String spanKind, String expected throws IOException { AggregateEntry e = AggregateEntryTestUtils.of( - "GET /users", "web", "servlet.request", null, "web", 0, false, true, spanKind, null, - null, null, null); + "GET /users", + "web", + "servlet.request", + null, + "web", + 0, + false, + true, + spanKind, + null, + null, + null, + null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; From 473dd657337c22376a4679de4f6863edbb503153 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Tue, 4 Aug 2026 16:49:19 -0400 Subject: [PATCH 03/15] Emit service.name unconditionally on OTLP trace-metrics data points service.name was only emitted on a data point when it differed from the writer's configured default service, which contradicts the RFC's always-present requirement already applied to status.code, span.kind, and is_trace_root. Drop the now-unused defaultService field/param. --- .../otlp/metrics/OtlpStatsMetricWriter.java | 23 +++------- .../metrics/OtlpStatsMetricWriterTest.java | 46 +++++++++---------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index 4f8742ced17..c75e40b510b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -69,8 +69,6 @@ public final class OtlpStatsMetricWriter implements MetricWriter { @Nullable private final OtlpSender sender; private final boolean otelSemanticsMode; - @Nullable private final String defaultService; - // own single-thread collector; forced to DELTA since trace-stats buckets are per-interval deltas. private final OtlpMetricsCollector collector; @@ -100,25 +98,19 @@ public OtlpStatsMetricWriter(Config config) { this( OtlpMetricsSenderFactory.create(config), config.getOtlpMetricsProtocol(), - config.isTraceOtelSemanticsEnabled(), - config.getServiceName()); + config.isTraceOtelSemanticsEnabled()); } // visible for testing: lets tests inject a capturing sender to decode the emitted payload and - // control the semantics mode and default service - OtlpStatsMetricWriter( - @Nullable OtlpSender sender, boolean otelSemanticsMode, @Nullable String defaultService) { - this(sender, OtlpConfig.Protocol.HTTP_PROTOBUF, otelSemanticsMode, defaultService); + // control the semantics mode + OtlpStatsMetricWriter(@Nullable OtlpSender sender, boolean otelSemanticsMode) { + this(sender, OtlpConfig.Protocol.HTTP_PROTOBUF, otelSemanticsMode); } private OtlpStatsMetricWriter( - @Nullable OtlpSender sender, - OtlpConfig.Protocol protocol, - boolean otelSemanticsMode, - @Nullable String defaultService) { + @Nullable OtlpSender sender, OtlpConfig.Protocol protocol, boolean otelSemanticsMode) { this.sender = sender; this.otelSemanticsMode = otelSemanticsMode; - this.defaultService = defaultService; // Default mode carries datadog.runtime_id / process tags on the Resource; OTel-semantics mode // uses the plain vendor-neutral resource (no datadog.*). this.collector = @@ -218,10 +210,7 @@ private void emitDataPointAttributes( emitStringAttribute(metric, STATUS_CODE, error ? STATUS_CODE_ERROR : STATUS_CODE_OK); emitStringAttribute(metric, SPAN_NAME, entry.getResource()); emitStringAttribute(metric, SPAN_KIND, canonicalSpanKind(entry.getSpanKind())); - UTF8BytesString service = entry.getService(); - if (service != null && service.length() > 0 && !service.toString().equals(defaultService)) { - emitStringAttribute(metric, SERVICE_NAME, service); - } + emitStringAttribute(metric, SERVICE_NAME, entry.getService()); if (entry.hasHttpMethod()) { emitStringAttribute(metric, HTTP_REQUEST_METHOD, entry.getHttpMethod()); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index e83c9e14f48..7b34111355b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -337,7 +337,7 @@ private static List readArrayValue(CodedInputStream arrayValue) throws I private static DecodedMetric writeAndDecode(boolean otelSemanticsMode, AggregateEntry entry) throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, otelSemanticsMode, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, otelSemanticsMode); writer.startBucket(1, BUCKET_START, BUCKET_DURATION); writer.add(entry); writer.finishBucket(); @@ -396,7 +396,7 @@ void okPlusErrorEntryProducesTwoDataPointsWithErrorStatus() throws IOException { @Test void errorSeriesDoesNotLingerAfterClearWhenBucketHasOnlyOkHits() throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); // Bucket 1: the entry sees an error, so its error histogram is allocated and emits a point. AggregateEntry e = entry("GET /users", false, 0, null, null, null); @@ -550,39 +550,35 @@ void emptyValueEmittedButMalformedSlotsSkipped() throws IOException { } @Test - void serviceNameEmittedOnlyForNonDefaultService() throws IOException { + void serviceNameAlwaysEmittedOnDataPoint() throws IOException { CapturingSender sender = new CapturingSender(); - // The configured default service ("web") is reported on the resource; only a span whose service - // differs from it repeats service.name on its own data point. - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false, "web"); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); long start = SECONDS.toNanos(1_700_000_000L); writer.startBucket(2, start, SECONDS.toNanos(10)); - writer.add(serviceEntry("web.request", "web")); // default service - writer.add(serviceEntry("db.query", "postgres")); // custom service + writer.add(serviceEntry("web.request", "web")); + writer.add(serviceEntry("db.query", "postgres")); writer.finishBucket(); DecodedMetric metric = decode(sender.lastPayload); assertEquals(2, metric.dataPoints.size()); - Map defaultAttrs = null; - Map customAttrs = null; + Map webAttrs = null; + Map postgresAttrs = null; for (DataPoint dp : metric.dataPoints) { if ("db.query".equals(dp.attributes.get("datadog.operation.name"))) { - customAttrs = dp.attributes; + postgresAttrs = dp.attributes; } else { - defaultAttrs = dp.attributes; + webAttrs = dp.attributes; } } - assertNotNull(customAttrs, "custom-service data point present"); - assertNotNull(defaultAttrs, "default-service data point present"); + assertNotNull(postgresAttrs, "postgres data point present"); + assertNotNull(webAttrs, "web data point present"); + assertEquals("postgres", postgresAttrs.get("service.name")); assertEquals( - "postgres", - customAttrs.get("service.name"), - "non-default service is carried on its own data point"); - assertFalse( - defaultAttrs.containsKey("service.name"), - "default service must not be repeated on its data point"); + "web", + webAttrs.get("service.name"), + "service.name is emitted unconditionally, even matching the tracer's own default service"); } /** An ok-only entry on the given service and operation name, recording a single 1s hit. */ @@ -609,7 +605,7 @@ private static AggregateEntry serviceEntry(String operationName, String service) @Test void emptyBucketSendsNothing() { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); writer.startBucket(0, BUCKET_START, BUCKET_DURATION); writer.finishBucket(); // no add() @@ -621,7 +617,7 @@ void emptyBucketSendsNothing() { @Test void nullSenderDoesNotThrowOnNonEmptyBucket() { // mirrors the HTTP_JSON path where createSender(config) returns null. - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(null, false, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(null, false); writer.startBucket(1, BUCKET_START, BUCKET_DURATION); writer.add(okEntry(SECONDS.toNanos(1), 2)); try { @@ -760,7 +756,7 @@ void snapshotsEntryDataBeforeAggregatorClearsIt() throws IOException { // (and the top-level count) at add() time; if it deferred reading to finishBucket() it would // encode the already-cleared (empty, zero-count) entry. CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); AggregateEntry e = entry("servlet.request", false, 0, null, null, null); AggregateEntryTestUtils.recordTopLevel(e, SECONDS.toNanos(1)); @@ -788,7 +784,7 @@ void defaultModeResourceCarriesRuntimeId() throws IOException { // runtime-id is enabled by default, so default-mode payloads carry datadog.runtime_id on the // Resource. CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); writer.startBucket(1, SECONDS.toNanos(1_700_000_000L), SECONDS.toNanos(10)); writer.add(okEntry(SECONDS.toNanos(1), 1)); writer.finishBucket(); @@ -805,7 +801,7 @@ void defaultModeResourceCarriesRuntimeId() throws IOException { @Test void otelSemanticsModeResourceOmitsDatadogAttributes() throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, true, null); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, true); writer.startBucket(1, SECONDS.toNanos(1_700_000_000L), SECONDS.toNanos(10)); writer.add(okEntry(SECONDS.toNanos(1), 1)); writer.finishBucket(); From 7145875a871b2f6758056d844c129d0f4f95f1e3 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Tue, 4 Aug 2026 17:04:17 -0400 Subject: [PATCH 04/15] Correct Javadoc wrap direction for the process_tags value-shape note The earlier spotless fix collapsed this Javadoc onto one physical line, but the formatter's own target (confirmed from the spotless job trace) keeps the three-line /** ... */ block and only unwraps the content itself onto a single line inside it. --- .../trace/core/otlp/common/OtlpResourceAttributes.java | 4 +++- .../java/datadog/trace/core/otlp/common/OtlpResourceJson.java | 4 +++- .../datadog/trace/core/otlp/common/OtlpResourceProto.java | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java index 67a71dea6ac..227c144b1b8 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java @@ -36,7 +36,9 @@ private OtlpResourceAttributes() {} "telemetry.sdk.version", "telemetry.sdk.language")); - /** {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. + */ static void visitResourceAttributes( Config config, Map extraAttributes, BiConsumer visitor) { String serviceName = config.getServiceName(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index 55e8cb39fcf..d400bcc030d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -51,7 +51,9 @@ static String buildResourceFragment(Config config, Map extraAttr } } - /** {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. + */ @SuppressWarnings("unchecked") private static void writeResourceAttribute(JsonWriter writer, String key, Object value) { if (value instanceof List) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index 2b617d33159..a28a87d2943 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -54,7 +54,9 @@ static byte[] buildResourceMessage(Config config, Map extraAttri return resourceMessage; } - /** {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. + */ @SuppressWarnings("unchecked") private static void writeResourceAttribute(StreamingBuffer buf, String key, Object value) { writeTag(buf, 1, LEN_WIRE_TYPE); From 43eb7e924e10d742e7ec5b2f578114de524e8f31 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 12:16:59 -0400 Subject: [PATCH 05/15] Emit datadog.peer_tags on OTLP trace-metrics data points Reuses the existing STRING_ARRAY_ATTRIBUTE visitor plumbing already wired for both proto and JSON collectors. Co-Authored-By: Claude Sonnet 5 --- .../otlp/metrics/OtlpStatsMetricWriter.java | 14 +++++++ .../metrics/OtlpStatsMetricWriterTest.java | 37 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index c75e40b510b..5b9c688880f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -3,6 +3,7 @@ import static datadog.trace.bootstrap.otel.metrics.OtelInstrumentType.HISTOGRAM; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.BOOLEAN_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.LONG_ATTRIBUTE; +import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import datadog.metrics.api.Histogram; @@ -58,6 +59,7 @@ public final class OtlpStatsMetricWriter implements MetricWriter { private static final String DATADOG_SPAN_TOP_LEVEL = "datadog.span.top_level"; private static final String DATADOG_IS_TRACE_ROOT = "datadog.is_trace_root"; private static final String DATADOG_ORIGIN = "datadog.origin"; + private static final String DATADOG_PEER_TAGS = "datadog.peer_tags"; private static final String SYNTHETICS_ORIGIN = "synthetics"; private static final String SPAN_KIND_SERVER = "SPAN_KIND_SERVER"; @@ -236,9 +238,21 @@ private void emitDataPointAttributes( if (entry.isSynthetics()) { emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); } + emitPeerTags(metric, entry.getPeerTags()); } } + private static void emitPeerTags(OtlpMetricVisitor metric, List peerTags) { + if (peerTags.isEmpty()) { + return; + } + List peerTagValues = new ArrayList<>(peerTags.size()); + for (UTF8BytesString peerTag : peerTags) { + peerTagValues.add(peerTag.toString()); + } + metric.visitAttribute(STRING_ARRAY_ATTRIBUTE, DATADOG_PEER_TAGS, peerTagValues); + } + private static String canonicalSpanKind(CharSequence spanKind) { if (Tags.SPAN_KIND_SERVER.contentEquals(spanKind)) { return SPAN_KIND_SERVER; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index 7b34111355b..af6a50ab43e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -21,6 +21,7 @@ import datadog.trace.core.otlp.common.OtlpSender; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -678,6 +679,42 @@ void otelSemanticsModeOmitsIsTraceRoot() throws IOException { assertFalse(attrs.containsKey("datadog.is_trace_root")); } + @Test + void defaultModeEmitsPeerTags() throws IOException { + AggregateEntry e = + AggregateEntryTestUtils.of( + "GET /users", + "web", + "servlet.request", + null, + "web", + 0, + false, + true, + "client", + Arrays.asList( + UTF8BytesString.create("peer.service:downstream"), + UTF8BytesString.create("net.peer.name:downstream.example.com")), + null, + null, + null); + AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); + + Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + assertEquals( + Arrays.asList("peer.service:downstream", "net.peer.name:downstream.example.com"), + attrs.get("datadog.peer_tags")); + } + + @Test + void defaultModeOmitsPeerTagsWhenEmpty() throws IOException { + AggregateEntry e = entry("GET /users", false, 0, null, null, null); + AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); + + Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + assertFalse(attrs.containsKey("datadog.peer_tags")); + } + @ParameterizedTest @CsvSource({ "server, SPAN_KIND_SERVER", From e47c3183cfde75eacbe3b85ae2f9bbfda67fa4c1 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 15:39:29 -0400 Subject: [PATCH 06/15] Preserve OTLP resource attribute types --- .../otlp/common/OtlpResourceAttributes.java | 57 ++++++++++++------- .../core/otlp/common/OtlpResourceJson.java | 27 +++------ .../core/otlp/common/OtlpResourceProto.java | 35 +++++------- .../otlp/common/OtlpResourceJsonTest.java | 7 +-- .../otlp/common/OtlpResourceProtoTest.java | 7 +-- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java index 227c144b1b8..79c16d975b5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java @@ -5,6 +5,7 @@ import datadog.trace.api.Config; import datadog.trace.api.ProcessTags; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -36,31 +37,31 @@ private OtlpResourceAttributes() {} "telemetry.sdk.version", "telemetry.sdk.language")); - /** - * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. - */ static void visitResourceAttributes( - Config config, Map extraAttributes, BiConsumer visitor) { + Config config, + ExtraAttributes extraAttributes, + BiConsumer stringVisitor, + BiConsumer> stringArrayVisitor) { String serviceName = config.getServiceName(); String env = config.getEnv(); String version = config.getVersion(); - visitor.accept("service.name", serviceName); + stringVisitor.accept("service.name", serviceName); if (!env.isEmpty()) { - visitor.accept("deployment.environment.name", env); + stringVisitor.accept("deployment.environment.name", env); } if (!version.isEmpty()) { - visitor.accept("service.version", version); + stringVisitor.accept("service.version", version); } if (config.isReportHostName()) { String hostName = config.getHostName(); if (hostName != null && !hostName.isEmpty()) { - visitor.accept("host.name", hostName); + stringVisitor.accept("host.name", hostName); } } - visitor.accept("telemetry.sdk.name", "datadog"); - visitor.accept("telemetry.sdk.version", TRACER_VERSION); - visitor.accept("telemetry.sdk.language", "java"); + stringVisitor.accept("telemetry.sdk.name", "datadog"); + stringVisitor.accept("telemetry.sdk.version", TRACER_VERSION); + stringVisitor.accept("telemetry.sdk.language", "java"); config .getGlobalTags() @@ -68,11 +69,14 @@ static void visitResourceAttributes( (key, value) -> { // ignore datadog tags and their otel equivalents that we map above if (!IGNORED_GLOBAL_TAGS.contains(key.toLowerCase(Locale.ROOT))) { - visitor.accept(key, value); + stringVisitor.accept(key, value); } }); - extraAttributes.forEach(visitor); + extraAttributes.stringAttributes.forEach(stringVisitor); + if (!extraAttributes.processTags.isEmpty()) { + stringArrayVisitor.accept(PROCESS_TAGS_KEY, extraAttributes.processTags); + } } private static final String PROCESS_TAGS_KEY = DATADOG_PREFIX + "process_tags"; @@ -82,16 +86,16 @@ static void visitResourceAttributes( * marker when the SDK is computing OTLP span metrics, so a downstream Agent does not recompute * them from the exported spans. */ - static Map traceResourceAttributes(Config config) { - Map attributes = new LinkedHashMap<>(); + static ExtraAttributes traceResourceAttributes(Config config) { + Map attributes = new LinkedHashMap<>(); if (config.isOtelTracesSpanMetricsEnabled()) { attributes.put(STATS_COMPUTED_KEY, "true"); } - return attributes; + return new ExtraAttributes(attributes, Collections.emptyList()); } - static Map datadogResourceAttributes(Config config) { - Map attributes = new LinkedHashMap<>(); + static ExtraAttributes datadogResourceAttributes(Config config) { + Map attributes = new LinkedHashMap<>(); String runtimeId = config.getRuntimeId(); if (runtimeId != null && !runtimeId.isEmpty()) { attributes.put(DATADOG_PREFIX + "runtime_id", runtimeId); @@ -99,8 +103,21 @@ static Map datadogResourceAttributes(Config config) { // Mirrors SerializingMetricWriter's v0.6 ProcessTags shape; keep both in sync if that changes. List processTags = ProcessTags.getTagsAsStringList(); if (processTags != null && !processTags.isEmpty()) { - attributes.put(PROCESS_TAGS_KEY, processTags); + return new ExtraAttributes(attributes, processTags); + } + return new ExtraAttributes(attributes, Collections.emptyList()); + } + + static final class ExtraAttributes { + static final ExtraAttributes EMPTY = + new ExtraAttributes(Collections.emptyMap(), Collections.emptyList()); + + private final Map stringAttributes; + private final List processTags; + + private ExtraAttributes(Map stringAttributes, List processTags) { + this.stringAttributes = stringAttributes; + this.processTags = processTags; } - return attributes; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index d400bcc030d..ae3acab46b7 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -3,23 +3,21 @@ import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.core.otlp.common.OtlpCommonJson.writeAttribute; +import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.visitResourceAttributes; import datadog.json.JsonWriter; import datadog.trace.api.Config; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes; /** Provides a canned JSON fragment for OpenTelemetry's "resource.proto" JSON encoding. */ public final class OtlpResourceJson { private OtlpResourceJson() {} /** Vendor-neutral resource (no {@code datadog.*}). Used by the OTLP metric export. */ - public static final String RESOURCE_FRAGMENT = - buildResourceFragment(Config.get(), Collections.emptyMap()); + public static final String RESOURCE_FRAGMENT = buildResourceFragment(Config.get(), EMPTY); /** * Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed @@ -37,29 +35,20 @@ private OtlpResourceJson() {} public static final String TRACE_RESOURCE_FRAGMENT = buildResourceFragment(Config.get(), traceResourceAttributes(Config.get())); - static String buildResourceFragment(Config config, Map extraAttributes) { + static String buildResourceFragment(Config config, ExtraAttributes extraAttributes) { try (JsonWriter writer = new JsonWriter()) { writer.beginObject(); writer.name("attributes").beginArray(); visitResourceAttributes( - config, extraAttributes, (key, value) -> writeResourceAttribute(writer, key, value)); + config, + extraAttributes, + (key, value) -> writeAttribute(writer, STRING_ATTRIBUTE, key, value), + (key, value) -> writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, value)); writer.endArray(); writer.endObject(); return writer.toString(); } } - - /** - * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. - */ - @SuppressWarnings("unchecked") - private static void writeResourceAttribute(JsonWriter writer, String key, Object value) { - if (value instanceof List) { - writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, (List) value); - } else { - writeAttribute(writer, STRING_ATTRIBUTE, key, value); - } - } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index a28a87d2943..cf1a06a39af 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -5,24 +5,21 @@ import static datadog.trace.core.otlp.common.OtlpCommonProto.LEN_WIRE_TYPE; import static datadog.trace.core.otlp.common.OtlpCommonProto.writeAttribute; import static datadog.trace.core.otlp.common.OtlpCommonProto.writeTag; +import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.visitResourceAttributes; import datadog.communication.serialization.GrowableBuffer; -import datadog.communication.serialization.StreamingBuffer; import datadog.trace.api.Config; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes; /** Provides a canned message for OpenTelemetry's "resource.proto" wire protocol. */ public final class OtlpResourceProto { private OtlpResourceProto() {} /** Vendor-neutral resource (no {@code datadog.*}). Used by the OTLP metric export. */ - public static final byte[] RESOURCE_MESSAGE = - buildResourceMessage(Config.get(), Collections.emptyMap()); + public static final byte[] RESOURCE_MESSAGE = buildResourceMessage(Config.get(), EMPTY); /** * Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed @@ -40,11 +37,20 @@ private OtlpResourceProto() {} public static final byte[] TRACE_RESOURCE_MESSAGE = buildResourceMessage(Config.get(), traceResourceAttributes(Config.get())); - static byte[] buildResourceMessage(Config config, Map extraAttributes) { + static byte[] buildResourceMessage(Config config, ExtraAttributes extraAttributes) { GrowableBuffer buf = new GrowableBuffer(512); visitResourceAttributes( - config, extraAttributes, (key, value) -> writeResourceAttribute(buf, key, value)); + config, + extraAttributes, + (key, value) -> { + writeTag(buf, 1, LEN_WIRE_TYPE); + writeAttribute(buf, STRING_ATTRIBUTE, key, value); + }, + (key, value) -> { + writeTag(buf, 1, LEN_WIRE_TYPE); + writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, value); + }); OtlpProtoBuffer protobuf = new OtlpProtoBuffer(buf.capacity()); int numBytes = protobuf.recordMessage(buf, 1); @@ -53,17 +59,4 @@ static byte[] buildResourceMessage(Config config, Map extraAttri return resourceMessage; } - - /** - * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. - */ - @SuppressWarnings("unchecked") - private static void writeResourceAttribute(StreamingBuffer buf, String key, Object value) { - writeTag(buf, 1, LEN_WIRE_TYPE); - if (value instanceof List) { - writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, (List) value); - } else { - writeAttribute(buf, STRING_ATTRIBUTE, key, value); - } - } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java index 1b9908676e2..9d09fa1e61e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java @@ -8,6 +8,7 @@ import static datadog.trace.api.config.GeneralConfig.VERSION; import static datadog.trace.api.config.OtlpConfig.OTEL_TRACES_SPAN_METRICS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_REPORT_HOSTNAME; +import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -19,7 +20,6 @@ import datadog.trace.api.ProcessTags; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -141,7 +141,7 @@ void testBuildResourceFragment( String caseName, Properties properties, Map expectedAttributes) throws IOException { Config config = Config.get(properties); - String fragment = OtlpResourceJson.buildResourceFragment(config, Collections.emptyMap()); + String fragment = OtlpResourceJson.buildResourceFragment(config, EMPTY); Map actualAttributes = parseResourceAttributes(fragment); assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName); @@ -156,8 +156,7 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { parseResourceAttributes( OtlpResourceJson.buildResourceFragment(config, datadogResourceAttributes(config))); Map plain = - parseResourceAttributes( - OtlpResourceJson.buildResourceFragment(config, Collections.emptyMap())); + parseResourceAttributes(OtlpResourceJson.buildResourceFragment(config, EMPTY)); assertTrue( withDatadog.containsKey("datadog.runtime_id"), diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java index 659c2b88786..7e7bef0d6a5 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java @@ -8,6 +8,7 @@ import static datadog.trace.api.config.GeneralConfig.VERSION; import static datadog.trace.api.config.OtlpConfig.OTEL_TRACES_SPAN_METRICS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_REPORT_HOSTNAME; +import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -20,7 +21,6 @@ import datadog.trace.api.ProcessTags; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -163,7 +163,7 @@ void testBuildResourceMessage( String caseName, Properties properties, Map expectedAttributes) throws IOException { Config config = Config.get(properties); - byte[] bytes = OtlpResourceProto.buildResourceMessage(config, Collections.emptyMap()); + byte[] bytes = OtlpResourceProto.buildResourceMessage(config, EMPTY); Map actualAttributes = parseResourceAttributes(bytes); assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName); @@ -181,8 +181,7 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { parseResourceAttributes( OtlpResourceProto.buildResourceMessage(config, datadogResourceAttributes(config))); Map plain = - parseResourceAttributes( - OtlpResourceProto.buildResourceMessage(config, Collections.emptyMap())); + parseResourceAttributes(OtlpResourceProto.buildResourceMessage(config, EMPTY)); assertTrue( withDatadog.containsKey("datadog.runtime_id"), From b67bf6c2ed1442c961e1aa83c4b8d87fbd68f864 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 15:54:53 -0400 Subject: [PATCH 07/15] Revert "Preserve OTLP resource attribute types" This reverts commit e47c3183cfde75eacbe3b85ae2f9bbfda67fa4c1. --- .../otlp/common/OtlpResourceAttributes.java | 57 +++++++------------ .../core/otlp/common/OtlpResourceJson.java | 27 ++++++--- .../core/otlp/common/OtlpResourceProto.java | 35 +++++++----- .../otlp/common/OtlpResourceJsonTest.java | 7 ++- .../otlp/common/OtlpResourceProtoTest.java | 7 ++- 5 files changed, 68 insertions(+), 65 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java index 79c16d975b5..227c144b1b8 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java @@ -5,7 +5,6 @@ import datadog.trace.api.Config; import datadog.trace.api.ProcessTags; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -37,31 +36,31 @@ private OtlpResourceAttributes() {} "telemetry.sdk.version", "telemetry.sdk.language")); + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. + */ static void visitResourceAttributes( - Config config, - ExtraAttributes extraAttributes, - BiConsumer stringVisitor, - BiConsumer> stringArrayVisitor) { + Config config, Map extraAttributes, BiConsumer visitor) { String serviceName = config.getServiceName(); String env = config.getEnv(); String version = config.getVersion(); - stringVisitor.accept("service.name", serviceName); + visitor.accept("service.name", serviceName); if (!env.isEmpty()) { - stringVisitor.accept("deployment.environment.name", env); + visitor.accept("deployment.environment.name", env); } if (!version.isEmpty()) { - stringVisitor.accept("service.version", version); + visitor.accept("service.version", version); } if (config.isReportHostName()) { String hostName = config.getHostName(); if (hostName != null && !hostName.isEmpty()) { - stringVisitor.accept("host.name", hostName); + visitor.accept("host.name", hostName); } } - stringVisitor.accept("telemetry.sdk.name", "datadog"); - stringVisitor.accept("telemetry.sdk.version", TRACER_VERSION); - stringVisitor.accept("telemetry.sdk.language", "java"); + visitor.accept("telemetry.sdk.name", "datadog"); + visitor.accept("telemetry.sdk.version", TRACER_VERSION); + visitor.accept("telemetry.sdk.language", "java"); config .getGlobalTags() @@ -69,14 +68,11 @@ static void visitResourceAttributes( (key, value) -> { // ignore datadog tags and their otel equivalents that we map above if (!IGNORED_GLOBAL_TAGS.contains(key.toLowerCase(Locale.ROOT))) { - stringVisitor.accept(key, value); + visitor.accept(key, value); } }); - extraAttributes.stringAttributes.forEach(stringVisitor); - if (!extraAttributes.processTags.isEmpty()) { - stringArrayVisitor.accept(PROCESS_TAGS_KEY, extraAttributes.processTags); - } + extraAttributes.forEach(visitor); } private static final String PROCESS_TAGS_KEY = DATADOG_PREFIX + "process_tags"; @@ -86,16 +82,16 @@ static void visitResourceAttributes( * marker when the SDK is computing OTLP span metrics, so a downstream Agent does not recompute * them from the exported spans. */ - static ExtraAttributes traceResourceAttributes(Config config) { - Map attributes = new LinkedHashMap<>(); + static Map traceResourceAttributes(Config config) { + Map attributes = new LinkedHashMap<>(); if (config.isOtelTracesSpanMetricsEnabled()) { attributes.put(STATS_COMPUTED_KEY, "true"); } - return new ExtraAttributes(attributes, Collections.emptyList()); + return attributes; } - static ExtraAttributes datadogResourceAttributes(Config config) { - Map attributes = new LinkedHashMap<>(); + static Map datadogResourceAttributes(Config config) { + Map attributes = new LinkedHashMap<>(); String runtimeId = config.getRuntimeId(); if (runtimeId != null && !runtimeId.isEmpty()) { attributes.put(DATADOG_PREFIX + "runtime_id", runtimeId); @@ -103,21 +99,8 @@ static ExtraAttributes datadogResourceAttributes(Config config) { // Mirrors SerializingMetricWriter's v0.6 ProcessTags shape; keep both in sync if that changes. List processTags = ProcessTags.getTagsAsStringList(); if (processTags != null && !processTags.isEmpty()) { - return new ExtraAttributes(attributes, processTags); - } - return new ExtraAttributes(attributes, Collections.emptyList()); - } - - static final class ExtraAttributes { - static final ExtraAttributes EMPTY = - new ExtraAttributes(Collections.emptyMap(), Collections.emptyList()); - - private final Map stringAttributes; - private final List processTags; - - private ExtraAttributes(Map stringAttributes, List processTags) { - this.stringAttributes = stringAttributes; - this.processTags = processTags; + attributes.put(PROCESS_TAGS_KEY, processTags); } + return attributes; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index ae3acab46b7..d400bcc030d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -3,21 +3,23 @@ import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ARRAY_ATTRIBUTE; import static datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor.STRING_ATTRIBUTE; import static datadog.trace.core.otlp.common.OtlpCommonJson.writeAttribute; -import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.visitResourceAttributes; import datadog.json.JsonWriter; import datadog.trace.api.Config; -import datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** Provides a canned JSON fragment for OpenTelemetry's "resource.proto" JSON encoding. */ public final class OtlpResourceJson { private OtlpResourceJson() {} /** Vendor-neutral resource (no {@code datadog.*}). Used by the OTLP metric export. */ - public static final String RESOURCE_FRAGMENT = buildResourceFragment(Config.get(), EMPTY); + public static final String RESOURCE_FRAGMENT = + buildResourceFragment(Config.get(), Collections.emptyMap()); /** * Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed @@ -35,20 +37,29 @@ private OtlpResourceJson() {} public static final String TRACE_RESOURCE_FRAGMENT = buildResourceFragment(Config.get(), traceResourceAttributes(Config.get())); - static String buildResourceFragment(Config config, ExtraAttributes extraAttributes) { + static String buildResourceFragment(Config config, Map extraAttributes) { try (JsonWriter writer = new JsonWriter()) { writer.beginObject(); writer.name("attributes").beginArray(); visitResourceAttributes( - config, - extraAttributes, - (key, value) -> writeAttribute(writer, STRING_ATTRIBUTE, key, value), - (key, value) -> writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, value)); + config, extraAttributes, (key, value) -> writeResourceAttribute(writer, key, value)); writer.endArray(); writer.endObject(); return writer.toString(); } } + + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. + */ + @SuppressWarnings("unchecked") + private static void writeResourceAttribute(JsonWriter writer, String key, Object value) { + if (value instanceof List) { + writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, (List) value); + } else { + writeAttribute(writer, STRING_ATTRIBUTE, key, value); + } + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index cf1a06a39af..a28a87d2943 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -5,21 +5,24 @@ import static datadog.trace.core.otlp.common.OtlpCommonProto.LEN_WIRE_TYPE; import static datadog.trace.core.otlp.common.OtlpCommonProto.writeAttribute; import static datadog.trace.core.otlp.common.OtlpCommonProto.writeTag; -import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.visitResourceAttributes; import datadog.communication.serialization.GrowableBuffer; +import datadog.communication.serialization.StreamingBuffer; import datadog.trace.api.Config; -import datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** Provides a canned message for OpenTelemetry's "resource.proto" wire protocol. */ public final class OtlpResourceProto { private OtlpResourceProto() {} /** Vendor-neutral resource (no {@code datadog.*}). Used by the OTLP metric export. */ - public static final byte[] RESOURCE_MESSAGE = buildResourceMessage(Config.get(), EMPTY); + public static final byte[] RESOURCE_MESSAGE = + buildResourceMessage(Config.get(), Collections.emptyMap()); /** * Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed @@ -37,20 +40,11 @@ private OtlpResourceProto() {} public static final byte[] TRACE_RESOURCE_MESSAGE = buildResourceMessage(Config.get(), traceResourceAttributes(Config.get())); - static byte[] buildResourceMessage(Config config, ExtraAttributes extraAttributes) { + static byte[] buildResourceMessage(Config config, Map extraAttributes) { GrowableBuffer buf = new GrowableBuffer(512); visitResourceAttributes( - config, - extraAttributes, - (key, value) -> { - writeTag(buf, 1, LEN_WIRE_TYPE); - writeAttribute(buf, STRING_ATTRIBUTE, key, value); - }, - (key, value) -> { - writeTag(buf, 1, LEN_WIRE_TYPE); - writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, value); - }); + config, extraAttributes, (key, value) -> writeResourceAttribute(buf, key, value)); OtlpProtoBuffer protobuf = new OtlpProtoBuffer(buf.capacity()); int numBytes = protobuf.recordMessage(buf, 1); @@ -59,4 +53,17 @@ static byte[] buildResourceMessage(Config config, ExtraAttributes extraAttribute return resourceMessage; } + + /** + * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. + */ + @SuppressWarnings("unchecked") + private static void writeResourceAttribute(StreamingBuffer buf, String key, Object value) { + writeTag(buf, 1, LEN_WIRE_TYPE); + if (value instanceof List) { + writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, (List) value); + } else { + writeAttribute(buf, STRING_ATTRIBUTE, key, value); + } + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java index 9d09fa1e61e..1b9908676e2 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java @@ -8,7 +8,6 @@ import static datadog.trace.api.config.GeneralConfig.VERSION; import static datadog.trace.api.config.OtlpConfig.OTEL_TRACES_SPAN_METRICS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_REPORT_HOSTNAME; -import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -20,6 +19,7 @@ import datadog.trace.api.ProcessTags; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -141,7 +141,7 @@ void testBuildResourceFragment( String caseName, Properties properties, Map expectedAttributes) throws IOException { Config config = Config.get(properties); - String fragment = OtlpResourceJson.buildResourceFragment(config, EMPTY); + String fragment = OtlpResourceJson.buildResourceFragment(config, Collections.emptyMap()); Map actualAttributes = parseResourceAttributes(fragment); assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName); @@ -156,7 +156,8 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { parseResourceAttributes( OtlpResourceJson.buildResourceFragment(config, datadogResourceAttributes(config))); Map plain = - parseResourceAttributes(OtlpResourceJson.buildResourceFragment(config, EMPTY)); + parseResourceAttributes( + OtlpResourceJson.buildResourceFragment(config, Collections.emptyMap())); assertTrue( withDatadog.containsKey("datadog.runtime_id"), diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java index 7e7bef0d6a5..659c2b88786 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java @@ -8,7 +8,6 @@ import static datadog.trace.api.config.GeneralConfig.VERSION; import static datadog.trace.api.config.OtlpConfig.OTEL_TRACES_SPAN_METRICS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_REPORT_HOSTNAME; -import static datadog.trace.core.otlp.common.OtlpResourceAttributes.ExtraAttributes.EMPTY; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.datadogResourceAttributes; import static datadog.trace.core.otlp.common.OtlpResourceAttributes.traceResourceAttributes; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -21,6 +20,7 @@ import datadog.trace.api.ProcessTags; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -163,7 +163,7 @@ void testBuildResourceMessage( String caseName, Properties properties, Map expectedAttributes) throws IOException { Config config = Config.get(properties); - byte[] bytes = OtlpResourceProto.buildResourceMessage(config, EMPTY); + byte[] bytes = OtlpResourceProto.buildResourceMessage(config, Collections.emptyMap()); Map actualAttributes = parseResourceAttributes(bytes); assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName); @@ -181,7 +181,8 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { parseResourceAttributes( OtlpResourceProto.buildResourceMessage(config, datadogResourceAttributes(config))); Map plain = - parseResourceAttributes(OtlpResourceProto.buildResourceMessage(config, EMPTY)); + parseResourceAttributes( + OtlpResourceProto.buildResourceMessage(config, Collections.emptyMap())); assertTrue( withDatadog.containsKey("datadog.runtime_id"), From 4c07a192595e34711339851f587bc9a39c9870e3 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 16:05:37 -0400 Subject: [PATCH 08/15] Remove local OTLP resource attribute casts --- .../java/datadog/trace/core/otlp/common/OtlpResourceJson.java | 3 +-- .../java/datadog/trace/core/otlp/common/OtlpResourceProto.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index d400bcc030d..187851326d2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -54,10 +54,9 @@ static String buildResourceFragment(Config config, Map extraAttr /** * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ - @SuppressWarnings("unchecked") private static void writeResourceAttribute(JsonWriter writer, String key, Object value) { if (value instanceof List) { - writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, (List) value); + writeAttribute(writer, STRING_ARRAY_ATTRIBUTE, key, value); } else { writeAttribute(writer, STRING_ATTRIBUTE, key, value); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index a28a87d2943..3d10e95454e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -57,11 +57,10 @@ static byte[] buildResourceMessage(Config config, Map extraAttri /** * {@code value} is a {@link String}, except {@code datadog.process_tags}: a {@code List}. */ - @SuppressWarnings("unchecked") private static void writeResourceAttribute(StreamingBuffer buf, String key, Object value) { writeTag(buf, 1, LEN_WIRE_TYPE); if (value instanceof List) { - writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, (List) value); + writeAttribute(buf, STRING_ARRAY_ATTRIBUTE, key, value); } else { writeAttribute(buf, STRING_ATTRIBUTE, key, value); } From 207ab6df21b75dd99a53b30463c5968136c6e79b Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 18:50:42 -0400 Subject: [PATCH 09/15] Avoid duplicate OTLP resource attributes --- .../core/otlp/common/OtlpResourceAttributes.java | 5 +++-- .../core/otlp/common/OtlpResourceJsonTest.java | 14 +++++++++++--- .../core/otlp/common/OtlpResourceProtoTest.java | 11 +++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java index 227c144b1b8..988307c6b03 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceAttributes.java @@ -66,8 +66,9 @@ static void visitResourceAttributes( .getGlobalTags() .forEach( (key, value) -> { - // ignore datadog tags and their otel equivalents that we map above - if (!IGNORED_GLOBAL_TAGS.contains(key.toLowerCase(Locale.ROOT))) { + // ignore global tags replaced by canonical or extra resource attributes + if (!IGNORED_GLOBAL_TAGS.contains(key.toLowerCase(Locale.ROOT)) + && !extraAttributes.containsKey(key)) { visitor.accept(key, value); } }); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java index 1b9908676e2..e73311993cf 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceJsonTest.java @@ -170,10 +170,16 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { } @Test - void datadogResourceAttributesVariantCarriesProcessTagsAsOneArrayValue() throws IOException { + void datadogResourceAttributesOverrideCollidingGlobalProcessTag() throws IOException { Config config = Config.get( - props(SERVICE_NAME, "my-service", EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true")); + props( + SERVICE_NAME, + "my-service", + TAGS, + "datadog.process_tags:user-value", + EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, + "true")); ProcessTags.reset(config); ProcessTags.addTag("entrypoint.name", "app"); ProcessTags.addTag("entrypoint.type", "web"); @@ -231,7 +237,9 @@ private static Map parseResourceAttributes(String fragment) thro for (Object attribute : attributes) { Map keyValue = (Map) attribute; Map value = (Map) keyValue.get("value"); - result.put((String) keyValue.get("key"), readAnyValue(value)); + String key = (String) keyValue.get("key"); + assertFalse(result.containsKey(key), "duplicate resource attribute key: " + key); + result.put(key, readAnyValue(value)); } return result; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java index 659c2b88786..24dda2ac60a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/common/OtlpResourceProtoTest.java @@ -195,10 +195,16 @@ void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException { } @Test - void datadogResourceAttributesVariantCarriesProcessTagsAsOneArrayValue() throws IOException { + void datadogResourceAttributesOverrideCollidingGlobalProcessTag() throws IOException { Config config = Config.get( - props(SERVICE_NAME, "my-service", EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true")); + props( + SERVICE_NAME, + "my-service", + TAGS, + "datadog.process_tags:user-value", + EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, + "true")); ProcessTags.reset(config); ProcessTags.addTag("entrypoint.name", "app"); ProcessTags.addTag("entrypoint.type", "web"); @@ -267,6 +273,7 @@ private static Map parseResourceAttributes(byte[] bytes) throws Object value = readAnyValueBody(av); assertTrue(kv.isAtEnd(), "no extra fields in KeyValue"); + assertFalse(attributes.containsKey(key), "duplicate resource attribute key: " + key); attributes.put(key, value); } return attributes; From 44db0a41cda3093b203a4d708fee06d8a599ae6e Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 19:27:21 -0400 Subject: [PATCH 10/15] Restrict OTLP semantic trace-metrics attributes --- .../otlp/metrics/OtlpStatsMetricWriter.java | 20 +++++++------- .../metrics/OtlpStatsMetricWriterTest.java | 27 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index 5b9c688880f..1f71ef4497b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -213,6 +213,9 @@ private void emitDataPointAttributes( emitStringAttribute(metric, SPAN_NAME, entry.getResource()); emitStringAttribute(metric, SPAN_KIND, canonicalSpanKind(entry.getSpanKind())); emitStringAttribute(metric, SERVICE_NAME, entry.getService()); + if (otelSemanticsMode) { + return; + } if (entry.hasHttpMethod()) { emitStringAttribute(metric, HTTP_REQUEST_METHOD, entry.getHttpMethod()); } @@ -229,17 +232,14 @@ private void emitDataPointAttributes( for (UTF8BytesString additionalTag : entry.getAdditionalTags()) { emitAdditionalTag(metric, additionalTag); } - // Default (Datadog) mode: emit datadog.* per-point attributes - if (!otelSemanticsMode) { - emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); - emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); - emitLongAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel ? 1L : 0L); - emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); - if (entry.isSynthetics()) { - emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); - } - emitPeerTags(metric, entry.getPeerTags()); + emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); + emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); + emitLongAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel ? 1L : 0L); + emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); + if (entry.isSynthetics()) { + emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); } + emitPeerTags(metric, entry.getPeerTags()); } private static void emitPeerTags(OtlpMetricVisitor metric, List peerTags) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index af6a50ab43e..5b3fd0fcd29 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -486,9 +487,7 @@ void additionalMetricTagsEmittedAsStringAttributes() throws IOException { } @Test - void additionalMetricTagsEmittedInOtelSemanticsMode() throws IOException { - // Unlike datadog.* attributes, additional tags are user-configured dimensions and are emitted - // in otel-semantics mode too. + void otelSemanticsModeEmitsOnlyDefaultConnectorAttributes() throws IOException { AggregateEntry e = AggregateEntryTestUtils.of( "GET /users", @@ -496,21 +495,25 @@ void additionalMetricTagsEmittedInOtelSemanticsMode() throws IOException { "servlet.request", null, "web", - 0, - false, + 200, + true, true, "server", - null, - null, - null, - null, + Arrays.asList(UTF8BytesString.create("peer.service:downstream")), + "GET", + "/users/{id}", + "0", new UTF8BytesString[] {UTF8BytesString.create("region:us-east-1")}); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); Map attrs = writeAndDecode(true, e).dataPoints.get(0).attributes; - assertEquals("us-east-1", attrs.get("region")); - assertFalse( - attrs.containsKey("datadog.operation.name"), "datadog.* still absent in otel-semantics"); + assertEquals( + new HashSet<>(Arrays.asList("service.name", "span.name", "span.kind", "status.code")), + attrs.keySet()); + assertEquals("web", attrs.get("service.name")); + assertEquals("GET /users", attrs.get("span.name")); + assertEquals("SPAN_KIND_SERVER", attrs.get("span.kind")); + assertEquals("STATUS_CODE_OK", attrs.get("status.code")); } @Test From 35657c08b9613de6353fa122840b975ece072b3a Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Wed, 5 Aug 2026 21:51:13 -0400 Subject: [PATCH 11/15] Restore OTLP trace-metrics attribute semantics --- .../otlp/metrics/OtlpStatsMetricWriter.java | 35 +++++++----- .../metrics/OtlpStatsMetricWriterTest.java | 57 +++++++++++++++---- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index 1f71ef4497b..765713f2ade 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -54,6 +54,8 @@ public final class OtlpStatsMetricWriter implements MetricWriter { private static final String STATUS_CODE = "status.code"; private static final String STATUS_CODE_OK = "STATUS_CODE_OK"; private static final String STATUS_CODE_ERROR = "STATUS_CODE_ERROR"; + private static final String DATADOG_ATTRIBUTE_PREFIX = "datadog."; + private static final String INTERNAL_DATADOG_ATTRIBUTE_PREFIX = "_datadog."; private static final String DATADOG_OPERATION_NAME = "datadog.operation.name"; private static final String DATADOG_SPAN_TYPE = "datadog.span.type"; private static final String DATADOG_SPAN_TOP_LEVEL = "datadog.span.top_level"; @@ -213,9 +215,6 @@ private void emitDataPointAttributes( emitStringAttribute(metric, SPAN_NAME, entry.getResource()); emitStringAttribute(metric, SPAN_KIND, canonicalSpanKind(entry.getSpanKind())); emitStringAttribute(metric, SERVICE_NAME, entry.getService()); - if (otelSemanticsMode) { - return; - } if (entry.hasHttpMethod()) { emitStringAttribute(metric, HTTP_REQUEST_METHOD, entry.getHttpMethod()); } @@ -230,16 +229,18 @@ private void emitDataPointAttributes( } // additional_metric_tags support is still evolving/TBD across most tracer SDKs. for (UTF8BytesString additionalTag : entry.getAdditionalTags()) { - emitAdditionalTag(metric, additionalTag); + emitAdditionalTag(metric, additionalTag, otelSemanticsMode); } - emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); - emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); - emitLongAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel ? 1L : 0L); - emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); - if (entry.isSynthetics()) { - emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); + if (!otelSemanticsMode) { + emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); + emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); + emitBooleanAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel); + emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); + if (entry.isSynthetics()) { + emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); + } + emitPeerTags(metric, entry.getPeerTags()); } - emitPeerTags(metric, entry.getPeerTags()); } private static void emitPeerTags(OtlpMetricVisitor metric, List peerTags) { @@ -267,14 +268,20 @@ private static String canonicalSpanKind(CharSequence spanKind) { } } - private static void emitAdditionalTag(OtlpMetricVisitor metric, UTF8BytesString additionalTag) { + private static void emitAdditionalTag( + OtlpMetricVisitor metric, UTF8BytesString additionalTag, boolean suppressDatadogAttributes) { String packed = additionalTag.toString(); int separator = packed.indexOf(':'); if (separator <= 0) { return; } - metric.visitAttribute( - STRING_ATTRIBUTE, packed.substring(0, separator), packed.substring(separator + 1)); + String key = packed.substring(0, separator); + if (suppressDatadogAttributes + && (key.startsWith(DATADOG_ATTRIBUTE_PREFIX) + || key.startsWith(INTERNAL_DATADOG_ATTRIBUTE_PREFIX))) { + return; + } + metric.visitAttribute(STRING_ATTRIBUTE, key, packed.substring(separator + 1)); } private static void emitStringAttribute( diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index 5b3fd0fcd29..acc4c29de42 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -476,7 +476,9 @@ void additionalMetricTagsEmittedAsStringAttributes() throws IOException { null, new UTF8BytesString[] { UTF8BytesString.create("region:us-east-1"), - UTF8BytesString.create("tenant_id:acme:corp") + UTF8BytesString.create("tenant_id:acme:corp"), + UTF8BytesString.create("datadog.custom:visible"), + UTF8BytesString.create("_datadog.custom:visible") }); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); @@ -484,10 +486,12 @@ void additionalMetricTagsEmittedAsStringAttributes() throws IOException { assertEquals("us-east-1", attrs.get("region")); // value may itself contain ':' — only the first ':' separates key from value assertEquals("acme:corp", attrs.get("tenant_id")); + assertEquals("visible", attrs.get("datadog.custom")); + assertEquals("visible", attrs.get("_datadog.custom")); } @Test - void otelSemanticsModeEmitsOnlyDefaultConnectorAttributes() throws IOException { + void otelSemanticsModeEmitsAllNonDatadogAttributes() throws IOException { AggregateEntry e = AggregateEntryTestUtils.of( "GET /users", @@ -503,17 +507,42 @@ void otelSemanticsModeEmitsOnlyDefaultConnectorAttributes() throws IOException { "GET", "/users/{id}", "0", - new UTF8BytesString[] {UTF8BytesString.create("region:us-east-1")}); + new UTF8BytesString[] { + UTF8BytesString.create("region:us-east-1"), + UTF8BytesString.create("custom.tag:custom-value"), + UTF8BytesString.create("datadog.custom:hidden"), + UTF8BytesString.create("_datadog.custom:hidden") + }); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); Map attrs = writeAndDecode(true, e).dataPoints.get(0).attributes; assertEquals( - new HashSet<>(Arrays.asList("service.name", "span.name", "span.kind", "status.code")), + new HashSet<>( + Arrays.asList( + "service.name", + "span.name", + "span.kind", + "status.code", + "http.request.method", + "http.response.status_code", + "http.route", + "rpc.response.status_code", + "region", + "custom.tag")), attrs.keySet()); assertEquals("web", attrs.get("service.name")); assertEquals("GET /users", attrs.get("span.name")); assertEquals("SPAN_KIND_SERVER", attrs.get("span.kind")); assertEquals("STATUS_CODE_OK", attrs.get("status.code")); + assertEquals("GET", attrs.get("http.request.method")); + assertEquals(200L, attrs.get("http.response.status_code")); + assertEquals("/users/{id}", attrs.get("http.route")); + assertEquals("0", attrs.get("rpc.response.status_code")); + assertEquals("us-east-1", attrs.get("region")); + assertEquals("custom-value", attrs.get("custom.tag")); + assertFalse( + attrs.keySet().stream() + .anyMatch(key -> key.startsWith("datadog.") || key.startsWith("_datadog."))); } @Test @@ -631,11 +660,15 @@ void nullSenderDoesNotThrowOnNonEmptyBucket() { } } - @Test - void defaultModeCarriesDatadogAttributes() throws IOException { - // use an entry where all hits are top-level: OR in TOP_LEVEL_TAG + @ParameterizedTest + @CsvSource({"true", "false"}) + void defaultModeCarriesDatadogAttributes(boolean topLevel) throws IOException { AggregateEntry e = entry("servlet.request", false, 0, null, null, null); - AggregateEntryTestUtils.recordTopLevel(e, SECONDS.toNanos(1)); + if (topLevel) { + AggregateEntryTestUtils.recordTopLevel(e, SECONDS.toNanos(1)); + } else { + AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); + } Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; assertTrue( @@ -643,7 +676,8 @@ void defaultModeCarriesDatadogAttributes() throws IOException { assertTrue(attrs.containsKey("datadog.span.type"), "span type present in default mode"); assertTrue( attrs.containsKey("datadog.span.top_level"), "span top-level present in default mode"); - assertEquals(1L, attrs.get("datadog.span.top_level"), "all hits top-level → 1"); + assertTrue(attrs.get("datadog.span.top_level") instanceof Boolean); + assertEquals(topLevel, attrs.get("datadog.span.top_level")); // OTel-semconv attrs are present in both modes assertTrue(attrs.containsKey("span.name"), "span.name present in both modes"); // datadog.origin presence/absence is covered by defaultModeEmitsSyntheticOrigin @@ -670,6 +704,7 @@ void defaultModeEmitsIsTraceRoot(boolean traceRoot) throws IOException { AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + assertTrue(attrs.get("datadog.is_trace_root") instanceof Boolean); assertEquals(traceRoot, attrs.get("datadog.is_trace_root")); } @@ -814,7 +849,9 @@ void snapshotsEntryDataBeforeAggregatorClearsIt() throws IOException { DataPoint dp = metric.dataPoints.get(0); assertEquals(3L, dp.count, "count must reflect the pre-clear snapshot, not the cleared entry"); assertEquals( - 1L, dp.attributes.get("datadog.span.top_level"), "all pre-clear hits were top-level"); + Boolean.TRUE, + dp.attributes.get("datadog.span.top_level"), + "all pre-clear hits were top-level"); } // ── resource attributes (datadog.runtime_id / process tags) ──────────────── From ed9c08025d3b50447fe57157df7d7675d7bbeaf2 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 6 Aug 2026 09:23:54 -0400 Subject: [PATCH 12/15] Fix OTLP Datadog attribute filtering --- .../core/otlp/metrics/OtlpStatsMetricWriter.java | 5 +---- .../core/otlp/metrics/OtlpStatsMetricWriterTest.java | 11 +++-------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index 765713f2ade..932a43e1a97 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -55,7 +55,6 @@ public final class OtlpStatsMetricWriter implements MetricWriter { private static final String STATUS_CODE_OK = "STATUS_CODE_OK"; private static final String STATUS_CODE_ERROR = "STATUS_CODE_ERROR"; private static final String DATADOG_ATTRIBUTE_PREFIX = "datadog."; - private static final String INTERNAL_DATADOG_ATTRIBUTE_PREFIX = "_datadog."; private static final String DATADOG_OPERATION_NAME = "datadog.operation.name"; private static final String DATADOG_SPAN_TYPE = "datadog.span.type"; private static final String DATADOG_SPAN_TOP_LEVEL = "datadog.span.top_level"; @@ -276,9 +275,7 @@ private static void emitAdditionalTag( return; } String key = packed.substring(0, separator); - if (suppressDatadogAttributes - && (key.startsWith(DATADOG_ATTRIBUTE_PREFIX) - || key.startsWith(INTERNAL_DATADOG_ATTRIBUTE_PREFIX))) { + if (suppressDatadogAttributes && key.startsWith(DATADOG_ATTRIBUTE_PREFIX)) { return; } metric.visitAttribute(STRING_ATTRIBUTE, key, packed.substring(separator + 1)); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index acc4c29de42..e7df3f40974 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -477,8 +477,7 @@ void additionalMetricTagsEmittedAsStringAttributes() throws IOException { new UTF8BytesString[] { UTF8BytesString.create("region:us-east-1"), UTF8BytesString.create("tenant_id:acme:corp"), - UTF8BytesString.create("datadog.custom:visible"), - UTF8BytesString.create("_datadog.custom:visible") + UTF8BytesString.create("datadog.custom:visible") }); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); @@ -487,7 +486,6 @@ void additionalMetricTagsEmittedAsStringAttributes() throws IOException { // value may itself contain ':' — only the first ':' separates key from value assertEquals("acme:corp", attrs.get("tenant_id")); assertEquals("visible", attrs.get("datadog.custom")); - assertEquals("visible", attrs.get("_datadog.custom")); } @Test @@ -510,8 +508,7 @@ void otelSemanticsModeEmitsAllNonDatadogAttributes() throws IOException { new UTF8BytesString[] { UTF8BytesString.create("region:us-east-1"), UTF8BytesString.create("custom.tag:custom-value"), - UTF8BytesString.create("datadog.custom:hidden"), - UTF8BytesString.create("_datadog.custom:hidden") + UTF8BytesString.create("datadog.custom:hidden") }); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); @@ -540,9 +537,7 @@ void otelSemanticsModeEmitsAllNonDatadogAttributes() throws IOException { assertEquals("0", attrs.get("rpc.response.status_code")); assertEquals("us-east-1", attrs.get("region")); assertEquals("custom-value", attrs.get("custom.tag")); - assertFalse( - attrs.keySet().stream() - .anyMatch(key -> key.startsWith("datadog.") || key.startsWith("_datadog."))); + assertFalse(attrs.keySet().stream().anyMatch(key -> key.startsWith("datadog."))); } @Test From bd53c6ef62361b3df0d51a2ab6496c761b7ab76c Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 6 Aug 2026 14:41:21 -0400 Subject: [PATCH 13/15] Make OTLP trace metrics mode-independent Always emit available Datadog attributes alongside OTel attributes and include datadog.svc_src when the aggregation key carries a service source. --- .../otlp/metrics/OtlpStatsMetricWriter.java | 56 ++--- .../metrics/OtlpStatsMetricWriterTest.java | 195 ++++++------------ 2 files changed, 78 insertions(+), 173 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index 932a43e1a97..d2c637aac1b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -54,11 +54,11 @@ public final class OtlpStatsMetricWriter implements MetricWriter { private static final String STATUS_CODE = "status.code"; private static final String STATUS_CODE_OK = "STATUS_CODE_OK"; private static final String STATUS_CODE_ERROR = "STATUS_CODE_ERROR"; - private static final String DATADOG_ATTRIBUTE_PREFIX = "datadog."; private static final String DATADOG_OPERATION_NAME = "datadog.operation.name"; private static final String DATADOG_SPAN_TYPE = "datadog.span.type"; private static final String DATADOG_SPAN_TOP_LEVEL = "datadog.span.top_level"; private static final String DATADOG_IS_TRACE_ROOT = "datadog.is_trace_root"; + private static final String DATADOG_SERVICE_SOURCE = "datadog.svc_src"; private static final String DATADOG_ORIGIN = "datadog.origin"; private static final String DATADOG_PEER_TAGS = "datadog.peer_tags"; private static final String SYNTHETICS_ORIGIN = "synthetics"; @@ -70,7 +70,6 @@ public final class OtlpStatsMetricWriter implements MetricWriter { private static final String SPAN_KIND_INTERNAL = "SPAN_KIND_INTERNAL"; @Nullable private final OtlpSender sender; - private final boolean otelSemanticsMode; // own single-thread collector; forced to DELTA since trace-stats buckets are per-interval deltas. private final OtlpMetricsCollector collector; @@ -98,38 +97,26 @@ private static final class PendingPoint { public OtlpStatsMetricWriter(Config config) { // shared protocol-based sender selection so both OTLP metrics export paths agree - this( - OtlpMetricsSenderFactory.create(config), - config.getOtlpMetricsProtocol(), - config.isTraceOtelSemanticsEnabled()); + this(OtlpMetricsSenderFactory.create(config), config.getOtlpMetricsProtocol()); } - // visible for testing: lets tests inject a capturing sender to decode the emitted payload and - // control the semantics mode - OtlpStatsMetricWriter(@Nullable OtlpSender sender, boolean otelSemanticsMode) { - this(sender, OtlpConfig.Protocol.HTTP_PROTOBUF, otelSemanticsMode); + // visible for testing: lets tests inject a capturing sender to decode the emitted payload + OtlpStatsMetricWriter(@Nullable OtlpSender sender) { + this(sender, OtlpConfig.Protocol.HTTP_PROTOBUF); } - private OtlpStatsMetricWriter( - @Nullable OtlpSender sender, OtlpConfig.Protocol protocol, boolean otelSemanticsMode) { + private OtlpStatsMetricWriter(@Nullable OtlpSender sender, OtlpConfig.Protocol protocol) { this.sender = sender; - this.otelSemanticsMode = otelSemanticsMode; - // Default mode carries datadog.runtime_id / process tags on the Resource; OTel-semantics mode - // uses the plain vendor-neutral resource (no datadog.*). this.collector = protocol == OtlpConfig.Protocol.HTTP_JSON ? new OtlpMetricsJsonCollector( SystemTimeSource.INSTANCE, true, - otelSemanticsMode - ? OtlpResourceJson.RESOURCE_FRAGMENT - : OtlpResourceJson.RESOURCE_FRAGMENT_WITH_DATADOG_ATTRS) + OtlpResourceJson.RESOURCE_FRAGMENT_WITH_DATADOG_ATTRS) : new OtlpMetricsProtoCollector( SystemTimeSource.INSTANCE, true, - otelSemanticsMode - ? OtlpResourceProto.RESOURCE_MESSAGE - : OtlpResourceProto.RESOURCE_MESSAGE_WITH_DATADOG_ATTRS); + OtlpResourceProto.RESOURCE_MESSAGE_WITH_DATADOG_ATTRS); } @Override @@ -228,18 +215,19 @@ private void emitDataPointAttributes( } // additional_metric_tags support is still evolving/TBD across most tracer SDKs. for (UTF8BytesString additionalTag : entry.getAdditionalTags()) { - emitAdditionalTag(metric, additionalTag, otelSemanticsMode); + emitAdditionalTag(metric, additionalTag); } - if (!otelSemanticsMode) { - emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); - emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); - emitBooleanAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel); - emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); - if (entry.isSynthetics()) { - emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); - } - emitPeerTags(metric, entry.getPeerTags()); + emitStringAttribute(metric, DATADOG_OPERATION_NAME, entry.getOperationName()); + emitStringAttribute(metric, DATADOG_SPAN_TYPE, entry.getType()); + emitBooleanAttribute(metric, DATADOG_SPAN_TOP_LEVEL, allTopLevel); + emitBooleanAttribute(metric, DATADOG_IS_TRACE_ROOT, entry.isTraceRoot()); + if (entry.hasServiceSource()) { + emitStringAttribute(metric, DATADOG_SERVICE_SOURCE, entry.getServiceSource()); + } + if (entry.isSynthetics()) { + emitStringAttribute(metric, DATADOG_ORIGIN, SYNTHETICS_ORIGIN); } + emitPeerTags(metric, entry.getPeerTags()); } private static void emitPeerTags(OtlpMetricVisitor metric, List peerTags) { @@ -267,17 +255,13 @@ private static String canonicalSpanKind(CharSequence spanKind) { } } - private static void emitAdditionalTag( - OtlpMetricVisitor metric, UTF8BytesString additionalTag, boolean suppressDatadogAttributes) { + private static void emitAdditionalTag(OtlpMetricVisitor metric, UTF8BytesString additionalTag) { String packed = additionalTag.toString(); int separator = packed.indexOf(':'); if (separator <= 0) { return; } String key = packed.substring(0, separator); - if (suppressDatadogAttributes && key.startsWith(DATADOG_ATTRIBUTE_PREFIX)) { - return; - } metric.visitAttribute(STRING_ATTRIBUTE, key, packed.substring(separator + 1)); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index e7df3f40974..b063e7ab5bb 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -336,10 +335,9 @@ private static List readArrayValue(CodedInputStream arrayValue) throws I * entry} over the fixed {@link #BUCKET_START}/{@link #BUCKET_DURATION} window, asserts that * exactly one payload was sent, and returns the decoded metric. */ - private static DecodedMetric writeAndDecode(boolean otelSemanticsMode, AggregateEntry entry) - throws IOException { + private static DecodedMetric writeAndDecode(AggregateEntry entry) throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, otelSemanticsMode); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender); writer.startBucket(1, BUCKET_START, BUCKET_DURATION); writer.add(entry); writer.finishBucket(); @@ -351,7 +349,7 @@ private static DecodedMetric writeAndDecode(boolean otelSemanticsMode, Aggregate @Test void okOnlyEntryProducesExactlyOneDataPoint() throws IOException { - DecodedMetric metric = writeAndDecode(false, okEntry(SECONDS.toNanos(1), 3)); + DecodedMetric metric = writeAndDecode(okEntry(SECONDS.toNanos(1), 3)); assertEquals("traces.span.sdk.metrics.duration", metric.name); assertEquals("s", metric.unit); @@ -372,7 +370,7 @@ void okPlusErrorEntryProducesTwoDataPointsWithErrorStatus() throws IOException { AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(2)); // ok AggregateEntryTestUtils.recordError(e, SECONDS.toNanos(3)); // error - DecodedMetric metric = writeAndDecode(false, e); + DecodedMetric metric = writeAndDecode(e); assertEquals(2, metric.dataPoints.size(), "ok+error → two data points"); long okCount = 0; @@ -398,7 +396,7 @@ void okPlusErrorEntryProducesTwoDataPointsWithErrorStatus() throws IOException { @Test void errorSeriesDoesNotLingerAfterClearWhenBucketHasOnlyOkHits() throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender); // Bucket 1: the entry sees an error, so its error histogram is allocated and emits a point. AggregateEntry e = entry("GET /users", false, 0, null, null, null); @@ -436,7 +434,7 @@ void httpAndGrpcAttributesAppearOnlyWhenSet() throws IOException { AggregateEntry e = entry("GET /users/{id}", false, 200, "GET", "/users/{id}", "0"); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - DecodedMetric metric = writeAndDecode(false, e); + DecodedMetric metric = writeAndDecode(e); assertEquals(1, metric.dataPoints.size()); Map attrs = metric.dataPoints.get(0).attributes; @@ -447,7 +445,7 @@ void httpAndGrpcAttributesAppearOnlyWhenSet() throws IOException { // a bare entry has none of these Map bareAttrs = - writeAndDecode(false, okEntry(SECONDS.toNanos(1), 1)).dataPoints.get(0).attributes; + writeAndDecode(okEntry(SECONDS.toNanos(1), 1)).dataPoints.get(0).attributes; assertFalse(bareAttrs.containsKey("http.request.method")); assertFalse(bareAttrs.containsKey("http.response.status_code")); assertFalse(bareAttrs.containsKey("http.route")); @@ -458,7 +456,7 @@ void httpAndGrpcAttributesAppearOnlyWhenSet() throws IOException { void additionalMetricTagsEmittedAsStringAttributes() throws IOException { // Additional tags arrive on the entry pre-packed as "key:value" UTF8 strings in schema order; // the writer splits each at the first ':' and emits it as a plain OTLP string attribute keyed - // by the tag name, in both semantics modes. + // by the tag name. AggregateEntry e = AggregateEntryTestUtils.of( "GET /users", @@ -481,65 +479,13 @@ void additionalMetricTagsEmittedAsStringAttributes() throws IOException { }); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; assertEquals("us-east-1", attrs.get("region")); // value may itself contain ':' — only the first ':' separates key from value assertEquals("acme:corp", attrs.get("tenant_id")); assertEquals("visible", attrs.get("datadog.custom")); } - @Test - void otelSemanticsModeEmitsAllNonDatadogAttributes() throws IOException { - AggregateEntry e = - AggregateEntryTestUtils.of( - "GET /users", - "web", - "servlet.request", - null, - "web", - 200, - true, - true, - "server", - Arrays.asList(UTF8BytesString.create("peer.service:downstream")), - "GET", - "/users/{id}", - "0", - new UTF8BytesString[] { - UTF8BytesString.create("region:us-east-1"), - UTF8BytesString.create("custom.tag:custom-value"), - UTF8BytesString.create("datadog.custom:hidden") - }); - AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - - Map attrs = writeAndDecode(true, e).dataPoints.get(0).attributes; - assertEquals( - new HashSet<>( - Arrays.asList( - "service.name", - "span.name", - "span.kind", - "status.code", - "http.request.method", - "http.response.status_code", - "http.route", - "rpc.response.status_code", - "region", - "custom.tag")), - attrs.keySet()); - assertEquals("web", attrs.get("service.name")); - assertEquals("GET /users", attrs.get("span.name")); - assertEquals("SPAN_KIND_SERVER", attrs.get("span.kind")); - assertEquals("STATUS_CODE_OK", attrs.get("status.code")); - assertEquals("GET", attrs.get("http.request.method")); - assertEquals(200L, attrs.get("http.response.status_code")); - assertEquals("/users/{id}", attrs.get("http.route")); - assertEquals("0", attrs.get("rpc.response.status_code")); - assertEquals("us-east-1", attrs.get("region")); - assertEquals("custom-value", attrs.get("custom.tag")); - assertFalse(attrs.keySet().stream().anyMatch(key -> key.startsWith("datadog."))); - } - @Test void emptyValueEmittedButMalformedSlotsSkipped() throws IOException { // A slot with no ':' or an empty key is dropped as malformed. An empty value ("key:") is NOT @@ -569,7 +515,7 @@ void emptyValueEmittedButMalformedSlotsSkipped() throws IOException { }); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; assertEquals("us-east-1", attrs.get("region"), "well-formed tag still emitted"); assertFalse(attrs.containsKey("noseparator"), "no-separator slot skipped"); assertFalse(attrs.containsKey(""), "empty-key slot skipped"); @@ -580,7 +526,7 @@ void emptyValueEmittedButMalformedSlotsSkipped() throws IOException { @Test void serviceNameAlwaysEmittedOnDataPoint() throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender); long start = SECONDS.toNanos(1_700_000_000L); writer.startBucket(2, start, SECONDS.toNanos(10)); @@ -633,7 +579,7 @@ private static AggregateEntry serviceEntry(String operationName, String service) @Test void emptyBucketSendsNothing() { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender); writer.startBucket(0, BUCKET_START, BUCKET_DURATION); writer.finishBucket(); // no add() @@ -645,7 +591,7 @@ void emptyBucketSendsNothing() { @Test void nullSenderDoesNotThrowOnNonEmptyBucket() { // mirrors the HTTP_JSON path where createSender(config) returns null. - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(null, false); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter((OtlpSender) null); writer.startBucket(1, BUCKET_START, BUCKET_DURATION); writer.add(okEntry(SECONDS.toNanos(1), 2)); try { @@ -657,7 +603,7 @@ void nullSenderDoesNotThrowOnNonEmptyBucket() { @ParameterizedTest @CsvSource({"true", "false"}) - void defaultModeCarriesDatadogAttributes(boolean topLevel) throws IOException { + void carriesDatadogAttributes(boolean topLevel) throws IOException { AggregateEntry e = entry("servlet.request", false, 0, null, null, null); if (topLevel) { AggregateEntryTestUtils.recordTopLevel(e, SECONDS.toNanos(1)); @@ -665,22 +611,18 @@ void defaultModeCarriesDatadogAttributes(boolean topLevel) throws IOException { AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); } - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; - assertTrue( - attrs.containsKey("datadog.operation.name"), "operation name present in default mode"); - assertTrue(attrs.containsKey("datadog.span.type"), "span type present in default mode"); - assertTrue( - attrs.containsKey("datadog.span.top_level"), "span top-level present in default mode"); + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; + assertTrue(attrs.containsKey("datadog.operation.name")); + assertTrue(attrs.containsKey("datadog.span.type")); + assertTrue(attrs.containsKey("datadog.span.top_level")); assertTrue(attrs.get("datadog.span.top_level") instanceof Boolean); assertEquals(topLevel, attrs.get("datadog.span.top_level")); - // OTel-semconv attrs are present in both modes - assertTrue(attrs.containsKey("span.name"), "span.name present in both modes"); - // datadog.origin presence/absence is covered by defaultModeEmitsSyntheticOrigin + assertTrue(attrs.containsKey("span.name")); } @ParameterizedTest @CsvSource({"true", "false"}) - void defaultModeEmitsIsTraceRoot(boolean traceRoot) throws IOException { + void emitsIsTraceRoot(boolean traceRoot) throws IOException { AggregateEntry e = AggregateEntryTestUtils.of( "GET /users", @@ -698,22 +640,41 @@ void defaultModeEmitsIsTraceRoot(boolean traceRoot) throws IOException { null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; assertTrue(attrs.get("datadog.is_trace_root") instanceof Boolean); assertEquals(traceRoot, attrs.get("datadog.is_trace_root")); } @Test - void otelSemanticsModeOmitsIsTraceRoot() throws IOException { - AggregateEntry e = entry("GET /users", false, 0, null, null, null); + void serviceSourceEmittedOnlyWhenSet() throws IOException { + AggregateEntry e = + AggregateEntryTestUtils.of( + "GET /users", + "web", + "servlet.request", + "component", + "web", + 0, + false, + true, + "server", + null, + null, + null, + null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(true, e).dataPoints.get(0).attributes; - assertFalse(attrs.containsKey("datadog.is_trace_root")); + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; + assertEquals("component", attrs.get("datadog.svc_src")); + assertTrue(attrs.get("datadog.svc_src") instanceof String); + + Map absentAttrs = + writeAndDecode(okEntry(SECONDS.toNanos(1), 1)).dataPoints.get(0).attributes; + assertFalse(absentAttrs.containsKey("datadog.svc_src")); } @Test - void defaultModeEmitsPeerTags() throws IOException { + void emitsPeerTags() throws IOException { AggregateEntry e = AggregateEntryTestUtils.of( "GET /users", @@ -733,18 +694,18 @@ void defaultModeEmitsPeerTags() throws IOException { null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; assertEquals( Arrays.asList("peer.service:downstream", "net.peer.name:downstream.example.com"), attrs.get("datadog.peer_tags")); } @Test - void defaultModeOmitsPeerTagsWhenEmpty() throws IOException { + void omitsPeerTagsWhenEmpty() throws IOException { AggregateEntry e = entry("GET /users", false, 0, null, null, null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; assertFalse(attrs.containsKey("datadog.peer_tags")); } @@ -776,25 +737,24 @@ void spanKindIsCanonicalizedToUppercaseEnumName(String spanKind, String expected null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; assertEquals(expected, attrs.get("span.kind")); } /** - * In default mode a synthetic entry emits {@code datadog.origin = "synthetics"}; a non-synthetic - * entry omits the attribute. Origin has collapsed to a boolean {@code synthetic} flag upstream, - * so {@code "synthetics"} is the only origin value that can reach the writer. + * A synthetic entry emits {@code datadog.origin = "synthetics"}; a non-synthetic entry omits the + * attribute. Origin has collapsed to a boolean {@code synthetic} flag upstream, so {@code + * "synthetics"} is the only origin value that can reach the writer. */ @ParameterizedTest(name = "synthetic={0} → datadog.origin={1}") @CsvSource( nullValues = "NULL", value = {"false, NULL", "true, synthetics"}) - void defaultModeEmitsSyntheticOrigin(boolean synthetic, String expectedOrigin) - throws IOException { + void emitsSyntheticOrigin(boolean synthetic, String expectedOrigin) throws IOException { AggregateEntry e = entry("servlet.request", synthetic, 0, null, null, null); AggregateEntryTestUtils.recordOk(e, SECONDS.toNanos(1)); - Map attrs = writeAndDecode(false, e).dataPoints.get(0).attributes; + Map attrs = writeAndDecode(e).dataPoints.get(0).attributes; if (expectedOrigin == null) { assertFalse(attrs.containsKey("datadog.origin"), "non-synthetic → datadog.origin absent"); } else { @@ -802,23 +762,6 @@ void defaultModeEmitsSyntheticOrigin(boolean synthetic, String expectedOrigin) } } - @Test - void otelSemanticsModeOmitsDatadogAttributes() throws IOException { - // otelSemanticsMode = true → datadog.* must be absent - Map attrs = - writeAndDecode(true, okEntry(SECONDS.toNanos(1), 1)).dataPoints.get(0).attributes; - assertFalse( - attrs.containsKey("datadog.operation.name"), - "operation name absent in otel-semantics mode"); - assertFalse(attrs.containsKey("datadog.span.type"), "span type absent in otel-semantics mode"); - assertFalse( - attrs.containsKey("datadog.span.top_level"), - "span top-level absent in otel-semantics mode"); - assertFalse(attrs.containsKey("datadog.origin"), "origin absent in otel-semantics mode"); - // OTel-semconv attrs must still be present - assertTrue(attrs.containsKey("span.name"), "span.name present even in otel-semantics mode"); - } - @Test void snapshotsEntryDataBeforeAggregatorClearsIt() throws IOException { // The aggregator clears each entry's per-interval data immediately after add() returns @@ -826,7 +769,7 @@ void snapshotsEntryDataBeforeAggregatorClearsIt() throws IOException { // (and the top-level count) at add() time; if it deferred reading to finishBucket() it would // encode the already-cleared (empty, zero-count) entry. CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender); AggregateEntry e = entry("servlet.request", false, 0, null, null, null); AggregateEntryTestUtils.recordTopLevel(e, SECONDS.toNanos(1)); @@ -852,40 +795,18 @@ void snapshotsEntryDataBeforeAggregatorClearsIt() throws IOException { // ── resource attributes (datadog.runtime_id / process tags) ──────────────── @Test - void defaultModeResourceCarriesRuntimeId() throws IOException { - // runtime-id is enabled by default, so default-mode payloads carry datadog.runtime_id on the - // Resource. + void resourceCarriesRuntimeId() throws IOException { CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false); + OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender); writer.startBucket(1, SECONDS.toNanos(1_700_000_000L), SECONDS.toNanos(10)); writer.add(okEntry(SECONDS.toNanos(1), 1)); writer.finishBucket(); Map resourceAttrs = decodeResourceAttributes(sender.lastPayload); assertTrue( - resourceAttrs.containsKey("datadog.runtime_id"), - "default mode resource carries datadog.runtime_id"); + resourceAttrs.containsKey("datadog.runtime_id"), "resource carries datadog.runtime_id"); Object runtimeId = resourceAttrs.get("datadog.runtime_id"); assertNotNull(runtimeId, "runtime id value present"); assertFalse(runtimeId.toString().isEmpty(), "runtime id value non-empty"); } - - @Test - void otelSemanticsModeResourceOmitsDatadogAttributes() throws IOException { - CapturingSender sender = new CapturingSender(); - OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, true); - writer.startBucket(1, SECONDS.toNanos(1_700_000_000L), SECONDS.toNanos(10)); - writer.add(okEntry(SECONDS.toNanos(1), 1)); - writer.finishBucket(); - - Map resourceAttrs = decodeResourceAttributes(sender.lastPayload); - assertFalse( - resourceAttrs.containsKey("datadog.runtime_id"), - "otel-semantics mode resource omits datadog.runtime_id"); - for (String key : resourceAttrs.keySet()) { - assertFalse( - key.startsWith("datadog."), - "otel-semantics mode resource has no datadog.* attrs: " + key); - } - } } From 6525e19a787beeac3568214eeb512d67efecde22 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 6 Aug 2026 14:46:42 -0400 Subject: [PATCH 14/15] Remove obsolete OTLP semantics mode wording --- .../java/datadog/trace/core/otlp/common/OtlpResourceJson.java | 3 +-- .../java/datadog/trace/core/otlp/common/OtlpResourceProto.java | 3 +-- .../trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java index 187851326d2..f6b8cfc6227 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceJson.java @@ -23,8 +23,7 @@ private OtlpResourceJson() {} /** * Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed - * {@code datadog.}). Used by the default-mode SDK trace-metrics export; omitted in OTel-semantics - * mode. + * {@code datadog.}). Used by the SDK trace-metrics export. */ public static final String RESOURCE_FRAGMENT_WITH_DATADOG_ATTRS = buildResourceFragment(Config.get(), datadogResourceAttributes(Config.get())); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java index 3d10e95454e..3a2d4c0c460 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpResourceProto.java @@ -26,8 +26,7 @@ private OtlpResourceProto() {} /** * Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed - * {@code datadog.}). Used by the default-mode SDK trace-metrics export; omitted in OTel-semantics - * mode. + * {@code datadog.}). Used by the SDK trace-metrics export. */ public static final byte[] RESOURCE_MESSAGE_WITH_DATADOG_ATTRS = buildResourceMessage(Config.get(), datadogResourceAttributes(Config.get())); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index b063e7ab5bb..01a1616693a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -162,7 +162,7 @@ private static DecodedMetric decode(byte[] payload) throws IOException { /** * Decodes the {@code Resource.attributes} ({@code ResourceMetrics.resource = 1} → {@code * Resource.attributes = 1}) into a key→value map, for asserting the {@code datadog.*} resource - * attributes emitted in default mode. + * attributes emitted by the trace-metrics exporter. */ private static Map decodeResourceAttributes(byte[] payload) throws IOException { CodedInputStream metricsData = CodedInputStream.newInstance(payload); From ad5b0efa25780ecbc08b93f0518e300af14f2132 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 6 Aug 2026 15:02:15 -0400 Subject: [PATCH 15/15] Handle missing OTLP trace metric span kind --- .../otlp/metrics/OtlpStatsMetricWriter.java | 4 +++- .../metrics/OtlpStatsMetricWriterTest.java | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java index d2c637aac1b..5e69d20e1f0 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriter.java @@ -242,7 +242,9 @@ private static void emitPeerTags(OtlpMetricVisitor metric, List } private static String canonicalSpanKind(CharSequence spanKind) { - if (Tags.SPAN_KIND_SERVER.contentEquals(spanKind)) { + if (spanKind == null) { + return SPAN_KIND_INTERNAL; + } else if (Tags.SPAN_KIND_SERVER.contentEquals(spanKind)) { return SPAN_KIND_SERVER; } else if (Tags.SPAN_KIND_CLIENT.contentEquals(spanKind)) { return SPAN_KIND_CLIENT; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java index 01a1616693a..e20f8713b84 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/metrics/OtlpStatsMetricWriterTest.java @@ -710,14 +710,17 @@ void omitsPeerTagsWhenEmpty() throws IOException { } @ParameterizedTest - @CsvSource({ - "server, SPAN_KIND_SERVER", - "client, SPAN_KIND_CLIENT", - "producer, SPAN_KIND_PRODUCER", - "consumer, SPAN_KIND_CONSUMER", - "broker, SPAN_KIND_INTERNAL", - "'', SPAN_KIND_INTERNAL", - }) + @CsvSource( + value = { + "server, SPAN_KIND_SERVER", + "client, SPAN_KIND_CLIENT", + "producer, SPAN_KIND_PRODUCER", + "consumer, SPAN_KIND_CONSUMER", + "broker, SPAN_KIND_INTERNAL", + "'', SPAN_KIND_INTERNAL", + "NULL, SPAN_KIND_INTERNAL", + }, + nullValues = "NULL") void spanKindIsCanonicalizedToUppercaseEnumName(String spanKind, String expected) throws IOException { AggregateEntry e =