Skip to content

Commit 20ebde7

Browse files
[mq] [skip ddci] working branch - merge b957f87 on top of master at 3fb3733
{"baseBranch":"master","baseCommit":"3fb37337abcd415ee65d91b50cab3710a582f77d","createdAt":"2026-04-16T20:58:00.220368Z","headSha":"b957f8718cf84bf38df2354bd48986ad9ea1b4a0","id":"9062695f-0d96-4e28-9277-37e217bc7a72","priority":"200","pullRequestNumber":"11116","queuedAt":"2026-04-16T20:58:00.219548Z","status":"STATUS_QUEUED"}
2 parents 73b4795 + b957f87 commit 20ebde7

5 files changed

Lines changed: 271 additions & 10 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
4+
5+
import datadog.trace.bootstrap.instrumentation.api.Tags;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Fork;
9+
import org.openjdk.jmh.annotations.Measurement;
10+
import org.openjdk.jmh.annotations.Mode;
11+
import org.openjdk.jmh.annotations.OutputTimeUnit;
12+
import org.openjdk.jmh.annotations.Scope;
13+
import org.openjdk.jmh.annotations.Setup;
14+
import org.openjdk.jmh.annotations.State;
15+
import org.openjdk.jmh.annotations.Warmup;
16+
17+
/**
18+
* Measures the cost of DDSpan.isOutbound(), which is called on every root span start and finish.
19+
*/
20+
@State(Scope.Thread)
21+
@Warmup(iterations = 3, time = 1)
22+
@Measurement(iterations = 5, time = 1)
23+
@BenchmarkMode(Mode.AverageTime)
24+
@OutputTimeUnit(NANOSECONDS)
25+
@Fork(value = 1)
26+
public class IsOutboundBenchmark {
27+
28+
static final CoreTracer TRACER = CoreTracer.builder().build();
29+
30+
private DDSpan clientSpan;
31+
private DDSpan serverSpan;
32+
private DDSpan unsetSpan;
33+
34+
@Setup
35+
public void setup() {
36+
clientSpan = (DDSpan) TRACER.startSpan("benchmark", "client.op");
37+
clientSpan.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);
38+
39+
serverSpan = (DDSpan) TRACER.startSpan("benchmark", "server.op");
40+
serverSpan.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
41+
42+
unsetSpan = (DDSpan) TRACER.startSpan("benchmark", "unset.op");
43+
}
44+
45+
@Benchmark
46+
public boolean isOutbound_client() {
47+
return clientSpan.isOutbound();
48+
}
49+
50+
@Benchmark
51+
public boolean isOutbound_server() {
52+
return serverSpan.isOutbound();
53+
}
54+
55+
@Benchmark
56+
public boolean isOutbound_unset() {
57+
return unsetSpan.isOutbound();
58+
}
59+
60+
@Benchmark
61+
public Object getTag_spanKind_client() {
62+
return clientSpan.getTag(Tags.SPAN_KIND);
63+
}
64+
65+
@Benchmark
66+
public Object getTag_spanKind_unset() {
67+
return unsetSpan.getTag(Tags.SPAN_KIND);
68+
}
69+
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities;
3131
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
3232
import datadog.trace.bootstrap.instrumentation.api.SpanWrapper;
33-
import datadog.trace.bootstrap.instrumentation.api.Tags;
3433
import datadog.trace.core.util.StackTraces;
3534
import edu.umd.cs.findbugs.annotations.NonNull;
3635
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -943,8 +942,8 @@ public DDSpan setMetaStruct(final String field, final Object value) {
943942

944943
@Override
945944
public boolean isOutbound() {
946-
Object spanKind = context.getTag(Tags.SPAN_KIND);
947-
return Tags.SPAN_KIND_CLIENT.equals(spanKind) || Tags.SPAN_KIND_PRODUCER.equals(spanKind);
945+
byte ordinal = context.getSpanKindOrdinal();
946+
return ordinal == DDSpanContext.SPAN_KIND_CLIENT || ordinal == DDSpanContext.SPAN_KIND_PRODUCER;
948947
}
949948

950949
@Override

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

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,32 @@ public class DDSpanContext
9797
private final UTF8BytesString threadName;
9898

9999
private volatile short httpStatusCode;
100+
101+
// Cached span.kind ordinal for fast isOutbound() checks.
102+
// Ordinal constants -- keep in sync with SPAN_KIND_VALUES array.
103+
static final byte SPAN_KIND_UNSET = 0;
104+
static final byte SPAN_KIND_SERVER = 1;
105+
static final byte SPAN_KIND_CLIENT = 2;
106+
static final byte SPAN_KIND_PRODUCER = 3;
107+
static final byte SPAN_KIND_CONSUMER = 4;
108+
static final byte SPAN_KIND_INTERNAL = 5;
109+
static final byte SPAN_KIND_BROKER = 6;
110+
static final byte SPAN_KIND_CUSTOM = 7;
111+
112+
/** Maps ordinal to canonical string constant. Index 0 (UNSET) and 7 (CUSTOM) are null. */
113+
static final String[] SPAN_KIND_VALUES = {
114+
null, // UNSET
115+
Tags.SPAN_KIND_SERVER,
116+
Tags.SPAN_KIND_CLIENT,
117+
Tags.SPAN_KIND_PRODUCER,
118+
Tags.SPAN_KIND_CONSUMER,
119+
Tags.SPAN_KIND_INTERNAL,
120+
Tags.SPAN_KIND_BROKER,
121+
null // CUSTOM
122+
};
123+
124+
private volatile byte spanKindOrdinal = SPAN_KIND_UNSET;
125+
100126
private CharSequence integrationName;
101127
private CharSequence serviceNameSource;
102128

@@ -716,6 +742,51 @@ public short getHttpStatusCode() {
716742
return httpStatusCode;
717743
}
718744

745+
/** Identity-first string comparison: checks reference equality, then falls back to equals. */
746+
static boolean tagEquals(String tagValue, String tagLiteral) {
747+
return (tagValue == tagLiteral) || tagLiteral.equals(tagValue);
748+
}
749+
750+
/**
751+
* Cache the span.kind ordinal for fast isOutbound() checks. Called from TagInterceptor when
752+
* span.kind is set.
753+
*/
754+
public void setSpanKindOrdinal(String kind) {
755+
if (kind == null) {
756+
spanKindOrdinal = SPAN_KIND_UNSET;
757+
} else if (tagEquals(kind, Tags.SPAN_KIND_SERVER)) {
758+
spanKindOrdinal = SPAN_KIND_SERVER;
759+
} else if (tagEquals(kind, Tags.SPAN_KIND_CLIENT)) {
760+
spanKindOrdinal = SPAN_KIND_CLIENT;
761+
} else if (tagEquals(kind, Tags.SPAN_KIND_PRODUCER)) {
762+
spanKindOrdinal = SPAN_KIND_PRODUCER;
763+
} else if (tagEquals(kind, Tags.SPAN_KIND_CONSUMER)) {
764+
spanKindOrdinal = SPAN_KIND_CONSUMER;
765+
} else if (tagEquals(kind, Tags.SPAN_KIND_INTERNAL)) {
766+
spanKindOrdinal = SPAN_KIND_INTERNAL;
767+
} else if (tagEquals(kind, Tags.SPAN_KIND_BROKER)) {
768+
spanKindOrdinal = SPAN_KIND_BROKER;
769+
} else {
770+
spanKindOrdinal = SPAN_KIND_CUSTOM;
771+
}
772+
}
773+
774+
byte getSpanKindOrdinal() {
775+
return spanKindOrdinal;
776+
}
777+
778+
/** Returns the span.kind string from the cached ordinal, or falls back to the tag map. */
779+
public String getSpanKindString() {
780+
byte ordinal = spanKindOrdinal;
781+
if (ordinal > SPAN_KIND_UNSET && ordinal < SPAN_KIND_CUSTOM) {
782+
return SPAN_KIND_VALUES[ordinal];
783+
}
784+
// UNSET or CUSTOM -- fall through to tag map
785+
synchronized (unsafeTags) {
786+
return unsafeTags.getString(Tags.SPAN_KIND);
787+
}
788+
}
789+
719790
public void setOrigin(final CharSequence origin) {
720791
DDSpanContext context = getRootSpanContextOrThis();
721792
context.origin = origin;
@@ -763,6 +834,10 @@ public void setMetric(final TagMap.EntryReader entry) {
763834
}
764835

765836
public void removeTag(String tag) {
837+
if (tagEquals(tag, Tags.SPAN_KIND)) {
838+
// Clear the cached ordinal; unsafeTags still needs to be updated below.
839+
spanKindOrdinal = SPAN_KIND_UNSET;
840+
}
766841
synchronized (unsafeTags) {
767842
unsafeTags.remove(tag);
768843
}
@@ -782,9 +857,7 @@ public void setTag(final String tag, final Object value) {
782857
return;
783858
}
784859
if (null == value) {
785-
synchronized (unsafeTags) {
786-
unsafeTags.remove(tag);
787-
}
860+
removeTag(tag);
788861
} else if (!tagInterceptor.interceptTag(this, tag, value)) {
789862
synchronized (unsafeTags) {
790863
unsafeTags.set(tag, value);
@@ -797,9 +870,7 @@ public void setTag(final String tag, final String value) {
797870
return;
798871
}
799872
if (null == value) {
800-
synchronized (unsafeTags) {
801-
unsafeTags.remove(tag);
802-
}
873+
removeTag(tag);
803874
} else if (!tagInterceptor.interceptTag(this, tag, value)) {
804875
synchronized (unsafeTags) {
805876
unsafeTags.set(tag, value);
@@ -1015,6 +1086,8 @@ Object getTag(final String key) {
10151086
return threadName.toString();
10161087
case Tags.HTTP_STATUS:
10171088
return 0 == httpStatusCode ? null : (int) httpStatusCode;
1089+
case Tags.SPAN_KIND:
1090+
return getSpanKindString();
10181091
default:
10191092
Object value;
10201093
synchronized (unsafeTags) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public boolean needsIntercept(String tag) {
123123
case HTTP_URL:
124124
case ORIGIN_KEY:
125125
case MEASURED:
126+
case Tags.SPAN_KIND:
126127
return true;
127128

128129
default:
@@ -193,6 +194,11 @@ public boolean interceptTag(DDSpanContext span, String tag, Object value) {
193194
return interceptOrigin(span, value);
194195
case MEASURED:
195196
return interceptMeasured(span, value);
197+
case Tags.SPAN_KIND:
198+
// Cache the ordinal for fast isOutbound() checks.
199+
// Return false so the value is still stored in unsafeTags for serialization.
200+
span.setSpanKindOrdinal(String.valueOf(value));
201+
return false;
196202
default:
197203
return intercept(span, tag, value);
198204
}
@@ -223,7 +229,7 @@ private static void setResourceFromUrl(
223229
path = uri == null ? null : uri.getPath();
224230
}
225231
if (path != null) {
226-
final boolean isClient = Tags.SPAN_KIND_CLIENT.equals(span.unsafeGetTag(Tags.SPAN_KIND));
232+
final boolean isClient = Tags.SPAN_KIND_CLIENT.equals(span.getSpanKindString());
227233
Pair<CharSequence, Byte> normalized =
228234
isClient
229235
? HttpResourceNames.computeForClient(method, path, false)

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext
66
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities
77
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration
88
import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources
9+
import datadog.trace.bootstrap.instrumentation.api.Tags
910
import datadog.trace.common.writer.ListWriter
1011
import datadog.trace.core.propagation.ExtractedContext
1112
import datadog.trace.core.test.DDCoreSpecification
@@ -352,4 +353,117 @@ class DDSpanContextTest extends DDCoreSpecification {
352353
}
353354
assert sourceWithoutCommonTags == comparison
354355
}
356+
357+
def "span kind ordinal constants and SPAN_KIND_VALUES array stay in sync"() {
358+
expect: "SPAN_KIND_VALUES array covers all ordinals"
359+
DDSpanContext.SPAN_KIND_VALUES.length == DDSpanContext.SPAN_KIND_CUSTOM + 1
360+
361+
and: "each known ordinal maps to the correct Tags constant"
362+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_SERVER] == Tags.SPAN_KIND_SERVER
363+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_CLIENT] == Tags.SPAN_KIND_CLIENT
364+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_PRODUCER] == Tags.SPAN_KIND_PRODUCER
365+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_CONSUMER] == Tags.SPAN_KIND_CONSUMER
366+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_INTERNAL] == Tags.SPAN_KIND_INTERNAL
367+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_BROKER] == Tags.SPAN_KIND_BROKER
368+
369+
and: "UNSET and CUSTOM map to null"
370+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_UNSET] == null
371+
DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_CUSTOM] == null
372+
}
373+
374+
def "setSpanKindOrdinal round-trips with SPAN_KIND_VALUES for all known kinds"() {
375+
when:
376+
def span = tracer.buildSpan("test", "test").start()
377+
def context = (DDSpanContext) span.context()
378+
context.setSpanKindOrdinal(kindString)
379+
380+
then:
381+
context.getSpanKindOrdinal() == expectedOrdinal
382+
DDSpanContext.SPAN_KIND_VALUES[expectedOrdinal] == kindString
383+
384+
cleanup:
385+
span.finish()
386+
387+
where:
388+
kindString | expectedOrdinal
389+
Tags.SPAN_KIND_SERVER | DDSpanContext.SPAN_KIND_SERVER
390+
Tags.SPAN_KIND_CLIENT | DDSpanContext.SPAN_KIND_CLIENT
391+
Tags.SPAN_KIND_PRODUCER | DDSpanContext.SPAN_KIND_PRODUCER
392+
Tags.SPAN_KIND_CONSUMER | DDSpanContext.SPAN_KIND_CONSUMER
393+
Tags.SPAN_KIND_INTERNAL | DDSpanContext.SPAN_KIND_INTERNAL
394+
Tags.SPAN_KIND_BROKER | DDSpanContext.SPAN_KIND_BROKER
395+
}
396+
397+
def "setTag and getTag round-trip for span.kind"() {
398+
when:
399+
def span = tracer.buildSpan("test", "test").start()
400+
span.setTag(Tags.SPAN_KIND, kindString)
401+
402+
then:
403+
span.getTag(Tags.SPAN_KIND) == kindString
404+
405+
cleanup:
406+
span.finish()
407+
408+
where:
409+
kindString << [
410+
Tags.SPAN_KIND_SERVER,
411+
Tags.SPAN_KIND_CLIENT,
412+
Tags.SPAN_KIND_PRODUCER,
413+
Tags.SPAN_KIND_CONSUMER,
414+
Tags.SPAN_KIND_INTERNAL,
415+
Tags.SPAN_KIND_BROKER,
416+
]
417+
}
418+
419+
def "getTag returns null when span.kind is not set"() {
420+
when:
421+
def span = tracer.buildSpan("test", "test").start()
422+
423+
then:
424+
span.getTag(Tags.SPAN_KIND) == null
425+
426+
cleanup:
427+
span.finish()
428+
}
429+
430+
def "setTag then removeTag clears span.kind"() {
431+
when:
432+
def span = tracer.buildSpan("test", "test").start()
433+
span.setTag(Tags.SPAN_KIND, kindString)
434+
435+
then:
436+
span.getTag(Tags.SPAN_KIND) == kindString
437+
438+
when:
439+
((DDSpan) span).context().removeTag(Tags.SPAN_KIND)
440+
441+
then:
442+
span.getTag(Tags.SPAN_KIND) == null
443+
444+
cleanup:
445+
span.finish()
446+
447+
where:
448+
kindString << [
449+
Tags.SPAN_KIND_SERVER,
450+
Tags.SPAN_KIND_CLIENT,
451+
Tags.SPAN_KIND_PRODUCER,
452+
Tags.SPAN_KIND_CONSUMER,
453+
Tags.SPAN_KIND_INTERNAL,
454+
Tags.SPAN_KIND_BROKER,
455+
]
456+
}
457+
458+
def "setTag with custom span.kind falls back to tag map"() {
459+
when:
460+
def span = tracer.buildSpan("test", "test").start()
461+
span.setTag(Tags.SPAN_KIND, "custom-kind")
462+
463+
then:
464+
span.getTag(Tags.SPAN_KIND) == "custom-kind"
465+
466+
cleanup:
467+
span.finish()
468+
}
355469
}

0 commit comments

Comments
 (0)