diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 441a90dcba0..31e30f879a2 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -73,6 +73,10 @@ gradlePlugin { id = "dd-trace-java.sca-enrichments" implementationClass = "datadog.gradle.plugin.sca.ScaEnrichmentsPlugin" } + create("tag-registry-generator") { + id = "dd-trace-java.tag-registry-generator" + implementationClass = "datadog.gradle.plugin.tags.TagRegistryGeneratorPlugin" + } } } @@ -102,6 +106,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.fasterxml.jackson.core:jackson-annotations") implementation("com.fasterxml.jackson.core:jackson-core") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") compileOnly(libs.develocity) } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/GenerateKnownTagsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/GenerateKnownTagsTask.kt new file mode 100644 index 00000000000..ab1c2631c60 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/GenerateKnownTagsTask.kt @@ -0,0 +1,38 @@ +package datadog.gradle.plugin.tags + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Generates the committed tag registry (KnownTags.java + layout reports) from the language-agnostic + * {@code tag-conventions.yaml} + the Java overlay. The actual emit lives in [TagRegistryGenerator]; + * this task just wires the inputs/outputs so Gradle can cache and up-to-date-check it. + */ +@CacheableTask +abstract class GenerateKnownTagsTask @Inject constructor(objects: ObjectFactory) : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + val domainYaml: RegularFileProperty = objects.fileProperty() + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + val overlayYaml: RegularFileProperty = objects.fileProperty() + + @get:OutputDirectory val destinationDirectory: DirectoryProperty = objects.directoryProperty() + + @TaskAction + fun generate() { + val outDir = destinationDirectory.get().asFile + TagRegistryGenerator.generate(domainYaml.get().asFile, overlayYaml.get().asFile, outDir) + logger.lifecycle("tag-registry: generated -> $outDir") + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt new file mode 100644 index 00000000000..ef1139c9c92 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/KnownTagsEmitter.kt @@ -0,0 +1,122 @@ +package datadog.gradle.plugin.tags + +/** + * Emits the generated `KnownTags.java` from a [TagRegistry]: id constants (literal + a + * `// tagId(...)` derivation comment), the `StringIndex.Support` keyOf table, the `globalSerial` + * switch `nameOf`, and resolver registration. Mirrors the hand-written KnownTags structure. + */ +object KnownTagsEmitter { + + fun emit(reg: TagRegistry, pkg: String, className: String): String { + // Sanitize tag names into unique Java constant identifiers. + val used = HashSet() + val cname = HashMap() + fun mk(name: String): String { + var c = name.uppercase().replace(Regex("[^A-Za-z0-9]"), "_").replace(Regex("_+"), "_").trim('_') + if (c.isEmpty() || c[0].isDigit()) c = "T_$c" + var u = c + var n = 2 + while (u in used) { + u = "${c}_$n"; n++ + } + used.add(u) + cname[name] = u + return u + } + reg.virtuals.forEach { mk(it.name) } + reg.stored.forEach { mk(it.name) } + + val order = reg.virtuals.map { it.name } + reg.stored.map { it.name } // stable emit order + val b = StringBuilder() + b.appendLine("package $pkg;") + b.appendLine() + b.appendLine("import datadog.trace.util.StringIndex;") + b.appendLine() + b.appendLine("// GENERATED by the tag-registry code generator (dd-trace-java.tag-registry-generator).") + b.appendLine("// DO NOT EDIT. Source: tag-conventions.yaml + tag-conventions.java.yaml.") + b.appendLine("public final class $className {") + b.appendLine(" static final int SLOT_COUNT = ${reg.slotCount};") + b.appendLine() + + b.appendLine(" // ---- reserved / virtual (routed to span fields or directives; not stored) ----") + for (v in reg.virtuals) { + val cn = cname[v.name]!! + b.appendLine(" static final int ${cn}_SERIAL = ${v.serial};") + b.appendLine(" // tagId(serial=${v.serial}, intercepted=true, slot=NO_SLOT) [${v.kind}${v.field?.let { " -> $it" } ?: ""}] \"${v.name}\"") + b.appendLine(" public static final long $cn = ${hex(v.id)};") + } + b.appendLine() + + b.appendLine(" // ---- stored (dense-store slot, or bucketed when slot=NO_SLOT) ----") + for (t in reg.stored) { + val cn = cname[t.name]!! + val slot = if (t.slotted) t.slot.toString() else "NO_SLOT" + b.appendLine(" static final int ${cn}_SERIAL = ${t.serial};") + b.appendLine(" // tagId(serial=${t.serial}, intercepted=${t.intercepted}, slot=$slot) <${t.required}${if (t.traceLevel) ", trace-level" else ""}> \"${t.name}\"") + b.appendLine(" public static final long $cn = ${hex(t.id)};") + } + b.appendLine() + + // keyOf table (open-addressed, via StringIndex.Support). + b.appendLine(" private static final String[] KEYOF_NAMES = {") + order.forEach { b.appendLine(" \"$it\",") } + b.appendLine(" };") + b.appendLine(" private static final long[] KEYOF_VALUES = {") + order.forEach { b.appendLine(" ${cname[it]},") } + b.appendLine(" };") + b.appendLine(" private static final int[] KEYOF_HASHES;") + b.appendLine(" private static final String[] KEYOF_KEYS;") + b.appendLine(" private static final long[] KEYOF_IDS;") + b.appendLine(" static {") + b.appendLine(" StringIndex.Data data = StringIndex.Support.create(KEYOF_NAMES);") + b.appendLine(" long[] ids = new long[data.names.length];") + b.appendLine(" for (int j = 0; j < KEYOF_NAMES.length; j++) {") + b.appendLine(" ids[StringIndex.Support.indexOf(data.hashes, data.names, KEYOF_NAMES[j])] = KEYOF_VALUES[j];") + b.appendLine(" }") + b.appendLine(" KEYOF_HASHES = data.hashes;") + b.appendLine(" KEYOF_KEYS = data.names;") + b.appendLine(" KEYOF_IDS = ids;") + b.appendLine(" }") + b.appendLine() + + // Resolver. + b.appendLine(" static final KnownTagCodec.Resolver RESOLVER =") + b.appendLine(" new KnownTagCodec.Resolver() {") + b.appendLine(" @Override") + b.appendLine(" public String nameOf(long tagId) {") + b.appendLine(" switch (KnownTagCodec.globalSerial(tagId)) {") + for (name in order) { + b.appendLine(" case ${cname[name]}_SERIAL:") + b.appendLine(" return \"$name\";") + } + b.appendLine(" default:") + b.appendLine(" return null;") + b.appendLine(" }") + b.appendLine(" }") + b.appendLine() + b.appendLine(" @Override") + b.appendLine(" public int slotCount() {") + b.appendLine(" return SLOT_COUNT;") + b.appendLine(" }") + b.appendLine() + b.appendLine(" @Override") + b.appendLine(" public long keyOf(String name) {") + b.appendLine(" int slot = StringIndex.Support.indexOf(KEYOF_HASHES, KEYOF_KEYS, name);") + b.appendLine(" return slot < 0 ? 0L : KEYOF_IDS[slot];") + b.appendLine(" }") + b.appendLine(" };") + b.appendLine() + b.appendLine(" static {") + b.appendLine(" KnownTagCodec.register(RESOLVER);") + b.appendLine(" }") + b.appendLine() + b.appendLine(" /** Forces resolver registration by triggering . Idempotent. */") + b.appendLine(" public static void init() {}") + b.appendLine() + b.appendLine(" private $className() {}") + b.appendLine("}") + return b.toString() + } + + private fun hex(id: Long): String = "0x%016XL".format(id) +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt new file mode 100644 index 00000000000..43ab8901e2f --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagConventions.kt @@ -0,0 +1,150 @@ +package datadog.gradle.plugin.tags + +/** + * Parsed tag-conventions domain model + the per-type tag-set resolver. Language-agnostic: it knows + * only structure (extends / include / applies) and per-tag semantics (name / type / required / + * source). Id assignment, coloring, and emission are layered on top of the resolved sets. + */ +class TagConventions +private constructor( + private val spanTypes: Map, + private val mixins: Map, + private val traceLevel: List, +) { + /** A tag declaration (domain semantics only). */ + data class Tag( + val name: String, + val type: String, + val required: String, + ) + + data class SpanType( + val name: String, + val abstract: Boolean, + val extends: String?, + val include: List, + val tags: List, + ) + + data class Mixin( + val name: String, + val appliesAll: Boolean, + val appliesTo: Set, + val tags: List, + ) + + /** Concrete (instantiable) span types — the ones a layout is computed for. */ + fun concreteTypes(): List = + spanTypes.values.filter { !it.abstract }.map { it.name }.sorted() + + /** + * resolved(type) = own tags + tags up the `extends` chain (incl. base) + tags of every mixin the + * type or an ancestor `include`s + tags of every mixin whose `applies` matches. De-duped by tag + * name (first occurrence wins). Base-first order, so it is stable across runs. + */ + fun resolve(typeName: String): List { + val result = LinkedHashMap() + fun add(t: Tag) = result.putIfAbsent(t.name, t) + + val chain = ArrayList() + var cur: SpanType? = spanTypes[typeName] + while (cur != null) { + chain.add(cur) + cur = cur.extends?.let { spanTypes[it] } + } + for (st in chain.asReversed()) { + st.tags.forEach { add(it) } + for (mixinName in st.include) mixins[mixinName]?.tags?.forEach { add(it) } + } + val chainNames = chain.map { it.name }.toSet() + for (mx in mixins.values) { + if (mx.appliesAll || mx.appliesTo.any { it in chainNames }) mx.tags.forEach { add(it) } + } + return result.values.toList() + } + + /** The explicit trace-level tier tags (their own TagMap "type" on the TraceSegment). */ + fun traceLevelTags(): List = traceLevel + + /** Full stored-tag universe (concrete span types' resolves + trace-level), de-duped by name. */ + fun allStoredTags(): List { + val union = LinkedHashMap() + for (type in concreteTypes()) for (t in resolve(type)) union.putIfAbsent(t.name, t) + for (t in traceLevel) union.putIfAbsent(t.name, t) + return union.values.toList() + } + + /** Resolved tag NAMES per concrete type — the co-occurrence structure the colorer needs. */ + fun coOccurrence(): Map> = + concreteTypes().associateWith { resolve(it).map { t -> t.name }.toSet() } + + /** + * Full composition for a type as (origin, tag) pairs, in composition order and NOT de-duped, so a + * tag contributed by more than one source shows up more than once. Origin is the contributing + * span type (via extends), `incl:` (via include), or `appl:` (via applies). + */ + fun compose(typeName: String): List> { + val out = ArrayList>() + val chain = ArrayList() + var cur: SpanType? = spanTypes[typeName] + while (cur != null) { + chain.add(cur) + cur = cur.extends?.let { spanTypes[it] } + } + for (st in chain.asReversed()) { + st.tags.forEach { out.add(st.name to it) } + for (mixinName in st.include) mixins[mixinName]?.tags?.forEach { out.add("incl:$mixinName" to it) } + } + val chainNames = chain.map { it.name }.toSet() + for (mx in mixins.values) { + if (mx.appliesAll || mx.appliesTo.any { it in chainNames }) { + mx.tags.forEach { out.add("appl:${mx.name}" to it) } + } + } + return out + } + + companion object { + @Suppress("UNCHECKED_CAST") + fun parse(root: Map): TagConventions { + val spanTypesRaw = (root["span_types"] as? Map) ?: emptyMap() + val spanTypes = + spanTypesRaw.mapValues { (name, v) -> + val m = v as Map + SpanType( + name = name, + abstract = (m["abstract"] as? Boolean) ?: false, + extends = m["extends"] as? String, + include = (m["include"] as? List) ?: emptyList(), + tags = tagList(m["tags"]), + ) + } + + val mixinsRaw = (root["mixins"] as? Map) ?: emptyMap() + val mixins = + mixinsRaw.mapValues { (name, v) -> + val m = v as Map + val applies = m["applies"] + Mixin( + name = name, + appliesAll = applies == "all", + appliesTo = if (applies is List<*>) applies.map { it.toString() }.toSet() else emptySet(), + tags = tagList(m["tags"]), + ) + } + + val traceLevel = tagList((root["trace_level"] as? Map)?.get("tags")) + return TagConventions(spanTypes, mixins, traceLevel) + } + + @Suppress("UNCHECKED_CAST") + private fun tagList(tags: Any?): List = + (tags as? List>)?.map { m -> + Tag( + name = m["tag"].toString(), + type = (m["type"] as? String) ?: "string", + required = (m["required"] as? String) ?: "optional", + ) + } ?: emptyList() + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt new file mode 100644 index 00000000000..731602c3ac4 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt @@ -0,0 +1,125 @@ +package datadog.gradle.plugin.tags + +/** + * Assigns tag ids + slots from a parsed [TagConventions] plus the Java overlay (intercepted set + + * virtual registry). Slots come from deterministic greedy graph coloring over the co-occurrence + * graph; colorability is derived from the domain `required` level. The id encoding mirrors + * KnownTagCodec: [63 intercepted][62-48 serial][47-32 slot][31-0 zero] (known ids carry no nameHash). + * + * Level-split: trace/process-constant tags (`source: core`) are their own TagMap "type" -- the + * `` layer that lives on the TraceSegment, not the per-span layout. It is just another + * coloring layer, so its tags get slots among themselves and reuse slot numbers with the span + * layers (a different TagMap, so no collision). + */ +class TagRegistry +private constructor( + val stored: List, + val virtuals: List, + val slotCount: Int, +) { + data class StoredTag( + val name: String, + val type: String, + val required: String, + val serial: Int, + val intercepted: Boolean, + val slot: Int, + val traceLevel: Boolean, + val id: Long, + ) { + val slotted: Boolean + get() = slot != NO_SLOT + } + + data class VirtualTag( + val name: String, + val kind: String, + val field: String?, + val serial: Int, + val id: Long, + ) + + /** Java overlay: intercepted tag names + the virtual/special-key registry. */ + class Overlay(val intercepted: Set, val virtuals: List) { + data class VirtualDef(val name: String, val kind: String, val field: String?) + + companion object { + @Suppress("UNCHECKED_CAST") + fun parse(root: Map): Overlay { + val intercepted = (root["intercepted"] as? List)?.toSet() ?: emptySet() + val virtuals = + (root["virtual"] as? List>)?.map { m -> + VirtualDef(m["tag"].toString(), (m["kind"] as? String) ?: "directive", m["field"] as? String) + } ?: emptyList() + return Overlay(intercepted, virtuals) + } + } + } + + companion object { + const val FIRST_STORED_SERIAL = 256 + const val NO_SLOT = 0xFFFF + const val TRACE_LAYER = "" + val COLORABLE = setOf("required", "conditional", "recommended") + + /** Mirrors KnownTagCodec.tagId(serial, intercepted, slot) — must stay in sync with it. */ + fun encode(serial: Int, intercepted: Boolean, slot: Int): Long { + val id = (serial.toLong() shl 48) or ((slot.toLong() and 0xFFFF) shl 32) + return if (intercepted) id or Long.MIN_VALUE else id + } + + fun build(conv: TagConventions, overlay: Overlay): TagRegistry { + val all = conv.allStoredTags() + val coOcc = conv.coOccurrence() // span type -> {tag names} + + val colorable = all.filter { it.required in COLORABLE }.map { it.name }.toSet() + // Trace-level tier is EXPLICIT (the trace_level section), not inferred from source — so + // core-set-but-per-span tags (parent_id / integration / svc_src on `base`) stay per-span. + val traceNames = conv.traceLevelTags().map { it.name }.toSet() + + // Coloring layers = each concrete span type + the layer (colorable trace-level tags). + // Tags within a layer form a clique. + val layers = LinkedHashMap>() + for ((type, names) in coOcc) layers[type] = names.filter { it in colorable && it !in traceNames }.toSet() + layers[TRACE_LAYER] = traceNames.filter { it in colorable }.toSet() + + val adj = HashMap>() + colorable.forEach { adj[it] = HashSet() } + for ((_, members) in layers) { + val clique = members.toList() + for (i in clique.indices) for (j in i + 1 until clique.size) { + adj[clique[i]]!!.add(clique[j]) + adj[clique[j]]!!.add(clique[i]) + } + } + + // Color the tags in the most layers first, so always-present base tags get the low slots. + val freq = colorable.associateWith { n -> layers.count { n in it.value } } + val order = colorable.sortedWith(compareByDescending { freq[it] }.thenBy { it }) + val color = HashMap() + for (n in order) { + val used = adj[n]!!.mapNotNull { color[it] }.toSet() + var c = 0 + while (c in used) c++ + color[n] = c + } + val slotCount = (color.values.maxOrNull() ?: -1) + 1 + + val virtuals = + overlay.virtuals.mapIndexed { i, v -> + val serial = 1 + i + VirtualTag(v.name, v.kind, v.field, serial, encode(serial, intercepted = true, slot = NO_SLOT)) + } + val stored = + all.sortedBy { it.name }.mapIndexed { i, t -> + val serial = FIRST_STORED_SERIAL + i + val intercepted = t.name in overlay.intercepted + val slot = color[t.name] ?: NO_SLOT + StoredTag(t.name, t.type, t.required, serial, intercepted, slot, + traceLevel = t.name in traceNames, id = encode(serial, intercepted, slot)) + } + + return TagRegistry(stored, virtuals, slotCount) + } + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistryGenerator.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistryGenerator.kt new file mode 100644 index 00000000000..4888ff6aec9 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistryGenerator.kt @@ -0,0 +1,171 @@ +package datadog.gradle.plugin.tags + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import java.io.File + +/** + * Turns the language-agnostic {@code tag-conventions.yaml} + the Java overlay into the generated tag + * registry: {@code KnownTags.java} (under {@code java/}) plus verification report dumps + * (resolved-tags / tag-assignment / layout-by-type / folded-types) at the destination root. + * + * Pure function of its inputs (deterministic ordering throughout), so the same inputs always produce + * byte-identical output -- which is what the {@code verifyKnownTags} freshness gate relies on. + */ +object TagRegistryGenerator { + /** Parses the two YAML files and writes the full generated tree under [outDir]. */ + fun generate(domainYaml: File, overlayYaml: File, outDir: File) { + val mapper = ObjectMapper(YAMLFactory()) + val domain: Map = + domainYaml.inputStream().use { + mapper.readValue(it, object : TypeReference>() {}) + } + val overlayMap: Map = + overlayYaml.inputStream().use { + mapper.readValue(it, object : TypeReference>() {}) + } + + outDir.mkdirs() + // KnownTags.java goes under java/ (added as a srcDir); the .txt reports sit at the root. + val javaPkg = File(outDir, "java/datadog/trace/api").apply { mkdirs() } + + val conv = TagConventions.parse(domain) + val overlay = TagRegistry.Overlay.parse(overlayMap) + val reg = TagRegistry.build(conv, overlay) + + File(outDir, "resolved-tags.txt").writeText(resolvedReport(conv)) + File(outDir, "tag-assignment.txt").writeText(assignmentReport(conv, reg)) + File(outDir, "layout-by-type.txt").writeText(layoutByTypeReport(conv, reg)) + File(outDir, "folded-types.txt").writeText(foldedTypesReport(conv, reg)) + File(javaPkg, "KnownTags.java") + .writeText(KnownTagsEmitter.emit(reg, "datadog.trace.api", "KnownTags")) + } + + /** resolved-tags.txt — the per-type resolved sets (composition check). */ + private fun resolvedReport(conv: TagConventions): String { + val resolved = StringBuilder() + resolved.appendLine("# Resolved per-type tag sets (concrete span types).") + for (type in conv.concreteTypes()) { + val tags = conv.resolve(type) + resolved.appendLine() + resolved.appendLine("$type (${tags.size} tags):") + for (t in tags) resolved.appendLine(" - ${t.name}") + } + return resolved.toString() + } + + /** tag-assignment.txt — serials, slots (coloring), ids, per-type packed sizes (assignment check). */ + private fun assignmentReport(conv: TagConventions, reg: TagRegistry): String { + val byName = reg.stored.associateBy { it.name } + val a = StringBuilder() + a.appendLine( + "# Tag id assignment. slotCount=${reg.slotCount} stored=${reg.stored.size} virtual=${reg.virtuals.size}") + a.appendLine() + a.appendLine("# STORED serial slot int id required name") + for (t in reg.stored) { + a.appendLine( + " %6d %4s %s %-18s %-12s %s".format( + t.serial, + if (t.slotted) t.slot.toString() else "-", + if (t.intercepted) "I" else "-", + "0x%016X".format(t.id), + t.required, + t.name)) + } + a.appendLine() + a.appendLine("# VIRTUAL serial id kind name") + for (v in reg.virtuals) { + a.appendLine( + " %6d %-18s %-12s %s%s".format( + v.serial, "0x%016X".format(v.id), v.kind, v.name, v.field?.let { " -> $it" } ?: "")) + } + a.appendLine() + a.appendLine("# PER-TYPE packed size (= max slot + 1). = the trace-level TagMap layer.") + for (type in conv.concreteTypes()) { + val slots = + conv.resolve(type).mapNotNull { byName[it.name] } + .filter { it.slotted && !it.traceLevel } + .map { it.slot } + .sorted() + val size = (slots.maxOrNull() ?: -1) + 1 + a.appendLine(" %-14s size=%-3d slots=%s".format(type, size, slots)) + } + val traceSlots = reg.stored.filter { it.traceLevel && it.slotted }.map { it.slot }.sorted() + a.appendLine( + " %-14s size=%-3d slots=%s".format("", (traceSlots.maxOrNull() ?: -1) + 1, traceSlots)) + return a.toString() + } + + /** + * layout-by-type.txt — full composition per type (origins shown, NOT de-duped), each tag annotated + * with its slot/tier: s = per-span slot, trace = trace-level layer, bkt = bucket. + */ + private fun layoutByTypeReport(conv: TagConventions, reg: TagRegistry): String { + val byName = reg.stored.associateBy { it.name } + val lay = StringBuilder() + lay.appendLine("# Full tag composition per concrete span type (after extends/include/applies).") + lay.appendLine("# Not de-duped: a tag from >1 source appears >1 time.") + lay.appendLine("# annotation: [s per-span slot | trace trace layer | bkt bucketed] I=intercepted") + for (type in conv.concreteTypes()) { + val comp = conv.compose(type) + val distinct = comp.map { it.second.name }.distinct().size + lay.appendLine() + lay.appendLine("$type (${comp.size} contributions, $distinct distinct):") + val byOrigin = LinkedHashMap>() + for ((origin, tag) in comp) byOrigin.getOrPut(origin) { ArrayList() }.add(tag) + for ((origin, tags) in byOrigin) { + lay.appendLine(" [$origin]") + for (t in tags) { + val st = byName[t.name] + val slot = + when { + st == null -> "?" + st.traceLevel && st.slotted -> "trace${st.slot}" + st.slotted -> "s${st.slot}" + else -> "bkt" + } + lay.appendLine( + " %-26s %-8s %-12s %s".format(t.name, slot, t.required, if (st?.intercepted == true) "I" else "")) + } + } + } + return lay.toString() + } + + /** + * folded-types.txt — each type's full resolved set (extends + include + applies, DE-DUPED) with its + * slot; plus the type. This is the "type with everything folded in" view. + */ + private fun foldedTypesReport(conv: TagConventions, reg: TagRegistry): String { + val byName = reg.stored.associateBy { it.name } + fun tierSlot(st: TagRegistry.StoredTag?): String = + when { + st == null -> "?" + st.traceLevel -> if (st.slotted) "trace${st.slot}" else "trace-bkt" + st.slotted -> "s${st.slot}" + else -> "bkt" + } + val f = StringBuilder() + f.appendLine("# Folded tag set per type (extends + include + applies, de-duped), with slots.") + f.appendLine( + "# slot: s=per-span bit trace=trace-level layer bit bkt=bucketed trace-bkt=trace-level bucketed I=intercepted") + for (type in conv.concreteTypes()) { + val tags = conv.resolve(type) + f.appendLine() + f.appendLine("$type (${tags.size} tags):") + for (t in tags) { + val st = byName[t.name] + f.appendLine(" %-9s %-26s %s".format(tierSlot(st), t.name, if (st?.intercepted == true) "I" else "")) + } + } + val traceTags = + reg.stored.filter { it.traceLevel }.sortedWith(compareBy({ !it.slotted }, { it.slot }, { it.name })) + f.appendLine() + f.appendLine(" (${traceTags.size} tags):") + for (st in traceTags) { + f.appendLine(" %-9s %-26s %s".format(tierSlot(st), st.name, if (st.intercepted) "I" else "")) + } + return f.toString() + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistryGeneratorPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistryGeneratorPlugin.kt new file mode 100644 index 00000000000..313bd859068 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistryGeneratorPlugin.kt @@ -0,0 +1,41 @@ +package datadog.gradle.plugin.tags + +import javax.inject.Inject +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory + +/** Extension configuring the tag-registry generator inputs/outputs. */ +abstract class TagRegistryExtension @Inject constructor(objects: ObjectFactory) { + val domainYaml: RegularFileProperty = objects.fileProperty() + val overlayYaml: RegularFileProperty = objects.fileProperty() + val destinationDirectory: DirectoryProperty = objects.directoryProperty() +} + +/** + * Registers {@code generateKnownTags} (emits the committed tag registry) and {@code verifyKnownTags} + * (a freshness gate that regenerates and byte-compares against the committed output). The verify task + * is wired into {@code check} so stale generated sources fail CI. + */ +class TagRegistryGeneratorPlugin : Plugin { + override fun apply(project: Project) { + val ext = project.extensions.create("tagRegistry", TagRegistryExtension::class.java) + project.tasks.register("generateKnownTags", GenerateKnownTagsTask::class.java) { + domainYaml.set(ext.domainYaml) + overlayYaml.set(ext.overlayYaml) + destinationDirectory.set(ext.destinationDirectory) + } + val verify = + project.tasks.register("verifyKnownTags", VerifyKnownTagsTask::class.java) { + domainYaml.set(ext.domainYaml) + overlayYaml.set(ext.overlayYaml) + committedDirectory.set(ext.destinationDirectory) + } + // `check` is contributed by lifecycle-base (via java-library); wait for it before wiring. + project.pluginManager.withPlugin("lifecycle-base") { + project.tasks.named("check").configure { dependsOn(verify) } + } + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/VerifyKnownTagsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/VerifyKnownTagsTask.kt new file mode 100644 index 00000000000..e990e15e957 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/VerifyKnownTagsTask.kt @@ -0,0 +1,67 @@ +package datadog.gradle.plugin.tags + +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Freshness gate: regenerates the tag registry into a scratch dir and byte-compares it against the + * committed [committedDirectory]. Fails (pointing at {@code generateKnownTags}) if they differ, so a + * stale commit of the generated sources can't slip through CI. Not cacheable -- it must actually run + * the generator to catch drift, and it is cheap. + */ +abstract class VerifyKnownTagsTask @Inject constructor(objects: ObjectFactory) : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + val domainYaml: RegularFileProperty = objects.fileProperty() + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + val overlayYaml: RegularFileProperty = objects.fileProperty() + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + val committedDirectory: DirectoryProperty = objects.directoryProperty() + + @TaskAction + fun verify() { + val committed = committedDirectory.get().asFile + val scratch = File(temporaryDir, "generated") + scratch.deleteRecursively() + TagRegistryGenerator.generate(domainYaml.get().asFile, overlayYaml.get().asFile, scratch) + + val diffs = ArrayList() + val freshFiles = scratch.walkTopDown().filter { it.isFile }.toList() + for (fresh in freshFiles) { + val rel = fresh.relativeTo(scratch).path + val committedFile = File(committed, rel) + when { + !committedFile.exists() -> diffs.add("missing (not committed): $rel") + committedFile.readText() != fresh.readText() -> diffs.add("out of date: $rel") + } + } + val freshRel = freshFiles.map { it.relativeTo(scratch).path }.toSet() + for (committedFile in committed.walkTopDown().filter { it.isFile }) { + val rel = committedFile.relativeTo(committed).path + if (rel !in freshRel) diffs.add("stale (no longer generated): $rel") + } + + if (diffs.isNotEmpty()) { + throw GradleException( + buildString { + appendLine("Generated tag registry is out of date with tag-conventions.yaml:") + diffs.forEach { appendLine(" - $it") } + append("Run `./gradlew :internal-api:generateKnownTags` and commit the result.") + }) + } + } +} diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index 93a817e6452..bb9f6f4f9e9 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -20,8 +20,8 @@ spotless { toggleOffOn() // set explicit target to workaround https://github.com/diffplug/spotless/issues/1163 target 'src/**/*.java' - // ignore embedded test projects and everything in build dir, e.g. generated sources - targetExclude('src/test/resources/**', buildDirectoryFiles) + // ignore embedded test projects, committed generated sources, and everything in build dir + targetExclude('src/test/resources/**', 'src/generated/**', buildDirectoryFiles) tableTestFormatter('1.1.1') googleJavaFormat('1.35.0') } diff --git a/internal-api/build.gradle.kts b/internal-api/build.gradle.kts index b23d575858c..20c26fe2a96 100644 --- a/internal-api/build.gradle.kts +++ b/internal-api/build.gradle.kts @@ -5,6 +5,7 @@ import groovy.lang.Closure plugins { `java-library` id("me.champeau.jmh") + id("dd-trace-java.tag-registry-generator") } apply(from = "$rootDir/gradle/java.gradle") @@ -15,6 +16,16 @@ java { } } +// Tag registry code generator: emits the committed KnownTags.java (+ layout reports) under +// src/generated. Regenerate with `./gradlew :internal-api:generateKnownTags` and commit the output. +tagRegistry { + domainYaml.set(rootProject.file("tag-conventions.yaml")) + overlayYaml.set(rootProject.file("tag-conventions.java.yaml")) + destinationDirectory.set(layout.projectDirectory.dir("src/generated")) +} + +sourceSets["main"].java.srcDir("src/generated/java") + tasks.withType().configureEach { configureCompiler(8, JavaVersion.VERSION_1_8, "Need access to sun.misc.SharedSecrets") } diff --git a/internal-api/src/generated/folded-types.txt b/internal-api/src/generated/folded-types.txt new file mode 100644 index 00000000000..e9aa668f425 --- /dev/null +++ b/internal-api/src/generated/folded-types.txt @@ -0,0 +1,93 @@ +# Folded tag set per type (extends + include + applies, de-duped), with slots. +# slot: s=per-span bit trace=trace-level layer bit bkt=bucketed trace-bkt=trace-level bucketed I=intercepted + +db.client (21 tags): + s1 _dd.parent_id + s2 component + s6 span.kind I + s0 _dd.integration + bkt _dd.svc_src + s5 error.type + s3 error.message + s4 error.stack + s12 db.type + s9 db.instance + s10 db.operation + s15 db.user + bkt db.pool.name + s11 db.statement I + s14 peer.service I + s8 _dd.peer.service.source + s7 _dd.peer.service.remapped_from + s13 peer.hostname + bkt peer.ipv4 + bkt peer.ipv6 + bkt peer.port + +http.client (20 tags): + s1 _dd.parent_id + s2 component + s6 span.kind I + s0 _dd.integration + bkt _dd.svc_src + s5 error.type + s3 error.message + s4 error.stack + s9 http.method I + s10 http.status_code + s12 network.protocol.version + s11 http.url I + s15 http.resend_count + s14 peer.service I + s8 _dd.peer.service.source + s7 _dd.peer.service.remapped_from + s13 peer.hostname + bkt peer.ipv4 + bkt peer.ipv6 + bkt peer.port + +http.server (18 tags): + s1 _dd.parent_id + s2 component + s6 span.kind I + s0 _dd.integration + bkt _dd.svc_src + s5 error.type + s3 error.message + s4 error.stack + s9 http.method I + s10 http.status_code + s12 network.protocol.version + s11 http.url I + s13 http.route + s7 http.hostname + s14 http.useragent + s8 http.query.string + bkt servlet.path + bkt servlet.context I + +view.render (9 tags): + s1 _dd.parent_id + s2 component + s6 span.kind I + s0 _dd.integration + bkt _dd.svc_src + s5 error.type + s3 error.message + s4 error.stack + s7 view.name + + (13 tags): + trace0 _dd.appsec.enabled + trace1 _dd.base_service + trace2 _dd.civisibility.enabled + trace3 _dd.djm.enabled + trace4 _dd.dsm.enabled + trace5 _dd.git.commit.sha + trace6 _dd.git.repository_url + trace7 _dd.profiling.enabled + trace8 _dd.tracer_host + trace9 env + trace10 language + trace11 runtime-id + trace12 version diff --git a/internal-api/src/generated/java/datadog/trace/api/KnownTags.java b/internal-api/src/generated/java/datadog/trace/api/KnownTags.java new file mode 100644 index 00000000000..24b328b6906 --- /dev/null +++ b/internal-api/src/generated/java/datadog/trace/api/KnownTags.java @@ -0,0 +1,454 @@ +package datadog.trace.api; + +import datadog.trace.util.StringIndex; + +// GENERATED by the tag-registry code generator (dd-trace-java.tag-registry-generator). +// DO NOT EDIT. Source: tag-conventions.yaml + tag-conventions.java.yaml. +public final class KnownTags { + static final int SLOT_COUNT = 16; + + // ---- reserved / virtual (routed to span fields or directives; not stored) ---- + static final int ERROR_SERIAL = 1; + // tagId(serial=1, intercepted=true, slot=NO_SLOT) [structural -> error] "error" + public static final long ERROR = 0x8001FFFF00000000L; + static final int SERVICE_SERIAL = 2; + // tagId(serial=2, intercepted=true, slot=NO_SLOT) [structural -> service] "service" + public static final long SERVICE = 0x8002FFFF00000000L; + static final int RESOURCE_NAME_SERIAL = 3; + // tagId(serial=3, intercepted=true, slot=NO_SLOT) [structural -> resource] "resource.name" + public static final long RESOURCE_NAME = 0x8003FFFF00000000L; + static final int SPAN_TYPE_SERIAL = 4; + // tagId(serial=4, intercepted=true, slot=NO_SLOT) [structural -> type] "span.type" + public static final long SPAN_TYPE = 0x8004FFFF00000000L; + static final int ORIGIN_SERIAL = 5; + // tagId(serial=5, intercepted=true, slot=NO_SLOT) [structural -> origin] "origin" + public static final long ORIGIN = 0x8005FFFF00000000L; + static final int SAMPLING_PRIORITY_SERIAL = 6; + // tagId(serial=6, intercepted=true, slot=NO_SLOT) [directive] "sampling.priority" + public static final long SAMPLING_PRIORITY = 0x8006FFFF00000000L; + static final int MANUAL_KEEP_SERIAL = 7; + // tagId(serial=7, intercepted=true, slot=NO_SLOT) [directive] "manual.keep" + public static final long MANUAL_KEEP = 0x8007FFFF00000000L; + static final int MANUAL_DROP_SERIAL = 8; + // tagId(serial=8, intercepted=true, slot=NO_SLOT) [directive] "manual.drop" + public static final long MANUAL_DROP = 0x8008FFFF00000000L; + static final int MEASURED_SERIAL = 9; + // tagId(serial=9, intercepted=true, slot=NO_SLOT) [directive] "measured" + public static final long MEASURED = 0x8009FFFF00000000L; + static final int ANALYTICS_SAMPLE_RATE_SERIAL = 10; + // tagId(serial=10, intercepted=true, slot=NO_SLOT) [directive] "analytics.sample_rate" + public static final long ANALYTICS_SAMPLE_RATE = 0x800AFFFF00000000L; + + // ---- stored (dense-store slot, or bucketed when slot=NO_SLOT) ---- + static final int DD_APPSEC_ENABLED_SERIAL = 256; + // tagId(serial=256, intercepted=false, slot=0) "_dd.appsec.enabled" + public static final long DD_APPSEC_ENABLED = 0x0100000000000000L; + static final int DD_BASE_SERVICE_SERIAL = 257; + // tagId(serial=257, intercepted=false, slot=1) "_dd.base_service" + public static final long DD_BASE_SERVICE = 0x0101000100000000L; + static final int DD_CIVISIBILITY_ENABLED_SERIAL = 258; + // tagId(serial=258, intercepted=false, slot=2) "_dd.civisibility.enabled" + public static final long DD_CIVISIBILITY_ENABLED = 0x0102000200000000L; + static final int DD_DJM_ENABLED_SERIAL = 259; + // tagId(serial=259, intercepted=false, slot=3) "_dd.djm.enabled" + public static final long DD_DJM_ENABLED = 0x0103000300000000L; + static final int DD_DSM_ENABLED_SERIAL = 260; + // tagId(serial=260, intercepted=false, slot=4) "_dd.dsm.enabled" + public static final long DD_DSM_ENABLED = 0x0104000400000000L; + static final int DD_GIT_COMMIT_SHA_SERIAL = 261; + // tagId(serial=261, intercepted=false, slot=5) "_dd.git.commit.sha" + public static final long DD_GIT_COMMIT_SHA = 0x0105000500000000L; + static final int DD_GIT_REPOSITORY_URL_SERIAL = 262; + // tagId(serial=262, intercepted=false, slot=6) "_dd.git.repository_url" + public static final long DD_GIT_REPOSITORY_URL = 0x0106000600000000L; + static final int DD_INTEGRATION_SERIAL = 263; + // tagId(serial=263, intercepted=false, slot=0) "_dd.integration" + public static final long DD_INTEGRATION = 0x0107000000000000L; + static final int DD_PARENT_ID_SERIAL = 264; + // tagId(serial=264, intercepted=false, slot=1) "_dd.parent_id" + public static final long DD_PARENT_ID = 0x0108000100000000L; + static final int DD_PEER_SERVICE_REMAPPED_FROM_SERIAL = 265; + // tagId(serial=265, intercepted=false, slot=7) "_dd.peer.service.remapped_from" + public static final long DD_PEER_SERVICE_REMAPPED_FROM = 0x0109000700000000L; + static final int DD_PEER_SERVICE_SOURCE_SERIAL = 266; + // tagId(serial=266, intercepted=false, slot=8) "_dd.peer.service.source" + public static final long DD_PEER_SERVICE_SOURCE = 0x010A000800000000L; + static final int DD_PROFILING_ENABLED_SERIAL = 267; + // tagId(serial=267, intercepted=false, slot=7) "_dd.profiling.enabled" + public static final long DD_PROFILING_ENABLED = 0x010B000700000000L; + static final int DD_SVC_SRC_SERIAL = 268; + // tagId(serial=268, intercepted=false, slot=NO_SLOT) "_dd.svc_src" + public static final long DD_SVC_SRC = 0x010CFFFF00000000L; + static final int DD_TRACER_HOST_SERIAL = 269; + // tagId(serial=269, intercepted=false, slot=8) "_dd.tracer_host" + public static final long DD_TRACER_HOST = 0x010D000800000000L; + static final int COMPONENT_SERIAL = 270; + // tagId(serial=270, intercepted=false, slot=2) "component" + public static final long COMPONENT = 0x010E000200000000L; + static final int DB_INSTANCE_SERIAL = 271; + // tagId(serial=271, intercepted=false, slot=9) "db.instance" + public static final long DB_INSTANCE = 0x010F000900000000L; + static final int DB_OPERATION_SERIAL = 272; + // tagId(serial=272, intercepted=false, slot=10) "db.operation" + public static final long DB_OPERATION = 0x0110000A00000000L; + static final int DB_POOL_NAME_SERIAL = 273; + // tagId(serial=273, intercepted=false, slot=NO_SLOT) "db.pool.name" + public static final long DB_POOL_NAME = 0x0111FFFF00000000L; + static final int DB_STATEMENT_SERIAL = 274; + // tagId(serial=274, intercepted=true, slot=11) "db.statement" + public static final long DB_STATEMENT = 0x8112000B00000000L; + static final int DB_TYPE_SERIAL = 275; + // tagId(serial=275, intercepted=false, slot=12) "db.type" + public static final long DB_TYPE = 0x0113000C00000000L; + static final int DB_USER_SERIAL = 276; + // tagId(serial=276, intercepted=false, slot=15) "db.user" + public static final long DB_USER = 0x0114000F00000000L; + static final int ENV_SERIAL = 277; + // tagId(serial=277, intercepted=false, slot=9) "env" + public static final long ENV = 0x0115000900000000L; + static final int ERROR_MESSAGE_SERIAL = 278; + // tagId(serial=278, intercepted=false, slot=3) "error.message" + public static final long ERROR_MESSAGE = 0x0116000300000000L; + static final int ERROR_STACK_SERIAL = 279; + // tagId(serial=279, intercepted=false, slot=4) "error.stack" + public static final long ERROR_STACK = 0x0117000400000000L; + static final int ERROR_TYPE_SERIAL = 280; + // tagId(serial=280, intercepted=false, slot=5) "error.type" + public static final long ERROR_TYPE = 0x0118000500000000L; + static final int HTTP_HOSTNAME_SERIAL = 281; + // tagId(serial=281, intercepted=false, slot=7) "http.hostname" + public static final long HTTP_HOSTNAME = 0x0119000700000000L; + static final int HTTP_METHOD_SERIAL = 282; + // tagId(serial=282, intercepted=true, slot=9) "http.method" + public static final long HTTP_METHOD = 0x811A000900000000L; + static final int HTTP_QUERY_STRING_SERIAL = 283; + // tagId(serial=283, intercepted=false, slot=8) "http.query.string" + public static final long HTTP_QUERY_STRING = 0x011B000800000000L; + static final int HTTP_RESEND_COUNT_SERIAL = 284; + // tagId(serial=284, intercepted=false, slot=15) "http.resend_count" + public static final long HTTP_RESEND_COUNT = 0x011C000F00000000L; + static final int HTTP_ROUTE_SERIAL = 285; + // tagId(serial=285, intercepted=false, slot=13) "http.route" + public static final long HTTP_ROUTE = 0x011D000D00000000L; + static final int HTTP_STATUS_CODE_SERIAL = 286; + // tagId(serial=286, intercepted=false, slot=10) "http.status_code" + public static final long HTTP_STATUS_CODE = 0x011E000A00000000L; + static final int HTTP_URL_SERIAL = 287; + // tagId(serial=287, intercepted=true, slot=11) "http.url" + public static final long HTTP_URL = 0x811F000B00000000L; + static final int HTTP_USERAGENT_SERIAL = 288; + // tagId(serial=288, intercepted=false, slot=14) "http.useragent" + public static final long HTTP_USERAGENT = 0x0120000E00000000L; + static final int LANGUAGE_SERIAL = 289; + // tagId(serial=289, intercepted=false, slot=10) "language" + public static final long LANGUAGE = 0x0121000A00000000L; + static final int NETWORK_PROTOCOL_VERSION_SERIAL = 290; + // tagId(serial=290, intercepted=false, slot=12) "network.protocol.version" + public static final long NETWORK_PROTOCOL_VERSION = 0x0122000C00000000L; + static final int PEER_HOSTNAME_SERIAL = 291; + // tagId(serial=291, intercepted=false, slot=13) "peer.hostname" + public static final long PEER_HOSTNAME = 0x0123000D00000000L; + static final int PEER_IPV4_SERIAL = 292; + // tagId(serial=292, intercepted=false, slot=NO_SLOT) "peer.ipv4" + public static final long PEER_IPV4 = 0x0124FFFF00000000L; + static final int PEER_IPV6_SERIAL = 293; + // tagId(serial=293, intercepted=false, slot=NO_SLOT) "peer.ipv6" + public static final long PEER_IPV6 = 0x0125FFFF00000000L; + static final int PEER_PORT_SERIAL = 294; + // tagId(serial=294, intercepted=false, slot=NO_SLOT) "peer.port" + public static final long PEER_PORT = 0x0126FFFF00000000L; + static final int PEER_SERVICE_SERIAL = 295; + // tagId(serial=295, intercepted=true, slot=14) "peer.service" + public static final long PEER_SERVICE = 0x8127000E00000000L; + static final int RUNTIME_ID_SERIAL = 296; + // tagId(serial=296, intercepted=false, slot=11) "runtime-id" + public static final long RUNTIME_ID = 0x0128000B00000000L; + static final int SERVLET_CONTEXT_SERIAL = 297; + // tagId(serial=297, intercepted=true, slot=NO_SLOT) "servlet.context" + public static final long SERVLET_CONTEXT = 0x8129FFFF00000000L; + static final int SERVLET_PATH_SERIAL = 298; + // tagId(serial=298, intercepted=false, slot=NO_SLOT) "servlet.path" + public static final long SERVLET_PATH = 0x012AFFFF00000000L; + static final int SPAN_KIND_SERIAL = 299; + // tagId(serial=299, intercepted=true, slot=6) "span.kind" + public static final long SPAN_KIND = 0x812B000600000000L; + static final int VERSION_SERIAL = 300; + // tagId(serial=300, intercepted=false, slot=12) "version" + public static final long VERSION = 0x012C000C00000000L; + static final int VIEW_NAME_SERIAL = 301; + // tagId(serial=301, intercepted=false, slot=7) "view.name" + public static final long VIEW_NAME = 0x012D000700000000L; + + private static final String[] KEYOF_NAMES = { + "error", + "service", + "resource.name", + "span.type", + "origin", + "sampling.priority", + "manual.keep", + "manual.drop", + "measured", + "analytics.sample_rate", + "_dd.appsec.enabled", + "_dd.base_service", + "_dd.civisibility.enabled", + "_dd.djm.enabled", + "_dd.dsm.enabled", + "_dd.git.commit.sha", + "_dd.git.repository_url", + "_dd.integration", + "_dd.parent_id", + "_dd.peer.service.remapped_from", + "_dd.peer.service.source", + "_dd.profiling.enabled", + "_dd.svc_src", + "_dd.tracer_host", + "component", + "db.instance", + "db.operation", + "db.pool.name", + "db.statement", + "db.type", + "db.user", + "env", + "error.message", + "error.stack", + "error.type", + "http.hostname", + "http.method", + "http.query.string", + "http.resend_count", + "http.route", + "http.status_code", + "http.url", + "http.useragent", + "language", + "network.protocol.version", + "peer.hostname", + "peer.ipv4", + "peer.ipv6", + "peer.port", + "peer.service", + "runtime-id", + "servlet.context", + "servlet.path", + "span.kind", + "version", + "view.name", + }; + private static final long[] KEYOF_VALUES = { + ERROR, + SERVICE, + RESOURCE_NAME, + SPAN_TYPE, + ORIGIN, + SAMPLING_PRIORITY, + MANUAL_KEEP, + MANUAL_DROP, + MEASURED, + ANALYTICS_SAMPLE_RATE, + DD_APPSEC_ENABLED, + DD_BASE_SERVICE, + DD_CIVISIBILITY_ENABLED, + DD_DJM_ENABLED, + DD_DSM_ENABLED, + DD_GIT_COMMIT_SHA, + DD_GIT_REPOSITORY_URL, + DD_INTEGRATION, + DD_PARENT_ID, + DD_PEER_SERVICE_REMAPPED_FROM, + DD_PEER_SERVICE_SOURCE, + DD_PROFILING_ENABLED, + DD_SVC_SRC, + DD_TRACER_HOST, + COMPONENT, + DB_INSTANCE, + DB_OPERATION, + DB_POOL_NAME, + DB_STATEMENT, + DB_TYPE, + DB_USER, + ENV, + ERROR_MESSAGE, + ERROR_STACK, + ERROR_TYPE, + HTTP_HOSTNAME, + HTTP_METHOD, + HTTP_QUERY_STRING, + HTTP_RESEND_COUNT, + HTTP_ROUTE, + HTTP_STATUS_CODE, + HTTP_URL, + HTTP_USERAGENT, + LANGUAGE, + NETWORK_PROTOCOL_VERSION, + PEER_HOSTNAME, + PEER_IPV4, + PEER_IPV6, + PEER_PORT, + PEER_SERVICE, + RUNTIME_ID, + SERVLET_CONTEXT, + SERVLET_PATH, + SPAN_KIND, + VERSION, + VIEW_NAME, + }; + private static final int[] KEYOF_HASHES; + private static final String[] KEYOF_KEYS; + private static final long[] KEYOF_IDS; + static { + StringIndex.Data data = StringIndex.Support.create(KEYOF_NAMES); + long[] ids = new long[data.names.length]; + for (int j = 0; j < KEYOF_NAMES.length; j++) { + ids[StringIndex.Support.indexOf(data.hashes, data.names, KEYOF_NAMES[j])] = KEYOF_VALUES[j]; + } + KEYOF_HASHES = data.hashes; + KEYOF_KEYS = data.names; + KEYOF_IDS = ids; + } + + static final KnownTagCodec.Resolver RESOLVER = + new KnownTagCodec.Resolver() { + @Override + public String nameOf(long tagId) { + switch (KnownTagCodec.globalSerial(tagId)) { + case ERROR_SERIAL: + return "error"; + case SERVICE_SERIAL: + return "service"; + case RESOURCE_NAME_SERIAL: + return "resource.name"; + case SPAN_TYPE_SERIAL: + return "span.type"; + case ORIGIN_SERIAL: + return "origin"; + case SAMPLING_PRIORITY_SERIAL: + return "sampling.priority"; + case MANUAL_KEEP_SERIAL: + return "manual.keep"; + case MANUAL_DROP_SERIAL: + return "manual.drop"; + case MEASURED_SERIAL: + return "measured"; + case ANALYTICS_SAMPLE_RATE_SERIAL: + return "analytics.sample_rate"; + case DD_APPSEC_ENABLED_SERIAL: + return "_dd.appsec.enabled"; + case DD_BASE_SERVICE_SERIAL: + return "_dd.base_service"; + case DD_CIVISIBILITY_ENABLED_SERIAL: + return "_dd.civisibility.enabled"; + case DD_DJM_ENABLED_SERIAL: + return "_dd.djm.enabled"; + case DD_DSM_ENABLED_SERIAL: + return "_dd.dsm.enabled"; + case DD_GIT_COMMIT_SHA_SERIAL: + return "_dd.git.commit.sha"; + case DD_GIT_REPOSITORY_URL_SERIAL: + return "_dd.git.repository_url"; + case DD_INTEGRATION_SERIAL: + return "_dd.integration"; + case DD_PARENT_ID_SERIAL: + return "_dd.parent_id"; + case DD_PEER_SERVICE_REMAPPED_FROM_SERIAL: + return "_dd.peer.service.remapped_from"; + case DD_PEER_SERVICE_SOURCE_SERIAL: + return "_dd.peer.service.source"; + case DD_PROFILING_ENABLED_SERIAL: + return "_dd.profiling.enabled"; + case DD_SVC_SRC_SERIAL: + return "_dd.svc_src"; + case DD_TRACER_HOST_SERIAL: + return "_dd.tracer_host"; + case COMPONENT_SERIAL: + return "component"; + case DB_INSTANCE_SERIAL: + return "db.instance"; + case DB_OPERATION_SERIAL: + return "db.operation"; + case DB_POOL_NAME_SERIAL: + return "db.pool.name"; + case DB_STATEMENT_SERIAL: + return "db.statement"; + case DB_TYPE_SERIAL: + return "db.type"; + case DB_USER_SERIAL: + return "db.user"; + case ENV_SERIAL: + return "env"; + case ERROR_MESSAGE_SERIAL: + return "error.message"; + case ERROR_STACK_SERIAL: + return "error.stack"; + case ERROR_TYPE_SERIAL: + return "error.type"; + case HTTP_HOSTNAME_SERIAL: + return "http.hostname"; + case HTTP_METHOD_SERIAL: + return "http.method"; + case HTTP_QUERY_STRING_SERIAL: + return "http.query.string"; + case HTTP_RESEND_COUNT_SERIAL: + return "http.resend_count"; + case HTTP_ROUTE_SERIAL: + return "http.route"; + case HTTP_STATUS_CODE_SERIAL: + return "http.status_code"; + case HTTP_URL_SERIAL: + return "http.url"; + case HTTP_USERAGENT_SERIAL: + return "http.useragent"; + case LANGUAGE_SERIAL: + return "language"; + case NETWORK_PROTOCOL_VERSION_SERIAL: + return "network.protocol.version"; + case PEER_HOSTNAME_SERIAL: + return "peer.hostname"; + case PEER_IPV4_SERIAL: + return "peer.ipv4"; + case PEER_IPV6_SERIAL: + return "peer.ipv6"; + case PEER_PORT_SERIAL: + return "peer.port"; + case PEER_SERVICE_SERIAL: + return "peer.service"; + case RUNTIME_ID_SERIAL: + return "runtime-id"; + case SERVLET_CONTEXT_SERIAL: + return "servlet.context"; + case SERVLET_PATH_SERIAL: + return "servlet.path"; + case SPAN_KIND_SERIAL: + return "span.kind"; + case VERSION_SERIAL: + return "version"; + case VIEW_NAME_SERIAL: + return "view.name"; + default: + return null; + } + } + + @Override + public int slotCount() { + return SLOT_COUNT; + } + + @Override + public long keyOf(String name) { + int slot = StringIndex.Support.indexOf(KEYOF_HASHES, KEYOF_KEYS, name); + return slot < 0 ? 0L : KEYOF_IDS[slot]; + } + }; + + static { + KnownTagCodec.register(RESOLVER); + } + + /** Forces resolver registration by triggering . Idempotent. */ + public static void init() {} + + private KnownTags() {} +} diff --git a/internal-api/src/generated/layout-by-type.txt b/internal-api/src/generated/layout-by-type.txt new file mode 100644 index 00000000000..8f618823fc4 --- /dev/null +++ b/internal-api/src/generated/layout-by-type.txt @@ -0,0 +1,91 @@ +# Full tag composition per concrete span type (after extends/include/applies). +# Not de-duped: a tag from >1 source appears >1 time. +# annotation: [s per-span slot | trace trace layer | bkt bucketed] I=intercepted + +db.client (21 contributions, 21 distinct): + [base] + _dd.parent_id s1 required + component s2 required + span.kind s6 required I + _dd.integration s0 recommended + _dd.svc_src bkt optional + error.type s5 recommended + error.message s3 recommended + error.stack s4 recommended + [db.client] + db.type s12 required + db.instance s9 recommended + db.operation s10 recommended + db.user s15 recommended + db.pool.name bkt optional + db.statement s11 recommended I + [incl:peer] + peer.service s14 recommended I + _dd.peer.service.source s8 recommended + _dd.peer.service.remapped_from s7 recommended + peer.hostname s13 recommended + peer.ipv4 bkt optional + peer.ipv6 bkt optional + peer.port bkt optional + +http.client (20 contributions, 20 distinct): + [base] + _dd.parent_id s1 required + component s2 required + span.kind s6 required I + _dd.integration s0 recommended + _dd.svc_src bkt optional + error.type s5 recommended + error.message s3 recommended + error.stack s4 recommended + [http] + http.method s9 required I + http.status_code s10 conditional + network.protocol.version s12 recommended + [http.client] + http.url s11 required I + http.resend_count s15 recommended + [incl:peer] + peer.service s14 recommended I + _dd.peer.service.source s8 recommended + _dd.peer.service.remapped_from s7 recommended + peer.hostname s13 recommended + peer.ipv4 bkt optional + peer.ipv6 bkt optional + peer.port bkt optional + +http.server (18 contributions, 18 distinct): + [base] + _dd.parent_id s1 required + component s2 required + span.kind s6 required I + _dd.integration s0 recommended + _dd.svc_src bkt optional + error.type s5 recommended + error.message s3 recommended + error.stack s4 recommended + [http] + http.method s9 required I + http.status_code s10 conditional + network.protocol.version s12 recommended + [http.server] + http.url s11 required I + http.route s13 conditional + http.hostname s7 required + http.useragent s14 recommended + http.query.string s8 recommended + servlet.path bkt optional + servlet.context bkt optional I + +view.render (9 contributions, 9 distinct): + [base] + _dd.parent_id s1 required + component s2 required + span.kind s6 required I + _dd.integration s0 recommended + _dd.svc_src bkt optional + error.type s5 recommended + error.message s3 recommended + error.stack s4 recommended + [view.render] + view.name s7 recommended diff --git a/internal-api/src/generated/resolved-tags.txt b/internal-api/src/generated/resolved-tags.txt new file mode 100644 index 00000000000..0edb3479608 --- /dev/null +++ b/internal-api/src/generated/resolved-tags.txt @@ -0,0 +1,77 @@ +# Resolved per-type tag sets (concrete span types). + +db.client (21 tags): + - _dd.parent_id + - component + - span.kind + - _dd.integration + - _dd.svc_src + - error.type + - error.message + - error.stack + - db.type + - db.instance + - db.operation + - db.user + - db.pool.name + - db.statement + - peer.service + - _dd.peer.service.source + - _dd.peer.service.remapped_from + - peer.hostname + - peer.ipv4 + - peer.ipv6 + - peer.port + +http.client (20 tags): + - _dd.parent_id + - component + - span.kind + - _dd.integration + - _dd.svc_src + - error.type + - error.message + - error.stack + - http.method + - http.status_code + - network.protocol.version + - http.url + - http.resend_count + - peer.service + - _dd.peer.service.source + - _dd.peer.service.remapped_from + - peer.hostname + - peer.ipv4 + - peer.ipv6 + - peer.port + +http.server (18 tags): + - _dd.parent_id + - component + - span.kind + - _dd.integration + - _dd.svc_src + - error.type + - error.message + - error.stack + - http.method + - http.status_code + - network.protocol.version + - http.url + - http.route + - http.hostname + - http.useragent + - http.query.string + - servlet.path + - servlet.context + +view.render (9 tags): + - _dd.parent_id + - component + - span.kind + - _dd.integration + - _dd.svc_src + - error.type + - error.message + - error.stack + - view.name diff --git a/internal-api/src/generated/tag-assignment.txt b/internal-api/src/generated/tag-assignment.txt new file mode 100644 index 00000000000..837a36d1d59 --- /dev/null +++ b/internal-api/src/generated/tag-assignment.txt @@ -0,0 +1,68 @@ +# Tag id assignment. slotCount=16 stored=46 virtual=10 + +# STORED serial slot int id required name + 256 0 - 0x0100000000000000 recommended _dd.appsec.enabled + 257 1 - 0x0101000100000000 required _dd.base_service + 258 2 - 0x0102000200000000 recommended _dd.civisibility.enabled + 259 3 - 0x0103000300000000 recommended _dd.djm.enabled + 260 4 - 0x0104000400000000 recommended _dd.dsm.enabled + 261 5 - 0x0105000500000000 recommended _dd.git.commit.sha + 262 6 - 0x0106000600000000 recommended _dd.git.repository_url + 263 0 - 0x0107000000000000 recommended _dd.integration + 264 1 - 0x0108000100000000 required _dd.parent_id + 265 7 - 0x0109000700000000 recommended _dd.peer.service.remapped_from + 266 8 - 0x010A000800000000 recommended _dd.peer.service.source + 267 7 - 0x010B000700000000 recommended _dd.profiling.enabled + 268 - - 0x010CFFFF00000000 optional _dd.svc_src + 269 8 - 0x010D000800000000 recommended _dd.tracer_host + 270 2 - 0x010E000200000000 required component + 271 9 - 0x010F000900000000 recommended db.instance + 272 10 - 0x0110000A00000000 recommended db.operation + 273 - - 0x0111FFFF00000000 optional db.pool.name + 274 11 I 0x8112000B00000000 recommended db.statement + 275 12 - 0x0113000C00000000 required db.type + 276 15 - 0x0114000F00000000 recommended db.user + 277 9 - 0x0115000900000000 recommended env + 278 3 - 0x0116000300000000 recommended error.message + 279 4 - 0x0117000400000000 recommended error.stack + 280 5 - 0x0118000500000000 recommended error.type + 281 7 - 0x0119000700000000 required http.hostname + 282 9 I 0x811A000900000000 required http.method + 283 8 - 0x011B000800000000 recommended http.query.string + 284 15 - 0x011C000F00000000 recommended http.resend_count + 285 13 - 0x011D000D00000000 conditional http.route + 286 10 - 0x011E000A00000000 conditional http.status_code + 287 11 I 0x811F000B00000000 required http.url + 288 14 - 0x0120000E00000000 recommended http.useragent + 289 10 - 0x0121000A00000000 required language + 290 12 - 0x0122000C00000000 recommended network.protocol.version + 291 13 - 0x0123000D00000000 recommended peer.hostname + 292 - - 0x0124FFFF00000000 optional peer.ipv4 + 293 - - 0x0125FFFF00000000 optional peer.ipv6 + 294 - - 0x0126FFFF00000000 optional peer.port + 295 14 I 0x8127000E00000000 recommended peer.service + 296 11 - 0x0128000B00000000 required runtime-id + 297 - I 0x8129FFFF00000000 optional servlet.context + 298 - - 0x012AFFFF00000000 optional servlet.path + 299 6 I 0x812B000600000000 required span.kind + 300 12 - 0x012C000C00000000 recommended version + 301 7 - 0x012D000700000000 recommended view.name + +# VIRTUAL serial id kind name + 1 0x8001FFFF00000000 structural error -> error + 2 0x8002FFFF00000000 structural service -> service + 3 0x8003FFFF00000000 structural resource.name -> resource + 4 0x8004FFFF00000000 structural span.type -> type + 5 0x8005FFFF00000000 structural origin -> origin + 6 0x8006FFFF00000000 directive sampling.priority + 7 0x8007FFFF00000000 directive manual.keep + 8 0x8008FFFF00000000 directive manual.drop + 9 0x8009FFFF00000000 directive measured + 10 0x800AFFFF00000000 directive analytics.sample_rate + +# PER-TYPE packed size (= max slot + 1). = the trace-level TagMap layer. + db.client size=16 slots=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + http.client size=16 slots=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + http.server size=15 slots=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + view.render size=8 slots=[0, 1, 2, 3, 4, 5, 6, 7] + size=13 slots=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 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 7ed9dff633a..fb5c259c584 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -128,6 +128,20 @@ public static long tagId(int globalSerial, String name) { return tagId(globalSerial, NO_SLOT, name); } + /** + * Builds a tagId from {@code globalSerial}, the {@code intercepted} flag, and the {@code slot} + * ({@code fieldPos}, or {@link #NO_SLOT}). Unlike the name-taking overloads, this leaves the low + * 32 bits (nameHash) zero: known-tag ids do not need a name hash — that lives only in the + * custom-tag {@link TagMap.Entry} path. This is the encoder the code generator uses; generated + * constants emit the resulting literal plus a {@code // tagId(serial=..., intercepted=..., + * slot=...)} comment (the literal so javac constant-folds it; the comment so a release branch + * stays auditable). + */ + public static long tagId(int globalSerial, boolean intercepted, int slot) { + long id = ((long) globalSerial << 48) | ((long) (slot & 0xFFFF) << 32); + return intercepted ? (id | INTERCEPTED) : id; + } + // Number of positional slots in the global layout = (max stored fieldPos) + 1, declared by the // registered provider. Captured once at registration and read as a dynamic constant; TagMap sizes // its knownEntries array to exactly this rather than a hardcoded max. 0 when no resolver. diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTags.java b/internal-api/src/main/java/datadog/trace/api/KnownTags.java deleted file mode 100644 index 3a702eae38d..00000000000 --- a/internal-api/src/main/java/datadog/trace/api/KnownTags.java +++ /dev/null @@ -1,326 +0,0 @@ -package datadog.trace.api; - -import datadog.trace.bootstrap.instrumentation.api.Tags; -import datadog.trace.util.StringIndex; - -/** - * Hand-assigned tag-id constants for well-known tags, plus the {@link KnownTagCodec.Resolver} that - * resolves them. This is the single registry shared by the tracer core and by instrumentation - * (decorators) — it lives in {@code internal-api} so both layers can reference the ids; the - * eventual code generator will replace the hand assignment here. - * - *

Reserved serials {@code [1, KnownTagCodec.FIRST_STORED_SERIAL)} name "virtual" tags handled by - * the tag interceptor / span fields and are NOT stored in the {@code TagMap}; their {@code - * fieldPos} is the {@link KnownTagCodec#NO_SLOT} sentinel that is out of slot range, so any - * incidental store routes to the hash buckets rather than a positional slot. Serials {@code >= - * FIRST_STORED_SERIAL} name stored tags that slot/bucket normally (or, with {@code NO_SLOT}, are - * stored bucket-only). - * - *

The resolver registers on class initialization, so simply referencing any constant here makes - * tag-id resolution live before the first span is built. - * - *

Slice-1 note (keyOf substrate): the {@code fieldPos} assignments below (and {@link - * #SLOT_COUNT}) describe a single universal positional layout (slots 0..25). That layout is - * currently dormant — no dense store consumes {@code fieldPos} yet — and is provisional: the - * dense-store slice replaces the universal layout with per-role / per-type sizing (see the - * over-provision finding in {@code dense-tagmap-design.md}). {@code keyOf}/{@code nameOf} depend - * only on {@code globalSerial} + name, not {@code fieldPos}, so the ids themselves are stable - * across any layout scheme. - */ -public final class KnownTags { - // slot count = (max stored fieldPos) + 1. Stored tags use fieldPos 0..25. PROVISIONAL universal - // layout — see the slice-1 note above; the dense-store slice supersedes this with role/type - // sizing. - static final int SLOT_COUNT = 26; - - // ---- reserved / virtual (tag-interceptor handled, not stored) ---- - // Reserved tags are always intercepted -> set the INTERCEPTED flag. - public static final int ERROR_SERIAL = 1; - public static final long ERROR_ID = - KnownTagCodec.intercepted(KnownTagCodec.tagId(ERROR_SERIAL, Tags.ERROR)); - - // ---- stored (slotted / bucketed) ---- - public static final int PARENT_ID_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL; - public static final long PARENT_ID = KnownTagCodec.tagId(PARENT_ID_SERIAL, 0, DDTags.PARENT_ID); - - // common (process-constant) tags added by InternalTagsAdder to ~every span - public static final int BASE_SERVICE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 1; - public static final long BASE_SERVICE_ID = - KnownTagCodec.tagId(BASE_SERVICE_SERIAL, 1, DDTags.BASE_SERVICE); - - public static final int VERSION_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 2; - public static final long VERSION_ID = KnownTagCodec.tagId(VERSION_SERIAL, 2, Tags.VERSION); - - // build-time-known constant tags merged into defaultSpanTags (see CoreTracer.withTracerTags). - // "env" is a base-mixin tag; the *_ENABLED flags are product-mixin tags. Hand-assigned for now. - public static final String ENV = "env"; - public static final int ENV_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 3; - public static final long ENV_ID = KnownTagCodec.tagId(ENV_SERIAL, 3, ENV); - - public static final int DJM_ENABLED_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 4; - public static final long DJM_ENABLED_ID = - KnownTagCodec.tagId(DJM_ENABLED_SERIAL, 4, DDTags.DJM_ENABLED); - - public static final int DSM_ENABLED_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 5; - public static final long DSM_ENABLED_ID = - KnownTagCodec.tagId(DSM_ENABLED_SERIAL, 5, DDTags.DSM_ENABLED); - - // common tags added by the tag post-processors (RemoteHostnameAdder / IntegrationAdder / - // ServiceNameSourceAdder). Not intercepted; stored. - public static final int TRACER_HOST_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 6; - public static final long TRACER_HOST_ID = - KnownTagCodec.tagId(TRACER_HOST_SERIAL, 6, DDTags.TRACER_HOST); - - public static final int INTEGRATION_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 7; - public static final long INTEGRATION_ID = - KnownTagCodec.tagId(INTEGRATION_SERIAL, 7, DDTags.DD_INTEGRATION); - - public static final int SVC_SRC_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 8; - public static final long SVC_SRC_ID = KnownTagCodec.tagId(SVC_SRC_SERIAL, 8, DDTags.DD_SVC_SRC); - - // peer.service tags, read/written by PeerServiceCalculator (post-processor; uses Map put/get that - // bypass the interceptor). peer.service is intercepted on the set-path but STORED, so it slots. - public static final int PEER_SERVICE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 9; - public static final long PEER_SERVICE_ID = - KnownTagCodec.intercepted(KnownTagCodec.tagId(PEER_SERVICE_SERIAL, 9, Tags.PEER_SERVICE)); - - public static final int PEER_SERVICE_REMAPPED_FROM_SERIAL = - KnownTagCodec.FIRST_STORED_SERIAL + 10; - public static final long PEER_SERVICE_REMAPPED_FROM_ID = - KnownTagCodec.tagId(PEER_SERVICE_REMAPPED_FROM_SERIAL, 10, DDTags.PEER_SERVICE_REMAPPED_FROM); - - // HTTP tags read by HttpEndpointPostProcessor. http.method/http.url are intercepted-but-stored - // (interceptTag side-effects then returns false → stored); http.route is not intercepted. All - // stored, so the string set-path slots them via keyOf and the id reads here find them. - public static final int HTTP_METHOD_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 11; - public static final long HTTP_METHOD_ID = - KnownTagCodec.intercepted(KnownTagCodec.tagId(HTTP_METHOD_SERIAL, 11, Tags.HTTP_METHOD)); - - public static final int HTTP_ROUTE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 12; - public static final long HTTP_ROUTE_ID = - KnownTagCodec.tagId(HTTP_ROUTE_SERIAL, 12, Tags.HTTP_ROUTE); - - public static final int HTTP_URL_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 13; - public static final long HTTP_URL_ID = - KnownTagCodec.intercepted(KnownTagCodec.tagId(HTTP_URL_SERIAL, 13, Tags.HTTP_URL)); - - // peer connection tags set by BaseDecorator.onPeerConnection on ~every client/producer span. - // Not intercepted; stored. Slotted (common across client instrumentations). - public static final int PEER_HOSTNAME_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 14; - public static final long PEER_HOSTNAME_ID = - KnownTagCodec.tagId(PEER_HOSTNAME_SERIAL, 14, Tags.PEER_HOSTNAME); - - public static final int PEER_HOST_IPV4_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 15; - public static final long PEER_HOST_IPV4_ID = - KnownTagCodec.tagId(PEER_HOST_IPV4_SERIAL, 15, Tags.PEER_HOST_IPV4); - - public static final int PEER_HOST_IPV6_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 16; - public static final long PEER_HOST_IPV6_ID = - KnownTagCodec.tagId(PEER_HOST_IPV6_SERIAL, 16, Tags.PEER_HOST_IPV6); - - public static final int PEER_PORT_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 17; - public static final long PEER_PORT_ID = KnownTagCodec.tagId(PEER_PORT_SERIAL, 17, Tags.PEER_PORT); - - // Universal decorator tags — set on ~every span (component/span.kind via Base/Server/Client - // decorators, language via ServerDecorator). span.kind is intercepted (setSpanKindOrdinal). - public static final int COMPONENT_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 18; - public static final long COMPONENT_ID = KnownTagCodec.tagId(COMPONENT_SERIAL, 18, Tags.COMPONENT); - - public static final int SPAN_KIND_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 19; - public static final long SPAN_KIND_ID = - KnownTagCodec.intercepted(KnownTagCodec.tagId(SPAN_KIND_SERIAL, 19, Tags.SPAN_KIND)); - - public static final int LANGUAGE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 20; - public static final long LANGUAGE_ID = - KnownTagCodec.tagId(LANGUAGE_SERIAL, 20, DDTags.LANGUAGE_TAG_KEY); - - // JDBC / database-client tags — set on every db span (58% of petclinic spans). Not intercepted - // (only db.statement is, and that's handled separately). - public static final int DB_TYPE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 21; - public static final long DB_TYPE_ID = KnownTagCodec.tagId(DB_TYPE_SERIAL, 21, Tags.DB_TYPE); - - public static final int DB_INSTANCE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 22; - public static final long DB_INSTANCE_ID = - KnownTagCodec.tagId(DB_INSTANCE_SERIAL, 22, Tags.DB_INSTANCE); - - public static final int DB_USER_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 23; - public static final long DB_USER_ID = KnownTagCodec.tagId(DB_USER_SERIAL, 23, Tags.DB_USER); - - public static final int DB_OPERATION_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 24; - public static final long DB_OPERATION_ID = - KnownTagCodec.tagId(DB_OPERATION_SERIAL, 24, Tags.DB_OPERATION); - - public static final int DB_POOL_NAME_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 25; - public static final long DB_POOL_NAME_ID = - KnownTagCodec.tagId(DB_POOL_NAME_SERIAL, 25, Tags.DB_POOL_NAME); - - // Open-addressed name -> id table backing keyOf (data, not a switch): scales flat as the known - // set grows, where a generated switch eventually falls off the inline threshold. KEYOF_NAMES and - // KEYOF_VALUES are parallel; the table places names by hash and a parallel ids[] by slot. - private static final String[] KEYOF_NAMES = { - Tags.ERROR, - DDTags.PARENT_ID, - DDTags.BASE_SERVICE, - Tags.VERSION, - ENV, - DDTags.DJM_ENABLED, - DDTags.DSM_ENABLED, - DDTags.TRACER_HOST, - DDTags.DD_INTEGRATION, - DDTags.DD_SVC_SRC, - Tags.PEER_SERVICE, - DDTags.PEER_SERVICE_REMAPPED_FROM, - Tags.HTTP_METHOD, - Tags.HTTP_ROUTE, - Tags.HTTP_URL, - Tags.PEER_HOSTNAME, - Tags.PEER_HOST_IPV4, - Tags.PEER_HOST_IPV6, - Tags.PEER_PORT, - Tags.COMPONENT, - Tags.SPAN_KIND, - DDTags.LANGUAGE_TAG_KEY, - Tags.DB_TYPE, - Tags.DB_INSTANCE, - Tags.DB_USER, - Tags.DB_OPERATION, - Tags.DB_POOL_NAME, - }; - - private static final long[] KEYOF_VALUES = { - ERROR_ID, - PARENT_ID, - BASE_SERVICE_ID, - VERSION_ID, - ENV_ID, - DJM_ENABLED_ID, - DSM_ENABLED_ID, - TRACER_HOST_ID, - INTEGRATION_ID, - SVC_SRC_ID, - PEER_SERVICE_ID, - PEER_SERVICE_REMAPPED_FROM_ID, - HTTP_METHOD_ID, - HTTP_ROUTE_ID, - HTTP_URL_ID, - PEER_HOSTNAME_ID, - PEER_HOST_IPV4_ID, - PEER_HOST_IPV6_ID, - PEER_PORT_ID, - COMPONENT_ID, - SPAN_KIND_ID, - LANGUAGE_ID, - DB_TYPE_ID, - DB_INSTANCE_ID, - DB_USER_ID, - DB_OPERATION_ID, - DB_POOL_NAME_ID, - }; - - // Static-final raw arrays placed by StringIndex.Support: the JIT folds these refs to constants on - // the keyOf hot path (the fastest of StringIndex's three usage modes — no instance dereference). - private static final int[] KEYOF_HASHES; - private static final String[] KEYOF_KEYS; - private static final long[] KEYOF_IDS; - - static { - StringIndex.Data data = StringIndex.Support.create(KEYOF_NAMES); - long[] ids = new long[data.names.length]; - for (int j = 0; j < KEYOF_NAMES.length; j++) { - ids[StringIndex.Support.indexOf(data.hashes, data.names, KEYOF_NAMES[j])] = KEYOF_VALUES[j]; - } - KEYOF_HASHES = data.hashes; - KEYOF_KEYS = data.names; - KEYOF_IDS = ids; - } - - static final KnownTagCodec.Resolver RESOLVER = - new KnownTagCodec.Resolver() { - @Override - public String nameOf(long tagId) { - switch (KnownTagCodec.globalSerial(tagId)) { - case ERROR_SERIAL: - return Tags.ERROR; - case PARENT_ID_SERIAL: - return DDTags.PARENT_ID; - case BASE_SERVICE_SERIAL: - return DDTags.BASE_SERVICE; - case VERSION_SERIAL: - return Tags.VERSION; - case ENV_SERIAL: - return ENV; - case DJM_ENABLED_SERIAL: - return DDTags.DJM_ENABLED; - case DSM_ENABLED_SERIAL: - return DDTags.DSM_ENABLED; - case TRACER_HOST_SERIAL: - return DDTags.TRACER_HOST; - case INTEGRATION_SERIAL: - return DDTags.DD_INTEGRATION; - case SVC_SRC_SERIAL: - return DDTags.DD_SVC_SRC; - case PEER_SERVICE_SERIAL: - return Tags.PEER_SERVICE; - case PEER_SERVICE_REMAPPED_FROM_SERIAL: - return DDTags.PEER_SERVICE_REMAPPED_FROM; - case HTTP_METHOD_SERIAL: - return Tags.HTTP_METHOD; - case HTTP_ROUTE_SERIAL: - return Tags.HTTP_ROUTE; - case HTTP_URL_SERIAL: - return Tags.HTTP_URL; - case PEER_HOSTNAME_SERIAL: - return Tags.PEER_HOSTNAME; - case PEER_HOST_IPV4_SERIAL: - return Tags.PEER_HOST_IPV4; - case PEER_HOST_IPV6_SERIAL: - return Tags.PEER_HOST_IPV6; - case PEER_PORT_SERIAL: - return Tags.PEER_PORT; - case COMPONENT_SERIAL: - return Tags.COMPONENT; - case SPAN_KIND_SERIAL: - return Tags.SPAN_KIND; - case LANGUAGE_SERIAL: - return DDTags.LANGUAGE_TAG_KEY; - case DB_TYPE_SERIAL: - return Tags.DB_TYPE; - case DB_INSTANCE_SERIAL: - return Tags.DB_INSTANCE; - case DB_USER_SERIAL: - return Tags.DB_USER; - case DB_OPERATION_SERIAL: - return Tags.DB_OPERATION; - case DB_POOL_NAME_SERIAL: - return Tags.DB_POOL_NAME; - default: - return null; - } - } - - @Override - public int slotCount() { - return SLOT_COUNT; - } - - @Override - public long keyOf(String name) { - int slot = StringIndex.Support.indexOf(KEYOF_HASHES, KEYOF_KEYS, name); - return slot < 0 ? 0L : KEYOF_IDS[slot]; - } - }; - - static { - KnownTagCodec.register(RESOLVER); - } - - /** - * Forces resolver registration. Merely invoking this static method runs {@code } (which - * registers {@link #RESOLVER}), so calling it once at tracer init flips the dense store live; - * idempotent. Until something references this class the registry stays dormant and {@code keyOf} - * returns 0, so tag storage is byte-identical to the bucket-only behavior. - */ - public static void init() {} - - private KnownTags() {} -} 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 11d01bc7ec8..7539f046ca8 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -2,123 +2,144 @@ 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; -import datadog.trace.bootstrap.instrumentation.api.Tags; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; /** - * Parity test for the keyOf substrate (slice 1): the {@link KnownTags} registry + the {@link - * KnownTagCodec.Resolver} it registers. Verifies name ↔ id resolution without any dense store - * — {@code keyOf}/{@code nameOf} depend only on globalSerial + name, not on the (dormant) - * positional layout. + * Property test for the generated {@link KnownTags} registry + the {@link KnownTagCodec.Resolver} + * it registers. Tests resolver behavior (name ↔ id round-trip, intercepted flag, + * reserved/stored tier, unique serials) over the tag names, rather than hard-coded ids — so it + * stays valid as the generator reassigns serials/slots across builds. */ class KnownTagsTest { - /** (name, id) pairs — the full registry. keyOf returns the id verbatim (incl. INTERCEPTED). */ - static Stream knownTags() { + /** Every tag name in the registry (virtual + stored + trace-level). keyOf must resolve each. */ + static Stream knownNames() { return Stream.of( - Arguments.of(Tags.ERROR, KnownTags.ERROR_ID), - Arguments.of(DDTags.PARENT_ID, KnownTags.PARENT_ID), - Arguments.of(DDTags.BASE_SERVICE, KnownTags.BASE_SERVICE_ID), - Arguments.of(Tags.VERSION, KnownTags.VERSION_ID), - Arguments.of(KnownTags.ENV, KnownTags.ENV_ID), - Arguments.of(DDTags.DJM_ENABLED, KnownTags.DJM_ENABLED_ID), - Arguments.of(DDTags.DSM_ENABLED, KnownTags.DSM_ENABLED_ID), - Arguments.of(DDTags.TRACER_HOST, KnownTags.TRACER_HOST_ID), - Arguments.of(DDTags.DD_INTEGRATION, KnownTags.INTEGRATION_ID), - Arguments.of(DDTags.DD_SVC_SRC, KnownTags.SVC_SRC_ID), - Arguments.of(Tags.PEER_SERVICE, KnownTags.PEER_SERVICE_ID), - Arguments.of(DDTags.PEER_SERVICE_REMAPPED_FROM, KnownTags.PEER_SERVICE_REMAPPED_FROM_ID), - Arguments.of(Tags.HTTP_METHOD, KnownTags.HTTP_METHOD_ID), - Arguments.of(Tags.HTTP_ROUTE, KnownTags.HTTP_ROUTE_ID), - Arguments.of(Tags.HTTP_URL, KnownTags.HTTP_URL_ID), - Arguments.of(Tags.PEER_HOSTNAME, KnownTags.PEER_HOSTNAME_ID), - Arguments.of(Tags.PEER_HOST_IPV4, KnownTags.PEER_HOST_IPV4_ID), - Arguments.of(Tags.PEER_HOST_IPV6, KnownTags.PEER_HOST_IPV6_ID), - Arguments.of(Tags.PEER_PORT, KnownTags.PEER_PORT_ID), - Arguments.of(Tags.COMPONENT, KnownTags.COMPONENT_ID), - Arguments.of(Tags.SPAN_KIND, KnownTags.SPAN_KIND_ID), - Arguments.of(DDTags.LANGUAGE_TAG_KEY, KnownTags.LANGUAGE_ID), - Arguments.of(Tags.DB_TYPE, KnownTags.DB_TYPE_ID), - Arguments.of(Tags.DB_INSTANCE, KnownTags.DB_INSTANCE_ID), - Arguments.of(Tags.DB_USER, KnownTags.DB_USER_ID), - Arguments.of(Tags.DB_OPERATION, KnownTags.DB_OPERATION_ID), - Arguments.of(Tags.DB_POOL_NAME, KnownTags.DB_POOL_NAME_ID)); - } - - /** - * The subset flagged INTERCEPTED (sign bit) — must agree with the interceptor's needsIntercept. - */ - static Stream interceptedTags() { - return Stream.of( - Arguments.of(KnownTags.ERROR_ID), - Arguments.of(KnownTags.PEER_SERVICE_ID), - Arguments.of(KnownTags.HTTP_METHOD_ID), - Arguments.of(KnownTags.HTTP_URL_ID), - Arguments.of(KnownTags.SPAN_KIND_ID)); + // virtual / reserved + "error", + "service", + "resource.name", + "span.type", + "origin", + "sampling.priority", + "manual.keep", + "manual.drop", + "measured", + "analytics.sample_rate", + // base (per-span) + "_dd.parent_id", + "component", + "span.kind", + "_dd.integration", + "_dd.svc_src", + "error.type", + "error.message", + "error.stack", + // http + "http.method", + "http.status_code", + "network.protocol.version", + "http.url", + "http.route", + "http.hostname", + "http.useragent", + "http.query.string", + "servlet.path", + "servlet.context", + "http.resend_count", + // db + "db.type", + "db.instance", + "db.operation", + "db.user", + "db.pool.name", + "db.statement", + // peer + "peer.service", + "_dd.peer.service.source", + "_dd.peer.service.remapped_from", + "peer.hostname", + "peer.ipv4", + "peer.ipv6", + "peer.port", + // view + "view.name", + // trace-level + "_dd.base_service", + "version", + "env", + "language", + "runtime-id", + "_dd.tracer_host", + "_dd.git.commit.sha", + "_dd.git.repository_url", + "_dd.profiling.enabled", + "_dd.dsm.enabled", + "_dd.appsec.enabled", + "_dd.djm.enabled", + "_dd.civisibility.enabled"); } @Test - void resolverIsActiveOnceReferenced() { - // referencing any constant triggers KnownTags. -> KnownTagCodec.register - assertTrue(KnownTags.ERROR_ID != 0L); + void resolverActiveOnceReferenced() { + KnownTags.init(); // triggers -> KnownTagCodec.register assertTrue(KnownTagCodec.isActive()); assertEquals(KnownTags.SLOT_COUNT, KnownTagCodec.slotCount()); } @ParameterizedTest - @MethodSource("knownTags") - void keyOfResolvesNameToId(String name, long id) { - assertEquals(id, KnownTagCodec.keyOf(name), "keyOf(" + name + ")"); + @MethodSource("knownNames") + void nameIdRoundTrips(String name) { + long id = KnownTagCodec.keyOf(name); + assertNotEquals(0L, id, "keyOf(" + name + ") should resolve"); + assertEquals(name, KnownTagCodec.nameOf(id), "nameOf(keyOf(" + name + "))"); } @ParameterizedTest - @MethodSource("knownTags") - void nameOfResolvesIdToName(String name, long id) { - assertEquals(name, KnownTagCodec.nameOf(id), "nameOf(" + name + ")"); + @ValueSource( + strings = { + "span.kind", + "http.method", + "http.url", + "db.statement", + "peer.service", + "error", + "service" + }) + void interceptedTagsCarryFlag(String name) { + assertTrue(KnownTagCodec.isIntercepted(KnownTagCodec.keyOf(name)), "intercepted: " + name); } @ParameterizedTest - @MethodSource("knownTags") - void nameHashMatchesEntryHash(String name, long id) { - assertEquals( - (int) TagMap.Entry._hash(name), KnownTagCodec.nameHash(id), "nameHash(" + name + ")"); - } - - @ParameterizedTest - @MethodSource("interceptedTags") - void interceptedTagsCarryFlag(long id) { - assertTrue(KnownTagCodec.isIntercepted(id), "isIntercepted"); + @ValueSource(strings = {"component", "db.type", "version", "http.route", "peer.hostname"}) + void nonInterceptedTagsDoNotCarryFlag(String name) { + assertFalse(KnownTagCodec.isIntercepted(KnownTagCodec.keyOf(name)), "not intercepted: " + name); } @Test - void nonInterceptedTagsDoNotCarryFlag() { - Set intercepted = new HashSet<>(); - interceptedTags().forEach(a -> intercepted.add((Long) a.get()[0])); - knownTags() - .forEach( - a -> { - long id = (Long) a.get()[1]; - if (!intercepted.contains(id)) { - assertFalse(KnownTagCodec.isIntercepted(id), "not intercepted: " + a.get()[0]); - } - }); + void reservedVsStored() { + assertTrue(KnownTagCodec.isReserved(KnownTagCodec.keyOf("error")), "error reserved"); + assertTrue(KnownTagCodec.isReserved(KnownTagCodec.keyOf("service")), "service reserved"); + assertTrue(KnownTagCodec.isStored(KnownTagCodec.keyOf("component")), "component stored"); + assertTrue( + KnownTagCodec.isStored(KnownTagCodec.keyOf("_dd.base_service")), "base_service stored"); } @Test void unknownNamesResolveToZero() { assertEquals(0L, KnownTagCodec.keyOf("definitely.not.a.known.tag")); - assertEquals(0L, KnownTagCodec.keyOf("http.statuscode")); // close-but-not-listed + assertEquals(0L, KnownTagCodec.keyOf("http.statuscode")); // close but not listed assertEquals(0L, KnownTagCodec.keyOf("")); } @@ -128,25 +149,11 @@ void unknownIdsResolveToNullName() { assertNull(KnownTagCodec.nameOf(KnownTagCodec.tagId(9999, "made.up"))); } - @Test - void errorIsReservedTheRestAreStored() { - assertTrue(KnownTagCodec.isReserved(KnownTags.ERROR_ID), "ERROR reserved"); - assertFalse(KnownTagCodec.isStored(KnownTags.ERROR_ID), "ERROR not stored"); - knownTags() - .forEach( - a -> { - long id = (Long) a.get()[1]; - if (id != KnownTags.ERROR_ID) { - assertTrue(KnownTagCodec.isStored(id), "stored: " + a.get()[0]); - assertFalse(KnownTagCodec.isReserved(id), "not reserved: " + a.get()[0]); - } - }); - } - @Test void globalSerialsAreUnique() { - List serials = new ArrayList<>(); - knownTags().forEach(a -> serials.add((long) KnownTagCodec.globalSerial((Long) a.get()[1]))); - assertEquals(serials.size(), new HashSet<>(serials).size(), "globalSerials must be unique"); + List serials = new ArrayList<>(); + knownNames().forEach(n -> serials.add(KnownTagCodec.globalSerial(KnownTagCodec.keyOf(n)))); + assertEquals(new HashSet<>(serials).size(), serials.size(), "globalSerials must be unique"); + assertFalse(serials.contains(0), "no known name should resolve to serial 0"); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java index a82742c3acd..982156b6afd 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java @@ -35,8 +35,9 @@ class TagMapDenseForkedTest { @BeforeAll static void registerResolver() { - // referencing any KnownTags constant triggers its -> KnownTagCodec.register - assertTrue(KnownTags.BASE_SERVICE_ID != 0L); + // Generated id constants are compile-time literals (javac-folded), so referencing one does NOT + // trigger ; init() forces the resolver registration explicitly. + KnownTags.init(); assertTrue(KnownTagCodec.isActive(), "resolver must be live for the dense store to engage"); assertTrue( KnownTagCodec.isStored(KnownTagCodec.keyOf(BASE_SERVICE)), "base_service routes dense"); diff --git a/tag-conventions.java.yaml b/tag-conventions.java.yaml new file mode 100644 index 00000000000..554b4351f42 --- /dev/null +++ b/tag-conventions.java.yaml @@ -0,0 +1,34 @@ +# dd-trace-java overlay — impl hints + special-tag registry. +# Consumed alongside the language-agnostic tag-conventions.yaml. Keyed by tag name +# (these are tag-intrinsic, not per-type). Only exceptions are listed. +# --------------------------------------------------------------------------- +# NOTE: slot/position is NOT here — the generator assigns it via graph coloring, and +# colorability is derived from the domain `required` level (required/recommended packed). + +# Tags whose set-path is handled by the Java TagInterceptor (side-effecting, but still stored). +# Generated ids carry the intercepted flag (bit 63). +intercepted: + - span.kind + - http.method + - http.url + - servlet.context + - db.statement + - peer.service + +# Virtual / special keys: accepted by the public setTag(...) API but ROUTED to a span field or a +# trace directive instead of tag storage. Reserved-tier ids (serial < FIRST_STORED_SERIAL, no slot). +# The generated id->handler dispatch table is the data-driven replacement for the imperative +# TagInterceptor chain. +# kind: structural -> sets a span/trace field (`field:` names it) +# kind: directive -> triggers sampling/trace behavior +virtual: + - { tag: error, kind: structural, field: error } + - { tag: service, kind: structural, field: service, aliases: [service.name] } + - { tag: resource.name, kind: structural, field: resource } + - { tag: span.type, kind: structural, field: type } + - { tag: origin, kind: structural, field: origin } # trace-level field + - { tag: sampling.priority, kind: directive } + - { tag: manual.keep, kind: directive } + - { tag: manual.drop, kind: directive } + - { tag: measured, kind: directive } + - { tag: analytics.sample_rate, kind: directive } # legacy diff --git a/tag-conventions.yaml b/tag-conventions.yaml new file mode 100644 index 00000000000..284c2b42848 --- /dev/null +++ b/tag-conventions.yaml @@ -0,0 +1,132 @@ +# Tag conventions — LANGUAGE-AGNOSTIC domain spec (structure + semantics only) +# --------------------------------------------------------------------------- +# The code generator consumes THIS file (domain) plus a per-language overlay +# (tag-conventions..yaml: impl hints like `intercepted`, and the virtual/ +# special-tag registry) to emit each language's tag-id constants, id<->name +# resolver, and slot (bitmask-bit) assignment. +# +# TRACE-LEVEL is its own thing (its own TagMap "type" on the TraceSegment) — the process/trace +# constants + product flags that are set once per trace, NOT per span. Declared explicitly in the +# `trace_level` section below (a distinct tier), never inferred from `source`. +# +# SPAN TYPES compose three ways: +# extends — structural is-a inheritance (http.server is-a http is-a base). `base` is implicitly +# in every span; abstract layers exist only to be extended. +# include — a span type PULLS in a mixin it intrinsically has (has-a; core-owned). +# applies — a mixin PUSHES itself onto span types, gated by `enabled_by`. +# resolved_tags(type) = own + extends-chain (incl base) + included mixins + applied mixins (de-duped). +# +# tag fields (DOMAIN only): tag | type (string|int|long|boolean|double) +# | required (required|conditional|recommended|optional|opt_in) | aliases. +# Slot/bit is NOT authored here — the generator assigns it via graph coloring; colorability derives +# from `required` (required/conditional/recommended packed; else bucketed). See the design doc. +# --------------------------------------------------------------------------- + +# Trace-level tier: its own TagMap on the TraceSegment. Set once per trace, not per span. +trace_level: + tags: + - { tag: _dd.base_service, type: string, required: required } + - { tag: version, type: string, required: recommended } + - { tag: env, type: string, required: recommended } + - { tag: language, type: string, required: required } + - { tag: runtime-id, type: string, required: required } + - { tag: _dd.tracer_host, type: string, required: recommended } + - { tag: _dd.git.commit.sha, type: string, required: recommended } + - { tag: _dd.git.repository_url, type: string, required: recommended } + # product .enabled flags — process-constant; present on the trace segment regardless of whether + # the product is enabled (the flag carries the state), so always-present => recommended. + - { tag: _dd.profiling.enabled, type: boolean, required: recommended } + - { tag: _dd.dsm.enabled, type: boolean, required: recommended } + - { tag: _dd.appsec.enabled, type: boolean, required: recommended } + - { tag: _dd.djm.enabled, type: boolean, required: recommended } + - { tag: _dd.civisibility.enabled, type: boolean, required: recommended } + +span_types: + # root: per-span tags every span has (incl. the per-span core tags parent_id / integration / svc_src + # — core-set but per-span, so NOT trace-level). + base: + abstract: true + tags: + - { tag: _dd.parent_id, type: string, required: required } + - { tag: component, type: string, required: required } + - { tag: span.kind, type: string, required: required } + - { tag: _dd.integration, type: string, required: recommended } + - { tag: _dd.svc_src, type: string, required: optional } + - { tag: error.type, type: string, required: recommended } + - { tag: error.message, type: string, required: recommended } + - { tag: error.stack, type: string, required: recommended } + + http: + abstract: true + extends: base + tags: + - { tag: http.method, type: string, required: required, aliases: [http.request.method] } + - { tag: http.status_code, type: int, required: conditional, aliases: [http.response.status_code] } + - { tag: network.protocol.version, type: string, required: recommended } + + http.server: + extends: http + tags: + - { tag: http.url, type: string, required: required, aliases: [url.full] } + - { tag: http.route, type: string, required: conditional } + - { tag: http.hostname, type: string, required: required, aliases: [server.address] } + - { tag: http.useragent, type: string, required: recommended } + - { tag: http.query.string, type: string, required: recommended, aliases: [url.query] } + - { tag: servlet.path, type: string, required: optional } + - { tag: servlet.context, type: string, required: optional } + + http.client: + extends: http + include: [ peer ] + tags: + - { tag: http.url, type: string, required: required, aliases: [url.full] } + - { tag: http.resend_count, type: int, required: recommended } + + db.client: + extends: base + include: [ peer ] + tags: + - { tag: db.type, type: string, required: required, aliases: [db.system] } + - { tag: db.instance, type: string, required: recommended } + - { tag: db.operation, type: string, required: recommended, aliases: [db.operation.name] } + - { tag: db.user, type: string, required: recommended } + - { tag: db.pool.name, type: string, required: optional } + - { tag: db.statement, type: string, required: recommended, aliases: [db.query.text] } + + view.render: + extends: base + tags: + - { tag: view.name, type: string, required: recommended } + +mixins: + # peer — outbound/remote-peer capability, PULLED via `include` by client span types. + peer: + tags: + - { tag: peer.service, type: string, required: recommended } + - { tag: _dd.peer.service.source, type: string, required: recommended } + - { tag: _dd.peer.service.remapped_from, type: string, required: recommended } + - { tag: peer.hostname, type: string, required: recommended } + - { tag: peer.ipv4, type: string } + - { tag: peer.ipv6, type: string } + - { tag: peer.port, type: int } + + # ci_visibility — per-span test tags. Its capability flag (_dd.civisibility.enabled) lives in + # trace_level, outside this mixin (general rule: capability flags are trace-level, mixins hold the + # per-span tags). Applies to the `test` span type (not modeled here yet). + ci_visibility: + enabled_by: dd.civisibility.enabled + applies: [ test ] + tags: + - { tag: test.name, type: string, required: recommended } + - { tag: test.suite, type: string, required: recommended } + - { tag: test.status, type: string, required: recommended } + - { tag: test.framework, type: string, required: recommended } + +# --------------------------------------------------------------------------- +# Notes +# - Product .enabled flags moved to `trace_level` (process-constant) — the old product mixins held +# only those flags, so they dissolved. `enabled_by`/attachment gating is a runtime concern. +# - span.kind enumerates: server | client | producer | consumer | internal | broker. +# - Virtual/special keys (service, resource.name, error, sampling.priority, ...) route to span +# fields/directives, not tag storage — they live in the per-language overlay, not here. +# ---------------------------------------------------------------------------