Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ object KnownTagsEmitter {
for (v in reg.reserved) {
b.appendLine(" public static final String ${nameC(v.name)} = \"${v.name}\";")
b.appendLine(" public static final long ${idC(v.name)} = ${hex(v.id)};")
b.appendLine(" // makeTagId(serial=${v.serial}, slot=NO_SLOT) + intercepted [${v.kind}${v.field?.let { " -> $it" } ?: ""}]")
b.appendLine(" // makeTagId(serial=${v.serial}, slot=NO_SLOT) + intercepted${if (v.otelName != null) " -> ${v.otelName}" else ""} [${v.kind}${v.field?.let { " -> $it" } ?: ""}]")
b.appendLine()
}

Expand All @@ -70,7 +70,7 @@ object KnownTagsEmitter {
val slot = if (t.slotted) t.slot.toString() else "NO_SLOT"
b.appendLine(" public static final String ${nameC(t.name)} = \"${t.name}\";")
b.appendLine(" public static final long ${idC(t.name)} = ${hex(t.id)};")
b.appendLine(" // makeTagId(serial=${t.serial}, slot=$slot)${if (t.intercepted) " + intercepted" else ""}${if (t.traceLevel) " + trace-level" else ""} <${t.required}>")
b.appendLine(" // makeTagId(serial=${t.serial}, slot=$slot)${if (t.intercepted) " + intercepted" else ""}${if (t.traceLevel) " + trace-level" else ""}${if (t.otelName != null) " -> ${t.otelName}" else ""} <${t.required}>")
b.appendLine()
}

Expand All @@ -84,11 +84,14 @@ object KnownTagsEmitter {
}
b.appendLine()

// OpenTelemetry name -> canonical tag name, for the tags that declare one. Deterministic order
// (by OTel name) so output stays byte-identical.
// OpenTelemetry name -> canonical tag name, for the tags that declare a DISTINCT one. A same-name
// dual (otel-name == dd-name) is already resolvable via the canonical row, so it is skipped here
// to keep the keyOf table free of redundant entries. Deterministic order (by OTel name) so
// output stays byte-identical.
val otelByCanonical =
(reg.stored.mapNotNull { t -> t.otelName?.let { it to t.name } } +
reg.reserved.mapNotNull { v -> v.otelName?.let { it to v.name } })
.filter { (otel, canonical) -> otel != canonical }
.sortedBy { it.first }

// keyOf table (open-addressed, via StringIndex.EmbeddingSupport). Canonical names first, then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ private constructor(
val type: String,
val required: String,
/**
* The tag's OpenTelemetry-namespace name, if it has one. keyOf resolves it to this tag's
* canonical id (inbound, many->one); openTelemetryNameOf recovers it (outbound). Further
* namespaces and serializer applicability are a follow-on concern.
* The tag's OpenTelemetry-namespace RENAME, or null when it has none. otel-name is optional and
* tri-state in the YAML: absent => the OpenTelemetry name is implicitly the dd-name (pass-through
* under the Datadog name; the RFC "retain" default) and this field is null; a name => a rename to
* that OpenTelemetry-namespace name; the literal `none` => Datadog-only (no OpenTelemetry name)
* and this field is null — a reserved value with no tags today (suppression is a follow-on), so
* it currently behaves as pass-through, indistinguishable from absent. keyOf resolves a rename
* to this tag's canonical id (inbound, many->one); openTelemetryNameOf recovers it (outbound).
*/
val otelName: String? = null,
)
Expand Down Expand Up @@ -162,19 +166,72 @@ private constructor(
)
}

// Trace-level tags pass through under their Datadog name for now; their OTel mapping (resource
// attributes) is a follow-on. TODO(otel follow-on).
val traceLevel = tagList((root["trace_level"] as? Map<String, Any?>)?.get("tags"))
validateOtelNameConsistency(spanTypes, mixins, traceLevel)
return TagConventions(spanTypes, mixins, traceLevel)
}

