Skip to content

Commit 93263f1

Browse files
dougqhclaude
andcommitted
Wire tag-id setTag into DDSpanContext (slice A): stored + intercepted paths
Connect the tag-id fast path into the span layer: - CoreTagIds: hand-assigned tag-id constants for core tags + a KnownTags.Resolver that registers on class init (so id resolution is live before the first span). PARENT_ID is a stored tag (serial >= FIRST_STORED_SERIAL); ERROR is a reserved virtual tag (serial < FIRST_STORED_SERIAL, fieldPos sentinel so it never slots). - DDSpanContext.setTag(long, Object): O(1) range-check routing — reserved tags go to the interceptor via id dispatch, stored tags go straight to the map by id (slot/bucket), bypassing the per-tag interceptor string switch. - TagInterceptor.interceptTag(span, long, value): int-switch on globalSerial (ERROR), falling back to the string path by resolved name for other reserved ids. - Migrate the constructor's PARENT_ID set to the id; drop the now-unused import. Tests: PARENT_ID set-by-id is findable/serialized as _dd.parent_id; ERROR set-by-id sets the error flag and is not stored. Existing DDSpanContext/serialization/tracer/ interceptor suites pass with the resolver now registered tracer-wide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c42402d commit 93263f1

4 files changed

Lines changed: 140 additions & 2 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package datadog.trace.core;
2+
3+
import datadog.trace.api.DDTags;
4+
import datadog.trace.api.KnownTags;
5+
import datadog.trace.bootstrap.instrumentation.api.Tags;
6+
7+
/**
8+
* Hand-assigned tag-id constants for tracer-core tags, plus the {@link KnownTags.Resolver} that
9+
* resolves them.
10+
*
11+
* <p>Reserved serials {@code [1, KnownTags.FIRST_STORED_SERIAL)} name "virtual" tags handled by the
12+
* tag interceptor / span fields and are NOT stored in the {@code TagMap}; their {@code fieldPos} is
13+
* a sentinel ({@link #RESERVED_FIELD_POS}) that is out of slot range, so any incidental store
14+
* routes to the hash buckets rather than a positional slot. Serials {@code >= FIRST_STORED_SERIAL}
15+
* name stored tags that slot/bucket normally.
16+
*
17+
* <p>The resolver registers on class initialization, so simply referencing any constant here makes
18+
* tag-id resolution live before the first span is built.
19+
*/
20+
public final class CoreTagIds {
21+
// sentinel fieldPos for reserved (non-stored) tags: >= TagMap KNOWN_ENTRIES_CAPACITY, so set()
22+
// can never place them in a positional slot
23+
static final int RESERVED_FIELD_POS = 0xFFFF;
24+
25+
// ---- reserved / virtual (tag-interceptor handled, not stored) ----
26+
public static final int ERROR_SERIAL = 1;
27+
public static final long ERROR = KnownTags.tagId(ERROR_SERIAL, RESERVED_FIELD_POS, Tags.ERROR);
28+
29+
// ---- stored (slotted / bucketed) ----
30+
public static final int PARENT_ID_SERIAL = KnownTags.FIRST_STORED_SERIAL;
31+
public static final long PARENT_ID = KnownTags.tagId(PARENT_ID_SERIAL, 0, DDTags.PARENT_ID);
32+
33+
static final KnownTags.Resolver RESOLVER =
34+
new KnownTags.Resolver() {
35+
@Override
36+
public String nameOf(long tagId) {
37+
switch (KnownTags.globalSerial(tagId)) {
38+
case ERROR_SERIAL:
39+
return Tags.ERROR;
40+
case PARENT_ID_SERIAL:
41+
return DDTags.PARENT_ID;
42+
default:
43+
return null;
44+
}
45+
}
46+
47+
@Override
48+
public long keyOf(String name) {
49+
switch (name) {
50+
case Tags.ERROR:
51+
return ERROR;
52+
case DDTags.PARENT_ID:
53+
return PARENT_ID;
54+
default:
55+
return 0L;
56+
}
57+
}
58+
};
59+
60+
static {
61+
KnownTags.register(RESOLVER);
62+
}
63+
64+
private CoreTagIds() {}
65+
}

dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package datadog.trace.core;
22

3-
import static datadog.trace.api.DDTags.PARENT_ID;
43
import static datadog.trace.api.DDTags.SPAN_LINKS;
54
import static datadog.trace.api.cache.RadixTreeCache.HTTP_STATUSES;
65
import static datadog.trace.bootstrap.instrumentation.api.ErrorPriorities.UNSET;
@@ -11,6 +10,7 @@
1110
import datadog.trace.api.DDTags;
1211
import datadog.trace.api.DDTraceId;
1312
import datadog.trace.api.Functions;
13+
import datadog.trace.api.KnownTags;
1414
import datadog.trace.api.ProcessTags;
1515
import datadog.trace.api.TagMap;
1616
import datadog.trace.api.cache.DDCache;
@@ -385,7 +385,7 @@ public DDSpanContext(
385385
if (samplingPriority != PrioritySampling.UNSET) {
386386
setSamplingPriority(samplingPriority, SamplingMechanism.UNKNOWN);
387387
}
388-
setTag(PARENT_ID, this.propagationTags.getLastParentId());
388+
setTag(CoreTagIds.PARENT_ID, this.propagationTags.getLastParentId());
389389
}
390390

