diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt index cce17f15feb..bdb1bb451cf 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt @@ -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() } @@ -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() } @@ -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 diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt index 180d2b63a08..c343b228780 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt @@ -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, ) @@ -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)?.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, + mixins: Map, + traceLevel: List, + ) { + val declared = HashMap() // name -> otelName from its first declaration + val declaredKeys = HashSet() + 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 = (tags as? List>)?.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? { + 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" } + } } } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt index 4c256e1221e..ce322e1e622 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt @@ -65,10 +65,11 @@ private constructor( val reserved = (root["reserved"] as? List>)?.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" }) } ?: emptyList() return Overlay(intercepted, reserved) } @@ -165,8 +166,9 @@ 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, reserved: List) { @@ -174,8 +176,8 @@ private constructor( val owner = HashMap() 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'" } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java index fa75f5c701d..60166f73ed7 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java @@ -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(); 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()); } } diff --git a/internal-api/src/generated/java/datadog/trace/api/KnownTags.java b/internal-api/src/generated/java/datadog/trace/api/KnownTags.java index 5912ed63bf0..16bb4854998 100644 --- a/internal-api/src/generated/java/datadog/trace/api/KnownTags.java +++ b/internal-api/src/generated/java/datadog/trace/api/KnownTags.java @@ -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; @@ -115,7 +115,7 @@ 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) + // makeTagId(serial=272, slot=10) -> db.operation.name public static final String DB_POOL_NAME = "db.pool.name"; public static final long DB_POOL_NAME_ID = 0x0111FFFF00000000L; @@ -123,11 +123,11 @@ public final class KnownTags { public static final String DB_STATEMENT_NAME = "db.statement"; public static final long DB_STATEMENT_ID = 0x8112000B00000000L; - // makeTagId(serial=274, slot=11) + intercepted + // makeTagId(serial=274, slot=11) + intercepted -> db.query.text public static final String DB_TYPE_NAME = "db.type"; public static final long DB_TYPE_ID = 0x0113000C00000000L; - // makeTagId(serial=275, slot=12) + // makeTagId(serial=275, slot=12) -> db.system public static final String DB_USER_NAME = "db.user"; public static final long DB_USER_ID = 0x0114000F00000000L; @@ -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) + // makeTagId(serial=281, slot=7) -> server.address public static final String HTTP_METHOD_NAME = "http.method"; public static final long HTTP_METHOD_ID = 0x811A000900000000L; - // makeTagId(serial=282, slot=9) + intercepted + // makeTagId(serial=282, slot=9) + intercepted -> http.request.method 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) + // makeTagId(serial=283, slot=8) -> url.query public static final String HTTP_RESEND_COUNT_NAME = "http.resend_count"; public static final long HTTP_RESEND_COUNT_ID = 0x011C000F00000000L; @@ -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) + // makeTagId(serial=286, slot=10) -> http.response.status_code public static final String HTTP_URL_NAME = "http.url"; public static final long HTTP_URL_ID = 0x811F000B00000000L; - // makeTagId(serial=287, slot=11) + intercepted + // makeTagId(serial=287, slot=11) + intercepted -> url.full public static final String HTTP_USERAGENT_NAME = "http.useragent"; public static final long HTTP_USERAGENT_ID = 0x0120000E00000000L; - // makeTagId(serial=288, slot=14) + // makeTagId(serial=288, slot=14) -> user_agent.original public static final String LANGUAGE_NAME = "language"; public static final long LANGUAGE_ID = 0x0121000A00000004L; @@ -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, @@ -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; @@ -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; } diff --git a/internal-api/src/generated/tag-assignment.txt b/internal-api/src/generated/tag-assignment.txt index 9045eb8cde9..6e4f0ee59e1 100644 --- a/internal-api/src/generated/tag-assignment.txt +++ b/internal-api/src/generated/tag-assignment.txt @@ -79,3 +79,4 @@ service.name -> service url.full -> http.url url.query -> http.query.string + user_agent.original -> http.useragent diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 0f5873389d8..26fee91b5bc 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -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 diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 63fe695afef..4e59ed36218 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -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()); + } + byte type(); boolean is(byte type); diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index c70e5ff65fa..75f34ec3767 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -69,6 +69,7 @@ static Stream 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"), diff --git a/tag-conventions.java.yaml b/tag-conventions.java.yaml index fcb91ac04f0..021a240fe27 100644 --- a/tag-conventions.java.yaml +++ b/tag-conventions.java.yaml @@ -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 diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 1639e0e8afc..056c422b6a1 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -16,8 +16,19 @@ # applies — a mixin PUSHES itself onto span types, gated by `enabled_by`. # resolved_tags(type) = own + extends-chain (incl base) + included mixins + applied mixins (de-duped). # -# tag fields (DOMAIN only): tag | type (string|int|long|boolean|double) -# | required (required|conditional|recommended|optional|opt_in) | open-telemetry-name. +# tag fields (DOMAIN only): dd-name | type (string|int|long|boolean|double) +# | required (required|conditional|recommended|optional|opt_in) | otel-name. +# dd-name is the canonical Datadog-namespace name AND the tag's identity. otel-name is OPTIONAL and +# tri-state: +# - absent => the OpenTelemetry name is IMPLICITLY the dd-name (the tag passes through under +# its Datadog name; this is the RFC "retain" default for tags with no rename). +# - a name => rename: the tag is emitted under that OpenTelemetry-namespace name instead. +# - the literal none => Datadog-only: the tag has NO OpenTelemetry name (suppressed from OTel). This +# value is reserved — no tag uses it today (the RFC renames or retains, never +# suppresses), and real suppression is a follow-on; it currently behaves as +# pass-through. +# A tag is one identity across span types/mixins, so it may carry only one otel-name — declaring it +# two different ways fails the build (a span-kind-dependent mapping is a derivation, not a rename). # The id coordinate (group-decl / field-decl) is NOT authored here — the generator assigns it: each # declaration source (the trace-level tier, each span type, each mixin) is a group, and within a # group `field-decl` numbers the dense (required/conditional/recommended) tags; the rest are @@ -25,23 +36,24 @@ # --------------------------------------------------------------------------- # Trace-level tier: its own TagMap on the TraceSegment. Set once per trace, not per span. +# (Their OTel mapping is a resource-attribute follow-on; they pass through under dd-name for now.) trace_level: tags: - - { tag: _dd.base_service, type: string, required: required } - - { tag: version, type: string, required: recommended } - - { tag: env, type: string, required: recommended } - - { tag: language, type: string, required: required } - - { tag: runtime-id, type: string, required: required } - - { tag: _dd.tracer_host, type: string, required: recommended } - - { tag: _dd.git.commit.sha, type: string, required: recommended } - - { tag: _dd.git.repository_url, type: string, required: recommended } + - { dd-name: _dd.base_service, type: string, required: required } + - { dd-name: version, type: string, required: recommended } + - { dd-name: env, type: string, required: recommended } + - { dd-name: language, type: string, required: required } + - { dd-name: runtime-id, type: string, required: required } + - { dd-name: _dd.tracer_host, type: string, required: recommended } + - { dd-name: _dd.git.commit.sha, type: string, required: recommended } + - { dd-name: _dd.git.repository_url, type: string, required: recommended } # product .enabled flags — process-constant; present on the trace segment regardless of whether # the product is enabled (the flag carries the state), so always-present => recommended. - - { tag: _dd.profiling.enabled, type: boolean, required: recommended } - - { tag: _dd.dsm.enabled, type: boolean, required: recommended } - - { tag: _dd.appsec.enabled, type: boolean, required: recommended } - - { tag: _dd.djm.enabled, type: boolean, required: recommended } - - { tag: _dd.civisibility.enabled, type: boolean, required: recommended } + - { dd-name: _dd.profiling.enabled, type: boolean, required: recommended } + - { dd-name: _dd.dsm.enabled, type: boolean, required: recommended } + - { dd-name: _dd.appsec.enabled, type: boolean, required: recommended } + - { dd-name: _dd.djm.enabled, type: boolean, required: recommended } + - { dd-name: _dd.civisibility.enabled, type: boolean, required: recommended } span_types: # root: per-span tags every span has (incl. the per-span core tags parent_id / integration / svc_src @@ -49,68 +61,68 @@ span_types: base: abstract: true tags: - - { tag: _dd.parent_id, type: string, required: required } - - { tag: component, type: string, required: required } - - { tag: span.kind, type: string, required: required } - - { tag: _dd.integration, type: string, required: recommended } - - { tag: _dd.svc_src, type: string, required: optional } - - { tag: error.type, type: string, required: recommended } - - { tag: error.message, type: string, required: recommended } - - { tag: error.stack, type: string, required: recommended } + - { dd-name: _dd.parent_id, type: string, required: required } + - { dd-name: component, type: string, required: required } + - { dd-name: span.kind, type: string, required: required } # OTel span kind is a first-class field, not an attribute + - { dd-name: _dd.integration, type: string, required: recommended } + - { dd-name: _dd.svc_src, type: string, required: optional } + - { dd-name: error.type, type: string, required: recommended } # TODO(otel): map error.* to exception.* semconv + - { dd-name: error.message, type: string, required: recommended } + - { dd-name: error.stack, type: string, required: recommended } http: abstract: true extends: base tags: - - { tag: http.method, type: string, required: required, open-telemetry-name: http.request.method } - - { tag: http.status_code, type: int, required: conditional, open-telemetry-name: http.response.status_code } - - { tag: network.protocol.version, type: string, required: recommended } + - { dd-name: http.method, type: string, required: required, otel-name: http.request.method } + - { dd-name: http.status_code, type: int, required: conditional, otel-name: http.response.status_code } + - { dd-name: network.protocol.version, type: string, required: recommended } # passes through: dd-name already is the OTel name http.server: extends: http tags: - - { tag: http.url, type: string, required: required, open-telemetry-name: url.full } - - { tag: http.route, type: string, required: conditional } - - { tag: http.hostname, type: string, required: required, open-telemetry-name: server.address } - - { tag: http.useragent, type: string, required: recommended } - - { tag: http.query.string, type: string, required: recommended, open-telemetry-name: url.query } - - { tag: servlet.path, type: string, required: optional } - - { tag: servlet.context, type: string, required: optional } + - { dd-name: http.url, type: string, required: required, otel-name: url.full } # single http.url identity (shared w/ http.client) => one otel-name. url.full is the client-correct rename; server's spec mapping (url.path + url.scheme + url.query) is a one-to-many split reserved for the derivation layer (needs span.kind). TODO(otel): server split. + - { dd-name: http.route, type: string, required: conditional } # passes through: dd-name already is the OTel name + - { dd-name: http.hostname, type: string, required: required, otel-name: server.address } + - { dd-name: http.useragent, type: string, required: recommended, otel-name: user_agent.original } + - { dd-name: http.query.string, type: string, required: recommended, otel-name: url.query } + - { dd-name: servlet.path, type: string, required: optional } + - { dd-name: servlet.context, type: string, required: optional } http.client: extends: http include: [ peer ] tags: - - { tag: http.url, type: string, required: required, open-telemetry-name: url.full } - - { tag: http.resend_count, type: int, required: recommended } + - { dd-name: http.url, type: string, required: required, otel-name: url.full } + - { dd-name: http.resend_count, type: int, required: recommended } db.client: extends: base include: [ peer ] tags: - - { tag: db.type, type: string, required: required, open-telemetry-name: db.system } - - { tag: db.instance, type: string, required: recommended } - - { tag: db.operation, type: string, required: recommended, open-telemetry-name: db.operation.name } - - { tag: db.user, type: string, required: recommended } - - { tag: db.pool.name, type: string, required: optional } - - { tag: db.statement, type: string, required: recommended, open-telemetry-name: db.query.text } + - { dd-name: db.type, type: string, required: required, otel-name: db.system } + - { dd-name: db.instance, type: string, required: recommended } # TODO(otel): db.namespace + - { dd-name: db.operation, type: string, required: recommended, otel-name: db.operation.name } + - { dd-name: db.user, type: string, required: recommended } + - { dd-name: db.pool.name, type: string, required: optional } + - { dd-name: db.statement, type: string, required: recommended, otel-name: db.query.text } view.render: extends: base tags: - - { tag: view.name, type: string, required: recommended } + - { dd-name: view.name, type: string, required: recommended } mixins: # peer — outbound/remote-peer capability, PULLED via `include` by client span types. peer: tags: - - { tag: peer.service, type: string, required: recommended } - - { tag: _dd.peer.service.source, type: string, required: recommended } - - { tag: _dd.peer.service.remapped_from, type: string, required: recommended } - - { tag: peer.hostname, type: string, required: recommended } - - { tag: peer.ipv4, type: string } - - { tag: peer.ipv6, type: string } - - { tag: peer.port, type: int } + - { dd-name: peer.service, type: string, required: recommended } + - { dd-name: _dd.peer.service.source, type: string, required: recommended } + - { dd-name: _dd.peer.service.remapped_from, type: string, required: recommended } + - { dd-name: peer.hostname, type: string, required: recommended } + - { dd-name: peer.ipv4, type: string } + - { dd-name: peer.ipv6, type: string } + - { dd-name: peer.port, type: int } # ci_visibility — per-span test tags. Its capability flag (_dd.civisibility.enabled) lives in # trace_level, outside this mixin (general rule: capability flags are trace-level, mixins hold the @@ -119,10 +131,10 @@ mixins: enabled_by: dd.civisibility.enabled applies: [ test ] tags: - - { tag: test.name, type: string, required: recommended } - - { tag: test.suite, type: string, required: recommended } - - { tag: test.status, type: string, required: recommended } - - { tag: test.framework, type: string, required: recommended } + - { dd-name: test.name, type: string, required: recommended } + - { dd-name: test.suite, type: string, required: recommended } + - { dd-name: test.status, type: string, required: recommended } + - { dd-name: test.framework, type: string, required: recommended } # --------------------------------------------------------------------------- # Notes @@ -131,4 +143,6 @@ mixins: # - span.kind enumerates: server | client | producer | consumer | internal | broker. # - Reserved/special keys (service, resource.name, error, sampling.priority, ...) route to span # fields/directives, not tag storage — they live in the per-language overlay, not here. +# - Tags with no otel-name pass through under their Datadog name (RFC "retain"). A `# TODO(otel)` note +# marks a pending OpenTelemetry-team review of a mapping that is not yet a settled rename. # ---------------------------------------------------------------------------