/**
* A tag is de-duped by name across span types / mixins (see [resolve] / [allStoredTags]), so its
* whole identity — including the OpenTelemetry name — must be declared consistently everywhere it
* appears. `http.url` on `http.server` and `http.client`, for instance, is ONE tag: it can carry
* exactly one otel-name. Without this check, two conflicting declarations would silently collapse
* to whichever the dedup happened to keep. Fail the build loudly instead. (A span-kind-dependent
* mapping is a derivation, not a rename, and belongs to the derivation layer — not two otel-names
* on one identity.)
*/
private fun validateOtelNameConsistency(
spanTypes: Map<String, SpanType>,
mixins: Map<String, Mixin>,
traceLevel: List<Tag>,
) {
val declared = HashMap<String, String?>() // name -> otelName from its first declaration
val declaredKeys = HashSet<String>()
val check = { t: Tag ->
if (declaredKeys.add(t.name)) {
declared[t.name] = t.otelName
} else {
require(declared[t.name] == t.otelName) {
"tag '${t.name}' declares conflicting otel-name: '${declared[t.name] ?: "none"}' vs " +
"'${t.otelName ?: "none"}'. A tag is one identity across span types/mixins and may " +
"carry only one otel-name; a span-kind-dependent mapping belongs to the derivation layer."
}
}
}
spanTypes.values.forEach { it.tags.forEach(check) }
mixins.values.forEach { it.tags.forEach(check) }
traceLevel.forEach(check)
}

@Suppress("UNCHECKED_CAST")
private fun tagList(tags: Any?): List<Tag> =
(tags as? List<Map<String, Any?>>)?.map { m ->
Tag(
name = m["tag"].toString(),
name = m["dd-name"].toString(),
type = (m["type"] as? String) ?: "string",
required = (m["required"] as? String) ?: "optional",
otelName = m["open-telemetry-name"] as? String,
otelName = parseOtelName(m),
)
} ?: emptyList()