391391
@Override
@@ -901,6 +901,33 @@ public void setTag(final String tag, final String value) {
901901
}
902902
}
903903

904+
/**
905+
* Sets a tag by its generated tag id. Reserved "virtual" tags (interceptor-handled, not stored)
906+
* are routed to the interceptor via an id dispatch; stored tags go straight to the map (slot or
907+
* bucket) keyed by id, bypassing the per-tag interceptor string switch. The id classification is
908+
* a single range check (see {@link KnownTags#isReserved}).
909+
*/
910+
public void setTag(final long tagId, final Object value) {
911+
if (null == value) {
912+
String name = KnownTags.nameOf(tagId);
913+
if (name != null) {
914+
removeTag(name);
915+
}
916+
return;
917+
}
918+
if (KnownTags.isReserved(tagId)) {
919+
if (!tagInterceptor.interceptTag(this, tagId, value)) {
920+
synchronized (unsafeTags) {
921+
unsafeTags.set(tagId, value);
922+
}
923+
}
924+
} else {
925+
synchronized (unsafeTags) {
926+
unsafeTags.set(tagId, value);
927+
}
928+
}
929+
}
930+
904931
public void setTag(TagMap.EntryReader entry) {
905932
if (entry == null) {
906933
return;

dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import datadog.trace.api.Config;
2525
import datadog.trace.api.ConfigDefaults;
2626
import datadog.trace.api.DDTags;
27+
import datadog.trace.api.KnownTags;
2728
import datadog.trace.api.Pair;
2829
import datadog.trace.api.TagMap;
2930
import datadog.trace.api.config.GeneralConfig;
@@ -36,6 +37,7 @@
3637
import datadog.trace.bootstrap.instrumentation.api.Tags;
3738
import datadog.trace.bootstrap.instrumentation.api.URIUtils;
3839
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
40+
import datadog.trace.core.CoreTagIds;
3941
import datadog.trace.core.DDSpanContext;
4042
import java.net.URI;
4143
import java.util.Map;
@@ -131,6 +133,22 @@ public boolean needsIntercept(String tag) {
131133
}
132134
}
133135

136+
/**
137+
* Id-dispatched variant of {@link #interceptTag(DDSpanContext, String, Object)}: switches on the
138+
* tagId's globalSerial (an int) instead of the tag-name string. Used by {@code
139+
* DDSpanContext.setTag(long, Object)} for reserved (virtual) tags. Falls back to the string path
140+
* for any reserved id without a dedicated case.
141+
*/
142+
public boolean interceptTag(DDSpanContext span, long tagId, Object value) {
143+
switch (KnownTags.globalSerial(tagId)) {
144+
case CoreTagIds.ERROR_SERIAL:
145+
return interceptError(span, value);
146+
default:
147+
String name = KnownTags.nameOf(tagId);
148+
return name != null && interceptTag(span, name, value);
149+
}
150+
}
151+
134152
public boolean interceptTag(DDSpanContext span, String tag, Object value) {
135153
switch (tag) {
136154
case DDTags.RESOURCE_NAME:

dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import static org.junit.jupiter.api.Assertions.assertFalse;
2525
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2626
import static org.junit.jupiter.api.Assertions.assertNull;
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
2728
import static org.mockito.Mockito.mock;
2829
import static org.mockito.Mockito.times;
2930
import static org.mockito.Mockito.verify;
@@ -70,6 +71,33 @@ void setup() {
7071
.build();
7172
}
7273

74+
@Test
75+
void setTagById_storedTagResolvesByName() {
76+
AgentSpan span = tracer.buildSpan("datadog", "fakeOperation").start();
77+
DDSpanContext context = (DDSpanContext) span.context();
78+
79+
// PARENT_ID is a stored tag (serial >= FIRST_STORED_SERIAL): set by id, it lands in the map and
80+
// is findable / serialized by its resolved name.
81+
context.setTag(CoreTagIds.PARENT_ID, "p123");
82+
assertEquals("p123", context.getTags().get(DDTags.PARENT_ID));
83+
84+
span.finish();
85+
}
86+
87+
@Test
88+
void setTagById_reservedTagIsIntercepted() {
89+
AgentSpan span = tracer.buildSpan("datadog", "fakeOperation").start();
90+
DDSpanContext context = (DDSpanContext) span.context();
91+
92+
// ERROR is a reserved (virtual) tag: setting it by id dispatches through the interceptor
93+
// (id-keyed), which sets the error flag and does NOT store an "error" tag.
94+
context.setTag(CoreTagIds.ERROR, true);
95+
assertTrue(context.getErrorFlag());
96+
assertNull(context.getTags().get(Tags.ERROR));
97+
98+
span.finish();
99+
}
100+
73101
@ParameterizedTest
74102
@ValueSource(strings = {DDTags.SERVICE_NAME, DDTags.RESOURCE_NAME, DDTags.SPAN_TYPE, "some.tag"})
75103
void nullValuesForTagsDeleteExistingTags(String name) throws Exception {

0 commit comments

Comments
 (0)