diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 660d14776d4..aca1421286d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ /SECURITY.md @DataDog/apm-java /SUPPORT.md @DataDog/apm-java /tag-conventions.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities +/tag-conventions-java.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities /dd-smoke-tests/README.md @DataDog/apm-java /docs/ @DataDog/apm-java @@ -87,6 +88,7 @@ /internal-api/src/*/*/datadog/trace/api/endpoint/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/env/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/function/ @DataDog/apm-java +/internal-api/src/*/*/datadog/apicontract/ @DataDog/apm-java /internal-api/src/*/*/datadog/perfcontract/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/intake/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/logging/ @DataDog/apm-java diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt index bd567a67854..4b96de6b26d 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt @@ -5,6 +5,7 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity @@ -20,12 +21,17 @@ abstract class GenerateKnownTagsTask : DefaultTask() { @get:PathSensitive(PathSensitivity.NONE) abstract val tagConventionsFile: RegularFileProperty + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.NONE) + abstract val tracerOverlayFile: RegularFileProperty + @get:OutputDirectory abstract val destinationDirectory: DirectoryProperty @TaskAction fun generate() { val outDir = destinationDirectory.get().asFile - TagRegistryGenerator.generate(tagConventionsFile.get().asFile, outDir) + TagRegistryGenerator.generate(tagConventionsFile.get().asFile, outDir, tracerOverlayFile.orNull?.asFile) logger.lifecycle("tag-registry: generated -> $outDir") } } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt index 1f808d8d357..a68d39d6701 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt @@ -6,8 +6,9 @@ import java.util.Locale /** * Emits the generated `KnownTags.java` from a [TagRegistry]. Public API first — per-tag * `_NAME` (string) + `_ID` (encoded long, literal) couplets with a trailing `// makeTagId(...)` - * derivation comment — then the package-private `_SERIAL_NUM` constants, the - * `StringIndex.EmbeddingSupport` keyOf table and the resolver's name switches. + * derivation comment — then the `_SERIAL_NUM` constants, the + * `StringIndex.EmbeddingSupport` keyOf table, the `NAMES_BY_SERIAL` array behind `nameOf`, and the + * resolver's `openTelemetryNameOf` switch. */ object KnownTagsEmitter { @@ -39,35 +40,25 @@ object KnownTagsEmitter { return u } - val nameOfConst = HashMap() - val idOfConst = HashMap() - val serialOfConst = HashMap() - val otelNameOfConst = HashMap() + val nameOfConst = HashMap() + val idOfConst = HashMap() + val serialOfConst = HashMap() + val otelNameOfConst = HashMap() for (t in reg.tags) { val base = sanitize(t.name) - // Shared Datadog names get no NAME constant because they identify several tags. - // Callers use a direction-specific ID (e.g. PEER_PORT_OUTBOUND_ID); nameOf emits the - // shared Datadog name as a literal. - if (t.sharedNameDirection == null) nameOfConst[t.identity] = unique(withSuffix(base, "_NAME")) - idOfConst[t.identity] = unique(withSuffix(base, "_ID")) - serialOfConst[t.identity] = unique(withSuffix(base, "_SERIAL_NUM")) + nameOfConst[t.ddName] = unique(withSuffix(base, "_NAME")) + idOfConst[t.ddName] = unique(withSuffix(base, "_ID")) + serialOfConst[t.ddName] = unique(withSuffix(base, "_SERIAL_NUM")) // Suffix the pre-suffix base (not nameC), same as the other three: suffixing an // already-suffixed identifier would produce a redundant compound like NAME_OTEL_NAME. - if (t.otelName != null) otelNameOfConst[t.identity] = unique(withSuffix(base, "_OTEL_NAME")) + if (t.otelName != null) otelNameOfConst[t.ddName] = unique(withSuffix(base, "_OTEL_NAME")) } - fun nameC(identity: TagConventions.TagIdentity) = nameOfConst.getValue(identity) - - // The expression nameOf returns: the NAME constant, or the shared name's literal. - fun nameExpr(t: TagRegistry.Tag) = nameOfConst[t.identity] ?: "\"${escape(t.ddName)}\"" - fun idC(identity: TagConventions.TagIdentity) = idOfConst.getValue(identity) - fun serialC(identity: TagConventions.TagIdentity) = serialOfConst.getValue(identity) - fun otelNameC(identity: TagConventions.TagIdentity) = otelNameOfConst.getValue(identity) - - val order = reg.tags.map { it.identity } // stable emit order - // keyOf has no direction argument, so omit shared names and let them resolve to 0 (unknown). - val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null }.map { it.identity } - // canonical name -> OpenTelemetry name, for the reverse (openTelemetryNameOf) switch. - val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.identity to it } }.toMap() + fun nameC(ddName: String) = nameOfConst.getValue(ddName) + fun idC(ddName: String) = idOfConst.getValue(ddName) + fun serialC(ddName: String) = serialOfConst.getValue(ddName) + fun otelNameC(ddName: String) = otelNameOfConst.getValue(ddName) + + val keyOfOrder = reg.tags.map { it.ddName } // stable emit order return buildString { // Public API first (name + encoded id couplets), so readers see the useful parts up top; the // serial ids and keyOf/resolver machinery follow below. Derivation is in the trailing comment. @@ -86,37 +77,35 @@ object KnownTagsEmitter { ) for (t in reg.tags) { - val direction = t.sharedNameDirection - if (direction == null) { - appendLine(" public static final String ${nameC(t.identity)} = \"${escape(t.ddName)}\";") - } else { - appendLine( - " /** {@code ${escape(t.ddName)}} on ${direction.yamlKey} spans. This ID identifies the direction; " + - "the Datadog name is shared across directions. */" - ) - } - appendLine(" public static final long ${idC(t.identity)} = ${hex(t.id)};") + appendLine(" public static final String ${nameC(t.ddName)} = \"${escape(t.ddName)}\";") + appendLine(" public static final long ${idC(t.ddName)} = ${hex(t.id)};") if (t.otelName != null) { - appendLine(" public static final String ${otelNameC(t.identity)} = \"${escape(t.otelName)}\";") + appendLine(" public static final String ${otelNameC(t.ddName)} = \"${escape(t.otelName)}\";") } append("// makeTagId(serial=${t.serial})") if (t.traceLevel) append(" + trace-level") + if (t.intercepted) append(" + intercepted") if (t.otelName != null) append(" -> ${escape(t.otelName)}") + for ((direction, otel) in t.scopedOtelNames) append(" -> ${escape(otel)} (${direction.yamlKey})") appendLine(" <${escape(t.required)}>") appendLine() } - // Serial numbers (globalSerial per tag) — package-private, consumed by the resolver switch. + // Serial numbers (globalSerial per tag) — public, so a switch on KnownTagCodec.serialNum outside + // this package (the resolver here, TagInterceptor in core) can name them as case labels. appendLine(" // ---- serial numbers ----") for (t in reg.tags) { - appendLine(" static final int ${serialC(t.identity)} = ${t.serial};") + appendLine(" public static final int ${serialC(t.ddName)} = ${t.serial};") } + // Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag. + val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1 + appendLine(" public static final int SERIAL_LIMIT = $serialLimit;") // OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all // canonical names. Sort by OTel name to keep output deterministic. val otelByCanonical = reg.tags - .mapNotNull { t -> t.otelName?.let { it to t.identity } } + .mapNotNull { t -> t.otelName?.let { it to t.ddName } } .sortedBy { it.first } // keyOf table (open-addressed, via StringIndex.EmbeddingSupport). Canonical names first, then @@ -163,6 +152,21 @@ object KnownTagsEmitter { KEYOF_IDS = ids; } + /** + * Each tag's Datadog name, indexed by serial; slot 0 (no tag) is null. An array rather than a + * switch keeps nameOf small enough to inline at every caller. + */ + static final String[] NAMES_BY_SERIAL = { + null, + """.trimIndent() + ) + for (t in reg.tags.sortedBy { it.serial }) { + appendLine(" ${nameC(t.ddName)},") + } + appendLine( + """ + }; + /** * The registry's name↔id tables, as a {@link KnownTagCodec.Resolver}. {@code KnownTagCodec} * reads this field from its own holder, so the two classes complete each other: the codec owns @@ -172,39 +176,33 @@ object KnownTagsEmitter { new KnownTagCodec.Resolver() { @Override public String nameOf(long tagId) { - switch (KnownTagCodec.serialNum(tagId)) { - """.trimIndent() - ) - for (t in reg.tags) { - appendLine( - """ - case ${serialC(t.identity)}: - return ${nameExpr(t)}; - """.trimIndent() - ) - } - // openTelemetryNameOf: canonical id -> OTel-namespace name, null when the tag has none. The - // caller (a serializer) owns any fall-back-to-Datadog-name policy; this stays a pure lookup. - appendLine( - """ - default: - return null; - } + int serial = KnownTagCodec.serialNum(tagId); + return serial < NAMES_BY_SERIAL.length ? NAMES_BY_SERIAL[serial] : null; } + // openTelemetryNameOf: id -> OTel-namespace name on spans of a direction, null when + // the tag has none there. The caller (a serializer) owns any fall-back-to-Datadog-name + // policy. @Override - public String openTelemetryNameOf(long tagId) { + public String openTelemetryNameOf(long tagId, int direction) { switch (KnownTagCodec.serialNum(tagId)) { """.trimIndent() ) - for (identity in order) { - if (otelName[identity] == null) continue - appendLine( - """ - case ${serialC(identity)}: - return ${otelNameC(identity)}; - """.trimIndent() - ) + for (t in reg.tags) { + val result = + if (t.otelName != null) { + otelNameC(t.ddName) + } else if (t.scopedOtelNames.isNotEmpty()) { + // Renamed only on spans of some directions: elsewhere, and while the direction is + // unknown, the tag keeps its Datadog name. + t.scopedOtelNames.entries.reversed().fold("null") { rest, (direction, otel) -> + "direction == KnownTagCodec.DIRECTION_${direction.name} ? \"${escape(otel)}\" : $rest" + } + } else { + continue + } + appendLine("case ${serialC(t.ddName)}:") + appendLine(" return $result;") } appendLine( """ diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt index 54fef064dfb..27ca3cfba0f 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt @@ -9,6 +9,8 @@ class TagConventions private constructor( private val spanTypes: Map, private val mixins: Map, private val traceLevel: List, + /** Datadog names the tracer intercepts on the set-path, from the tracer overlay. */ + val interceptedNames: Set = emptySet(), ) { /** * A tag's identity: its Datadog name, plus the direction when that name is declared once per @@ -59,13 +61,9 @@ class TagConventions private constructor( val name: String get() = identity.label - /** The Datadog-namespace name, shared by the tags of a name declared per direction. */ + /** The Datadog-namespace name, shared by the declarations of a name declared per direction. */ val ddName: String get() = identity.ddName - - /** The direction of a tag declared per direction, or null for every other tag. */ - val sharedNameDirection: Direction? - get() = identity.direction } /** @@ -241,6 +239,16 @@ class TagConventions private constructor( /** The type's own or nearest inherited `span-kind` direction, or null when none is declared. */ private fun directionOf(spanTypes: Map, st: SpanType): Direction? = chainOf(spanTypes, st.name).firstNotNullOfOrNull { it.direction } + /** The tracer overlay's `tags`, as a mixin that applies to no span type; empty when none. */ + private fun overlayMixin(overlay: Map): Map { + val tags = tagList(overlay["tags"]) + if (tags.isEmpty()) return emptyMap() + return mapOf( + OVERLAY_MIXIN to + Mixin(name = OVERLAY_MIXIN, appliesAll = false, appliesTo = emptySet(), tags = tags, refs = emptyList()) + ) + } + /** Reads `span-kind` as the direction it sets, or null when absent. */ private fun parseDirection(m: Map, owner: String): Direction? { val spanKind = m["span-kind"] @@ -250,11 +258,31 @@ class TagConventions private constructor( return (spanKind as String?)?.let { SPAN_KIND_DIRECTIONS.getValue(it) } } + /** The synthetic mixin holding the tracer overlay's tags; it applies to no span type. */ + const val OVERLAY_MIXIN = "tracer overlay" + + /** + * Parses [root], the language-agnostic conventions, plus [overlay], this tracer's own set-path + * routing: `tags` declares keys that exist only to be routed (e.g. `resource.name`), so each is + * intercepted, and `intercepted` adds the tags declared in [root] that the tracer also routes. + */ @Suppress("UNCHECKED_CAST") - fun parse(root: Map): TagConventions { + fun parse(root: Map, overlay: Map = emptyMap()): TagConventions { for (section in listOf("span_types", "mixins", "trace_level")) { require(root[section] == null || root[section] is Map<*, *>) { "$section must be a mapping" } } + require(overlay.keys.all { it == "tags" || it == "intercepted" }) { + "the tracer overlay may only declare `tags` and `intercepted`, not ${overlay.keys - setOf("tags", "intercepted")}" + } + require(refList(overlay["tags"]).isEmpty()) { "tracer overlay tags must be declarations, not refs" } + val interceptedRaw = overlay["intercepted"] + require(interceptedRaw == null || (interceptedRaw is List<*> && interceptedRaw.all { it is String })) { + "intercepted must be a list of Datadog tag names" + } + // An overlay tag exists only to be routed, so it is intercepted without being listed. + val intercepted = + ((interceptedRaw as? List) ?: emptyList()).toSet() + + tagList(overlay["tags"]).map { it.ddName } val spanTypesRaw = (root["span_types"] as? Map) ?: emptyMap() val parsedSpanTypes = spanTypesRaw.mapValues { (name, v) -> @@ -298,7 +326,7 @@ class TagConventions private constructor( refs = refList(m["tags"]), direction = parseDirection(m, "mixin '$name'"), ) - } + } + overlayMixin(overlay) for (spanType in parsedSpanTypes.values) { for (included in spanType.include) { @@ -342,7 +370,12 @@ class TagConventions private constructor( ) } validateOtelNameScope(spanTypes, mixins, traceLevel) - return TagConventions(spanTypes, mixins, traceLevel) + val conv = TagConventions(spanTypes, mixins, traceLevel, intercepted) + val declared = conv.allDeclaredTags().map { it.ddName }.toSet() + for (name in intercepted) { + require(name in declared) { "intercepted tag '$name' is not declared" } + } + return conv } /** diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index 664ee9f6687..248471c228a 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -4,78 +4,82 @@ package datadog.buildlogic.tagRegistry * Assigns tag ids from a parsed [TagConventions]. The id encoding mirrors KnownTagCodec: [63-48 * serial][47-32 reserved][31-0 flags]. * - *

An id is IDENTITY only: a globally unique serial plus the trace-level classification bit. It + *

An id is a globally unique serial plus two classification bits: trace-level and intercepted. It * carries no storage-layout coordinate -- bits [47-32] are held vacant for the co-occurrence slot * that the dense tag store assigns by graph coloring, which lands with the dense store itself. * Nothing here needs to know how (or whether) a tag is stored. * - *

Nor does anything here know how a tag is SET. Whether the tracer intercepts a tag on the - * set-path (routing it to a span field or a sampling directive instead of tag storage) is a - * property of TagInterceptor, not of the tag's identity, and modelling it was the source of a whole - * class of drift between this registry and the interceptor's actual switch. It arrives with the - * work that consumes it -- the id->handler dispatch table that retires TagInterceptor -- where the - * interceptor can be the authority. Re-adding a classification bit then is purely additive. + *

The intercepted bit marks a tag the tracer may route on the set-path (to a span field or a + * sampling directive instead of tag storage), as listed by the tracer overlay. A setter called with + * a constant id then folds the interception test away. TagInterceptor's switch stays the authority + * on what each tag does; a test there keeps the bit and the switch in agreement. + * + *

There is one tag, and one id, per Datadog name. A name the conventions declare once per + * direction, as `peer.port` is, is still one tag: only its OpenTelemetry name depends on the span's + * direction, so direction is applied when a tag is named, never when it is stored. */ class TagRegistry private constructor(val tags: List) { data class Tag( - val identity: TagConventions.TagIdentity, + val ddName: String, val type: String, val required: String, val serial: Int, val traceLevel: Boolean, + val intercepted: Boolean, val id: Long, - /** The tag's OpenTelemetry name, in [otelDirection] or every direction; null when not renamed. */ - val declaredOtelName: String? = null, - /** - * The one direction [declaredOtelName] applies in, or null when it applies in every direction. A - * tag has one declaration, so its rename covers either every direction or exactly one. - */ - val otelDirection: TagConventions.Direction? = null, + /** The tag's OpenTelemetry name in every direction; null when not renamed, or renamed per direction. */ + val otelName: String? = null, + /** The tag's OpenTelemetry name on spans of each direction that renames it, when [otelName] is null. */ + val scopedOtelNames: Map = emptyMap(), ) { - /** The identity's label, as reports and constant names show it. */ - val name: String = identity.label - val ddName: String = identity.ddName - val sharedNameDirection: TagConventions.Direction? = identity.direction - - /** - * The rename included in the direction-free lookup tables, or null when absent or scoped - * to one direction. Scoped renames remain in `declaredOtelName` for later resolution. - */ - val otelName: String? = declaredOtelName.takeIf { otelDirection == null } + /** The tag as reports and generated constant names show it. */ + val name: String + get() = ddName } companion object { const val FIRST_SERIAL = 1 + const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE /** - * Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() -- must stay in sync. LEVEL_TRACE at - * bit 2, other low bits and the reserved [47-32] window zero. + * Mirrors KnownTagCodec.makeTagId(serial) + intercepted() + traceLevel() -- must stay in sync. + * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits and the reserved [47-32] window + * zero. */ - fun encode(serial: Int, traceLevel: Boolean): Long { + fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long { var id = serial.toLong() shl 48 if (traceLevel) id = id or LEVEL_TRACE + if (intercepted) id = id or INTERCEPTED return id } fun build(conv: TagConventions): TagRegistry { - val traceLevel = conv.traceLevelTags().map { it.identity }.toSet() - val renames = conv.otelMappings().associateBy { it.tag } + val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet() + val renames = conv.otelMappings().groupBy { it.tag.ddName } - // Stable order (by name) so serials -- and therefore ids -- are a pure function of the input. + // One tag per Datadog name, in a stable order (by name) so serials -- and therefore ids -- are + // a pure function of the input. A name's declarations per direction merge into its one tag. val tags = - conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t -> + conv.allDeclaredTags().groupBy { it.ddName }.toSortedMap().values.mapIndexed { i, decls -> + val first = decls.first() + require(decls.all { it.type == first.type }) { + "'${first.ddName}' is declared per direction with different types: ${decls.map { it.type }}" + } val serial = FIRST_SERIAL + i - val isTraceLevel = t.identity in traceLevel + val isTraceLevel = first.ddName in traceLevel + val isIntercepted = first.ddName in conv.interceptedNames + val (otelName, scoped) = otelNamesOf(first.ddName, renames[first.ddName].orEmpty()) Tag( - t.identity, - t.type, - t.required, + first.ddName, + first.type, + strongestRequirement(decls.map { it.required }), serial, isTraceLevel, - id = encode(serial, isTraceLevel), - declaredOtelName = renames[t.identity]?.otelName, - otelDirection = renames[t.identity]?.direction, + isIntercepted, + id = encode(serial, isTraceLevel, isIntercepted), + otelName = otelName, + scopedOtelNames = scoped, ) } @@ -83,6 +87,38 @@ class TagRegistry private constructor(val tags: List) { return TagRegistry(tags) } + /** + * A tag's renames, as one name for every direction or one name per direction. The rename of a + * declaration made per direction applies in that declaration's direction. + */ + private fun otelNamesOf( + ddName: String, + mappings: List, + ): Pair> { + val everywhere = mappings.filter { (it.direction ?: it.tag.direction) == null }.map { it.otelName }.distinct() + require(everywhere.size <= 1) { "'$ddName' has several direction-free OpenTelemetry names: $everywhere" } + if (everywhere.isNotEmpty()) { + require(mappings.all { it.otelName == everywhere[0] }) { + "'$ddName' has a direction-free OpenTelemetry name and a different one per direction" + } + return everywhere[0] to emptyMap() + } + val scoped = sortedMapOf() + for (m in mappings) { + val direction = m.direction ?: m.tag.direction!! + val prev = scoped.put(direction, m.otelName) + require(prev == null || prev == m.otelName) { + "'$ddName' has two OpenTelemetry names on ${direction.yamlKey} spans: '$prev' and '${m.otelName}'" + } + } + return null to scoped + } + + private val REQUIREMENT_STRENGTH = listOf("required", "conditional", "recommended", "optional") + + /** The strongest of a name's requirement levels across its declarations per direction. */ + private fun strongestRequirement(levels: List): String = levels.minBy { REQUIREMENT_STRENGTH.indexOf(it).let { i -> if (i < 0) Int.MAX_VALUE else i } } + /** * Rejects OpenTelemetry names that collide with any Datadog name. Within each direction, * a rename must also belong to a single tag. Direction-free renames reserve their name @@ -93,8 +129,9 @@ class TagRegistry private constructor(val tags: List) { val canonical = tags.map { it.ddName }.toSet() val owner = HashMap, String>() for (t in tags) { - val otel = t.declaredOtelName ?: continue - for (direction in t.otelDirection?.let { listOf(it) } ?: TagConventions.Direction.entries) { + val byDirection = + t.otelName?.let { otel -> TagConventions.Direction.entries.associateWith { otel } } ?: t.scopedOtelNames + for ((direction, otel) in byDirection) { require(otel !in canonical) { "OpenTelemetry name '$otel' (of '${t.name}') collides with canonical tag name '$otel'" } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt index f6d07ca586b..1193de5c3cc 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt @@ -16,6 +16,12 @@ abstract class TagRegistryExtension @Inject constructor( */ abstract val tagConventionsFile: RegularFileProperty + /** + * Optional tracer overlay: this tracer's set-path routing (intercepted tags, and keys that exist + * only to be routed), kept out of the language-agnostic conventions. + */ + abstract val tracerOverlayFile: RegularFileProperty + /** * Destination of the generated sources, by convention under `build/generated/tag-registry`. */ diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index 9b69f0fe0b0..eaae7ccc1a6 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -8,17 +8,19 @@ import java.util.Locale /** Emits the Java tag registry and reports in a deterministic order. */ object TagRegistryGenerator { - /** Parses the conventions YAML and writes the full generated tree under [outDir]. */ - fun generate(tagConventionsFile: File, outDir: File) { + /** + * Parses the conventions YAML, plus the tracer overlay when there is one, and writes the full + * generated tree under [outDir]. + */ + fun generate(tagConventionsFile: File, outDir: File, tracerOverlayFile: File? = null) { val mapper = ObjectMapper(YAMLFactory()) - val domain: Map = - tagConventionsFile.inputStream().use { - mapper.readValue(it, object : TypeReference>() {}) - } + fun read(file: File): Map = file.inputStream().use { mapper.readValue(it, object : TypeReference>() {}) } ?: emptyMap() + val domain = read(tagConventionsFile) + val overlay = tracerOverlayFile?.let(::read) ?: emptyMap() // Validate before touching the destination tree: an invalid domain model must fail loudly, // not after the previous (valid) generated output has already been wiped out. - val conv = TagConventions.parse(domain) + val conv = TagConventions.parse(domain, overlay) val reg = TagRegistry.build(conv) // Remove obsolete generated files when the output changes. @@ -68,18 +70,19 @@ object TagRegistryGenerator { """ # Tag id assignment. tags=${reg.tags.size} - # TAGS serial lvl id required name + # TAGS serial lvl int id required name """.trimIndent() ) - for ((name, _, required, serial, traceLevel, id) in reg.tags) { + for (t in reg.tags) { appendLine( - " %6d %s %-18s %-12s %s".format( + " %6d %s %s %-18s %-12s %s".format( Locale.ROOT, - serial, - if (traceLevel) "T" else "-", - "0x%016X".format(Locale.ROOT, id), - required, - name + t.serial, + if (t.traceLevel) "T" else "-", + if (t.intercepted) "I" else "-", + "0x%016X".format(Locale.ROOT, t.id), + t.required, + t.name ) ) } @@ -98,23 +101,16 @@ object TagRegistryGenerator { """ # DIRECTION-SCOPED OPENTELEMETRY NAMES. Each applies only on spans of the given direction, so - # it is not in the tables above; name resolution does not use it until it knows the direction. + # it is not in the tables above: a tag is emitted under it when its span has that direction, + # but name resolution does not use it until it knows the direction. """.trimIndent() ) val scoped = - reg.tags.filter { it.otelDirection != null }.sortedWith(compareBy({ it.declaredOtelName }, { it.otelDirection })) - for (t in scoped) { - appendLine(" %-30s %-9s -> %s".format(Locale.ROOT, t.declaredOtelName, t.otelDirection!!.yamlKey, t.name)) - } - appendLine( - """ - - # SHARED DATADOG NAMES. One Datadog name for a tag per direction: emitting it needs no context, - # but resolving the name to a tag needs the span's direction, so keyOf does not resolve it yet. - """.trimIndent() - ) - for ((ddName, shared) in reg.tags.filter { it.sharedNameDirection != null }.groupBy { it.ddName }.toSortedMap()) { - appendLine(" %-30s -> %s".format(Locale.ROOT, ddName, shared.joinToString(", ") { it.name })) + reg.tags + .flatMap { t -> t.scopedOtelNames.map { (direction, otel) -> Triple(otel, direction, t.name) } } + .sortedWith(compareBy({ it.first }, { it.second })) + for ((otel, direction, canonical) in scoped) { + appendLine(" %-30s %-9s -> %s".format(Locale.ROOT, otel, direction.yamlKey, canonical)) } } } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt index c350d3c302e..a93b92df9f7 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt @@ -16,6 +16,7 @@ class TagRegistryGeneratorPlugin : Plugin { group = "build" description = "Generates the Java tag registry and assignment reports." tagConventionsFile.convention(ext.tagConventionsFile) + tracerOverlayFile.convention(ext.tracerOverlayFile) destinationDirectory.convention(ext.destinationDirectory) } project.pluginManager.withPlugin("java") { diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt index fa08bc5b177..3b676ad52e4 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt @@ -247,9 +247,13 @@ class TagRegistryGeneratorPluginTest { public final class KnownTagCodec { public interface Resolver { String nameOf(long id); - String openTelemetryNameOf(long id); + String openTelemetryNameOf(long id, int direction); long keyOf(String name); } + public static final int DIRECTION_UNKNOWN = -1; + public static final int DIRECTION_INBOUND = 0; + public static final int DIRECTION_OUTBOUND = 1; + public static final int DIRECTION_NONE = 2; public static int serialNum(long id) { return (int) (id >>> 48); } } """ diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index 1ff22853571..a88f7509144 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -293,7 +293,10 @@ class TagRegistryGeneratorTest { TagRegistryGenerator.generate(yaml, output) val generated = contents(output) - assertThat(generated.getValue("java/datadog/trace/api/KnownTags.java")).doesNotContain("server.address") + val source = generated.getValue("java/datadog/trace/api/KnownTags.java") + assertThat(source).doesNotContain("HTTP_HOSTNAME_OTEL_NAME") + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).doesNotContain("server.address") + assertThat(source).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") assertThat(generated.getValue("tag-assignment.txt")) .containsPattern("server\\.address +inbound +-> http\\.hostname") } @@ -324,23 +327,21 @@ class TagRegistryGeneratorTest { val tags = TagRegistry.build(conventions).tags.associateBy { it.name } - assertThat(tags.getValue("http.hostname").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.address" to TagConventions.Direction.INBOUND) - assertThat(tags.getValue("peer.hostname").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.address" to TagConventions.Direction.OUTBOUND) - assertThat(tags.getValue("peer.port@outbound").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.port" to TagConventions.Direction.OUTBOUND) - assertThat(tags.getValue("peer.port@inbound").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("client.port" to TagConventions.Direction.INBOUND) - assertThat(tags.values.filter { it.name.startsWith("peer.port") }.map { it.ddName }) - .containsOnly("peer.port") + assertThat(tags.getValue("http.hostname").scopedOtelNames) + .isEqualTo(mapOf(TagConventions.Direction.INBOUND to "server.address")) + assertThat(tags.getValue("peer.hostname").scopedOtelNames) + .isEqualTo(mapOf(TagConventions.Direction.OUTBOUND to "server.address")) + assertThat(tags.getValue("peer.port").scopedOtelNames) + .isEqualTo( + mapOf(TagConventions.Direction.INBOUND to "client.port", TagConventions.Direction.OUTBOUND to "server.port") + ) assertThat(tags.values.map { it.otelName }).containsOnlyNulls() assertThat(conventions.resolve("http.client").map { it.name }).contains("peer.port@outbound") assertThat(conventions.resolve("http.server").map { it.name }).contains("peer.port@inbound") } @Test - fun `a Datadog name declared per direction is not resolvable without a direction`() { + fun `a Datadog name declared per direction is one tag`() { val yaml = directory.conventionsFile( """ span_types: @@ -358,12 +359,43 @@ class TagRegistryGeneratorTest { val generated = contents(output) val source = generated.getValue("java/datadog/trace/api/KnownTags.java") assertThat(source) - .contains("PEER_PORT_INBOUND_ID", "PEER_PORT_OUTBOUND_ID") - .contains("{@code peer.port} on outbound spans.", "{@code peer.port} on inbound spans.") - .doesNotContain("PEER_PORT_NAME", "PEER_PORT_INBOUND_NAME", "PEER_PORT_OUTBOUND_NAME") - assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).doesNotContain("PEER_PORT") - assertThat(generated.getValue("tag-assignment.txt")) - .containsPattern("peer\\.port +-> peer\\.port@inbound, peer\\.port@outbound") + .contains("PEER_PORT_NAME = \"peer.port\"", "PEER_PORT_ID") + .doesNotContain("PEER_PORT_INBOUND", "PEER_PORT_OUTBOUND", "shared") + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).contains("PEER_PORT_NAME,") + assertThat(source.substringAfter("KEYOF_VALUES = {").substringBefore("};")).contains("PEER_PORT_ID,") + assertThat(generated.getValue("tag-assignment.txt").lines().filter { it.endsWith(" peer.port") }) + .hasSize(1) + } + + @Test + fun `a tag declared per direction is emitted under the OpenTelemetry name for the direction`() { + val yaml = directory.conventionsFile( + """ + span_types: + http.client: {span-kind: client, include: [outbound_peer]} + http.server: + span-kind: server + include: [inbound_peer] + tags: [{dd-name: http.hostname, otel-name: server.address}] + mixins: + outbound_peer: {span-kind: client, tags: [{dd-name: peer.port, type: int, otel-name: server.port}]} + inbound_peer: {span-kind: server, tags: [{dd-name: peer.port, type: int, otel-name: client.port}]} + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + val emit = source.substringAfter("public String openTelemetryNameOf(long tagId, int direction)") + assertThat(emit).contains( + "return direction == KnownTagCodec.DIRECTION_INBOUND ? \"client.port\" : " + + "direction == KnownTagCodec.DIRECTION_OUTBOUND ? \"server.port\" : null;" + ) + assertThat(emit).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") + // Direction-scoped names are not resolved by name yet. + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")) + .doesNotContain("client.port", "server.port", "server.address") } @Test @@ -396,7 +428,7 @@ class TagRegistryGeneratorTest { val dbType = TagRegistry.build(conventions).tags.single() assertThat(dbType.otelName).isEqualTo("db.system") - assertThat(dbType.otelDirection).isNull() + assertThat(dbType.scopedOtelNames).isEmpty() } @TableTest( @@ -437,12 +469,13 @@ class TagRegistryGeneratorTest { """ ) - assertThat(TagRegistry.build(conventions).tags.map { it.identity }) + assertThat(conventions.allDeclaredTags().map { it.identity }) .containsExactlyInAnyOrder( TagConventions.TagIdentity("x", TagConventions.Direction.INBOUND), TagConventions.TagIdentity("x", TagConventions.Direction.OUTBOUND), TagConventions.TagIdentity("x@inbound"), ) + assertThat(TagRegistry.build(conventions).tags.map { it.ddName }).containsExactly("x", "x@inbound") } @Test @@ -460,7 +493,7 @@ class TagRegistryGeneratorTest { ) assertThat(TagRegistry.build(conventions).tags.map { it.name }) - .containsExactlyInAnyOrder("peer.port@inbound", "peer.port@outbound", "user@domain") + .containsExactlyInAnyOrder("peer.port", "user@domain") } @Test @@ -478,6 +511,56 @@ class TagRegistryGeneratorTest { .withMessageContaining("span-kind-neutral without an otel-name") } + @Test + fun `the tracer overlay declares routed keys and marks intercepted tags`() { + val yaml = directory.conventionsFile( + """ + span_types: + base: + abstract: true + tags: [{dd-name: service, otel-name: service.name}, {dd-name: component}] + """ + ) + val overlay = directory.writeFile( + "overlay.yaml", + """ + tags: [{dd-name: resource.name}] + intercepted: [service] + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output, overlay) + + val knownTags = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + assertThat(knownTags) + .contains( + "COMPONENT_ID = 0x0001000000000000L", + "RESOURCE_NAME_ID = 0x0002000000000002L", + "SERVICE_ID = 0x0003000000000002L", + "public static final int SERVICE_SERIAL_NUM = 3;", + "public static final int SERIAL_LIMIT = 4;" + ) + } + + @TableTest( + """ + scenario | overlay | message + undeclared name | 'intercepted: [missing]' | is not declared + ref in overlay | 'tags: [{ref: service}]' | must be declarations, not refs + unknown section | 'span_types: {}' | may only declare + duplicate of a tag | 'tags: [{dd-name: service}]' | declared in both + """ + ) + fun `invalid tracer overlays are rejected`(overlay: String, message: String) { + val yaml = directory.conventionsFile("span_types: {base: {tags: [{dd-name: service}]}}") + val overlayFile = directory.writeFile("overlay.yaml", overlay) + + assertThatIllegalArgumentException() + .isThrownBy { TagRegistryGenerator.generate(yaml, File(directory, "generated"), overlayFile) } + .withMessageContaining(message) + } + private fun tagConventions(@Language("yaml") yamlText: String) = TagConventions.parse( ObjectMapper(YAMLFactory()).readValue( directory.conventionsFile(yamlText), diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java new file mode 100644 index 00000000000..a9d1c25d13e --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java @@ -0,0 +1,107 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import datadog.trace.api.KnownTags; +import datadog.trace.api.TagMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Measures {@code DDSpan.setTag} on a live span, overwriting one tag per call, by how the tag is + * named: a constant id ({@code knownTag_byId}, {@code knownIntTag_byId}), a constant intercepted id + * ({@code interceptedTag_byId}, {@code span.kind}), a known tag's name, and a custom name. + * + *

Controls: {@code tagMap_byId} is the same store on a bare {@link TagMap}, with no span, lock, + * or interceptor; {@code tagMap_byId_synchronized} adds an uncontended lock, as the span takes one; + * {@code knownTag_byNonConstantId} reads the id from a non-final field, which C2 never + * constant-folds. + * + *

With a constant id, the interception test folds to the id's INTERCEPTED bit and there is no + * custom-tag path, so comparing {@code knownTag_byId} with {@code tagMap_byId_synchronized} + * isolates the span's own dispatch. The name variants add a registry lookup. + */ +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@Fork(value = 3) +public class SetTagBenchmark { + static final CoreTracer TRACER = CoreTracer.builder().build(); + + private DDSpan span; + private TagMap tagMap; + private long nonConstantTagId; + + @Setup + public void setup() { + span = (DDSpan) TRACER.startSpan("benchmark", "set.tag"); + tagMap = TagMap.create(); + nonConstantTagId = KnownTags.HTTP_ROUTE_ID; + } + + @TearDown + public void tearDown() { + span.finish(); + } + + @Benchmark + public Object tagMap_byId() { + tagMap.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + return tagMap; + } + + @Benchmark + public Object tagMap_byId_synchronized() { + TagMap tagMap = this.tagMap; + synchronized (tagMap) { + tagMap.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + } + return tagMap; + } + + @Benchmark + public Object knownTag_byNonConstantId() { + return span.setTag(nonConstantTagId, "/users/{id}"); + } + + @Benchmark + public Object knownTag_byId() { + return span.setTag(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + } + + @Benchmark + public Object interceptedTag_byId() { + return span.setTag(KnownTags.SPAN_KIND_ID, "client"); + } + + @Benchmark + public Object knownTag_byName() { + return span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}"); + } + + @Benchmark + public Object customTag_byName() { + return span.setTag("my.custom.tag", "/users/{id}"); + } + + @Benchmark + public Object knownIntTag_byId() { + return span.setTag(KnownTags.HTTP_RESEND_COUNT_ID, 2); + } + + @Benchmark + public Object knownIntTag_byName() { + return span.setTag(KnownTags.HTTP_RESEND_COUNT_NAME, 2); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 584b1ddc8d1..9281c377afb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -617,9 +617,9 @@ public void setSpanType(final CharSequence spanType) { * #applyOverwriting} for the decorator {@code afterStart} seam (via {@link * DDSpan#applyOverwriting}), which needs different precedence. The context owns the tag map, so * the eventual cheaper bulk-share path (skipping interception for non-intercepted tags) and the - * identity short-circuit will land here -- deferred to the dense-store / tag-registry work, which - * exposes intercept status at the internal-api level. Until then the constant tags route through - * the interceptor, identical to the per-tag calls this replaces. + * identity short-circuit will land here, with the dense store; a tag's intercept status is + * already on its id ({@link KnownTagCodec#isIntercepted}). Until then each constant tag is + * prechecked by the interceptor, identical to the per-tag calls this replaces. */ public void apply(@Nonnull final SpanPrototype prototype) { if (this.spanType == null) { @@ -674,8 +674,7 @@ private void seedAbsentTags(final TagMap map) { if (ctx.unsafeTags.containsKey(tag)) { return; } - final Object value = tagEntry.objectValue(); - if (!ctx.tagInterceptor.interceptTag(ctx, tag, value)) { + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { ctx.unsafeTags.set(tagEntry); } }); @@ -1023,11 +1022,13 @@ public void removeTag(String tag) { } /** - * Removes a known tag by id, rather than by its name, which need not resolve back to the id. An - * id that names no known tag is ignored, like the id-keyed setters do. + * Removes a known tag by id rather than by its name. An id the id-keyed setters ignore is ignored + * here too. + * + * @param tagId a {@code KnownTags.*_ID} constant */ public void removeTag(long tagId) { - if (KnownTagCodec.nameOf(tagId) == null) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagId == KnownTags.SPAN_KIND_ID) { @@ -1044,6 +1045,9 @@ public void removeTag(long tagId) { *

Existing tag value with the same value will be replaced. Setting a tag with a {@code null} * value will remove the tag from the span. * + *

A known tag's name is resolved to its id once, and the id-keyed setter does the rest; only a + * custom tag is handled by name. + * * @param tag The tag name. * @param value The nullable tag value. */ @@ -1051,9 +1055,12 @@ public void setTag(final String tag, final Object value) { if (null == tag) { return; } - if (null == value) { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1064,9 +1071,12 @@ public void setTag(final String tag, final String value) { if (null == tag) { return; } - if (null == value) { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1074,104 +1084,99 @@ public void setTag(final String tag, final String value) { } /* - * Id-keyed setters, mirroring the String setters above: interception still runs on the tag's - * name (TagInterceptor is name-keyed), but a stored tag is set by id, skipping the name lookup. - * An id that names no known tag is ignored, like a null tag. + * Id-keyed setters, mirroring the String setters above. With a constant id, the + * KnownTagCodec.INTERCEPTED test folds, leaving only the split-by-tags table load; an id set never + * takes the custom-tag path. An id that names no known tag is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.needsIntercept(tagId) + || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } public void setTag(final long tagId, final CharSequence value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.needsIntercept(tagId) + || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } public void setTag(final long tagId, final boolean value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } public void setTag(final long tagId, final int value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } public void setTag(final long tagId, final long value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } public void setTag(final long tagId, final float value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } public void setTag(final long tagId, final double value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (!KnownTagCodec.isKnown(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1181,11 +1186,8 @@ public void setTag(TagMap.EntryReader entry) { return; } - // pre-check to avoid boxing - boolean intercepted = - precheckIntercept(entry.tag()) - && tagInterceptor.interceptTag(this, entry.tag(), entry.objectValue()); - if (!intercepted) { + // the interceptor prechecks the entry, to avoid boxing + if (!tagInterceptor.interceptTag(this, entry)) { synchronized (unsafeTags) { unsafeTags.set(entry); } @@ -1193,16 +1195,7 @@ public void setTag(TagMap.EntryReader entry) { } /* - * Uses to determine if there's an opportunity to avoid primitve boxing. - * If the underlying map doesn't support efficient primitives, then boxing is used. - * If the tag may be intercepted, then boxing is also used. - */ - private boolean precheckIntercept(String tag) { - return tagInterceptor.needsIntercept(tag); - } - - /* - * Used when precheckIntercept determines that boxing is unavoidable + * Used when the interceptor's precheck says the tag may be intercepted, so boxing is unavoidable * * Either because the tagInterceptor needs to be fully checked (which requires boxing) * In that case, a box has already been created so it makes sense to pass the box @@ -1213,10 +1206,10 @@ private boolean precheckIntercept(String tag) { * The TagMap isn't optimized and will need to box the primitive regardless of * tag interception */ - private void setBox(String tag, Object box) { - if (!tagInterceptor.interceptTag(this, tag, box)) { + private void setBox(long tagId, Object box) { + if (!tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.set(tag, box); + unsafeTags.internals().setKnown(tagId, box); } } } @@ -1225,9 +1218,11 @@ public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1238,9 +1233,11 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1251,9 +1248,11 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1264,9 +1263,11 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1277,9 +1278,11 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1303,10 +1306,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { map.forEach( this, (ctx, tagEntry) -> { - String tag = tagEntry.tag(); - Object value = tagEntry.objectValue(); - - if (!ctx.tagInterceptor.interceptTag(ctx, tag, value)) { + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { ctx.unsafeTags.set(tagEntry); } }); @@ -1333,9 +1333,7 @@ void setAllTags(final TagMap.Ledger ledger) { } else { TagMap.Entry entry = (TagMap.Entry) entryChange; - Object value = entry.objectValue(); - - if (!tagInterceptor.interceptTag(this, tag, value)) { + if (!tagInterceptor.interceptTag(this, entry)) { unsafeTags.set(entry); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java index 32877b15017..1575b26d228 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java @@ -160,12 +160,16 @@ public static void writeSpanLink(JsonWriter writer, AgentSpanLink spanLink) { writer.endObject(); } - private static void writeSpanTag(JsonWriter writer, TagMap.EntryReader tagEntry) { + private static void writeSpanTag( + JsonWriter writer, Integer direction, TagMap.EntryReader tagEntry) { // OTLP is the OpenTelemetry wire format, but the rename itself is opt-in: emit the // OpenTelemetry-namespace name only when OTel semantics are enabled, else the entry's own - // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. + // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. A few + // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = - Config.get().isTraceOtelSemanticsEnabled() ? tagEntry.openTelemetryTag() : tagEntry.tag(); + Config.get().isTraceOtelSemanticsEnabled() + ? tagEntry.openTelemetryTag(direction) + : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: writeAttribute(writer, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue()); @@ -248,7 +252,10 @@ public void accept(Metadata metadata) { writeSpanTag(writer, PROCESS_TAGS_KEY, metadata.processTags()); } - metadata.getTags().forEach(writer, OtlpTraceJson::writeSpanTag); + // The direction is computed once per span; boxing -1..2 hits the Integer cache. + TagMap tags = metadata.getTags(); + Integer direction = KnownTagCodec.directionOf(tags); + tags.forEach(writer, direction, OtlpTraceJson::writeSpanTag); // reset for next span includeProcessTags = false; 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 6aabcbc9428..ab64bde46cd 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 @@ -224,13 +224,17 @@ public static void writeSpanId(StreamingBuffer buf, long spanId) { buf.putLong(spanId); } - private static void writeSpanTag(StreamingBuffer buf, TagMap.EntryReader tagEntry) { + private static void writeSpanTag( + StreamingBuffer buf, Integer direction, TagMap.EntryReader tagEntry) { writeTag(buf, 9, LEN_WIRE_TYPE); // OTLP is the OpenTelemetry wire format, but the rename itself is opt-in: emit the // OpenTelemetry-namespace name only when OTel semantics are enabled, else the entry's own - // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. + // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. A few + // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = - Config.get().isTraceOtelSemanticsEnabled() ? tagEntry.openTelemetryTag() : tagEntry.tag(); + Config.get().isTraceOtelSemanticsEnabled() + ? tagEntry.openTelemetryTag(direction) + : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: writeAttribute(buf, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue()); @@ -325,7 +329,10 @@ public void accept(Metadata metadata) { writeSpanTag(buf, PROCESS_TAGS_KEY, metadata.processTags()); } - metadata.getTags().forEach(buf, OtlpTraceProto::writeSpanTag); + // The direction is computed once per span; boxing -1..2 hits the Integer cache. + TagMap tags = metadata.getTags(); + Integer direction = KnownTagCodec.directionOf(tags); + tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag); // reset for next span includeProcessTags = false; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index f144122cdec..b487d4665b7 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -1,19 +1,13 @@ package datadog.trace.core.taginterceptor; import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; -import static datadog.trace.api.DDTags.MEASURED; -import static datadog.trace.api.DDTags.ORIGIN_KEY; -import static datadog.trace.api.DDTags.SPAN_TYPE; -import static datadog.trace.api.KnownTags.DB_STATEMENT_OTEL_NAME; -import static datadog.trace.api.KnownTags.HTTP_METHOD_OTEL_NAME; -import static datadog.trace.api.KnownTags.HTTP_STATUS_CODE_OTEL_NAME; -import static datadog.trace.api.KnownTags.HTTP_URL_OTEL_NAME; +import static datadog.trace.api.KnownTags.COMPONENT_ID; +import static datadog.trace.api.KnownTags.LANGUAGE_ID; +import static datadog.trace.api.KnownTags.SERVLET_CONTEXT_ID; import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.SERVLET_CONTEXT; import static datadog.trace.bootstrap.instrumentation.api.ServiceNameSources.SPLIT_BY_SERVLET_CONTEXT; import static datadog.trace.bootstrap.instrumentation.api.ServiceNameSources.SPLIT_BY_TAGS; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_STATUS; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; import static datadog.trace.core.taginterceptor.RuleFlags.Feature.FORCE_MANUAL_DROP; import static datadog.trace.core.taginterceptor.RuleFlags.Feature.FORCE_SAMPLING_PRIORITY; @@ -28,6 +22,7 @@ import datadog.trace.api.ConfigDefaults; import datadog.trace.api.DDTags; import datadog.trace.api.KnownTagCodec; +import datadog.trace.api.KnownTags; import datadog.trace.api.Pair; import datadog.trace.api.TagMap; import datadog.trace.api.config.GeneralConfig; @@ -57,12 +52,14 @@ public class TagInterceptor { private final boolean splitByServletContext; private final String inferredServiceName; - /** Decorator identity tags in split-by-tags precedence order, highest first. */ - private static final String[] PROTOTYPE_SPLIT_BY_TAGS_PRECEDENCE = { - Tags.COMPONENT, DDTags.LANGUAGE_TAG_KEY - }; + /** + * Whether each known tag, indexed by serial, is a split-by-tags entry. Sized to every known + * serial and never changed after construction, so the check is a single load. + */ + private final boolean[] splitServiceSerials; - private final Set splitServiceTags; + /** split-by-tags entries naming custom tags; null when there are none. */ + private final Set splitServiceCustomNames; private final boolean shouldSet404ResourceName; private final boolean shouldSetUrlResourceAsName; @@ -85,9 +82,10 @@ public TagInterceptor( boolean jeeSplitByDeployment) { this.isServiceNameSetByUser = isServiceNameSetByUser; this.inferredServiceName = inferredServiceName; - this.splitServiceTags = withCanonicalNames(splitServiceTags); + this.splitServiceSerials = knownTagSerials(splitServiceTags); + this.splitServiceCustomNames = customTagNames(splitServiceTags); this.ruleFlags = ruleFlags; - splitByServletContext = this.splitServiceTags.contains(SERVLET_CONTEXT); + splitByServletContext = isSplitServiceTag(SERVLET_CONTEXT_ID); shouldSet404ResourceName = ruleFlags.isEnabled(URL_AS_RESOURCE_NAME) @@ -98,31 +96,66 @@ public TagInterceptor( } /** - * Adds the Datadog name of each configured tag: entries set on the span builder arrive under it, - * so a tag configured by its OpenTelemetry name (e.g. {@code db.system}) would otherwise be - * missed. + * Marks, by serial, each known tag that {@code names} names under any of its names, so a tag set + * by id or by its OpenTelemetry name matches too. */ - private static Set withCanonicalNames(Set tags) { - Set result = null; - for (String tag : tags) { - String canonical = KnownTagCodec.canonicalTagName(tag); - if (!canonical.equals(tag) && !tags.contains(canonical)) { - if (result == null) { - result = new HashSet<>(tags); + private static boolean[] knownTagSerials(Set names) { + boolean[] serials = new boolean[KnownTagCodec.serialLimit()]; + if (names.isEmpty()) { + return serials; + } + Set canonicalNames = new HashSet<>(); + for (String name : names) { + canonicalNames.add(KnownTagCodec.canonicalTagName(name)); + } + for (int serial = 1; serial < serials.length; ++serial) { + serials[serial] = + canonicalNames.contains(KnownTagCodec.nameOf(KnownTagCodec.makeTagId(serial))); + } + return serials; + } + + /** The names in {@code names} that do not resolve to a known tag, or null when there are none. */ + private static Set customTagNames(Set names) { + Set custom = null; + for (String name : names) { + if (KnownTagCodec.keyOf(name) == 0) { + if (custom == null) { + custom = new HashSet<>(); } - result.add(canonical); + custom.add(name); } } - return result == null ? tags : result; + return custom; + } + + /** Requires a known id, whose serial is always in range of the table. */ + private boolean isSplitServiceTag(long tagId) { + return splitServiceSerials[KnownTagCodec.serialNum(tagId)]; + } + + private boolean isSplitServiceTag(String customTag) { + Set names = splitServiceCustomNames; + return names != null && names.contains(customTag); } public boolean needsIntercept(TagMap map) { for (TagMap.EntryReader entry : map) { - if (needsIntercept(entry.tag())) return true; + if (needsIntercept(entry)) return true; } return false; } + public boolean needsIntercept(TagMap.EntryReader entry) { + long tagId = entry.tagId(); + return tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(entry.tag()); + } + + /** Whether a custom tag (one with no id) may be routed; only split-by-tags routes them. */ + public boolean needsInterceptCustomTag(String customTag) { + return isSplitServiceTag(customTag); + } + public boolean needsIntercept(Map map) { for (String tag : map.keySet()) { if (needsIntercept(tag)) return true; @@ -130,131 +163,137 @@ public boolean needsIntercept(Map map) { return false; } - public boolean needsIntercept(String tag) { - switch (tag) { - case DDTags.RESOURCE_NAME: - case Tags.DB_STATEMENT: - case DB_STATEMENT_OTEL_NAME: - case DDTags.SERVICE_NAME: - case "service": - case Tags.PEER_SERVICE: - case DDTags.MANUAL_KEEP: - case DDTags.MANUAL_DROP: - case Tags.ASM_KEEP: - case Tags.AI_GUARD_KEEP: - case Tags.SAMPLING_PRIORITY: - case Tags.PROPAGATED_TRACE_SOURCE: - case Tags.PROPAGATED_DEBUG: - case SERVLET_CONTEXT: - case SPAN_TYPE: - case ANALYTICS_SAMPLE_RATE: - case Tags.ERROR: - case HTTP_STATUS: - case HTTP_STATUS_CODE_OTEL_NAME: - case HTTP_METHOD: - case HTTP_METHOD_OTEL_NAME: - case HTTP_URL: - case HTTP_URL_OTEL_NAME: - case ORIGIN_KEY: - case MEASURED: - case Tags.SPAN_KIND: - return true; + /** + * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag. With a constant + * id, the {@link KnownTagCodec#INTERCEPTED} test folds to a constant, leaving only the + * split-by-tags table load at run time. {@code tagId} must be a known id. + */ + public boolean needsIntercept(long tagId) { + return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); + } - default: - return splitServiceTags.contains(tag); - } + public boolean needsIntercept(String tag) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(tag); } public boolean interceptTag(DDSpanContext span, String tag, Object value) { - switch (tag) { - case DDTags.RESOURCE_NAME: + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? interceptTag(span, tagId, value) : interceptCustomTag(span, tag, value); + } + + /** + * Routes a known tag to a span field or a sampling directive. Returns true when the tag was + * consumed, false when it should still be stored. Every tag with a case here carries the {@link + * KnownTagCodec#INTERCEPTED} bit, declared in {@code tag-conventions-java.yaml}. + * + *

This method is deliberately too big for C2 to inline, so the handler bodies stay out of + * {@code DDSpanContext.setTag}'s compiled code; {@code TagInterceptorInliningTest} pins that. + */ + public boolean interceptTag(DDSpanContext span, long tagId, Object value) { + switch (KnownTagCodec.serialNum(tagId)) { + case KnownTags.RESOURCE_NAME_SERIAL_NUM: return interceptResourceName(span, value); - case Tags.DB_STATEMENT: - case DB_STATEMENT_OTEL_NAME: + case KnownTags.DB_STATEMENT_SERIAL_NUM: return interceptDbStatement(span, value); - case DDTags.SERVICE_NAME: - case "service": + case KnownTags.SERVICE_SERIAL_NUM: return interceptServiceName(SERVICE_NAME, span, value); - case Tags.PEER_SERVICE: + case KnownTags.PEER_SERVICE_SERIAL_NUM: // we still need to intercept and add this tag when the user manually set span.setTag(DDTags.PEER_SERVICE_SOURCE, Tags.PEER_SERVICE); return interceptServiceName(PEER_SERVICE, span, value); - case DDTags.MANUAL_KEEP: + case KnownTags.MANUAL_KEEP_SERIAL_NUM: if (asBoolean(value)) { span.forceKeep(); return true; } return false; - case DDTags.MANUAL_DROP: + case KnownTags.MANUAL_DROP_SERIAL_NUM: return interceptSamplingPriority( FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); - case Tags.ASM_KEEP: + case KnownTags.ASM_KEEP_SERIAL_NUM: if (asBoolean(value)) { span.forceKeep(SamplingMechanism.APPSEC); return true; } return false; - case Tags.AI_GUARD_KEEP: + case KnownTags.AI_GUARD_KEEP_SERIAL_NUM: if (asBoolean(value)) { span.forceKeep(SamplingMechanism.AI_GUARD); return true; } return false; - case Tags.SAMPLING_PRIORITY: + case KnownTags.SAMPLING_PRIORITY_SERIAL_NUM: return interceptSamplingPriority(span, value); - case Tags.PROPAGATED_TRACE_SOURCE: + case KnownTags.DD_P_TS_SERIAL_NUM: if (value instanceof Integer) { span.addPropagatedTraceSource((Integer) value); return true; } return false; - case Tags.PROPAGATED_DEBUG: + case KnownTags.DD_P_DEBUG_SERIAL_NUM: span.updateDebugPropagation(String.valueOf(value)); return true; - case SERVLET_CONTEXT: + case KnownTags.SERVLET_CONTEXT_SERIAL_NUM: return interceptServletContext(span, value); - case SPAN_TYPE: + case KnownTags.SPAN_TYPE_SERIAL_NUM: return interceptSpanType(span, value); - case ANALYTICS_SAMPLE_RATE: + case KnownTags.DD1_SR_EAUSR_SERIAL_NUM: return interceptAnalyticsSampleRate(span, value); - case Tags.ERROR: + case KnownTags.ERROR_SERIAL_NUM: return interceptError(span, value); - case HTTP_STATUS: - case HTTP_STATUS_CODE_OTEL_NAME: + case KnownTags.HTTP_STATUS_CODE_SERIAL_NUM: // not set internally but may come from manual instrumentation return interceptHttpStatusCode(span, value); - case HTTP_METHOD: - case HTTP_METHOD_OTEL_NAME: - case HTTP_URL: - case HTTP_URL_OTEL_NAME: - return interceptUrlResourceAsNameRule(span, tag, value); - case ORIGIN_KEY: + case KnownTags.HTTP_METHOD_SERIAL_NUM: + case KnownTags.HTTP_URL_SERIAL_NUM: + return interceptUrlResourceAsNameRule(span, tagId, value); + case KnownTags.DD_ORIGIN_SERIAL_NUM: return interceptOrigin(span, value); - case MEASURED: + case KnownTags.DD_MEASURED_SERIAL_NUM: return interceptMeasured(span, value); - case Tags.SPAN_KIND: + case KnownTags.SPAN_KIND_SERIAL_NUM: // Cache the ordinal for fast isOutbound() checks. // Return false so the value is still stored in unsafeTags for serialization. span.setSpanKindOrdinal(String.valueOf(value)); return false; default: - return intercept(span, tag, value); + return isSplitServiceTag(tagId) && splitService(span, value); + } + } + + /** + * Routes an entry by its id, or by its name when it is a custom tag. Prechecks, so a primitive + * entry that needs no interception is never boxed. + */ + public boolean interceptTag(DDSpanContext span, TagMap.EntryReader entry) { + long tagId = entry.tagId(); + if (tagId != 0) { + return needsIntercept(tagId) && interceptTag(span, tagId, entry.objectValue()); } + return isSplitServiceTag(entry.tag()) && splitService(span, entry.objectValue()); } - private boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { + /** Routes a custom tag -- one with no id. */ + public boolean interceptCustomTag(DDSpanContext span, String customTag, Object value) { + return isSplitServiceTag(customTag) && splitService(span, value); + } + + private static boolean splitService(DDSpanContext span, Object value) { + span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); + return true; + } + + private boolean interceptUrlResourceAsNameRule(DDSpanContext span, long tagId, Object value) { if (shouldSetUrlResourceAsName) { - if (HTTP_METHOD.equals(tag) || HTTP_METHOD_OTEL_NAME.equals(tag)) { + // Read by Datadog name only: entries are stored under it whichever spelling set them. + if (tagId == KnownTags.HTTP_METHOD_ID) { final Object url = span.unsafeGetTag(HTTP_URL); if (url != null) { setResourceFromUrl(span, value.toString(), url); } - } else if (HTTP_URL.equals(tag) || HTTP_URL_OTEL_NAME.equals(tag)) { - // the method may have been set under either spelling -- see HTTP_METHOD_OTEL_NAME. - Object method = span.unsafeGetTag(HTTP_METHOD); - if (method == null) { - method = span.unsafeGetTag(HTTP_METHOD_OTEL_NAME); - } + } else { + final Object method = span.unsafeGetTag(HTTP_METHOD); setResourceFromUrl(span, method != null ? method.toString() : null, value); } } @@ -295,26 +334,19 @@ private static void setResourceFromUrl( * its interception never reaches the split-by-tags check.) */ public void resolveSplitByTagsPrecedence(DDSpanContext span, TagMap prototypeTags) { - if (splitServiceTags.isEmpty()) { - return; - } - for (String tag : PROTOTYPE_SPLIT_BY_TAGS_PRECEDENCE) { - if (splitServiceTags.contains(tag)) { - Object value = prototypeTags.getObject(tag); - if (value != null) { - span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); - return; - } + if (isSplitServiceTag(COMPONENT_ID)) { + TagMap.Entry component = prototypeTags.getEntry(COMPONENT_ID); + if (component != null) { + splitService(span, component.objectValue()); + return; } } - } - - private boolean intercept(DDSpanContext span, String tag, Object value) { - if (splitServiceTags.contains(tag)) { - span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); - return true; + if (isSplitServiceTag(LANGUAGE_ID)) { + TagMap.Entry language = prototypeTags.getEntry(LANGUAGE_ID); + if (language != null) { + splitService(span, language.objectValue()); + } } - return false; } private boolean interceptResourceName(DDSpanContext span, Object value) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index 0c953fa5b65..a07f15bd87b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -61,6 +61,19 @@ void clearingAnUnknownIdIsIgnored() { assertNull(span.getTag(KnownTags.PEER_HOSTNAME_NAME)); } + @Test + void anIdReplacesAValueSetUnderTheName() { + span.setTag("peer.port", 8080); + + span.setTag(KnownTags.PEER_PORT_ID, 443); + + assertEquals(443, span.getTag("peer.port")); + + span.setTag(KnownTags.PEER_PORT_ID, (Object) null); + + assertNull(span.getTag("peer.port")); + } + @Test void removingTheSpanKindByIdClearsTheKind() { span.setTag(KnownTags.SPAN_KIND_ID, "client"); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index a0456e9d20e..811e3cdf32b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test; import org.mockito.AdditionalAnswers; import org.mockito.MockedStatic; +import org.tabletest.junit.TableTest; /** * Tests for {@link OtlpTraceJsonCollector}, parsing the produced JSON back with {@link JsonMapper} @@ -117,6 +118,36 @@ void tagsAreEmittedUnderTheirDatadogNameWhenOtelSemanticsDisabled() throws IOExc "a tag the registry does not name passes through unchanged; got " + attrKeys); } + @TableTest({ + "scenario | kind | tag | otelName ", + "client hostname | client | peer.hostname | server.address ", + "server hostname | server | http.hostname | server.address ", + "server client ip | server | network.client.ip | network.peer.address", + "client peer port | client | peer.port | server.port ", + "server peer port | server | peer.port | client.port " + }) + void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( + String kind, String tag, String otelName) throws IOException { + Config realConfig = Config.get(); + try (MockedStatic configMock = mockStatic(Config.class)) { + Config config = mock(Config.class, AdditionalAnswers.delegatesTo(realConfig)); + when(config.isTraceOtelSemanticsEnabled()).thenReturn(true); + configMock.when(Config::get).thenReturn(config); + + AgentSpan agentSpan = TRACER.startSpan("test", "op.directional"); + agentSpan.setTag(SPAN_KIND, kind); + agentSpan.setTag(tag, "value"); + agentSpan.finish(); + + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) agentSpan)); + Set attrKeys = attributeKeys(onlySpan(collector.collectTraces())); + + assertTrue(attrKeys.contains(otelName), "expected " + otelName + "; got " + attrKeys); + assertFalse(attrKeys.contains(tag), "expected no " + tag + "; got " + attrKeys); + } + } + private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled) throws IOException { Config realConfig = Config.get(); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java new file mode 100644 index 00000000000..e11811d641c --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java @@ -0,0 +1,78 @@ +package datadog.trace.core.taginterceptor; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; + +/** + * Pins a code shape that C2's inlining depends on and no behavioral test can see. + * + *

{@code interceptTag(DDSpanContext, long, Object)} must stay too big for C2 to inline: more + * than {@code FreqInlineSize}, 325 bytes of bytecode by default. DDSpanContext.setTag calls it only + * for intercepted tags. If it inlined into setTag's own compiled code, the profiled handler bodies + * would come with it and can push setTag past {@code InlineSmallCode} (2500 bytes of machine code + * by default). Callers then refuse to inline setTag, and a constant id no longer folds its + * interception test away. + */ +class TagInterceptorInliningTest { + private static final int FREQ_INLINE_SIZE = 325; + + @Test + void interceptTagByIdStaysTooBigToInline() throws IOException { + int length = + codeLength("interceptTag", "(Ldatadog/trace/core/DDSpanContext;JLjava/lang/Object;)Z"); + + assertTrue( + length > FREQ_INLINE_SIZE, + "interceptTag(span, long, value) is " + + length + + " bytes of bytecode, small enough for C2 to inline into DDSpanContext.setTag; keep" + + " its handlers in the switch (see this test's Javadoc)"); + } + + /** The {@code code_length} of a method's Code attribute, read from the class file. */ + private static int codeLength(String name, String descriptor) throws IOException { + ClassReader reader; + try (InputStream in = TagInterceptor.class.getResourceAsStream("TagInterceptor.class")) { + reader = new ClassReader(in); + } + char[] buf = new char[reader.getMaxStringLength()]; + int offset = reader.header + 6; // access_flags, this_class, super_class + offset += 2 + 2 * reader.readUnsignedShort(offset); // interfaces + int fields = reader.readUnsignedShort(offset); + offset += 2; + for (int i = 0; i < fields; ++i) { + offset = skipAttributes(reader, offset + 6); // access_flags, name, descriptor + } + int methods = reader.readUnsignedShort(offset); + offset += 2; + for (int i = 0; i < methods; ++i) { + if (reader.readUTF8(offset + 2, buf).equals(name) + && reader.readUTF8(offset + 4, buf).equals(descriptor)) { + int attributes = reader.readUnsignedShort(offset + 6); + int attribute = offset + 8; + for (int j = 0; j < attributes; ++j) { + if ("Code".equals(reader.readUTF8(attribute, buf))) { + return reader.readInt(attribute + 10); // name, length, max_stack, max_locals + } + attribute += 6 + reader.readInt(attribute + 2); + } + } + offset = skipAttributes(reader, offset + 6); + } + throw new AssertionError(name + descriptor + " not found"); + } + + /** Skips the attributes whose count is at {@code offset}; returns the offset after them. */ + private static int skipAttributes(ClassReader reader, int offset) { + int attributes = reader.readUnsignedShort(offset); + offset += 2; + for (int i = 0; i < attributes; ++i) { + offset += 6 + reader.readInt(offset + 2); // name, length, info + } + return offset; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 260241f8fb3..6cb01e76f08 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -26,7 +27,10 @@ import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; +import datadog.trace.api.KnownTagCodec; +import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; +import datadog.trace.api.TagMap; import datadog.trace.api.env.CapturedEnvironment; import datadog.trace.api.remoteconfig.ServiceNameCollector; import datadog.trace.api.remoteconfig.ServiceNameCollectorTestBridge; @@ -35,6 +39,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; +import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources; import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.sampling.AllSampler; @@ -50,6 +55,7 @@ import datadog.trace.test.junit.utils.config.WithConfig; import datadog.trace.test.junit.utils.converter.ConfigDefaultsConverter; import datadog.trace.test.junit.utils.converter.TagsConverter; +import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -199,7 +205,7 @@ void mappingCausesServletContextToNotChangeServiceName(String serviceName) { "scenario | configured | tag | expectedBuilder | expectedSetter", "configured by OTel name | db.system | db.system | split | split ", "configured by Datadog name | db.type | db.type | split | split ", - "Datadog config, OTel tag | db.type | db.system | split | my-service ", + "Datadog config, OTel tag | db.type | db.system | split | split ", "OTel config, Datadog tag | db.system | db.type | split | split ", "custom tag | custom.tag | custom.tag | split | split ", "unconfigured tag | db.system | other.tag | my-service | my-service ", @@ -370,6 +376,36 @@ void splitByTagsDecoratorComponentOverwritesCandidateSetAtConstruction() { assertEquals("my-component", span.getServiceName()); } + /** + * Without a {@code component}, a prototype's {@code language} is re-applied after the prototype's + * tags are intercepted, so it wins over another split-by-tags candidate the prototype sets. + * Called directly: the prototype's map is intercepted in hash order, so a full application can't + * pin the order that needs this. + */ + @Test + void splitByTagsLanguageIsReappliedWithoutAComponent() { + TagInterceptor interceptor = + new TagInterceptor( + true, + "my-service", + new HashSet<>(Arrays.asList("tenant", DDTags.LANGUAGE_TAG_KEY)), + new RuleFlags(), + false); + CoreTracer tracer = + tracerBuilder().writer(new LoggingWriter()).tagInterceptor(interceptor).build(); + AgentSpan span = tracer.buildSpan("datadog", "some span").start(); + span.setTag("tenant", "acme"); // as if the prototype's tenant was intercepted last + assertEquals("acme", span.getServiceName()); + + TagMap prototypeTags = TagMap.create(); + prototypeTags.set(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE); + prototypeTags.set("tenant", "acme"); + interceptor.resolveSplitByTagsPrecedence((DDSpanContext) span.spanContext(), prototypeTags); + span.finish(); + + assertEquals(DDTags.LANGUAGE_TAG_VALUE, span.getServiceName()); + } + /** Separate tag writes stay last-writer-wins, whichever split-by-tags candidate they set. */ @Test void splitByTagsSeparateWritesAreLastWriterWins() { @@ -1008,4 +1044,79 @@ void whenInterceptsProductTraceSourcePropagationTagUpdatePropagatedTraceSourceIs verify(context, times(1)).addPropagatedTraceSource(ProductTraceSource.ASM); } + + @Test + void exactlyTheTagsWithTheInterceptedBitHaveACase() throws IllegalAccessException { + RuleFlags ruleFlags = mock(RuleFlags.class); + when(ruleFlags.isEnabled(any())).thenReturn(true); + TagInterceptor interceptor = + new TagInterceptor(false, "my-service", emptySet(), ruleFlags, false); + + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) { + continue; + } + long tagId = field.getLong(null); + DDSpanContext span = mock(DDSpanContext.class); + when(span.getServiceName()).thenReturn(""); + + // Each case consumes one of these values or acts on the span for it; with no split-by-tags, + // the default case does neither. + boolean consumed = false; + for (Object value : new Object[] {true, 1, "1"}) { + consumed |= interceptor.interceptTag(span, tagId, value); + } + + boolean hasCase = consumed || !mockingDetails(span).getInvocations().isEmpty(); + assertEquals(KnownTagCodec.isIntercepted(tagId), hasCase, field.getName()); + assertEquals(KnownTagCodec.isIntercepted(tagId), interceptor.needsIntercept(tagId)); + } + } + + @TableTest({ + "scenario | configured | tag ", + "configured by Datadog name | db.type | db.type ", + "configured by OTel name | db.system | db.type ", + "set by OTel name | db.type | db.system" + }) + void splitByTagsMatchesAKnownTagByIdUnderAnyName(String configured, String tag) { + TagInterceptor interceptor = splittingInterceptor(configured); + DDSpanContext span = mock(DDSpanContext.class); + + assertTrue(interceptor.needsIntercept(KnownTags.DB_TYPE_ID)); + assertTrue(interceptor.needsIntercept(tag)); + assertFalse(interceptor.needsIntercept(KnownTags.COMPONENT_ID)); + assertTrue(interceptor.interceptTag(span, KnownTags.DB_TYPE_ID, "postgres")); + assertTrue(interceptor.interceptTag(span, tag, "mysql")); + + verify(span).setServiceName("postgres", ServiceNameSources.SPLIT_BY_TAGS); + verify(span).setServiceName("mysql", ServiceNameSources.SPLIT_BY_TAGS); + } + + @Test + void splitByTagsMatchesACustomTagByName() { + TagInterceptor interceptor = splittingInterceptor("my.custom.tag"); + DDSpanContext span = mock(DDSpanContext.class); + + assertTrue(interceptor.needsIntercept("my.custom.tag")); + assertFalse(interceptor.needsIntercept("other.custom.tag")); + assertFalse(interceptor.needsIntercept(KnownTags.COMPONENT_ID)); + assertTrue(interceptor.interceptTag(span, "my.custom.tag", "split")); + assertFalse(interceptor.interceptTag(span, "other.custom.tag", "split")); + + verify(span).setServiceName("split", ServiceNameSources.SPLIT_BY_TAGS); + } + + @Test + void splitByTagsOnANameDeclaredPerDirectionMatchesItsOneTag() { + TagInterceptor interceptor = splittingInterceptor(Tags.PEER_PORT); + + assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_ID)); + assertTrue(interceptor.needsIntercept(Tags.PEER_PORT)); + } + + private static TagInterceptor splittingInterceptor(String splitByTag) { + return new TagInterceptor( + true, "my-service", Collections.singleton(splitByTag), new RuleFlags(), false); + } } diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index de3910a8ac8..5ffb1f7868d 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -2,3 +2,8 @@ # Use AgentSpan#setServiceName(String, CharSequence) instead to track the source of the service name @defaultMessage Use AgentSpan#setServiceName(String, CharSequence) instead to track the source of the service name. The source should typically be the instrumentation name. datadog.trace.bootstrap.instrumentation.api.AgentSpan#setServiceName(java.lang.String) + +# TagMap.internals() skips checks the tracer core has already made; it is @Restricted to the core +# (see datadog.apicontract.Restricted). Instrumentation uses TagMap's checked methods. +@defaultMessage TagMap.internals() is restricted to the tracer core; use TagMap's checked methods instead. +datadog.trace.api.TagMap#internals() diff --git a/internal-api/build.gradle.kts b/internal-api/build.gradle.kts index a9d82cebc72..9c877c4562d 100644 --- a/internal-api/build.gradle.kts +++ b/internal-api/build.gradle.kts @@ -266,6 +266,7 @@ extra["excludedClassesInstructionCoverage"] = listOf("datadog.trace.util.stacktr tagRegistry { tagConventionsFile.set(rootProject.layout.projectDirectory.file("tag-conventions.yaml")) + tracerOverlayFile.set(rootProject.layout.projectDirectory.file("tag-conventions-java.yaml")) } dependencies { diff --git a/internal-api/src/main/java/datadog/apicontract/Restricted.java b/internal-api/src/main/java/datadog/apicontract/Restricted.java new file mode 100644 index 00000000000..ab0372fa06f --- /dev/null +++ b/internal-api/src/main/java/datadog/apicontract/Restricted.java @@ -0,0 +1,24 @@ +package datadog.apicontract; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a member that is public only because Java has no narrower visibility across modules: it may + * be used only from the packages in {@link #allowedIn}. It typically trades a safety check for + * speed, trusting its caller to have made the check already. + * + *

This marker changes no runtime behavior. Instrumentation modules are kept out by the + * forbidden-APIs signatures in {@code gradle/forbiddenApiFilters/instrumentation.txt}, which must + * list every {@code @Restricted} member. + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Restricted { + /** Package prefixes allowed to use the member, such as {@code datadog.trace.core}. */ + String[] allowedIn(); +} 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 d84e1e6a537..605f7140d4c 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -1,12 +1,15 @@ package datadog.trace.api; +import datadog.trace.bootstrap.instrumentation.api.Tags; + /** * Resolves generated tag IDs and names. {@link KnownTags} owns the lookup tables; this class owns * the ID layout and namespace naming policy. * *

Datadog and OpenTelemetry names can resolve to the same ID. {@link #canonicalTagName(String)} - * provides the Datadog key used by {@link TagMap}; the namespace readers choose output names. The - * ID does not determine whether a tag is intercepted or where it is stored. + * provides the Datadog key used by {@link TagMap}; the namespace readers choose output names. An ID + * marks a tag that may be intercepted ({@link #INTERCEPTED}), but {@code TagInterceptor} decides + * whether to consume the value. The ID says nothing about where the tag is stored. */ public final class KnownTagCodec { /* @@ -15,19 +18,24 @@ public final class KnownTagCodec { * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] * are RESERVED and always zero here: they are the window the dense tag store uses for its * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is - * the trace/span LEVEL bit (set ⟹ trace-level); bits 1-0 are reserved. Unknown (string-only) - * custom tags are NOT known ids — {@code keyOf} returns 0 for them. + * the trace/span LEVEL bit (set ⟹ trace-level), and bit 1 is the INTERCEPTED bit. Unknown + * (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for them. + * + *

There is one id per Datadog name. A tag the conventions declare once per direction, such as + * {@code peer.port}, is still one tag: only its OpenTelemetry name depends on the span's + * direction, so direction applies when a tag is named, never when it is stored. * - *

An id says what a tag IS, not how it is SET. Whether the tracer intercepts a tag on the - * set-path — routing it to a span field or a sampling directive instead of tag storage — belongs - * to TagInterceptor, whose {@code needsIntercept} switch is the authority; mirroring it here as a - * classification bit and a serial-range tier only created drift between the two. That - * classification returns with the work that consumes it (the id→handler dispatch table that - * retires TagInterceptor), and re-adding a bit then is purely additive. + *

The INTERCEPTED bit marks a tag TagInterceptor may route on the set-path, to a span field or + * a sampling directive instead of tag storage. It is a hint, not a decision: the interceptor's + * switch decides, and may still store the tag. A setter called with a constant id tests the bit + * at JIT time, so the interception path folds away for every tag without it. * *

There is deliberately NO OpenTelemetry-applicability flag: an absent otel-name means * pass-through (the tag is emitted under its Datadog name), so today every known tag has an * OpenTelemetry name and such a flag would be constant. It returns once a Datadog-only tag exists. + * + *

Serials are assigned at build time and are NOT stable across releases: they follow the tags' + * names in order, so adding a tag renumbers others. Never persist or transmit a raw id. */ public static int serialNum(long tagId) { return (int) (tagId >>> 48); @@ -46,6 +54,17 @@ public static boolean isTraceLevel(long tagId) { return (tagId & LEVEL_TRACE) != 0L; } + /** + * INTERCEPTED bit (low-32 carve, bit 1): marks a tag TagInterceptor may route on the set-path. + * The tracer overlay, {@code tag-conventions-java.yaml}, lists these tags. + */ + public static final long INTERCEPTED = 1L << 1; + + /** True if the tagId names a tag TagInterceptor may route on the set-path. */ + public static boolean isIntercepted(long tagId) { + return (tagId & INTERCEPTED) != 0L; + } + /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; @@ -61,12 +80,61 @@ public static long makeTagId(int serialNum) { return (long) serialNum << 48; } + /* + * A span's direction, which names the few tags whose OpenTelemetry name depends on it: peer.port + * is client.port on inbound spans and server.port on outbound ones. A tag is stored the same way + * in every direction; direction applies only when naming it. Pass one of these to + * openTelemetryTagOf(long, int). + */ + + /** Direction not known, for example a span with no kind: only names that need none apply. */ + public static final int DIRECTION_UNKNOWN = -1; + + /** Server and consumer spans, which receive a request or message. */ + public static final int DIRECTION_INBOUND = 0; + + /** Client and producer spans, which send a request or message. */ + public static final int DIRECTION_OUTBOUND = 1; + + /** Internal spans, which have no other end. */ + public static final int DIRECTION_NONE = 2; + + /** The direction of a span with these tags, from its {@code span.kind}. */ + public static int directionOf(TagMap tags) { + TagMap.Entry kind = tags.getEntry(KnownTags.SPAN_KIND_ID); + return kind == null ? DIRECTION_UNKNOWN : directionOfKind(kind.stringValue()); + } + + /** The direction of a span of {@code kind}; brokers and custom kinds have none. */ + public static int directionOfKind(String kind) { + if (kind == null) { + return DIRECTION_UNKNOWN; + } + switch (kind) { + case Tags.SPAN_KIND_SERVER: + case Tags.SPAN_KIND_CONSUMER: + return DIRECTION_INBOUND; + case Tags.SPAN_KIND_CLIENT: + case Tags.SPAN_KIND_PRODUCER: + return DIRECTION_OUTBOUND; + case Tags.SPAN_KIND_INTERNAL: + return DIRECTION_NONE; + default: + return DIRECTION_UNKNOWN; + } + } + + /** The registry's tables. Generated as {@code KnownTags.RESOLVER}; this class owns the policy. */ public interface Resolver { /** The tag's Datadog-namespace (canonical) name. */ String nameOf(long tagId); - /** The tag's OpenTelemetry-namespace name, or {@code null} when it declares none. */ - String openTelemetryNameOf(long tagId); + /** + * The tag's OpenTelemetry-namespace name on spans of {@code direction}, or {@code null} when it + * declares none there. {@link #DIRECTION_UNKNOWN} returns only a name that holds in every + * direction. + */ + String openTelemetryNameOf(long tagId, int direction); /** The id for {@code name} in ANY namespace (many→one), or 0 when it is not a known tag. */ long keyOf(String name); @@ -97,7 +165,7 @@ public static String datadogNameOf(long tagId) { * #openTelemetryTagOf} instead. */ public static String openTelemetryNameOf(long tagId) { - return Installed.RESOLVER.openTelemetryNameOf(tagId); + return Installed.RESOLVER.openTelemetryNameOf(tagId, DIRECTION_UNKNOWN); } /** @@ -109,13 +177,45 @@ public static String openTelemetryNameOf(long tagId) { *

This is the one place the pass-through policy lives, so no serializer re-decides it. Pair it * with {@link #datadogNameOf} for the same tag under the Datadog namespace; outbound naming is * per-namespace, never normalized to one of them. + * + *

Needs no direction for nearly every tag. The exception is a tag renamed in some directions + * only ({@code http.hostname} is {@code server.address} on inbound spans, {@code peer.port} is + * {@code client.port} inbound and {@code server.port} outbound): without a direction it passes + * through under its Datadog name. A caller naming a span's tags uses {@link + * #openTelemetryTagOf(long, int)}. */ public static String openTelemetryTagOf(long tagId) { + return openTelemetryTagOf(tagId, DIRECTION_UNKNOWN); + } + + /** + * {@link #openTelemetryTagOf(long)} on spans of {@code direction}, which also applies a tag's + * rename that holds only in that direction: {@code http.hostname} is {@code server.address} on + * inbound spans, but stays {@code http.hostname} on outbound ones. + */ + public static String openTelemetryTagOf(long tagId, int direction) { Resolver resolver = Installed.RESOLVER; - String otelName = resolver.openTelemetryNameOf(tagId); + String otelName = resolver.openTelemetryNameOf(tagId, direction); return otelName != null ? otelName : resolver.nameOf(tagId); } + /** + * One past the largest known serial: an array of this length, indexed by {@link #serialNum}, has + * a slot for every known tag (slot 0 is no tag). + */ + public static int serialLimit() { + return KnownTags.SERIAL_LIMIT; + } + + /** + * True if {@code tagId} names a known tag. Gives the same answer as {@code nameOf(tagId) != + * null}, but as a range check on the serial, which folds away for a constant id. + */ + public static boolean isKnown(long tagId) { + int serial = serialNum(tagId); + return serial != 0 && serial < serialLimit(); // serial 0 is no tag + } + /** The id for {@code name} in any namespace, or 0 when it is not a known tag. */ public static long keyOf(String name) { return Installed.RESOLVER.keyOf(name); 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 eaf3fccdf27..be07fadbeeb 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1,5 +1,7 @@ package datadog.trace.api; +import datadog.apicontract.Restricted; +import datadog.trace.api.function.NoEscape; import datadog.trace.api.function.TriConsumer; import datadog.trace.api.internal.VisibleForTesting; import java.util.AbstractCollection; @@ -195,6 +197,16 @@ default String openTelemetryTag() { return otelTag != null ? otelTag : tag(); } + /** + * {@link #openTelemetryTag()} on a span of {@code direction} (a {@code + * KnownTagCodec.DIRECTION_*} constant), which also applies a rename that holds only in that + * direction: {@code peer.port} is {@code server.port} on a client span. + */ + default String openTelemetryTag(int direction) { + long tagId = tagId(); + return tagId == 0 ? tag() : KnownTagCodec.openTelemetryTagOf(tagId, direction); + } + byte type(); boolean is(byte type); @@ -250,34 +262,34 @@ public static boolean isEmptyValue(Object value) { */ @Nullable public static final Entry create(@Nonnull String tag, Object value) { - return isEmptyValue(value) ? null : TagMap.Entry.newAnyEntry(tag, value); + return isEmptyValue(value) ? null : TagMap.anyEntryFor(tag, value); } /** If value is non-null, returns a new TagMap.Entry If value is null or empty, returns null */ @Nullable public static final Entry create(@Nonnull String tag, CharSequence value) { // NOTE: From the static typing, we know that value is not a primitive box - return isEmptyValue(value) ? null : TagMap.Entry.newObjectEntry(tag, value); + return isEmptyValue(value) ? null : TagMap.objectEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, boolean value) { - return TagMap.Entry.newBooleanEntry(tag, value); + return TagMap.booleanEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, int value) { - return TagMap.Entry.newIntEntry(tag, value); + return TagMap.intEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, long value) { - return TagMap.Entry.newLongEntry(tag, value); + return TagMap.longEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, float value) { - return TagMap.Entry.newFloatEntry(tag, value); + return TagMap.floatEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, double value) { - return TagMap.Entry.newDoubleEntry(tag, value); + return TagMap.doubleEntryFor(tag, value); } /* @@ -319,7 +331,7 @@ public static final Entry create(long tagId, double value) { } static Entry newAnyEntry(Map.Entry entry) { - return newAnyEntry(entry.getKey(), entry.getValue()); + return anyEntryFor(entry.getKey(), entry.getValue()); } static Entry newAnyEntry(String tag, Object value) { @@ -435,16 +447,17 @@ static Entry newDoubleEntry(String tag, Double box) { volatile String strCache = null; + /** + * A custom tag's entry: no registry lookup. A known tag is built from its id instead -- callers + * that start from a name resolve it once and pick the id or name factory (see {@code + * TagMap.anyEntryFor}). + */ private Entry(String tag, byte type, long prim, Object obj) { - /* - * Canonicalize known names at the single Entry construction point, so Datadog and - * OpenTelemetry spellings use the same TagMap key. The one StringIndex lookup this costs also - * yields the tag id, which becomes the tag hash. - */ - this(KnownTagCodec.keyOf(tag), tag, type, prim, obj); + this(0L, tag, type, prim, obj); + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; build it from its id"; } - private Entry(long tagId, byte type, long prim, Object obj) { + Entry(long tagId, byte type, long prim, Object obj) { // Resolve the name once: it both validates the id and names the entry. super(requireKnownName(tagId)); this.tagHash = tagId; @@ -455,7 +468,7 @@ private Entry(long tagId, byte type, long prim, Object obj) { } /** {@code tagId} is a known id, or 0 for the custom tag {@code customTag}. */ - private Entry(long tagId, String customTag, byte type, long prim, Object obj) { + Entry(long tagId, String customTag, byte type, long prim, Object obj) { super(tagId != 0 ? KnownTagCodec.nameOf(tagId) : customTag); this.tagHash = tagId != 0 ? tagId : customHash(customTag); @@ -464,7 +477,7 @@ private Entry(long tagId, String customTag, byte type, long prim, Object obj) { this.rawObj = obj; } - private static String requireKnownName(long tagId) { + static String requireKnownName(long tagId) { String name = tagId == 0 ? null : KnownTagCodec.nameOf(tagId); if (name == null) { throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); @@ -914,7 +927,7 @@ public boolean equals(Object obj) { return this.tag.equals(that.tag) && this.objectValue().equals(that.objectValue()); } - private static long boolean2Prim(boolean value) { + static long boolean2Prim(boolean value) { return value ? 1L : 0L; } @@ -922,7 +935,7 @@ private static boolean prim2Boolean(long prim) { return (prim != 0L); } - private static long int2Prim(int value) { + static long int2Prim(int value) { return value; } @@ -930,7 +943,7 @@ private static int prim2Int(long prim) { return (int) prim; } - private static long long2Prim(long value) { + static long long2Prim(long value) { return value; } @@ -938,7 +951,7 @@ private static long prim2Long(long prim) { return prim; } - private static long float2Prim(float value) { + static long float2Prim(float value) { return Float.floatToIntBits(value); } @@ -946,7 +959,7 @@ private static float prim2Float(long prim) { return Float.intBitsToFloat((int) prim); } - private static long double2Prim(double value) { + static long double2Prim(double value) { return Double.doubleToRawLongBits(value); } @@ -969,11 +982,11 @@ public static final class Ledger implements Iterable { int nextPos = 0; boolean containsRemovals = false; - private Ledger() { + Ledger() { this(8); } - private Ledger(int size) { + Ledger(int size) { this.entryChanges = new EntryChange[size]; } @@ -996,31 +1009,31 @@ public boolean containsRemovals() { } public Ledger set(String tag, Object value) { - return this.recordEntry(Entry.newAnyEntry(tag, value)); + return this.recordEntry(anyEntryFor(tag, value)); } public Ledger set(String tag, CharSequence value) { - return this.recordEntry(Entry.newObjectEntry(tag, value)); + return this.recordEntry(objectEntryFor(tag, value)); } public Ledger set(String tag, boolean value) { - return this.recordEntry(Entry.newBooleanEntry(tag, value)); + return this.recordEntry(booleanEntryFor(tag, value)); } public Ledger set(String tag, int value) { - return this.recordEntry(Entry.newIntEntry(tag, value)); + return this.recordEntry(intEntryFor(tag, value)); } public Ledger set(String tag, long value) { - return this.recordEntry(Entry.newLongEntry(tag, value)); + return this.recordEntry(longEntryFor(tag, value)); } public Ledger set(String tag, float value) { - return this.recordEntry(Entry.newFloatEntry(tag, value)); + return this.recordEntry(floatEntryFor(tag, value)); } public Ledger set(String tag, double value) { - return this.recordEntry(Entry.newDoubleEntry(tag, value)); + return this.recordEntry(doubleEntryFor(tag, value)); } public Ledger set(Entry entry) { @@ -1176,7 +1189,7 @@ public EntryChange next() { * removed from the collision chain. */ - private final Object[] buckets; + final Object[] buckets; private int size; private boolean frozen; @@ -1468,7 +1481,7 @@ private static Entry findInBucket(Object bucket, int hash, String tag) { * that level's same-index bucket, so each nearer level is probed at one bucket using {@code * parentEntry}'s cached hash (no re-hash, no full-map probe). */ - private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { + boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { int hash = parentEntry.hash(); String tag = parentEntry.tag; for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { @@ -1489,7 +1502,7 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { @Deprecated @Override public Object put(@Nonnull String tag, Object value) { - TagMap.Entry entry = this.getAndSet(Entry.newAnyEntry(tag, value)); + TagMap.Entry entry = this.getAndSet(anyEntryFor(tag, value)); return entry == null ? null : entry.objectValue(); } @@ -1504,32 +1517,106 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { } } + /* + * Entry factories for callers that start from a name: resolve it once, then build a known tag + * from its id or a custom tag from its name. (Entry's own name factories are custom-only.) + */ + static Entry anyEntryFor(String tag, Object value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value); + } + + static Entry objectEntryFor(String tag, CharSequence value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value); + } + + static Entry booleanEntryFor(String tag, boolean value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value); + } + + static Entry intEntryFor(String tag, int value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value); + } + + static Entry longEntryFor(String tag, long value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value); + } + + static Entry floatEntryFor(String tag, float value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value); + } + + static Entry doubleEntryFor(String tag, double value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value); + } + public void set(@Nonnull String tag, @Nonnull Object value) { - this.putEntry(Entry.newAnyEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newAnyEntry(tag, value)); + } } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - this.putEntry(Entry.newObjectEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newObjectEntry(tag, value)); + } } public void set(@Nonnull String tag, boolean value) { - this.putEntry(Entry.newBooleanEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newBooleanEntry(tag, value)); + } } public void set(@Nonnull String tag, int value) { - this.putEntry(Entry.newIntEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newIntEntry(tag, value)); + } } public void set(@Nonnull String tag, long value) { - this.putEntry(Entry.newLongEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newLongEntry(tag, value)); + } } public void set(@Nonnull String tag, float value) { - this.putEntry(Entry.newFloatEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newFloatEntry(tag, value)); + } } public void set(@Nonnull String tag, double value) { - this.putEntry(Entry.newDoubleEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newDoubleEntry(tag, value)); + } } /* @@ -1537,6 +1624,62 @@ public void set(@Nonnull String tag, double value) { * canonical name without the name lookup the String setters pay. The id must name a known tag; * custom tags have no id and use the String setters. */ + /** + * The tracer core's trusted operations on this map, which skip checks the core has already made. + * Restricted to the core; instrumentation uses the checked methods. + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public Internals internals() { + return new Internals(this); + } + + /** + * See {@link TagMap#internals()}. Created per call so escape analysis removes it: use it within + * the expression ({@code map.internals().setKnown(...)}) rather than storing it. + */ + @NoEscape + public static final class Internals { + private final TagMap map; + + Internals(TagMap map) { + this.map = map; + } + + /* + * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has + * already checked KnownTagCodec.isKnown(tagId). + */ + public void setKnown(long tagId, @Nonnull Object value) { + this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); + } + + public void setKnown(long tagId, @Nonnull CharSequence value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + } + + public void setKnown(long tagId, boolean value) { + this.map.putEntry( + new Entry( + tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + } + + public void setKnown(long tagId, int value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + } + + public void setKnown(long tagId, long value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + } + + public void setKnown(long tagId, float value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + } + + public void setKnown(long tagId, double value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + } + } + public void set(long tagId, @Nonnull Object value) { this.putEntry(Entry.newAnyEntry(tagId, value)); } @@ -1589,8 +1732,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { if (wasTombstoned || this.parent == null) { return null; } - // By the entry's own identity, not its name: a shared Datadog name (peer.port) resolves to no - // tag without a direction, and every other name would only resolve back to the same hash. + // By the entry's own identity, not its name: the name would only resolve back to the same hash. return this.parent.getEntry(newEntry.tag, newEntry.tagHash); } @@ -1600,7 +1742,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { * for a prior-value lookup they discard; {@link #getAndSet(Entry)} layers the parent fallback on * top. */ - private Entry putEntry(@Nonnull Entry newEntry) { + Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); // Re-setting a key clears any read-through tombstone for it (the new value overrides the @@ -1655,31 +1797,31 @@ private Entry putEntry(@Nonnull Entry newEntry) { } public Entry getAndSet(@Nonnull String tag, Object value) { - return this.getAndSet(Entry.newAnyEntry(tag, value)); + return this.getAndSet(anyEntryFor(tag, value)); } public Entry getAndSet(@Nonnull String tag, CharSequence value) { - return this.getAndSet(Entry.newObjectEntry(tag, value)); + return this.getAndSet(objectEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, boolean value) { - return this.getAndSet(Entry.newBooleanEntry(tag, value)); + return this.getAndSet(booleanEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, int value) { - return this.getAndSet(Entry.newIntEntry(tag, value)); + return this.getAndSet(intEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, long value) { - return this.getAndSet(Entry.newLongEntry(tag, value)); + return this.getAndSet(longEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, float value) { - return this.getAndSet(Entry.newFloatEntry(tag, value)); + return this.getAndSet(floatEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, double value) { - return this.getAndSet(Entry.newDoubleEntry(tag, value)); + return this.getAndSet(doubleEntryFor(tag, value)); } public void putAll(Map map) { @@ -3163,7 +3305,7 @@ public Object objectValue() { @Override public TagMap.Entry entry() { - return TagMap.Entry.newAnyEntry(this.tag, this.value); + return TagMap.anyEntryFor(this.tag, this.value); } @Override 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 f2022391203..8562ef3c8bf 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,6 +14,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; /** * Checks the generated registry's name resolution and tag ID encoding. Datadog and OpenTelemetry @@ -160,16 +160,39 @@ void unknownNamesResolveToZero() { } /** - * {@code peer.port} means the server's port on outbound spans and the client's on inbound ones, - * so it is one tag per direction. Both share the Datadog name, which therefore resolves to no tag - * until name resolution knows the span's direction. + * {@code peer.port} is declared once per direction, but it is one tag with one id: only its + * OpenTelemetry name depends on the span's direction ({@code client.port} inbound, {@code + * server.port} outbound). */ @Test - void aNameDeclaredPerDirectionIsOneTagPerDirection() { - assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_INBOUND_ID)); - assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertNotEquals(KnownTags.PEER_PORT_INBOUND_ID, KnownTags.PEER_PORT_OUTBOUND_ID); - assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); + void aNameDeclaredPerDirectionIsOneTag() { + assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.keyOf(Tags.PEER_PORT)); + assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_ID)); + assertNull(KnownTagCodec.openTelemetryNameOf(KnownTags.PEER_PORT_ID)); + assertEquals(Tags.PEER_PORT, KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID)); + assertEquals( + "client.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_INBOUND)); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND)); + assertEquals( + Tags.PEER_PORT, + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_NONE)); + } + + @TableTest({ + "scenario | kind | direction", + "server | server | 0 ", + "consumer | consumer | 0 ", + "client | client | 1 ", + "producer | producer | 1 ", + "internal | internal | 2 ", + "broker | broker | -1 ", + "custom | custom | -1 " + }) + void theDirectionOfASpanFollowsItsKind(String kind, int direction) { + assertEquals(direction, KnownTagCodec.directionOfKind(kind)); } @Test @@ -237,4 +260,58 @@ void serialEncodingRoundTrips() { assertEquals(263, KnownTagCodec.serialNum(id)); assertFalse(KnownTagCodec.isTraceLevel(id)); } + + @Test + void theInterceptedBitMarksTheTracerOverlayTags() { + assertTrue(KnownTagCodec.isIntercepted(KnownTags.SPAN_KIND_ID)); + assertFalse(KnownTagCodec.isIntercepted(KnownTags.HTTP_ROUTE_ID)); + } + + @Test + void isKnownRejectsTheNoTagSerialAndSerialsPastTheLimit() { + assertTrue(KnownTagCodec.isKnown(KnownTags.HTTP_ROUTE_ID)); + assertFalse(KnownTagCodec.isKnown(0L)); + assertFalse(KnownTagCodec.isKnown(KnownTagCodec.makeTagId(KnownTagCodec.serialLimit()))); + } + + @Test + void canonicalTagNameMapsAnOpenTelemetryRenameToItsDatadogName() { + assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(KnownTags.HTTP_METHOD_OTEL_NAME)); + assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(Tags.HTTP_METHOD)); + assertEquals("my.custom.tag", KnownTagCodec.canonicalTagName("my.custom.tag")); + } + + @TableTest({ + "scenario | kind | direction", + "no kind | | -1 ", + "server | 'server' | 0 ", + "consumer | 'consumer' | 0 ", + "client | 'client' | 1 ", + "producer | 'producer' | 1 ", + "internal | 'internal' | 2 ", + "broker | 'broker' | -1 " + }) + void directionOfKind(String kind, int direction) { + assertEquals(direction, KnownTagCodec.directionOfKind(kind)); + } + + @Test + void directionOfReadsTheMapsSpanKind() { + TagMap tags = TagMap.create(); + assertEquals(KnownTagCodec.DIRECTION_UNKNOWN, KnownTagCodec.directionOf(tags)); + + tags.set(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT); + assertEquals(KnownTagCodec.DIRECTION_OUTBOUND, KnownTagCodec.directionOf(tags)); + } + + @Test + void anEntrysDirectionalOpenTelemetryTagFallsBackToItsNameForACustomTag() { + TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); + assertEquals("my.custom.tag", custom.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); + + TagMap.Entry peerPort = TagMap.Entry.newAnyEntry(KnownTags.PEER_PORT_ID, 443); + assertEquals( + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND), + peerPort.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java index fbd197e1839..5001994e2b0 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java @@ -594,23 +594,6 @@ void getAndSetByIdReturnsTheInheritedEntryOfThatId() { assertEquals("/parent/{id}", prior.objectValue()); } - /** - * A shared Datadog name ({@code peer.port}) resolves to no tag without a direction, so the - * inherited prior must be found by the entry's id, not by its name. - */ - @Test - void getAndSetBySharedNameIdReturnsTheInheritedEntryOfThatId() { - TagMap parent = TagMap.create(); - parent.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); - TagMap child = TagMap.createFromParent(parent.freeze()); - - TagMap.Entry prior = - child.getAndSet(TagMap.Entry.create(KnownTags.PEER_PORT_OUTBOUND_ID, (Object) 443)); - - assertNotNull(prior, "the inherited peer.port@outbound is the previous value"); - assertEquals(5432, prior.objectValue()); - } - @Test void setDoesNotReportPriorButStillClearsTombstone() { TagMap child = TagMap.createFromParent(frozenParent()); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index ecef85afdd4..4634a02da58 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -72,4 +72,40 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(0L, "value")); assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } + + @Test + void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { + TagMap checked = TagMap.create(); + checked.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + checked.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + checked.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + checked.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + + TagMap trusted = TagMap.create(); + trusted.internals().setKnown(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + trusted.internals().setKnown(KnownTags.HTTP_STATUS_CODE_ID, 200); + trusted.internals().setKnown(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + trusted.internals().setKnown(KnownTags.DD_PROFILING_ENABLED_ID, true); + + assertEquals(new HashMap<>(checked), new HashMap<>(trusted)); + assertEquals(KnownTags.HTTP_ROUTE_ID, trusted.getEntry(KnownTags.HTTP_ROUTE_NAME).tagId()); + } + + @Test + void theCoresTrustedSetsCoverTheRemainingValueShapes() { + Object component = new StringBuilder("jdbc"); // not a String: exercises the Object overload + TagMap checked = TagMap.create(); + checked.set(KnownTags.COMPONENT_ID, component); + checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); + + TagMap trusted = TagMap.create(); + trusted.internals().setKnown(KnownTags.COMPONENT_ID, component); + trusted.internals().setKnown(KnownTags.DB_POOL_NAME_ID, 1.5f); + trusted.internals().setKnown(KnownTags.DB_INSTANCE_ID, 2.25d); + + assertEquals(new HashMap<>(checked), new HashMap<>(trusted)); + assertEquals(1.5f, trusted.getEntry(KnownTags.DB_POOL_NAME_ID).floatValue()); + assertEquals(2.25d, trusted.getEntry(KnownTags.DB_INSTANCE_ID).doubleValue()); + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java new file mode 100644 index 00000000000..3042436cb54 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java @@ -0,0 +1,49 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * TagMap and its nested classes have no synthetic accessors. Targeting Java 8, javac reaches a + * private member of a nested or outer class through a generated {@code access$NNN} method, or a + * constructor taking a dummy {@code $1} class. C2 refuses to inline a call whose signature names a + * never-loaded class, and every accessor adds a level of inlining depth, so on TagMap's hot paths a + * member shared between nested classes must be package-private instead. Becomes a + * {@code @NoSyntheticAccess} contract check (APMLP-1976). + */ +class TagMapSyntheticAccessorTest { + @Test + void theTagMapFamilyHasNoSyntheticAccessors() { + List> classes = new ArrayList<>(); + for (Class root : Arrays.asList(TagMap.class)) { + collect(root, classes); + } + List accessors = new ArrayList<>(); + for (Class c : classes) { + for (Method method : c.getDeclaredMethods()) { + if (method.isSynthetic() && method.getName().startsWith("access$")) { + accessors.add(method.toString()); + } + } + for (Constructor constructor : c.getDeclaredConstructors()) { + if (constructor.isSynthetic()) { + accessors.add(constructor.toString()); + } + } + } + assertEquals(new ArrayList(), accessors); + } + + private static void collect(Class c, List> into) { + into.add(c); + for (Class nested : c.getDeclaredClasses()) { + collect(nested, into); + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index 5a5e56b7c78..3a9c6a585df 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -17,8 +18,8 @@ class TagMapTagHashTest { @Test void aKnownTagHashesToItsIdHoweverItIsSet() { TagMap.Entry byId = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "GET"); - TagMap.Entry byName = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_NAME, "GET"); - TagMap.Entry byOtelName = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + TagMap.Entry byName = TagMap.anyEntryFor(KnownTags.HTTP_METHOD_NAME, "GET"); + TagMap.Entry byOtelName = TagMap.anyEntryFor(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); assertEquals(KnownTags.HTTP_METHOD_ID, byId.tagHash); assertEquals(KnownTags.HTTP_METHOD_ID, byName.tagHash); @@ -26,6 +27,15 @@ void aKnownTagHashesToItsIdHoweverItIsSet() { assertEquals(KnownTags.HTTP_METHOD_ID, byName.tagId()); } + @Test + void theNameFactoriesAreForCustomTagsOnly() { + // Entry's name factories skip the registry, so a known name must come in through its id; + // anyEntryFor and friends resolve a name first. (Tests run with assertions enabled.) + assertThrows( + AssertionError.class, () -> TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_NAME, "GET")); + assertEquals(0L, TagMap.Entry.newAnyEntry("my.custom.tag", "value").tagId()); + } + @Test void aCustomTagHashesToItsNameWithNoId() { TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); @@ -72,9 +82,6 @@ void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessExcept assertEquals(tagIds.size(), copy.size()); for (long tagId : tagIds) { String name = KnownTagCodec.nameOf(tagId); - if (KnownTagCodec.keyOf(name) != tagId) { - continue; // a name declared per direction does not resolve to either direction's id - } assertEquals("value", map.getObject(name), name); assertEquals("value", copy.getObject(name), name); } @@ -84,7 +91,10 @@ private static List knownTagIds() throws IllegalAccessException { List tagIds = new ArrayList<>(); for (Field field : KnownTags.class.getFields()) { if (field.getType() == long.class && Modifier.isStatic(field.getModifiers())) { - tagIds.add(field.getLong(null)); + long tagId = field.getLong(null); + if (KnownTagCodec.isKnown(tagId)) { + tagIds.add(tagId); + } } } return tagIds; diff --git a/tag-conventions-java.yaml b/tag-conventions-java.yaml new file mode 100644 index 00000000000..2f3d89e39ed --- /dev/null +++ b/tag-conventions-java.yaml @@ -0,0 +1,40 @@ +# Tracer overlay for tag-conventions.yaml — dd-trace-java's set-path routing. +# --------------------------------------------------------------------------- +# tag-conventions.yaml is language-agnostic; which keys a tracer intercepts on the set-path (routing +# them to a span field or a sampling directive instead of tag storage) is this tracer's concern, so +# it lives here. +# +# tags — keys that exist only to be routed, so the conventions do not declare them. They get +# ids like any other tag (instrumentation can set them by id) but belong to no span type. +# Each is intercepted, so it is not listed again below. Same declaration syntax as +# tag-conventions.yaml; refs are not allowed. +# intercepted — tags declared in tag-conventions.yaml that TagInterceptor also routes. Each one's id carries +# the INTERCEPTED bit, so a setter called with a constant id folds the interception test +# away. TagInterceptor's switch is the authority on what each one does; a test there +# keeps this list and the switch in agreement. +# --------------------------------------------------------------------------- + +tags: + - { dd-name: resource.name, type: string } + - { dd-name: span.type, type: string } + - { dd-name: error, type: boolean } + - { dd-name: manual.keep, type: boolean } + - { dd-name: manual.drop, type: boolean } + - { dd-name: asm.keep, type: boolean } + - { dd-name: ai_guard.keep, type: boolean } + - { dd-name: sampling.priority, type: int } + - { dd-name: _dd.p.ts, type: int } + - { dd-name: _dd.p.debug, type: string } + - { dd-name: _dd.origin, type: string } + - { dd-name: _dd.measured, type: boolean } + - { dd-name: _dd1.sr.eausr, type: double } + +intercepted: + - service + - peer.service + - span.kind + - servlet.context + - db.statement + - http.method + - http.url + - http.status_code diff --git a/tag-conventions.yaml b/tag-conventions.yaml index b6cdfe5deb4..b54462a6a62 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -42,13 +42,12 @@ # abstract type, a plain mixin) it applies in every direction; in a directional scope, only in that # direction. # A tag whose MEANING flips with direction, like peer.port (the server's port on an outbound span, the -# client's on an inbound one), is declared once per direction, each in a directional mixin. Each -# declaration becomes its own tag, with fixed names in both namespaces; the Datadog name is shared. -# (The generator labels these tags `@` in its reports; that label is not YAML -# syntax.) Any other repeated declaration is still an error. -# Until name resolution knows a span's direction, only renames that apply in every direction are used. -# A directional rename, including one under `span-kind: internal` (spans with no direction), is -# listed in tag-assignment.txt but not yet applied. +# client's on an inbound one), is declared once per direction, each in a directional mixin. The +# declarations merge into ONE tag with one id; only its OpenTelemetry name depends on the span's +# direction. Any other repeated declaration is still an error. +# Name resolution does not yet know a span's direction, so only renames that apply in every direction +# resolve a name to a tag. A directional rename, including one under `span-kind: internal` (spans with +# no direction), is applied only when a span's tags are emitted under OpenTelemetry names. # `span-kind-neutral: true` widens a directional rename to every direction -- the author's assertion # that OTel only uses that name for this tag (db.system, say, only ever names a database). It is # interim, and goes away once resolution is direction-aware. A rename on a concrete type with no @@ -162,9 +161,9 @@ mixins: - { dd-name: peer.ipv4, type: string } - { dd-name: peer.ipv6, type: string } - # outbound_peer / inbound_peer declare peer.port once per direction. `peer.port` is then a SHARED - # Datadog name for two tags (reported as peer.port@outbound and peer.port@inbound): emitting it needs no context, - # but resolving the bare name needs the span's direction. + # outbound_peer / inbound_peer declare peer.port once per direction. It is still one tag + # (PEER_PORT_ID); only its OpenTelemetry name depends on the span's direction: client.port on + # inbound spans, server.port on outbound ones. # outbound_peer — the other end of an outbound connection: the server. outbound_peer: @@ -200,8 +199,9 @@ mixins: # - span.kind enumerates: server | client | producer | consumer | internal | broker. # - Some keys (resource.name, error, sampling.priority, ...) are accepted by setTag but routed to a # span field or a trace directive instead of tag storage. That routing is a per-language tracer -# concern, so it is NOT modelled here; such a key appears above only when it also needs an id and -# a name (service does, for OpenTelemetry's service.name). +# concern, so it is NOT modelled here: dd-trace-java declares those keys, and marks every tag it +# intercepts, in its own overlay (tag-conventions-java.yaml). Such a key appears above only when it +# also needs an id and a name across tracers (service does, for OpenTelemetry's service.name). # - 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. # - db.statement/db.query.text is deliberately never re-exposed as a stored tag once consumed into