Skip to content

Commit e6800b8

Browse files
dougqhclaude
andcommitted
Generate KnownTags from tag-conventions via the tag-registry code generator
Replaces the hand-maintained known-tag table with one emitted from a single source of truth, so tag ids / resolver / (later) per-type layouts stop drifting across core, instrumentation, and language tracers. Generator (buildSrc, à la SupportedConfigPlugin): - TagRegistryGeneratorPlugin + GenerateKnownTagsTask read tag-conventions.yaml (domain) + tag-conventions.java.yaml (Java overlay), resolve per-type tag sets (extends/include/applies, de-duped), assign ids, and greedily graph-color the colorable set into slots. - KnownTagsEmitter writes google-java-format-clean Java to committed internal-api/src/generated (literal id + `// tagId(...)` audit comment). - VerifyKnownTagsTask (verifyKnownTags) regenerates + git-diffs to catch stale commits; generated classes are jacoco-excluded. internal-api: - Deletes the interim hand-maintained src/main KnownTags in favor of the generated src/generated/.../KnownTags.java (byte-equal ids). - Read-through stays chain-aware: keeps parentDenseVisible (multi-level) over the pre-reconcile single-level form. The generated keyOf table uses StringIndex.EmbeddingSupport (matching the tagset base rename) in both the emitter and its committed output, so regeneration is drift-free. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d6e7088 commit e6800b8

22 files changed

Lines changed: 1956 additions & 556 deletions

buildSrc/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ gradlePlugin {
5959
implementationClass = "datadog.gradle.plugin.config.SupportedConfigPlugin"
6060
}
6161

