Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package datadog.trace.core.otlp.trace;

import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DD_MEASURED;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DD_PARTIAL_VERSION;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DD_TOP_LEVEL;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DD_WAS_LONG_RUNNING;
import static datadog.trace.common.writer.ddagent.TraceMapper.ORIGIN_KEY;
import static datadog.trace.common.writer.ddagent.TraceMapper.PROCESS_TAGS_KEY;
import static datadog.trace.common.writer.ddagent.TraceMapper.SAMPLING_PRIORITY_KEY;
import static datadog.trace.common.writer.ddagent.TraceMapper.THREAD_ID;
import static datadog.trace.common.writer.ddagent.TraceMapper.THREAD_NAME;

import datadog.trace.api.KnownTagCodec;
import datadog.trace.api.KnownTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/** Span attribute keys shared by the OTLP protobuf and JSON trace writers. */
final class OtlpTraceAttributes {

@mcculls mcculls Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: OtlpSpanAttributes since this holds span attributes?


static final UTF8BytesString RESOURCE_NAME = UTF8BytesString.create("resource.name");
static final UTF8BytesString OPERATION_NAME = UTF8BytesString.create("operation.name");
static final UTF8BytesString SPAN_TYPE = UTF8BytesString.create("span.type");

/*
* Span fields and intercepted Metadata fields bypass TagMap entry projection. Resolve their
* keys through the registry here so OpenTelemetry names stay consistent with entry tags.
*/
static final UTF8BytesString SERVICE_NAME_KEY = otelKey(KnownTags.SERVICE_ID);

static final UTF8BytesString HTTP_STATUS_CODE_KEY = otelKey(KnownTags.HTTP_STATUS_CODE_ID);

static final UTF8BytesString HTTP_STATUS_CODE_KEY_DD =
UTF8BytesString.create(KnownTags.HTTP_STATUS_CODE_NAME);

/**
* Keys that baggage must not export as attributes: the attributes the writers emit directly from
* span fields and intercepted Metadata fields (the span's own value wins), plus _dd.p.tid, whose
* upper 64 bits the OTLP traceId already carries.
*/
static final Set<String> SKIPPED_BAGGAGE_KEYS =
new HashSet<>(
Arrays.asList(
"_dd.p.tid",
SERVICE_NAME_KEY.toString(),
RESOURCE_NAME.toString(),
OPERATION_NAME.toString(),
SPAN_TYPE.toString(),
SAMPLING_PRIORITY_KEY.toString(),
DD_MEASURED.toString(),
DD_TOP_LEVEL.toString(),
DD_PARTIAL_VERSION.toString(),
DD_WAS_LONG_RUNNING.toString(),
THREAD_ID.toString(),
THREAD_NAME.toString(),
HTTP_STATUS_CODE_KEY.toString(),
HTTP_STATUS_CODE_KEY_DD.toString(),
ORIGIN_KEY.toString(),
PROCESS_TAGS_KEY.toString()));

/**
* True if a span tag is exported under exactly {@code key}, so baggage under that key would
* duplicate it. TagMap lookups resolve aliases (http.request.method finds http.method), so the
* matching entry's exported name is compared with the key: it depends on OTel semantics and, for
* direction-scoped tags, on the span's direction (peer.port is server.port on a client).
*/
static boolean isExportedTagKey(TagMap tags, String key, int direction, boolean otelSemantics) {
if (exportsAs(tags.getEntry(key), key, direction, otelSemantics)) {
return true;
}
if (otelSemantics) {
long directionalTagId = KnownTagCodec.directionalKeyOf(key, direction);
return directionalTagId != 0
&& exportsAs(tags.getEntry(directionalTagId), key, direction, true);
}
return false;
}

private static boolean exportsAs(
TagMap.Entry entry, String key, int direction, boolean otelSemantics) {
return entry != null
&& key.equals(otelSemantics ? entry.openTelemetryTag(direction) : entry.tag());
}

/** The OpenTelemetry-namespace key for a known tag, as named by the registry. */
private static UTF8BytesString otelKey(long tagId) {
return UTF8BytesString.create(KnownTagCodec.openTelemetryTagOf(tagId));
}

private OtlpTraceAttributes() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,27 @@
import static datadog.trace.common.writer.ddagent.TraceMapper.SAMPLING_PRIORITY_KEY;
import static datadog.trace.common.writer.ddagent.TraceMapper.THREAD_ID;
import static datadog.trace.common.writer.ddagent.TraceMapper.THREAD_NAME;
import static datadog.trace.core.CoreSpan.OWN_PROPAGATION_TAGS;
import static datadog.trace.core.CoreSpan.ROOT_PROPAGATION_TAGS;
import static datadog.trace.core.otlp.common.OtlpCommonJson.hexSpanId;
import static datadog.trace.core.otlp.common.OtlpCommonJson.hexTraceId;
import static datadog.trace.core.otlp.common.OtlpCommonJson.writeAttribute;
import static datadog.trace.core.otlp.common.OtlpTraceFlags.NO_TRACE_FLAGS;
import static datadog.trace.core.otlp.common.OtlpTraceFlags.REMOTE_TRACE_FLAG;
import static datadog.trace.core.otlp.common.OtlpTraceFlags.SAMPLED_TRACE_FLAG;
import static datadog.trace.core.otlp.trace.OtlpSpanKind.spanKind;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.HTTP_STATUS_CODE_KEY;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.HTTP_STATUS_CODE_KEY_DD;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.OPERATION_NAME;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.RESOURCE_NAME;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.SERVICE_NAME_KEY;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.SKIPPED_BAGGAGE_KEYS;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.SPAN_TYPE;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.isExportedTagKey;

import datadog.json.JsonWriter;
import datadog.trace.api.Config;
import datadog.trace.api.DDTags;
import datadog.trace.api.KnownTagCodec;
import datadog.trace.api.KnownTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
Expand All @@ -43,27 +50,6 @@
/** Provides writers for OpenTelemetry's "trace.proto" JSON encoding. */
public final class OtlpTraceJson {

private static final UTF8BytesString RESOURCE_NAME = UTF8BytesString.create("resource.name");
private static final UTF8BytesString OPERATION_NAME = UTF8BytesString.create("operation.name");
private static final UTF8BytesString SPAN_TYPE = UTF8BytesString.create("span.type");

/*
* Span fields and intercepted Metadata fields bypass TagMap entry projection. Resolve their
* keys through the registry here so JSON and protobuf use the same OpenTelemetry names.
*/
private static final UTF8BytesString SERVICE_NAME_KEY = otelKey(KnownTags.SERVICE_ID);

private static final UTF8BytesString HTTP_STATUS_CODE_KEY =
otelKey(KnownTags.HTTP_STATUS_CODE_ID);

private static final UTF8BytesString HTTP_STATUS_CODE_KEY_DD =
UTF8BytesString.create(KnownTags.HTTP_STATUS_CODE_NAME);

/** The OpenTelemetry-namespace key for a known tag, as named by the registry. */
private static UTF8BytesString otelKey(long tagId) {
return UTF8BytesString.create(KnownTagCodec.openTelemetryTagOf(tagId));
}

private OtlpTraceJson() {}

/** Writes one complete {@code Span} JSON object. */
Expand Down Expand Up @@ -113,7 +99,10 @@ public static void writeSpan(
if (span.getSpanType() != null) {
writeSpanTag(writer, SPAN_TYPE, span.getSpanType());
}
span.processTagsAndBaggage(metaWriter, OWN_PROPAGATION_TAGS);
// Export root propagation tags on the span selected by the collector for sampling tags,
// including when the exported trace contains no local root span.
span.processTagsAndBaggage(
metaWriter, metaWriter.includeSamplingTags ? ROOT_PROPAGATION_TAGS : 0);
writer.endArray();

if (!links.isEmpty()) {
Expand Down Expand Up @@ -252,9 +241,23 @@ public void accept(Metadata metadata) {
writeSpanTag(writer, PROCESS_TAGS_KEY, metadata.processTags());
}

// The direction is computed once per span; boxing -1..2 hits the Integer cache.
TagMap tags = metadata.getTags();
// The direction is computed once per span; boxing -1..2 hits the Integer cache.
Integer direction = KnownTagCodec.directionOf(tags);
boolean otelSemantics = Config.get().isTraceOtelSemanticsEnabled();

// Baggage carries the propagation tags (e.g. the _dd.p.dm sampling decision maker) and any
// baggage items injected as tags, as in the Datadog mappers. Span tags and the attributes
// written directly above win when they are exported under the same key, which keeps OTLP
// attribute keys unique without dropping baggage whose exported key differs.
for (Map.Entry<String, String> entry : metadata.getBaggage().entrySet()) {
if (SKIPPED_BAGGAGE_KEYS.contains(entry.getKey())
|| isExportedTagKey(tags, entry.getKey(), direction, otelSemantics)) {
continue;
}
writeAttribute(writer, STRING_ATTRIBUTE, entry.getKey(), entry.getValue());
}

tags.forEach(writer, direction, OtlpTraceJson::writeSpanTag);
Comment thread
mcculls marked this conversation as resolved.

// reset for next span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import static datadog.trace.common.writer.ddagent.TraceMapper.SAMPLING_PRIORITY_KEY;
import static datadog.trace.common.writer.ddagent.TraceMapper.THREAD_ID;
import static datadog.trace.common.writer.ddagent.TraceMapper.THREAD_NAME;
import static datadog.trace.core.CoreSpan.OWN_PROPAGATION_TAGS;
import static datadog.trace.core.CoreSpan.ROOT_PROPAGATION_TAGS;
import static datadog.trace.core.otlp.common.OtlpCommonProto.I32_WIRE_TYPE;
import static datadog.trace.core.otlp.common.OtlpCommonProto.I64_WIRE_TYPE;
import static datadog.trace.core.otlp.common.OtlpCommonProto.LEN_WIRE_TYPE;
Expand All @@ -31,6 +31,14 @@
import static datadog.trace.core.otlp.common.OtlpTraceFlags.REMOTE_TRACE_FLAG;
import static datadog.trace.core.otlp.common.OtlpTraceFlags.SAMPLED_TRACE_FLAG;
import static datadog.trace.core.otlp.trace.OtlpSpanKind.spanKind;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.HTTP_STATUS_CODE_KEY;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.HTTP_STATUS_CODE_KEY_DD;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.OPERATION_NAME;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.RESOURCE_NAME;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.SERVICE_NAME_KEY;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.SKIPPED_BAGGAGE_KEYS;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.SPAN_TYPE;
import static datadog.trace.core.otlp.trace.OtlpTraceAttributes.isExportedTagKey;
import static java.nio.charset.StandardCharsets.UTF_8;

import datadog.communication.serialization.GrowableBuffer;
Expand All @@ -39,7 +47,6 @@
import datadog.trace.api.DDTags;
import datadog.trace.api.DDTraceId;
import datadog.trace.api.KnownTagCodec;
import datadog.trace.api.KnownTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
Expand All @@ -51,31 +58,11 @@
import datadog.trace.core.otlp.common.OtlpProtoBuffer;
import datadog.trace.core.propagation.PropagationTags;
import datadog.trace.core.propagation.PropagationTags.SamplingState;
import java.util.Map;

/** Provides optimized writers for OpenTelemetry's "trace.proto" wire protocol. */
public final class OtlpTraceProto {

private static final UTF8BytesString RESOURCE_NAME = UTF8BytesString.create("resource.name");
private static final UTF8BytesString OPERATION_NAME = UTF8BytesString.create("operation.name");
private static final UTF8BytesString SPAN_TYPE = UTF8BytesString.create("span.type");

/*
* Span fields and intercepted Metadata fields bypass TagMap entry projection. Resolve their
* keys through the registry here so OpenTelemetry names stay consistent with entry tags.
*/
private static final UTF8BytesString SERVICE_NAME_KEY = otelKey(KnownTags.SERVICE_ID);

private static final UTF8BytesString HTTP_STATUS_CODE_KEY =
otelKey(KnownTags.HTTP_STATUS_CODE_ID);

private static final UTF8BytesString HTTP_STATUS_CODE_KEY_DD =
UTF8BytesString.create(KnownTags.HTTP_STATUS_CODE_NAME);

/** The OpenTelemetry-namespace key for a known tag, as named by the registry. */
private static UTF8BytesString otelKey(long tagId) {
return UTF8BytesString.create(KnownTagCodec.openTelemetryTagOf(tagId));
}

private OtlpTraceProto() {}

/** Records a scoped spans message after its nested span messages have been recorded. */
Expand Down Expand Up @@ -160,7 +147,10 @@ public static int recordSpanMessage(
writeSpanTag(buf, SPAN_TYPE, span.getSpanType());
}

span.processTagsAndBaggage(metaWriter, OWN_PROPAGATION_TAGS);
// Export root propagation tags on the span selected by the collector for sampling tags,
// including when the exported trace contains no local root span.
span.processTagsAndBaggage(
metaWriter, metaWriter.includeSamplingTags ? ROOT_PROPAGATION_TAGS : 0);

if (span.isError()) {
int stateSize = 2;
Expand Down Expand Up @@ -329,9 +319,24 @@ public void accept(Metadata metadata) {
writeSpanTag(buf, PROCESS_TAGS_KEY, metadata.processTags());
}

// The direction is computed once per span; boxing -1..2 hits the Integer cache.
TagMap tags = metadata.getTags();
// The direction is computed once per span; boxing -1..2 hits the Integer cache.
Integer direction = KnownTagCodec.directionOf(tags);
boolean otelSemantics = Config.get().isTraceOtelSemanticsEnabled();

// Baggage carries the propagation tags (e.g. the _dd.p.dm sampling decision maker) and any
// baggage items injected as tags, as in the Datadog mappers. Span tags and the attributes
// written directly above win when they are exported under the same key, which keeps OTLP
// attribute keys unique without dropping baggage whose exported key differs.
for (Map.Entry<String, String> entry : metadata.getBaggage().entrySet()) {
if (SKIPPED_BAGGAGE_KEYS.contains(entry.getKey())
|| isExportedTagKey(tags, entry.getKey(), direction, otelSemantics)) {
continue;
}
writeTag(buf, 9, LEN_WIRE_TYPE);
writeAttribute(buf, STRING_ATTRIBUTE, entry.getKey(), entry.getValue());
}

tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag);

// reset for next span
Expand Down
Loading
Loading