Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3ab5862
Set known tags by id on TagMap and spans
dougqh Oct 9, 2026
4bf02bb
Ignore unknown ids when clearing a tag by id; fix a comment wrap
dougqh Oct 9, 2026
a2829d1
Precheck and reuse the box in long-valued setters
dougqh Oct 9, 2026
2d87cde
Keep peer.port off the id path
dougqh Oct 9, 2026
7a017a5
Merge set-by-id (#12715) onto master
dougqh Oct 9, 2026
0401a43
Mark intercepted tags with an id bit, via a tracer overlay
dougqh Oct 9, 2026
8949a57
Dispatch TagInterceptor on tag ids
dougqh Oct 9, 2026
cfe2368
Set tags on spans by id first
dougqh Oct 9, 2026
38c9947
Look up tag names by serial in an array, not a switch
dougqh Oct 9, 2026
3545c15
Point stale comments at the tracer overlay and the intercepted bit
dougqh Oct 9, 2026
1642cc9
Index the split-by-tags table by serial, sized to every known tag
dougqh Oct 9, 2026
b9dd573
Check for a known tag id with a range check, not a name lookup
dougqh Oct 9, 2026
75d3fbc
Pin interceptTag(long) above C2's inlining size
dougqh Oct 9, 2026
ecb4f73
Tighten comments on the id-keyed interceptor
dougqh Oct 9, 2026
3f65031
Treat overlay tags as intercepted; tidy from review
dougqh Oct 9, 2026
69d7849
Reject shared-name tag ids on the id-keyed TagMap and span paths
dougqh Oct 9, 2026
46c3b47
Test rejection of shared-name tag ids
dougqh Oct 9, 2026
ad2fa97
Build custom tags by name and known tags by id
dougqh Oct 10, 2026
628da33
Merge master into id-tag-interceptor (#12817)
dougqh Oct 10, 2026
dac7060
Give the tracer core a restricted, trusted path into TagMap
dougqh Oct 10, 2026
fbab551
Mark TagMap.Internals @NoEscape
dougqh Oct 10, 2026
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
/SECURITY.md @DataDog/apm-java
/SUPPORT.md @DataDog/apm-java
/tag-conventions.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities
/tag-conventions-java.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities
/dd-smoke-tests/README.md @DataDog/apm-java
/docs/ @DataDog/apm-java

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
Expand All @@ -20,12 +21,18 @@ abstract class GenerateKnownTagsTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.NONE)
abstract val tagConventionsFile: RegularFileProperty

@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.NONE)
abstract val tracerOverlayFile: RegularFileProperty

@get:OutputDirectory abstract val destinationDirectory: DirectoryProperty

