Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
f08c1c3
Add datadog.apicontract to CODEOWNERS
dougqh Oct 13, 2026
f796149
Merge branch 'master' into dougqh/id-tag-interceptor
dougqh Oct 13, 2026
22d0fd1
Give peer.port one direction-neutral id
dougqh Oct 13, 2026
ba715de
Name peer.port by the span's kind in OTLP export
dougqh Oct 13, 2026
584310c
Let C2 inline the constructors of TagMap's nested classes
dougqh Oct 11, 2026
3842b16
Remove TagMap's remaining synthetic accessors and guard against them
dougqh Oct 11, 2026
64d6cf3
Merge master into id-tag-interceptor
dougqh Oct 14, 2026
bb0bbb0
Cover TagMap.Internals, KnownTagCodec and EntryReader paths
dougqh Oct 14, 2026
fb6410e
Format the tag-registry generator to its spotless rules
dougqh Oct 14, 2026
97a508b
Order the tag-id carve bits by position
dougqh Oct 14, 2026
fa54aca
Merge master into id-tag-interceptor
dougqh Oct 14, 2026
1c88d5e
Re-apply split-by-tags language by its id
dougqh Oct 14, 2026
5ba0a09
Say what a tag id's intercepted bit does and doesn't decide
dougqh Oct 14, 2026
0faed2d
Color known tags into co-occurrence slots in their ids
dougqh Oct 15, 2026
683d11d
Keep TagMap.Ledger in parallel arrays keyed by tag id
dougqh Oct 15, 2026
8610d72
Allocate the ledger's names array only for custom tags
dougqh Oct 15, 2026
3a309ee
Color known tags into co-occurrence slots in their ids
dougqh Oct 15, 2026
b78f5bb
Treat a custom tag's zero id as unslotted
dougqh Oct 15, 2026
06bac77
Say a slot bit is only a may-be-present signal
dougqh Oct 15, 2026
22b39e4
Merge the rebased tag-slot-coloring (#12882) into ledger-ids
dougqh Oct 15, 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
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ 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.otelName != null) append(" -> ${escape(t.otelName)}")
Expand All @@ -100,6 +101,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ 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].
*
* <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>An id is a globally unique serial, a slot, and two classification bits: trace-level and
* intercepted.
*
* <p>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`. Every tag resolved onto a span type, or trace-level, is slotted whatever its
* requirement level; 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.
*
* <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
Expand All @@ -26,6 +34,8 @@ class TagRegistry private constructor(val tags: List<Tag>) {
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,
Expand All @@ -42,13 +52,17 @@ class TagRegistry private constructor(val tags: List<Tag>) {
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
Expand All @@ -57,6 +71,7 @@ class TagRegistry private constructor(val tags: List<Tag>) {
fun build(conv: TagConventions): TagRegistry {
val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet()
val renames = conv.otelMappings().groupBy { it.tag.ddName }
val slots = 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.
Expand All @@ -70,14 +85,16 @@ class TagRegistry private constructor(val tags: List<Tag>) {
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,
strongestRequirement(decls.map { it.required }),
serial,
isTraceLevel,
isIntercepted,
id = encode(serial, isTraceLevel, isIntercepted),
slot,
id = encode(serial, isTraceLevel, isIntercepted, slot),
otelName = otelName,
scopedOtelNames = scoped,
)
Expand All @@ -87,6 +104,41 @@ class TagRegistry private constructor(val tags: List<Tag>) {
return TagRegistry(tags)
}

/**
* Colors the co-occurrence graph of slotted tags: tags resolved onto one concrete span type, or
* both trace-level, must get distinct slots. Greedy, most-constrained first (by clique count,
* then name), so the result is a pure function of the conventions. A name declared per direction
* is one tag, so it is one vertex.
*/
private fun colorSlots(conv: TagConventions): Map<String, Int> {
val cliques = ArrayList<Set<String>>()
for (type in conv.concreteTypes()) {
cliques.add(conv.resolve(type).map { it.ddName }.toSet())
}
cliques.add(conv.traceLevelTags().map { it.ddName }.toSet())

val neighbors = HashMap<String, MutableSet<String>>()
for (clique in cliques) {
for (a in clique) {
val adjacent = neighbors.getOrPut(a) { HashSet() }
for (b in clique) if (a != b) adjacent.add(b)
}
}
val cliqueCount = neighbors.keys.associateWith { tag -> cliques.count { tag in it } }
val order = neighbors.keys.sortedWith(compareByDescending<String> { cliqueCount.getValue(it) }.thenBy { it })
val slots = HashMap<String, Int>()
for (tag in order) {
val taken = neighbors.getValue(tag).mapNotNull { slots[it] }.toSet()
var slot = 0
while (slot in taken) slot++
require(slot < MAX_SLOTS) {
"coloring needs more than $MAX_SLOTS slots at '$tag'; one occupancy long holds $MAX_SLOTS"
}
slots[tag] = slot
}
return slots
}

/**
* A tag's renames, as one name for every direction or one name per direction. The rename of a
* declaration made per direction applies in that declaration's direction.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,17 @@ object TagRegistryGenerator {
"""
# Tag id assignment. tags=${reg.tags.size}

# TAGS serial lvl int id required name
# TAGS serial lvl int slot id required name
""".trimIndent()
)
for (t in reg.tags) {
appendLine(
" %6d %s %s %-18s %-12s %s".format(
" %6d %s %s %4s %-18s %-12s %s".format(
Locale.ROOT,
t.serial,
if (t.traceLevel) "T" else "-",
if (t.intercepted) "I" else "-",
if (t.slot == TagRegistry.NO_SLOT) "-" else t.slot.toString(),
"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 @@ -535,9 +535,9 @@ class TagRegistryGeneratorTest {
val knownTags = contents(output).getValue("java/datadog/trace/api/KnownTags.java")
assertThat(knownTags)
.contains(
"COMPONENT_ID = 0x0001000000000000L",
"RESOURCE_NAME_ID = 0x0002000000000002L",
"SERVICE_ID = 0x0003000000000002L",
"COMPONENT_ID = 0x0001FFFF00000000L",
"RESOURCE_NAME_ID = 0x0002FFFF00000002L",
"SERVICE_ID = 0x0003FFFF00000002L",
"public static final int SERVICE_SERIAL_NUM = 3;",
"public static final int SERIAL_LIMIT = 4;"
)
Expand All @@ -561,6 +561,44 @@ class TagRegistryGeneratorTest {
.withMessageContaining(message)
}

@Test
fun `tags on one span type get distinct slots and tags that never co-occur share one`() {
val yaml = directory.conventionsFile(
"""
trace_level:
tags: [{dd-name: env, required: required}]
span_types:
base:
abstract: true
tags: [{dd-name: service, required: required}]
http.server:
extends: base
span-kind: server
tags: [{dd-name: http.route, required: required}]
db.client:
extends: base
span-kind: client
tags: [{dd-name: db.type, required: required}, {dd-name: db.user}]
"""
)
val output = File(directory, "generated")

TagRegistryGenerator.generate(yaml, output)

// service is on both span types, so it takes slot 0; db.type and db.user share db.client, so
// they take distinct slots, and http.route -- never on a span type with either -- reuses one of
// them; env is trace-level (its own TagMap) and reuses slot 0. Every level is slotted.
assertThat(contents(output).getValue("java/datadog/trace/api/KnownTags.java"))
.contains(
"DB_TYPE_ID = 0x0001000100000000L",
"DB_USER_ID = 0x0002000200000000L",
"ENV_ID = 0x0003000000000004L",
"HTTP_ROUTE_ID = 0x0004000100000000L",
"SERVICE_ID = 0x0005000000000000L",
"public static final int SLOT_COUNT = 3;"
)
}

private fun tagConventions(@Language("yaml") yamlText: String) = TagConventions.parse(
ObjectMapper(YAMLFactory()).readValue(
directory.conventionsFile(yamlText),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static java.util.concurrent.TimeUnit.MICROSECONDS;

import datadog.trace.api.KnownTags;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import datadog.trace.bootstrap.instrumentation.api.Tags;
Expand Down Expand Up @@ -222,6 +223,26 @@ public void webServerSpanViaBuilder() {
span.finish();
}

/**
* {@link #webServerSpanViaBuilder}, setting each builder tag by its {@link KnownTags} id: no name
* lookup when the tag is recorded, and the ledger stores the id directly.
*/
@Benchmark
public void webServerSpanViaBuilderById() {
AgentSpan span =
tracer
.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME)
.withTag(KnownTags.COMPONENT_ID, COMPONENT_VALUE)
.withTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_SERVER)
.withTag(KnownTags.HTTP_METHOD_ID, HTTP_METHOD_VALUE)
.withTag(KnownTags.HTTP_ROUTE_ID, HTTP_ROUTE_VALUE)
.withTag(KnownTags.HTTP_URL_ID, HTTP_URL_VALUE)
.withTag(KnownTags.HTTP_STATUS_CODE_ID, HTTP_STATUS_VALUE)
.withTag(KnownTags.PEER_PORT_ID, PEER_PORT_VALUE)
.start();
span.finish();
}

/** JDBC/DB-client-shaped span: create -> set the typical DB known tags (9) -> finish. */
@Benchmark
public void jdbcClientSpan() {
Expand Down
19 changes: 19 additions & 0 deletions dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import datadog.trace.api.EndpointTracker;
import datadog.trace.api.IdGenerationStrategy;
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.api.KnownTagCodec;
import datadog.trace.api.Pair;
import datadog.trace.api.TagMap;
import datadog.trace.api.TraceConfig;
Expand Down Expand Up @@ -2075,6 +2076,24 @@ public final CoreSpanBuilder withTag(final String tag, final Object value) {
return this;
}

@Override
public final CoreSpanBuilder withTag(final long tagId, final Object value) {
if (!KnownTagCodec.isKnown(tagId)) {
return this;
}
TagMap.Ledger tagLedger = this.tagLedger;
if (tagLedger == null) {
this.tagLedger = tagLedger = TagMap.ledger();
}
if (value == null) {
// see withTag(String, Object): only a removal of a tag this builder set is recorded
tagLedger.smartRemove(tagId);
} else {
tagLedger.set(tagId, value);
}
return this;
}

@Override
public final <T> AgentTracer.SpanBuilder withRequestContextData(
RequestContextSlot slot, T data) {
Expand Down
47 changes: 33 additions & 14 deletions dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -1322,22 +1322,41 @@ void setAllTags(final TagMap.Ledger ledger) {
}

synchronized (unsafeTags) {
for (final TagMap.EntryChange entryChange : ledger) {
String tag = entryChange.tag();
if (entryChange.isRemoval()) {
if (tagEquals(tag, Tags.SPAN_KIND)) {
// mirror removeTag(String): keep the cached ordinal in sync with unsafeTags
spanKindOrdinal = SPAN_KIND_UNSET;
}
unsafeTags.remove(tag);
} else {
TagMap.Entry entry = (TagMap.Entry) entryChange;
// Interception needs an intercepted id in the ledger, or a split-by-tags entry, which is
// config rather than an id bit. Without either, every change goes straight to storage.
if (ledger.hasInterceptedTags() || tagInterceptor.hasSplitServiceTags()) {
ledger.forEach(this, DDSpanContext::interceptOrApplyLedgerChange);
} else {
ledger.forEach(this, DDSpanContext::applyLedgerChange);
}
}
}

if (!tagInterceptor.interceptTag(this, entry)) {
unsafeTags.set(entry);
}
}
/** Call while holding the {@code unsafeTags} lock. */
private static void interceptOrApplyLedgerChange(
DDSpanContext ctx, TagMap.EntryChangeReader change) {
if (change.isRemoval() || !ctx.tagInterceptor.interceptTag(ctx, change)) {
applyLedgerChange(ctx, change);
}
}

/** Call while holding the {@code unsafeTags} lock. */
private static void applyLedgerChange(DDSpanContext ctx, TagMap.EntryChangeReader change) {
long tagId = change.tagId();
if (change.isRemoval()) {
if (tagId == KnownTags.SPAN_KIND_ID) {
// mirror removeTag(String): keep the cached ordinal in sync with unsafeTags
ctx.spanKindOrdinal = SPAN_KIND_UNSET;
}
if (tagId != 0) {
ctx.unsafeTags.getAndRemove(tagId);
} else {
ctx.unsafeTags.remove(change.tag());
}
} else if (tagId != 0) {
ctx.unsafeTags.internals().setKnown(tagId, change.objectValue());
} else {
ctx.unsafeTags.set(change);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public class TagInterceptor {
/** split-by-tags entries naming custom tags; null when there are none. */
private final Set<String> splitServiceCustomNames;

/** Whether any split-by-tags entry is configured; see {@link #hasSplitServiceTags}. */
private final boolean hasSplitServiceTags;

private final boolean shouldSet404ResourceName;
private final boolean shouldSetUrlResourceAsName;
private final boolean jeeSplitByDeployment;
Expand All @@ -84,6 +87,7 @@ public TagInterceptor(
this.inferredServiceName = inferredServiceName;
this.splitServiceSerials = knownTagSerials(splitServiceTags);
this.splitServiceCustomNames = customTagNames(splitServiceTags);
this.hasSplitServiceTags = !splitServiceTags.isEmpty();
this.ruleFlags = ruleFlags;
splitByServletContext = isSplitServiceTag(SERVLET_CONTEXT_ID);

Expand Down Expand Up @@ -129,6 +133,15 @@ private static Set<String> customTagNames(Set<String> names) {
return custom;
}

/**
* Whether any split-by-tags entry is configured. split-by-tags is tracer config, not an id bit,
* so a batch of tags needs no interception only when this is false and no id in the batch has
* {@link KnownTagCodec#INTERCEPTED}.
*/
public boolean hasSplitServiceTags() {
return hasSplitServiceTags;
}

/** Requires a known id, whose serial is always in range of the table. */
private boolean isSplitServiceTag(long tagId) {
return splitServiceSerials[KnownTagCodec.serialNum(tagId)];
Expand Down
Loading
Loading