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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ 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.Optional
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 + assignment reports) from the language-agnostic
* {@code tag-conventions.yaml}. The actual emit lives in [TagRegistryGenerator];
* {@code tag-conventions.yaml} and this language's routing 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
Expand All @@ -23,13 +24,20 @@ abstract class GenerateKnownTagsTask @Inject constructor(objects: ObjectFactory)
@get:PathSensitive(PathSensitivity.NONE)
val domainYaml: RegularFileProperty = objects.fileProperty()

/** This language's routing overlay. Optional -- absent means "no reserved keys". */
@get:InputFile
@get:Optional
@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, outDir)
TagRegistryGenerator.generate(
domainYaml.get().asFile, overlayYaml.orNull?.asFile, outDir)
logger.lifecycle("tag-registry: generated -> $outDir")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.util.Locale
/**
* Emits the generated `KnownTags.java` from a [TagRegistry]. Public API first — per-tag
* `<X>_NAME` (string) + `<X>_ID` (encoded long, literal) couplets with a trailing `// makeTagId(...)`
* derivation comment — then the package-private `<X>_SERIAL_NUM` constants, the
* derivation comment — then the `<X>_SERIAL_NUM` constants, the
* `StringIndex.EmbeddingSupport` keyOf table, the `serialNum` switch `nameOf`, and resolver
* registration.
*/
Expand Down Expand Up @@ -46,7 +46,7 @@ object KnownTagsEmitter {
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.")
b.appendLine("// DO NOT EDIT. Sources: tag-conventions.yaml + tag-conventions.java.yaml.")
b.appendLine("public final class $className {")
b.appendLine()

Expand All @@ -60,10 +60,13 @@ object KnownTagsEmitter {
b.appendLine()
}

// Serial numbers (globalSerial per tag) — package-private, consumed by the resolver switch.
// Serial numbers (globalSerial per tag). Public: besides the resolver switch below, they are
// the case labels of the tracer's set-path dispatch switch, which lives in another package. An
// int switch over dense serials compiles to a tableswitch, where the equivalent switch over tag
// NAMES is a lookupswitch on string hashes plus an equals() per hit.
b.appendLine(" // ---- serial numbers ----")
for (t in reg.tags) {
b.appendLine(" static final int ${serialC(t.name)} = ${t.serial};")
b.appendLine(" public static final int ${serialC(t.name)} = ${t.serial};")
}
b.appendLine()

Expand Down
105 changes: 105 additions & 0 deletions buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagOverlay.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package datadog.gradle.plugin.tags

/**
* Parsed per-language overlay (`tag-conventions-java.yaml`): the keys that exist only because this
* tracer ROUTES them on the set-path, and so need an identity to dispatch on but no place in the
* language-agnostic domain spec.
*
* <p>Deliberately separate from [TagConventions] rather than a section of it. The domain model's
* value is that it knows only structure and semantics; folding one language's routing vocabulary
* into it would make it not that. Composition happens in [TagRegistry], which is already the layer
* that turns declarations into ids.
*
* <p>An overlay tag carries a name and a type and nothing else. It has no `required` grade (that
* grades how a tag is STORED, and a reserved key is an identity for dispatch), no `otel-name` (a
* domain concern), and — pointedly — no flag saying whether it is also stored, because that is
* decided per call from the value. See the file header and KnownTagCodec for why a static bit there
* is drift rather than information.
*/
class TagOverlay
private constructor(
val reserved: List<Tag>,
/**
* Names of DOMAIN tags this tracer also routes. Names, not declarations: the tag's identity comes
* from the domain spec and is not duplicated here -- being listed only adds the INTERCEPTED flag
* to the id it already has.
*/
val intercepted: List<String>,
) {
/** One reserved key: an identity for set-path dispatch. */
data class Tag(val name: String, val type: String)

companion object {
/** An overlay with nothing in it — the shape used when a language declares no reserved keys. */
fun empty(): TagOverlay = TagOverlay(emptyList(), emptyList())

@Suppress("UNCHECKED_CAST")
fun parse(root: Map<String, Any?>): TagOverlay {
val raw = (root["reserved"] as? Map<String, Any?>)?.get("tags") as? List<Map<String, Any?>>
val decls = raw ?: emptyList()
decls.forEach { rejectDomainFields(it) }
val tags = decls.map { Tag(name = parseDdName(it), type = (it["type"] as? String) ?: "string") }
validateNoDuplicates(tags)
val intercepted = parseIntercepted(root)
return TagOverlay(tags, intercepted)
}

/**
* The `intercepted` list: plain domain tag names, so a string list rather than declarations.
* A non-string entry (an accidental `{ dd-name: x }` mapping, say) must fail rather than
* `toString()` into a name that matches no domain tag and then silently flags nothing.
*/
@Suppress("UNCHECKED_CAST")
private fun parseIntercepted(root: Map<String, Any?>): List<String> {
val raw = (root["intercepted"] as? Map<String, Any?>)?.get("tags") as? List<Any?>
val names =
(raw ?: emptyList()).map { e ->
require(e is String && e.isNotBlank()) {
"intercepted entry is not a tag name: '$e'. List domain tag names as plain strings; " +
"a tag that needs its own identity goes under `reserved:` instead."
}
e
}
val seen = HashSet<String>()
for (n in names) require(seen.add(n)) { "intercepted names '$n' more than once" }
return names
}

/**
* The same routing key declared twice. Harmless to the id assignment (the union de-dupes), but
* it means one of the two declarations is dead and nobody can tell which was intended.
*/
private fun validateNoDuplicates(tags: List<Tag>) {
val seen = HashSet<String>()
for (t in tags) {
require(seen.add(t.name)) { "overlay declares reserved key '${t.name}' more than once" }
}
}

/**
* Domain-only fields on an overlay tag. `required` grades how a tag is STORED and `otel-name` is
* a cross-language naming decision; neither means anything for a routing identity. Ignoring them
* silently would let someone believe they had graded a reserved key as dense, or given it an
* OpenTelemetry name that nothing will ever emit. A key that genuinely needs either belongs in
* the domain spec.
*/
private fun rejectDomainFields(m: Map<String, Any?>) {
for (key in DOMAIN_ONLY_FIELDS) {
require(!m.containsKey(key)) {
"reserved key '${m["dd-name"]}' declares '$key', which is a domain-spec field and has no " +
"meaning for a set-path routing identity. Declare the tag in tag-conventions.yaml if " +
"it needs one."
}
}
}

private val DOMAIN_ONLY_FIELDS = listOf("required", "otel-name")

/** Mirrors [TagConventions] — a missing or non-string name would flow on as the literal "null". */
private fun parseDdName(m: Map<String, Any?>): String {
val raw = m["dd-name"]
require(raw is String && raw.isNotBlank()) { "reserved key declaration has no valid dd-name: $m" }
return raw
}
}
}
110 changes: 105 additions & 5 deletions buildSrc/src/main/kotlin/datadog/gradle/plugin/tags/TagRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,45 +25,145 @@ class TagRegistry private constructor(val tags: List<Tag>) {
val traceLevel: Boolean,
val id: Long,
val otelName: String? = null,
/**
* True when this tracer routes the tag on the set-path -- the INTERCEPTED flag is set in [id].
* Says nothing about whether the tag is also STORED: that is decided per call from the value
* (`http.url` is routed and stored; `manual.keep` is consumed only when its value coerces to a
* boolean), so it is not a property of the tag at all.
*/
val intercepted: Boolean = false,
/**
* True for a tag declared by the per-language overlay (a set-path ROUTING identity) rather than
* by the domain spec. Affects nothing about the id -- an overlay tag's id is an ordinary
* identity -- it only records where the declaration came from, so the reports can show the two
* blocks apart and the overlap guard has something to check.
*/
val overlay: Boolean = false,
)

companion object {
const val FIRST_SERIAL = 1
const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE
const val INTERCEPTED = 1L shl 3 // low-32 carve bit 3; mirrors KnownTagCodec.INTERCEPTED
const val TRACE_LAYER = "<trace>"

/**
* The `required` grade recorded for an overlay tag. A reserved key has no storage grade -- it is
* an identity for set-path dispatch -- so it gets its own value rather than being filed under
* `optional`, which would read as "stored, but rarely".
*/
const val RESERVED = "reserved"

/**
* Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() -- must stay in sync. LEVEL_TRACE at
* bit 2, other low bits and the reserved [47-32] window zero.
*/
fun encode(serial: Int, traceLevel: Boolean): Long {
fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean = false): Long {
var id = serial.toLong() shl 48
if (traceLevel) id = id or LEVEL_TRACE
if (intercepted) id = id or INTERCEPTED
return id
}

fun build(conv: TagConventions): TagRegistry {
fun build(conv: TagConventions): TagRegistry = build(conv, TagOverlay.empty())

/**
* Assigns serials over the domain declarations and then the overlay's reserved keys.
*
* <p>Domain tags are numbered FIRST, sorted by name, exactly as they are without an overlay. So
* the domain block's serials -- and therefore its ids and its generated output -- stay a pure
* function of tag-conventions.yaml alone: adding a Java-only reserved key cannot renumber the
* shared spec. Overlay serials continue from there, also sorted by name, so they too are stable
* against anything but a change to the overlay itself.
*/
fun build(conv: TagConventions, overlay: TagOverlay): TagRegistry {
val traceNames = conv.traceLevelTags().map { it.name }.toSet()
val routedDomain = overlay.intercepted.toSet()

// Stable order (by name) so serials -- and therefore ids -- are a pure function of the input.
val tags =
val domain =
conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t ->
val serial = FIRST_SERIAL + i
val traceLevel = t.name in traceNames
val intercepted = t.name in routedDomain
Tag(
t.name,
t.type,
t.required,
serial,
traceLevel,
id = encode(serial, traceLevel),
otelName = t.otelName)
id = encode(serial, traceLevel, intercepted),
otelName = t.otelName,
intercepted = intercepted)
}

validateNoOverlap(domain, overlay)
validateIntercepted(domain, overlay)

val reserved =
overlay.reserved.sortedBy { it.name }.mapIndexed { i, t ->
val serial = FIRST_SERIAL + domain.size + i
Tag(
t.name,
t.type,
RESERVED,
serial,
traceLevel = false,
id = encode(serial, traceLevel = false, intercepted = true),
otelName = null,
intercepted = true,
overlay = true)
}

val tags = domain + reserved
validateOtelNames(tags)
return TagRegistry(tags)
}

/**
* A reserved key that the domain spec already declares. Both declarations are for one tag, so the
* overlay's would mint a SECOND id for it -- two identities, and dispatch would key off whichever
* the caller happened to resolve. The eight interceptor keys that are domain tags
* (db.statement, service, peer.service, servlet.context, http.status_code, http.method,
* http.url, span.kind) must therefore be absent from the overlay, and this is what enforces it.
*
* <p>An OpenTelemetry name counts as taken too: keyOf is many->one, so a reserved key colliding
* with a domain tag's otel-name would make keyOf(name) ambiguous in exactly the same way.
*/
private fun validateNoOverlap(domain: List<Tag>, overlay: TagOverlay) {
val byName = domain.associateBy { it.name }
val byOtel = domain.mapNotNull { t -> t.otelName?.let { it to t.name } }.toMap()
for (t in overlay.reserved) {
require(t.name !in byName) {
"reserved key '${t.name}' is already declared in the domain spec (tag-conventions.yaml), " +
"so it already has an id; declaring it again in the overlay would mint a second " +
"identity for one tag. Remove it from the overlay."
}
byOtel[t.name]?.let { canonical ->
throw IllegalArgumentException(
"reserved key '${t.name}' collides with the OpenTelemetry name of domain tag " +
"'$canonical', so keyOf('${t.name}') would have two answers.")
}
}
}

/**
* Every `intercepted` name must actually BE a domain tag. A typo there would otherwise flag
* nothing at all: the name matches no declaration, no id gets the INTERCEPTED bit, and the
* set-path pre-screen silently stops recognising a key the interceptor still handles. That is a
* behaviour change with no error message, which is the worst shape this file can fail in.
*/
private fun validateIntercepted(domain: List<Tag>, overlay: TagOverlay) {
val names = domain.map { it.name }.toSet()
for (n in overlay.intercepted) {
require(n in names) {
"intercepted names '$n', which is not declared in the domain spec " +
"(tag-conventions.yaml). Use the tag's canonical dd-name; a key with no domain " +
"declaration belongs under `routed:` instead."
}
}
}

/**
* An OpenTelemetry name must be unambiguous: it may not collide with any canonical tag name, nor
* be claimed by two different tags. Otherwise keyOf(otelName) would have no single right answer.
Expand Down
Loading