@TaskAction
fun generate() {
val outDir = destinationDirectory.get().asFile
TagRegistryGenerator.generate(tagConventionsFile.get().asFile, outDir)
TagRegistryGenerator.generate(
tagConventionsFile.get().asFile, outDir, tracerOverlayFile.orNull?.asFile)
logger.lifecycle("tag-registry: generated -> $outDir")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ 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
* `StringIndex.EmbeddingSupport` keyOf table and the resolver's name switches.
* derivation comment — then the `<X>_SERIAL_NUM` constants, the
* `StringIndex.EmbeddingSupport` keyOf table, the `NAMES_BY_SERIAL` array behind `nameOf`, and the
* resolver's `openTelemetryNameOf` switch.
*/
object KnownTagsEmitter {

Expand Down Expand Up @@ -101,16 +102,22 @@ object KnownTagsEmitter {
}
append("// makeTagId(serial=${t.serial})")
if (t.traceLevel) append(" + trace-level")
if (t.intercepted) append(" + intercepted")
if (t.sharedNameDirection != null) append(" + shared-name")
if (t.otelName != null) append(" -> ${escape(t.otelName)}")
appendLine(" <${escape(t.required)}>")
appendLine()
}

// Serial numbers (globalSerial per tag) — package-private, consumed by the resolver switch.
// Serial numbers (globalSerial per tag) — public, so a switch on KnownTagCodec.serialNum outside
// this package (the resolver here, TagInterceptor in core) can name them as case labels.
appendLine(" // ---- serial numbers ----")
for (t in reg.tags) {
appendLine(" static final int ${serialC(t.identity)} = ${t.serial};")
appendLine(" public static final int ${serialC(t.identity)} = ${t.serial};")
Comment thread
AlexeyKuznetsov-DD marked this conversation as resolved.
}
// Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag.
val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1
appendLine(" public static final int SERIAL_LIMIT = $serialLimit;")

// OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all
// canonical names. Sort by OTel name to keep output deterministic.
Expand Down Expand Up @@ -163,6 +170,21 @@ object KnownTagsEmitter {
KEYOF_IDS = ids;
}

/**
* Each tag's Datadog name, indexed by serial; slot 0 (no tag) is null. An array rather than a
* switch keeps nameOf small enough to inline at every caller.
*/
static final String[] NAMES_BY_SERIAL = {
null,
""".trimIndent()
)
for (t in reg.tags.sortedBy { it.serial }) {
appendLine(" ${nameExpr(t)},")
}
appendLine(
"""
};

/**
* The registry's name&harr;id tables, as a {@link KnownTagCodec.Resolver}. {@code KnownTagCodec}
* reads this field from its own holder, so the two classes complete each other: the codec owns
Expand All @@ -172,26 +194,12 @@ object KnownTagsEmitter {
new KnownTagCodec.Resolver() {
@Override
public String nameOf(long tagId) {
switch (KnownTagCodec.serialNum(tagId)) {
""".trimIndent()
)
for (t in reg.tags) {
appendLine(
"""
case ${serialC(t.identity)}:
return ${nameExpr(t)};
""".trimIndent()
)
}
// openTelemetryNameOf: canonical id -> OTel-namespace name, null when the tag has none. The
// caller (a serializer) owns any fall-back-to-Datadog-name policy; this stays a pure lookup.
appendLine(
"""
default:
return null;
}
int serial = KnownTagCodec.serialNum(tagId);
return serial < NAMES_BY_SERIAL.length ? NAMES_BY_SERIAL[serial] : null;
}

// openTelemetryNameOf: canonical id -> OTel-namespace name, null when the tag has
// none. The caller (a serializer) owns any fall-back-to-Datadog-name policy.
@Override
public String openTelemetryNameOf(long tagId) {
switch (KnownTagCodec.serialNum(tagId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class TagConventions private constructor(
private val spanTypes: Map<String, SpanType>,
private val mixins: Map<String, Mixin>,
private val traceLevel: List<Tag>,
/** Datadog names the tracer intercepts on the set-path, from the tracer overlay. */
val interceptedNames: Set<String> = emptySet(),
) {
/**
* A tag's identity: its Datadog name, plus the direction when that name is declared once per
Expand Down Expand Up @@ -241,6 +243,16 @@ class TagConventions private constructor(
/** The type's own or nearest inherited `span-kind` direction, or null when none is declared. */
private fun directionOf(spanTypes: Map<String, SpanType>, st: SpanType): Direction? = chainOf(spanTypes, st.name).firstNotNullOfOrNull { it.direction }

/** The tracer overlay's `tags`, as a mixin that applies to no span type; empty when none. */
private fun overlayMixin(overlay: Map<String, Any?>): Map<String, Mixin> {
val tags = tagList(overlay["tags"])
if (tags.isEmpty()) return emptyMap()
return mapOf(
OVERLAY_MIXIN to
Mixin(name = OVERLAY_MIXIN, appliesAll = false, appliesTo = emptySet(), tags = tags, refs = emptyList())
)
}

/** Reads `span-kind` as the direction it sets, or null when absent. */
private fun parseDirection(m: Map<String, Any?>, owner: String): Direction? {
val spanKind = m["span-kind"]
Expand All @@ -250,11 +262,31 @@ class TagConventions private constructor(
return (spanKind as String?)?.let { SPAN_KIND_DIRECTIONS.getValue(it) }
}

/** The synthetic mixin holding the tracer overlay's tags; it applies to no span type. */
const val OVERLAY_MIXIN = "tracer overlay"

/**
* Parses [root], the language-agnostic conventions, plus [overlay], this tracer's own set-path
* routing: `tags` declares keys that exist only to be routed (e.g. `resource.name`), so each is
* intercepted, and `intercepted` adds the tags declared in [root] that the tracer also routes.
*/
@Suppress("UNCHECKED_CAST")
fun parse(root: Map<String, Any?>): TagConventions {
fun parse(root: Map<String, Any?>, overlay: Map<String, Any?> = emptyMap()): TagConventions {
for (section in listOf("span_types", "mixins", "trace_level")) {
require(root[section] == null || root[section] is Map<*, *>) { "$section must be a mapping" }
}
require(overlay.keys.all { it == "tags" || it == "intercepted" }) {
"the tracer overlay may only declare `tags` and `intercepted`, not ${overlay.keys - setOf("tags", "intercepted")}"
}
require(refList(overlay["tags"]).isEmpty()) { "tracer overlay tags must be declarations, not refs" }
val interceptedRaw = overlay["intercepted"]
require(interceptedRaw == null || (interceptedRaw is List<*> && interceptedRaw.all { it is String })) {
"intercepted must be a list of Datadog tag names"
}
// An overlay tag exists only to be routed, so it is intercepted without being listed.
val intercepted =
((interceptedRaw as? List<String>) ?: emptyList()).toSet() +
tagList(overlay["tags"]).map { it.ddName }
val spanTypesRaw = (root["span_types"] as? Map<String, Any?>) ?: emptyMap()
val parsedSpanTypes =
spanTypesRaw.mapValues { (name, v) ->
Expand Down Expand Up @@ -298,7 +330,7 @@ class TagConventions private constructor(
refs = refList(m["tags"]),
direction = parseDirection(m, "mixin '$name'"),
)
}
} + overlayMixin(overlay)

for (spanType in parsedSpanTypes.values) {
for (included in spanType.include) {
Expand Down Expand Up @@ -342,7 +374,12 @@ class TagConventions private constructor(
)
}
validateOtelNameScope(spanTypes, mixins, traceLevel)
return TagConventions(spanTypes, mixins, traceLevel)
val conv = TagConventions(spanTypes, mixins, traceLevel, intercepted)
val declared = conv.allDeclaredTags().map { it.ddName }.toSet()
for (name in intercepted) {
require(name in declared) { "intercepted tag '$name' is not declared" }
}
return conv
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ package datadog.buildlogic.tagRegistry
* Assigns tag ids from a parsed [TagConventions]. The id encoding mirrors KnownTagCodec: [63-48
* serial][47-32 reserved][31-0 flags].
*
* <p>An id is IDENTITY only: a globally unique serial plus the trace-level classification bit. It
* <p>An id is a globally unique serial plus two classification bits: trace-level and intercepted. It
* carries no storage-layout coordinate -- bits [47-32] are held vacant for the co-occurrence slot
* that the dense tag store assigns by graph coloring, which lands with the dense store itself.
* Nothing here needs to know how (or whether) a tag is stored.
*
* <p>Nor does anything here know how a tag is SET. Whether the tracer intercepts a tag on the
* set-path (routing it to a span field or a sampling directive instead of tag storage) is a
* property of TagInterceptor, not of the tag's identity, and modelling it was the source of a whole
* class of drift between this registry and the interceptor's actual switch. It arrives with the
* work that consumes it -- the id->handler dispatch table that retires TagInterceptor -- where the
* interceptor can be the authority. Re-adding a classification bit then is purely additive.
* <p>The intercepted bit marks a tag the tracer may route on the set-path (to a span field or a
* sampling directive instead of tag storage), as listed by the tracer overlay. A setter called with
* a constant id then folds the interception test away. TagInterceptor's switch stays the authority
* on what each tag does; a test there keeps the bit and the switch in agreement.
*/
class TagRegistry private constructor(val tags: List<Tag>) {
data class Tag(
Expand All @@ -23,6 +21,7 @@ class TagRegistry private constructor(val tags: List<Tag>) {
val required: String,
val serial: Int,
val traceLevel: Boolean,
val intercepted: Boolean,
val id: Long,
/** The tag's OpenTelemetry name, in [otelDirection] or every direction; null when not renamed. */
val declaredOtelName: String? = null,
Expand All @@ -47,14 +46,19 @@ class TagRegistry private constructor(val tags: List<Tag>) {
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 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED
const val SHARED_NAME = 1L shl 0 // low-32 carve bit 0; mirrors KnownTagCodec.SHARED_NAME

/**
* Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() -- must stay in sync. LEVEL_TRACE at
* bit 2, other low bits and the reserved [47-32] window zero.
* Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() + intercepted() -- must stay in sync.
* LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, SHARED_NAME at bit 0, 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, sharedName: Boolean = false): Long {
var id = serial.toLong() shl 48
if (traceLevel) id = id or LEVEL_TRACE
if (intercepted) id = id or INTERCEPTED
if (sharedName) id = id or SHARED_NAME
return id
}

Expand All @@ -67,13 +71,15 @@ class TagRegistry private constructor(val tags: List<Tag>) {
conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t ->
val serial = FIRST_SERIAL + i
val isTraceLevel = t.identity in traceLevel
val isIntercepted = t.ddName in conv.interceptedNames
Tag(
t.identity,
t.type,
t.required,
serial,
isTraceLevel,
id = encode(serial, isTraceLevel),
isIntercepted,
id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null),
declaredOtelName = renames[t.identity]?.otelName,
otelDirection = renames[t.identity]?.direction,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ abstract class TagRegistryExtension @Inject constructor(
*/
abstract val tagConventionsFile: RegularFileProperty

/**
* Optional tracer overlay: this tracer's set-path routing (intercepted tags, and keys that exist
* only to be routed), kept out of the language-agnostic conventions.
*/
abstract val tracerOverlayFile: RegularFileProperty

/**
* Destination of the generated sources, by convention under `build/generated/tag-registry`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ import java.util.Locale

/** Emits the Java tag registry and reports in a deterministic order. */
object TagRegistryGenerator {
/** Parses the conventions YAML and writes the full generated tree under [outDir]. */
fun generate(tagConventionsFile: File, outDir: File) {
/**
* Parses the conventions YAML, plus the tracer overlay when there is one, and writes the full
* generated tree under [outDir].
*/
fun generate(tagConventionsFile: File, outDir: File, tracerOverlayFile: File? = null) {
val mapper = ObjectMapper(YAMLFactory())
val domain: Map<String, Any?> =
tagConventionsFile.inputStream().use {
mapper.readValue(it, object : TypeReference<Map<String, Any?>>() {})
}
fun read(file: File): Map<String, Any?> =
file.inputStream().use { mapper.readValue(it, object : TypeReference<Map<String, Any?>>() {}) } ?: emptyMap()
val domain = read(tagConventionsFile)
val overlay = tracerOverlayFile?.let(::read) ?: emptyMap()

// Validate before touching the destination tree: an invalid domain model must fail loudly,
// not after the previous (valid) generated output has already been wiped out.
val conv = TagConventions.parse(domain)
val conv = TagConventions.parse(domain, overlay)
val reg = TagRegistry.build(conv)

// Remove obsolete generated files when the output changes.
Expand Down Expand Up @@ -68,18 +71,19 @@ object TagRegistryGenerator {
"""
# Tag id assignment. tags=${reg.tags.size}

# TAGS serial lvl id required name
# TAGS serial lvl int id required name
""".trimIndent()
)
for ((name, _, required, serial, traceLevel, id) in reg.tags) {
for (t in reg.tags) {
appendLine(
" %6d %s %-18s %-12s %s".format(
" %6d %s %s %-18s %-12s %s".format(
Locale.ROOT,
serial,
if (traceLevel) "T" else "-",
"0x%016X".format(Locale.ROOT, id),
required,
name
t.serial,
if (t.traceLevel) "T" else "-",
if (t.intercepted) "I" else "-",
"0x%016X".format(Locale.ROOT, t.id),
t.required,
t.name
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class TagRegistryGeneratorPlugin : Plugin<Project> {
group = "build"
description = "Generates the Java tag registry and assignment reports."
tagConventionsFile.convention(ext.tagConventionsFile)
tracerOverlayFile.convention(ext.tracerOverlayFile)
destinationDirectory.convention(ext.destinationDirectory)
}
project.pluginManager.withPlugin("java") {
Expand Down
Loading
Loading