62+
create("tag-registry-generator") {
63+
id = "dd-trace-java.tag-registry-generator"
64+
implementationClass = "datadog.gradle.plugin.tags.TagRegistryGeneratorPlugin"
65+
}
66+
6267
create("supported-config-linter") {
6368
id = "dd-trace-java.config-inversion-linter"
6469
implementationClass = "datadog.gradle.plugin.config.ConfigInversionLinter"
@@ -107,6 +112,7 @@ dependencies {
107112
implementation("com.fasterxml.jackson.core:jackson-databind")
108113
implementation("com.fasterxml.jackson.core:jackson-annotations")
109114
implementation("com.fasterxml.jackson.core:jackson-core")
115+
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
110116

111117
compileOnly(libs.develocity)
112118
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package datadog.gradle.plugin.tags
2+
3+
import javax.inject.Inject
4+
import org.gradle.api.DefaultTask
5+
import org.gradle.api.file.DirectoryProperty
6+
import org.gradle.api.file.RegularFileProperty
7+
import org.gradle.api.model.ObjectFactory
8+
import org.gradle.api.tasks.CacheableTask
9+
import org.gradle.api.tasks.InputFile
10+
import org.gradle.api.tasks.OutputDirectory
11+
import org.gradle.api.tasks.PathSensitive
12+
import org.gradle.api.tasks.PathSensitivity
13+
import org.gradle.api.tasks.TaskAction
14+
15+
/**
16+
* Generates the committed tag registry (KnownTags.java + layout reports) from the language-agnostic
17+
* {@code tag-conventions.yaml} + the Java overlay. The actual emit lives in [TagRegistryGenerator];
18+
* this task just wires the inputs/outputs so Gradle can cache and up-to-date-check it.
19+
*/
20+
@CacheableTask
21+
abstract class GenerateKnownTagsTask @Inject constructor(objects: ObjectFactory) : DefaultTask() {
22+
@get:InputFile
23+
@get:PathSensitive(PathSensitivity.NONE)
24+
val domainYaml: RegularFileProperty = objects.fileProperty()
25+
26+
@get:InputFile
27+
@get:PathSensitive(PathSensitivity.NONE)
28+
val overlayYaml: RegularFileProperty = objects.fileProperty()
29+
30+
@get:OutputDirectory val destinationDirectory: DirectoryProperty = objects.directoryProperty()
31+
32+
@TaskAction
33+
fun generate() {
34+
val outDir = destinationDirectory.get().asFile
35+
TagRegistryGenerator.generate(domainYaml.get().asFile, overlayYaml.get().asFile, outDir)
36+
logger.lifecycle("tag-registry: generated -> $outDir")
37+
}
38+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package datadog.gradle.plugin.tags
2+
3+
/**
4+
* Emits the generated `KnownTags.java` from a [TagRegistry]. Public API first — per-tag
5+
* `<X>_NAME` (string) + `<X>_ID` (encoded long, literal) couplets with a trailing `// makeTagId(...)`
6+
* derivation comment — then the package-private `<X>_SERIAL_NUM` constants, the
7+
* `StringIndex.EmbeddingSupport` keyOf table, the `serialNum` switch `nameOf`, and resolver
8+
* registration.
9+
*/
10+
object KnownTagsEmitter {
11+
12+
fun emit(reg: TagRegistry, pkg: String, className: String): String {
13+
// Sanitize tag names into unique Java constant identifiers.
14+
val used = HashSet<String>()
15+
val cname = HashMap<String, String>()
16+
fun mk(name: String): String {
17+
var c = name.uppercase().replace(Regex("[^A-Za-z0-9]"), "_").replace(Regex("_+"), "_").trim('_')
18+
if (c.isEmpty() || c[0].isDigit()) c = "T_$c"
19+
var u = c
20+
var n = 2
21+
while (u in used) {
22+
u = "${c}_$n"; n++
23+
}
24+
used.add(u)
25+
cname[name] = u
26+
return u
27+
}
28+
reg.reserved.forEach { mk(it.name) }
29+
reg.stored.forEach { mk(it.name) }
30+
31+
// Constant names. Collapse a duplicated trailing token so e.g. "resource.name" yields NAME
32+
// (not NAME_NAME) and "_dd.parent_id" yields ID (not ID_ID); the non-duplicating pairs
33+
// (ID + _NAME -> ID_NAME, NAME + _ID -> NAME_ID) are kept as-is.
34+
fun withSuffix(base: String, suffix: String) = if (base.endsWith(suffix)) base else "$base$suffix"
35+
fun nameC(name: String) = withSuffix(cname[name]!!, "_NAME")
36+
fun idC(name: String) = withSuffix(cname[name]!!, "_ID")
37+
fun serialC(name: String) = withSuffix(cname[name]!!, "_SERIAL_NUM")
38+
39+
val order = reg.reserved.map { it.name } + reg.stored.map { it.name } // stable emit order
40+
val b = StringBuilder()
41+
b.appendLine("package $pkg;")
42+
b.appendLine()
43+
b.appendLine("import datadog.trace.util.StringIndex;")
44+
b.appendLine()
45+
b.appendLine("// GENERATED by the tag-registry code generator (dd-trace-java.tag-registry-generator).")
46+
b.appendLine("// DO NOT EDIT. Source: tag-conventions.yaml + tag-conventions.java.yaml.")
47+
b.appendLine("public final class $className {")
48+
b.appendLine(" static final int SLOT_COUNT = ${reg.slotCount};")
49+
b.appendLine()
50+
51+
// Public API first (name + encoded id couplets), so readers see the useful parts up top; the
52+
// serial ids and keyOf/resolver machinery follow below. Derivation is in the trailing comment.
53+
b.appendLine(" // ---- reserved (routed to span fields or directives; not stored) ----")
54+
for (v in reg.reserved) {
55+
b.appendLine(" public static final String ${nameC(v.name)} = \"${v.name}\";")
56+
b.appendLine(" public static final long ${idC(v.name)} = ${hex(v.id)};")
57+
b.appendLine(" // makeTagId(serial=${v.serial}, group=0, field=NO_SLOT) + intercepted [${v.kind}${v.field?.let { " -> $it" } ?: ""}]")
58+
b.appendLine()
59+
}
60+
61+
b.appendLine(" // ---- stored (dense field-decl, or bucketed when field=NO_SLOT) ----")
62+
for (t in reg.stored) {
63+
val field = if (t.slotted) t.fieldDecl.toString() else "NO_SLOT"
64+
b.appendLine(" public static final String ${nameC(t.name)} = \"${t.name}\";")
65+
b.appendLine(" public static final long ${idC(t.name)} = ${hex(t.id)};")
66+
b.appendLine(" // makeTagId(serial=${t.serial}, group=${t.groupDecl}, field=$field)${if (t.intercepted) " + intercepted" else ""} <${t.required}${if (t.traceLevel) ", trace-level" else ""}>")
67+
b.appendLine()
68+
}
69+
70+
// Serial numbers (globalSerial per tag) — package-private, consumed by the resolver switch.
71+
b.appendLine(" // ---- serial numbers ----")
72+
for (v in reg.reserved) {
73+
b.appendLine(" static final int ${serialC(v.name)} = ${v.serial};")
74+
}
75+
for (t in reg.stored) {
76+
b.appendLine(" static final int ${serialC(t.name)} = ${t.serial};")
77+
}
78+
b.appendLine()
79+
80+
// keyOf table (open-addressed, via StringIndex.EmbeddingSupport).
81+
b.appendLine(" private static final String[] KEYOF_NAMES = {")
82+
order.forEach { b.appendLine(" ${nameC(it)},") }
83+
b.appendLine(" };")
84+
b.appendLine(" private static final long[] KEYOF_VALUES = {")
85+
order.forEach { b.appendLine(" ${idC(it)},") }
86+
b.appendLine(" };")
87+
b.appendLine(" private static final int[] KEYOF_HASHES;")
88+
b.appendLine(" private static final String[] KEYOF_KEYS;")
89+
b.appendLine(" private static final long[] KEYOF_IDS;")
90+
b.appendLine(" static {")
91+
b.appendLine(" StringIndex.Data data = StringIndex.EmbeddingSupport.create(KEYOF_NAMES);")
92+
b.appendLine(" long[] ids = new long[data.names.length];")
93+
b.appendLine(" for (int j = 0; j < KEYOF_NAMES.length; j++) {")
94+
b.appendLine(" ids[StringIndex.EmbeddingSupport.indexOf(data.hashes, data.names, KEYOF_NAMES[j])] = KEYOF_VALUES[j];")
95+
b.appendLine(" }")
96+
b.appendLine(" KEYOF_HASHES = data.hashes;")
97+
b.appendLine(" KEYOF_KEYS = data.names;")
98+
b.appendLine(" KEYOF_IDS = ids;")
99+
b.appendLine(" }")
100+
b.appendLine()
101+
102+
// Resolver.
103+
b.appendLine(" static final KnownTagCodec.Resolver RESOLVER =")
104+
b.appendLine(" new KnownTagCodec.Resolver() {")
105+
b.appendLine(" @Override")
106+
b.appendLine(" public String nameOf(long tagId) {")
107+
b.appendLine(" switch (KnownTagCodec.serialNum(tagId)) {")
108+
for (name in order) {
109+
b.appendLine(" case ${serialC(name)}:")
110+
b.appendLine(" return ${nameC(name)};")
111+
}
112+
b.appendLine(" default:")
113+
b.appendLine(" return null;")
114+
b.appendLine(" }")
115+
b.appendLine(" }")
116+
b.appendLine()
117+
b.appendLine(" @Override")
118+
b.appendLine(" public int slotCount() {")
119+
b.appendLine(" return SLOT_COUNT;")
120+
b.appendLine(" }")
121+
b.appendLine()
122+
b.appendLine(" @Override")
123+
b.appendLine(" public long keyOf(String name) {")
124+
b.appendLine(" int slot = StringIndex.EmbeddingSupport.indexOf(KEYOF_HASHES, KEYOF_KEYS, name);")
125+
b.appendLine(" return slot < 0 ? 0L : KEYOF_IDS[slot];")
126+
b.appendLine(" }")
127+
b.appendLine(" };")
128+
b.appendLine()
129+
b.appendLine(" static {")
130+
b.appendLine(" KnownTagCodec.register(RESOLVER);")
131+
b.appendLine(" }")
132+
b.appendLine()
133+
b.appendLine(" /** Forces resolver registration by triggering <clinit>. Idempotent. */")
134+
b.appendLine(" public static void init() {}")
135+
b.appendLine()
136+
b.appendLine(" private $className() {}")
137+
b.appendLine("}")
138+
return b.toString()
139+
}
140+
141+
private fun hex(id: Long): String = "0x%016XL".format(id)
142+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package datadog.gradle.plugin.tags
2+
3+
/**
4+
* Parsed tag-conventions domain model + the per-type tag-set resolver. Language-agnostic: it knows
5+
* only structure (extends / include / applies) and per-tag semantics (name / type / required /
6+
* source). Id assignment and emission are layered on top of the resolved sets.
7+
*/
8+
class TagConventions
9+
private constructor(
10+
private val spanTypes: Map<String, SpanType>,
11+
private val mixins: Map<String, Mixin>,
12+
private val traceLevel: List<Tag>,
13+
) {
14+
/** A tag declaration (domain semantics only). */
15+
data class Tag(
16+
val name: String,
17+
val type: String,
18+
val required: String,
19+
)
20+
21+
data class SpanType(
22+
val name: String,
23+
val abstract: Boolean,
24+
val extends: String?,
25+
val include: List<String>,
26+
val tags: List<Tag>,
27+
)
28+
29+
data class Mixin(
30+
val name: String,
31+
val appliesAll: Boolean,
32+
val appliesTo: Set<String>,
33+
val tags: List<Tag>,
34+
)
35+
36+
/** Concrete (instantiable) span types — the ones a layout is computed for. */
37+
fun concreteTypes(): List<String> =
38+
spanTypes.values.filter { !it.abstract }.map { it.name }.sorted()
39+
40+
/**
41+
* resolved(type) = own tags + tags up the `extends` chain (incl. base) + tags of every mixin the
42+
* type or an ancestor `include`s + tags of every mixin whose `applies` matches. De-duped by tag
43+
* name (first occurrence wins). Base-first order, so it is stable across runs.
44+
*/
45+
fun resolve(typeName: String): List<Tag> {
46+
val result = LinkedHashMap<String, Tag>()
47+
fun add(t: Tag) = result.putIfAbsent(t.name, t)
48+
49+
val chain = ArrayList<SpanType>()
50+
var cur: SpanType? = spanTypes[typeName]
51+
while (cur != null) {
52+
chain.add(cur)
53+
cur = cur.extends?.let { spanTypes[it] }
54+
}
55+
for (st in chain.asReversed()) {
56+
st.tags.forEach { add(it) }
57+
for (mixinName in st.include) mixins[mixinName]?.tags?.forEach { add(it) }
58+
}
59+
val chainNames = chain.map { it.name }.toSet()
60+
for (mx in mixins.values) {
61+
if (mx.appliesAll || mx.appliesTo.any { it in chainNames }) mx.tags.forEach { add(it) }
62+
}
63+
return result.values.toList()
64+
}
65+
66+
/** The explicit trace-level tier tags (their own TagMap "type" on the TraceSegment). */
67+
fun traceLevelTags(): List<Tag> = traceLevel
68+
69+
/** A declaration group: the source that *declares* a set of tags (its own `tags:` list). */
70+
data class Group(val name: String, val kind: String, val tags: List<Tag>)
71+
72+
/**
73+
* The declaration groups, in a stable order: the trace-level tier first, then every span type
74+
* (abstract included — `base`/`http` declare real tags) sorted by name, then every mixin sorted by
75+
* name. Each maps to one `group-decl`. A tag is *declared* once (in its own container's `tags:`);
76+
* the same tag reached via extends/include/applies is not re-declared, so first-declaration (in
77+
* this order) is its home group. Groups with no declared tags are omitted.
78+
*/
79+
fun declarationGroups(): List<Group> {
80+
val groups = ArrayList<Group>()
81+
if (traceLevel.isNotEmpty()) groups.add(Group(TRACE_LAYER, "trace", traceLevel))
82+
for (name in spanTypes.keys.sorted()) {
83+
val st = spanTypes.getValue(name)
84+
if (st.tags.isNotEmpty()) groups.add(Group(name, "span_type", st.tags))
85+
}
86+
for (name in mixins.keys.sorted()) {
87+
val mx = mixins.getValue(name)
88+
if (mx.tags.isNotEmpty()) groups.add(Group(name, "mixin", mx.tags))
89+
}
90+
return groups
91+
}
92+
93+
/** Full stored-tag universe (concrete span types' resolves + trace-level), de-duped by name. */
94+
fun allStoredTags(): List<Tag> {
95+
val union = LinkedHashMap<String, Tag>()
96+
for (type in concreteTypes()) for (t in resolve(type)) union.putIfAbsent(t.name, t)
97+
for (t in traceLevel) union.putIfAbsent(t.name, t)
98+
return union.values.toList()
99+
}
100+
101+
/**
102+
* Full composition for a type as (origin, tag) pairs, in composition order and NOT de-duped, so a
103+
* tag contributed by more than one source shows up more than once. Origin is the contributing
104+
* span type (via extends), `incl:<mixin>` (via include), or `appl:<mixin>` (via applies).
105+
*/
106+
fun compose(typeName: String): List<Pair<String, Tag>> {
107+
val out = ArrayList<Pair<String, Tag>>()
108+
val chain = ArrayList<SpanType>()
109+
var cur: SpanType? = spanTypes[typeName]
110+
while (cur != null) {
111+
chain.add(cur)
112+
cur = cur.extends?.let { spanTypes[it] }
113+
}
114+
for (st in chain.asReversed()) {
115+
st.tags.forEach { out.add(st.name to it) }
116+
for (mixinName in st.include) mixins[mixinName]?.tags?.forEach { out.add("incl:$mixinName" to it) }
117+
}
118+
val chainNames = chain.map { it.name }.toSet()
119+
for (mx in mixins.values) {
120+
if (mx.appliesAll || mx.appliesTo.any { it in chainNames }) {
121+
mx.tags.forEach { out.add("appl:${mx.name}" to it) }
122+
}
123+
}
124+
return out
125+
}
126+
127+
companion object {
128+
/** Group name of the trace-level tier (its own TagMap layer on the TraceSegment). */
129+
const val TRACE_LAYER = "<trace>"
130+
131+
@Suppress("UNCHECKED_CAST")
132+
fun parse(root: Map<String, Any?>): TagConventions {
133+
val spanTypesRaw = (root["span_types"] as? Map<String, Any?>) ?: emptyMap()
134+
val spanTypes =
135+
spanTypesRaw.mapValues { (name, v) ->
136+
val m = v as Map<String, Any?>
137+
SpanType(
138+
name = name,
139+
abstract = (m["abstract"] as? Boolean) ?: false,
140+
extends = m["extends"] as? String,
141+
include = (m["include"] as? List<String>) ?: emptyList(),
142+
tags = tagList(m["tags"]),
143+
)
144+
}
145+
146+
val mixinsRaw = (root["mixins"] as? Map<String, Any?>) ?: emptyMap()
147+
val mixins =
148+
mixinsRaw.mapValues { (name, v) ->
149+
val m = v as Map<String, Any?>
150+
val applies = m["applies"]
151+
Mixin(
152+
name = name,
153+
appliesAll = applies == "all",
154+
appliesTo = if (applies is List<*>) applies.map { it.toString() }.toSet() else emptySet(),
155+
tags = tagList(m["tags"]),
156+
)
157+
}
158+
159+
val traceLevel = tagList((root["trace_level"] as? Map<String, Any?>)?.get("tags"))
160+
return TagConventions(spanTypes, mixins, traceLevel)
161+
}
162+
163+
@Suppress("UNCHECKED_CAST")
164+
private fun tagList(tags: Any?): List<Tag> =
165+
(tags as? List<Map<String, Any?>>)?.map { m ->
166+
Tag(
167+
name = m["tag"].toString(),
168+
type = (m["type"] as? String) ?: "string",
169+
required = (m["required"] as? String) ?: "optional",
170+
)
171+
} ?: emptyList()
172+
}
173+
}

0 commit comments

Comments
 (0)