/**
* Parse the optional, tri-state `otel-name` of one tag. Absent (key not present) => implicit
* dd-name (pass-through) => null; the literal `none` => Datadog-only (reserved) => null; any other
* non-blank string => a rename => that value. A present-but-invalid value (empty/blank, or a
* non-string such as a number or an unquoted YAML `null`) is a typo that would otherwise slip
* through the `as? String` cast into a silent pass-through or an empty rename — fail the build
* loudly instead.
*/
private fun parseOtelName(m: Map<String, Any?>): String? {
if (!m.containsKey("otel-name")) return null // absent => pass-through
val raw = m["otel-name"]
require(raw is String && raw.isNotBlank()) {
"tag '${m["dd-name"]}' has an invalid otel-name: '$raw'. Use a non-empty name, the literal " +
"`none`, or omit the key entirely for pass-through under the Datadog name."
}
return raw.takeUnless { it == "none" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ private constructor(
val reserved =
(root["reserved"] as? List<Map<String, Any?>>)?.map { m ->
ReservedDef(
m["tag"].toString(),
m["dd-name"].toString(),
(m["kind"] as? String) ?: "directive",
m["field"] as? String,
m["open-telemetry-name"] as? String)
// Reserved tags are not required to declare otel-name; absent or "none" -> no name.
(m["otel-name"] as? String)?.takeUnless { it == "none" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject invalid reserved-tag OTel names

For reserved entries, an otel-name: with no value or a non-string value is silently cast to null, while a blank string is accepted as a rename. This bypasses the validation now applied to stored tags: a typo can generate a pass-through mapping or even an empty alias/key instead of failing the registry build. Parse reserved otel-name values with the same nonblank-string-or-literal-none validation used by TagConventions.parseOtelName.

Useful? React with 👍 / 👎.

} ?: emptyList()
return Overlay(intercepted, reserved)
}
Expand Down Expand Up @@ -165,17 +166,18 @@ private constructor(
}

/**
* An OpenTelemetry name must be unambiguous: it may not collide with any canonical tag name, nor
* be claimed by two different tags. Otherwise keyOf(otelName) would have no single right answer.
* An OpenTelemetry name must be unambiguous: it may not collide with a DIFFERENT tag's canonical
* name, nor be claimed by two different tags. A tag sharing its OWN Datadog name across both
* namespaces (the same-name tri-state, e.g. http.route) is allowed — keyOf still has one answer.
* Fail the build loudly rather than silently pick a winner.
*/
private fun validateOtelNames(stored: List<StoredTag>, reserved: List<ReservedTag>) {
val canonical = (stored.map { it.name } + reserved.map { it.name }).toSet()
val owner = HashMap<String, String>()
val check = { name: String, otel: String? ->
if (otel != null) {
require(otel !in canonical) {
"OpenTelemetry name '$otel' (of '$name') collides with canonical tag name '$otel'"
require(otel == name || otel !in canonical) {
"OpenTelemetry name '$otel' (of '$name') collides with a different canonical tag name"
}
val prev = owner.put(otel, name)
require(prev == null) { "OpenTelemetry name '$otel' is claimed by both '$prev' and '$name'" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,26 @@ public static void writeSpanId(StreamingBuffer buf, long spanId) {

private static void writeSpanTag(StreamingBuffer buf, TagMap.EntryReader tagEntry) {
writeTag(buf, 9, LEN_WIRE_TYPE);
// OTLP is the OpenTelemetry wire format, so render each known tag under its OpenTelemetry rename
// when it declares one, falling back to the Datadog name otherwise (pass-through, the default).
// This is the straight rename projection only — suppressing a Datadog-only tag from OpenTelemetry,
// per-exporter opt-in, and additional namespaces are deferred to the OpenTelemetry follow-on.
String otelName = tagEntry.openTelemetryName();
String key = otelName != null ? otelName : tagEntry.tag();
Comment on lines +214 to +215

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Activate the registry before resolving OTLP tag names

With the default trace.experimental.dense.tags.enabled=false, the only production call to KnownTags.init() remains guarded by DENSE_TAGS_ENABLED in CoreTracer, so KnownTagCodec is inactive here. Consequently tagEntry.openTelemetryName() returns null for every tag and normal OTLP exports continue emitting http.method, http.status_code, and the other Datadog names instead of the mappings introduced by this change; only users enabling the experimental dense-tag store see the new behavior. Registry name resolution needs to be initialized independently of whether dense storage is enabled.

Useful? React with 👍 / 👎.

Comment on lines +214 to +215

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update OTLP tests to expect renamed attributes

With the registry activated by the static CoreTracer in OtlpTraceProtoTest, this lookup changes the http.method test tags at lines 363 and 378 into http.request.method. However, verifySpan at lines 996-1000 still requires every original extraTags key to be present, so the string-tag and mixed-tag parameterized cases fail deterministically instead of validating the new output namespace.

Useful? React with 👍 / 👎.

Comment on lines +214 to +215

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Deduplicate aliases before emitting renamed attributes

With the default KnownTagCodec.DENSE_STORE=false, TagMap.set keeps a Datadog name and its OpenTelemetry alias as separate bucket entries. If a span contains both http.method and http.request.method—for example when Datadog and OpenTelemetry instrumentation both contribute attributes—this projection maps both entries to http.request.method, and MetaWriter emits two attributes with the same key and potentially conflicting, iteration-order-dependent values. These keys remained distinct before this change; the exporter needs to canonicalize or deduplicate projected names independently of the experimental dense store.

Useful? React with 👍 / 👎.

switch (tagEntry.type()) {
case TagMap.EntryReader.BOOLEAN:
writeAttribute(buf, BOOLEAN_ATTRIBUTE, tagEntry.tag(), tagEntry.objectValue());
writeAttribute(buf, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue());
break;
case TagMap.EntryReader.INT:
case TagMap.EntryReader.LONG:
writeAttribute(buf, LONG_ATTRIBUTE, tagEntry.tag(), tagEntry.objectValue());
writeAttribute(buf, LONG_ATTRIBUTE, key, tagEntry.objectValue());
break;
case TagMap.EntryReader.FLOAT:
case TagMap.EntryReader.DOUBLE:
writeAttribute(buf, DOUBLE_ATTRIBUTE, tagEntry.tag(), tagEntry.objectValue());
writeAttribute(buf, DOUBLE_ATTRIBUTE, key, tagEntry.objectValue());
break;
default:
writeAttribute(buf, STRING_ATTRIBUTE, tagEntry.tag(), tagEntry.stringValue());
writeAttribute(buf, STRING_ATTRIBUTE, key, tagEntry.stringValue());
}
}

Expand Down
24 changes: 14 additions & 10 deletions internal-api/src/generated/java/datadog/trace/api/KnownTags.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public final class KnownTags {

public static final String SERVICE_NAME = "service";
public static final long SERVICE_ID = 0x8002FFFF00000000L;
// makeTagId(serial=2, slot=NO_SLOT) + intercepted [structural -> service]
// makeTagId(serial=2, slot=NO_SLOT) + intercepted -> service.name [structural -> service]

public static final String RESOURCE_NAME = "resource.name";
public static final long RESOURCE_NAME_ID = 0x8003FFFF00000000L;
Expand Down Expand Up @@ -115,19 +115,19 @@ public final class KnownTags {

public static final String DB_OPERATION_NAME = "db.operation";
public static final long DB_OPERATION_ID = 0x0110000A00000000L;
// makeTagId(serial=272, slot=10) <recommended>
// makeTagId(serial=272, slot=10) -> db.operation.name <recommended>

public static final String DB_POOL_NAME = "db.pool.name";
public static final long DB_POOL_NAME_ID = 0x0111FFFF00000000L;
// makeTagId(serial=273, slot=NO_SLOT) <optional>

public static final String DB_STATEMENT_NAME = "db.statement";
public static final long DB_STATEMENT_ID = 0x8112000B00000000L;
// makeTagId(serial=274, slot=11) + intercepted <recommended>
// makeTagId(serial=274, slot=11) + intercepted -> db.query.text <recommended>

public static final String DB_TYPE_NAME = "db.type";
public static final long DB_TYPE_ID = 0x0113000C00000000L;
// makeTagId(serial=275, slot=12) <required>
// makeTagId(serial=275, slot=12) -> db.system <required>

public static final String DB_USER_NAME = "db.user";
public static final long DB_USER_ID = 0x0114000F00000000L;
Expand All @@ -151,15 +151,15 @@ public final class KnownTags {

public static final String HTTP_HOSTNAME_NAME = "http.hostname";
public static final long HTTP_HOSTNAME_ID = 0x0119000700000000L;
// makeTagId(serial=281, slot=7) <required>
// makeTagId(serial=281, slot=7) -> server.address <required>

public static final String HTTP_METHOD_NAME = "http.method";
public static final long HTTP_METHOD_ID = 0x811A000900000000L;
// makeTagId(serial=282, slot=9) + intercepted <required>
// makeTagId(serial=282, slot=9) + intercepted -> http.request.method <required>

public static final String HTTP_QUERY_STRING_NAME = "http.query.string";
public static final long HTTP_QUERY_STRING_ID = 0x011B000800000000L;
// makeTagId(serial=283, slot=8) <recommended>
// makeTagId(serial=283, slot=8) -> url.query <recommended>

public static final String HTTP_RESEND_COUNT_NAME = "http.resend_count";
public static final long HTTP_RESEND_COUNT_ID = 0x011C000F00000000L;
Expand All @@ -171,15 +171,15 @@ public final class KnownTags {

public static final String HTTP_STATUS_CODE_NAME = "http.status_code";
public static final long HTTP_STATUS_CODE_ID = 0x011E000A00000000L;
// makeTagId(serial=286, slot=10) <conditional>
// makeTagId(serial=286, slot=10) -> http.response.status_code <conditional>

public static final String HTTP_URL_NAME = "http.url";
public static final long HTTP_URL_ID = 0x811F000B00000000L;
// makeTagId(serial=287, slot=11) + intercepted <required>
// makeTagId(serial=287, slot=11) + intercepted -> url.full <required>

public static final String HTTP_USERAGENT_NAME = "http.useragent";
public static final long HTTP_USERAGENT_ID = 0x0120000E00000000L;
// makeTagId(serial=288, slot=14) <recommended>
// makeTagId(serial=288, slot=14) -> user_agent.original <recommended>

public static final String LANGUAGE_NAME = "language";
public static final long LANGUAGE_ID = 0x0121000A00000004L;
Expand Down Expand Up @@ -357,6 +357,7 @@ public final class KnownTags {
"service.name",
"url.full",
"url.query",
"user_agent.original",
};
private static final long[] KEYOF_VALUES = {
ERROR_ID,
Expand Down Expand Up @@ -424,6 +425,7 @@ public final class KnownTags {
SERVICE_ID,
HTTP_URL_ID,
HTTP_QUERY_STRING_ID,
HTTP_USERAGENT_ID,
};
private static final int[] KEYOF_HASHES;
private static final String[] KEYOF_KEYS;
Expand Down Expand Up @@ -584,6 +586,8 @@ public String openTelemetryNameOf(long tagId) {
return "http.response.status_code";
case HTTP_URL_SERIAL_NUM:
return "url.full";
case HTTP_USERAGENT_SERIAL_NUM:
return "user_agent.original";
default:
return null;
}
Expand Down
1 change: 1 addition & 0 deletions internal-api/src/generated/tag-assignment.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@
service.name -> service
url.full -> http.url
url.query -> http.query.string
user_agent.original -> http.useragent
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ public static boolean isActive() {
* {@link TagMap}). The low 32 bits are unused for known ids (the whole id is fully determined by
* serial + slot, so the generator can emit a literal). The low 32 bits are being carved for
* cross-cutting flags; bit 2 is the trace/span LEVEL bit (set ⟹ trace-level), and bits 1-0 are
* reserved for the dd/otel applicability flags that land with increment 1. The level bit lets
* read-through skip the shadow check across the trace/span boundary — trace and span tags reuse
* reserved. (An OpenTelemetry-applicability flag was considered but omitted: with pass-through as
* the default — a tag with no rename is emitted under its Datadog name — every known tag today is
* emitted under OpenTelemetry, so the flag would be constant. A tag's OpenTelemetry name, when it
* renames, is recovered by {@link #openTelemetryNameOf}; suppression of a Datadog-only tag from
* OpenTelemetry is a follow-on that would reintroduce a flag once such a tag exists.) The level
* bit lets read-through skip the shadow check across the trace/span boundary — trace and span tags reuse
* the same slots, so occupancy alone can't tell them apart, but a span map (no trace-level tags)
* can never shadow a trace-level ancestor entry (see {@link TagMap}). Unknown (string-only) custom
* tags are NOT known ids — they key off {@code TagMap.Entry#_hash(name)} in their own bucket path
Expand Down
11 changes: 11 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/TagMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ public interface EntryReader {
*/
long tagId();

/**
* This entry's tag RENAME in the OpenTelemetry namespace, or {@code null} when the tag has no
* rename — in which case it passes through under its Datadog name ({@link #tag()}), which is
* the default. Also {@code null} for a custom tag or when the resolver is inactive. Pure lookup
* via {@link KnownTagCodec#openTelemetryNameOf(long)} on {@link #tagId()}; a serializer owns
* the fall-back-to-Datadog-name policy.
*/
default String openTelemetryName() {
return KnownTagCodec.openTelemetryNameOf(tagId());
Comment on lines +185 to +186

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reuse the known tag ID during dense serialization

perf: When dense tags are enabled, MetaWriter.accept reaches this method through TagMap.forEach for every serialized tag, but that dense iteration path initializes EntryReadingHelper with only nameOf(knownIds[i]). Calling tagId() here therefore hashes and probes keyOf(name) again for every emitted dense tag, even though knownIds[i] is already available and the existing three-argument EntryReadingHelper.set overload can retain it. This is mechanism-determined repeated work on the per-span serialization path; pass the known ID through and verify the improvement with the serialization benchmark.

AGENTS.md reference: AGENTS.md:L79-L81

Useful? React with 👍 / 👎.

}

byte type();

boolean is(byte type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ static Stream<Arguments> otelNamedTags() {
"http.response.status_code", KnownTags.HTTP_STATUS_CODE_ID, "http.status_code"),
Arguments.of("url.full", KnownTags.HTTP_URL_ID, "http.url"),
Arguments.of("server.address", KnownTags.HTTP_HOSTNAME_ID, "http.hostname"),
Arguments.of("user_agent.original", KnownTags.HTTP_USERAGENT_ID, "http.useragent"),
Arguments.of("url.query", KnownTags.HTTP_QUERY_STRING_ID, "http.query.string"),
Arguments.of("db.system", KnownTags.DB_TYPE_ID, "db.type"),
Arguments.of("db.operation.name", KnownTags.DB_OPERATION_ID, "db.operation"),
Expand Down
20 changes: 10 additions & 10 deletions tag-conventions.java.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ intercepted:
# kind: structural -> sets a span/trace field (`field:` names it)
# kind: directive -> triggers sampling/trace behavior
reserved:
- { tag: error, kind: structural, field: error }
- { tag: service, kind: structural, field: service, open-telemetry-name: service.name }
- { tag: resource.name, kind: structural, field: resource }
- { tag: span.type, kind: structural, field: type }
- { tag: origin, kind: structural, field: origin } # trace-level field
- { tag: sampling.priority, kind: directive }
- { tag: manual.keep, kind: directive }
- { tag: manual.drop, kind: directive }
- { tag: measured, kind: directive }
- { tag: analytics.sample_rate, kind: directive } # legacy
- { dd-name: error, kind: structural, field: error }
- { dd-name: service, kind: structural, field: service, otel-name: service.name }
- { dd-name: resource.name, kind: structural, field: resource }
- { dd-name: span.type, kind: structural, field: type }
- { dd-name: origin, kind: structural, field: origin } # trace-level field
- { dd-name: sampling.priority, kind: directive }
- { dd-name: manual.keep, kind: directive }
- { dd-name: manual.drop, kind: directive }
- { dd-name: measured, kind: directive }
- { dd-name: analytics.sample_rate, kind: directive } # legacy
Loading
Loading