diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt
index a68d39d6701..5d430f9d7ae 100644
--- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt
+++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt
@@ -83,8 +83,10 @@ object KnownTagsEmitter {
appendLine(" public static final String ${otelNameC(t.ddName)} = \"${escape(t.otelName)}\";")
}
append("// makeTagId(serial=${t.serial})")
+ append(if (t.slot == TagRegistry.NO_SLOT) " + no slot" else " + slot=${t.slot}")
if (t.traceLevel) append(" + trace-level")
if (t.intercepted) append(" + intercepted")
+ if (t.sharesSlot) append(" + shares slot")
if (t.otelName != null) append(" -> ${escape(t.otelName)}")
for ((direction, otel) in t.scopedOtelNames) append(" -> ${escape(otel)} (${direction.yamlKey})")
appendLine(" <${escape(t.required)}>")
@@ -100,6 +102,9 @@ object KnownTagsEmitter {
// 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;")
+ // Slots in use: every slotted tag's slot is below SLOT_COUNT.
+ val slotCount = (reg.tags.filter { it.slot != TagRegistry.NO_SLOT }.maxOfOrNull { it.slot } ?: -1) + 1
+ appendLine(" public static final int SLOT_COUNT = $slotCount;")
// OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all
// canonical names. Sort by OTel name to keep output deterministic.
diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt
index 248471c228a..bd6937f6703 100644
--- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt
+++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt
@@ -2,12 +2,22 @@ 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].
+ * serial][47-32 slot][31-0 flags].
*
- *
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.
+ *
An id is a globally unique serial, a slot, and two classification bits: trace-level and
+ * intercepted.
+ *
+ *
The slot comes from GRAPH COLORING the tag co-occurrence graph. Each concrete span type's
+ * resolved tags appear together on one span, so they form a clique and need distinct slots; the
+ * trace-level tier is its own clique (it lives in a separate TagMap). Tags that never share a clique
+ * may share a slot, so the slot count stays bounded by the largest clique, small enough for one
+ * occupancy `long`. A span type with more tags than slots fits as long as its tags at a
+ * `required`, `conditional` or `recommended` level do: on overflow, its `optional` and `opt_in` tags
+ * share slots with each other. Every tag resolved onto a span type, or trace-level, is slotted; a
+ * tag on no modeled span type (the tracer overlay's routing-only keys, tags of unmodeled span types)
+ * gets [NO_SLOT]. Nothing in the tracer reads slots yet: they are for
+ * per-span structures that want an exact, compact presence word, such as a dense tag store or a
+ * span builder's tag ledger.
*
*
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
@@ -26,11 +36,15 @@ class TagRegistry private constructor(val tags: List) {
val serial: Int,
val traceLevel: Boolean,
val intercepted: Boolean,
+ /** The tag's slot, or [NO_SLOT] for a tag on no modeled span type. */
+ val slot: Int,
val id: Long,
/** The tag's OpenTelemetry name in every direction; null when not renamed, or renamed per direction. */
val otelName: String? = null,
/** The tag's OpenTelemetry name on spans of each direction that renames it, when [otelName] is null. */
val scopedOtelNames: Map = emptyMap(),
+ /** The tag's slot is shared with another optional tag on one of its span types, after an overflow. */
+ val sharesSlot: Boolean = false,
) {
/** The tag as reports and generated constant names show it. */
val name: String
@@ -42,13 +56,17 @@ class TagRegistry private constructor(val tags: List) {
const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED
const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE
+ const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT
+ const val NO_SLOT = 0xFFFF // all-ones slot: no modeled span type; mirrors KnownTagCodec.NO_SLOT
+ const val MAX_SLOTS = 63 // fits one occupancy long; bit 63 is left for unslotted tags to share
+
/**
- * Mirrors KnownTagCodec.makeTagId(serial) + intercepted() + traceLevel() -- must stay in sync.
- * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits and the reserved [47-32] window
- * zero.
+ * Mirrors KnownTagCodec's layout -- must stay in sync: serial at [63-48], slot at [47-32],
+ * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits zero.
*/
- fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long {
- var id = serial.toLong() shl 48
+ fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, slot: Int = NO_SLOT): Long {
+ require(slot == NO_SLOT || slot in 0 until MAX_SLOTS) { "slot $slot is outside [0, $MAX_SLOTS)" }
+ var id = (serial.toLong() shl 48) or (slot.toLong() shl SLOT_SHIFT)
if (traceLevel) id = id or LEVEL_TRACE
if (intercepted) id = id or INTERCEPTED
return id
@@ -57,6 +75,7 @@ class TagRegistry private constructor(val tags: List) {
fun build(conv: TagConventions): TagRegistry {
val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet()
val renames = conv.otelMappings().groupBy { it.tag.ddName }
+ val (slots, sharing) = colorSlots(conv)
// One tag per Datadog name, in a stable order (by name) so serials -- and therefore ids -- are
// a pure function of the input. A name's declarations per direction merge into its one tag.
@@ -70,6 +89,7 @@ class TagRegistry private constructor(val tags: List) {
val isTraceLevel = first.ddName in traceLevel
val isIntercepted = first.ddName in conv.interceptedNames
val (otelName, scoped) = otelNamesOf(first.ddName, renames[first.ddName].orEmpty())
+ val slot = slots[first.ddName] ?: NO_SLOT
Tag(
first.ddName,
first.type,
@@ -77,9 +97,11 @@ class TagRegistry private constructor(val tags: List) {
serial,
isTraceLevel,
isIntercepted,
- id = encode(serial, isTraceLevel, isIntercepted),
+ slot,
+ id = encode(serial, isTraceLevel, isIntercepted, slot),
otelName = otelName,
scopedOtelNames = scoped,
+ sharesSlot = first.ddName in sharing,
)
}
@@ -87,6 +109,71 @@ class TagRegistry private constructor(val tags: List) {
return TagRegistry(tags)
}
+ /** Requirement levels whose tags may share a slot with each other when a span type overflows. */
+ private val OCCASIONAL = setOf("optional", "opt_in")
+
+ /**
+ * Colors the co-occurrence graph of slotted tags: tags resolved onto one concrete span type, or
+ * both trace-level, get distinct slots while the slots last. Tags that are `required`,
+ * `conditional` or `recommended` on some span type are colored first, most-constrained first (by
+ * clique count, then name), so they take the low slots and always get exact ones. Only on a span
+ * type with more tags than slots may two tags that are both `optional` or `opt_in` there share a
+ * slot, and only once the free slots run out, on the slot the fewest of them already hold; a span
+ * type that fits never has two tags on one slot. Returns the slots, and
+ * the tags that share one. The result is a pure function of the conventions.
+ */
+ private fun colorSlots(conv: TagConventions): Pair