From 3ab58621ade5dc3a7fadf5c195620f9980963d4d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 08:28:28 -0400 Subject: [PATCH 01/35] Set known tags by id on TagMap and spans Adds id-keyed setters, getters and removal to TagMap, TagMap.Entry and spans, so a writer that knows the tag skips the name lookup. Squashed from the review history of #12715. Co-Authored-By: Claude Opus 5.5 --- .../decorator/BaseDecorator.java | 11 +- .../decorator/BaseDecoratorTest.groovy | 11 +- .../decorator/ClientDecoratorTest.groovy | 2 +- ...ocessingDatabaseClientDecoratorTest.groovy | 3 +- .../DatabaseClientDecoratorTest.groovy | 2 +- .../decorator/HttpClientDecoratorTest.groovy | 2 +- .../decorator/HttpServerDecoratorTest.groovy | 2 +- .../decorator/ServerDecoratorTest.groovy | 2 +- .../decorator/NameKeyedAgentSpan.java | 52 +++++ .../main/java/datadog/trace/core/DDSpan.java | 62 +++++ .../datadog/trace/core/DDSpanContext.java | 114 +++++++++ .../trace/core/DDSpanSetTagByIdTest.java | 93 ++++++++ .../trace/api/TagMapInsertBenchmark.java | 108 +++++++++ .../main/java/datadog/trace/api/TagMap.java | 221 +++++++++++++++--- .../instrumentation/api/AgentSpan.java | 38 +++ .../trace/api/TagMapEntryCreateByIdTest.java | 59 +++++ .../datadog/trace/api/TagMapSetByIdTest.java | 75 ++++++ .../datadog/trace/api/TagMapTagHashTest.java | 101 ++++++++ 18 files changed, 915 insertions(+), 43 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java create mode 100644 internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java index 5c6628c5d35..8eb238b678d 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java @@ -8,6 +8,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.Functions; +import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.cache.QualifiedClassNameCache; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -194,23 +195,23 @@ public final void onPeerConnection( if (remoteAddress != null) { String ip = remoteAddress.getHostAddress(); if (resolved && Config.get().isPeerHostNameEnabled()) { - span.setTag(Tags.PEER_HOSTNAME, hostName(remoteAddress, ip)); + span.setTag(KnownTags.PEER_HOSTNAME_ID, hostName(remoteAddress, ip)); } if (remoteAddress instanceof Inet4Address) { - span.setTag(Tags.PEER_HOST_IPV4, ip); + span.setTag(KnownTags.PEER_IPV4_ID, ip); } else if (remoteAddress instanceof Inet6Address) { - span.setTag(Tags.PEER_HOST_IPV6, ip); + span.setTag(KnownTags.PEER_IPV6_ID, ip); } } } public void setPeerPort(AgentSpan span, String port) { - span.setTag(Tags.PEER_PORT, port); + span.setTag(KnownTags.PEER_PORT_ID, port); } public void setPeerPort(AgentSpan span, int port) { if (port > UNSET_PORT) { - span.setTag(Tags.PEER_PORT, port); + span.setTag(KnownTags.PEER_PORT_ID, port); } } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy index 3151d655739..a04a75712f0 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy @@ -24,7 +24,16 @@ class BaseDecoratorTest extends DDSpecification { @Shared def errorPriority = null as Byte - def span = Mock(AgentSpan) + def span = mockSpan() + + /** + * Mocks an {@link AgentSpan} that reports every id-keyed {@code setTag} as the name-keyed call, + * so expectations are written by tag name however the code under test sets the tag. See + * {@link NameKeyedAgentSpan}. + */ + AgentSpan mockSpan() { + return Mock(NameKeyedAgentSpan) + } def spanContext = Mock(AgentSpanContext) def "test afterStart"() { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy index fec5748f089..a1568c81af5 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy @@ -8,7 +8,7 @@ import datadog.trace.bootstrap.instrumentation.api.Tags class ClientDecoratorTest extends BaseDecoratorTest { - def span = Mock(AgentSpan) + def span = mockSpan() def "test afterStart"() { setup: diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DBTypeProcessingDatabaseClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DBTypeProcessingDatabaseClientDecoratorTest.groovy index 624100fcc50..efe550ee1bf 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DBTypeProcessingDatabaseClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DBTypeProcessingDatabaseClientDecoratorTest.groovy @@ -2,14 +2,13 @@ package datadog.trace.bootstrap.instrumentation.decorator import datadog.trace.api.DDTags import datadog.trace.api.TagMap -import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString import spock.lang.Ignore class DBTypeProcessingDatabaseClientDecoratorTest extends ClientDecoratorTest { - def span = Mock(AgentSpan) + def span = mockSpan() @Ignore("https://github.com/DataDog/dd-trace-java/pull/5213") def "test afterStart"() { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy index 93852ccc88c..b8d38b32f0d 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy @@ -12,7 +12,7 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST class DatabaseClientDecoratorTest extends ClientDecoratorTest { - def span = Mock(AgentSpan) + def span = mockSpan() def "test afterStart"() { setup: diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecoratorTest.groovy index 1bc83457bd0..92f709a5d79 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecoratorTest.groovy @@ -52,7 +52,7 @@ class HttpClientDecoratorTest extends ClientDecoratorTest { AgentTracer.forceRegister(ORIGINAL_TRACER) } - def span = Mock(AgentSpan) + def span = mockSpan() def "test onRequest"() { setup: diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy index da411dc2431..5ac781b913b 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy @@ -36,7 +36,7 @@ import static datadog.trace.api.gateway.Events.EVENTS class HttpServerDecoratorTest extends ServerDecoratorTest { - def span = Mock(AgentSpan) + def span = mockSpan() static class MapCarrierVisitor implements AgentPropagation.ContextVisitor { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy index d60c1534627..0e096772976 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy @@ -12,7 +12,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND class ServerDecoratorTest extends BaseDecoratorTest { - def span = Mock(AgentSpan) + def span = mockSpan() def "test afterStart"() { def decorator = newDecorator() diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java new file mode 100644 index 00000000000..435ef32cdb5 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/NameKeyedAgentSpan.java @@ -0,0 +1,52 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import datadog.trace.api.KnownTagCodec; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; + +/** + * An {@link AgentSpan} to mock in tests whose id-keyed {@code setTag} overloads are {@code final} + * and delegate to the name-keyed ones. A mock cannot override a final method, so it never sees the + * id-keyed call: tests keep expecting {@code setTag(name, value)} whether the code under test sets + * a tag by name or by {@code KnownTags} id. + */ +public abstract class NameKeyedAgentSpan implements AgentSpan { + @Override + public final AgentSpan setTag(long tagId, boolean value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, int value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, long value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, float value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, double value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, String value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, CharSequence value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + @Override + public final AgentSpan setTag(long tagId, Object value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index b0cc812aadc..3427d085306 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -15,6 +15,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.EndpointTracker; +import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.debugger.DebuggerConfigBridge; @@ -514,6 +515,67 @@ public DDSpan setTag(final String tag, final Object value) { return this; } + // Id-keyed overrides: same behavior as the String family above, including interception and the + // http.status_code quirk, but the tag is stored by id with no name lookup. + @Override + public DDSpan setTag(final long tagId, final String value) { + if (value == null || value.isEmpty()) { + context.removeTag(tagId); + } else { + context.setTag(tagId, value); + } + return this; + } + + @Override + public DDSpan setTag(final long tagId, final boolean value) { + context.setTag(tagId, value); + return this; + } + + @Override + public DDSpan setTag(final long tagId, final int value) { + if (tagId == KnownTags.HTTP_STATUS_CODE_ID) { + context.setHttpStatusCode((short) value); + } + context.setTag(tagId, value); + return this; + } + + @Override + public DDSpan setTag(final long tagId, final long value) { + context.setTag(tagId, value); + return this; + } + + @Override + public DDSpan setTag(final long tagId, final float value) { + context.setTag(tagId, value); + return this; + } + + @Override + public DDSpan setTag(final long tagId, final double value) { + context.setTag(tagId, value); + return this; + } + + @Override + public DDSpan setTag(final long tagId, final CharSequence value) { + if (value == null || value.length() == 0) { + context.setTag(tagId, (Object) null); + } else { + context.setTag(tagId, value); + } + return this; + } + + @Override + public DDSpan setTag(final long tagId, final Object value) { + context.setTag(tagId, value); + return this; + } + @Override public AgentSpan setAllTags(Map map) { context.setAllTags(map); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 09698ee0c5c..101b19b49ef 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -10,6 +10,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; +import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; import datadog.trace.api.TagMap; @@ -995,6 +996,16 @@ public void removeTag(String tag) { } } + /** Removes a known tag by id, rather than by its name, which need not resolve back to the id. */ + public void removeTag(long tagId) { + if (tagId == KnownTags.SPAN_KIND_ID) { + spanKindOrdinal = SPAN_KIND_UNSET; + } + synchronized (unsafeTags) { + unsafeTags.getAndRemove(tagId); + } + } + /** * Sets a tag to the span. Tags are not propagated to the children. * @@ -1030,6 +1041,109 @@ public void setTag(final String tag, final String value) { } } + /* + * Id-keyed setters, mirroring the String setters above: interception still runs on the tag's + * name (TagInterceptor is name-keyed), but a stored tag is set by id, skipping the name lookup. + * An id that names no known tag is ignored, like a null tag. + */ + public void setTag(final long tagId, final Object value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + if (null == value) { + removeTag(tagId); + } else if (!tagInterceptor.interceptTag(this, tag, value)) { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + + public void setTag(final long tagId, final CharSequence value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + if (null == value) { + removeTag(tagId); + } else if (!tagInterceptor.interceptTag(this, tag, value)) { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + + public void setTag(final long tagId, final boolean value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + if (precheckIntercept(tag)) { + this.setBox(tag, value); + } else { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + + public void setTag(final long tagId, final int value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + if (precheckIntercept(tag)) { + this.setBox(tag, value); + } else { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + + public void setTag(final long tagId, final long value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + boolean intercepted = + tagInterceptor.needsIntercept(tag) && tagInterceptor.interceptTag(this, tag, value); + if (!intercepted) { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + + public void setTag(final long tagId, final float value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + if (precheckIntercept(tag)) { + this.setBox(tag, value); + } else { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + + public void setTag(final long tagId, final double value) { + final String tag = KnownTagCodec.nameOf(tagId); + if (null == tag) { + return; + } + if (precheckIntercept(tag)) { + this.setBox(tag, value); + } else { + synchronized (unsafeTags) { + unsafeTags.set(tagId, value); + } + } + } + public void setTag(TagMap.EntryReader entry) { if (entry == null) { return; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java new file mode 100644 index 00000000000..e36512259ad --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -0,0 +1,93 @@ +package datadog.trace.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.api.KnownTags; +import datadog.trace.common.writer.ListWriter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * {@code DDSpan.setTag(long, ...)}: setting a known tag by id behaves exactly like setting it by + * name, including interception. + */ +class DDSpanSetTagByIdTest extends DDCoreJavaSpecification { + private CoreTracer tracer; + private DDSpan span; + + @BeforeEach + void setup() { + tracer = tracerBuilder().writer(new ListWriter()).build(); + span = (DDSpan) tracer.buildSpan("datadog", "fakeOperation").start(); + } + + @AfterEach + void tearDown() { + span.finish(); + tracer.close(); + } + + @Test + void storesTheTagUnderItsName() { + span.setTag(KnownTags.PEER_PORT_ID, 5432); + span.setTag(KnownTags.PEER_HOSTNAME_ID, "db.internal"); + span.setTag(KnownTags.DD_PROFILING_ENABLED_ID, true); + + assertEquals(5432, span.getTag(KnownTags.PEER_PORT_NAME)); + assertEquals("db.internal", span.getTag(KnownTags.PEER_HOSTNAME_NAME)); + assertEquals(true, span.getTag(KnownTags.DD_PROFILING_ENABLED_NAME)); + } + + @Test + void aNullOrEmptyValueRemovesTheTagById() { + span.setTag(KnownTags.PEER_HOSTNAME_ID, "db.internal"); + span.setTag(KnownTags.COMPONENT_ID, "okhttp"); + + span.setTag(KnownTags.PEER_HOSTNAME_ID, (String) null); + span.setTag(KnownTags.COMPONENT_ID, ""); + + assertNull(span.getTag(KnownTags.PEER_HOSTNAME_NAME)); + assertNull(span.getTag(KnownTags.COMPONENT_NAME)); + } + + @Test + void removingTheSpanKindByIdClearsTheKind() { + span.setTag(KnownTags.SPAN_KIND_ID, "client"); + assertEquals("client", span.getSpanKindString()); + + span.setTag(KnownTags.SPAN_KIND_ID, (String) null); + + assertNull(span.getSpanKindString()); + assertNull(span.getTag(KnownTags.SPAN_KIND_NAME)); + } + + @Test + void interceptedTagsAreStillIntercepted() { + span.setTag(KnownTags.DB_STATEMENT_ID, "select 1"); + span.setTag(KnownTags.SERVICE_ID, "orders-db"); + + assertEquals("select 1", span.getResourceName().toString()); + assertNull(span.getTag(KnownTags.DB_STATEMENT_NAME)); + assertEquals("orders-db", span.getServiceName()); + } + + @Test + void httpStatusCodeSetsTheStatusField() { + span.setTag(KnownTags.HTTP_STATUS_CODE_ID, 503); + + assertEquals(503, span.getHttpStatusCode()); + } + + @Test + void anEmptyOrNullValueRemovesTheTag() { + span.setTag(KnownTags.PEER_HOSTNAME_ID, "db.internal"); + span.setTag(KnownTags.PEER_HOSTNAME_ID, ""); + assertNull(span.getTag(KnownTags.PEER_HOSTNAME_NAME)); + + span.setTag(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.internal"); + span.setTag(KnownTags.PEER_HOSTNAME_ID, (CharSequence) null); + assertNull(span.getTag(KnownTags.PEER_HOSTNAME_NAME)); + } +} diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java new file mode 100644 index 00000000000..3a5ec10ddd0 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java @@ -0,0 +1,108 @@ +package datadog.trace.api; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Inserting a span's worth of tags into a fresh {@link TagMap}, the way span decoration does: known + * tags by Datadog name, the same known tags by id, and custom tags by name. + * + *

Single-threaded: each invocation builds its own map, so more threads would mostly measure + * allocation bandwidth. Run with {@code -prof gc} for allocation per operation. Tag names are + * string constants, as the tracer's are. + * + *

Results (MacBook, JDK 21, default flags, {@code @Fork(2)} triage), 12 tags per op: + * + *

+ * Benchmark      ns/op         B/op
+ * knownByName    121.1 +- 3.8  736
+ * knownById       94.7 +- 1.7  736
+ * customByName    80.6 +- 2.4  736
+ * 
+ */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(2) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Threads(1) +@State(Scope.Benchmark) +public class TagMapInsertBenchmark { + // Per-span tags a client or server decorator typically sets. + static final String[] KNOWN_NAMES = { + KnownTags.COMPONENT_NAME, + KnownTags.SPAN_KIND_NAME, + KnownTags.HTTP_METHOD_NAME, + KnownTags.HTTP_URL_NAME, + KnownTags.HTTP_ROUTE_NAME, + KnownTags.HTTP_HOSTNAME_NAME, + KnownTags.HTTP_USERAGENT_NAME, + KnownTags.HTTP_CLIENT_IP_NAME, + KnownTags.PEER_HOSTNAME_NAME, + KnownTags.PEER_IPV4_NAME, + KnownTags.PEER_PORT_NAME, + KnownTags.DD_INTEGRATION_NAME, + }; + + static final long[] KNOWN_IDS = { + KnownTags.COMPONENT_ID, + KnownTags.SPAN_KIND_ID, + KnownTags.HTTP_METHOD_ID, + KnownTags.HTTP_URL_ID, + KnownTags.HTTP_ROUTE_ID, + KnownTags.HTTP_HOSTNAME_ID, + KnownTags.HTTP_USERAGENT_ID, + KnownTags.HTTP_CLIENT_IP_ID, + KnownTags.PEER_HOSTNAME_ID, + KnownTags.PEER_IPV4_ID, + KnownTags.PEER_PORT_ID, + KnownTags.DD_INTEGRATION_ID, + }; + + static final String[] CUSTOM_NAMES = new String[KNOWN_NAMES.length]; + + static final Object[] VALUES = new Object[KNOWN_NAMES.length]; + + static { + for (int i = 0; i < KNOWN_NAMES.length; ++i) { + CUSTOM_NAMES[i] = ("app.custom.tag." + i).intern(); + VALUES[i] = "value-" + i; + } + } + + @Benchmark + public TagMap knownByName() { + TagMap map = TagMap.create(); + for (int i = 0; i < KNOWN_NAMES.length; ++i) { + map.set(KNOWN_NAMES[i], VALUES[i]); + } + return map; + } + + @Benchmark + public TagMap knownById() { + TagMap map = TagMap.create(); + for (int i = 0; i < KNOWN_IDS.length; ++i) { + map.set(KNOWN_IDS[i], VALUES[i]); + } + return map; + } + + @Benchmark + public TagMap customByName() { + TagMap map = TagMap.create(); + for (int i = 0; i < CUSTOM_NAMES.length; ++i) { + map.set(CUSTOM_NAMES[i], VALUES[i]); + } + return map; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 52d2ef32a2b..0b1a9bd6862 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -280,6 +280,44 @@ public static final Entry create(@Nonnull String tag, double value) { return TagMap.Entry.newDoubleEntry(tag, value); } + /* + * Id-keyed counterparts of the create overloads above, for a KnownTags.*_ID: same contract (a + * null or empty value yields no entry), without the name lookup. An id that names no known tag + * is rejected. + */ + + /** Entry for a known tag id, or null when {@code value} is null or an empty CharSequence. */ + @Nullable + public static final Entry create(long tagId, Object value) { + return isEmptyValue(value) ? null : TagMap.Entry.newAnyEntry(tagId, value); + } + + /** Entry for a known tag id, or null when {@code value} is null or empty. */ + @Nullable + public static final Entry create(long tagId, CharSequence value) { + return isEmptyValue(value) ? null : TagMap.Entry.newObjectEntry(tagId, value); + } + + public static final Entry create(long tagId, boolean value) { + return TagMap.Entry.newBooleanEntry(tagId, value); + } + + public static final Entry create(long tagId, int value) { + return TagMap.Entry.newIntEntry(tagId, value); + } + + public static final Entry create(long tagId, long value) { + return TagMap.Entry.newLongEntry(tagId, value); + } + + public static final Entry create(long tagId, float value) { + return TagMap.Entry.newFloatEntry(tagId, value); + } + + public static final Entry create(long tagId, double value) { + return TagMap.Entry.newDoubleEntry(tagId, value); + } + static Entry newAnyEntry(Map.Entry entry) { return newAnyEntry(entry.getKey(), entry.getValue()); } @@ -299,6 +337,37 @@ static Entry newObjectEntry(String tag, Object value) { return new Entry(tag, OBJECT, 0, value); } + // Id-keyed factories: the tag id already names a canonical tag, so these skip the + // canonicalizing + // name lookup the String factories pay. + static Entry newAnyEntry(long tagId, Object value) { + return new Entry(tagId, ANY, 0L, value); + } + + static Entry newObjectEntry(long tagId, Object value) { + return new Entry(tagId, OBJECT, 0, value); + } + + static Entry newBooleanEntry(long tagId, boolean value) { + return new Entry(tagId, BOOLEAN, boolean2Prim(value), Boolean.valueOf(value)); + } + + static Entry newIntEntry(long tagId, int value) { + return new Entry(tagId, INT, int2Prim(value), null); + } + + static Entry newLongEntry(long tagId, long value) { + return new Entry(tagId, LONG, long2Prim(value), null); + } + + static Entry newFloatEntry(long tagId, float value) { + return new Entry(tagId, FLOAT, float2Prim(value), null); + } + + static Entry newDoubleEntry(long tagId, double value) { + return new Entry(tagId, DOUBLE, double2Prim(value), null); + } + static Entry newBooleanEntry(String tag, boolean value) { return new Entry(tag, BOOLEAN, boolean2Prim(value), Boolean.valueOf(value)); } @@ -340,10 +409,12 @@ static Entry newDoubleEntry(String tag, Double box) { } /* - * hash is stored in line for fast handling of Entry-s coming from another TagMap - * However, hash is lazily computed using the same trick as {@link java.lang.String}. + * The tag's 64-bit hash, computed once at construction. For a known tag it is the tag id, whose + * serial sits in the high bits, so the upper 32 bits are never zero; for a custom tag it is the + * name's hash in the low 32 bits, with the upper 32 bits zero. The two can never collide, and + * tagId() is a field read. See tagHashOf. */ - int lazyTagHash; + final long tagHash; // To optimize construction of Entry around boxed primitives and Object entries, // no type checks are done during construction. @@ -368,35 +439,72 @@ static Entry newDoubleEntry(String tag, Double box) { private Entry(String tag, byte type, long prim, Object obj) { /* * Canonicalize known names at the single Entry construction point, so Datadog and - * OpenTelemetry spellings use the same TagMap key. This adds a StringIndex lookup to every - * new entry, including on the application thread. + * OpenTelemetry spellings use the same TagMap key. The one StringIndex lookup this costs also + * yields the tag id, which becomes the tag hash. */ - super(KnownTagCodec.canonicalTagName(tag)); - this.lazyTagHash = 0; // lazily computed + this(KnownTagCodec.keyOf(tag), tag, type, prim, obj); + } + + private Entry(long tagId, byte type, long prim, Object obj) { + // Resolve the name once: it both validates the id and names the entry. + super(requireKnownName(tagId)); + this.tagHash = tagId; this.rawType = type; this.rawPrim = prim; this.rawObj = obj; } - int hash() { - // If value of hash read in this thread is zero, then hash is computed. - // hash is not held as a volatile, since this computation can safely be repeated as any time - int hash = this.lazyTagHash; - if (hash != 0) return hash; + /** {@code tagId} is a known id, or 0 for the custom tag {@code customTag}. */ + private Entry(long tagId, String customTag, byte type, long prim, Object obj) { + super(tagId != 0 ? KnownTagCodec.nameOf(tagId) : customTag); + this.tagHash = tagId != 0 ? tagId : customHash(customTag); - hash = _hash(this.tag); - this.lazyTagHash = hash; - return hash; + this.rawType = type; + this.rawPrim = prim; + this.rawObj = obj; + } + + private static String requireKnownName(long tagId) { + String name = tagId == 0 ? null : KnownTagCodec.nameOf(tagId); + if (name == null) { + throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); + } + return name; + } + + /** The tag hash a name maps to: its id when known, else its {@link #customHash}. */ + static long tagHashOf(String tag) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? tagId : customHash(tag); + } + + /** A custom tag's hash: the name hash in the low 32 bits, never colliding with a tag id. */ + static long customHash(String tag) { + return _hash(tag) & 0xFFFFFFFFL; + } + + /** + * Folds a tag hash to the 32-bit bucket hash. A tag id's serial is in bits 63-48, so it is + * folded down from there rather than from bit 32, where a bucket mask would discard it. + * + *

Never zero: BucketGroup treats a zero hash as a vacant slot. The fold can produce zero + * when a serial equals the id's flag bits (serial 4 with the trace-level bit, 4), so zero maps + * to the same nonzero sentinel {@link #_hash} uses. + */ + static int bucketHash(long tagHash) { + int hash = (int) (tagHash >>> 48) ^ (int) tagHash; + return hash == 0 ? 0xDD06 : hash; + } + + int hash() { + return bucketHash(this.tagHash); } @Override public long tagId() { - /* - * Resolve on demand. Only OTLP serialization currently needs the ID, so caching it here - * would add a field to every Entry to save a lookup on exported tags. - */ - return KnownTagCodec.keyOf(this.tag); + // A known tag's hash is its id, whose upper 32 bits are never zero; a custom tag's are. + return (this.tagHash >>> 32) != 0 ? this.tagHash : 0L; } @Override @@ -1301,9 +1409,18 @@ public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must canonicalize the same way, or it would hash to the wrong bucket // and silently miss the entry stored under the Datadog name. - String canonicalTag = KnownTagCodec.canonicalTagName(tag); + long tagHash = Entry.tagHashOf(tag); + String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; + return this.getEntry(canonicalTag, tagHash); + } - Entry local = this.getLocalEntry(canonicalTag); + /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ + public Entry getEntry(long tagId) { + return this.getEntry(Entry.requireKnownName(tagId), tagId); + } + + private Entry getEntry(String canonicalTag, long tagHash) { + Entry local = this.getLocalEntry(canonicalTag, tagHash); if (local != null) { // Local entry shadows the parent (local-wins) — unchanged hot path. return local; @@ -1318,13 +1435,13 @@ public Entry getEntry(String tag) { if (this.removedFromParent != null && this.removedFromParent.contains(canonicalTag)) { return null; // tombstoned: removed locally, do not read through } - return parent.getEntry(canonicalTag); + return parent.getEntry(canonicalTag, tagHash); } /** Looks up an entry in this map's own buckets only — no read-through to the parent. */ - private Entry getLocalEntry(String tag) { + private Entry getLocalEntry(String tag, long tagHash) { Object[] thisBuckets = this.buckets; - int hash = TagMap.Entry._hash(tag); + int hash = Entry.bucketHash(tagHash); return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag); } @@ -1416,6 +1533,39 @@ public void set(@Nonnull String tag, double value) { this.putEntry(Entry.newDoubleEntry(tag, value)); } + /* + * Id-keyed setters: the caller passes a KnownTags.*_ID, so the entry is built under the tag's + * canonical name without the name lookup the String setters pay. The id must name a known tag; + * custom tags have no id and use the String setters. + */ + public void set(long tagId, @Nonnull Object value) { + this.putEntry(Entry.newAnyEntry(tagId, value)); + } + + public void set(long tagId, @Nonnull CharSequence value) { + this.putEntry(Entry.newObjectEntry(tagId, value)); + } + + public void set(long tagId, boolean value) { + this.putEntry(Entry.newBooleanEntry(tagId, value)); + } + + public void set(long tagId, int value) { + this.putEntry(Entry.newIntEntry(tagId, value)); + } + + public void set(long tagId, long value) { + this.putEntry(Entry.newLongEntry(tagId, value)); + } + + public void set(long tagId, float value) { + this.putEntry(Entry.newFloatEntry(tagId, value)); + } + + public void set(long tagId, double value) { + this.putEntry(Entry.newDoubleEntry(tagId, value)); + } + /** * Places an Entry directly into the map, avoiding a new Entry allocation. Null-tolerant: a null * {@code newEntry} is a no-op returning null, so an Entry producer (e.g. {@link @@ -1785,9 +1935,20 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must canonicalize first to find (and tombstone) the right entry. - String canonicalTag = KnownTagCodec.canonicalTagName(tag); + long tagHash = Entry.tagHashOf(tag); + String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; + return this.getAndRemove(canonicalTag, tagHash); + } + + /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ + public Entry getAndRemove(long tagId) { + this.checkWriteAccess(); + + return this.getAndRemove(Entry.requireKnownName(tagId), tagId); + } - Entry localRemoved = this.removeLocal(canonicalTag); + private Entry getAndRemove(String canonicalTag, long tagHash) { + Entry localRemoved = this.removeLocal(canonicalTag, tagHash); TagMap parent = this.parent; if (parent != null) { @@ -1798,7 +1959,7 @@ public Entry getAndRemove(String tag) { boolean alreadyTombstoned = this.removedFromParent != null && this.removedFromParent.contains(canonicalTag); if (!alreadyTombstoned) { - Entry parentEntry = parent.getEntry(canonicalTag); + Entry parentEntry = parent.getEntry(canonicalTag, tagHash); if (parentEntry != null) { if (this.removedFromParent == null) { // Small initial capacity: this set is rare and almost always holds only a handful of @@ -1814,10 +1975,10 @@ public Entry getAndRemove(String tag) { } /** Removes an entry from this map's own buckets only — no parent/tombstone handling. */ - private Entry removeLocal(String tag) { + private Entry removeLocal(String tag, long tagHash) { Object[] thisBuckets = this.buckets; - int hash = TagMap.Entry._hash(tag); + int hash = Entry.bucketHash(tagHash); int bucketIndex = hash & (thisBuckets.length - 1); Object bucket = thisBuckets[bucketIndex]; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index 94c0f80572a..af3aa211c15 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -9,6 +9,7 @@ import datadog.context.ImplicitContextKeyed; import datadog.trace.api.DDSpanId; import datadog.trace.api.DDTraceId; +import datadog.trace.api.KnownTagCodec; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.gateway.IGSpanInfo; @@ -103,6 +104,43 @@ default boolean isValid() { AgentSpan setTag(String key, Object value); + /* + * Id-keyed setTag family: the caller passes a KnownTags.*_ID instead of a name. The default + * resolves the id to its name and delegates to the String setter, which is correct for every + * implementation; the core span overrides these to skip the name lookup when storing the tag. + */ + default AgentSpan setTag(long tagId, boolean value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, int value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, long value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, float value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, double value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, String value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, CharSequence value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + + default AgentSpan setTag(long tagId, Object value) { + return setTag(KnownTagCodec.nameOf(tagId), value); + } + /** entry may be null - in which case the tags remained unchanged */ AgentSpan setTag(TagMap.EntryReader entry); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java new file mode 100644 index 00000000000..5c1e61ae771 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java @@ -0,0 +1,59 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** {@link TagMap.Entry#create(long, Object)} and its overloads: creating an entry by tag id. */ +class TagMapEntryCreateByIdTest { + @Test + void matchesCreateByNameForEveryValueType() { + assertEquivalent( + TagMap.Entry.create(KnownTags.COMPONENT_NAME, (Object) "okhttp"), + TagMap.Entry.create(KnownTags.COMPONENT_ID, (Object) "okhttp")); + assertEquivalent( + TagMap.Entry.create(KnownTags.PEER_HOSTNAME_NAME, (CharSequence) "db.internal"), + TagMap.Entry.create(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.internal")); + assertEquivalent( + TagMap.Entry.create(KnownTags.DD_PROFILING_ENABLED_NAME, true), + TagMap.Entry.create(KnownTags.DD_PROFILING_ENABLED_ID, true)); + assertEquivalent( + TagMap.Entry.create(KnownTags.PEER_PORT_NAME, 5432), + TagMap.Entry.create(KnownTags.PEER_PORT_ID, 5432)); + assertEquivalent( + TagMap.Entry.create(KnownTags.HTTP_RESEND_COUNT_NAME, 2L), + TagMap.Entry.create(KnownTags.HTTP_RESEND_COUNT_ID, 2L)); + assertEquivalent( + TagMap.Entry.create(KnownTags.DB_USER_NAME, 1.5f), + TagMap.Entry.create(KnownTags.DB_USER_ID, 1.5f)); + assertEquivalent( + TagMap.Entry.create(KnownTags.DB_POOL_NAME, 2.5d), + TagMap.Entry.create(KnownTags.DB_POOL_NAME_ID, 2.5d)); + } + + @Test + void aNullOrEmptyValueCreatesNoEntry() { + assertNull(TagMap.Entry.create(KnownTags.COMPONENT_ID, (Object) null)); + assertNull(TagMap.Entry.create(KnownTags.COMPONENT_ID, (Object) "")); + assertNull(TagMap.Entry.create(KnownTags.COMPONENT_ID, (CharSequence) null)); + assertNull(TagMap.Entry.create(KnownTags.COMPONENT_ID, (CharSequence) "")); + } + + @Test + void anUnknownIdIsRejected() { + assertThrows(IllegalArgumentException.class, () -> TagMap.Entry.create(0L, (Object) "value")); + assertThrows( + IllegalArgumentException.class, + () -> TagMap.Entry.create(KnownTagCodec.makeTagId(9999), 1)); + } + + /** Same tag, id, type and value -- the two creation paths build equivalent entries. */ + private static void assertEquivalent(TagMap.Entry byName, TagMap.Entry byId) { + assertEquals(byName.tag(), byId.tag()); + assertEquals(byName.tagId(), byId.tagId()); + assertEquals(byName.type(), byId.type()); + assertEquals(byName.objectValue(), byId.objectValue()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java new file mode 100644 index 00000000000..1b6dc0b0e5c --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -0,0 +1,75 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +/** {@link TagMap#set(long, Object)} and its overloads: setting a known tag by its id. */ +class TagMapSetByIdTest { + @Test + void storesUnderTheCanonicalName() { + TagMap map = TagMap.create(); + + map.set(KnownTags.PEER_HOSTNAME_ID, "db.internal"); + + assertEquals("db.internal", map.getObject(KnownTags.PEER_HOSTNAME_NAME)); + assertEquals(KnownTags.PEER_HOSTNAME_ID, map.getEntry(KnownTags.PEER_HOSTNAME_NAME).tagId()); + } + + @Test + void anOpenTelemetryRenameIsStoredUnderTheDatadogName() { + TagMap byId = TagMap.create(); + TagMap byOtelName = TagMap.create(); + + byId.set(KnownTags.HTTP_METHOD_ID, "GET"); + byOtelName.set(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + + assertEquals(new HashMap<>(byOtelName), new HashMap<>(byId)); + } + + @Test + void matchesTheNameKeyedSettersForEveryValueType() { + TagMap byId = TagMap.create(); + byId.set(KnownTags.PEER_PORT_ID, 5432); + byId.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + byId.set(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.internal"); + byId.set(KnownTags.DB_INSTANCE_ID, (Object) "orders"); + byId.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + byId.set(KnownTags.DB_USER_ID, 1.5f); + byId.set(KnownTags.DB_POOL_NAME_ID, 2.5d); + + TagMap byName = TagMap.create(); + byName.set(KnownTags.PEER_PORT_NAME, 5432); + byName.set(KnownTags.HTTP_RESEND_COUNT_NAME, 2L); + byName.set(KnownTags.PEER_HOSTNAME_NAME, (CharSequence) "db.internal"); + byName.set(KnownTags.DB_INSTANCE_NAME, (Object) "orders"); + byName.set(KnownTags.DD_PROFILING_ENABLED_NAME, true); + byName.set(KnownTags.DB_USER_NAME, 1.5f); + byName.set(KnownTags.DB_POOL_NAME, 2.5d); + + assertEquals(new HashMap<>(byName), new HashMap<>(byId)); + assertEquals(5432, byId.getEntry(KnownTags.PEER_PORT_NAME).intValue()); + } + + @Test + void getsAndRemovesById() { + TagMap map = TagMap.create(); + map.set(KnownTags.HTTP_METHOD_NAME, "GET"); + + assertEquals("GET", map.getEntry(KnownTags.HTTP_METHOD_ID).stringValue()); + assertEquals("GET", map.getAndRemove(KnownTags.HTTP_METHOD_ID).stringValue()); + assertNull(map.getEntry(KnownTags.HTTP_METHOD_ID)); + assertNull(map.getAndRemove(KnownTags.HTTP_METHOD_ID)); + } + + @Test + void anUnknownIdIsRejected() { + TagMap map = TagMap.create(); + + assertThrows(IllegalArgumentException.class, () -> map.set(0L, "value")); + assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java new file mode 100644 index 00000000000..7cd4231ce9b --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -0,0 +1,101 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * The 64-bit tag hash on {@link TagMap.Entry}: a known tag's hash is its id, a custom tag's is its + * name hash in the low 32 bits. + */ +class TagMapTagHashTest { + @Test + void aKnownTagHashesToItsIdHoweverItIsSet() { + TagMap.Entry byId = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "GET"); + TagMap.Entry byName = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_NAME, "GET"); + TagMap.Entry byOtelName = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + + assertEquals(KnownTags.HTTP_METHOD_ID, byId.tagHash); + assertEquals(KnownTags.HTTP_METHOD_ID, byName.tagHash); + assertEquals(KnownTags.HTTP_METHOD_ID, byOtelName.tagHash); + assertEquals(KnownTags.HTTP_METHOD_ID, byName.tagId()); + } + + @Test + void aCustomTagHashesToItsNameWithNoId() { + TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); + + assertEquals(0L, custom.tagHash >>> 32); + assertEquals(0L, custom.tagId()); + assertEquals(TagMap.Entry.tagHashOf("my.custom.tag"), custom.tagHash); + } + + @Test + void theBucketHashOfACustomTagIsUnchanged() { + assertEquals( + TagMap.Entry._hash("my.custom.tag"), + TagMap.Entry.bucketHash(TagMap.Entry.tagHashOf("my.custom.tag"))); + } + + @Test + void theBucketHashOfAKnownTagCarriesItsSerial() { + // Folding from bit 32 would leave the serial above any bucket mask; it must come from bit 48. + assertNotEquals( + TagMap.Entry.bucketHash(KnownTags.HTTP_METHOD_ID) & 0xFF, + TagMap.Entry.bucketHash(KnownTags.HTTP_ROUTE_ID) & 0xFF); + } + + @Test + void noKnownTagFoldsToTheVacantSlotHash() throws IllegalAccessException { + // BucketGroup treats a zero hash as a vacant slot; _dd.djm.enabled once folded to zero. + for (long tagId : knownTagIds()) { + assertNotEquals(0, TagMap.Entry.bucketHash(tagId), Long.toHexString(tagId)); + } + assertNotEquals(0, TagMap.Entry.bucketHash(KnownTags.DD_DJM_ENABLED_ID)); + } + + @Test + void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessException { + List tagIds = knownTagIds(); + TagMap map = TagMap.create(); + for (long tagId : tagIds) { + map.set(tagId, "value"); + } + TagMap copy = map.copy(); + + assertEquals(tagIds.size(), map.size()); + assertEquals(tagIds.size(), copy.size()); + for (long tagId : tagIds) { + String name = KnownTagCodec.nameOf(tagId); + assertEquals("value", map.getObject(name), name); + assertEquals("value", copy.getObject(name), name); + } + } + + private static List knownTagIds() throws IllegalAccessException { + List tagIds = new ArrayList<>(); + for (Field field : KnownTags.class.getFields()) { + if (field.getType() == long.class && Modifier.isStatic(field.getModifiers())) { + tagIds.add(field.getLong(null)); + } + } + return tagIds; + } + + @Test + void lookupsFindEntriesSetEitherWay() { + TagMap map = TagMap.create(); + map.set(KnownTags.PEER_PORT_ID, 5432); + map.set("my.custom.tag", "value"); + + assertEquals(5432, map.getEntry(KnownTags.PEER_PORT_NAME).intValue()); + assertEquals("value", map.getString("my.custom.tag")); + assertEquals(5432, map.getAndRemove(KnownTags.PEER_PORT_NAME).intValue()); + assertEquals(null, map.getEntry(KnownTags.PEER_PORT_NAME)); + } +} From 4bf02bb7d56bdc941c87802dbf00e7c7cd54ec3a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 10:01:50 -0400 Subject: [PATCH 02/35] Ignore unknown ids when clearing a tag by id; fix a comment wrap span.setTag(0L, "") or a null value reached TagMap.getAndRemove(long), which rejects unknown ids, so clearing threw IllegalArgumentException while every id-keyed setter ignores an unknown id. removeTag(long) now ignores it too. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/core/DDSpanContext.java | 8 +++++++- .../java/datadog/trace/core/DDSpanSetTagByIdTest.java | 9 +++++++++ internal-api/src/main/java/datadog/trace/api/TagMap.java | 3 +-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 101b19b49ef..40d4706d283 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -996,8 +996,14 @@ public void removeTag(String tag) { } } - /** Removes a known tag by id, rather than by its name, which need not resolve back to the id. */ + /** + * Removes a known tag by id, rather than by its name, which need not resolve back to the id. An + * id that names no known tag is ignored, like the id-keyed setters do. + */ public void removeTag(long tagId) { + if (KnownTagCodec.nameOf(tagId) == null) { + return; + } if (tagId == KnownTags.SPAN_KIND_ID) { spanKindOrdinal = SPAN_KIND_UNSET; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index e36512259ad..c4b7fdbd808 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -52,6 +52,15 @@ void aNullOrEmptyValueRemovesTheTagById() { assertNull(span.getTag(KnownTags.COMPONENT_NAME)); } + @Test + void clearingAnUnknownIdIsIgnored() { + span.setTag(0L, (String) null); + span.setTag(0L, ""); + span.setTag(0L, (Object) null); + + assertNull(span.getTag(KnownTags.PEER_HOSTNAME_NAME)); + } + @Test void removingTheSpanKindByIdClearsTheKind() { span.setTag(KnownTags.SPAN_KIND_ID, "client"); diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 0b1a9bd6862..59dec3eb961 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -338,8 +338,7 @@ static Entry newObjectEntry(String tag, Object value) { } // Id-keyed factories: the tag id already names a canonical tag, so these skip the - // canonicalizing - // name lookup the String factories pay. + // canonicalizing name lookup the String factories pay. static Entry newAnyEntry(long tagId, Object value) { return new Entry(tagId, ANY, 0L, value); } From a2829d1df1a41f7837990ec4479fee25a7717c15 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 10:48:48 -0400 Subject: [PATCH 03/35] Precheck and reuse the box in long-valued setters The long-valued setters prechecked with needsIntercept, but when the interceptor declined the tag they discarded the box it was given and stored the primitive. Follow the same precheckIntercept -> setBox shape as the other primitive setters, so TagMap keeps the box. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/core/DDSpanContext.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 40d4706d283..94218f92916 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1113,9 +1113,9 @@ public void setTag(final long tagId, final long value) { if (null == tag) { return; } - boolean intercepted = - tagInterceptor.needsIntercept(tag) && tagInterceptor.interceptTag(this, tag, value); - if (!intercepted) { + if (precheckIntercept(tag)) { + this.setBox(tag, value); + } else { synchronized (unsafeTags) { unsafeTags.set(tagId, value); } @@ -1225,10 +1225,9 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - // check needsIntercept first to avoid unnecessary boxing - boolean intercepted = - tagInterceptor.needsIntercept(tag) && tagInterceptor.interceptTag(this, tag, value); - if (!intercepted) { + if (precheckIntercept(tag)) { + this.setBox(tag, value); + } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); } From 2d87cde804f8558317f34dc68099108cf454b449 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 10:28:57 -0400 Subject: [PATCH 04/35] Keep peer.port off the id path peer.port is declared per direction in the registry (#12713), so it has no direction-free id: BaseDecorator, shared by client and server decorators, sets it by name, and the set-by-id tests use an unshared int tag. Co-Authored-By: Claude Opus 5.5 --- .../instrumentation/decorator/BaseDecorator.java | 4 ++-- .../java/datadog/trace/core/DDSpanSetTagByIdTest.java | 4 ++-- .../java/datadog/trace/api/TagMapInsertBenchmark.java | 4 ++-- .../datadog/trace/api/TagMapEntryCreateByIdTest.java | 4 ++-- .../java/datadog/trace/api/TagMapSetByIdTest.java | 6 +++--- .../java/datadog/trace/api/TagMapTagHashTest.java | 11 +++++++---- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java index 8eb238b678d..4621d251729 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java @@ -206,12 +206,12 @@ public final void onPeerConnection( } public void setPeerPort(AgentSpan span, String port) { - span.setTag(KnownTags.PEER_PORT_ID, port); + span.setTag(Tags.PEER_PORT, port); } public void setPeerPort(AgentSpan span, int port) { if (port > UNSET_PORT) { - span.setTag(KnownTags.PEER_PORT_ID, port); + span.setTag(Tags.PEER_PORT, port); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index c4b7fdbd808..0c953fa5b65 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -31,11 +31,11 @@ void tearDown() { @Test void storesTheTagUnderItsName() { - span.setTag(KnownTags.PEER_PORT_ID, 5432); + span.setTag(KnownTags.HTTP_RESEND_COUNT_ID, 2); span.setTag(KnownTags.PEER_HOSTNAME_ID, "db.internal"); span.setTag(KnownTags.DD_PROFILING_ENABLED_ID, true); - assertEquals(5432, span.getTag(KnownTags.PEER_PORT_NAME)); + assertEquals(2, span.getTag(KnownTags.HTTP_RESEND_COUNT_NAME)); assertEquals("db.internal", span.getTag(KnownTags.PEER_HOSTNAME_NAME)); assertEquals(true, span.getTag(KnownTags.DD_PROFILING_ENABLED_NAME)); } diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java index 3a5ec10ddd0..628d711a9e6 100644 --- a/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertBenchmark.java @@ -49,7 +49,7 @@ public class TagMapInsertBenchmark { KnownTags.HTTP_CLIENT_IP_NAME, KnownTags.PEER_HOSTNAME_NAME, KnownTags.PEER_IPV4_NAME, - KnownTags.PEER_PORT_NAME, + KnownTags.PEER_IPV6_NAME, KnownTags.DD_INTEGRATION_NAME, }; @@ -64,7 +64,7 @@ public class TagMapInsertBenchmark { KnownTags.HTTP_CLIENT_IP_ID, KnownTags.PEER_HOSTNAME_ID, KnownTags.PEER_IPV4_ID, - KnownTags.PEER_PORT_ID, + KnownTags.PEER_IPV6_ID, KnownTags.DD_INTEGRATION_ID, }; diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java index 5c1e61ae771..f970855c212 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByIdTest.java @@ -20,8 +20,8 @@ void matchesCreateByNameForEveryValueType() { TagMap.Entry.create(KnownTags.DD_PROFILING_ENABLED_NAME, true), TagMap.Entry.create(KnownTags.DD_PROFILING_ENABLED_ID, true)); assertEquivalent( - TagMap.Entry.create(KnownTags.PEER_PORT_NAME, 5432), - TagMap.Entry.create(KnownTags.PEER_PORT_ID, 5432)); + TagMap.Entry.create(KnownTags.HTTP_STATUS_CODE_NAME, 5432), + TagMap.Entry.create(KnownTags.HTTP_STATUS_CODE_ID, 5432)); assertEquivalent( TagMap.Entry.create(KnownTags.HTTP_RESEND_COUNT_NAME, 2L), TagMap.Entry.create(KnownTags.HTTP_RESEND_COUNT_ID, 2L)); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 1b6dc0b0e5c..ecef85afdd4 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -33,7 +33,7 @@ void anOpenTelemetryRenameIsStoredUnderTheDatadogName() { @Test void matchesTheNameKeyedSettersForEveryValueType() { TagMap byId = TagMap.create(); - byId.set(KnownTags.PEER_PORT_ID, 5432); + byId.set(KnownTags.HTTP_STATUS_CODE_ID, 5432); byId.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); byId.set(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.internal"); byId.set(KnownTags.DB_INSTANCE_ID, (Object) "orders"); @@ -42,7 +42,7 @@ void matchesTheNameKeyedSettersForEveryValueType() { byId.set(KnownTags.DB_POOL_NAME_ID, 2.5d); TagMap byName = TagMap.create(); - byName.set(KnownTags.PEER_PORT_NAME, 5432); + byName.set(KnownTags.HTTP_STATUS_CODE_NAME, 5432); byName.set(KnownTags.HTTP_RESEND_COUNT_NAME, 2L); byName.set(KnownTags.PEER_HOSTNAME_NAME, (CharSequence) "db.internal"); byName.set(KnownTags.DB_INSTANCE_NAME, (Object) "orders"); @@ -51,7 +51,7 @@ void matchesTheNameKeyedSettersForEveryValueType() { byName.set(KnownTags.DB_POOL_NAME, 2.5d); assertEquals(new HashMap<>(byName), new HashMap<>(byId)); - assertEquals(5432, byId.getEntry(KnownTags.PEER_PORT_NAME).intValue()); + assertEquals(5432, byId.getEntry(KnownTags.HTTP_STATUS_CODE_NAME).intValue()); } @Test diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index 7cd4231ce9b..5a5e56b7c78 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -72,6 +72,9 @@ void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessExcept assertEquals(tagIds.size(), copy.size()); for (long tagId : tagIds) { String name = KnownTagCodec.nameOf(tagId); + if (KnownTagCodec.keyOf(name) != tagId) { + continue; // a name declared per direction does not resolve to either direction's id + } assertEquals("value", map.getObject(name), name); assertEquals("value", copy.getObject(name), name); } @@ -90,12 +93,12 @@ private static List knownTagIds() throws IllegalAccessException { @Test void lookupsFindEntriesSetEitherWay() { TagMap map = TagMap.create(); - map.set(KnownTags.PEER_PORT_ID, 5432); + map.set(KnownTags.HTTP_STATUS_CODE_ID, 5432); map.set("my.custom.tag", "value"); - assertEquals(5432, map.getEntry(KnownTags.PEER_PORT_NAME).intValue()); + assertEquals(5432, map.getEntry(KnownTags.HTTP_STATUS_CODE_NAME).intValue()); assertEquals("value", map.getString("my.custom.tag")); - assertEquals(5432, map.getAndRemove(KnownTags.PEER_PORT_NAME).intValue()); - assertEquals(null, map.getEntry(KnownTags.PEER_PORT_NAME)); + assertEquals(5432, map.getAndRemove(KnownTags.HTTP_STATUS_CODE_NAME).intValue()); + assertEquals(null, map.getEntry(KnownTags.HTTP_STATUS_CODE_NAME)); } } From 0401a430c26b2f7c33cc61e4d0401e14955423dd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 10:32:35 -0400 Subject: [PATCH 05/35] Mark intercepted tags with an id bit, via a tracer overlay Set-path routing is a per-language concern, so it lives in a Java overlay next to tag-conventions.yaml rather than in the language-agnostic conventions. The overlay declares the keys that exist only to be routed (resource.name, error, sampling directives, ...) and lists every tag TagInterceptor may route. Each listed tag's id carries the INTERCEPTED bit (bit 1), so a setter called with a constant id can fold the interception test away. Serial numbers become public so TagInterceptor can switch on them. Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 1 + .../tagRegistry/GenerateKnownTagsTask.kt | 9 +++- .../tagRegistry/KnownTagsEmitter.kt | 8 +-- .../buildlogic/tagRegistry/TagConventions.kt | 39 ++++++++++++-- .../buildlogic/tagRegistry/TagRegistry.kt | 25 +++++---- .../tagRegistry/TagRegistryExtension.kt | 6 +++ .../tagRegistry/TagRegistryGenerator.kt | 34 ++++++------ .../tagRegistry/TagRegistryGeneratorPlugin.kt | 1 + .../tagRegistry/TagRegistryGeneratorTest.kt | 49 +++++++++++++++++ internal-api/build.gradle.kts | 1 + .../java/datadog/trace/api/KnownTagCodec.java | 27 +++++++--- tag-conventions-java.yaml | 54 +++++++++++++++++++ 12 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 tag-conventions-java.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bf1ce358fdf..ea941639256 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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-java +/tag-conventions-java.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities-java /dd-smoke-tests/README.md @DataDog/apm-java /docs/ @DataDog/apm-java diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt index bd567a67854..72c2dfc1514 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt @@ -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 @@ -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") } } 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 1f808d8d357..ca81859af96 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 @@ -6,7 +6,7 @@ import java.util.Locale /** * Emits the generated `KnownTags.java` from a [TagRegistry]. Public API first — per-tag * `_NAME` (string) + `_ID` (encoded long, literal) couplets with a trailing `// makeTagId(...)` - * derivation comment — then the package-private `_SERIAL_NUM` constants, the + * derivation comment — then the `_SERIAL_NUM` constants, the * `StringIndex.EmbeddingSupport` keyOf table and the resolver's name switches. */ object KnownTagsEmitter { @@ -101,15 +101,17 @@ object KnownTagsEmitter { } append("// makeTagId(serial=${t.serial})") if (t.traceLevel) append(" + trace-level") + if (t.intercepted) append(" + intercepted") 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};") } // OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt index 54fef064dfb..b17eac293b7 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt @@ -9,6 +9,8 @@ class TagConventions private constructor( private val spanTypes: Map, private val mixins: Map, private val traceLevel: List, + /** Datadog names the tracer intercepts on the set-path, from the tracer overlay. */ + val interceptedNames: Set = emptySet(), ) { /** * A tag's identity: its Datadog name, plus the direction when that name is declared once per @@ -242,6 +244,15 @@ class TagConventions private constructor( private fun directionOf(spanTypes: Map, st: SpanType): Direction? = chainOf(spanTypes, st.name).firstNotNullOfOrNull { it.direction } /** Reads `span-kind` as the direction it sets, or null when absent. */ + private fun overlayMixin(overlay: Map): Map { + 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()) + ) + } + private fun parseDirection(m: Map, owner: String): Direction? { val spanKind = m["span-kind"] require(spanKind == null || spanKind in SPAN_KIND_DIRECTIONS) { @@ -251,10 +262,27 @@ class TagConventions private constructor( } @Suppress("UNCHECKED_CAST") - fun parse(root: Map): TagConventions { + /** 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`), and + * `intercepted` lists the Datadog names the tracer intercepts, from either file. + */ + fun parse(root: Map, overlay: Map = 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" + } + val intercepted = (interceptedRaw as? List)?.toSet() ?: emptySet() val spanTypesRaw = (root["span_types"] as? Map) ?: emptyMap() val parsedSpanTypes = spanTypesRaw.mapValues { (name, v) -> @@ -298,7 +326,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) { @@ -342,7 +370,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 } /** 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 664ee9f6687..73c38c2f7ef 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 @@ -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]. * - *

An id is IDENTITY only: a globally unique serial plus the trace-level classification bit. It + *

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. * - *

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. + *

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) { data class Tag( @@ -23,6 +21,7 @@ class TagRegistry private constructor(val tags: List) { 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, @@ -47,14 +46,16 @@ class TagRegistry private constructor(val tags: List) { 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 /** - * 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, 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 = false): Long { var id = serial.toLong() shl 48 if (traceLevel) id = id or LEVEL_TRACE + if (intercepted) id = id or INTERCEPTED return id } @@ -67,13 +68,15 @@ class TagRegistry private constructor(val tags: List) { 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), declaredOtelName = renames[t.identity]?.otelName, otelDirection = renames[t.identity]?.direction, ) diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt index f6d07ca586b..1193de5c3cc 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryExtension.kt @@ -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`. */ diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index 9b69f0fe0b0..50960175cc0 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -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 = - tagConventionsFile.inputStream().use { - mapper.readValue(it, object : TypeReference>() {}) - } + fun read(file: File): Map = + file.inputStream().use { mapper.readValue(it, object : TypeReference>() {}) } ?: 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. @@ -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 ) ) } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt index c350d3c302e..a93b92df9f7 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPlugin.kt @@ -16,6 +16,7 @@ class TagRegistryGeneratorPlugin : Plugin { 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") { diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index 1ff22853571..e4fdba9d853 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -478,6 +478,55 @@ class TagRegistryGeneratorTest { .withMessageContaining("span-kind-neutral without an otel-name") } + @Test + fun `the tracer overlay declares routed keys and marks intercepted tags`() { + val yaml = directory.conventionsFile( + """ + span_types: + base: + abstract: true + tags: [{dd-name: service, otel-name: service.name}, {dd-name: component}] + """ + ) + val overlay = directory.writeFile( + "overlay.yaml", + """ + tags: [{dd-name: resource.name}] + intercepted: [resource.name, service] + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output, overlay) + + val knownTags = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + assertThat(knownTags) + .contains( + "COMPONENT_ID = 0x0001000000000000L", + "RESOURCE_NAME_ID = 0x0002000000000002L", + "SERVICE_ID = 0x0003000000000002L", + "public static final int SERVICE_SERIAL_NUM = 3;" + ) + } + + @TableTest( + """ + scenario | overlay | message + undeclared name | 'intercepted: [missing]' | is not declared + ref in overlay | 'tags: [{ref: service}]' | must be declarations, not refs + unknown section | 'span_types: {}' | may only declare + duplicate of a tag | 'tags: [{dd-name: service}]' | declared in both + """ + ) + fun `invalid tracer overlays are rejected`(overlay: String, message: String) { + val yaml = directory.conventionsFile("span_types: {base: {tags: [{dd-name: service}]}}") + val overlayFile = directory.writeFile("overlay.yaml", overlay) + + assertThatIllegalArgumentException() + .isThrownBy { TagRegistryGenerator.generate(yaml, File(directory, "generated"), overlayFile) } + .withMessageContaining(message) + } + private fun tagConventions(@Language("yaml") yamlText: String) = TagConventions.parse( ObjectMapper(YAMLFactory()).readValue( directory.conventionsFile(yamlText), diff --git a/internal-api/build.gradle.kts b/internal-api/build.gradle.kts index a9d82cebc72..9c877c4562d 100644 --- a/internal-api/build.gradle.kts +++ b/internal-api/build.gradle.kts @@ -266,6 +266,7 @@ extra["excludedClassesInstructionCoverage"] = listOf("datadog.trace.util.stacktr tagRegistry { tagConventionsFile.set(rootProject.layout.projectDirectory.file("tag-conventions.yaml")) + tracerOverlayFile.set(rootProject.layout.projectDirectory.file("tag-conventions-java.yaml")) } dependencies { diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index d84e1e6a537..4ea9b574a8a 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -15,15 +15,15 @@ public final class KnownTagCodec { * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] * are RESERVED and always zero here: they are the window the dense tag store uses for its * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is - * the trace/span LEVEL bit (set ⟹ trace-level); bits 1-0 are reserved. Unknown (string-only) - * custom tags are NOT known ids — {@code keyOf} returns 0 for them. + * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is + * reserved. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for + * them. * - *

An id says what a tag IS, not how it 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 — belongs - * to TagInterceptor, whose {@code needsIntercept} switch is the authority; mirroring it here as a - * classification bit and a serial-range tier only created drift between the two. That - * classification returns with the work that consumes it (the id→handler dispatch table that - * retires TagInterceptor), and re-adding a bit then is purely additive. + *

Bit 1 is the INTERCEPTED bit: set on a tag TagInterceptor may route on the set-path (to a + * span field or a sampling directive instead of tag storage). It is a hint, not a decision: the + * interceptor's switch says what each such tag does, and may still store it. Because a setter + * called with a constant id can test the bit at JIT time, the interception path folds away for + * every tag without it. * *

There is deliberately NO OpenTelemetry-applicability flag: an absent otel-name means * pass-through (the tag is emitted under its Datadog name), so today every known tag has an @@ -46,6 +46,17 @@ public static boolean isTraceLevel(long tagId) { return (tagId & LEVEL_TRACE) != 0L; } + /** + * INTERCEPTED bit (low-32 carve, bit 1). Set marks a tag TagInterceptor may route on the + * set-path. Declared in the tracer overlay ({@code tag-conventions-java.yaml}). + */ + public static final long INTERCEPTED = 1L << 1; + + /** True if the tagId names a tag TagInterceptor may route on the set-path. */ + public static boolean isIntercepted(long tagId) { + return (tagId & INTERCEPTED) != 0L; + } + /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; diff --git a/tag-conventions-java.yaml b/tag-conventions-java.yaml new file mode 100644 index 00000000000..c2304396785 --- /dev/null +++ b/tag-conventions-java.yaml @@ -0,0 +1,54 @@ +# Tracer overlay for tag-conventions.yaml — dd-trace-java's set-path routing. +# --------------------------------------------------------------------------- +# tag-conventions.yaml is language-agnostic; which keys a tracer intercepts on the set-path (routing +# them to a span field or a sampling directive instead of tag storage) is this tracer's concern, so +# it lives here. +# +# tags — keys that exist only to be routed, so the conventions do not declare them. They get +# ids like any other tag (instrumentation can set them by id) but belong to no span type. +# Same declaration syntax as tag-conventions.yaml; refs are not allowed. +# intercepted — Datadog names TagInterceptor may route, declared in either file. Each one's id carries +# the INTERCEPTED bit, so a setter called with a constant id folds the interception test +# away. TagInterceptor's switch is the authority on what each one does; a test there +# keeps this list and the switch in agreement. +# --------------------------------------------------------------------------- + +tags: + - { dd-name: resource.name, type: string } + - { dd-name: span.type, type: string } + - { dd-name: error, type: boolean } + - { dd-name: manual.keep, type: boolean } + - { dd-name: manual.drop, type: boolean } + - { dd-name: asm.keep, type: boolean } + - { dd-name: ai_guard.keep, type: boolean } + - { dd-name: sampling.priority, type: int } + - { dd-name: _dd.p.ts, type: int } + - { dd-name: _dd.p.debug, type: string } + - { dd-name: _dd.origin, type: string } + - { dd-name: _dd.measured, type: boolean } + - { dd-name: _dd1.sr.eausr, type: double } + +intercepted: + # declared above + - resource.name + - span.type + - error + - manual.keep + - manual.drop + - asm.keep + - ai_guard.keep + - sampling.priority + - _dd.p.ts + - _dd.p.debug + - _dd.origin + - _dd.measured + - _dd1.sr.eausr + # declared in tag-conventions.yaml + - service + - peer.service + - span.kind + - servlet.context + - db.statement + - http.method + - http.url + - http.status_code From 8949a571475f87cf627bbf4b2c624beece16cde7 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 10:38:27 -0400 Subject: [PATCH 06/35] Dispatch TagInterceptor on tag ids TagInterceptor switches on the tag's serial rather than its name, so a known tag is matched under any of its names and the *_OTEL_NAME cases go away. needsIntercept(long) tests the INTERCEPTED bit first, which folds away for a constant id. split-by-tags entries resolve to serials at construction (each direction for a name declared per direction); only custom tags are still matched by name. A test checks that exactly the tags carrying the bit have a case. Co-Authored-By: Claude Opus 5.5 --- .../core/taginterceptor/TagInterceptor.java | 218 ++++++++++-------- .../taginterceptor/TagInterceptorTest.java | 81 +++++++ 2 files changed, 209 insertions(+), 90 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 89e5a01adb8..ca552cd8541 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -1,19 +1,11 @@ package datadog.trace.core.taginterceptor; import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; -import static datadog.trace.api.DDTags.MEASURED; -import static datadog.trace.api.DDTags.ORIGIN_KEY; -import static datadog.trace.api.DDTags.SPAN_TYPE; -import static datadog.trace.api.KnownTags.DB_STATEMENT_OTEL_NAME; -import static datadog.trace.api.KnownTags.HTTP_METHOD_OTEL_NAME; -import static datadog.trace.api.KnownTags.HTTP_STATUS_CODE_OTEL_NAME; -import static datadog.trace.api.KnownTags.HTTP_URL_OTEL_NAME; +import static datadog.trace.api.KnownTags.SERVLET_CONTEXT_ID; import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.SERVLET_CONTEXT; import static datadog.trace.bootstrap.instrumentation.api.ServiceNameSources.SPLIT_BY_SERVLET_CONTEXT; import static datadog.trace.bootstrap.instrumentation.api.ServiceNameSources.SPLIT_BY_TAGS; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_STATUS; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; import static datadog.trace.core.taginterceptor.RuleFlags.Feature.FORCE_MANUAL_DROP; import static datadog.trace.core.taginterceptor.RuleFlags.Feature.FORCE_SAMPLING_PRIORITY; @@ -27,6 +19,8 @@ import datadog.trace.api.Config; import datadog.trace.api.ConfigDefaults; import datadog.trace.api.DDTags; +import datadog.trace.api.KnownTagCodec; +import datadog.trace.api.KnownTags; import datadog.trace.api.Pair; import datadog.trace.api.TagMap; import datadog.trace.api.config.GeneralConfig; @@ -41,6 +35,8 @@ import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; import java.net.URI; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; @@ -54,7 +50,12 @@ public class TagInterceptor { private final boolean isServiceNameSetByUser; private final boolean splitByServletContext; private final String inferredServiceName; - private final Set splitServiceTags; + + /** split-by-tags entries naming known tags, indexed by serial; null when there are none. */ + private final boolean[] splitServiceSerials; + + /** split-by-tags entries naming custom tags; null when there are none. */ + private final Set splitServiceCustomNames; private final boolean shouldSet404ResourceName; private final boolean shouldSetUrlResourceAsName; @@ -77,9 +78,10 @@ public TagInterceptor( boolean jeeSplitByDeployment) { this.isServiceNameSetByUser = isServiceNameSetByUser; this.inferredServiceName = inferredServiceName; - this.splitServiceTags = splitServiceTags; + this.splitServiceSerials = knownTagSerials(splitServiceTags); + this.splitServiceCustomNames = customTagNames(splitServiceTags); this.ruleFlags = ruleFlags; - splitByServletContext = splitServiceTags.contains(SERVLET_CONTEXT); + splitByServletContext = isSplitServiceTag(SERVLET_CONTEXT_ID); shouldSet404ResourceName = ruleFlags.isEnabled(URL_AS_RESOURCE_NAME) @@ -89,9 +91,63 @@ public TagInterceptor( this.jeeSplitByDeployment = jeeSplitByDeployment; } + /** + * Marks, by serial, each known tag that {@code names} names under any of its names, so a tag set + * by id or by its OpenTelemetry name matches too. A name declared per direction marks each + * direction's tag. Returns null when no name is a known tag's. + */ + private static boolean[] knownTagSerials(Set names) { + if (names.isEmpty()) { + return null; + } + Set canonicalNames = new HashSet<>(); + for (String name : names) { + canonicalNames.add(KnownTagCodec.canonicalTagName(name)); + } + boolean[] serials = null; + for (int serial = 1; ; ++serial) { + String name = KnownTagCodec.nameOf(KnownTagCodec.makeTagId(serial)); + if (name == null) { + return serials; + } + if (canonicalNames.contains(name)) { + if (serials == null || serial >= serials.length) { + serials = serials == null ? new boolean[serial + 1] : Arrays.copyOf(serials, serial + 1); + } + serials[serial] = true; + } + } + } + + /** The names in {@code names} that do not resolve to a known tag, or null when there are none. */ + private static Set customTagNames(Set names) { + Set custom = null; + for (String name : names) { + if (KnownTagCodec.keyOf(name) == 0) { + if (custom == null) { + custom = new HashSet<>(); + } + custom.add(name); + } + } + return custom; + } + + private boolean isSplitServiceTag(long tagId) { + boolean[] serials = splitServiceSerials; + int serial = KnownTagCodec.serialNum(tagId); + return serials != null && serial < serials.length && serials[serial]; + } + + private boolean isSplitServiceTag(String customTag) { + Set names = splitServiceCustomNames; + return names != null && names.contains(customTag); + } + public boolean needsIntercept(TagMap map) { for (TagMap.EntryReader entry : map) { - if (needsIntercept(entry.tag())) return true; + long tagId = entry.tagId(); + if (tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(entry.tag())) return true; } return false; } @@ -103,131 +159,121 @@ public boolean needsIntercept(Map map) { return false; } - public boolean needsIntercept(String tag) { - switch (tag) { - case DDTags.RESOURCE_NAME: - case Tags.DB_STATEMENT: - case DB_STATEMENT_OTEL_NAME: - case DDTags.SERVICE_NAME: - case "service": - case Tags.PEER_SERVICE: - case DDTags.MANUAL_KEEP: - case DDTags.MANUAL_DROP: - case Tags.ASM_KEEP: - case Tags.AI_GUARD_KEEP: - case Tags.SAMPLING_PRIORITY: - case Tags.PROPAGATED_TRACE_SOURCE: - case Tags.PROPAGATED_DEBUG: - case SERVLET_CONTEXT: - case SPAN_TYPE: - case ANALYTICS_SAMPLE_RATE: - case Tags.ERROR: - case HTTP_STATUS: - case HTTP_STATUS_CODE_OTEL_NAME: - case HTTP_METHOD: - case HTTP_METHOD_OTEL_NAME: - case HTTP_URL: - case HTTP_URL_OTEL_NAME: - case ORIGIN_KEY: - case MEASURED: - case Tags.SPAN_KIND: - return true; + /** + * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag. Called with a + * constant id, the {@link KnownTagCodec#INTERCEPTED} test folds away; only a configured + * split-by-tags is left to check at run time. + */ + public boolean needsIntercept(long tagId) { + return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); + } - default: - return splitServiceTags.contains(tag); - } + public boolean needsIntercept(String tag) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(tag); } public boolean interceptTag(DDSpanContext span, String tag, Object value) { - switch (tag) { - case DDTags.RESOURCE_NAME: + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? interceptTag(span, tagId, value) : interceptCustomTag(span, tag, value); + } + + /** + * Routes a known tag to a span field or a sampling directive. Returns true when the tag was + * consumed, false when it should still be stored. Every tag with a case here carries the {@link + * KnownTagCodec#INTERCEPTED} bit, declared in {@code tag-conventions-java.yaml}. + */ + public boolean interceptTag(DDSpanContext span, long tagId, Object value) { + switch (KnownTagCodec.serialNum(tagId)) { + case KnownTags.RESOURCE_NAME_SERIAL_NUM: return interceptResourceName(span, value); - case Tags.DB_STATEMENT: - case DB_STATEMENT_OTEL_NAME: + case KnownTags.DB_STATEMENT_SERIAL_NUM: return interceptDbStatement(span, value); - case DDTags.SERVICE_NAME: - case "service": + case KnownTags.SERVICE_SERIAL_NUM: return interceptServiceName(SERVICE_NAME, span, value); - case Tags.PEER_SERVICE: + case KnownTags.PEER_SERVICE_SERIAL_NUM: // we still need to intercept and add this tag when the user manually set span.setTag(DDTags.PEER_SERVICE_SOURCE, Tags.PEER_SERVICE); return interceptServiceName(PEER_SERVICE, span, value); - case DDTags.MANUAL_KEEP: + case KnownTags.MANUAL_KEEP_SERIAL_NUM: if (asBoolean(value)) { span.forceKeep(); return true; } return false; - case DDTags.MANUAL_DROP: + case KnownTags.MANUAL_DROP_SERIAL_NUM: return interceptSamplingPriority( FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); - case Tags.ASM_KEEP: + case KnownTags.ASM_KEEP_SERIAL_NUM: if (asBoolean(value)) { span.forceKeep(SamplingMechanism.APPSEC); return true; } return false; - case Tags.AI_GUARD_KEEP: + case KnownTags.AI_GUARD_KEEP_SERIAL_NUM: if (asBoolean(value)) { span.forceKeep(SamplingMechanism.AI_GUARD); return true; } return false; - case Tags.SAMPLING_PRIORITY: + case KnownTags.SAMPLING_PRIORITY_SERIAL_NUM: return interceptSamplingPriority(span, value); - case Tags.PROPAGATED_TRACE_SOURCE: + case KnownTags.DD_P_TS_SERIAL_NUM: if (value instanceof Integer) { span.addPropagatedTraceSource((Integer) value); return true; } return false; - case Tags.PROPAGATED_DEBUG: + case KnownTags.DD_P_DEBUG_SERIAL_NUM: span.updateDebugPropagation(String.valueOf(value)); return true; - case SERVLET_CONTEXT: + case KnownTags.SERVLET_CONTEXT_SERIAL_NUM: return interceptServletContext(span, value); - case SPAN_TYPE: + case KnownTags.SPAN_TYPE_SERIAL_NUM: return interceptSpanType(span, value); - case ANALYTICS_SAMPLE_RATE: + case KnownTags.DD1_SR_EAUSR_SERIAL_NUM: return interceptAnalyticsSampleRate(span, value); - case Tags.ERROR: + case KnownTags.ERROR_SERIAL_NUM: return interceptError(span, value); - case HTTP_STATUS: - case HTTP_STATUS_CODE_OTEL_NAME: + case KnownTags.HTTP_STATUS_CODE_SERIAL_NUM: // not set internally but may come from manual instrumentation return interceptHttpStatusCode(span, value); - case HTTP_METHOD: - case HTTP_METHOD_OTEL_NAME: - case HTTP_URL: - case HTTP_URL_OTEL_NAME: - return interceptUrlResourceAsNameRule(span, tag, value); - case ORIGIN_KEY: + case KnownTags.HTTP_METHOD_SERIAL_NUM: + case KnownTags.HTTP_URL_SERIAL_NUM: + return interceptUrlResourceAsNameRule(span, tagId, value); + case KnownTags.DD_ORIGIN_SERIAL_NUM: return interceptOrigin(span, value); - case MEASURED: + case KnownTags.DD_MEASURED_SERIAL_NUM: return interceptMeasured(span, value); - case Tags.SPAN_KIND: + case KnownTags.SPAN_KIND_SERIAL_NUM: // Cache the ordinal for fast isOutbound() checks. // Return false so the value is still stored in unsafeTags for serialization. span.setSpanKindOrdinal(String.valueOf(value)); return false; default: - return intercept(span, tag, value); + return isSplitServiceTag(tagId) && splitService(span, value); } } - private boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { + private boolean interceptCustomTag(DDSpanContext span, String tag, Object value) { + return isSplitServiceTag(tag) && splitService(span, value); + } + + private static boolean splitService(DDSpanContext span, Object value) { + span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); + return true; + } + + private boolean interceptUrlResourceAsNameRule(DDSpanContext span, long tagId, Object value) { if (shouldSetUrlResourceAsName) { - if (HTTP_METHOD.equals(tag) || HTTP_METHOD_OTEL_NAME.equals(tag)) { + // Values are stored under their Datadog name, whichever spelling set them. + if (tagId == KnownTags.HTTP_METHOD_ID) { final Object url = span.unsafeGetTag(HTTP_URL); if (url != null) { setResourceFromUrl(span, value.toString(), url); } - } else if (HTTP_URL.equals(tag) || HTTP_URL_OTEL_NAME.equals(tag)) { - // the method may have been set under either spelling -- see HTTP_METHOD_OTEL_NAME. - Object method = span.unsafeGetTag(HTTP_METHOD); - if (method == null) { - method = span.unsafeGetTag(HTTP_METHOD_OTEL_NAME); - } + } else { + final Object method = span.unsafeGetTag(HTTP_METHOD); setResourceFromUrl(span, method != null ? method.toString() : null, value); } } @@ -258,14 +304,6 @@ private static void setResourceFromUrl( } } - private boolean intercept(DDSpanContext span, String tag, Object value) { - if (splitServiceTags.contains(tag)) { - span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); - return true; - } - return false; - } - private boolean interceptResourceName(DDSpanContext span, Object value) { if (ruleFlags.isEnabled(RESOURCE_NAME)) { if (null == value) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 91f3da0910f..1b1a925b856 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -25,6 +26,8 @@ import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; +import datadog.trace.api.KnownTagCodec; +import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; import datadog.trace.api.env.CapturedEnvironment; import datadog.trace.api.remoteconfig.ServiceNameCollector; @@ -34,6 +37,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; +import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.sampling.AllSampler; import datadog.trace.common.writer.ListWriter; @@ -48,6 +52,7 @@ import datadog.trace.test.junit.utils.config.WithConfig; import datadog.trace.test.junit.utils.converter.ConfigDefaultsConverter; import datadog.trace.test.junit.utils.converter.TagsConverter; +import java.lang.reflect.Field; import java.util.Collections; import java.util.Map; import java.util.function.Function; @@ -905,4 +910,80 @@ void whenInterceptsProductTraceSourcePropagationTagUpdatePropagatedTraceSourceIs verify(context, times(1)).addPropagatedTraceSource(ProductTraceSource.ASM); } + + @Test + void exactlyTheTagsWithTheInterceptedBitHaveACase() throws IllegalAccessException { + RuleFlags ruleFlags = mock(RuleFlags.class); + when(ruleFlags.isEnabled(any())).thenReturn(true); + TagInterceptor interceptor = + new TagInterceptor(false, "my-service", emptySet(), ruleFlags, false); + + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) { + continue; + } + long tagId = field.getLong(null); + DDSpanContext span = mock(DDSpanContext.class); + when(span.getServiceName()).thenReturn(""); + + // Each case consumes one of these values or acts on the span for it; with no split-by-tags, + // the default case does neither. + boolean consumed = false; + for (Object value : new Object[] {true, 1, "1"}) { + consumed |= interceptor.interceptTag(span, tagId, value); + } + + boolean hasCase = consumed || !mockingDetails(span).getInvocations().isEmpty(); + assertEquals(KnownTagCodec.isIntercepted(tagId), hasCase, field.getName()); + assertEquals(KnownTagCodec.isIntercepted(tagId), interceptor.needsIntercept(tagId)); + } + } + + @TableTest({ + "scenario | configured | tag ", + "configured by Datadog name | db.type | db.type ", + "configured by OTel name | db.system | db.type ", + "set by OTel name | db.type | db.system" + }) + void splitByTagsMatchesAKnownTagByIdUnderAnyName(String configured, String tag) { + TagInterceptor interceptor = splittingInterceptor(configured); + DDSpanContext span = mock(DDSpanContext.class); + + assertTrue(interceptor.needsIntercept(KnownTags.DB_TYPE_ID)); + assertTrue(interceptor.needsIntercept(tag)); + assertFalse(interceptor.needsIntercept(KnownTags.COMPONENT_ID)); + assertTrue(interceptor.interceptTag(span, KnownTags.DB_TYPE_ID, "postgres")); + assertTrue(interceptor.interceptTag(span, tag, "mysql")); + + verify(span).setServiceName("postgres", ServiceNameSources.SPLIT_BY_TAGS); + verify(span).setServiceName("mysql", ServiceNameSources.SPLIT_BY_TAGS); + } + + @Test + void splitByTagsMatchesACustomTagByName() { + TagInterceptor interceptor = splittingInterceptor("my.custom.tag"); + DDSpanContext span = mock(DDSpanContext.class); + + assertTrue(interceptor.needsIntercept("my.custom.tag")); + assertFalse(interceptor.needsIntercept("other.custom.tag")); + assertFalse(interceptor.needsIntercept(KnownTags.COMPONENT_ID)); + assertTrue(interceptor.interceptTag(span, "my.custom.tag", "split")); + assertFalse(interceptor.interceptTag(span, "other.custom.tag", "split")); + + verify(span).setServiceName("split", ServiceNameSources.SPLIT_BY_TAGS); + } + + @Test + void splitByTagsOnANameDeclaredPerDirectionMatchesEachDirection() { + TagInterceptor interceptor = splittingInterceptor(Tags.PEER_PORT); + + assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_INBOUND_ID)); + assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertTrue(interceptor.needsIntercept(Tags.PEER_PORT)); + } + + private static TagInterceptor splittingInterceptor(String splitByTag) { + return new TagInterceptor( + true, "my-service", Collections.singleton(splitByTag), new RuleFlags(), false); + } } From cfe23689e3476019e21c7fff178fc39f58f76bc9 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 10:44:20 -0400 Subject: [PATCH 07/35] Set tags on spans by id first A name setter resolves a known tag's name to its id once and sets it as the id-keyed setters do; only a custom tag is still handled by name. The id-keyed setters no longer look up the name to feed the interceptor, so with a constant id the interception test folds to the INTERCEPTED bit. Entry paths (builder ledger, prototypes, default tags) route by the entry's id and precheck before boxing. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 150 +++++++++--------- .../core/taginterceptor/TagInterceptor.java | 30 +++- 2 files changed, 101 insertions(+), 79 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 94218f92916..ecf25e717d3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -648,8 +648,7 @@ private void seedAbsentTags(final TagMap map) { if (ctx.unsafeTags.containsKey(tag)) { return; } - final Object value = tagEntry.objectValue(); - if (!ctx.tagInterceptor.interceptTag(ctx, tag, value)) { + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { ctx.unsafeTags.set(tagEntry); } }); @@ -1018,6 +1017,9 @@ public void removeTag(long tagId) { *

Existing tag value with the same value will be replaced. Setting a tag with a {@code null} * value will remove the tag from the span. * + *

A known tag's name is resolved to its id once, and the tag is then set as the id-keyed + * setters set it; only a custom tag is handled by name. + * * @param tag The tag name. * @param value The nullable tag value. */ @@ -1025,9 +1027,12 @@ public void setTag(final String tag, final Object value) { if (null == tag) { return; } - if (null == value) { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1038,9 +1043,12 @@ public void setTag(final String tag, final String value) { if (null == tag) { return; } - if (null == value) { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1048,18 +1056,18 @@ public void setTag(final String tag, final String value) { } /* - * Id-keyed setters, mirroring the String setters above: interception still runs on the tag's - * name (TagInterceptor is name-keyed), but a stored tag is set by id, skipping the name lookup. - * An id that names no known tag is ignored, like a null tag. + * Id-keyed setters, mirroring the String setters above. With a constant id, the interception + * test folds to the KnownTagCodec.INTERCEPTED bit, and there is no custom-tag path to take. An id + * that names no known tag is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.needsIntercept(tagId) + || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { unsafeTags.set(tagId, value); } @@ -1067,13 +1075,13 @@ public void setTag(final long tagId, final Object value) { } public void setTag(final long tagId, final CharSequence value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + } else if (!tagInterceptor.needsIntercept(tagId) + || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { unsafeTags.set(tagId, value); } @@ -1081,12 +1089,11 @@ public void setTag(final long tagId, final CharSequence value) { } public void setTag(final long tagId, final boolean value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1095,12 +1102,11 @@ public void setTag(final long tagId, final boolean value) { } public void setTag(final long tagId, final int value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1109,12 +1115,11 @@ public void setTag(final long tagId, final int value) { } public void setTag(final long tagId, final long value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1123,12 +1128,11 @@ public void setTag(final long tagId, final long value) { } public void setTag(final long tagId, final float value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1137,12 +1141,11 @@ public void setTag(final long tagId, final float value) { } public void setTag(final long tagId, final double value) { - final String tag = KnownTagCodec.nameOf(tagId); - if (null == tag) { + if (isUnknownTag(tagId)) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + if (tagInterceptor.needsIntercept(tagId)) { + this.setBox(tagId, value); } else { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1150,16 +1153,17 @@ public void setTag(final long tagId, final double value) { } } + private static boolean isUnknownTag(long tagId) { + return KnownTagCodec.nameOf(tagId) == null; + } + public void setTag(TagMap.EntryReader entry) { if (entry == null) { return; } - // pre-check to avoid boxing - boolean intercepted = - precheckIntercept(entry.tag()) - && tagInterceptor.interceptTag(this, entry.tag(), entry.objectValue()); - if (!intercepted) { + // the interceptor prechecks the entry, to avoid boxing + if (!tagInterceptor.interceptTag(this, entry)) { synchronized (unsafeTags) { unsafeTags.set(entry); } @@ -1167,16 +1171,7 @@ public void setTag(TagMap.EntryReader entry) { } /* - * Uses to determine if there's an opportunity to avoid primitve boxing. - * If the underlying map doesn't support efficient primitives, then boxing is used. - * If the tag may be intercepted, then boxing is also used. - */ - private boolean precheckIntercept(String tag) { - return tagInterceptor.needsIntercept(tag); - } - - /* - * Used when precheckIntercept determines that boxing is unavoidable + * Used when the interceptor's precheck determines that boxing is unavoidable * * Either because the tagInterceptor needs to be fully checked (which requires boxing) * In that case, a box has already been created so it makes sense to pass the box @@ -1187,10 +1182,10 @@ private boolean precheckIntercept(String tag) { * The TagMap isn't optimized and will need to box the primitive regardless of * tag interception */ - private void setBox(String tag, Object box) { - if (!tagInterceptor.interceptTag(this, tag, box)) { + private void setBox(long tagId, Object box) { + if (!tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.set(tag, box); + unsafeTags.set(tagId, box); } } } @@ -1199,9 +1194,11 @@ public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1212,9 +1209,11 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1225,9 +1224,11 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1238,9 +1239,11 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1251,9 +1254,11 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); - } else { + final long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + setTag(tagId, value); + } else if (!tagInterceptor.needsInterceptCustomTag(tag) + || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -1277,10 +1282,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { map.forEach( this, (ctx, tagEntry) -> { - String tag = tagEntry.tag(); - Object value = tagEntry.objectValue(); - - if (!ctx.tagInterceptor.interceptTag(ctx, tag, value)) { + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { ctx.unsafeTags.set(tagEntry); } }); @@ -1307,9 +1309,7 @@ void setAllTags(final TagMap.Ledger ledger) { } else { TagMap.Entry entry = (TagMap.Entry) entryChange; - Object value = entry.objectValue(); - - if (!tagInterceptor.interceptTag(this, tag, value)) { + if (!tagInterceptor.interceptTag(this, entry)) { unsafeTags.set(entry); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index ca552cd8541..95d11a4eea5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -146,12 +146,21 @@ private boolean isSplitServiceTag(String customTag) { public boolean needsIntercept(TagMap map) { for (TagMap.EntryReader entry : map) { - long tagId = entry.tagId(); - if (tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(entry.tag())) return true; + if (needsIntercept(entry)) return true; } return false; } + public boolean needsIntercept(TagMap.EntryReader entry) { + long tagId = entry.tagId(); + return tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(entry.tag()); + } + + /** Whether a custom tag -- one with no id -- may be routed: only split-by-tags routes one. */ + public boolean needsInterceptCustomTag(String customTag) { + return isSplitServiceTag(customTag); + } + public boolean needsIntercept(Map map) { for (String tag : map.keySet()) { if (needsIntercept(tag)) return true; @@ -255,8 +264,21 @@ public boolean interceptTag(DDSpanContext span, long tagId, Object value) { } } - private boolean interceptCustomTag(DDSpanContext span, String tag, Object value) { - return isSplitServiceTag(tag) && splitService(span, value); + /** + * Routes an entry by its id, or by its name when it is a custom tag. Prechecks, so a primitive + * entry that needs no interception is never boxed. + */ + public boolean interceptTag(DDSpanContext span, TagMap.EntryReader entry) { + long tagId = entry.tagId(); + if (tagId != 0) { + return needsIntercept(tagId) && interceptTag(span, tagId, entry.objectValue()); + } + return isSplitServiceTag(entry.tag()) && splitService(span, entry.objectValue()); + } + + /** Routes a custom tag -- one with no id. */ + public boolean interceptCustomTag(DDSpanContext span, String customTag, Object value) { + return isSplitServiceTag(customTag) && splitService(span, value); } private static boolean splitService(DDSpanContext span, Object value) { From 38c9947a5185a8f9bfc538912cdbdc20d33d4eb0 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 11:10:21 -0400 Subject: [PATCH 08/35] Look up tag names by serial in an array, not a switch The generated nameOf switched over every tag's serial: 491 bytes of bytecode, too big for C2 to inline, so each id-keyed set paid two out-of-line calls into it (the span's unknown-id guard and the entry's name). Read a NAMES_BY_SERIAL array instead; nameOf is now 23 bytes and inlines at every caller. Also adds SetTagBenchmark (span setTag by constant id, non-constant id, name, and custom name, against a bare and a synchronized TagMap store). Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 37 +++--- .../datadog/trace/core/SetTagBenchmark.java | 106 ++++++++++++++++++ 2 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java 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 ca81859af96..b04a52c9378 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 @@ -165,6 +165,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↔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 @@ -174,26 +189,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)) { diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java new file mode 100644 index 00000000000..ac3799860b7 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java @@ -0,0 +1,106 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import datadog.trace.api.KnownTags; +import datadog.trace.api.TagMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Measures {@code DDSpan.setTag} on a live span, overwriting one tag per call, by how the tag is + * named: by a constant id, by a known tag's name, and by a custom name. {@code tagMap_byId} is the + * floor -- the same store on a bare {@link TagMap}, with no span, lock, or interceptor. + * + *

A constant id should cost close to the floor plus the span's lock: its interception test folds + * to the id's INTERCEPTED bit, and an id set has no custom-tag path. The name variants pay a + * registry lookup first. + * + *

Two controls separate those costs. {@code knownTag_byNonConstantId} reads the id from a + * non-final field, which C2 never folds, so nothing about the id is known at compile time. {@code + * tagMap_byId_synchronized} is the floor plus an uncontended lock, as the span takes one. + */ +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@Fork(value = 3) +public class SetTagBenchmark { + static final CoreTracer TRACER = CoreTracer.builder().build(); + + private DDSpan span; + private TagMap tagMap; + private long nonConstantTagId; + + @Setup + public void setup() { + span = (DDSpan) TRACER.startSpan("benchmark", "set.tag"); + tagMap = TagMap.create(); + nonConstantTagId = KnownTags.HTTP_ROUTE_ID; + } + + @TearDown + public void tearDown() { + span.finish(); + } + + @Benchmark + public Object tagMap_byId() { + tagMap.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + return tagMap; + } + + @Benchmark + public Object tagMap_byId_synchronized() { + TagMap tagMap = this.tagMap; + synchronized (tagMap) { + tagMap.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + } + return tagMap; + } + + @Benchmark + public Object knownTag_byNonConstantId() { + return span.setTag(nonConstantTagId, "/users/{id}"); + } + + @Benchmark + public Object knownTag_byId() { + return span.setTag(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + } + + @Benchmark + public Object interceptedTag_byId() { + return span.setTag(KnownTags.SPAN_KIND_ID, "client"); + } + + @Benchmark + public Object knownTag_byName() { + return span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}"); + } + + @Benchmark + public Object customTag_byName() { + return span.setTag("my.custom.tag", "/users/{id}"); + } + + @Benchmark + public Object knownIntTag_byId() { + return span.setTag(KnownTags.HTTP_RESEND_COUNT_ID, 2); + } + + @Benchmark + public Object knownIntTag_byName() { + return span.setTag(KnownTags.HTTP_RESEND_COUNT_NAME, 2); + } +} From 3545c1544b9a58c868a76da8fde3d15a7fdcec85 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 11:35:58 -0400 Subject: [PATCH 09/35] Point stale comments at the tracer overlay and the intercepted bit Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/core/DDSpanContext.java | 6 +++--- tag-conventions.yaml | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index ecf25e717d3..ac473fd18cb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -611,9 +611,9 @@ public void setSpanType(final CharSequence spanType) { *

This is the shared seam for both the construction path ({@code CoreSpanBuilder}) and * decorator {@code afterStart} (via {@link DDSpan#apply}). The context owns the tag map, so the * eventual cheaper bulk-share path (skipping interception for non-intercepted tags) and the - * identity short-circuit will land here -- deferred to the dense-store / tag-registry work, which - * exposes intercept status at the internal-api level. Until then the constant tags route through - * the interceptor, identical to the per-tag calls this replaces. + * identity short-circuit will land here, with the dense store; a tag's intercept status is + * already on its id ({@link KnownTagCodec#isIntercepted}). Until then each constant tag is + * prechecked by the interceptor, identical to the per-tag calls this replaces. */ public void apply(@Nonnull final SpanPrototype prototype) { if (this.spanType == null) { diff --git a/tag-conventions.yaml b/tag-conventions.yaml index b6cdfe5deb4..c4a5f9f9b70 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -200,8 +200,9 @@ mixins: # - span.kind enumerates: server | client | producer | consumer | internal | broker. # - Some keys (resource.name, error, sampling.priority, ...) are accepted by setTag but routed to a # span field or a trace directive instead of tag storage. That routing is a per-language tracer -# concern, so it is NOT modelled here; such a key appears above only when it also needs an id and -# a name (service does, for OpenTelemetry's service.name). +# concern, so it is NOT modelled here: dd-trace-java declares those keys, and marks every tag it +# intercepts, in its own overlay (tag-conventions-java.yaml). Such a key appears above only when it +# also needs an id and a name across tracers (service does, for OpenTelemetry's service.name). # - Tags with no otel-name pass through under their Datadog name (RFC "retain"). A `# TODO(otel)` note # marks a pending OpenTelemetry-team review of a mapping that is not yet a settled rename. # - db.statement/db.query.text is deliberately never re-exposed as a stored tag once consumed into From 1642cc9b5cb536185dd85ac38e93bada9ccbef0d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 12:03:52 -0400 Subject: [PATCH 10/35] Index the split-by-tags table by serial, sized to every known tag The table is always allocated and never resized, so the split check -- the only run-time check left on a non-intercepted constant-id set -- is a single load, with no null or length test. Co-Authored-By: Claude Opus 5.5 --- .../core/taginterceptor/TagInterceptor.java | 36 ++++++++----------- .../java/datadog/trace/api/KnownTagCodec.java | 8 +++++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 95d11a4eea5..181889cb9a0 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -35,7 +35,6 @@ import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; import java.net.URI; -import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -51,7 +50,10 @@ public class TagInterceptor { private final boolean splitByServletContext; private final String inferredServiceName; - /** split-by-tags entries naming known tags, indexed by serial; null when there are none. */ + /** + * Whether each known tag, indexed by serial, is a split-by-tags entry. Sized to every known + * serial and never changed after construction, so the check is a single load. + */ private final boolean[] splitServiceSerials; /** split-by-tags entries naming custom tags; null when there are none. */ @@ -94,29 +96,22 @@ public TagInterceptor( /** * Marks, by serial, each known tag that {@code names} names under any of its names, so a tag set * by id or by its OpenTelemetry name matches too. A name declared per direction marks each - * direction's tag. Returns null when no name is a known tag's. + * direction's tag. */ private static boolean[] knownTagSerials(Set names) { + boolean[] serials = new boolean[KnownTagCodec.serialLimit()]; if (names.isEmpty()) { - return null; + return serials; } Set canonicalNames = new HashSet<>(); for (String name : names) { canonicalNames.add(KnownTagCodec.canonicalTagName(name)); } - boolean[] serials = null; - for (int serial = 1; ; ++serial) { - String name = KnownTagCodec.nameOf(KnownTagCodec.makeTagId(serial)); - if (name == null) { - return serials; - } - if (canonicalNames.contains(name)) { - if (serials == null || serial >= serials.length) { - serials = serials == null ? new boolean[serial + 1] : Arrays.copyOf(serials, serial + 1); - } - serials[serial] = true; - } + for (int serial = 1; serial < serials.length; ++serial) { + serials[serial] = + canonicalNames.contains(KnownTagCodec.nameOf(KnownTagCodec.makeTagId(serial))); } + return serials; } /** The names in {@code names} that do not resolve to a known tag, or null when there are none. */ @@ -133,10 +128,9 @@ private static Set customTagNames(Set names) { return custom; } + /** {@code tagId} is a known id, so its serial is in range. */ private boolean isSplitServiceTag(long tagId) { - boolean[] serials = splitServiceSerials; - int serial = KnownTagCodec.serialNum(tagId); - return serials != null && serial < serials.length && serials[serial]; + return splitServiceSerials[KnownTagCodec.serialNum(tagId)]; } private boolean isSplitServiceTag(String customTag) { @@ -170,8 +164,8 @@ public boolean needsIntercept(Map map) { /** * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag. Called with a - * constant id, the {@link KnownTagCodec#INTERCEPTED} test folds away; only a configured - * split-by-tags is left to check at run time. + * constant id, the {@link KnownTagCodec#INTERCEPTED} test folds away; only the split-by-tags + * table is left to check at run time. {@code tagId} must be a known id. */ public boolean needsIntercept(long tagId) { return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 4ea9b574a8a..953b62aa16e 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -127,6 +127,14 @@ public static String openTelemetryTagOf(long tagId) { return otelName != null ? otelName : resolver.nameOf(tagId); } + /** + * One past the largest known serial: an array of this length, indexed by {@link #serialNum}, has + * a slot for every known tag (slot 0 is no tag). + */ + public static int serialLimit() { + return KnownTags.NAMES_BY_SERIAL.length; + } + /** The id for {@code name} in any namespace, or 0 when it is not a known tag. */ public static long keyOf(String name) { return Installed.RESOLVER.keyOf(name); From b9dd5731bb73ff679763484cc9a53287e9aa9673 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 13:07:31 -0400 Subject: [PATCH 11/35] Check for a known tag id with a range check, not a name lookup The span's unknown-id guard called nameOf; KnownTagCodec.isKnown answers the same question from the serial and a generated SERIAL_LIMIT, so it folds away for a constant id. Also note that serials are not stable across releases. Co-Authored-By: Claude Opus 5.5 --- .../buildlogic/tagRegistry/KnownTagsEmitter.kt | 3 +++ .../tagRegistry/TagRegistryGeneratorTest.kt | 3 ++- .../java/datadog/trace/core/DDSpanContext.java | 4 ++-- .../main/java/datadog/trace/api/KnownTagCodec.java | 14 +++++++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) 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 b04a52c9378..8a21f8e0045 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 @@ -113,6 +113,9 @@ object KnownTagsEmitter { for (t in reg.tags) { appendLine(" public static final int ${serialC(t.identity)} = ${t.serial};") } + // 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. diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index e4fdba9d853..256367ccc0e 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -505,7 +505,8 @@ class TagRegistryGeneratorTest { "COMPONENT_ID = 0x0001000000000000L", "RESOURCE_NAME_ID = 0x0002000000000002L", "SERVICE_ID = 0x0003000000000002L", - "public static final int SERVICE_SERIAL_NUM = 3;" + "public static final int SERVICE_SERIAL_NUM = 3;", + "public static final int SERIAL_LIMIT = 4;" ) } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index ac473fd18cb..406135760de 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1000,7 +1000,7 @@ public void removeTag(String tag) { * id that names no known tag is ignored, like the id-keyed setters do. */ public void removeTag(long tagId) { - if (KnownTagCodec.nameOf(tagId) == null) { + if (isUnknownTag(tagId)) { return; } if (tagId == KnownTags.SPAN_KIND_ID) { @@ -1154,7 +1154,7 @@ public void setTag(final long tagId, final double value) { } private static boolean isUnknownTag(long tagId) { - return KnownTagCodec.nameOf(tagId) == null; + return !KnownTagCodec.isKnown(tagId); } public void setTag(TagMap.EntryReader entry) { diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 953b62aa16e..faf336cc40f 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -28,6 +28,9 @@ public final class KnownTagCodec { *

There is deliberately NO OpenTelemetry-applicability flag: an absent otel-name means * pass-through (the tag is emitted under its Datadog name), so today every known tag has an * OpenTelemetry name and such a flag would be constant. It returns once a Datadog-only tag exists. + * + *

Serials are assigned at build time and are NOT stable across releases: they follow the tags' + * names in order, so adding a tag renumbers others. Never persist or transmit a raw id. */ public static int serialNum(long tagId) { return (int) (tagId >>> 48); @@ -132,7 +135,16 @@ public static String openTelemetryTagOf(long tagId) { * a slot for every known tag (slot 0 is no tag). */ public static int serialLimit() { - return KnownTags.NAMES_BY_SERIAL.length; + return KnownTags.SERIAL_LIMIT; + } + + /** + * True if {@code tagId} names a known tag -- the same answer as {@code nameOf(tagId) != null}, as + * a range check on its serial, so it folds away for a constant id. + */ + public static boolean isKnown(long tagId) { + int serial = serialNum(tagId); + return serial != 0 && serial < KnownTags.SERIAL_LIMIT; // serial 0 is no tag } /** The id for {@code name} in any namespace, or 0 when it is not a known tag. */ From 75d3fbc52247934accf571c081441a4fdc2310ff Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 13:07:33 -0400 Subject: [PATCH 12/35] Pin interceptTag(long) above C2's inlining size DDSpanContext.setTag calls interceptTag only for intercepted tags. Kept out of line (over FreqInlineSize, 325 bytes of bytecode), it never brings the profiled handler bodies into setTag's compiled code, which would push setTag past InlineSmallCode and stop callers inlining it -- the inlining a constant id needs to fold its interception test. A test now fails if the switch shrinks below the limit. Co-Authored-By: Claude Opus 5.5 --- .../TagInterceptorInliningTest.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java new file mode 100644 index 00000000000..2b0d107eec3 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java @@ -0,0 +1,77 @@ +package datadog.trace.core.taginterceptor; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; + +/** + * Pins a code-shape property that C2 depends on but no other test can see. + * + *

{@code interceptTag(DDSpanContext, long, Object)} must stay too big for C2 to inline (more + * than {@code FreqInlineSize}, 325 bytes of bytecode). DDSpanContext.setTag calls it only for + * intercepted tags; if it inlines into setTag's own compiled code, the profiled handler bodies come + * with it and push setTag past {@code InlineSmallCode}, so callers stop inlining setTag and a + * constant id no longer folds its interception test away. + */ +class TagInterceptorInliningTest { + private static final int FREQ_INLINE_SIZE = 325; + + @Test + void interceptTagByIdStaysTooBigToInline() throws IOException { + int length = + codeLength("interceptTag", "(Ldatadog/trace/core/DDSpanContext;JLjava/lang/Object;)Z"); + + assertTrue( + length > FREQ_INLINE_SIZE, + "interceptTag(span, long, value) is " + + length + + " bytes of bytecode, small enough for C2 to inline into DDSpanContext.setTag; keep" + + " its handlers in the switch (see this test's Javadoc)"); + } + + /** The {@code code_length} of a method's Code attribute, read from the class file. */ + private static int codeLength(String name, String descriptor) throws IOException { + ClassReader reader; + try (InputStream in = TagInterceptor.class.getResourceAsStream("TagInterceptor.class")) { + reader = new ClassReader(in); + } + char[] buf = new char[reader.getMaxStringLength()]; + int offset = reader.header + 6; // access_flags, this_class, super_class + offset += 2 + 2 * reader.readUnsignedShort(offset); // interfaces + int fields = reader.readUnsignedShort(offset); + offset += 2; + for (int i = 0; i < fields; ++i) { + offset = skipAttributes(reader, offset + 6); // access_flags, name, descriptor + } + int methods = reader.readUnsignedShort(offset); + offset += 2; + for (int i = 0; i < methods; ++i) { + if (reader.readUTF8(offset + 2, buf).equals(name) + && reader.readUTF8(offset + 4, buf).equals(descriptor)) { + int attributes = reader.readUnsignedShort(offset + 6); + int attribute = offset + 8; + for (int j = 0; j < attributes; ++j) { + if ("Code".equals(reader.readUTF8(attribute, buf))) { + return reader.readInt(attribute + 10); // name, length, max_stack, max_locals + } + attribute += 6 + reader.readInt(attribute + 2); + } + } + offset = skipAttributes(reader, offset + 6); + } + throw new AssertionError(name + descriptor + " not found"); + } + + /** Skips the attributes whose count is at {@code offset}; returns the offset after them. */ + private static int skipAttributes(ClassReader reader, int offset) { + int attributes = reader.readUnsignedShort(offset); + offset += 2; + for (int i = 0; i < attributes; ++i) { + offset += 6 + reader.readInt(offset + 2); // name, length, info + } + return offset; + } +} From ecb4f73ce8442432f3260839029100e145ebab69 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 15:08:19 -0400 Subject: [PATCH 13/35] Tighten comments on the id-keyed interceptor Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/SetTagBenchmark.java | 17 +++++++++-------- .../java/datadog/trace/core/DDSpanContext.java | 12 ++++++------ .../core/taginterceptor/TagInterceptor.java | 15 +++++++++------ .../TagInterceptorInliningTest.java | 13 +++++++------ .../java/datadog/trace/api/KnownTagCodec.java | 17 ++++++++--------- 5 files changed, 39 insertions(+), 35 deletions(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java index ac3799860b7..a9d1c25d13e 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java @@ -18,16 +18,17 @@ /** * Measures {@code DDSpan.setTag} on a live span, overwriting one tag per call, by how the tag is - * named: by a constant id, by a known tag's name, and by a custom name. {@code tagMap_byId} is the - * floor -- the same store on a bare {@link TagMap}, with no span, lock, or interceptor. + * named: a constant id ({@code knownTag_byId}, {@code knownIntTag_byId}), a constant intercepted id + * ({@code interceptedTag_byId}, {@code span.kind}), a known tag's name, and a custom name. * - *

A constant id should cost close to the floor plus the span's lock: its interception test folds - * to the id's INTERCEPTED bit, and an id set has no custom-tag path. The name variants pay a - * registry lookup first. + *

Controls: {@code tagMap_byId} is the same store on a bare {@link TagMap}, with no span, lock, + * or interceptor; {@code tagMap_byId_synchronized} adds an uncontended lock, as the span takes one; + * {@code knownTag_byNonConstantId} reads the id from a non-final field, which C2 never + * constant-folds. * - *

Two controls separate those costs. {@code knownTag_byNonConstantId} reads the id from a - * non-final field, which C2 never folds, so nothing about the id is known at compile time. {@code - * tagMap_byId_synchronized} is the floor plus an uncontended lock, as the span takes one. + *

With a constant id, the interception test folds to the id's INTERCEPTED bit and there is no + * custom-tag path, so comparing {@code knownTag_byId} with {@code tagMap_byId_synchronized} + * isolates the span's own dispatch. The name variants add a registry lookup. */ @State(Scope.Thread) @Warmup(iterations = 3, time = 1) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 406135760de..6a87f8dca33 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1017,8 +1017,8 @@ public void removeTag(long tagId) { *

Existing tag value with the same value will be replaced. Setting a tag with a {@code null} * value will remove the tag from the span. * - *

A known tag's name is resolved to its id once, and the tag is then set as the id-keyed - * setters set it; only a custom tag is handled by name. + *

A known tag's name is resolved to its id once, and the id-keyed setter does the rest; only a + * custom tag is handled by name. * * @param tag The tag name. * @param value The nullable tag value. @@ -1056,9 +1056,9 @@ public void setTag(final String tag, final String value) { } /* - * Id-keyed setters, mirroring the String setters above. With a constant id, the interception - * test folds to the KnownTagCodec.INTERCEPTED bit, and there is no custom-tag path to take. An id - * that names no known tag is ignored, like a null tag. + * Id-keyed setters, mirroring the String setters above. With a constant id, the + * KnownTagCodec.INTERCEPTED test folds, leaving only the split-by-tags table load; an id set never + * takes the custom-tag path. An id that names no known tag is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { if (isUnknownTag(tagId)) { @@ -1171,7 +1171,7 @@ public void setTag(TagMap.EntryReader entry) { } /* - * Used when the interceptor's precheck determines that boxing is unavoidable + * Used when the interceptor's precheck says the tag may be intercepted, so boxing is unavoidable * * Either because the tagInterceptor needs to be fully checked (which requires boxing) * In that case, a box has already been created so it makes sense to pass the box diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 181889cb9a0..82a6cb16c06 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -128,7 +128,7 @@ private static Set customTagNames(Set names) { return custom; } - /** {@code tagId} is a known id, so its serial is in range. */ + /** Requires a known id, whose serial is always in range of the table. */ private boolean isSplitServiceTag(long tagId) { return splitServiceSerials[KnownTagCodec.serialNum(tagId)]; } @@ -150,7 +150,7 @@ public boolean needsIntercept(TagMap.EntryReader entry) { return tagId != 0 ? needsIntercept(tagId) : isSplitServiceTag(entry.tag()); } - /** Whether a custom tag -- one with no id -- may be routed: only split-by-tags routes one. */ + /** Whether a custom tag (one with no id) may be routed; only split-by-tags routes them. */ public boolean needsInterceptCustomTag(String customTag) { return isSplitServiceTag(customTag); } @@ -163,9 +163,9 @@ public boolean needsIntercept(Map map) { } /** - * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag. Called with a - * constant id, the {@link KnownTagCodec#INTERCEPTED} test folds away; only the split-by-tags - * table is left to check at run time. {@code tagId} must be a known id. + * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag. With a constant + * id, the {@link KnownTagCodec#INTERCEPTED} test folds to a constant, leaving only the + * split-by-tags table load at run time. {@code tagId} must be a known id. */ public boolean needsIntercept(long tagId) { return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); @@ -185,6 +185,9 @@ public boolean interceptTag(DDSpanContext span, String tag, Object value) { * Routes a known tag to a span field or a sampling directive. Returns true when the tag was * consumed, false when it should still be stored. Every tag with a case here carries the {@link * KnownTagCodec#INTERCEPTED} bit, declared in {@code tag-conventions-java.yaml}. + * + *

This method is deliberately too big for C2 to inline, so the handler bodies stay out of + * {@code DDSpanContext.setTag}'s compiled code; {@code TagInterceptorInliningTest} pins that. */ public boolean interceptTag(DDSpanContext span, long tagId, Object value) { switch (KnownTagCodec.serialNum(tagId)) { @@ -282,7 +285,7 @@ private static boolean splitService(DDSpanContext span, Object value) { private boolean interceptUrlResourceAsNameRule(DDSpanContext span, long tagId, Object value) { if (shouldSetUrlResourceAsName) { - // Values are stored under their Datadog name, whichever spelling set them. + // Read by Datadog name only: entries are stored under it whichever spelling set them. if (tagId == KnownTags.HTTP_METHOD_ID) { final Object url = span.unsafeGetTag(HTTP_URL); if (url != null) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java index 2b0d107eec3..e11811d641c 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorInliningTest.java @@ -8,13 +8,14 @@ import org.objectweb.asm.ClassReader; /** - * Pins a code-shape property that C2 depends on but no other test can see. + * Pins a code shape that C2's inlining depends on and no behavioral test can see. * - *

{@code interceptTag(DDSpanContext, long, Object)} must stay too big for C2 to inline (more - * than {@code FreqInlineSize}, 325 bytes of bytecode). DDSpanContext.setTag calls it only for - * intercepted tags; if it inlines into setTag's own compiled code, the profiled handler bodies come - * with it and push setTag past {@code InlineSmallCode}, so callers stop inlining setTag and a - * constant id no longer folds its interception test away. + *

{@code interceptTag(DDSpanContext, long, Object)} must stay too big for C2 to inline: more + * than {@code FreqInlineSize}, 325 bytes of bytecode by default. DDSpanContext.setTag calls it only + * for intercepted tags. If it inlined into setTag's own compiled code, the profiled handler bodies + * would come with it and can push setTag past {@code InlineSmallCode} (2500 bytes of machine code + * by default). Callers then refuse to inline setTag, and a constant id no longer folds its + * interception test away. */ class TagInterceptorInliningTest { private static final int FREQ_INLINE_SIZE = 325; diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index faf336cc40f..9c5687bf655 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -19,11 +19,10 @@ public final class KnownTagCodec { * reserved. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for * them. * - *

Bit 1 is the INTERCEPTED bit: set on a tag TagInterceptor may route on the set-path (to a - * span field or a sampling directive instead of tag storage). It is a hint, not a decision: the - * interceptor's switch says what each such tag does, and may still store it. Because a setter - * called with a constant id can test the bit at JIT time, the interception path folds away for - * every tag without it. + *

The INTERCEPTED bit marks a tag TagInterceptor may route on the set-path, to a span field or + * a sampling directive instead of tag storage. It is a hint, not a decision: the interceptor's + * switch decides, and may still store the tag. A setter called with a constant id tests the bit + * at JIT time, so the interception path folds away for every tag without it. * *

There is deliberately NO OpenTelemetry-applicability flag: an absent otel-name means * pass-through (the tag is emitted under its Datadog name), so today every known tag has an @@ -50,8 +49,8 @@ public static boolean isTraceLevel(long tagId) { } /** - * INTERCEPTED bit (low-32 carve, bit 1). Set marks a tag TagInterceptor may route on the - * set-path. Declared in the tracer overlay ({@code tag-conventions-java.yaml}). + * INTERCEPTED bit (low-32 carve, bit 1): marks a tag TagInterceptor may route on the set-path. + * The tracer overlay, {@code tag-conventions-java.yaml}, lists these tags. */ public static final long INTERCEPTED = 1L << 1; @@ -139,8 +138,8 @@ public static int serialLimit() { } /** - * True if {@code tagId} names a known tag -- the same answer as {@code nameOf(tagId) != null}, as - * a range check on its serial, so it folds away for a constant id. + * True if {@code tagId} names a known tag. Gives the same answer as {@code nameOf(tagId) != + * null}, but as a range check on the serial, which folds away for a constant id. */ public static boolean isKnown(long tagId) { int serial = serialNum(tagId); From 3f65031560a4d8ca7d9a8655de42985aca5ab008 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 15:32:07 -0400 Subject: [PATCH 14/35] Treat overlay tags as intercepted; tidy from review An overlay tag exists only to be routed, so the generator now marks it intercepted without a second listing; intercepted: lists only the tags declared in tag-conventions.yaml that the tracer also routes. Also restore the doc comment and @Suppress that the overlay helper displaced, read the serial limit one way, inline isUnknownTag, and drop an unused default. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 3 ++- .../buildlogic/tagRegistry/TagConventions.kt | 14 ++++++++----- .../buildlogic/tagRegistry/TagRegistry.kt | 2 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 2 +- .../datadog/trace/core/DDSpanContext.java | 20 ++++++++----------- .../java/datadog/trace/api/KnownTagCodec.java | 2 +- tag-conventions-java.yaml | 20 +++---------------- 7 files changed, 25 insertions(+), 38 deletions(-) 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 8a21f8e0045..05c3b9cdd26 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 @@ -7,7 +7,8 @@ import java.util.Locale * Emits the generated `KnownTags.java` from a [TagRegistry]. Public API first — per-tag * `_NAME` (string) + `_ID` (encoded long, literal) couplets with a trailing `// makeTagId(...)` * derivation comment — then the `_SERIAL_NUM` constants, the - * `StringIndex.EmbeddingSupport` keyOf table and the resolver's name switches. + * `StringIndex.EmbeddingSupport` keyOf table, the `NAMES_BY_SERIAL` array behind `nameOf`, and the + * resolver's `openTelemetryNameOf` switch. */ object KnownTagsEmitter { diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt index b17eac293b7..7b1239fbc40 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt @@ -243,7 +243,7 @@ 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, st: SpanType): Direction? = chainOf(spanTypes, st.name).firstNotNullOfOrNull { it.direction } - /** Reads `span-kind` as the direction it sets, or null when absent. */ + /** The tracer overlay's `tags`, as a mixin that applies to no span type; empty when none. */ private fun overlayMixin(overlay: Map): Map { val tags = tagList(overlay["tags"]) if (tags.isEmpty()) return emptyMap() @@ -253,6 +253,7 @@ class TagConventions private constructor( ) } + /** Reads `span-kind` as the direction it sets, or null when absent. */ private fun parseDirection(m: Map, owner: String): Direction? { val spanKind = m["span-kind"] require(spanKind == null || spanKind in SPAN_KIND_DIRECTIONS) { @@ -261,15 +262,15 @@ class TagConventions private constructor( return (spanKind as String?)?.let { SPAN_KIND_DIRECTIONS.getValue(it) } } - @Suppress("UNCHECKED_CAST") /** 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`), and - * `intercepted` lists the Datadog names the tracer intercepts, from either file. + * 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, overlay: Map = emptyMap()): TagConventions { for (section in listOf("span_types", "mixins", "trace_level")) { require(root[section] == null || root[section] is Map<*, *>) { "$section must be a mapping" } @@ -282,7 +283,10 @@ class TagConventions private constructor( require(interceptedRaw == null || (interceptedRaw is List<*> && interceptedRaw.all { it is String })) { "intercepted must be a list of Datadog tag names" } - val intercepted = (interceptedRaw as? List)?.toSet() ?: emptySet() + // An overlay tag exists only to be routed, so it is intercepted without being listed. + val intercepted = + ((interceptedRaw as? List) ?: emptyList()).toSet() + + tagList(overlay["tags"]).map { it.ddName } val spanTypesRaw = (root["span_types"] as? Map) ?: emptyMap() val parsedSpanTypes = spanTypesRaw.mapValues { (name, v) -> 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 73c38c2f7ef..55cdacd243c 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 @@ -52,7 +52,7 @@ class TagRegistry private constructor(val tags: List) { * Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() + intercepted() -- must stay in sync. * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, other low bits and the reserved [47-32] window zero. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean = false): Long { + fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long { var id = serial.toLong() shl 48 if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index 256367ccc0e..16b899d19ab 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -492,7 +492,7 @@ class TagRegistryGeneratorTest { "overlay.yaml", """ tags: [{dd-name: resource.name}] - intercepted: [resource.name, service] + intercepted: [service] """ ) val output = File(directory, "generated") diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 6a87f8dca33..b37ab392ddd 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1000,7 +1000,7 @@ public void removeTag(String tag) { * id that names no known tag is ignored, like the id-keyed setters do. */ public void removeTag(long tagId) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagId == KnownTags.SPAN_KIND_ID) { @@ -1061,7 +1061,7 @@ public void setTag(final String tag, final String value) { * takes the custom-tag path. An id that names no known tag is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { @@ -1075,7 +1075,7 @@ public void setTag(final long tagId, final Object value) { } public void setTag(final long tagId, final CharSequence value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { @@ -1089,7 +1089,7 @@ public void setTag(final long tagId, final CharSequence value) { } public void setTag(final long tagId, final boolean value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1102,7 +1102,7 @@ public void setTag(final long tagId, final boolean value) { } public void setTag(final long tagId, final int value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1115,7 +1115,7 @@ public void setTag(final long tagId, final int value) { } public void setTag(final long tagId, final long value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1128,7 +1128,7 @@ public void setTag(final long tagId, final long value) { } public void setTag(final long tagId, final float value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1141,7 +1141,7 @@ public void setTag(final long tagId, final float value) { } public void setTag(final long tagId, final double value) { - if (isUnknownTag(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1153,10 +1153,6 @@ public void setTag(final long tagId, final double value) { } } - private static boolean isUnknownTag(long tagId) { - return !KnownTagCodec.isKnown(tagId); - } - public void setTag(TagMap.EntryReader entry) { if (entry == null) { return; diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 9c5687bf655..47d4e9451bc 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -143,7 +143,7 @@ public static int serialLimit() { */ public static boolean isKnown(long tagId) { int serial = serialNum(tagId); - return serial != 0 && serial < KnownTags.SERIAL_LIMIT; // serial 0 is no tag + return serial != 0 && serial < serialLimit(); // serial 0 is no tag } /** The id for {@code name} in any namespace, or 0 when it is not a known tag. */ diff --git a/tag-conventions-java.yaml b/tag-conventions-java.yaml index c2304396785..2f3d89e39ed 100644 --- a/tag-conventions-java.yaml +++ b/tag-conventions-java.yaml @@ -6,8 +6,9 @@ # # tags — keys that exist only to be routed, so the conventions do not declare them. They get # ids like any other tag (instrumentation can set them by id) but belong to no span type. -# Same declaration syntax as tag-conventions.yaml; refs are not allowed. -# intercepted — Datadog names TagInterceptor may route, declared in either file. Each one's id carries +# Each is intercepted, so it is not listed again below. Same declaration syntax as +# tag-conventions.yaml; refs are not allowed. +# intercepted — tags declared in tag-conventions.yaml that TagInterceptor also routes. Each one's id carries # the INTERCEPTED bit, so a setter called with a constant id folds the interception test # away. TagInterceptor's switch is the authority on what each one does; a test there # keeps this list and the switch in agreement. @@ -29,21 +30,6 @@ tags: - { dd-name: _dd1.sr.eausr, type: double } intercepted: - # declared above - - resource.name - - span.type - - error - - manual.keep - - manual.drop - - asm.keep - - ai_guard.keep - - sampling.priority - - _dd.p.ts - - _dd.p.debug - - _dd.origin - - _dd.measured - - _dd1.sr.eausr - # declared in tag-conventions.yaml - service - peer.service - span.kind From 69d7849f4046ecbde35b0de898f11decdde12909 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 17:57:14 -0400 Subject: [PATCH 15/35] Reject shared-name tag ids on the id-keyed TagMap and span paths A tag declared once per direction (peer.port) has two ids but one shared Datadog name, and keyOf resolves that name to neither. An entry keyed by one of those ids hashed by the id while name-based access hashed the name as a custom tag, so get/remove/containsKey by name missed it and a later set by name produced a duplicate key. Encode a SHARED_NAME flag in the reserved id bit 0 so the check folds for a constant id. TagMap's id-keyed create/get/remove reject such ids, and DDSpanContext's id-keyed setters ignore them, as they do unknown ids, until name resolution knows the span's direction. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 1 + .../buildlogic/tagRegistry/TagRegistry.kt | 9 ++++--- .../datadog/trace/core/DDSpanContext.java | 25 +++++++++++-------- .../java/datadog/trace/api/KnownTagCodec.java | 25 +++++++++++++++++-- .../main/java/datadog/trace/api/TagMap.java | 5 ++++ 5 files changed, 49 insertions(+), 16 deletions(-) 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 05c3b9cdd26..f409bc18bdc 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 @@ -103,6 +103,7 @@ 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() 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 55cdacd243c..983eaad5ba4 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 @@ -47,15 +47,18 @@ class TagRegistry private constructor(val tags: List) { 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() + intercepted() -- must stay in sync. - * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, other low bits and the reserved [47-32] window zero. + * 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, intercepted: 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 } @@ -76,7 +79,7 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted), + id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null), declaredOtelName = renames[t.identity]?.otelName, otelDirection = renames[t.identity]?.direction, ) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index b37ab392ddd..b533a7de481 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -996,11 +996,13 @@ public void removeTag(String tag) { } /** - * Removes a known tag by id, rather than by its name, which need not resolve back to the id. An - * id that names no known tag is ignored, like the id-keyed setters do. + * Removes a known tag by id rather than by its name. An id the id-keyed setters ignore is ignored + * here too. + * + * @param tagId a {@code KnownTags.*_ID} constant */ public void removeTag(long tagId) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (tagId == KnownTags.SPAN_KIND_ID) { @@ -1058,10 +1060,11 @@ public void setTag(final String tag, final String value) { /* * Id-keyed setters, mirroring the String setters above. With a constant id, the * KnownTagCodec.INTERCEPTED test folds, leaving only the split-by-tags table load; an id set never - * takes the custom-tag path. An id that names no known tag is ignored, like a null tag. + * takes the custom-tag path. An id that names no known tag, or whose name is shared across + * directions (see KnownTagCodec#isKeyableById), is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (null == value) { @@ -1075,7 +1078,7 @@ public void setTag(final long tagId, final Object value) { } public void setTag(final long tagId, final CharSequence value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (null == value) { @@ -1089,7 +1092,7 @@ public void setTag(final long tagId, final CharSequence value) { } public void setTag(final long tagId, final boolean value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1102,7 +1105,7 @@ public void setTag(final long tagId, final boolean value) { } public void setTag(final long tagId, final int value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1115,7 +1118,7 @@ public void setTag(final long tagId, final int value) { } public void setTag(final long tagId, final long value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1128,7 +1131,7 @@ public void setTag(final long tagId, final long value) { } public void setTag(final long tagId, final float value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1141,7 +1144,7 @@ public void setTag(final long tagId, final float value) { } public void setTag(final long tagId, final double value) { - if (!KnownTagCodec.isKnown(tagId)) { + if (!KnownTagCodec.isKeyableById(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 47d4e9451bc..314fe6d67dc 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -15,8 +15,8 @@ public final class KnownTagCodec { * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] * are RESERVED and always zero here: they are the window the dense tag store uses for its * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is - * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is - * reserved. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for + * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is the + * SHARED_NAME bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for * them. * *

The INTERCEPTED bit marks a tag TagInterceptor may route on the set-path, to a span field or @@ -59,6 +59,27 @@ public static boolean isIntercepted(long tagId) { return (tagId & INTERCEPTED) != 0L; } + /** + * SHARED_NAME bit (low-32 carve, bit 0): marks one direction of a tag declared once per direction + * under a shared Datadog name, such as {@code peer.port}. {@link #keyOf} resolves that name to + * neither id, so the name alone cannot find an entry stored under one of them. + */ + public static final long SHARED_NAME = 1L << 0; + + /** True if the tagId is one direction of a tag whose Datadog name is shared across directions. */ + public static boolean hasSharedName(long tagId) { + return (tagId & SHARED_NAME) != 0L; + } + + /** + * True if {@code tagId} can key a {@link TagMap} entry: a known tag whose name resolves back to + * it. A {@link #hasSharedName shared-name} id cannot, until name resolution knows the span's + * direction. Like {@link #isKnown}, this folds away for a constant id. + */ + public static boolean isKeyableById(long tagId) { + return isKnown(tagId) && !hasSharedName(tagId); + } + /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 59dec3eb961..c4775b22d08 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -469,6 +469,11 @@ private static String requireKnownName(long tagId) { if (name == null) { throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); } + if (KnownTagCodec.hasSharedName(tagId)) { + // Name-keyed access hashes the shared name as a custom tag, so it could never find an entry + // keyed by this id; set the tag by name until resolution knows the span's direction. + throw new IllegalArgumentException("tag id has a shared name, set it by name: " + name); + } return name; } From 46c3b47f34143a62e0e1c499091f973be6eacae5 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 17:57:18 -0400 Subject: [PATCH 16/35] Test rejection of shared-name tag ids Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/DDSpanSetTagByIdTest.java | 10 ++++++++++ .../src/test/java/datadog/trace/api/KnownTagsTest.java | 4 ++++ .../test/java/datadog/trace/api/TagMapSetByIdTest.java | 10 ++++++++++ .../test/java/datadog/trace/api/TagMapTagHashTest.java | 8 ++++---- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index 0c953fa5b65..34fc1bc2028 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -61,6 +61,16 @@ void clearingAnUnknownIdIsIgnored() { assertNull(span.getTag(KnownTags.PEER_HOSTNAME_NAME)); } + @Test + void aSharedNameIdIsIgnoredAndLeavesTheNamedTag() { + span.setTag("peer.port", 8080); + + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); + span.setTag(KnownTags.PEER_PORT_INBOUND_ID, (Object) null); + + assertEquals(8080, span.getTag("peer.port")); + } + @Test void removingTheSpanKindByIdClearsTheKind() { span.setTag(KnownTags.SPAN_KIND_ID, "client"); diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index f2022391203..08570b034fa 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -169,6 +169,10 @@ void aNameDeclaredPerDirectionIsOneTagPerDirection() { assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_INBOUND_ID)); assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_OUTBOUND_ID)); assertNotEquals(KnownTags.PEER_PORT_INBOUND_ID, KnownTags.PEER_PORT_OUTBOUND_ID); + assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_INBOUND_ID)); + assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertFalse(KnownTagCodec.isKeyableById(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertTrue(KnownTagCodec.isKeyableById(KnownTags.PEER_HOSTNAME_ID)); assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index ecef85afdd4..88ac5d57ca5 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -72,4 +72,14 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(0L, "value")); assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } + + @Test + void rejectsSharedNameIds() { + TagMap map = TagMap.create(); + assertThrows(IllegalArgumentException.class, () -> map.set(KnownTags.PEER_PORT_INBOUND_ID, 80)); + assertThrows( + IllegalArgumentException.class, () -> map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 443)); + assertThrows( + IllegalArgumentException.class, () -> map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID)); + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index 5a5e56b7c78..c740b2b7a1c 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -72,9 +72,6 @@ void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessExcept assertEquals(tagIds.size(), copy.size()); for (long tagId : tagIds) { String name = KnownTagCodec.nameOf(tagId); - if (KnownTagCodec.keyOf(name) != tagId) { - continue; // a name declared per direction does not resolve to either direction's id - } assertEquals("value", map.getObject(name), name); assertEquals("value", copy.getObject(name), name); } @@ -84,7 +81,10 @@ private static List knownTagIds() throws IllegalAccessException { List tagIds = new ArrayList<>(); for (Field field : KnownTags.class.getFields()) { if (field.getType() == long.class && Modifier.isStatic(field.getModifiers())) { - tagIds.add(field.getLong(null)); + long tagId = field.getLong(null); + if (KnownTagCodec.isKeyableById(tagId)) { + tagIds.add(tagId); + } } } return tagIds; From ad2fa97a02bba7b8f1f89b43c9d84077df707f00 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 21:43:46 -0400 Subject: [PATCH 17/35] Build custom tags by name and known tags by id Entry's package-private name factories are now custom-only: they skip the registry and assert the name is not a known tag. Callers that start from a name resolve it once -- TagMap.set(String, ...) forwards a known tag to set(long, ...), while getAndSet, put, Ledger.set, putAll and the public Entry.create(String, ...) API go through anyEntryFor and friends -- so a custom tag pays one registry lookup instead of two. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 149 +++++++++++++----- .../datadog/trace/api/TagMapTagHashTest.java | 14 +- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index c4775b22d08..ea98fa43561 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -250,34 +250,34 @@ public static boolean isEmptyValue(Object value) { */ @Nullable public static final Entry create(@Nonnull String tag, Object value) { - return isEmptyValue(value) ? null : TagMap.Entry.newAnyEntry(tag, value); + return isEmptyValue(value) ? null : TagMap.anyEntryFor(tag, value); } /** If value is non-null, returns a new TagMap.Entry If value is null or empty, returns null */ @Nullable public static final Entry create(@Nonnull String tag, CharSequence value) { // NOTE: From the static typing, we know that value is not a primitive box - return isEmptyValue(value) ? null : TagMap.Entry.newObjectEntry(tag, value); + return isEmptyValue(value) ? null : TagMap.objectEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, boolean value) { - return TagMap.Entry.newBooleanEntry(tag, value); + return TagMap.booleanEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, int value) { - return TagMap.Entry.newIntEntry(tag, value); + return TagMap.intEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, long value) { - return TagMap.Entry.newLongEntry(tag, value); + return TagMap.longEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, float value) { - return TagMap.Entry.newFloatEntry(tag, value); + return TagMap.floatEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, double value) { - return TagMap.Entry.newDoubleEntry(tag, value); + return TagMap.doubleEntryFor(tag, value); } /* @@ -319,7 +319,7 @@ public static final Entry create(long tagId, double value) { } static Entry newAnyEntry(Map.Entry entry) { - return newAnyEntry(entry.getKey(), entry.getValue()); + return anyEntryFor(entry.getKey(), entry.getValue()); } static Entry newAnyEntry(String tag, Object value) { @@ -435,13 +435,14 @@ static Entry newDoubleEntry(String tag, Double box) { volatile String strCache = null; + /** + * A custom tag's entry: no registry lookup. A known tag is built from its id instead -- callers + * that start from a name resolve it once and pick the id or name factory (see {@code + * TagMap.anyEntryFor}). + */ private Entry(String tag, byte type, long prim, Object obj) { - /* - * Canonicalize known names at the single Entry construction point, so Datadog and - * OpenTelemetry spellings use the same TagMap key. The one StringIndex lookup this costs also - * yields the tag id, which becomes the tag hash. - */ - this(KnownTagCodec.keyOf(tag), tag, type, prim, obj); + this(0L, tag, type, prim, obj); + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; build it from its id"; } private Entry(long tagId, byte type, long prim, Object obj) { @@ -1001,31 +1002,31 @@ public boolean containsRemovals() { } public Ledger set(String tag, Object value) { - return this.recordEntry(Entry.newAnyEntry(tag, value)); + return this.recordEntry(anyEntryFor(tag, value)); } public Ledger set(String tag, CharSequence value) { - return this.recordEntry(Entry.newObjectEntry(tag, value)); + return this.recordEntry(objectEntryFor(tag, value)); } public Ledger set(String tag, boolean value) { - return this.recordEntry(Entry.newBooleanEntry(tag, value)); + return this.recordEntry(booleanEntryFor(tag, value)); } public Ledger set(String tag, int value) { - return this.recordEntry(Entry.newIntEntry(tag, value)); + return this.recordEntry(intEntryFor(tag, value)); } public Ledger set(String tag, long value) { - return this.recordEntry(Entry.newLongEntry(tag, value)); + return this.recordEntry(longEntryFor(tag, value)); } public Ledger set(String tag, float value) { - return this.recordEntry(Entry.newFloatEntry(tag, value)); + return this.recordEntry(floatEntryFor(tag, value)); } public Ledger set(String tag, double value) { - return this.recordEntry(Entry.newDoubleEntry(tag, value)); + return this.recordEntry(doubleEntryFor(tag, value)); } public Ledger set(Entry entry) { @@ -1494,7 +1495,7 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { @Deprecated @Override public Object put(@Nonnull String tag, Object value) { - TagMap.Entry entry = this.getAndSet(Entry.newAnyEntry(tag, value)); + TagMap.Entry entry = this.getAndSet(anyEntryFor(tag, value)); return entry == null ? null : entry.objectValue(); } @@ -1509,32 +1510,106 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { } } + /* + * Entry factories for callers that start from a name: resolve it once, then build a known tag + * from its id or a custom tag from its name. (Entry's own name factories are custom-only.) + */ + static Entry anyEntryFor(String tag, Object value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value); + } + + static Entry objectEntryFor(String tag, CharSequence value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value); + } + + static Entry booleanEntryFor(String tag, boolean value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value); + } + + static Entry intEntryFor(String tag, int value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value); + } + + static Entry longEntryFor(String tag, long value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value); + } + + static Entry floatEntryFor(String tag, float value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value); + } + + static Entry doubleEntryFor(String tag, double value) { + long tagId = KnownTagCodec.keyOf(tag); + return tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value); + } + public void set(@Nonnull String tag, @Nonnull Object value) { - this.putEntry(Entry.newAnyEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newAnyEntry(tag, value)); + } } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - this.putEntry(Entry.newObjectEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newObjectEntry(tag, value)); + } } public void set(@Nonnull String tag, boolean value) { - this.putEntry(Entry.newBooleanEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newBooleanEntry(tag, value)); + } } public void set(@Nonnull String tag, int value) { - this.putEntry(Entry.newIntEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newIntEntry(tag, value)); + } } public void set(@Nonnull String tag, long value) { - this.putEntry(Entry.newLongEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newLongEntry(tag, value)); + } } public void set(@Nonnull String tag, float value) { - this.putEntry(Entry.newFloatEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newFloatEntry(tag, value)); + } } public void set(@Nonnull String tag, double value) { - this.putEntry(Entry.newDoubleEntry(tag, value)); + long tagId = KnownTagCodec.keyOf(tag); + if (tagId != 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newDoubleEntry(tag, value)); + } } /* @@ -1658,31 +1733,31 @@ private Entry putEntry(@Nonnull Entry newEntry) { } public Entry getAndSet(@Nonnull String tag, Object value) { - return this.getAndSet(Entry.newAnyEntry(tag, value)); + return this.getAndSet(anyEntryFor(tag, value)); } public Entry getAndSet(@Nonnull String tag, CharSequence value) { - return this.getAndSet(Entry.newObjectEntry(tag, value)); + return this.getAndSet(objectEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, boolean value) { - return this.getAndSet(Entry.newBooleanEntry(tag, value)); + return this.getAndSet(booleanEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, int value) { - return this.getAndSet(Entry.newIntEntry(tag, value)); + return this.getAndSet(intEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, long value) { - return this.getAndSet(Entry.newLongEntry(tag, value)); + return this.getAndSet(longEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, float value) { - return this.getAndSet(Entry.newFloatEntry(tag, value)); + return this.getAndSet(floatEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, double value) { - return this.getAndSet(Entry.newDoubleEntry(tag, value)); + return this.getAndSet(doubleEntryFor(tag, value)); } public void putAll(Map map) { @@ -3166,7 +3241,7 @@ public Object objectValue() { @Override public TagMap.Entry entry() { - return TagMap.Entry.newAnyEntry(this.tag, this.value); + return TagMap.anyEntryFor(this.tag, this.value); } @Override diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index c740b2b7a1c..dd477db0eb9 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -17,8 +18,8 @@ class TagMapTagHashTest { @Test void aKnownTagHashesToItsIdHoweverItIsSet() { TagMap.Entry byId = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "GET"); - TagMap.Entry byName = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_NAME, "GET"); - TagMap.Entry byOtelName = TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + TagMap.Entry byName = TagMap.anyEntryFor(KnownTags.HTTP_METHOD_NAME, "GET"); + TagMap.Entry byOtelName = TagMap.anyEntryFor(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); assertEquals(KnownTags.HTTP_METHOD_ID, byId.tagHash); assertEquals(KnownTags.HTTP_METHOD_ID, byName.tagHash); @@ -26,6 +27,15 @@ void aKnownTagHashesToItsIdHoweverItIsSet() { assertEquals(KnownTags.HTTP_METHOD_ID, byName.tagId()); } + @Test + void theNameFactoriesAreForCustomTagsOnly() { + // Entry's name factories skip the registry, so a known name must come in through its id; + // anyEntryFor and friends resolve a name first. (Tests run with assertions enabled.) + assertThrows( + AssertionError.class, () -> TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_NAME, "GET")); + assertEquals(0L, TagMap.Entry.newAnyEntry("my.custom.tag", "value").tagId()); + } + @Test void aCustomTagHashesToItsNameWithNoId() { TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); From 3a269e121ed750feadbf7aed73a5598b4589a61d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 14:46:08 -0400 Subject: [PATCH 18/35] Resolve direction-dependent tag names by the span's kind Builds on the registry directions (#12713) and id-keyed setters (#12715, included here): KnownTagCodec resolves names by direction, TagMap finds a shared name's tag whichever direction it holds, and DDSpanContext resolves direction-dependent names by the span's kind. Squashed from the review history of #12731. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 81 ++++++-- .../buildlogic/tagRegistry/TagRegistry.kt | 13 +- .../TagRegistryGeneratorPluginTest.kt | 11 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 58 +++++- .../datadog/trace/core/DDSpanContext.java | 179 +++++++++++++++++- .../trace/core/DDSpanDirectionalTagsTest.java | 120 ++++++++++++ .../java/datadog/trace/api/KnownTagCodec.java | 140 +++++++++++++- .../main/java/datadog/trace/api/TagMap.java | 92 +++++++-- .../java/datadog/trace/api/KnownTagsTest.java | 67 +++++++ .../api/TagMapEntryCreateByNameTest.java | 52 +++++ .../datadog/trace/api/TagMapTagHashTest.java | 56 +++++- 11 files changed, 814 insertions(+), 55 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java 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 f409bc18bdc..0632b636145 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 @@ -65,10 +65,27 @@ object KnownTagsEmitter { fun otelNameC(identity: TagConventions.TagIdentity) = otelNameOfConst.getValue(identity) val order = reg.tags.map { it.identity } // stable emit order - // keyOf has no direction argument, so omit shared names and let them resolve to 0 (unknown). + // keyOf(name) has no direction, so shared names are left out here and resolve to 0 (unknown); + // they are added below, marked, for keyOf(name, direction). val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null }.map { it.identity } // canonical name -> OpenTelemetry name, for the reverse (openTelemetryNameOf) switch. val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.identity to it } }.toMap() + // Names whose tag depends on the span's direction: a shared Datadog name (one tag per direction) + // and a direction-scoped OpenTelemetry name. keyOf(name) resolves none of them; keyOf(name, + // direction) switches on each one's tag per direction. + val directional = sortedMapOf>() + // Shared Datadog names, marked in the keyOf table so a lookup by that name alone can probe its tags. + val sharedNames = sortedSetOf() + for (t in reg.tags) { + t.sharedNameDirection?.let { + directional.getOrPut(t.ddName) { mutableMapOf() }[it] = t.identity + sharedNames.add(t.ddName) + } + t.otelDirection?.let { directional.getOrPut(t.declaredOtelName!!) { mutableMapOf() }[it] = t.identity } + } + check(directional.keys.none { name -> keyOfOrder.any { it.ddName == name } || name in otelName.values }) { + "a direction-dependent name is also direction-free: ${directional.keys}" + } return buildString { // Public API first (name + encoded id couplets), so readers see the useful parts up top; the // serial ids and keyOf/resolver machinery follow below. Derivation is in the trailing comment. @@ -139,6 +156,7 @@ object KnownTagsEmitter { otelByCanonical.forEach { (otel, _) -> appendLine(" \"${escape(otel)}\",") } + directional.keys.forEach { appendLine(" \"${escape(it)}\",") } appendLine( """ }; @@ -149,6 +167,11 @@ object KnownTagsEmitter { otelByCanonical.forEach { (_, canonical) -> appendLine(" ${idC(canonical)},") } + // A direction-dependent name's value is not an id but a marker saying which kind it is. + for (name in directional.keys) { + val marker = if (name in sharedNames) "SHARED_DATADOG_NAME_SENTINEL" else "DIRECTION_SCOPED_OTEL_NAME_SENTINEL" + appendLine(" KnownTagCodec.$marker, // $name") + } // Resolver. KnownTagCodec.Installed links to this field directly, so merely resolving a tag // name initializes this class -- there is no registration step and no ordering to get wrong. appendLine( @@ -198,21 +221,29 @@ object KnownTagsEmitter { 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. + // openTelemetryNameOf: id -> OTel-namespace name on spans of a direction, null when + // the tag has none there. The caller (a serializer) owns any fall-back-to-Datadog-name + // policy. @Override - public String openTelemetryNameOf(long tagId) { + public String openTelemetryNameOf(long tagId, int direction) { switch (KnownTagCodec.serialNum(tagId)) { """.trimIndent() ) - for (identity in order) { - if (otelName[identity] == null) continue - appendLine( - """ - case ${serialC(identity)}: - return ${otelNameC(identity)}; - """.trimIndent() - ) + for (t in reg.tags) { + val otel = t.declaredOtelName ?: continue + val scoped = t.otelDirection + val result = + when { + scoped == null -> otelNameC(t.identity) + + // A tag declared per direction exists only on spans of its direction, so emitting it + // needs none; only resolving the name to it does. + t.sharedNameDirection != null -> "\"${escape(otel)}\"" + + else -> "direction == KnownTagCodec.DIRECTION_${scoped.name} ? \"${escape(otel)}\" : null" + } + appendLine("case ${serialC(t.identity)}:") + appendLine(" return $result;") } appendLine( """ @@ -222,10 +253,34 @@ object KnownTagsEmitter { } @Override - public long keyOf(String name) { + public long lookup(String name) { int slot = StringIndex.EmbeddingSupport.indexOf(KEYOF_HASHES, KEYOF_KEYS, name); return slot < 0 ? 0L : KEYOF_IDS[slot]; } + + @Override + public long directionalKeyOf(String name, int direction) { + switch (name) { + """.trimIndent() + ) + for ((name, byDirection) in directional) { + appendLine(" case \"${escape(name)}\":") + appendLine(" switch (direction) {") + for (d in TagConventions.Direction.entries) { + val tag = byDirection[d] ?: continue + appendLine(" case KnownTagCodec.DIRECTION_${d.name}:") + appendLine(" return ${idC(tag)};") + } + appendLine(" default:") + appendLine(" return KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL;") + appendLine(" }") + } + appendLine( + """ + default: + return 0L; + } + } }; private $className() {} 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 983eaad5ba4..6b96a1f5283 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 @@ -49,12 +49,23 @@ class TagRegistry private constructor(val tags: List) { 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 + /** + * Serials stay below 2^15, so bit 63 of an id is never set: every id is positive, clear of 0 + * (unknown) and of KnownTagCodec's negative lookup sentinels. + */ + const val SERIAL_CAPACITY = 1 shl 15 + /** * 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. + * reserved [47-32] window zero. Every id is made here, and the range check makes each one + * positive by construction. */ fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, sharedName: Boolean = false): Long { + require(serial in FIRST_SERIAL until SERIAL_CAPACITY) { + "serial $serial is outside [$FIRST_SERIAL, $SERIAL_CAPACITY): more tags would make tag ids " + + "negative, where KnownTagCodec's lookup sentinels live" + } var id = serial.toLong() shl 48 if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt index fa08bc5b177..b37ae96dee7 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt @@ -245,10 +245,17 @@ class TagRegistryGeneratorPluginTest { """ package datadog.trace.api; public final class KnownTagCodec { + public static final int DIRECTION_INBOUND = 0; + public static final int DIRECTION_OUTBOUND = 1; + public static final int DIRECTION_NONE = 2; + static final long SHARED_DATADOG_NAME_SENTINEL = -1L; + static final long DIRECTION_SCOPED_OTEL_NAME_SENTINEL = -2L; + public static final long NO_TAG_IN_DIRECTION_SENTINEL = -3L; public interface Resolver { String nameOf(long id); - String openTelemetryNameOf(long id); - long keyOf(String name); + String openTelemetryNameOf(long id, int direction); + long lookup(String name); + long directionalKeyOf(String name, int direction); } public static int serialNum(long id) { return (int) (id >>> 48); } } diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index 16b899d19ab..b47fe5f0cf4 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -293,7 +293,13 @@ class TagRegistryGeneratorTest { TagRegistryGenerator.generate(yaml, output) val generated = contents(output) - assertThat(generated.getValue("java/datadog/trace/api/KnownTags.java")).doesNotContain("server.address") + val source = generated.getValue("java/datadog/trace/api/KnownTags.java") + assertThat(source).doesNotContain("HTTP_HOSTNAME_OTEL_NAME") + assertThat(source).contains("KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // server.address") + assertThat(source.substringAfter("case \"server.address\":").substringBefore("default:")) + .contains("case KnownTagCodec.DIRECTION_INBOUND:", "return HTTP_HOSTNAME_ID;") + .doesNotContain("DIRECTION_OUTBOUND") + assertThat(source).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") assertThat(generated.getValue("tag-assignment.txt")) .containsPattern("server\\.address +inbound +-> http\\.hostname") } @@ -340,7 +346,7 @@ class TagRegistryGeneratorTest { } @Test - fun `a Datadog name declared per direction is not resolvable without a direction`() { + fun `a Datadog name declared per direction resolves only with a direction`() { val yaml = directory.conventionsFile( """ span_types: @@ -361,11 +367,40 @@ class TagRegistryGeneratorTest { .contains("PEER_PORT_INBOUND_ID", "PEER_PORT_OUTBOUND_ID") .contains("{@code peer.port} on outbound spans.", "{@code peer.port} on inbound spans.") .doesNotContain("PEER_PORT_NAME", "PEER_PORT_INBOUND_NAME", "PEER_PORT_OUTBOUND_NAME") - assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).doesNotContain("PEER_PORT") + assertThat(source.substringAfter("KEYOF_VALUES = {").substringBefore("};")) + .doesNotContain("PEER_PORT") + .contains("KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL, // peer.port") + assertThat(source.substringAfter("case \"peer.port\":").substringBefore("default:")) + .contains("return PEER_PORT_INBOUND_ID;", "return PEER_PORT_OUTBOUND_ID;") assertThat(generated.getValue("tag-assignment.txt")) .containsPattern("peer\\.port +-> peer\\.port@inbound, peer\\.port@outbound") } + @Test + fun `a tag declared per direction is emitted under its OpenTelemetry name without a direction`() { + val yaml = directory.conventionsFile( + """ + span_types: + http.client: {span-kind: client, include: [outbound_peer]} + http.server: + span-kind: server + include: [inbound_peer] + tags: [{dd-name: http.hostname, otel-name: server.address}] + mixins: + outbound_peer: {span-kind: client, tags: [{dd-name: peer.port, type: int, otel-name: server.port}]} + inbound_peer: {span-kind: server, tags: [{dd-name: peer.port, type: int, otel-name: client.port}]} + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + val emit = source.substringAfter("public String openTelemetryNameOf(long tagId, int direction)") + assertThat(emit).contains("return \"server.port\";", "return \"client.port\";") + assertThat(emit).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") + } + @Test fun `a ref to a Datadog name declared per direction resolves by the referencing direction`() { val conventions = tagConventions( @@ -423,6 +458,23 @@ class TagRegistryGeneratorTest { .withMessageContaining(message) } + @Test + fun `every tag id is positive, at both ends of the serial range`() { + for (serial in listOf(TagRegistry.FIRST_SERIAL, TagRegistry.SERIAL_CAPACITY - 1)) { + assertThat(TagRegistry.encode(serial, traceLevel = false, intercepted = false)).isPositive() + assertThat(TagRegistry.encode(serial, traceLevel = true, intercepted = false)).isPositive() + } + } + + @Test + fun `a serial outside the range that keeps ids positive is rejected`() { + for (serial in listOf(0, TagRegistry.SERIAL_CAPACITY, Short.MAX_VALUE + 1)) { + assertThatIllegalArgumentException() + .isThrownBy { TagRegistry.encode(serial, traceLevel = false, intercepted = false) } + .withMessageContaining("serial $serial is outside") + } + } + @Test fun `a declared name that looks like a derived identity is a separate tag`() { val conventions = tagConventions( diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index b533a7de481..26b9f1555fc 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -41,6 +41,7 @@ import datadog.trace.util.TagsHelper; import java.io.Closeable; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -127,8 +128,29 @@ public class DDSpanContext null // CUSTOM }; + /** + * The direction each span kind ordinal resolves direction-dependent tag names in ({@code + * peer.port} is the client's port on a server span). Broker and custom kinds have none. + */ + private static final int[] TAG_DIRECTION_OF_SPAN_KIND = { + KnownTagCodec.DIRECTION_UNKNOWN, // UNSET + KnownTagCodec.DIRECTION_INBOUND, // SERVER + KnownTagCodec.DIRECTION_OUTBOUND, // CLIENT + KnownTagCodec.DIRECTION_OUTBOUND, // PRODUCER + KnownTagCodec.DIRECTION_INBOUND, // CONSUMER + KnownTagCodec.DIRECTION_NONE, // INTERNAL + KnownTagCodec.DIRECTION_UNKNOWN, // BROKER + KnownTagCodec.DIRECTION_UNKNOWN // CUSTOM + }; + private volatile byte spanKindOrdinal = SPAN_KIND_UNSET; + /** + * Set when a direction-dependent tag name was stored before the span had a direction, so it is + * stored under its bare name. Setting the span kind then re-keys it to the tag it denotes. + */ + private volatile boolean unresolvedDirectionalTags; + private CharSequence integrationName; private CharSequence serviceNameSource; @@ -901,6 +923,61 @@ static boolean tagEquals(String tagValue, String tagLiteral) { */ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); + if (unresolvedDirectionalTags) { + resolveDirectionalTags(); + } + } + + /** The direction this span resolves direction-dependent tag names in. */ + private int tagDirection() { + return TAG_DIRECTION_OF_SPAN_KIND[spanKindOrdinal]; + } + + /** + * The tag a direction-dependent name denotes on this span, or 0 for any other name, which {@link + * TagMap} resolves itself. Only a handful of names depend on direction, so a miss is cheap. + */ + private long directionalTagId(String tag) { + long tagId = KnownTagCodec.directionalKeyOf(tag, tagDirection()); + return tagId > 0 ? tagId : 0L; + } + + /** + * {@link #directionalTagId} for a tag about to be stored, noting a direction-dependent name that + * cannot resolve until the span has a direction. Call while holding the {@code unsafeTags} lock. + */ + private long directionalTagIdToStore(String tag) { + int direction = tagDirection(); + long tagId = KnownTagCodec.directionalKeyOf(tag, direction); + if (tagId > 0) { + return tagId; + } + if (tagId == KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL + && direction == KnownTagCodec.DIRECTION_UNKNOWN) { + unresolvedDirectionalTags = true; + } + return 0L; + } + + /** Re-keys tags stored under a direction-dependent name before the span had a direction. */ + private void resolveDirectionalTags() { + int direction = tagDirection(); + if (direction == KnownTagCodec.DIRECTION_UNKNOWN) { + return; + } + synchronized (unsafeTags) { + unresolvedDirectionalTags = false; + List unresolved = new ArrayList<>(2); + for (TagMap.EntryReader entry : unsafeTags) { + if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { + unresolved.add(entry.entry()); + } + } + for (TagMap.Entry entry : unresolved) { + unsafeTags.remove(entry.tag()); + unsafeTags.set(KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); + } + } } static byte spanKindOrdinalOf(String kind) { @@ -985,12 +1062,85 @@ public void setMetric(final TagMap.EntryReader entry) { } } + /* + * Store a tag that interception let through, by id when its name depends on the span's direction + * (peer.port, server.address), otherwise by name. Call while holding the unsafeTags lock. + */ + private void storeTag(String tag, Object value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + private void storeTag(String tag, CharSequence value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + private void storeTag(String tag, boolean value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + private void storeTag(String tag, int value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + private void storeTag(String tag, long value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + private void storeTag(String tag, float value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + private void storeTag(String tag, double value) { + long tagId = directionalTagIdToStore(tag); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + public void removeTag(String tag) { if (tagEquals(tag, Tags.SPAN_KIND)) { // Clear the cached ordinal; unsafeTags still needs to be updated below. spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (unsafeTags) { + // A direction-dependent name removes the tag it denotes here, and any tag stored under the + // bare name before the span had a direction. + long tagId = directionalTagId(tag); + if (tagId != 0) { + unsafeTags.getAndRemove(tagId); + } unsafeTags.remove(tag); } } @@ -1036,7 +1186,7 @@ public void setTag(final String tag, final Object value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1052,7 +1202,7 @@ public void setTag(final String tag, final String value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1164,7 +1314,12 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!tagInterceptor.interceptTag(this, entry)) { synchronized (unsafeTags) { - unsafeTags.set(entry); + long tagId = entry.tagId() == 0 ? directionalTagIdToStore(entry.tag()) : 0L; + if (tagId != 0) { + unsafeTags.set(tagId, entry.objectValue()); + } else { + unsafeTags.set(entry); + } } } } @@ -1199,7 +1354,7 @@ public void setTag(final String tag, final boolean value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1214,7 +1369,7 @@ public void setTag(final String tag, final int value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1229,7 +1384,7 @@ public void setTag(final String tag, final long value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1244,7 +1399,7 @@ public void setTag(final String tag, final float value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1259,7 +1414,7 @@ public void setTag(final String tag, final double value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - unsafeTags.set(tag, value); + storeTag(tag, value); } } } @@ -1378,6 +1533,14 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { + long tagId = directionalTagId(tag); + if (tagId != 0) { + TagMap.Entry entry = unsafeTags.getEntry(tagId); + if (entry != null) { + return entry.objectValue(); + } + // else stored under the bare name before the span had a direction + } return unsafeTags.getObject(tag); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java new file mode 100644 index 00000000000..c61692bbebd --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java @@ -0,0 +1,120 @@ +package datadog.trace.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.api.KnownTags; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.common.writer.ListWriter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * A few tag names mean different tags depending on the span's direction: {@code peer.port} is the + * client's port on a server span and the server's on a client span, and {@code server.address} is + * {@code http.hostname} inbound but {@code peer.hostname} outbound. A span resolves them by its + * kind. + */ +class DDSpanDirectionalTagsTest extends DDCoreJavaSpecification { + private CoreTracer tracer; + private DDSpan span; + + @BeforeEach + void setup() { + tracer = tracerBuilder().writer(new ListWriter()).build(); + span = (DDSpan) tracer.buildSpan("datadog", "fakeOperation").start(); + } + + @AfterEach + void tearDown() { + span.finish(); + tracer.close(); + } + + @Test + void aSharedNameIsStoredAsTheTagOfTheSpansDirection() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_PORT, 5432); + + assertEquals(5432, storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertNull(storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals(5432, span.getTag(Tags.PEER_PORT)); + } + + @Test + void aSharedNameOnAServerSpanIsTheClientsPort() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + span.setTag(Tags.PEER_PORT, "1234"); + + assertEquals("1234", storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals("1234", span.getTag(Tags.PEER_PORT)); + } + + @Test + void aDirectionScopedOpenTelemetryNameIsStoredAsTheTagItDenotes() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + span.setTag("server.address", "inbound.example"); + + assertEquals("inbound.example", span.getTag(Tags.HTTP_HOSTNAME)); + assertEquals("inbound.example", span.getTag("server.address")); + assertNull(span.getTag(Tags.PEER_HOSTNAME)); + } + + @Test + void theSameOpenTelemetryNameIsADifferentTagOnAClientSpan() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag("server.address", "outbound.example"); + + assertEquals("outbound.example", span.getTag(Tags.PEER_HOSTNAME)); + assertNull(span.getTag(Tags.HTTP_HOSTNAME)); + } + + @Test + void aNameWithNoTagInTheSpansDirectionStaysACustomTag() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag("client.port", 1234); + + assertEquals(1234, span.getTag("client.port")); + assertNull(span.getTag(Tags.PEER_PORT)); + } + + @Test + void aNameSetBeforeTheSpanHasADirectionResolvesWhenItGetsOne() { + span.setTag(Tags.PEER_PORT, 5432); + span.setTag("server.address", "outbound.example"); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + assertEquals(5432, storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertEquals("outbound.example", span.getTag(Tags.PEER_HOSTNAME)); + assertNull(span.getTags().get("server.address")); + } + + @Test + void removingASharedNameRemovesTheTagOfTheSpansDirection() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_PORT, 5432); + + span.setTag(Tags.PEER_PORT, (String) null); + + assertNull(storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertNull(span.getTag(Tags.PEER_PORT)); + } + + @Test + void anInternalSpanHasNoPeerPort() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_INTERNAL); + span.setTag(Tags.PEER_PORT, 5432); + + assertNull(storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertNull(storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals(5432, span.getTag(Tags.PEER_PORT)); + } + + private Object storedValue(long tagId) { + TagMap.Entry entry = span.getTags().getEntry(tagId); + return entry == null ? null : entry.objectValue(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 314fe6d67dc..05512eacc61 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -10,7 +10,9 @@ */ public final class KnownTagCodec { /* - * tagId bit layout: [63-48 serialNum (16 bits)] [47-32 reserved, zero] [31-0 flags]. serialNum is + * tagId bit layout: [63-48 serialNum (16 bits, 15 used: bit 63 is kept clear)] [47-32 reserved, + * zero] [31-0 flags]. Keeping bit 63 clear makes every id positive, so 0 (unknown) and the + * negative lookup sentinels below can never be ids; the generator enforces it. serialNum is * globally unique per known tag and is the whole of the tag's identity — nameOf/ * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] * are RESERVED and always zero here: they are the window the dense tag store uses for its @@ -95,15 +97,70 @@ public static long makeTagId(int serialNum) { return (long) serialNum << 48; } + /* + * A span's direction, which resolves the few names whose tag depends on it: peer.port is the + * client's port on inbound spans and the server's on outbound ones. Pass one of these to + * keyOf(String, int) and openTelemetryTagOf(long, int). + */ + + /** Direction not known, for example a span with no kind: only names that need none resolve. */ + public static final int DIRECTION_UNKNOWN = -1; + + /** Server and consumer spans, which receive a request or message. */ + public static final int DIRECTION_INBOUND = 0; + + /** Client and producer spans, which send a request or message. */ + public static final int DIRECTION_OUTBOUND = 1; + + /** Internal spans, which have no other end. */ + public static final int DIRECTION_NONE = 2; + + /* + * Lookup sentinels. Each is negative and distinct; a tag id never is, since the generator keeps + * every serial below 2^15. Not ids: nothing may store one as a tag. + */ + + /** + * What {@link Resolver#lookup} returns for a Datadog name shared by a tag per direction ({@code + * peer.port}): no single tag, but whichever of them a map holds is that name's value. + */ + static final long SHARED_DATADOG_NAME_SENTINEL = -1L; + + /** + * What {@link Resolver#lookup} returns for an OpenTelemetry name that applies in one direction + * only: {@code server.address} is {@code http.hostname} on inbound spans and {@code + * peer.hostname} on outbound ones. + */ + static final long DIRECTION_SCOPED_OTEL_NAME_SENTINEL = -2L; + + /** + * What {@link #directionalKeyOf} returns for a name whose tag depends on the span's direction but + * that names no tag on spans of the given direction: {@code client.port} on an outbound span, or + * any such name while the direction is unknown. + */ + public static final long NO_TAG_IN_DIRECTION_SENTINEL = -3L; + + /** The registry's tables. Generated as {@code KnownTags.RESOLVER}; this class owns the policy. */ public interface Resolver { /** The tag's Datadog-namespace (canonical) name. */ String nameOf(long tagId); - /** The tag's OpenTelemetry-namespace name, or {@code null} when it declares none. */ - String openTelemetryNameOf(long tagId); + /** + * The tag's OpenTelemetry-namespace name on spans of {@code direction}, or {@code null} when it + * declares none there. {@link #DIRECTION_UNKNOWN} returns every name that needs no direction: + * one that holds in every direction, or that of a tag declared per direction. + */ + String openTelemetryNameOf(long tagId, int direction); + + /** + * The id for {@code name} in any namespace; {@link #SHARED_DATADOG_NAME_SENTINEL} or {@link + * #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} when its tag depends on direction; 0 when it is not a + * known tag. + */ + long lookup(String name); - /** The id for {@code name} in ANY namespace (many→one), or 0 when it is not a known tag. */ - long keyOf(String name); + /** See {@link KnownTagCodec#directionalKeyOf}. */ + long directionalKeyOf(String name, int direction); } /** @@ -131,7 +188,7 @@ public static String datadogNameOf(long tagId) { * #openTelemetryTagOf} instead. */ public static String openTelemetryNameOf(long tagId) { - return Installed.RESOLVER.openTelemetryNameOf(tagId); + return Installed.RESOLVER.openTelemetryNameOf(tagId, DIRECTION_UNKNOWN); } /** @@ -143,10 +200,25 @@ public static String openTelemetryNameOf(long tagId) { *

This is the one place the pass-through policy lives, so no serializer re-decides it. Pair it * with {@link #datadogNameOf} for the same tag under the Datadog namespace; outbound naming is * per-namespace, never normalized to one of them. + * + *

Needs no direction for nearly every tag, including one declared per direction ({@code + * peer.port} on an outbound span is {@code server.port}). The exception is a single tag whose + * rename holds in one direction only ({@code http.hostname} is {@code server.address} on inbound + * spans): without a direction it passes through under its Datadog name. A caller naming a span's + * tags uses {@link #openTelemetryTagOf(long, int)}. */ public static String openTelemetryTagOf(long tagId) { + return openTelemetryTagOf(tagId, DIRECTION_UNKNOWN); + } + + /** + * {@link #openTelemetryTagOf(long)} on spans of {@code direction}, which also applies a single + * tag's rename that holds only in that direction: {@code http.hostname} is {@code server.address} + * on inbound spans, but stays {@code http.hostname} on outbound ones. + */ + public static String openTelemetryTagOf(long tagId, int direction) { Resolver resolver = Installed.RESOLVER; - String otelName = resolver.openTelemetryNameOf(tagId); + String otelName = resolver.openTelemetryNameOf(tagId, direction); return otelName != null ? otelName : resolver.nameOf(tagId); } @@ -167,9 +239,59 @@ public static boolean isKnown(long tagId) { return serial != 0 && serial < serialLimit(); // serial 0 is no tag } - /** The id for {@code name} in any namespace, or 0 when it is not a known tag. */ + /** + * The id for {@code name} in any namespace, or 0 when it is not a known tag. A name whose tag + * depends on the span's direction also resolves to 0; see {@link #keyOf(String, int)}. + */ public static long keyOf(String name) { - return Installed.RESOLVER.keyOf(name); + long key = Installed.RESOLVER.lookup(name); + return key < 0 ? 0L : key; + } + + /** + * The id for {@code name} on spans of {@code direction}, or 0 when it is not a known tag there. + * Costs the same as {@link #keyOf(String)} for every name but the few that depend on direction. + */ + public static long keyOf(String name, int direction) { + Resolver resolver = Installed.RESOLVER; + long key = resolver.lookup(name); + if (key >= 0) { + return key; + } + long tagId = resolver.directionalKeyOf(name, direction); + return tagId > 0 ? tagId : 0L; + } + + /** + * Only the direction-dependent part of {@link #keyOf(String, int)}, for a caller that already + * resolves every other name: the tag {@code name} denotes on spans of {@code direction} when it + * is one of the few names that depend on direction ({@code peer.port}, {@code server.address}); 0 + * for every other name; or {@link #NO_TAG_IN_DIRECTION_SENTINEL}. + * + *

A switch over just those names, so a miss is far cheaper than a full lookup. A span uses it + * before handing a name to {@link TagMap}, which then does its usual single lookup. + */ + public static long directionalKeyOf(String name, int direction) { + return Installed.RESOLVER.directionalKeyOf(name, direction); + } + + /** + * The raw registry lookup: {@code name}'s id, 0 when it is not a known tag, or {@link + * #SHARED_DATADOG_NAME_SENTINEL} / {@link #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} when its tag + * depends on direction. + */ + static long lookup(String name) { + return Installed.RESOLVER.lookup(name); + } + + /** + * {@link #keyOf(String)}, except that a Datadog name shared by a tag per direction returns {@link + * #SHARED_DATADOG_NAME_SENTINEL}, so {@link TagMap} can find whichever of those tags it holds, + * through {@link #directionalKeyOf}. One lookup, like {@code keyOf}. + */ + static long keyOrSharedName(String name) { + long key = Installed.RESOLVER.lookup(name); + return key == DIRECTION_SCOPED_OTEL_NAME_SENTINEL ? 0L : key; } /** diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index ea98fa43561..445fca16bf2 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -243,41 +243,65 @@ public static boolean isEmptyValue(Object value) { || (value instanceof CharSequence && ((CharSequence) value).length() == 0); } + /* + * Name-keyed factories. They accept only a name whose tag needs no span direction: an entry is + * built with no span, so it cannot resolve peer.port (the client's port inbound, the server's + * outbound) or server.address. For those, use the id-keyed create(long, ...) below with the + * tag for the direction, or set the name on the span, which resolves it by its kind. Prefer + * the id-keyed factories generally: these are expected to be deprecated. + */ + /** * Entry for {@code (tag, value)}, or null when {@code value} is null or an empty {@code * CharSequence} -- checked by runtime type, so an empty String passed as {@code Object} skips * the same as via the {@link #create(String, CharSequence)} overload. + * + * @throws IllegalArgumentException if {@code tag}'s tag depends on the span's direction */ @Nullable public static final Entry create(@Nonnull String tag, Object value) { - return isEmptyValue(value) ? null : TagMap.anyEntryFor(tag, value); + return isEmptyValue(value) ? null : new Entry(directionFreeKeyOf(tag), tag, ANY, 0L, value); } /** If value is non-null, returns a new TagMap.Entry If value is null or empty, returns null */ @Nullable public static final Entry create(@Nonnull String tag, CharSequence value) { // NOTE: From the static typing, we know that value is not a primitive box - return isEmptyValue(value) ? null : TagMap.objectEntryFor(tag, value); + return isEmptyValue(value) ? null : new Entry(directionFreeKeyOf(tag), tag, OBJECT, 0, value); } public static final Entry create(@Nonnull String tag, boolean value) { - return TagMap.booleanEntryFor(tag, value); + return new Entry( + directionFreeKeyOf(tag), tag, BOOLEAN, boolean2Prim(value), Boolean.valueOf(value)); } public static final Entry create(@Nonnull String tag, int value) { - return TagMap.intEntryFor(tag, value); + return new Entry(directionFreeKeyOf(tag), tag, INT, int2Prim(value), null); } public static final Entry create(@Nonnull String tag, long value) { - return TagMap.longEntryFor(tag, value); + return new Entry(directionFreeKeyOf(tag), tag, LONG, long2Prim(value), null); } public static final Entry create(@Nonnull String tag, float value) { - return TagMap.floatEntryFor(tag, value); + return new Entry(directionFreeKeyOf(tag), tag, FLOAT, float2Prim(value), null); } public static final Entry create(@Nonnull String tag, double value) { - return TagMap.doubleEntryFor(tag, value); + return new Entry(directionFreeKeyOf(tag), tag, DOUBLE, double2Prim(value), null); + } + + /** {@code tag}'s id, or 0 for a custom tag; rejects a name whose tag depends on direction. */ + private static long directionFreeKeyOf(String tag) { + long key = KnownTagCodec.lookup(tag); + if (key < 0) { + throw new IllegalArgumentException( + "'" + + tag + + "' names a different tag depending on the span's direction; create the entry" + + " with a KnownTags id for the direction, or set the name on the span"); + } + return key; } /* @@ -1414,12 +1438,31 @@ public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must canonicalize the same way, or it would hash to the wrong bucket // and silently miss the entry stored under the Datadog name. - long tagHash = Entry.tagHashOf(tag); - String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; - return this.getEntry(canonicalTag, tagHash); + long tagId = KnownTagCodec.keyOrSharedName(tag); + if (tagId == 0) { + return this.getEntry(tag, Entry.customHash(tag)); + } else if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { + return this.getEntry(KnownTagCodec.nameOf(tagId), tagId); + } + // A Datadog name shared by a tag per direction: a map holds at most one of them per span. + for (int direction = KnownTagCodec.DIRECTION_INBOUND; + direction <= KnownTagCodec.DIRECTION_NONE; + direction++) { + long sharingId = KnownTagCodec.directionalKeyOf(tag, direction); + Entry entry = sharingId > 0 ? this.getEntry(tag, sharingId) : null; + if (entry != null) { + return entry; + } + } + // Set by that name alone, with no direction: stored as a custom tag. + return this.getEntry(tag, Entry.customHash(tag)); } - /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ + /** + * The entry for a known tag id. Unlike a lookup by a Datadog name shared by a tag per direction + * ({@code peer.port}), which finds whichever of those tags the map holds, this finds only the + * one. + */ public Entry getEntry(long tagId) { return this.getEntry(Entry.requireKnownName(tagId), tagId); } @@ -1456,7 +1499,7 @@ private Entry getLocalEntry(String tag, long tagHash) { private static Entry findInBucket(Object bucket, int hash, String tag) { if (bucket instanceof Entry) { Entry tagEntry = (Entry) bucket; - return tagEntry.matches(tag) ? tagEntry : null; + return tagEntry.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; } else if (bucket instanceof BucketGroup) { return ((BucketGroup) bucket).findInChain(hash, tag); } @@ -1700,7 +1743,7 @@ private Entry putEntry(@Nonnull Entry newEntry) { return null; } else if (bucket instanceof Entry) { Entry existingEntry = (Entry) bucket; - if (existingEntry.matches(newEntry.tag)) { + if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { thisBuckets[bucketIndex] = newEntry; // replaced existing entry - no size change @@ -2014,9 +2057,24 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must canonicalize first to find (and tombstone) the right entry. - long tagHash = Entry.tagHashOf(tag); - String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; - return this.getAndRemove(canonicalTag, tagHash); + long tagId = KnownTagCodec.keyOrSharedName(tag); + if (tagId == 0) { + return this.getAndRemove(tag, Entry.customHash(tag)); + } else if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { + return this.getAndRemove(KnownTagCodec.nameOf(tagId), tagId); + } + // A shared name removes whichever of its tags the map holds, and any custom tag of that name. + Entry removed = this.getAndRemove(tag, Entry.customHash(tag)); + for (int direction = KnownTagCodec.DIRECTION_INBOUND; + direction <= KnownTagCodec.DIRECTION_NONE; + direction++) { + long sharingId = KnownTagCodec.directionalKeyOf(tag, direction); + Entry entry = sharingId > 0 ? this.getAndRemove(tag, sharingId) : null; + if (removed == null) { + removed = entry; + } + } + return removed; } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ @@ -2064,7 +2122,7 @@ private Entry removeLocal(String tag, long tagHash) { // null bucket case - do nothing if (bucket instanceof Entry) { Entry existingEntry = (Entry) bucket; - if (existingEntry.matches(tag)) { + if (existingEntry.hash() == hash && existingEntry.matches(tag)) { thisBuckets[bucketIndex] = null; this.size -= 1; diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index 08570b034fa..f5cf0050783 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -1,5 +1,9 @@ package datadog.trace.api; +import static datadog.trace.api.KnownTagCodec.DIRECTION_INBOUND; +import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; +import static datadog.trace.api.KnownTagCodec.DIRECTION_OUTBOUND; +import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -176,6 +180,69 @@ void aNameDeclaredPerDirectionIsOneTagPerDirection() { assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); } + static Stream directionalNames() { + return Stream.of( + Arguments.of(Tags.PEER_PORT, DIRECTION_INBOUND, KnownTags.PEER_PORT_INBOUND_ID), + Arguments.of(Tags.PEER_PORT, DIRECTION_OUTBOUND, KnownTags.PEER_PORT_OUTBOUND_ID), + Arguments.of(Tags.PEER_PORT, DIRECTION_NONE, 0L), + Arguments.of(Tags.PEER_PORT, DIRECTION_UNKNOWN, 0L), + Arguments.of("server.address", DIRECTION_INBOUND, KnownTags.HTTP_HOSTNAME_ID), + Arguments.of("server.address", DIRECTION_OUTBOUND, KnownTags.PEER_HOSTNAME_ID), + Arguments.of("server.address", DIRECTION_UNKNOWN, 0L), + Arguments.of("server.port", DIRECTION_OUTBOUND, KnownTags.PEER_PORT_OUTBOUND_ID), + Arguments.of("server.port", DIRECTION_INBOUND, 0L), + Arguments.of("client.port", DIRECTION_INBOUND, KnownTags.PEER_PORT_INBOUND_ID), + Arguments.of("client.port", DIRECTION_OUTBOUND, 0L), + Arguments.of("network.peer.address", DIRECTION_INBOUND, KnownTags.NETWORK_CLIENT_IP_ID)); + } + + /** A name whose tag depends on the span's direction resolves only once the direction is known. */ + @ParameterizedTest(name = "{0} on direction {1}") + @MethodSource("directionalNames") + void aDirectionalNameResolvesByDirection(String name, int direction, long expected) { + assertEquals(expected, KnownTagCodec.keyOf(name, direction)); + assertEquals(0L, KnownTagCodec.keyOf(name)); + } + + @Test + void aDirectionFreeNameResolvesTheSameInEveryDirection() { + for (int direction = DIRECTION_UNKNOWN; direction <= DIRECTION_NONE; direction++) { + assertEquals(KnownTags.HTTP_METHOD_ID, KnownTagCodec.keyOf("http.request.method", direction)); + assertEquals(KnownTags.PEER_HOSTNAME_ID, KnownTagCodec.keyOf(Tags.PEER_HOSTNAME, direction)); + assertEquals(0L, KnownTagCodec.keyOf("my.custom.tag", direction)); + } + } + + @Test + void aDirectionScopedRenameIsEmittedOnlyInItsDirection() { + assertEquals( + "server.address", + KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND)); + assertEquals( + Tags.HTTP_HOSTNAME, + KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_OUTBOUND_ID, DIRECTION_OUTBOUND)); + assertEquals( + "client.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_INBOUND_ID, DIRECTION_INBOUND)); + assertEquals( + "http.request.method", + KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN)); + assertEquals(Tags.HTTP_HOSTNAME, KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID)); + } + + /** A tag declared per direction exists only on spans of its direction, so its name needs none. */ + @Test + void aTagDeclaredPerDirectionIsEmittedWithoutADirection() { + assertEquals("server.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertEquals("client.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_OUTBOUND_ID, DIRECTION_UNKNOWN)); + } + @Test void unknownIdsResolveToNullName() { assertNull(KnownTagCodec.nameOf(0L)); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java new file mode 100644 index 00000000000..adf18cc6e83 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java @@ -0,0 +1,52 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +/** + * {@link TagMap.Entry#create(String, Object)} and its overloads: an entry is built with no span, so + * a name whose tag depends on the span's direction is rejected. + */ +class TagMapEntryCreateByNameTest { + @TableTest({ + "scenario | name ", + "shared Datadog name | peer.port ", + "OTel name, both directions | server.address ", + "OTel name, outbound only | server.port ", + "OTel name, inbound only | client.port ", + "OTel name, inbound only (ip) | network.peer.address" + }) + void aNameWhoseTagDependsOnDirectionIsRejected(String name) { + assertThrows(IllegalArgumentException.class, () -> TagMap.Entry.create(name, 5432)); + assertThrows(IllegalArgumentException.class, () -> TagMap.Entry.create(name, "value")); + assertThrows(IllegalArgumentException.class, () -> TagMap.Entry.create(name, (Object) "value")); + } + + @Test + void aKnownNameCreatesTheKnownTag() { + assertEquals( + KnownTags.HTTP_METHOD_ID, TagMap.Entry.create("http.request.method", "GET").tagId()); + assertEquals( + KnownTags.HTTP_METHOD_NAME, TagMap.Entry.create("http.request.method", "GET").tag()); + } + + @Test + void aCustomNameCreatesACustomTag() { + TagMap.Entry entry = TagMap.Entry.create("my.custom.tag", 3); + + assertEquals(0L, entry.tagId()); + assertEquals("my.custom.tag", entry.tag()); + assertEquals(3, entry.intValue()); + } + + @Test + void theIdOfATagDeclaredPerDirectionCreatesIt() { + TagMap.Entry entry = TagMap.Entry.create(KnownTags.PEER_PORT_OUTBOUND_ID, (Object) 5432); + + assertEquals(KnownTags.PEER_PORT_OUTBOUND_ID, entry.tagId()); + assertEquals("peer.port", entry.tag()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index dd477db0eb9..18f5f56298e 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -3,7 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import datadog.trace.bootstrap.instrumentation.api.Tags; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -82,11 +85,60 @@ void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessExcept assertEquals(tagIds.size(), copy.size()); for (long tagId : tagIds) { String name = KnownTagCodec.nameOf(tagId); - assertEquals("value", map.getObject(name), name); - assertEquals("value", copy.getObject(name), name); + assertEquals("value", map.getEntry(tagId).objectValue(), name); + assertEquals("value", copy.getEntry(tagId).objectValue(), name); } } + /** + * A Datadog name shared by a tag per direction names no single tag, but a span holds only one of + * them, so a lookup by that name finds whichever the map holds. + */ + @Test + void aSharedNameFindsWhicheverOfItsTagsTheMapHolds() { + TagMap inbound = TagMap.create(); + inbound.set(KnownTags.PEER_PORT_INBOUND_ID, 1234); + TagMap outbound = TagMap.create(); + outbound.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); + + assertEquals(1234, inbound.getEntry(Tags.PEER_PORT).intValue()); + assertEquals(5432, outbound.getEntry(Tags.PEER_PORT).intValue()); + assertEquals(5432, outbound.get(Tags.PEER_PORT)); + assertTrue(outbound.containsKey(Tags.PEER_PORT)); + assertEquals(5432, outbound.copy().get(Tags.PEER_PORT)); + assertNull(outbound.getEntry(KnownTags.PEER_PORT_INBOUND_ID)); + } + + @Test + void aSharedNameSetWithoutADirectionIsACustomTag() { + TagMap map = TagMap.create(); + map.set(Tags.PEER_PORT, 5432); + + assertEquals(5432, map.getEntry(Tags.PEER_PORT).intValue()); + assertEquals(0L, map.getEntry(Tags.PEER_PORT).tagId()); + } + + @Test + void removingASharedNameRemovesWhicheverOfItsTagsTheMapHolds() { + TagMap map = TagMap.create(); + map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); + + assertEquals(5432, map.getAndRemove(Tags.PEER_PORT).intValue()); + assertNull(map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertEquals(0, map.size()); + } + + @Test + void aSharedNameReadsThroughToTheParent() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); + TagMap child = TagMap.createFromParent(parent.freeze()); + + assertEquals(5432, child.get(Tags.PEER_PORT)); + child.remove(Tags.PEER_PORT); + assertNull(child.get(Tags.PEER_PORT)); + } + private static List knownTagIds() throws IllegalAccessException { List tagIds = new ArrayList<>(); for (Field field : KnownTags.class.getFields()) { From d5091197e54be45ac29ea63b8fd62a226280dc35 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 22:02:14 -0400 Subject: [PATCH 19/35] Key shared-name tags by direction now that resolution knows it #12817 rejected shared-name ids (peer.port inbound/outbound) on the id paths until name resolution could see a span's direction. With direction resolution in place, accept them again: a read or removal by the shared name already checks each direction's id. To keep one entry per tag, a write by the shared name stores under whichever direction's tag the map holds (otherwise under the name, which the span re-keys once it has a direction), and a write by id drops a value held under the name alone. isKeyableById goes; the SHARED_NAME bit stays and keeps the new check foldable for a constant id. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 23 +++--- .../trace/core/DDSpanSetTagByIdTest.java | 7 +- .../java/datadog/trace/api/KnownTagCodec.java | 11 +-- .../main/java/datadog/trace/api/TagMap.java | 82 ++++++++++++++----- .../java/datadog/trace/api/KnownTagsTest.java | 2 - .../datadog/trace/api/TagMapSetByIdTest.java | 34 ++++++-- .../datadog/trace/api/TagMapTagHashTest.java | 7 +- 7 files changed, 108 insertions(+), 58 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 26b9f1555fc..f8d8eb91918 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1146,13 +1146,13 @@ public void removeTag(String tag) { } /** - * Removes a known tag by id rather than by its name. An id the id-keyed setters ignore is ignored - * here too. + * Removes a known tag by id rather than by its name. An id that names no known tag is ignored, + * like the id-keyed setters do. * * @param tagId a {@code KnownTags.*_ID} constant */ public void removeTag(long tagId) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagId == KnownTags.SPAN_KIND_ID) { @@ -1210,11 +1210,10 @@ public void setTag(final String tag, final String value) { /* * Id-keyed setters, mirroring the String setters above. With a constant id, the * KnownTagCodec.INTERCEPTED test folds, leaving only the split-by-tags table load; an id set never - * takes the custom-tag path. An id that names no known tag, or whose name is shared across - * directions (see KnownTagCodec#isKeyableById), is ignored, like a null tag. + * takes the custom-tag path. An id that names no known tag is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { @@ -1228,7 +1227,7 @@ public void setTag(final long tagId, final Object value) { } public void setTag(final long tagId, final CharSequence value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { @@ -1242,7 +1241,7 @@ public void setTag(final long tagId, final CharSequence value) { } public void setTag(final long tagId, final boolean value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1255,7 +1254,7 @@ public void setTag(final long tagId, final boolean value) { } public void setTag(final long tagId, final int value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1268,7 +1267,7 @@ public void setTag(final long tagId, final int value) { } public void setTag(final long tagId, final long value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1281,7 +1280,7 @@ public void setTag(final long tagId, final long value) { } public void setTag(final long tagId, final float value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1294,7 +1293,7 @@ public void setTag(final long tagId, final float value) { } public void setTag(final long tagId, final double value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index 34fc1bc2028..cb08be5fe0b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -62,13 +62,14 @@ void clearingAnUnknownIdIsIgnored() { } @Test - void aSharedNameIdIsIgnoredAndLeavesTheNamedTag() { + void aSharedNameIdReplacesAValueSetUnderTheSharedName() { span.setTag("peer.port", 8080); span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); - span.setTag(KnownTags.PEER_PORT_INBOUND_ID, (Object) null); + span.setTag( + KnownTags.PEER_PORT_INBOUND_ID, (Object) null); // the other direction: nothing to clear - assertEquals(8080, span.getTag("peer.port")); + assertEquals(443, span.getTag("peer.port")); } @Test diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 05512eacc61..ec49d4619bf 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -64,7 +64,7 @@ public static boolean isIntercepted(long tagId) { /** * SHARED_NAME bit (low-32 carve, bit 0): marks one direction of a tag declared once per direction * under a shared Datadog name, such as {@code peer.port}. {@link #keyOf} resolves that name to - * neither id, so the name alone cannot find an entry stored under one of them. + * neither id; TagMap reads and writes by that name check each direction's id instead. */ public static final long SHARED_NAME = 1L << 0; @@ -73,15 +73,6 @@ public static boolean hasSharedName(long tagId) { return (tagId & SHARED_NAME) != 0L; } - /** - * True if {@code tagId} can key a {@link TagMap} entry: a known tag whose name resolves back to - * it. A {@link #hasSharedName shared-name} id cannot, until name resolution knows the span's - * direction. Like {@link #isKnown}, this folds away for a constant id. - */ - public static boolean isKeyableById(long tagId) { - return isKnown(tagId) && !hasSharedName(tagId); - } - /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 445fca16bf2..aebc8c8a5f4 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -494,11 +494,6 @@ private static String requireKnownName(long tagId) { if (name == null) { throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); } - if (KnownTagCodec.hasSharedName(tagId)) { - // Name-keyed access hashes the shared name as a custom tag, so it could never find an entry - // keyed by this id; set the tag by name until resolution knows the span's direction. - throw new IllegalArgumentException("tag id has a shared name, set it by name: " + name); - } return name; } @@ -1538,7 +1533,10 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { @Deprecated @Override public Object put(@Nonnull String tag, Object value) { - TagMap.Entry entry = this.getAndSet(anyEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + TagMap.Entry entry = + this.getAndSet( + tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); return entry == null ? null : entry.objectValue(); } @@ -1557,6 +1555,28 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { * Entry factories for callers that start from a name: resolve it once, then build a known tag * from its id or a custom tag from its name. (Entry's own name factories are custom-only.) */ + /** + * The id a write by name stores under: the tag's id, 0 for a custom tag, or -- for a Datadog name + * shared by a tag per direction ({@code peer.port}) -- whichever of those tags this map already + * holds. When it holds neither, the write stays a custom tag under the name, which a span re-keys + * once it knows its direction. + */ + private long writeKeyOf(String tag) { + long tagId = KnownTagCodec.keyOrSharedName(tag); + if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { + return tagId; + } + for (int direction = KnownTagCodec.DIRECTION_INBOUND; + direction <= KnownTagCodec.DIRECTION_NONE; + direction++) { + long sharingId = KnownTagCodec.directionalKeyOf(tag, direction); + if (sharingId > 0 && this.getEntry(tag, sharingId) != null) { + return sharingId; + } + } + return 0L; + } + static Entry anyEntryFor(String tag, Object value) { long tagId = KnownTagCodec.keyOf(tag); return tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value); @@ -1593,7 +1613,7 @@ static Entry doubleEntryFor(String tag, double value) { } public void set(@Nonnull String tag, @Nonnull Object value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1602,7 +1622,7 @@ public void set(@Nonnull String tag, @Nonnull Object value) { } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1611,7 +1631,7 @@ public void set(@Nonnull String tag, @Nonnull CharSequence value) { } public void set(@Nonnull String tag, boolean value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1620,7 +1640,7 @@ public void set(@Nonnull String tag, boolean value) { } public void set(@Nonnull String tag, int value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1629,7 +1649,7 @@ public void set(@Nonnull String tag, int value) { } public void set(@Nonnull String tag, long value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1638,7 +1658,7 @@ public void set(@Nonnull String tag, long value) { } public void set(@Nonnull String tag, float value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1647,7 +1667,7 @@ public void set(@Nonnull String tag, float value) { } public void set(@Nonnull String tag, double value) { - long tagId = KnownTagCodec.keyOf(tag); + long tagId = this.writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1724,6 +1744,14 @@ public Entry getAndSet(@Nullable Entry newEntry) { private Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); + long newTagHash = newEntry.tagHash; + if ((newTagHash >>> 32) != 0 && KnownTagCodec.hasSharedName(newTagHash)) { + // One direction of a tag declared per direction, written by id, supersedes a value written + // under the shared name alone (before a span knew its direction), so the map never holds + // both. + this.removeLocal(newEntry.tag, Entry.customHash(newEntry.tag)); + } + // Re-setting a key clears any read-through tombstone for it (the new value overrides the // removal). Gated on the lazy field, so this is a no-op for the common no-tombstone case. if (this.removedFromParent != null) { @@ -1776,31 +1804,45 @@ private Entry putEntry(@Nonnull Entry newEntry) { } public Entry getAndSet(@Nonnull String tag, Object value) { - return this.getAndSet(anyEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); } public Entry getAndSet(@Nonnull String tag, CharSequence value) { - return this.getAndSet(objectEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, boolean value) { - return this.getAndSet(booleanEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, int value) { - return this.getAndSet(intEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, long value) { - return this.getAndSet(longEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, float value) { - return this.getAndSet(floatEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, double value) { - return this.getAndSet(doubleEntryFor(tag, value)); + long tagId = this.writeKeyOf(tag); + return this.getAndSet( + tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value)); } public void putAll(Map map) { diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index f5cf0050783..a039a2bb088 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -175,8 +175,6 @@ void aNameDeclaredPerDirectionIsOneTagPerDirection() { assertNotEquals(KnownTags.PEER_PORT_INBOUND_ID, KnownTags.PEER_PORT_OUTBOUND_ID); assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_INBOUND_ID)); assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertFalse(KnownTagCodec.isKeyableById(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertTrue(KnownTagCodec.isKeyableById(KnownTags.PEER_HOSTNAME_ID)); assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 88ac5d57ca5..d558ecf59dc 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -74,12 +74,34 @@ void anUnknownIdIsRejected() { } @Test - void rejectsSharedNameIds() { + void aSharedNameIdIsFoundByItsSharedName() { TagMap map = TagMap.create(); - assertThrows(IllegalArgumentException.class, () -> map.set(KnownTags.PEER_PORT_INBOUND_ID, 80)); - assertThrows( - IllegalArgumentException.class, () -> map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 443)); - assertThrows( - IllegalArgumentException.class, () -> map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID)); + map.set(KnownTags.PEER_PORT_INBOUND_ID, 80); + + assertEquals(80, map.getEntry(KnownTags.PEER_PORT_INBOUND_ID).intValue()); + assertEquals(80, map.getEntry("peer.port").intValue()); + } + + @Test + void aWriteBySharedNameReplacesTheHeldDirectionsTag() { + TagMap map = TagMap.create(); + map.set(KnownTags.PEER_PORT_INBOUND_ID, 80); + + map.set("peer.port", 81); + + assertEquals(1, map.size()); + assertEquals(81, map.getEntry(KnownTags.PEER_PORT_INBOUND_ID).intValue()); + } + + @Test + void aWriteByIdSupersedesAValueSetUnderTheSharedNameAlone() { + TagMap map = TagMap.create(); + map.set("peer.port", 8080); // no direction known: stored under the name + + map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 443); + + assertEquals(1, map.size()); + assertEquals(443, map.getEntry("peer.port").intValue()); + assertEquals(443, map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID).intValue()); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index 18f5f56298e..d5925380f62 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -143,10 +143,7 @@ private static List knownTagIds() throws IllegalAccessException { List tagIds = new ArrayList<>(); for (Field field : KnownTags.class.getFields()) { if (field.getType() == long.class && Modifier.isStatic(field.getModifiers())) { - long tagId = field.getLong(null); - if (KnownTagCodec.isKeyableById(tagId)) { - tagIds.add(tagId); - } + tagIds.add(field.getLong(null)); } } return tagIds; From dac7060632fc966c08c1009553b5065eedfe6c9d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 10 Oct 2026 12:18:41 -0400 Subject: [PATCH 20/35] Give the tracer core a restricted, trusted path into TagMap TagMap.internals() returns a small Internals view whose setKnown(...) skips the id validation the checked set(long, ...) repeats; DDSpanContext's id-keyed setters, which have already checked isKeyableById, now use it. Internals is created per call, so escape analysis removes it. internals() is marked with a new @Restricted(allowedIn = ...) annotation, and gradle/forbiddenApiFilters/instrumentation.txt bans it in instrumentation modules, so calling it there fails forbiddenApisMain. Internals is also where the batched setTagsFrom/TagSink writes will go. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 16 +++--- .../forbiddenApiFilters/instrumentation.txt | 5 ++ .../java/datadog/apicontract/Restricted.java | 24 +++++++++ .../main/java/datadog/trace/api/TagMap.java | 53 +++++++++++++++++++ .../datadog/trace/api/TagMapSetByIdTest.java | 18 +++++++ 5 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 internal-api/src/main/java/datadog/apicontract/Restricted.java diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index b533a7de481..a3a86d0e669 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1072,7 +1072,7 @@ public void setTag(final long tagId, final Object value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1086,7 +1086,7 @@ public void setTag(final long tagId, final CharSequence value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1099,7 +1099,7 @@ public void setTag(final long tagId, final boolean value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1112,7 +1112,7 @@ public void setTag(final long tagId, final int value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1125,7 +1125,7 @@ public void setTag(final long tagId, final long value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1138,7 +1138,7 @@ public void setTag(final long tagId, final float value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1151,7 +1151,7 @@ public void setTag(final long tagId, final double value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - unsafeTags.set(tagId, value); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1184,7 +1184,7 @@ public void setTag(TagMap.EntryReader entry) { private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.set(tagId, box); + unsafeTags.internals().setKnown(tagId, box); } } } diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index de3910a8ac8..5ffb1f7868d 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -2,3 +2,8 @@ # Use AgentSpan#setServiceName(String, CharSequence) instead to track the source of the service name @defaultMessage Use AgentSpan#setServiceName(String, CharSequence) instead to track the source of the service name. The source should typically be the instrumentation name. datadog.trace.bootstrap.instrumentation.api.AgentSpan#setServiceName(java.lang.String) + +# TagMap.internals() skips checks the tracer core has already made; it is @Restricted to the core +# (see datadog.apicontract.Restricted). Instrumentation uses TagMap's checked methods. +@defaultMessage TagMap.internals() is restricted to the tracer core; use TagMap's checked methods instead. +datadog.trace.api.TagMap#internals() diff --git a/internal-api/src/main/java/datadog/apicontract/Restricted.java b/internal-api/src/main/java/datadog/apicontract/Restricted.java new file mode 100644 index 00000000000..ab0372fa06f --- /dev/null +++ b/internal-api/src/main/java/datadog/apicontract/Restricted.java @@ -0,0 +1,24 @@ +package datadog.apicontract; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a member that is public only because Java has no narrower visibility across modules: it may + * be used only from the packages in {@link #allowedIn}. It typically trades a safety check for + * speed, trusting its caller to have made the check already. + * + *

This marker changes no runtime behavior. Instrumentation modules are kept out by the + * forbidden-APIs signatures in {@code gradle/forbiddenApiFilters/instrumentation.txt}, which must + * list every {@code @Restricted} member. + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Restricted { + /** Package prefixes allowed to use the member, such as {@code datadog.trace.core}. */ + String[] allowedIn(); +} diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index ea98fa43561..8aaac45ba9d 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1,5 +1,6 @@ package datadog.trace.api; +import datadog.apicontract.Restricted; import datadog.trace.api.function.TriConsumer; import datadog.trace.api.internal.VisibleForTesting; import java.util.AbstractCollection; @@ -1617,6 +1618,58 @@ public void set(@Nonnull String tag, double value) { * canonical name without the name lookup the String setters pay. The id must name a known tag; * custom tags have no id and use the String setters. */ + /** + * The tracer core's trusted operations on this map, which skip checks the core has already made. + * Restricted to the core; instrumentation uses the checked methods. + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public Internals internals() { + return new Internals(this); + } + + /** See {@link TagMap#internals()}. Created per call, so escape analysis removes it. */ + public static final class Internals { + private final TagMap map; + + private Internals(TagMap map) { + this.map = map; + } + + /* + * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has + * already checked KnownTagCodec.isKeyableById(tagId). + */ + public void setKnown(long tagId, @Nonnull Object value) { + this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); + } + + public void setKnown(long tagId, @Nonnull CharSequence value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + } + + public void setKnown(long tagId, boolean value) { + this.map.putEntry( + new Entry( + tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + } + + public void setKnown(long tagId, int value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + } + + public void setKnown(long tagId, long value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + } + + public void setKnown(long tagId, float value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + } + + public void setKnown(long tagId, double value) { + this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + } + } + public void set(long tagId, @Nonnull Object value) { this.putEntry(Entry.newAnyEntry(tagId, value)); } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 88ac5d57ca5..9bc0edc80e9 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -82,4 +82,22 @@ void rejectsSharedNameIds() { assertThrows( IllegalArgumentException.class, () -> map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID)); } + + @Test + void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { + TagMap checked = TagMap.create(); + checked.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + checked.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + checked.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + checked.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + + TagMap trusted = TagMap.create(); + trusted.internals().setKnown(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + trusted.internals().setKnown(KnownTags.HTTP_STATUS_CODE_ID, 200); + trusted.internals().setKnown(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + trusted.internals().setKnown(KnownTags.DD_PROFILING_ENABLED_ID, true); + + assertEquals(new HashMap<>(checked), new HashMap<>(trusted)); + assertEquals(KnownTags.HTTP_ROUTE_ID, trusted.getEntry(KnownTags.HTTP_ROUTE_NAME).tagId()); + } } From fbab5517cb073e1c965bbf5647928af4882b14e2 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 10 Oct 2026 12:21:54 -0400 Subject: [PATCH 21/35] Mark TagMap.Internals @NoEscape It exists to be created, used within the expression, and scalar-replaced; storing one would force a real allocation. Co-Authored-By: Claude Opus 5.5 --- internal-api/src/main/java/datadog/trace/api/TagMap.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 8aaac45ba9d..0dbef18896b 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1,6 +1,7 @@ package datadog.trace.api; import datadog.apicontract.Restricted; +import datadog.trace.api.function.NoEscape; import datadog.trace.api.function.TriConsumer; import datadog.trace.api.internal.VisibleForTesting; import java.util.AbstractCollection; @@ -1627,7 +1628,11 @@ public Internals internals() { return new Internals(this); } - /** See {@link TagMap#internals()}. Created per call, so escape analysis removes it. */ + /** + * See {@link TagMap#internals()}. Created per call so escape analysis removes it: use it within + * the expression ({@code map.internals().setKnown(...)}) rather than storing it. + */ + @NoEscape public static final class Internals { private final TagMap map; From 92fb191d744e15ef21a33b24641fe63d80b28416 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 10 Oct 2026 14:25:59 -0400 Subject: [PATCH 22/35] Point Internals' trust note at isKnown, the span's check here Co-Authored-By: Claude Opus 5.5 --- internal-api/src/main/java/datadog/trace/api/TagMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 8430d17c7f3..0cd1dae06e9 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1705,7 +1705,7 @@ private Internals(TagMap map) { /* * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has - * already checked KnownTagCodec.isKeyableById(tagId). + * already checked KnownTagCodec.isKnown(tagId). */ public void setKnown(long tagId, @Nonnull Object value) { this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); From 06dec0fc6ab88ed1bd3b7e075c0094c72e5834de Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 12:18:42 -0400 Subject: [PATCH 23/35] Store each direction-dependent tag once and apply direction on read peer.port gets one direction-neutral id (PEER_PORT_ID); only its OpenTelemetry name (client.port inbound, server.port outbound) depends on the span's direction. The generator merges per-direction declarations into one tag, and the SHARED_NAME bit and shared-name sentinel go away. Direction-scoped OpenTelemetry names (server.address, client.port, ...) are stored as written. SpanTags resolves them by the rule (name, direction) -> tag when a span reads or removes a tag, with the tag winning over its scoped name. processTagsAndBaggage then folds them into their tags once the kind is final. This removes the unresolvedDirectionalTags deferral and re-keying from DDSpanContext, and with them: - bulk/builder writes bypassing resolution - name-vs-id hash mismatch and parent shadowing in TagMap - Entry equals/hashCode disagreement (equals now compares tagHash) - the race between a deferred write and the first kind - alias write order and kind changes not re-keying Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 103 +++++------- .../buildlogic/tagRegistry/TagRegistry.kt | 106 +++++++----- .../tagRegistry/TagRegistryGenerator.kt | 24 +-- .../TagRegistryGeneratorPluginTest.kt | 1 - .../tagRegistry/TagRegistryGeneratorTest.kt | 52 +++--- .../datadog/trace/core/DDSpanContext.java | 149 +++-------------- .../core/taginterceptor/TagInterceptor.java | 3 +- .../DDSpanDirectionalTagConsistencyTest.java | 133 +++++++++++++++ .../trace/core/DDSpanDirectionalTagsTest.java | 68 +++++--- .../trace/core/DDSpanSetTagByIdTest.java | 6 +- .../taginterceptor/TagInterceptorTest.java | 5 +- .../java/datadog/trace/api/KnownTagCodec.java | 119 ++++++++------ .../main/java/datadog/trace/api/SpanTags.java | 151 ++++++++++++++++++ .../main/java/datadog/trace/api/TagMap.java | 124 +++++--------- .../java/datadog/trace/api/KnownTagsTest.java | 46 +++--- .../trace/api/TagMapDirectionalNameTest.java | 87 ++++++++++ .../api/TagMapEntryCreateByNameTest.java | 8 +- .../datadog/trace/api/TagMapSetByIdTest.java | 32 ---- .../datadog/trace/api/TagMapTagHashTest.java | 52 ------ 19 files changed, 718 insertions(+), 551 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java create mode 100644 internal-api/src/main/java/datadog/trace/api/SpanTags.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java 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 0632b636145..09ad4f35fe1 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 @@ -40,50 +40,36 @@ object KnownTagsEmitter { return u } - val nameOfConst = HashMap() - val idOfConst = HashMap() - val serialOfConst = HashMap() - val otelNameOfConst = HashMap() + val nameOfConst = HashMap() + val idOfConst = HashMap() + val serialOfConst = HashMap() + val otelNameOfConst = HashMap() for (t in reg.tags) { val base = sanitize(t.name) - // Shared Datadog names get no NAME constant because they identify several tags. - // Callers use a direction-specific ID (e.g. PEER_PORT_OUTBOUND_ID); nameOf emits the - // shared Datadog name as a literal. - if (t.sharedNameDirection == null) nameOfConst[t.identity] = unique(withSuffix(base, "_NAME")) - idOfConst[t.identity] = unique(withSuffix(base, "_ID")) - serialOfConst[t.identity] = unique(withSuffix(base, "_SERIAL_NUM")) + nameOfConst[t.ddName] = unique(withSuffix(base, "_NAME")) + idOfConst[t.ddName] = unique(withSuffix(base, "_ID")) + serialOfConst[t.ddName] = unique(withSuffix(base, "_SERIAL_NUM")) // Suffix the pre-suffix base (not nameC), same as the other three: suffixing an // already-suffixed identifier would produce a redundant compound like NAME_OTEL_NAME. - if (t.otelName != null) otelNameOfConst[t.identity] = unique(withSuffix(base, "_OTEL_NAME")) + if (t.otelName != null) otelNameOfConst[t.ddName] = unique(withSuffix(base, "_OTEL_NAME")) } - fun nameC(identity: TagConventions.TagIdentity) = nameOfConst.getValue(identity) + fun nameC(ddName: String) = nameOfConst.getValue(ddName) + fun idC(ddName: String) = idOfConst.getValue(ddName) + fun serialC(ddName: String) = serialOfConst.getValue(ddName) + fun otelNameC(ddName: String) = otelNameOfConst.getValue(ddName) - // The expression nameOf returns: the NAME constant, or the shared name's literal. - fun nameExpr(t: TagRegistry.Tag) = nameOfConst[t.identity] ?: "\"${escape(t.ddName)}\"" - fun idC(identity: TagConventions.TagIdentity) = idOfConst.getValue(identity) - fun serialC(identity: TagConventions.TagIdentity) = serialOfConst.getValue(identity) - fun otelNameC(identity: TagConventions.TagIdentity) = otelNameOfConst.getValue(identity) - - val order = reg.tags.map { it.identity } // stable emit order - // keyOf(name) has no direction, so shared names are left out here and resolve to 0 (unknown); - // they are added below, marked, for keyOf(name, direction). - val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null }.map { it.identity } + val keyOfOrder = reg.tags.map { it.ddName } // stable emit order // canonical name -> OpenTelemetry name, for the reverse (openTelemetryNameOf) switch. - val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.identity to it } }.toMap() - // Names whose tag depends on the span's direction: a shared Datadog name (one tag per direction) - // and a direction-scoped OpenTelemetry name. keyOf(name) resolves none of them; keyOf(name, - // direction) switches on each one's tag per direction. - val directional = sortedMapOf>() - // Shared Datadog names, marked in the keyOf table so a lookup by that name alone can probe its tags. - val sharedNames = sortedSetOf() + val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.ddName to it } }.toMap() + // OpenTelemetry names that denote a tag only on spans of some directions. keyOf(name) resolves + // none of them; directionalKeyOf(name, direction) switches on each one's tag per direction. + val directional = sortedMapOf>() for (t in reg.tags) { - t.sharedNameDirection?.let { - directional.getOrPut(t.ddName) { mutableMapOf() }[it] = t.identity - sharedNames.add(t.ddName) + for ((direction, otel) in t.scopedOtelNames) { + directional.getOrPut(otel) { mutableMapOf() }[direction] = t.ddName } - t.otelDirection?.let { directional.getOrPut(t.declaredOtelName!!) { mutableMapOf() }[it] = t.identity } } - check(directional.keys.none { name -> keyOfOrder.any { it.ddName == name } || name in otelName.values }) { + check(directional.keys.none { name -> name in nameOfConst || name in otelName.values }) { "a direction-dependent name is also direction-free: ${directional.keys}" } return buildString { @@ -104,24 +90,16 @@ object KnownTagsEmitter { ) for (t in reg.tags) { - val direction = t.sharedNameDirection - if (direction == null) { - appendLine(" public static final String ${nameC(t.identity)} = \"${escape(t.ddName)}\";") - } else { - appendLine( - " /** {@code ${escape(t.ddName)}} on ${direction.yamlKey} spans. This ID identifies the direction; " + - "the Datadog name is shared across directions. */" - ) - } - appendLine(" public static final long ${idC(t.identity)} = ${hex(t.id)};") + appendLine(" public static final String ${nameC(t.ddName)} = \"${escape(t.ddName)}\";") + appendLine(" public static final long ${idC(t.ddName)} = ${hex(t.id)};") if (t.otelName != null) { - appendLine(" public static final String ${otelNameC(t.identity)} = \"${escape(t.otelName)}\";") + appendLine(" public static final String ${otelNameC(t.ddName)} = \"${escape(t.otelName)}\";") } 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)}") + for ((direction, otel) in t.scopedOtelNames) append(" -> ${escape(otel)} (${direction.yamlKey})") appendLine(" <${escape(t.required)}>") appendLine() } @@ -130,7 +108,7 @@ object KnownTagsEmitter { // this package (the resolver here, TagInterceptor in core) can name them as case labels. appendLine(" // ---- serial numbers ----") for (t in reg.tags) { - appendLine(" public static final int ${serialC(t.identity)} = ${t.serial};") + appendLine(" public static final int ${serialC(t.ddName)} = ${t.serial};") } // Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag. val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1 @@ -140,7 +118,7 @@ object KnownTagsEmitter { // canonical names. Sort by OTel name to keep output deterministic. val otelByCanonical = reg.tags - .mapNotNull { t -> t.otelName?.let { it to t.identity } } + .mapNotNull { t -> t.otelName?.let { it to t.ddName } } .sortedBy { it.first } // keyOf table (open-addressed, via StringIndex.EmbeddingSupport). Canonical names first, then @@ -167,10 +145,9 @@ object KnownTagsEmitter { otelByCanonical.forEach { (_, canonical) -> appendLine(" ${idC(canonical)},") } - // A direction-dependent name's value is not an id but a marker saying which kind it is. + // A direction-dependent name's value is not an id but a marker saying it depends on direction. for (name in directional.keys) { - val marker = if (name in sharedNames) "SHARED_DATADOG_NAME_SENTINEL" else "DIRECTION_SCOPED_OTEL_NAME_SENTINEL" - appendLine(" KnownTagCodec.$marker, // $name") + appendLine(" KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // $name") } // Resolver. KnownTagCodec.Installed links to this field directly, so merely resolving a tag // name initializes this class -- there is no registration step and no ordering to get wrong. @@ -202,7 +179,7 @@ object KnownTagsEmitter { """.trimIndent() ) for (t in reg.tags.sortedBy { it.serial }) { - appendLine(" ${nameExpr(t)},") + appendLine(" ${nameC(t.ddName)},") } appendLine( """ @@ -230,19 +207,19 @@ object KnownTagsEmitter { """.trimIndent() ) for (t in reg.tags) { - val otel = t.declaredOtelName ?: continue - val scoped = t.otelDirection val result = - when { - scoped == null -> otelNameC(t.identity) - - // A tag declared per direction exists only on spans of its direction, so emitting it - // needs none; only resolving the name to it does. - t.sharedNameDirection != null -> "\"${escape(otel)}\"" - - else -> "direction == KnownTagCodec.DIRECTION_${scoped.name} ? \"${escape(otel)}\" : null" + if (t.otelName != null) { + otelNameC(t.ddName) + } else if (t.scopedOtelNames.isNotEmpty()) { + // Renamed only on spans of some directions: elsewhere, and while the direction is + // unknown, the tag keeps its Datadog name. + t.scopedOtelNames.entries.reversed().fold("null") { rest, (direction, otel) -> + "direction == KnownTagCodec.DIRECTION_${direction.name} ? \"${escape(otel)}\" : $rest" + } + } else { + continue } - appendLine("case ${serialC(t.identity)}:") + appendLine("case ${serialC(t.ddName)}:") appendLine(" return $result;") } appendLine( 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 6b96a1f5283..79d070e7727 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 @@ -13,41 +13,34 @@ package datadog.buildlogic.tagRegistry * 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. + * + *

There is one tag, and one id, per Datadog name. A name the conventions declare once per + * direction, as `peer.port` is, is still one tag: only its OpenTelemetry name depends on the span's + * direction, so direction is applied when a tag is named, never when it is stored. */ class TagRegistry private constructor(val tags: List) { data class Tag( - val identity: TagConventions.TagIdentity, + val ddName: String, val type: String, 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, - /** - * The one direction [declaredOtelName] applies in, or null when it applies in every direction. A - * tag has one declaration, so its rename covers either every direction or exactly one. - */ - val otelDirection: TagConventions.Direction? = null, + /** 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 identity's label, as reports and constant names show it. */ - val name: String = identity.label - val ddName: String = identity.ddName - val sharedNameDirection: TagConventions.Direction? = identity.direction - - /** - * The rename included in the direction-free lookup tables, or null when absent or scoped - * to one direction. Scoped renames remain in `declaredOtelName` for later resolution. - */ - val otelName: String? = declaredOtelName.takeIf { otelDirection == null } + /** The tag as reports and generated constant names show it. */ + val name: String + get() = ddName } 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 /** * Serials stay below 2^15, so bit 63 of an id is never set: every id is positive, clear of 0 @@ -57,11 +50,10 @@ class TagRegistry private constructor(val tags: List) { /** * 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. Every id is made here, and the range check makes each one - * positive by construction. + * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, other low bits and the reserved [47-32] window + * zero. Every id is made here, and the range check makes each one positive by construction. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, sharedName: Boolean = false): Long { + fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long { require(serial in FIRST_SERIAL until SERIAL_CAPACITY) { "serial $serial is outside [$FIRST_SERIAL, $SERIAL_CAPACITY): more tags would make tag ids " + "negative, where KnownTagCodec's lookup sentinels live" @@ -69,30 +61,35 @@ class TagRegistry private constructor(val tags: List) { 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 } fun build(conv: TagConventions): TagRegistry { - val traceLevel = conv.traceLevelTags().map { it.identity }.toSet() - val renames = conv.otelMappings().associateBy { it.tag } + val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet() + val renames = conv.otelMappings().groupBy { it.tag.ddName } - // Stable order (by name) so serials -- and therefore ids -- are a pure function of the input. + // 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. val tags = - conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t -> + conv.allDeclaredTags().groupBy { it.ddName }.toSortedMap().values.mapIndexed { i, decls -> + val first = decls.first() + require(decls.all { it.type == first.type }) { + "'${first.ddName}' is declared per direction with different types: ${decls.map { it.type }}" + } val serial = FIRST_SERIAL + i - val isTraceLevel = t.identity in traceLevel - val isIntercepted = t.ddName in conv.interceptedNames + val isTraceLevel = first.ddName in traceLevel + val isIntercepted = first.ddName in conv.interceptedNames + val (otelName, scoped) = otelNamesOf(first.ddName, renames[first.ddName].orEmpty()) Tag( - t.identity, - t.type, - t.required, + first.ddName, + first.type, + strongestRequirement(decls.map { it.required }), serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null), - declaredOtelName = renames[t.identity]?.otelName, - otelDirection = renames[t.identity]?.direction, + id = encode(serial, isTraceLevel, isIntercepted), + otelName = otelName, + scopedOtelNames = scoped, ) } @@ -100,6 +97,38 @@ class TagRegistry private constructor(val tags: List) { return TagRegistry(tags) } + /** + * 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. + */ + private fun otelNamesOf( + ddName: String, + mappings: List, + ): Pair> { + val everywhere = mappings.filter { (it.direction ?: it.tag.direction) == null }.map { it.otelName }.distinct() + require(everywhere.size <= 1) { "'$ddName' has several direction-free OpenTelemetry names: $everywhere" } + if (everywhere.isNotEmpty()) { + require(mappings.all { it.otelName == everywhere[0] }) { + "'$ddName' has a direction-free OpenTelemetry name and a different one per direction" + } + return everywhere[0] to emptyMap() + } + val scoped = sortedMapOf() + for (m in mappings) { + val direction = m.direction ?: m.tag.direction!! + val prev = scoped.put(direction, m.otelName) + require(prev == null || prev == m.otelName) { + "'$ddName' has two OpenTelemetry names on ${direction.yamlKey} spans: '$prev' and '${m.otelName}'" + } + } + return null to scoped + } + + private val REQUIREMENT_STRENGTH = listOf("required", "conditional", "recommended", "optional") + + /** The strongest of a name's requirement levels across its declarations per direction. */ + private fun strongestRequirement(levels: List): String = levels.minBy { REQUIREMENT_STRENGTH.indexOf(it).let { i -> if (i < 0) Int.MAX_VALUE else i } } + /** * Rejects OpenTelemetry names that collide with any Datadog name. Within each direction, * a rename must also belong to a single tag. Direction-free renames reserve their name @@ -110,8 +139,9 @@ class TagRegistry private constructor(val tags: List) { val canonical = tags.map { it.ddName }.toSet() val owner = HashMap, String>() for (t in tags) { - val otel = t.declaredOtelName ?: continue - for (direction in t.otelDirection?.let { listOf(it) } ?: TagConventions.Direction.entries) { + val byDirection = + t.otelName?.let { otel -> TagConventions.Direction.entries.associateWith { otel } } ?: t.scopedOtelNames + for ((direction, otel) in byDirection) { require(otel !in canonical) { "OpenTelemetry name '$otel' (of '${t.name}') collides with canonical tag name '$otel'" } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index 50960175cc0..f2a44a962aa 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -14,8 +14,7 @@ object TagRegistryGenerator { */ fun generate(tagConventionsFile: File, outDir: File, tracerOverlayFile: File? = null) { val mapper = ObjectMapper(YAMLFactory()) - fun read(file: File): Map = - file.inputStream().use { mapper.readValue(it, object : TypeReference>() {}) } ?: emptyMap() + fun read(file: File): Map = file.inputStream().use { mapper.readValue(it, object : TypeReference>() {}) } ?: emptyMap() val domain = read(tagConventionsFile) val overlay = tracerOverlayFile?.let(::read) ?: emptyMap() @@ -102,23 +101,16 @@ object TagRegistryGenerator { """ # DIRECTION-SCOPED OPENTELEMETRY NAMES. Each applies only on spans of the given direction, so - # it is not in the tables above; name resolution does not use it until it knows the direction. + # it is not in the tables above: a tag is emitted under it when its span has that direction, + # and a value written under it is resolved to the tag once the span's direction is known. """.trimIndent() ) val scoped = - reg.tags.filter { it.otelDirection != null }.sortedWith(compareBy({ it.declaredOtelName }, { it.otelDirection })) - for (t in scoped) { - appendLine(" %-30s %-9s -> %s".format(Locale.ROOT, t.declaredOtelName, t.otelDirection!!.yamlKey, t.name)) - } - appendLine( - """ - - # SHARED DATADOG NAMES. One Datadog name for a tag per direction: emitting it needs no context, - # but resolving the name to a tag needs the span's direction, so keyOf does not resolve it yet. - """.trimIndent() - ) - for ((ddName, shared) in reg.tags.filter { it.sharedNameDirection != null }.groupBy { it.ddName }.toSortedMap()) { - appendLine(" %-30s -> %s".format(Locale.ROOT, ddName, shared.joinToString(", ") { it.name })) + reg.tags + .flatMap { t -> t.scopedOtelNames.map { (direction, otel) -> Triple(otel, direction, t.name) } } + .sortedWith(compareBy({ it.first }, { it.second })) + for ((otel, direction, canonical) in scoped) { + appendLine(" %-30s %-9s -> %s".format(Locale.ROOT, otel, direction.yamlKey, canonical)) } } } diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt index b37ae96dee7..442cf2b9bc6 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt @@ -248,7 +248,6 @@ class TagRegistryGeneratorPluginTest { public static final int DIRECTION_INBOUND = 0; public static final int DIRECTION_OUTBOUND = 1; public static final int DIRECTION_NONE = 2; - static final long SHARED_DATADOG_NAME_SENTINEL = -1L; static final long DIRECTION_SCOPED_OTEL_NAME_SENTINEL = -2L; public static final long NO_TAG_IN_DIRECTION_SENTINEL = -3L; public interface Resolver { diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index b47fe5f0cf4..d49499d1022 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -330,23 +330,21 @@ class TagRegistryGeneratorTest { val tags = TagRegistry.build(conventions).tags.associateBy { it.name } - assertThat(tags.getValue("http.hostname").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.address" to TagConventions.Direction.INBOUND) - assertThat(tags.getValue("peer.hostname").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.address" to TagConventions.Direction.OUTBOUND) - assertThat(tags.getValue("peer.port@outbound").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.port" to TagConventions.Direction.OUTBOUND) - assertThat(tags.getValue("peer.port@inbound").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("client.port" to TagConventions.Direction.INBOUND) - assertThat(tags.values.filter { it.name.startsWith("peer.port") }.map { it.ddName }) - .containsOnly("peer.port") + assertThat(tags.getValue("http.hostname").scopedOtelNames) + .isEqualTo(mapOf(TagConventions.Direction.INBOUND to "server.address")) + assertThat(tags.getValue("peer.hostname").scopedOtelNames) + .isEqualTo(mapOf(TagConventions.Direction.OUTBOUND to "server.address")) + assertThat(tags.getValue("peer.port").scopedOtelNames) + .isEqualTo( + mapOf(TagConventions.Direction.INBOUND to "client.port", TagConventions.Direction.OUTBOUND to "server.port") + ) assertThat(tags.values.map { it.otelName }).containsOnlyNulls() assertThat(conventions.resolve("http.client").map { it.name }).contains("peer.port@outbound") assertThat(conventions.resolve("http.server").map { it.name }).contains("peer.port@inbound") } @Test - fun `a Datadog name declared per direction resolves only with a direction`() { + fun `a Datadog name declared per direction is one tag`() { val yaml = directory.conventionsFile( """ span_types: @@ -364,20 +362,17 @@ class TagRegistryGeneratorTest { val generated = contents(output) val source = generated.getValue("java/datadog/trace/api/KnownTags.java") assertThat(source) - .contains("PEER_PORT_INBOUND_ID", "PEER_PORT_OUTBOUND_ID") - .contains("{@code peer.port} on outbound spans.", "{@code peer.port} on inbound spans.") - .doesNotContain("PEER_PORT_NAME", "PEER_PORT_INBOUND_NAME", "PEER_PORT_OUTBOUND_NAME") + .contains("PEER_PORT_NAME = \"peer.port\"", "PEER_PORT_ID") + .doesNotContain("PEER_PORT_INBOUND", "PEER_PORT_OUTBOUND", "case \"peer.port\":") assertThat(source.substringAfter("KEYOF_VALUES = {").substringBefore("};")) - .doesNotContain("PEER_PORT") - .contains("KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL, // peer.port") - assertThat(source.substringAfter("case \"peer.port\":").substringBefore("default:")) - .contains("return PEER_PORT_INBOUND_ID;", "return PEER_PORT_OUTBOUND_ID;") - assertThat(generated.getValue("tag-assignment.txt")) - .containsPattern("peer\\.port +-> peer\\.port@inbound, peer\\.port@outbound") + .contains("PEER_PORT_ID,") + .doesNotContain("SENTINEL") + assertThat(generated.getValue("tag-assignment.txt").lines().filter { it.endsWith(" peer.port") }) + .hasSize(1) } @Test - fun `a tag declared per direction is emitted under its OpenTelemetry name without a direction`() { + fun `a tag declared per direction is emitted under the OpenTelemetry name for the direction`() { val yaml = directory.conventionsFile( """ span_types: @@ -397,8 +392,14 @@ class TagRegistryGeneratorTest { val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") val emit = source.substringAfter("public String openTelemetryNameOf(long tagId, int direction)") - assertThat(emit).contains("return \"server.port\";", "return \"client.port\";") + assertThat(emit).contains( + "return direction == KnownTagCodec.DIRECTION_INBOUND ? \"client.port\" : " + + "direction == KnownTagCodec.DIRECTION_OUTBOUND ? \"server.port\" : null;" + ) assertThat(emit).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") + assertThat(source.substringAfter("case \"server.port\":").substringBefore("default:")) + .contains("case KnownTagCodec.DIRECTION_OUTBOUND:", "return PEER_PORT_ID;") + .doesNotContain("DIRECTION_INBOUND") } @Test @@ -431,7 +432,7 @@ class TagRegistryGeneratorTest { val dbType = TagRegistry.build(conventions).tags.single() assertThat(dbType.otelName).isEqualTo("db.system") - assertThat(dbType.otelDirection).isNull() + assertThat(dbType.scopedOtelNames).isEmpty() } @TableTest( @@ -489,12 +490,13 @@ class TagRegistryGeneratorTest { """ ) - assertThat(TagRegistry.build(conventions).tags.map { it.identity }) + assertThat(conventions.allDeclaredTags().map { it.identity }) .containsExactlyInAnyOrder( TagConventions.TagIdentity("x", TagConventions.Direction.INBOUND), TagConventions.TagIdentity("x", TagConventions.Direction.OUTBOUND), TagConventions.TagIdentity("x@inbound"), ) + assertThat(TagRegistry.build(conventions).tags.map { it.ddName }).containsExactly("x", "x@inbound") } @Test @@ -512,7 +514,7 @@ class TagRegistryGeneratorTest { ) assertThat(TagRegistry.build(conventions).tags.map { it.name }) - .containsExactlyInAnyOrder("peer.port@inbound", "peer.port@outbound", "user@domain") + .containsExactlyInAnyOrder("peer.port", "user@domain") } @Test diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index f8d8eb91918..a323cd1472d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -13,6 +13,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; +import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; @@ -41,7 +42,6 @@ import datadog.trace.util.TagsHelper; import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -129,8 +129,9 @@ public class DDSpanContext }; /** - * The direction each span kind ordinal resolves direction-dependent tag names in ({@code - * peer.port} is the client's port on a server span). Broker and custom kinds have none. + * The direction each span kind ordinal reads direction-dependent tag names in ({@code + * server.address} is {@code peer.hostname} on a client span). Broker and custom kinds have none. + * Mirrors {@link SpanTags#directionOfKind}, by ordinal. */ private static final int[] TAG_DIRECTION_OF_SPAN_KIND = { KnownTagCodec.DIRECTION_UNKNOWN, // UNSET @@ -145,12 +146,6 @@ public class DDSpanContext private volatile byte spanKindOrdinal = SPAN_KIND_UNSET; - /** - * Set when a direction-dependent tag name was stored before the span had a direction, so it is - * stored under its bare name. Setting the span kind then re-keys it to the tag it denotes. - */ - private volatile boolean unresolvedDirectionalTags; - private CharSequence integrationName; private CharSequence serviceNameSource; @@ -923,63 +918,13 @@ static boolean tagEquals(String tagValue, String tagLiteral) { */ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); - if (unresolvedDirectionalTags) { - resolveDirectionalTags(); - } } - /** The direction this span resolves direction-dependent tag names in. */ + /** The direction this span reads direction-dependent tag names in; see {@link SpanTags}. */ private int tagDirection() { return TAG_DIRECTION_OF_SPAN_KIND[spanKindOrdinal]; } - /** - * The tag a direction-dependent name denotes on this span, or 0 for any other name, which {@link - * TagMap} resolves itself. Only a handful of names depend on direction, so a miss is cheap. - */ - private long directionalTagId(String tag) { - long tagId = KnownTagCodec.directionalKeyOf(tag, tagDirection()); - return tagId > 0 ? tagId : 0L; - } - - /** - * {@link #directionalTagId} for a tag about to be stored, noting a direction-dependent name that - * cannot resolve until the span has a direction. Call while holding the {@code unsafeTags} lock. - */ - private long directionalTagIdToStore(String tag) { - int direction = tagDirection(); - long tagId = KnownTagCodec.directionalKeyOf(tag, direction); - if (tagId > 0) { - return tagId; - } - if (tagId == KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL - && direction == KnownTagCodec.DIRECTION_UNKNOWN) { - unresolvedDirectionalTags = true; - } - return 0L; - } - - /** Re-keys tags stored under a direction-dependent name before the span had a direction. */ - private void resolveDirectionalTags() { - int direction = tagDirection(); - if (direction == KnownTagCodec.DIRECTION_UNKNOWN) { - return; - } - synchronized (unsafeTags) { - unresolvedDirectionalTags = false; - List unresolved = new ArrayList<>(2); - for (TagMap.EntryReader entry : unsafeTags) { - if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { - unresolved.add(entry.entry()); - } - } - for (TagMap.Entry entry : unresolved) { - unsafeTags.remove(entry.tag()); - unsafeTags.set(KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); - } - } - } - static byte spanKindOrdinalOf(String kind) { if (kind == null) { return SPAN_KIND_UNSET; @@ -1063,70 +1008,35 @@ public void setMetric(final TagMap.EntryReader entry) { } /* - * Store a tag that interception let through, by id when its name depends on the span's direction - * (peer.port, server.address), otherwise by name. Call while holding the unsafeTags lock. + * Store a tag that interception let through, as written: a direction-dependent name is resolved + * when it is read or emitted (see SpanTags), never here. Call while holding the unsafeTags lock. */ private void storeTag(String tag, Object value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } private void storeTag(String tag, CharSequence value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } private void storeTag(String tag, boolean value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } private void storeTag(String tag, int value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } private void storeTag(String tag, long value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } private void storeTag(String tag, float value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } private void storeTag(String tag, double value) { - long tagId = directionalTagIdToStore(tag); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.set(tag, value); } public void removeTag(String tag) { @@ -1135,13 +1045,8 @@ public void removeTag(String tag) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (unsafeTags) { - // A direction-dependent name removes the tag it denotes here, and any tag stored under the - // bare name before the span had a direction. - long tagId = directionalTagId(tag); - if (tagId != 0) { - unsafeTags.getAndRemove(tagId); - } - unsafeTags.remove(tag); + // A direction-dependent name and the tag it denotes here are removed together. + SpanTags.remove(unsafeTags, tag, tagDirection()); } } @@ -1159,7 +1064,7 @@ public void removeTag(long tagId) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (unsafeTags) { - unsafeTags.getAndRemove(tagId); + SpanTags.remove(unsafeTags, tagId, tagDirection()); } } @@ -1313,12 +1218,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!tagInterceptor.interceptTag(this, entry)) { synchronized (unsafeTags) { - long tagId = entry.tagId() == 0 ? directionalTagIdToStore(entry.tag()) : 0L; - if (tagId != 0) { - unsafeTags.set(tagId, entry.objectValue()); - } else { - unsafeTags.set(entry); - } + unsafeTags.set(entry); } } } @@ -1532,15 +1432,7 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - long tagId = directionalTagId(tag); - if (tagId != 0) { - TagMap.Entry entry = unsafeTags.getEntry(tagId); - if (entry != null) { - return entry.objectValue(); - } - // else stored under the bare name before the span had a direction - } - return unsafeTags.getObject(tag); + return SpanTags.get(unsafeTags, tag, tagDirection()); } @Deprecated @@ -1670,6 +1562,9 @@ void processTagsAndBaggage( // - creating an extra wrapper object that would create significant allocation // - implementing an interface to read the spans that require making the read method public synchronized (unsafeTags) { + // Direction-scoped names: the span's kind is final now, so each resolves to its one tag. + SpanTags.resolveScopedNames(unsafeTags, tagDirection()); + // Tags TagsPostProcessorFactory.lazyProcessor().processTags(unsafeTags, this, restrictedSpan); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 82a6cb16c06..d65866c385b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -95,8 +95,7 @@ public TagInterceptor( /** * Marks, by serial, each known tag that {@code names} names under any of its names, so a tag set - * by id or by its OpenTelemetry name matches too. A name declared per direction marks each - * direction's tag. + * by id or by its OpenTelemetry name matches too. */ private static boolean[] knownTagSerials(Set names) { boolean[] serials = new boolean[KnownTagCodec.serialLimit()]; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java new file mode 100644 index 00000000000..dd6a21161ce --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java @@ -0,0 +1,133 @@ +package datadog.trace.core; + +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.common.writer.ListWriter; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * A direction-dependent name ({@code peer.port}, {@code server.address}) must hold one value per + * span, whichever path writes it and whenever the span learns its kind. + */ +class DDSpanDirectionalTagConsistencyTest extends DDCoreJavaSpecification { + private CoreTracer tracer; + private DDSpan span; + + @BeforeEach + void setup() { + tracer = tracerBuilder().writer(new ListWriter()).build(); + span = (DDSpan) tracer.buildSpan("datadog", "fakeOperation").start(); + } + + @AfterEach + void tearDown() { + span.finish(); + tracer.close(); + } + + @Test + void aBulkWriteReplacesTheValueOfASharedName() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_PORT, 80); + + span.setAllTags(singletonMap(Tags.PEER_PORT, 81)); + + assertEquals(81, span.getTag(Tags.PEER_PORT)); + assertEquals(1, entriesNamed(span.getTags(), Tags.PEER_PORT)); + } + + @Test + void aBuilderTagWithASharedNameIsAKnownTag() { + DDSpan built = + (DDSpan) + tracer + .buildSpan("datadog", "built") + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) + .withTag(Tags.PEER_PORT, 5432) + .start(); + try { + assertEquals(5432, built.getTag(Tags.PEER_PORT)); + assertNotEquals(0L, tagIdOf(built.getTags(), Tags.PEER_PORT)); + } finally { + built.finish(); + } + } + + @Test + void aLaterCanonicalWriteWinsOverAnEarlierAlias() { + span.setTag("server.address", "old.example"); + span.setTag(Tags.PEER_HOSTNAME, "new.example"); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + assertEquals("new.example", span.getTag(Tags.PEER_HOSTNAME)); + } + + @Test + void aSharedNameKeepsItsValueWhenTheKindChanges() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_PORT, 5432); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + + assertEquals(5432, span.getTag(Tags.PEER_PORT)); + assertEquals(1, entriesNamed(span.getTags(), Tags.PEER_PORT)); + } + + @Test + void anAliasWrittenWhileAnotherThreadSetsTheKindStillResolves() throws Exception { + // A race window, not a deterministic failure: repeat to make losing it likely. + for (int i = 0; i < 500; i++) { + DDSpan raced = (DDSpan) tracer.buildSpan("datadog", "raced").start(); + CountDownLatch start = new CountDownLatch(1); + Thread kindSetter = + new Thread( + () -> { + awaitQuietly(start); + raced.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + }); + kindSetter.start(); + start.countDown(); + raced.setTag("server.address", "raced.example"); + kindSetter.join(TimeUnit.SECONDS.toMillis(5)); + + assertEquals("raced.example", raced.getTag(Tags.PEER_HOSTNAME), "iteration " + i); + raced.finish(); + } + } + + private static int entriesNamed(TagMap tags, String name) { + int count = 0; + for (TagMap.EntryReader entry : tags) { + if (name.equals(entry.tag())) { + count++; + } + } + return count; + } + + private static long tagIdOf(TagMap tags, String name) { + for (TagMap.EntryReader entry : tags) { + if (name.equals(entry.tag())) { + return entry.tagId(); + } + } + return 0L; + } + + private static void awaitQuietly(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java index c61692bbebd..941207dff4f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java @@ -12,10 +12,11 @@ import org.junit.jupiter.api.Test; /** - * A few tag names mean different tags depending on the span's direction: {@code peer.port} is the - * client's port on a server span and the server's on a client span, and {@code server.address} is - * {@code http.hostname} inbound but {@code peer.hostname} outbound. A span resolves them by its - * kind. + * A few tag names depend on the span's direction. {@code peer.port} is one tag in every direction, + * emitted as {@code client.port} inbound and {@code server.port} outbound. {@code server.address} + * is {@code http.hostname} inbound but {@code peer.hostname} outbound: a value written under it is + * stored as written, and read -- and, once the span is emitted, stored -- as the tag it denotes on + * the span's kind. */ class DDSpanDirectionalTagsTest extends DDCoreJavaSpecification { private CoreTracer tracer; @@ -34,26 +35,24 @@ void tearDown() { } @Test - void aSharedNameIsStoredAsTheTagOfTheSpansDirection() { + void aNameDeclaredPerDirectionIsOneTagInEveryDirection() { span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); span.setTag(Tags.PEER_PORT, 5432); - assertEquals(5432, storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertNull(storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals(5432, storedValue(KnownTags.PEER_PORT_ID)); assertEquals(5432, span.getTag(Tags.PEER_PORT)); } @Test - void aSharedNameOnAServerSpanIsTheClientsPort() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + void aNameDeclaredPerDirectionIsStoredBeforeTheSpanHasAKind() { span.setTag(Tags.PEER_PORT, "1234"); - assertEquals("1234", storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals("1234", storedValue(KnownTags.PEER_PORT_ID)); assertEquals("1234", span.getTag(Tags.PEER_PORT)); } @Test - void aDirectionScopedOpenTelemetryNameIsStoredAsTheTagItDenotes() { + void aDirectionScopedOpenTelemetryNameReadsAsTheTagItDenotes() { span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); span.setTag("server.address", "inbound.example"); @@ -81,35 +80,64 @@ void aNameWithNoTagInTheSpansDirectionStaysACustomTag() { } @Test - void aNameSetBeforeTheSpanHasADirectionResolvesWhenItGetsOne() { - span.setTag(Tags.PEER_PORT, 5432); + void aNameSetBeforeTheSpanHasADirectionReadsAsTheTagOnceItHasOne() { span.setTag("server.address", "outbound.example"); + assertNull(span.getTag(Tags.PEER_HOSTNAME)); span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - assertEquals(5432, storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); assertEquals("outbound.example", span.getTag(Tags.PEER_HOSTNAME)); - assertNull(span.getTags().get("server.address")); + assertEquals("outbound.example", span.getTag("server.address")); + } + + @Test + void theTagWinsOverItsDirectionScopedName() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_HOSTNAME, "tag.example"); + span.setTag("server.address", "scoped.example"); + + assertEquals("tag.example", span.getTag(Tags.PEER_HOSTNAME)); + assertEquals("tag.example", span.getTag("server.address")); + } + + @Test + void removingADirectionScopedNameRemovesTheTagItDenotes() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_HOSTNAME, "tag.example"); + + span.setTag("server.address", (String) null); + + assertNull(span.getTag(Tags.PEER_HOSTNAME)); + } + + @Test + void removingATagRemovesAValueWrittenUnderItsDirectionScopedName() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag("server.address", "scoped.example"); + + span.setTag(Tags.PEER_HOSTNAME, (String) null); + + assertNull(span.getTag(Tags.PEER_HOSTNAME)); + assertNull(span.getTag("server.address")); } @Test - void removingASharedNameRemovesTheTagOfTheSpansDirection() { + void removingANameDeclaredPerDirectionRemovesItsTag() { span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); span.setTag(Tags.PEER_PORT, 5432); span.setTag(Tags.PEER_PORT, (String) null); - assertNull(storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertNull(storedValue(KnownTags.PEER_PORT_ID)); assertNull(span.getTag(Tags.PEER_PORT)); } @Test - void anInternalSpanHasNoPeerPort() { + void anInternalSpanStoresPeerPortAsItsOneTag() { span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_INTERNAL); span.setTag(Tags.PEER_PORT, 5432); - assertNull(storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertNull(storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals(5432, storedValue(KnownTags.PEER_PORT_ID)); assertEquals(5432, span.getTag(Tags.PEER_PORT)); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index cb08be5fe0b..247d9f3958f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -62,12 +62,10 @@ void clearingAnUnknownIdIsIgnored() { } @Test - void aSharedNameIdReplacesAValueSetUnderTheSharedName() { + void anIdReplacesAValueSetUnderTheName() { span.setTag("peer.port", 8080); - span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); - span.setTag( - KnownTags.PEER_PORT_INBOUND_ID, (Object) null); // the other direction: nothing to clear + span.setTag(KnownTags.PEER_PORT_ID, 443); assertEquals(443, span.getTag("peer.port")); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 1b1a925b856..d988b73c34f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -974,11 +974,10 @@ void splitByTagsMatchesACustomTagByName() { } @Test - void splitByTagsOnANameDeclaredPerDirectionMatchesEachDirection() { + void splitByTagsOnANameDeclaredPerDirectionMatchesItsTag() { TagInterceptor interceptor = splittingInterceptor(Tags.PEER_PORT); - assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_INBOUND_ID)); - assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_ID)); assertTrue(interceptor.needsIntercept(Tags.PEER_PORT)); } diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index ec49d4619bf..c9f63f72d04 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -1,5 +1,8 @@ package datadog.trace.api; +import java.util.HashMap; +import java.util.Map; + /** * Resolves generated tag IDs and names. {@link KnownTags} owns the lookup tables; this class owns * the ID layout and namespace naming policy. @@ -17,8 +20,7 @@ public final class KnownTagCodec { * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] * are RESERVED and always zero here: they are the window the dense tag store uses for its * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is - * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is the - * SHARED_NAME bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for + * the trace/span LEVEL bit (set ⟹ trace-level), and bit 1 is the INTERCEPTED bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for * them. * *

The INTERCEPTED bit marks a tag TagInterceptor may route on the set-path, to a span field or @@ -61,18 +63,6 @@ public static boolean isIntercepted(long tagId) { return (tagId & INTERCEPTED) != 0L; } - /** - * SHARED_NAME bit (low-32 carve, bit 0): marks one direction of a tag declared once per direction - * under a shared Datadog name, such as {@code peer.port}. {@link #keyOf} resolves that name to - * neither id; TagMap reads and writes by that name check each direction's id instead. - */ - public static final long SHARED_NAME = 1L << 0; - - /** True if the tagId is one direction of a tag whose Datadog name is shared across directions. */ - public static boolean hasSharedName(long tagId) { - return (tagId & SHARED_NAME) != 0L; - } - /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; @@ -89,9 +79,10 @@ public static long makeTagId(int serialNum) { } /* - * A span's direction, which resolves the few names whose tag depends on it: peer.port is the - * client's port on inbound spans and the server's on outbound ones. Pass one of these to - * keyOf(String, int) and openTelemetryTagOf(long, int). + * A span's direction, which names the few tags whose OpenTelemetry name depends on it: peer.port + * is client.port on inbound spans and server.port on outbound ones. A tag is stored the same way + * in every direction; direction applies only when naming it. Pass one of these to + * openTelemetryTagOf(long, int) and directionalKeyOf(String, int). */ /** Direction not known, for example a span with no kind: only names that need none resolve. */ @@ -111,12 +102,6 @@ public static long makeTagId(int serialNum) { * every serial below 2^15. Not ids: nothing may store one as a tag. */ - /** - * What {@link Resolver#lookup} returns for a Datadog name shared by a tag per direction ({@code - * peer.port}): no single tag, but whichever of them a map holds is that name's value. - */ - static final long SHARED_DATADOG_NAME_SENTINEL = -1L; - /** * What {@link Resolver#lookup} returns for an OpenTelemetry name that applies in one direction * only: {@code server.address} is {@code http.hostname} on inbound spans and {@code @@ -138,15 +123,14 @@ public interface Resolver { /** * The tag's OpenTelemetry-namespace name on spans of {@code direction}, or {@code null} when it - * declares none there. {@link #DIRECTION_UNKNOWN} returns every name that needs no direction: - * one that holds in every direction, or that of a tag declared per direction. + * declares none there. {@link #DIRECTION_UNKNOWN} returns only a name that holds in every + * direction. */ String openTelemetryNameOf(long tagId, int direction); /** - * The id for {@code name} in any namespace; {@link #SHARED_DATADOG_NAME_SENTINEL} or {@link - * #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} when its tag depends on direction; 0 when it is not a - * known tag. + * The id for {@code name} in any namespace; {@link #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} when + * its tag depends on direction; 0 when it is not a known tag. */ long lookup(String name); @@ -192,11 +176,11 @@ public static String openTelemetryNameOf(long tagId) { * with {@link #datadogNameOf} for the same tag under the Datadog namespace; outbound naming is * per-namespace, never normalized to one of them. * - *

Needs no direction for nearly every tag, including one declared per direction ({@code - * peer.port} on an outbound span is {@code server.port}). The exception is a single tag whose - * rename holds in one direction only ({@code http.hostname} is {@code server.address} on inbound - * spans): without a direction it passes through under its Datadog name. A caller naming a span's - * tags uses {@link #openTelemetryTagOf(long, int)}. + *

Needs no direction for nearly every tag. The exception is a tag renamed in some directions + * only ({@code http.hostname} is {@code server.address} on inbound spans, {@code peer.port} is + * {@code client.port} inbound and {@code server.port} outbound): without a direction it passes + * through under its Datadog name. A caller naming a span's tags uses {@link + * #openTelemetryTagOf(long, int)}. */ public static String openTelemetryTagOf(long tagId) { return openTelemetryTagOf(tagId, DIRECTION_UNKNOWN); @@ -254,35 +238,70 @@ public static long keyOf(String name, int direction) { } /** - * Only the direction-dependent part of {@link #keyOf(String, int)}, for a caller that already - * resolves every other name: the tag {@code name} denotes on spans of {@code direction} when it - * is one of the few names that depend on direction ({@code peer.port}, {@code server.address}); 0 - * for every other name; or {@link #NO_TAG_IN_DIRECTION_SENTINEL}. + * The tag a direction-scoped OpenTelemetry name ({@code server.address}, {@code client.port}) + * denotes on spans of {@code direction}; 0 for every other name; or {@link + * #NO_TAG_IN_DIRECTION_SENTINEL}. A value written under such a name is stored as written and + * resolved to its tag here, once the span's direction is known. * - *

A switch over just those names, so a miss is far cheaper than a full lookup. A span uses it - * before handing a name to {@link TagMap}, which then does its usual single lookup. + *

A switch over just those names, so a miss is far cheaper than a full lookup. */ public static long directionalKeyOf(String name, int direction) { return Installed.RESOLVER.directionalKeyOf(name, direction); } /** - * The raw registry lookup: {@code name}'s id, 0 when it is not a known tag, or {@link - * #SHARED_DATADOG_NAME_SENTINEL} / {@link #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} when its tag - * depends on direction. + * The inverse of {@link #directionalKeyOf}: the direction-scoped OpenTelemetry name {@code tagId} + * has on spans of {@code direction} ({@code server.address} for {@code http.hostname} inbound), + * or null when it has none there, or its name there holds in every direction. */ - static long lookup(String name) { - return Installed.RESOLVER.lookup(name); + public static String directionScopedNameOf(long tagId, int direction) { + if (direction == DIRECTION_UNKNOWN) { + return null; + } + Resolver resolver = Installed.RESOLVER; + String name = resolver.openTelemetryNameOf(tagId, direction); + return name != null && resolver.openTelemetryNameOf(tagId, DIRECTION_UNKNOWN) == null + ? name + : null; } /** - * {@link #keyOf(String)}, except that a Datadog name shared by a tag per direction returns {@link - * #SHARED_DATADOG_NAME_SENTINEL}, so {@link TagMap} can find whichever of those tags it holds, - * through {@link #directionalKeyOf}. One lookup, like {@code keyOf}. + * {@link #directionScopedNameOf(long, int)} by Datadog name. Costs one small map probe for every + * name but the few tags renamed in some directions only. */ - static long keyOrSharedName(String name) { - long key = Installed.RESOLVER.lookup(name); - return key == DIRECTION_SCOPED_OTEL_NAME_SENTINEL ? 0L : key; + public static String directionScopedNameOf(String ddName, int direction) { + if (direction == DIRECTION_UNKNOWN) { + return null; + } + Long tagId = DirectionScoped.TAG_IDS.get(ddName); + return tagId == null ? null : directionScopedNameOf(tagId, direction); + } + + /** The tags renamed in some directions only, by Datadog name; built once from the registry. */ + private static final class DirectionScoped { + static final Map TAG_IDS = directionScopedTagIds(); + + private static Map directionScopedTagIds() { + Map tagIds = new HashMap<>(); + for (int serial = 1; serial < serialLimit(); serial++) { + String name = nameOf(makeTagId(serial)); + long tagId = keyOf(name); + for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { + if (directionScopedNameOf(tagId, direction) != null) { + tagIds.put(name, tagId); + } + } + } + return tagIds; + } + } + + /** + * The raw registry lookup: {@code name}'s id, 0 when it is not a known tag, or {@link + * #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} when its tag depends on direction. + */ + static long lookup(String name) { + return Installed.RESOLVER.lookup(name); } /** diff --git a/internal-api/src/main/java/datadog/trace/api/SpanTags.java b/internal-api/src/main/java/datadog/trace/api/SpanTags.java new file mode 100644 index 00000000000..0ef85c7ae13 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/SpanTags.java @@ -0,0 +1,151 @@ +package datadog.trace.api; + +import static datadog.trace.api.KnownTagCodec.DIRECTION_INBOUND; +import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; +import static datadog.trace.api.KnownTagCodec.DIRECTION_OUTBOUND; +import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; + +import datadog.trace.bootstrap.instrumentation.api.Tags; +import java.util.ArrayList; +import java.util.List; + +/** + * A span's reading of the few tag names that depend on its direction. {@link TagMap} stores every + * name as written and knows nothing of span kinds -- baggage is a TagMap too -- so the direction is + * applied here, by the span layer, when a tag is read, removed or emitted. + * + *

A tag is stored once, under its id, whatever the direction: {@code peer.port} is one tag that + * is emitted as {@code client.port} inbound and {@code server.port} outbound. A direction-scoped + * OpenTelemetry name ({@code server.address}, which is {@code http.hostname} inbound and {@code + * peer.hostname} outbound) cannot be resolved until the span has a direction, so a value written + * under it is stored under that name, and resolved by the rule {@code (name, direction) -> tag}. + * When both the tag and its scoped name hold a value, the tag's wins. + */ +public final class SpanTags { + /** The direction of a span with these tags, from its {@code span.kind}. */ + public static int directionOf(TagMap tags) { + TagMap.Entry kind = tags.getEntry(KnownTags.SPAN_KIND_ID); + return kind == null ? DIRECTION_UNKNOWN : directionOfKind(kind.stringValue()); + } + + /** The direction of a span of {@code kind}; brokers and custom kinds have none. */ + public static int directionOfKind(String kind) { + if (kind == null) { + return DIRECTION_UNKNOWN; + } + switch (kind) { + case Tags.SPAN_KIND_SERVER: + case Tags.SPAN_KIND_CONSUMER: + return DIRECTION_INBOUND; + case Tags.SPAN_KIND_CLIENT: + case Tags.SPAN_KIND_PRODUCER: + return DIRECTION_OUTBOUND; + case Tags.SPAN_KIND_INTERNAL: + return DIRECTION_NONE; + default: + return DIRECTION_UNKNOWN; + } + } + + /** + * The name {@code entry} is emitted under in the OpenTelemetry namespace on a span of {@code + * direction}: {@code peer.port} is {@code server.port} on a client span. A custom tag keeps its + * own name. + */ + public static String openTelemetryTagOf(TagMap.EntryReader entry, int direction) { + long tagId = entry.tagId(); + return tagId == 0 ? entry.tag() : KnownTagCodec.openTelemetryTagOf(tagId, direction); + } + + /** + * The value of {@code name} on a span of {@code direction}: a direction-scoped name reads its + * tag, and a tag reads a value written under its scoped name. Costs one small switch more than + * {@link TagMap#getObject} for any other name. + */ + public static Object get(TagMap tags, String name, int direction) { + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + if (tagId > 0) { + TagMap.Entry tag = tags.getEntry(tagId); + return tag != null ? tag.objectValue() : tags.getObject(name); + } + Object value = tags.getObject(name); + if (value != null || direction == DIRECTION_UNKNOWN) { + return value; + } + String scopedName = KnownTagCodec.directionScopedNameOf(name, direction); + return scopedName == null ? null : tags.getObject(scopedName); + } + + /** + * The value of the known tag {@code tagId}, or of its scoped name on a span of {@code direction}. + */ + public static Object get(TagMap tags, long tagId, int direction) { + TagMap.Entry tag = tags.getEntry(tagId); + if (tag != null || direction == DIRECTION_UNKNOWN) { + return tag == null ? null : tag.objectValue(); + } + String scopedName = KnownTagCodec.directionScopedNameOf(tagId, direction); + return scopedName == null ? null : tags.getObject(scopedName); + } + + /** + * Removes {@code name} from a span of {@code direction}: the tag and its scoped name together. + */ + public static void remove(TagMap tags, String name, int direction) { + tags.remove(name); + if (direction == DIRECTION_UNKNOWN) { + return; + } + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + if (tagId > 0) { + tags.getAndRemove(tagId); + } else { + String scopedName = KnownTagCodec.directionScopedNameOf(name, direction); + if (scopedName != null) { + tags.remove(scopedName); + } + } + } + + /** Removes the known tag {@code tagId}, and its scoped name, from a span of {@code direction}. */ + public static void remove(TagMap tags, long tagId, int direction) { + tags.getAndRemove(tagId); + String scopedName = KnownTagCodec.directionScopedNameOf(tagId, direction); + if (scopedName != null) { + tags.remove(scopedName); + } + } + + /** + * Resolves each value written under a direction-scoped name to its tag, for a span of {@code + * direction} about to be emitted: the tag takes the value unless it holds one already, and the + * scoped name is dropped either way. A name with no tag in {@code direction} -- or any, while the + * direction is unknown -- stays as written, a custom tag. + */ + public static void resolveScopedNames(TagMap tags, int direction) { + if (direction == DIRECTION_UNKNOWN) { + return; + } + List scoped = null; + for (TagMap.EntryReader entry : tags) { + if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { + if (scoped == null) { + scoped = new ArrayList<>(2); + } + scoped.add(entry.entry()); + } + } + if (scoped == null) { + return; + } + for (TagMap.Entry entry : scoped) { + tags.remove(entry.tag()); + long tagId = KnownTagCodec.directionalKeyOf(entry.tag(), direction); + if (tags.getEntry(tagId) == null) { + tags.set(tagId, entry.objectValue()); + } + } + } + + private SpanTags() {} +} diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index aebc8c8a5f4..e82335204d6 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -245,9 +245,9 @@ public static boolean isEmptyValue(Object value) { /* * Name-keyed factories. They accept only a name whose tag needs no span direction: an entry is - * built with no span, so it cannot resolve peer.port (the client's port inbound, the server's - * outbound) or server.address. For those, use the id-keyed create(long, ...) below with the - * tag for the direction, or set the name on the span, which resolves it by its kind. Prefer + * built with no span, so it cannot resolve a direction-scoped OpenTelemetry name such as + * server.address (http.hostname inbound, peer.hostname outbound). For those, use the id-keyed + * create(long, ...) below, or set the name on the span, which resolves it by its kind. Prefer * the id-keyed factories generally: these are expected to be deprecated. */ @@ -299,7 +299,7 @@ private static long directionFreeKeyOf(String tag) { "'" + tag + "' names a different tag depending on the span's direction; create the entry" - + " with a KnownTags id for the direction, or set the name on the span"); + + " with a KnownTags id, or set the name on the span"); } return key; } @@ -936,7 +936,10 @@ public boolean equals(Object obj) { if (!(obj instanceof TagMap.Entry)) return false; TagMap.Entry that = (TagMap.Entry) obj; - return this.tag.equals(that.tag) && this.objectValue().equals(that.objectValue()); + // hashCode folds tagHash, so equal entries must agree on it, not just on the name. + return this.tagHash == that.tagHash + && this.tag.equals(that.tag) + && this.objectValue().equals(that.objectValue()); } private static long boolean2Prim(boolean value) { @@ -1433,31 +1436,13 @@ public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must canonicalize the same way, or it would hash to the wrong bucket // and silently miss the entry stored under the Datadog name. - long tagId = KnownTagCodec.keyOrSharedName(tag); - if (tagId == 0) { - return this.getEntry(tag, Entry.customHash(tag)); - } else if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { - return this.getEntry(KnownTagCodec.nameOf(tagId), tagId); - } - // A Datadog name shared by a tag per direction: a map holds at most one of them per span. - for (int direction = KnownTagCodec.DIRECTION_INBOUND; - direction <= KnownTagCodec.DIRECTION_NONE; - direction++) { - long sharingId = KnownTagCodec.directionalKeyOf(tag, direction); - Entry entry = sharingId > 0 ? this.getEntry(tag, sharingId) : null; - if (entry != null) { - return entry; - } - } - // Set by that name alone, with no direction: stored as a custom tag. - return this.getEntry(tag, Entry.customHash(tag)); + long tagId = KnownTagCodec.keyOf(tag); + return tagId == 0 + ? this.getEntry(tag, Entry.customHash(tag)) + : this.getEntry(KnownTagCodec.nameOf(tagId), tagId); } - /** - * The entry for a known tag id. Unlike a lookup by a Datadog name shared by a tag per direction - * ({@code peer.port}), which finds whichever of those tags the map holds, this finds only the - * one. - */ + /** The entry for a known tag id. */ public Entry getEntry(long tagId) { return this.getEntry(Entry.requireKnownName(tagId), tagId); } @@ -1533,7 +1518,7 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { @Deprecated @Override public Object put(@Nonnull String tag, Object value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); TagMap.Entry entry = this.getAndSet( tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); @@ -1556,25 +1541,12 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { * from its id or a custom tag from its name. (Entry's own name factories are custom-only.) */ /** - * The id a write by name stores under: the tag's id, 0 for a custom tag, or -- for a Datadog name - * shared by a tag per direction ({@code peer.port}) -- whichever of those tags this map already - * holds. When it holds neither, the write stays a custom tag under the name, which a span re-keys - * once it knows its direction. + * The id a write by name stores under: the tag's id, or 0 for a custom tag. A direction-scoped + * OpenTelemetry name ({@code server.address}) is stored as written, a custom tag, and resolved by + * whoever knows the direction. */ - private long writeKeyOf(String tag) { - long tagId = KnownTagCodec.keyOrSharedName(tag); - if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { - return tagId; - } - for (int direction = KnownTagCodec.DIRECTION_INBOUND; - direction <= KnownTagCodec.DIRECTION_NONE; - direction++) { - long sharingId = KnownTagCodec.directionalKeyOf(tag, direction); - if (sharingId > 0 && this.getEntry(tag, sharingId) != null) { - return sharingId; - } - } - return 0L; + private static long writeKeyOf(String tag) { + return KnownTagCodec.keyOf(tag); } static Entry anyEntryFor(String tag, Object value) { @@ -1613,7 +1585,7 @@ static Entry doubleEntryFor(String tag, double value) { } public void set(@Nonnull String tag, @Nonnull Object value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1622,7 +1594,7 @@ public void set(@Nonnull String tag, @Nonnull Object value) { } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1631,7 +1603,7 @@ public void set(@Nonnull String tag, @Nonnull CharSequence value) { } public void set(@Nonnull String tag, boolean value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1640,7 +1612,7 @@ public void set(@Nonnull String tag, boolean value) { } public void set(@Nonnull String tag, int value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1649,7 +1621,7 @@ public void set(@Nonnull String tag, int value) { } public void set(@Nonnull String tag, long value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1658,7 +1630,7 @@ public void set(@Nonnull String tag, long value) { } public void set(@Nonnull String tag, float value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1667,7 +1639,7 @@ public void set(@Nonnull String tag, float value) { } public void set(@Nonnull String tag, double value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1744,14 +1716,6 @@ public Entry getAndSet(@Nullable Entry newEntry) { private Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); - long newTagHash = newEntry.tagHash; - if ((newTagHash >>> 32) != 0 && KnownTagCodec.hasSharedName(newTagHash)) { - // One direction of a tag declared per direction, written by id, supersedes a value written - // under the shared name alone (before a span knew its direction), so the map never holds - // both. - this.removeLocal(newEntry.tag, Entry.customHash(newEntry.tag)); - } - // Re-setting a key clears any read-through tombstone for it (the new value overrides the // removal). Gated on the lazy field, so this is a no-op for the common no-tombstone case. if (this.removedFromParent != null) { @@ -1804,43 +1768,43 @@ private Entry putEntry(@Nonnull Entry newEntry) { } public Entry getAndSet(@Nonnull String tag, Object value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); } public Entry getAndSet(@Nonnull String tag, CharSequence value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, boolean value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, int value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, long value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, float value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, double value) { - long tagId = this.writeKeyOf(tag); + long tagId = writeKeyOf(tag); return this.getAndSet( tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value)); } @@ -2099,24 +2063,10 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must canonicalize first to find (and tombstone) the right entry. - long tagId = KnownTagCodec.keyOrSharedName(tag); - if (tagId == 0) { - return this.getAndRemove(tag, Entry.customHash(tag)); - } else if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { - return this.getAndRemove(KnownTagCodec.nameOf(tagId), tagId); - } - // A shared name removes whichever of its tags the map holds, and any custom tag of that name. - Entry removed = this.getAndRemove(tag, Entry.customHash(tag)); - for (int direction = KnownTagCodec.DIRECTION_INBOUND; - direction <= KnownTagCodec.DIRECTION_NONE; - direction++) { - long sharingId = KnownTagCodec.directionalKeyOf(tag, direction); - Entry entry = sharingId > 0 ? this.getAndRemove(tag, sharingId) : null; - if (removed == null) { - removed = entry; - } - } - return removed; + long tagId = KnownTagCodec.keyOf(tag); + return tagId == 0 + ? this.getAndRemove(tag, Entry.customHash(tag)) + : this.getAndRemove(KnownTagCodec.nameOf(tagId), tagId); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index a039a2bb088..db722a44ace 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -6,7 +6,6 @@ import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -165,32 +164,28 @@ void unknownNamesResolveToZero() { /** * {@code peer.port} means the server's port on outbound spans and the client's on inbound ones, - * so it is one tag per direction. Both share the Datadog name, which therefore resolves to no tag - * until name resolution knows the span's direction. + * but it is one tag either way: one id, one stored value. Only its OpenTelemetry name depends on + * the span's direction. */ @Test - void aNameDeclaredPerDirectionIsOneTagPerDirection() { - assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_INBOUND_ID)); - assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertNotEquals(KnownTags.PEER_PORT_INBOUND_ID, KnownTags.PEER_PORT_OUTBOUND_ID); - assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_INBOUND_ID)); - assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); + void aNameDeclaredPerDirectionIsOneTag() { + assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_ID)); + assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.keyOf(Tags.PEER_PORT)); + for (int direction = DIRECTION_UNKNOWN; direction <= DIRECTION_NONE; direction++) { + assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.keyOf(Tags.PEER_PORT, direction)); + } } static Stream directionalNames() { return Stream.of( - Arguments.of(Tags.PEER_PORT, DIRECTION_INBOUND, KnownTags.PEER_PORT_INBOUND_ID), - Arguments.of(Tags.PEER_PORT, DIRECTION_OUTBOUND, KnownTags.PEER_PORT_OUTBOUND_ID), - Arguments.of(Tags.PEER_PORT, DIRECTION_NONE, 0L), - Arguments.of(Tags.PEER_PORT, DIRECTION_UNKNOWN, 0L), Arguments.of("server.address", DIRECTION_INBOUND, KnownTags.HTTP_HOSTNAME_ID), Arguments.of("server.address", DIRECTION_OUTBOUND, KnownTags.PEER_HOSTNAME_ID), Arguments.of("server.address", DIRECTION_UNKNOWN, 0L), - Arguments.of("server.port", DIRECTION_OUTBOUND, KnownTags.PEER_PORT_OUTBOUND_ID), + Arguments.of("server.port", DIRECTION_OUTBOUND, KnownTags.PEER_PORT_ID), Arguments.of("server.port", DIRECTION_INBOUND, 0L), - Arguments.of("client.port", DIRECTION_INBOUND, KnownTags.PEER_PORT_INBOUND_ID), + Arguments.of("client.port", DIRECTION_INBOUND, KnownTags.PEER_PORT_ID), Arguments.of("client.port", DIRECTION_OUTBOUND, 0L), + Arguments.of("client.port", DIRECTION_NONE, 0L), Arguments.of("network.peer.address", DIRECTION_INBOUND, KnownTags.NETWORK_CLIENT_IP_ID)); } @@ -219,26 +214,23 @@ void aDirectionScopedRenameIsEmittedOnlyInItsDirection() { assertEquals( Tags.HTTP_HOSTNAME, KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertEquals( - "server.port", - KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_OUTBOUND_ID, DIRECTION_OUTBOUND)); - assertEquals( - "client.port", - KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_INBOUND_ID, DIRECTION_INBOUND)); assertEquals( "http.request.method", KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN)); assertEquals(Tags.HTTP_HOSTNAME, KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID)); } - /** A tag declared per direction exists only on spans of its direction, so its name needs none. */ + /** One tag, named for the span's direction; with no direction it keeps its Datadog name. */ @Test - void aTagDeclaredPerDirectionIsEmittedWithoutADirection() { - assertEquals("server.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertEquals("client.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_INBOUND_ID)); + void aTagDeclaredPerDirectionIsEmittedUnderTheNameForTheDirection() { assertEquals( "server.port", - KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_OUTBOUND_ID, DIRECTION_UNKNOWN)); + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND)); + assertEquals( + "client.port", KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, DIRECTION_INBOUND)); + assertEquals( + Tags.PEER_PORT, KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, DIRECTION_NONE)); + assertEquals(Tags.PEER_PORT, KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID)); } @Test diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java new file mode 100644 index 00000000000..4991c1ee294 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java @@ -0,0 +1,87 @@ +package datadog.trace.api; + +import static datadog.trace.api.KnownTags.PEER_PORT_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +/** + * A name declared per direction ({@code peer.port}) is one tag to a map: a write by name and a + * write by id land on the same entry, in a map and across a frozen parent. A direction-scoped + * OpenTelemetry name ({@code server.address}) is stored as written, for the span to resolve. + */ +class TagMapDirectionalNameTest { + @Test + void aWriteByNameAndAWriteByIdAreOneEntry() { + TagMap map = TagMap.create(); + map.set("peer.port", 80); + map.set(PEER_PORT_ID, 81); + + assertEquals(81, map.getEntry(PEER_PORT_ID).intValue()); + assertEquals(81, map.getEntry("peer.port").intValue()); + assertEquals(PEER_PORT_ID, map.getEntry("peer.port").tagId()); + assertEquals(1, entriesNamed(map, "peer.port")); + } + + @Test + void aChildWriteByIdShadowsAParentWriteByName() { + TagMap parent = TagMap.create(); + parent.set("peer.port", 80); + parent.freeze(); + + TagMap child = TagMap.createFromParent(parent); + child.set(PEER_PORT_ID, 81); + + assertEquals(81, child.getEntry("peer.port").intValue()); + assertEquals(1, entriesNamed(child, "peer.port")); + } + + @Test + void aRemovalByNameHidesTheParentsEntry() { + TagMap parent = TagMap.create(); + parent.set(PEER_PORT_ID, 5432); + TagMap child = TagMap.createFromParent(parent.freeze()); + + assertEquals(5432, child.get("peer.port")); + child.remove("peer.port"); + assertNull(child.get("peer.port")); + assertNull(child.getEntry(PEER_PORT_ID)); + } + + @Test + void entriesThatAreEqualHashAlike() { + TagMap byName = TagMap.create(); + byName.set("peer.port", 80); + TagMap byId = TagMap.create(); + byId.set(PEER_PORT_ID, 80); + + TagMap.Entry a = byName.getEntry("peer.port"); + TagMap.Entry b = byId.getEntry(PEER_PORT_ID); + + if (a.equals(b)) { + assertEquals(a.hashCode(), b.hashCode()); + } + assertEquals(b.tagId(), a.tagId()); + } + + @Test + void aDirectionScopedOpenTelemetryNameIsStoredAsWritten() { + TagMap map = TagMap.create(); + map.set("server.address", "example.com"); + + assertEquals("example.com", map.get("server.address")); + assertEquals(0L, map.getEntry("server.address").tagId()); + assertNull(map.getEntry(KnownTags.PEER_HOSTNAME_ID)); + } + + private static int entriesNamed(TagMap map, String name) { + int count = 0; + for (TagMap.EntryReader entry : map) { + if (name.equals(entry.tag())) { + count++; + } + } + return count; + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java index adf18cc6e83..05663dea4e4 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java @@ -13,7 +13,6 @@ class TagMapEntryCreateByNameTest { @TableTest({ "scenario | name ", - "shared Datadog name | peer.port ", "OTel name, both directions | server.address ", "OTel name, outbound only | server.port ", "OTel name, inbound only | client.port ", @@ -42,11 +41,12 @@ void aCustomNameCreatesACustomTag() { assertEquals(3, entry.intValue()); } + /** {@code peer.port} is one tag in every direction; only its OpenTelemetry name varies. */ @Test - void theIdOfATagDeclaredPerDirectionCreatesIt() { - TagMap.Entry entry = TagMap.Entry.create(KnownTags.PEER_PORT_OUTBOUND_ID, (Object) 5432); + void aDatadogNameDeclaredPerDirectionCreatesItsOneTag() { + TagMap.Entry entry = TagMap.Entry.create("peer.port", 5432); - assertEquals(KnownTags.PEER_PORT_OUTBOUND_ID, entry.tagId()); + assertEquals(KnownTags.PEER_PORT_ID, entry.tagId()); assertEquals("peer.port", entry.tag()); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index d558ecf59dc..ecef85afdd4 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -72,36 +72,4 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(0L, "value")); assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } - - @Test - void aSharedNameIdIsFoundByItsSharedName() { - TagMap map = TagMap.create(); - map.set(KnownTags.PEER_PORT_INBOUND_ID, 80); - - assertEquals(80, map.getEntry(KnownTags.PEER_PORT_INBOUND_ID).intValue()); - assertEquals(80, map.getEntry("peer.port").intValue()); - } - - @Test - void aWriteBySharedNameReplacesTheHeldDirectionsTag() { - TagMap map = TagMap.create(); - map.set(KnownTags.PEER_PORT_INBOUND_ID, 80); - - map.set("peer.port", 81); - - assertEquals(1, map.size()); - assertEquals(81, map.getEntry(KnownTags.PEER_PORT_INBOUND_ID).intValue()); - } - - @Test - void aWriteByIdSupersedesAValueSetUnderTheSharedNameAlone() { - TagMap map = TagMap.create(); - map.set("peer.port", 8080); // no direction known: stored under the name - - map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 443); - - assertEquals(1, map.size()); - assertEquals(443, map.getEntry("peer.port").intValue()); - assertEquals(443, map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID).intValue()); - } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index d5925380f62..522a2a0058c 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -2,11 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import datadog.trace.bootstrap.instrumentation.api.Tags; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -90,55 +87,6 @@ void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessExcept } } - /** - * A Datadog name shared by a tag per direction names no single tag, but a span holds only one of - * them, so a lookup by that name finds whichever the map holds. - */ - @Test - void aSharedNameFindsWhicheverOfItsTagsTheMapHolds() { - TagMap inbound = TagMap.create(); - inbound.set(KnownTags.PEER_PORT_INBOUND_ID, 1234); - TagMap outbound = TagMap.create(); - outbound.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); - - assertEquals(1234, inbound.getEntry(Tags.PEER_PORT).intValue()); - assertEquals(5432, outbound.getEntry(Tags.PEER_PORT).intValue()); - assertEquals(5432, outbound.get(Tags.PEER_PORT)); - assertTrue(outbound.containsKey(Tags.PEER_PORT)); - assertEquals(5432, outbound.copy().get(Tags.PEER_PORT)); - assertNull(outbound.getEntry(KnownTags.PEER_PORT_INBOUND_ID)); - } - - @Test - void aSharedNameSetWithoutADirectionIsACustomTag() { - TagMap map = TagMap.create(); - map.set(Tags.PEER_PORT, 5432); - - assertEquals(5432, map.getEntry(Tags.PEER_PORT).intValue()); - assertEquals(0L, map.getEntry(Tags.PEER_PORT).tagId()); - } - - @Test - void removingASharedNameRemovesWhicheverOfItsTagsTheMapHolds() { - TagMap map = TagMap.create(); - map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); - - assertEquals(5432, map.getAndRemove(Tags.PEER_PORT).intValue()); - assertNull(map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertEquals(0, map.size()); - } - - @Test - void aSharedNameReadsThroughToTheParent() { - TagMap parent = TagMap.create(); - parent.set(KnownTags.PEER_PORT_OUTBOUND_ID, 5432); - TagMap child = TagMap.createFromParent(parent.freeze()); - - assertEquals(5432, child.get(Tags.PEER_PORT)); - child.remove(Tags.PEER_PORT); - assertNull(child.get(Tags.PEER_PORT)); - } - private static List knownTagIds() throws IllegalAccessException { List tagIds = new ArrayList<>(); for (Field field : KnownTags.class.getFields()) { From d469c665b0aa53361cbdf6158d7cc4b67c746c53 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 12:18:43 -0400 Subject: [PATCH 24/35] Name direction-dependent tags by span kind in OTLP export The OTLP JSON and protobuf writers compute the span's direction once from its span.kind (SpanTags.directionOf) and name each tag for it, so peer.hostname exports as server.address on a client span, and network.client.ip as network.peer.address on a server span. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/otlp/trace/OtlpTraceJson.java | 16 +++++++--- .../trace/core/otlp/trace/OtlpTraceProto.java | 16 +++++++--- .../trace/OtlpTraceJsonCollectorTest.java | 31 +++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java index 17963da3514..79e0dc665c6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java @@ -27,6 +27,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; +import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -159,12 +160,16 @@ public static void writeSpanLink(JsonWriter writer, AgentSpanLink spanLink) { writer.endObject(); } - private static void writeSpanTag(JsonWriter writer, TagMap.EntryReader tagEntry) { + private static void writeSpanTag( + JsonWriter writer, Integer direction, TagMap.EntryReader tagEntry) { // OTLP is the OpenTelemetry wire format, but the rename itself is opt-in: emit the // OpenTelemetry-namespace name only when OTel semantics are enabled, else the entry's own - // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. + // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. A few + // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = - Config.get().isTraceOtelSemanticsEnabled() ? tagEntry.openTelemetryTag() : tagEntry.tag(); + Config.get().isTraceOtelSemanticsEnabled() + ? SpanTags.openTelemetryTagOf(tagEntry, direction) + : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: writeAttribute(writer, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue()); @@ -247,7 +252,10 @@ public void accept(Metadata metadata) { writeSpanTag(writer, PROCESS_TAGS_KEY, metadata.processTags()); } - metadata.getTags().forEach(writer, OtlpTraceJson::writeSpanTag); + // The direction is computed once per span; boxing -1..2 hits the Integer cache. + TagMap tags = metadata.getTags(); + Integer direction = SpanTags.directionOf(tags); + tags.forEach(writer, direction, OtlpTraceJson::writeSpanTag); // reset for next span includeProcessTags = false; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java index e7b0310bc42..2e33f623c97 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java @@ -39,6 +39,7 @@ import datadog.trace.api.DDTraceId; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; +import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -223,13 +224,17 @@ public static void writeSpanId(StreamingBuffer buf, long spanId) { buf.putLong(spanId); } - private static void writeSpanTag(StreamingBuffer buf, TagMap.EntryReader tagEntry) { + private static void writeSpanTag( + StreamingBuffer buf, Integer direction, TagMap.EntryReader tagEntry) { writeTag(buf, 9, LEN_WIRE_TYPE); // OTLP is the OpenTelemetry wire format, but the rename itself is opt-in: emit the // OpenTelemetry-namespace name only when OTel semantics are enabled, else the entry's own - // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. + // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. A few + // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = - Config.get().isTraceOtelSemanticsEnabled() ? tagEntry.openTelemetryTag() : tagEntry.tag(); + Config.get().isTraceOtelSemanticsEnabled() + ? SpanTags.openTelemetryTagOf(tagEntry, direction) + : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: writeAttribute(buf, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue()); @@ -324,7 +329,10 @@ public void accept(Metadata metadata) { writeSpanTag(buf, PROCESS_TAGS_KEY, metadata.processTags()); } - metadata.getTags().forEach(buf, OtlpTraceProto::writeSpanTag); + // The direction is computed once per span; boxing -1..2 hits the Integer cache. + TagMap tags = metadata.getTags(); + Integer direction = SpanTags.directionOf(tags); + tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag); // reset for next span includeProcessTags = false; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index a0456e9d20e..811e3cdf32b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test; import org.mockito.AdditionalAnswers; import org.mockito.MockedStatic; +import org.tabletest.junit.TableTest; /** * Tests for {@link OtlpTraceJsonCollector}, parsing the produced JSON back with {@link JsonMapper} @@ -117,6 +118,36 @@ void tagsAreEmittedUnderTheirDatadogNameWhenOtelSemanticsDisabled() throws IOExc "a tag the registry does not name passes through unchanged; got " + attrKeys); } + @TableTest({ + "scenario | kind | tag | otelName ", + "client hostname | client | peer.hostname | server.address ", + "server hostname | server | http.hostname | server.address ", + "server client ip | server | network.client.ip | network.peer.address", + "client peer port | client | peer.port | server.port ", + "server peer port | server | peer.port | client.port " + }) + void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( + String kind, String tag, String otelName) throws IOException { + Config realConfig = Config.get(); + try (MockedStatic configMock = mockStatic(Config.class)) { + Config config = mock(Config.class, AdditionalAnswers.delegatesTo(realConfig)); + when(config.isTraceOtelSemanticsEnabled()).thenReturn(true); + configMock.when(Config::get).thenReturn(config); + + AgentSpan agentSpan = TRACER.startSpan("test", "op.directional"); + agentSpan.setTag(SPAN_KIND, kind); + agentSpan.setTag(tag, "value"); + agentSpan.finish(); + + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) agentSpan)); + Set attrKeys = attributeKeys(onlySpan(collector.collectTraces())); + + assertTrue(attrKeys.contains(otelName), "expected " + otelName + "; got " + attrKeys); + assertFalse(attrKeys.contains(tag), "expected no " + tag + "; got " + attrKeys); + } + } + private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled) throws IOException { Config realConfig = Config.get(); From 748a8af98a90071b2c0df63585ea49a5e2d46d96 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 12:32:18 -0400 Subject: [PATCH 25/35] Apply last write wins between a tag and its direction-scoped alias With no ordering state (a TagMap has no insertion order): - Once the span has a kind, an alias write (server.address on a client span) is stored as its tag, overwriting it. - Before then, the alias is stored as written. A write of a tag that has an alias removes every pending alias that could denote it, so a pending alias still present is the newer write, and it wins on read and when processTagsAndBaggage folds it into the tag. Every DDSpanContext write path (by name, by id, by entry, and in bulk) goes through SpanTags.prepareWrite. Names that don't depend on direction cost one small map probe; ids that don't, one array load. The accepted corner (a pre-kind write of either possible tag drops a pending alias) is documented in SpanTags and DDSpanContext. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 113 ++++++++++--- .../DDSpanDirectionalTagConsistencyTest.java | 49 ++++++ .../trace/core/DDSpanDirectionalTagsTest.java | 7 +- .../java/datadog/trace/api/KnownTagCodec.java | 45 ++++- .../main/java/datadog/trace/api/SpanTags.java | 160 +++++++++++++----- 5 files changed, 292 insertions(+), 82 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index a323cd1472d..b124c29bcf1 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -666,7 +666,7 @@ private void seedAbsentTags(final TagMap map) { return; } if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { - ctx.unsafeTags.set(tagEntry); + ctx.storeEntry(tagEntry); } }); } @@ -975,25 +975,25 @@ public void setMetric(final CharSequence key, final Number value) { public void setMetric(final CharSequence key, final int value) { synchronized (unsafeTags) { - unsafeTags.set(key.toString(), value); + storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { synchronized (unsafeTags) { - unsafeTags.set(key.toString(), value); + storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { synchronized (unsafeTags) { - unsafeTags.set(key.toString(), value); + storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { synchronized (unsafeTags) { - unsafeTags.set(key.toString(), value); + storeTag(key.toString(), value); } } @@ -1003,40 +1003,94 @@ public void setMetric(final TagMap.EntryReader entry) { } synchronized (unsafeTags) { - unsafeTags.set(entry); + storeEntry(entry); } } /* - * Store a tag that interception let through, as written: a direction-dependent name is resolved - * when it is read or emitted (see SpanTags), never here. Call while holding the unsafeTags lock. + * Store a tag that interception let through. A direction-dependent name goes through SpanTags, + * which applies last-write-wins between a tag and its direction-scoped alias without ordering + * state; every other name costs one small map probe. Known corner, accepted: before the span has + * a kind, writing an alias and then either of its possible tags drops the alias, even when the + * eventual kind maps it to the other tag (see SpanTags). Call while holding the unsafeTags lock. */ private void storeTag(String tag, Object value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } private void storeTag(String tag, CharSequence value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } private void storeTag(String tag, boolean value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } private void storeTag(String tag, int value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } private void storeTag(String tag, long value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } private void storeTag(String tag, float value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } private void storeTag(String tag, double value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } + } + + /** {@link #storeTag} for an entry, by id or by name. */ + private void storeEntry(TagMap.EntryReader entry) { + long tagId = entry.tagId(); + if (tagId != 0) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + unsafeTags.set(entry); + return; + } + long aliasedId = SpanTags.prepareWrite(unsafeTags, entry.tag(), tagDirection()); + if (aliasedId != 0) { + unsafeTags.set(aliasedId, entry.objectValue()); + } else { + unsafeTags.set(entry); + } } public void removeTag(String tag) { @@ -1126,6 +1180,7 @@ public void setTag(final long tagId, final Object value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1140,6 +1195,7 @@ public void setTag(final long tagId, final CharSequence value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1153,6 +1209,7 @@ public void setTag(final long tagId, final boolean value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1166,6 +1223,7 @@ public void setTag(final long tagId, final int value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1179,6 +1237,7 @@ public void setTag(final long tagId, final long value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1192,6 +1251,7 @@ public void setTag(final long tagId, final float value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1205,6 +1265,7 @@ public void setTag(final long tagId, final double value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, value); } } @@ -1218,7 +1279,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!tagInterceptor.interceptTag(this, entry)) { synchronized (unsafeTags) { - unsafeTags.set(entry); + storeEntry(entry); } } } @@ -1238,6 +1299,7 @@ public void setTag(TagMap.EntryReader entry) { private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); unsafeTags.set(tagId, box); } } @@ -1336,11 +1398,11 @@ void setAllTags(final TagMap map, boolean needsIntercept) { this, (ctx, tagEntry) -> { if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { - ctx.unsafeTags.set(tagEntry); + ctx.storeEntry(tagEntry); } }); } else { - unsafeTags.putAll(map); + map.forEach(this, (ctx, tagEntry) -> ctx.storeEntry(tagEntry)); } } } @@ -1363,7 +1425,7 @@ void setAllTags(final TagMap.Ledger ledger) { TagMap.Entry entry = (TagMap.Entry) entryChange; if (!tagInterceptor.interceptTag(this, entry)) { - unsafeTags.set(entry); + storeEntry(entry); } } } @@ -1387,19 +1449,24 @@ void setAllTags(final Map map) { } void unsafeSetTag(final String tag, final Object value) { - unsafeTags.set(tag, value); + storeTag(tag, value); } void unsafeSetTag(final String tag, final CharSequence value) { - unsafeTags.set(tag, value); + storeTag(tag, value); } void unsafeSetTag(final String tag, final byte value) { - unsafeTags.set(tag, value); + long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); + if (tagId != 0) { + unsafeTags.set(tagId, value); + } else { + unsafeTags.set(tag, value); + } } void unsafeSetTag(final String tag, final double value) { - unsafeTags.set(tag, value); + storeTag(tag, value); } Object getTag(final String key) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java index dd6a21161ce..3d256ad8867 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java @@ -3,7 +3,9 @@ import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.ListWriter; @@ -12,6 +14,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; /** * A direction-dependent name ({@code peer.port}, {@code server.address}) must hold one value per @@ -71,6 +74,52 @@ void aLaterCanonicalWriteWinsOverAnEarlierAlias() { assertEquals("new.example", span.getTag(Tags.PEER_HOSTNAME)); } + @TableTest({ + "scenario | kindFirst | kind | direction | first | second ", + "client: tag then alias, pre-kind | false | client | 1 | peer.hostname | server.address", + "client: alias then tag, pre-kind | false | client | 1 | server.address | peer.hostname ", + "client: alias overwrites tag | true | client | 1 | peer.hostname | server.address", + "client: tag overwrites alias | true | client | 1 | server.address | peer.hostname ", + "server: tag then alias, pre-kind | false | server | 0 | http.hostname | server.address", + "server: alias then tag, pre-kind | false | server | 0 | server.address | http.hostname ", + "server: alias overwrites tag | true | server | 0 | http.hostname | server.address", + "server: tag overwrites alias | true | server | 0 | server.address | http.hostname " + }) + void theLastWriteWinsBetweenATagAndItsAlias( + boolean kindFirst, String kind, int direction, String first, String second) { + String tag = first.equals("server.address") ? second : first; + if (kindFirst) { + span.setTag(Tags.SPAN_KIND, kind); + } + span.setTag(first, "first.example"); + span.setTag(second, "second.example"); + if (!kindFirst) { + span.setTag(Tags.SPAN_KIND, kind); + } + + assertEquals("second.example", span.getTag(tag)); + assertEquals("second.example", span.getTag("server.address")); + + // And when the span is emitted: the pending alias, if any, is folded into the tag. + TagMap emitted = span.getTags().copy(); + SpanTags.resolveScopedNames(emitted, direction); + assertEquals("second.example", emitted.getObject(tag)); + assertNull(emitted.getObject("server.address")); + } + + /** + * The accepted corner (see SpanTags): a pre-kind write of either possible tag drops the alias. + */ + @Test + void aPreKindWriteOfTheOtherPossibleTagDropsAPendingAlias() { + span.setTag("server.address", "scoped.example"); + span.setTag(Tags.PEER_HOSTNAME, "peer.example"); + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + + assertNull(span.getTag(Tags.HTTP_HOSTNAME)); + assertEquals("peer.example", span.getTag(Tags.PEER_HOSTNAME)); + } + @Test void aSharedNameKeepsItsValueWhenTheKindChanges() { span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java index 941207dff4f..5cfd93aa836 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java @@ -91,13 +91,14 @@ void aNameSetBeforeTheSpanHasADirectionReadsAsTheTagOnceItHasOne() { } @Test - void theTagWinsOverItsDirectionScopedName() { + void aDirectionScopedNameWrittenLastOverwritesItsTag() { span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); span.setTag(Tags.PEER_HOSTNAME, "tag.example"); span.setTag("server.address", "scoped.example"); - assertEquals("tag.example", span.getTag(Tags.PEER_HOSTNAME)); - assertEquals("tag.example", span.getTag("server.address")); + assertEquals("scoped.example", span.getTag(Tags.PEER_HOSTNAME)); + assertEquals("scoped.example", span.getTag("server.address")); + assertEquals("scoped.example", storedValue(KnownTags.PEER_HOSTNAME_ID)); } @Test diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index c9f63f72d04..631290719c0 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -273,26 +273,53 @@ public static String directionScopedNameOf(String ddName, int direction) { if (direction == DIRECTION_UNKNOWN) { return null; } - Long tagId = DirectionScoped.TAG_IDS.get(ddName); - return tagId == null ? null : directionScopedNameOf(tagId, direction); + long role = directionRoleOf(ddName); + return role > 0 ? directionScopedNameOf(role, direction) : null; } - /** The tags renamed in some directions only, by Datadog name; built once from the registry. */ + /** What {@link #directionRoleOf} returns for a name that does not depend on direction. */ + static final long NOT_DIRECTION_DEPENDENT = -1L; + + /** + * How {@code name} takes part in direction-scoped naming: 0 for a direction-scoped OpenTelemetry + * name ({@code server.address}); the id of a tag that has one ({@code peer.hostname}); or {@link + * #NOT_DIRECTION_DEPENDENT} for every other name. One small map probe. + */ + static long directionRoleOf(String name) { + Long role = DirectionScoped.ROLES.get(name); + return role == null ? NOT_DIRECTION_DEPENDENT : role; + } + + /** True if {@code tagId} has a direction-scoped OpenTelemetry name; an array load. */ + static boolean hasDirectionScopedName(long tagId) { + int serial = serialNum(tagId); + boolean[] bySerial = DirectionScoped.BY_SERIAL; + return serial < bySerial.length && bySerial[serial]; + } + + /** + * The names that depend on direction, built once from the registry: each direction-scoped + * OpenTelemetry name, and each tag that has one. + */ private static final class DirectionScoped { - static final Map TAG_IDS = directionScopedTagIds(); + static final boolean[] BY_SERIAL = new boolean[serialLimit()]; + static final Map ROLES = directionRoles(); - private static Map directionScopedTagIds() { - Map tagIds = new HashMap<>(); + private static Map directionRoles() { + Map roles = new HashMap<>(); for (int serial = 1; serial < serialLimit(); serial++) { String name = nameOf(makeTagId(serial)); long tagId = keyOf(name); for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { - if (directionScopedNameOf(tagId, direction) != null) { - tagIds.put(name, tagId); + String scopedName = directionScopedNameOf(tagId, direction); + if (scopedName != null) { + BY_SERIAL[serial] = true; + roles.put(name, tagId); + roles.put(scopedName, 0L); } } } - return tagIds; + return roles; } } diff --git a/internal-api/src/main/java/datadog/trace/api/SpanTags.java b/internal-api/src/main/java/datadog/trace/api/SpanTags.java index 0ef85c7ae13..aaf967f74b1 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanTags.java +++ b/internal-api/src/main/java/datadog/trace/api/SpanTags.java @@ -12,14 +12,32 @@ /** * A span's reading of the few tag names that depend on its direction. {@link TagMap} stores every * name as written and knows nothing of span kinds -- baggage is a TagMap too -- so the direction is - * applied here, by the span layer, when a tag is read, removed or emitted. + * applied here, by the span layer, when a tag is written, read, removed or emitted. * *

A tag is stored once, under its id, whatever the direction: {@code peer.port} is one tag that * is emitted as {@code client.port} inbound and {@code server.port} outbound. A direction-scoped - * OpenTelemetry name ({@code server.address}, which is {@code http.hostname} inbound and {@code - * peer.hostname} outbound) cannot be resolved until the span has a direction, so a value written - * under it is stored under that name, and resolved by the rule {@code (name, direction) -> tag}. - * When both the tag and its scoped name hold a value, the tag's wins. + * OpenTelemetry name -- an alias, such as {@code server.address}, which is {@code http.hostname} + * inbound and {@code peer.hostname} outbound -- denotes its tag by the rule {@code (alias, + * direction) -> tag}. + * + *

Between a tag and its alias, the last write wins, with no ordering state (a TagMap has no + * insertion order): + * + *

    + *
  • Once the span has a direction, an alias write is stored as its tag, overwriting it. + *
  • Before then, an alias write is stored as written, pending. A write of a tag that has an + * alias removes every pending alias that could denote it. So a pending alias that is still + * present is, by construction, newer than its tag, and wins over it when read and when + * resolved for emission. + *
+ * + *

The accepted corner: before the span has a direction, a write of either possible tag of an + * alias removes the pending alias, even if the span's eventual direction maps the alias to the + * other tag. Writing {@code server.address}, then {@code peer.hostname}, then making the span a + * server loses the {@code server.address} value, which would have been {@code http.hostname}. + * Likewise a value written under an alias that turns out to have no tag in the span's direction + * ({@code client.port} on what becomes a client span) is removed by an earlier-direction write of a + * tag it could have denoted ({@code peer.port}). */ public final class SpanTags { /** The direction of a span with these tags, from its {@code span.kind}. */ @@ -58,39 +76,91 @@ public static String openTelemetryTagOf(TagMap.EntryReader entry, int direction) } /** - * The value of {@code name} on a span of {@code direction}: a direction-scoped name reads its - * tag, and a tag reads a value written under its scoped name. Costs one small switch more than - * {@link TagMap#getObject} for any other name. + * Prepares a write of {@code name} to a span of {@code direction}, returning the id to store the + * value under, or 0 to store it under {@code name}. An alias the direction resolves becomes its + * tag's id; a write of a tag that has an alias removes the pending aliases it supersedes. Every + * other name costs one small map probe. */ - public static Object get(TagMap tags, String name, int direction) { - long tagId = KnownTagCodec.directionalKeyOf(name, direction); - if (tagId > 0) { - TagMap.Entry tag = tags.getEntry(tagId); - return tag != null ? tag.objectValue() : tags.getObject(name); + public static long prepareWrite(TagMap tags, String name, int direction) { + long role = KnownTagCodec.directionRoleOf(name); + if (role == KnownTagCodec.NOT_DIRECTION_DEPENDENT) { + return 0L; } - Object value = tags.getObject(name); - if (value != null || direction == DIRECTION_UNKNOWN) { - return value; + if (role == 0) { + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + if (tagId <= 0) { + return 0L; // no direction yet, or no tag in it: stored as written + } + tags.remove(name); // a value pending under the alias is older than this write + return tagId; + } + removePendingAliases(tags, role, direction); + return 0L; + } + + /** {@link #prepareWrite(TagMap, String, int)} for a write of the known tag {@code tagId}. */ + public static void prepareWrite(TagMap tags, long tagId, int direction) { + if (KnownTagCodec.hasDirectionScopedName(tagId)) { + removePendingAliases(tags, tagId, direction); } - String scopedName = KnownTagCodec.directionScopedNameOf(name, direction); - return scopedName == null ? null : tags.getObject(scopedName); } /** - * The value of the known tag {@code tagId}, or of its scoped name on a span of {@code direction}. + * Removes the aliases a write of {@code tagId} supersedes: the one it has in {@code direction}, + * or, with no direction yet, any it could have. */ - public static Object get(TagMap tags, long tagId, int direction) { - TagMap.Entry tag = tags.getEntry(tagId); - if (tag != null || direction == DIRECTION_UNKNOWN) { - return tag == null ? null : tag.objectValue(); + private static void removePendingAliases(TagMap tags, long tagId, int direction) { + if (direction != DIRECTION_UNKNOWN) { + String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); + if (alias != null) { + tags.remove(alias); + } + return; + } + for (int d = DIRECTION_INBOUND; d <= DIRECTION_NONE; d++) { + String alias = KnownTagCodec.directionScopedNameOf(tagId, d); + if (alias != null) { + tags.remove(alias); + } } - String scopedName = KnownTagCodec.directionScopedNameOf(tagId, direction); - return scopedName == null ? null : tags.getObject(scopedName); } /** - * Removes {@code name} from a span of {@code direction}: the tag and its scoped name together. + * The value of {@code name} on a span of {@code direction}. An alias reads its tag, and a tag + * reads its alias; a pending alias, being the newer write, wins. Every other name costs one small + * map probe more than {@link TagMap#getObject}. */ + public static Object get(TagMap tags, String name, int direction) { + long role = KnownTagCodec.directionRoleOf(name); + if (role == KnownTagCodec.NOT_DIRECTION_DEPENDENT) { + return tags.getObject(name); + } + if (role == 0) { + Object pending = tags.getObject(name); + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + if (pending != null || tagId <= 0) { + return pending; + } + TagMap.Entry tag = tags.getEntry(tagId); + return tag == null ? null : tag.objectValue(); + } + return get(tags, role, direction); + } + + /** The value of the known tag {@code tagId} on a span of {@code direction}; see {@link #get}. */ + public static Object get(TagMap tags, long tagId, int direction) { + if (direction != DIRECTION_UNKNOWN && KnownTagCodec.hasDirectionScopedName(tagId)) { + String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); + Object pending = alias == null ? null : tags.getObject(alias); + if (pending != null) { + return pending; + } + } + TagMap.Entry tag = tags.getEntry(tagId); + return tag == null ? null : tag.objectValue(); + } + + /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ public static void remove(TagMap tags, String name, int direction) { tags.remove(name); if (direction == DIRECTION_UNKNOWN) { @@ -100,50 +170,46 @@ public static void remove(TagMap tags, String name, int direction) { if (tagId > 0) { tags.getAndRemove(tagId); } else { - String scopedName = KnownTagCodec.directionScopedNameOf(name, direction); - if (scopedName != null) { - tags.remove(scopedName); + String alias = KnownTagCodec.directionScopedNameOf(name, direction); + if (alias != null) { + tags.remove(alias); } } } - /** Removes the known tag {@code tagId}, and its scoped name, from a span of {@code direction}. */ + /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ public static void remove(TagMap tags, long tagId, int direction) { tags.getAndRemove(tagId); - String scopedName = KnownTagCodec.directionScopedNameOf(tagId, direction); - if (scopedName != null) { - tags.remove(scopedName); + String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); + if (alias != null) { + tags.remove(alias); } } /** - * Resolves each value written under a direction-scoped name to its tag, for a span of {@code - * direction} about to be emitted: the tag takes the value unless it holds one already, and the - * scoped name is dropped either way. A name with no tag in {@code direction} -- or any, while the - * direction is unknown -- stays as written, a custom tag. + * Resolves each pending alias to its tag, for a span of {@code direction} about to be emitted. + * The alias, being the newer write, overwrites its tag. An alias with no tag in {@code direction} + * -- or any, while the direction is unknown -- stays as written, a custom tag. */ public static void resolveScopedNames(TagMap tags, int direction) { if (direction == DIRECTION_UNKNOWN) { return; } - List scoped = null; + List pending = null; for (TagMap.EntryReader entry : tags) { if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { - if (scoped == null) { - scoped = new ArrayList<>(2); + if (pending == null) { + pending = new ArrayList<>(2); } - scoped.add(entry.entry()); + pending.add(entry.entry()); } } - if (scoped == null) { + if (pending == null) { return; } - for (TagMap.Entry entry : scoped) { + for (TagMap.Entry entry : pending) { tags.remove(entry.tag()); - long tagId = KnownTagCodec.directionalKeyOf(entry.tag(), direction); - if (tags.getEntry(tagId) == null) { - tags.set(tagId, entry.objectValue()); - } + tags.set(KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); } } From 44826c69b0cc3ebce98cc1d041d3ce872103fd6c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 12:50:31 -0400 Subject: [PATCH 26/35] Carry the direction role in tag ids so plain setTag pays nothing extra 748a8af98a added a map probe to every write by name and routed bulk copies entry by entry. Now: - The generator sets a HAS_SCOPED_ALIAS bit (bit 0) on each tag with a direction-scoped OpenTelemetry name (peer.hostname, http.hostname, network.client.ip, peer.port). A mask tests it, and that folds away for a constant id. - A write by name does one lookup, KnownTagCodec.keyOrAliasOf, which returns the id (bit included) or the existing alias sentinel. It then stores through the new TagMap.setResolved without a second lookup. Only ids with the bit, or the alias sentinel, go through SpanTags. - Bulk copies use putAll again. needsIntercept(TagMap), computed once per tracer-level map, is also true for a map with a direction-dependent name. Per-entry paths test the id bit, or a 64-bit hash filter of the aliases for a custom entry. The last-write-wins semantics and their tests are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 1 + .../buildlogic/tagRegistry/TagRegistry.kt | 9 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 2 + .../datadog/trace/core/SetTagBenchmark.java | 6 + .../datadog/trace/core/DDSpanContext.java | 133 +++++++++--------- .../core/taginterceptor/TagInterceptor.java | 8 +- .../taginterceptor/TagInterceptorTest.java | 18 +++ .../java/datadog/trace/api/KnownTagCodec.java | 85 ++++++----- .../main/java/datadog/trace/api/SpanTags.java | 80 ++++++----- .../main/java/datadog/trace/api/TagMap.java | 42 ++++++ .../java/datadog/trace/api/KnownTagsTest.java | 24 ++++ 11 files changed, 256 insertions(+), 152 deletions(-) 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 09ad4f35fe1..c88b895281b 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 @@ -98,6 +98,7 @@ object KnownTagsEmitter { append("// makeTagId(serial=${t.serial})") if (t.traceLevel) append(" + trace-level") if (t.intercepted) append(" + intercepted") + if (t.scopedOtelNames.isNotEmpty()) append(" + scoped-alias") if (t.otelName != null) append(" -> ${escape(t.otelName)}") for ((direction, otel) in t.scopedOtelNames) append(" -> ${escape(otel)} (${direction.yamlKey})") appendLine(" <${escape(t.required)}>") 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 79d070e7727..e0bfbe97f3d 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 @@ -41,6 +41,7 @@ class TagRegistry private constructor(val tags: List) { 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 HAS_SCOPED_ALIAS = 1L shl 0 // low-32 carve bit 0; mirrors KnownTagCodec.HAS_SCOPED_ALIAS /** * Serials stay below 2^15, so bit 63 of an id is never set: every id is positive, clear of 0 @@ -50,10 +51,11 @@ class TagRegistry private constructor(val tags: List) { /** * Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() + intercepted() -- must stay in sync. - * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, other low bits and the reserved [47-32] window + * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, HAS_SCOPED_ALIAS at bit 0 (the tag has an + * OpenTelemetry name in some directions only), other low bits and the reserved [47-32] window * zero. Every id is made here, and the range check makes each one positive by construction. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long { + fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, hasScopedAlias: Boolean = false): Long { require(serial in FIRST_SERIAL until SERIAL_CAPACITY) { "serial $serial is outside [$FIRST_SERIAL, $SERIAL_CAPACITY): more tags would make tag ids " + "negative, where KnownTagCodec's lookup sentinels live" @@ -61,6 +63,7 @@ class TagRegistry private constructor(val tags: List) { var id = serial.toLong() shl 48 if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED + if (hasScopedAlias) id = id or HAS_SCOPED_ALIAS return id } @@ -87,7 +90,7 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted), + id = encode(serial, isTraceLevel, isIntercepted, scoped.isNotEmpty()), otelName = otelName, scopedOtelNames = scoped, ) diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index d49499d1022..2169afd1908 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -339,6 +339,8 @@ class TagRegistryGeneratorTest { mapOf(TagConventions.Direction.INBOUND to "client.port", TagConventions.Direction.OUTBOUND to "server.port") ) assertThat(tags.values.map { it.otelName }).containsOnlyNulls() + assertThat(tags.values.filter { it.id and TagRegistry.HAS_SCOPED_ALIAS != 0L }.map { it.name }) + .containsExactlyInAnyOrder("http.hostname", "peer.hostname", "peer.port") assertThat(conventions.resolve("http.client").map { it.name }).contains("peer.port@outbound") assertThat(conventions.resolve("http.server").map { it.name }).contains("peer.port@inbound") } diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java index a9d1c25d13e..353bcd69c42 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java @@ -90,6 +90,12 @@ public Object knownTag_byName() { return span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}"); } + /** A tag with a direction-scoped alias: the only names that pay for alias handling. */ + @Benchmark + public Object aliasTargetTag_byName() { + return span.setTag(KnownTags.PEER_HOSTNAME_NAME, "db.example"); + } + @Benchmark public Object customTag_byName() { return span.setTag("my.custom.tag", "/users/{id}"); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index b124c29bcf1..f9c032bf586 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1008,86 +1008,69 @@ public void setMetric(final TagMap.EntryReader entry) { } /* - * Store a tag that interception let through. A direction-dependent name goes through SpanTags, - * which applies last-write-wins between a tag and its direction-scoped alias without ordering - * state; every other name costs one small map probe. Known corner, accepted: before the span has - * a kind, writing an alias and then either of its possible tags drops the alias, even when the - * eventual kind maps it to the other tag (see SpanTags). Call while holding the unsafeTags lock. + * Store a tag that interception let through. The name is looked up once, as a plain write by + * name would: the result is the id to store under, and also says whether the name takes part in + * direction-scoped naming -- an id with KnownTagCodec.HAS_SCOPED_ALIAS, or the alias sentinel. + * Only those go through SpanTags, which applies last-write-wins between a tag and its alias + * without ordering state; every other name costs nothing extra. Known corner, accepted: before + * the span has a kind, writing an alias and then either of its possible tags drops the alias, + * even when the eventual kind maps it to the other tag (see SpanTags). Call while holding the + * unsafeTags lock. */ - private void storeTag(String tag, Object value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); + private long writeKeyOf(String tag) { + long key = KnownTagCodec.keyOrAliasOf(tag); + if (key > 0) { + if (KnownTagCodec.hasScopedAlias(key)) { + SpanTags.prepareWrite(unsafeTags, key, tagDirection()); + } + return key; } + return key == 0 ? 0L : SpanTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); + } + + private void storeTag(String tag, Object value) { + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } private void storeTag(String tag, CharSequence value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } private void storeTag(String tag, boolean value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } private void storeTag(String tag, int value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } private void storeTag(String tag, long value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } private void storeTag(String tag, float value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } private void storeTag(String tag, double value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + unsafeTags.setResolved(writeKeyOf(tag), tag, value); } - /** {@link #storeTag} for an entry, by id or by name. */ + /** {@link #storeTag} for an entry, which carries its id: no lookup unless it may be an alias. */ private void storeEntry(TagMap.EntryReader entry) { long tagId = entry.tagId(); if (tagId != 0) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(entry); - return; - } - long aliasedId = SpanTags.prepareWrite(unsafeTags, entry.tag(), tagDirection()); - if (aliasedId != 0) { - unsafeTags.set(aliasedId, entry.objectValue()); + } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { + long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); + if (aliasedId != 0) { + unsafeTags.set(aliasedId, entry.objectValue()); + } else { + unsafeTags.set(entry); + } } else { unsafeTags.set(entry); } @@ -1180,7 +1163,9 @@ public void setTag(final long tagId, final Object value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1195,7 +1180,9 @@ public void setTag(final long tagId, final CharSequence value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1209,7 +1196,9 @@ public void setTag(final long tagId, final boolean value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1223,7 +1212,9 @@ public void setTag(final long tagId, final int value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1237,7 +1228,9 @@ public void setTag(final long tagId, final long value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1251,7 +1244,9 @@ public void setTag(final long tagId, final float value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1265,7 +1260,9 @@ public void setTag(final long tagId, final double value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, value); } } @@ -1299,7 +1296,9 @@ public void setTag(TagMap.EntryReader entry) { private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + if (KnownTagCodec.hasScopedAlias(tagId)) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } unsafeTags.set(tagId, box); } } @@ -1402,7 +1401,8 @@ void setAllTags(final TagMap map, boolean needsIntercept) { } }); } else { - map.forEach(this, (ctx, tagEntry) -> ctx.storeEntry(tagEntry)); + // needsIntercept(TagMap) is also true for a map with a direction-dependent name. + unsafeTags.putAll(map); } } } @@ -1457,12 +1457,7 @@ void unsafeSetTag(final String tag, final CharSequence value) { } void unsafeSetTag(final String tag, final byte value) { - long tagId = SpanTags.prepareWrite(unsafeTags, tag, tagDirection()); - if (tagId != 0) { - unsafeTags.set(tagId, value); - } else { - unsafeTags.set(tag, value); - } + storeTag(tag, value); } void unsafeSetTag(final String tag, final double value) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index d65866c385b..2e224319f54 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -22,6 +22,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.Pair; +import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.env.CapturedEnvironment; @@ -137,9 +138,14 @@ private boolean isSplitServiceTag(String customTag) { return names != null && names.contains(customTag); } + /** + * Whether {@code map} must be copied onto a span entry by entry: some entry may be routed, or + * holds a direction-dependent name, which the span resolves through {@link SpanTags}. Computed + * once for a tracer-level map, so a map with neither takes the bulk-copy fast path. + */ public boolean needsIntercept(TagMap map) { for (TagMap.EntryReader entry : map) { - if (needsIntercept(entry)) return true; + if (needsIntercept(entry) || SpanTags.isDirectionDependent(entry)) return true; } return false; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index d988b73c34f..36304a6b7b4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -29,6 +29,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; +import datadog.trace.api.TagMap; import datadog.trace.api.env.CapturedEnvironment; import datadog.trace.api.remoteconfig.ServiceNameCollector; import datadog.trace.api.remoteconfig.ServiceNameCollectorTestBridge; @@ -981,6 +982,23 @@ void splitByTagsOnANameDeclaredPerDirectionMatchesItsTag() { assertTrue(interceptor.needsIntercept(Tags.PEER_PORT)); } + /** A map with a direction-dependent name must be copied entry by entry, not in bulk. */ + @Test + void aMapWithADirectionDependentNameIsNotBulkCopied() { + TagInterceptor interceptor = splittingInterceptor("unrelated.tag"); + TagMap plain = TagMap.create(); + plain.set("my.custom.tag", "value"); + plain.set(KnownTags.HTTP_ROUTE_NAME, "/users"); + TagMap alias = TagMap.create(); + alias.set("server.address", "example.com"); + TagMap aliasTarget = TagMap.create(); + aliasTarget.set(Tags.PEER_HOSTNAME, "example.com"); + + assertFalse(interceptor.needsIntercept(plain)); + assertTrue(interceptor.needsIntercept(alias)); + assertTrue(interceptor.needsIntercept(aliasTarget)); + } + private static TagInterceptor splittingInterceptor(String splitByTag) { return new TagInterceptor( true, "my-service", Collections.singleton(splitByTag), new RuleFlags(), false); diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 631290719c0..c307b38ae88 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -1,8 +1,5 @@ package datadog.trace.api; -import java.util.HashMap; -import java.util.Map; - /** * Resolves generated tag IDs and names. {@link KnownTags} owns the lookup tables; this class owns * the ID layout and namespace naming policy. @@ -63,6 +60,19 @@ public static boolean isIntercepted(long tagId) { return (tagId & INTERCEPTED) != 0L; } + /** + * HAS_SCOPED_ALIAS bit (low-32 carve, bit 0): marks a tag with an OpenTelemetry name that holds + * in some directions only ({@code peer.hostname} is {@code server.address} on outbound spans), so + * a value written under that alias can denote it. A write of a constant id tests the bit at JIT + * time, so the alias handling folds away for every tag without it. + */ + public static final long HAS_SCOPED_ALIAS = 1L << 0; + + /** True if {@code tagId} has an OpenTelemetry name that holds in some directions only. */ + public static boolean hasScopedAlias(long tagId) { + return (tagId & HAS_SCOPED_ALIAS) != 0L; + } + /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; @@ -105,9 +115,9 @@ public static long makeTagId(int serialNum) { /** * What {@link Resolver#lookup} returns for an OpenTelemetry name that applies in one direction * only: {@code server.address} is {@code http.hostname} on inbound spans and {@code - * peer.hostname} on outbound ones. + * peer.hostname} on outbound ones. {@link #keyOrAliasOf} returns it too. */ - static final long DIRECTION_SCOPED_OTEL_NAME_SENTINEL = -2L; + public static final long DIRECTION_SCOPED_OTEL_NAME_SENTINEL = -2L; /** * What {@link #directionalKeyOf} returns for a name whose tag depends on the span's direction but @@ -255,7 +265,7 @@ public static long directionalKeyOf(String name, int direction) { * or null when it has none there, or its name there holds in every direction. */ public static String directionScopedNameOf(long tagId, int direction) { - if (direction == DIRECTION_UNKNOWN) { + if (direction == DIRECTION_UNKNOWN || !hasScopedAlias(tagId)) { return null; } Resolver resolver = Installed.RESOLVER; @@ -265,61 +275,50 @@ public static String directionScopedNameOf(long tagId, int direction) { : null; } - /** - * {@link #directionScopedNameOf(long, int)} by Datadog name. Costs one small map probe for every - * name but the few tags renamed in some directions only. - */ + /** {@link #directionScopedNameOf(long, int)} by Datadog name. */ public static String directionScopedNameOf(String ddName, int direction) { if (direction == DIRECTION_UNKNOWN) { return null; } - long role = directionRoleOf(ddName); - return role > 0 ? directionScopedNameOf(role, direction) : null; + long tagId = keyOf(ddName); + return tagId != 0 ? directionScopedNameOf(tagId, direction) : null; } - /** What {@link #directionRoleOf} returns for a name that does not depend on direction. */ - static final long NOT_DIRECTION_DEPENDENT = -1L; - /** - * How {@code name} takes part in direction-scoped naming: 0 for a direction-scoped OpenTelemetry - * name ({@code server.address}); the id of a tag that has one ({@code peer.hostname}); or {@link - * #NOT_DIRECTION_DEPENDENT} for every other name. One small map probe. + * {@link #keyOf(String)}, except that a direction-scoped alias ({@code server.address}) returns + * {@link #DIRECTION_SCOPED_OTEL_NAME_SENTINEL} instead of 0. One lookup, like {@code keyOf}: a + * span writing a tag by name learns from it both the id to store under and whether the name + * depends on its direction (the alias sentinel, or an id with {@link #HAS_SCOPED_ALIAS}). */ - static long directionRoleOf(String name) { - Long role = DirectionScoped.ROLES.get(name); - return role == null ? NOT_DIRECTION_DEPENDENT : role; - } - - /** True if {@code tagId} has a direction-scoped OpenTelemetry name; an array load. */ - static boolean hasDirectionScopedName(long tagId) { - int serial = serialNum(tagId); - boolean[] bySerial = DirectionScoped.BY_SERIAL; - return serial < bySerial.length && bySerial[serial]; + public static long keyOrAliasOf(String name) { + return Installed.RESOLVER.lookup(name); } /** - * The names that depend on direction, built once from the registry: each direction-scoped - * OpenTelemetry name, and each tag that has one. + * A cheap pre-check for a name that may be a direction-scoped alias: false for nearly every other + * name, at the cost of a bit test on its (cached) hash. A true answer still needs {@link + * #directionalKeyOf} to confirm. */ - private static final class DirectionScoped { - static final boolean[] BY_SERIAL = new boolean[serialLimit()]; - static final Map ROLES = directionRoles(); + public static boolean mayBeScopedAlias(String name) { + return (AliasFilter.MASK & (1L << name.hashCode())) != 0L; + } + + /** A 64-bit filter over the direction-scoped aliases' hashes, built once from the registry. */ + private static final class AliasFilter { + static final long MASK = aliasMask(); - private static Map directionRoles() { - Map roles = new HashMap<>(); + private static long aliasMask() { + long mask = 0L; for (int serial = 1; serial < serialLimit(); serial++) { - String name = nameOf(makeTagId(serial)); - long tagId = keyOf(name); + long tagId = keyOf(nameOf(makeTagId(serial))); for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { - String scopedName = directionScopedNameOf(tagId, direction); - if (scopedName != null) { - BY_SERIAL[serial] = true; - roles.put(name, tagId); - roles.put(scopedName, 0L); + String alias = directionScopedNameOf(tagId, direction); + if (alias != null) { + mask |= 1L << alias.hashCode(); } } } - return roles; + return mask; } } diff --git a/internal-api/src/main/java/datadog/trace/api/SpanTags.java b/internal-api/src/main/java/datadog/trace/api/SpanTags.java index aaf967f74b1..cac68efbc20 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanTags.java +++ b/internal-api/src/main/java/datadog/trace/api/SpanTags.java @@ -76,35 +76,46 @@ public static String openTelemetryTagOf(TagMap.EntryReader entry, int direction) } /** - * Prepares a write of {@code name} to a span of {@code direction}, returning the id to store the - * value under, or 0 to store it under {@code name}. An alias the direction resolves becomes its - * tag's id; a write of a tag that has an alias removes the pending aliases it supersedes. Every - * other name costs one small map probe. + * Prepares a write of the alias {@code alias} -- a name {@link KnownTagCodec#keyOrAliasOf} + * returned {@link KnownTagCodec#DIRECTION_SCOPED_OTEL_NAME_SENTINEL} for -- to a span of {@code + * direction}, returning the id of the tag to store the value under, or 0 to store it under the + * alias. Also safe for any other name, which returns 0. */ - public static long prepareWrite(TagMap tags, String name, int direction) { - long role = KnownTagCodec.directionRoleOf(name); - if (role == KnownTagCodec.NOT_DIRECTION_DEPENDENT) { - return 0L; - } - if (role == 0) { - long tagId = KnownTagCodec.directionalKeyOf(name, direction); - if (tagId <= 0) { - return 0L; // no direction yet, or no tag in it: stored as written - } - tags.remove(name); // a value pending under the alias is older than this write - return tagId; + public static long prepareAliasWrite(TagMap tags, String alias, int direction) { + long tagId = KnownTagCodec.directionalKeyOf(alias, direction); + if (tagId <= 0) { + return 0L; // not an alias, no direction yet, or no tag in it: stored as written } - removePendingAliases(tags, role, direction); - return 0L; + tags.remove(alias); // a value pending under the alias is older than this write + return tagId; } - /** {@link #prepareWrite(TagMap, String, int)} for a write of the known tag {@code tagId}. */ + /** + * Prepares a write of the known tag {@code tagId} to a span of {@code direction}: when the tag + * has an alias ({@link KnownTagCodec#HAS_SCOPED_ALIAS}), removes the pending aliases the write + * supersedes. A caller with a constant id that lacks the bit can skip the call. + */ public static void prepareWrite(TagMap tags, long tagId, int direction) { - if (KnownTagCodec.hasDirectionScopedName(tagId)) { + if (KnownTagCodec.hasScopedAlias(tagId)) { removePendingAliases(tags, tagId, direction); } } + /** + * True if a span must route {@code entry} through {@link #prepareWrite} or {@link + * #prepareAliasWrite}: a tag with an alias, or an alias. A mask test for a known tag; a bit test + * on the hash, almost always, for a custom one. + */ + public static boolean isDirectionDependent(TagMap.EntryReader entry) { + long tagId = entry.tagId(); + if (tagId != 0) { + return KnownTagCodec.hasScopedAlias(tagId); + } + String tag = entry.tag(); + return KnownTagCodec.mayBeScopedAlias(tag) + && KnownTagCodec.keyOrAliasOf(tag) == KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL; + } + /** * Removes the aliases a write of {@code tagId} supersedes: the one it has in {@code direction}, * or, with no direction yet, any it could have. @@ -127,29 +138,26 @@ private static void removePendingAliases(TagMap tags, long tagId, int direction) /** * The value of {@code name} on a span of {@code direction}. An alias reads its tag, and a tag - * reads its alias; a pending alias, being the newer write, wins. Every other name costs one small - * map probe more than {@link TagMap#getObject}. + * reads its alias; a pending alias, being the newer write, wins. Every other name costs the same + * single lookup as {@link TagMap#getObject}. */ public static Object get(TagMap tags, String name, int direction) { - long role = KnownTagCodec.directionRoleOf(name); - if (role == KnownTagCodec.NOT_DIRECTION_DEPENDENT) { - return tags.getObject(name); - } - if (role == 0) { - Object pending = tags.getObject(name); - long tagId = KnownTagCodec.directionalKeyOf(name, direction); - if (pending != null || tagId <= 0) { - return pending; - } - TagMap.Entry tag = tags.getEntry(tagId); - return tag == null ? null : tag.objectValue(); + long key = KnownTagCodec.keyOrAliasOf(name); + if (key > 0) { + return get(tags, key, direction); } - return get(tags, role, direction); + TagMap.Entry asWritten = tags.getEntryResolved(0L, name); + if (key == 0 || asWritten != null) { + return asWritten == null ? null : asWritten.objectValue(); + } + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + TagMap.Entry tag = tagId > 0 ? tags.getEntry(tagId) : null; + return tag == null ? null : tag.objectValue(); } /** The value of the known tag {@code tagId} on a span of {@code direction}; see {@link #get}. */ public static Object get(TagMap tags, long tagId, int direction) { - if (direction != DIRECTION_UNKNOWN && KnownTagCodec.hasDirectionScopedName(tagId)) { + if (KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); Object pending = alias == null ? null : tags.getObject(alias); if (pending != null) { diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index e82335204d6..24767169191 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1647,6 +1647,48 @@ public void set(@Nonnull String tag, double value) { } } + /* + * Pre-resolved setters: for a caller that has already looked {@code tag} up with + * KnownTagCodec.keyOf (or keyOrAliasOf) and passes the result as {@code tagId} -- the tag's id, + * or 0 (or a negative sentinel) for a custom tag. The same write as set(tag, value), without a + * second lookup. + */ + public void setResolved(long tagId, @Nonnull String tag, @Nonnull Object value) { + this.putEntry(tagId > 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); + } + + public void setResolved(long tagId, @Nonnull String tag, @Nonnull CharSequence value) { + this.putEntry( + tagId > 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value)); + } + + public void setResolved(long tagId, @Nonnull String tag, boolean value) { + this.putEntry( + tagId > 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value)); + } + + public void setResolved(long tagId, @Nonnull String tag, int value) { + this.putEntry(tagId > 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value)); + } + + public void setResolved(long tagId, @Nonnull String tag, long value) { + this.putEntry(tagId > 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value)); + } + + public void setResolved(long tagId, @Nonnull String tag, float value) { + this.putEntry(tagId > 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value)); + } + + public void setResolved(long tagId, @Nonnull String tag, double value) { + this.putEntry( + tagId > 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value)); + } + + /** The read counterpart of {@link #setResolved(long, String, Object)}. */ + public Entry getEntryResolved(long tagId, String tag) { + return tagId > 0 ? this.getEntry(tagId) : this.getEntry(tag, Entry.customHash(tag)); + } + /* * Id-keyed setters: the caller passes a KnownTags.*_ID, so the entry is built under the tag's * canonical name without the name lookup the String setters pay. The id must name a known tag; diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index db722a44ace..7ab62e60314 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -167,6 +167,30 @@ void unknownNamesResolveToZero() { * but it is one tag either way: one id, one stored value. Only its OpenTelemetry name depends on * the span's direction. */ + /** The direction role rides on the one lookup a write by name already does. */ + @Test + void theLookupCarriesTheDirectionRole() { + assertEquals( + KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, + KnownTagCodec.keyOrAliasOf("server.address")); + assertEquals(KnownTags.PEER_HOSTNAME_ID, KnownTagCodec.keyOrAliasOf(Tags.PEER_HOSTNAME)); + assertEquals(0L, KnownTagCodec.keyOrAliasOf("my.custom.tag")); + for (long tagId : + new long[] { + KnownTags.PEER_HOSTNAME_ID, + KnownTags.HTTP_HOSTNAME_ID, + KnownTags.NETWORK_CLIENT_IP_ID, + KnownTags.PEER_PORT_ID + }) { + assertTrue(KnownTagCodec.hasScopedAlias(tagId), KnownTagCodec.nameOf(tagId)); + } + assertFalse(KnownTagCodec.hasScopedAlias(KnownTags.HTTP_METHOD_ID)); + for (String alias : + new String[] {"server.address", "server.port", "client.port", "network.peer.address"}) { + assertTrue(KnownTagCodec.mayBeScopedAlias(alias), alias); + } + } + @Test void aNameDeclaredPerDirectionIsOneTag() { assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_ID)); From dc0672555aa778eb01213cdf44b658d8784cce22 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 14:37:00 -0400 Subject: [PATCH 27/35] Move alias superseding off the setTag store fast path The HAS_SCOPED_ALIAS test in the id setters cost about 5 ns on setTag by name. That chain sits at C2's InlineSmallCode limit; with the extra code, DDSpan.setTag "failed to inline: already compiled into a big method". TagInterceptor.needsIntercept(long) now tests INTERCEPTED and HAS_SCOPED_ALIAS with one mask, the same instruction count as before. interceptTag, which stays out of line, calls DDSpanContext.supersedeAliases for a tag with the bit and still lets the tag be stored. The store fast path is back to exactly its pre-alias shape. Tags with an alias pay a second lock acquisition, and primitive writes of them (peer.port) box, as an intercepted tag's do. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 37 +++++++------------ .../core/taginterceptor/TagInterceptor.java | 16 ++++++-- .../taginterceptor/TagInterceptorTest.java | 11 +++++- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index f9c032bf586..66c6a2f0646 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1149,6 +1149,19 @@ public void setTag(final String tag, final String value) { } } + /** + * Removes the pending aliases a write of {@code tagId} supersedes. Called by {@link + * TagInterceptor} for a tag with {@link KnownTagCodec#HAS_SCOPED_ALIAS}, which it routes like an + * intercepted tag, so the id setters' fast path carries no alias handling at all. The tag itself + * is stored right after, under its own lock acquisition; a concurrent write of the alias from + * another thread in between has no defined order anyway. + */ + public void supersedeAliases(long tagId) { + synchronized (unsafeTags) { + SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + } + } + /* * Id-keyed setters, mirroring the String setters above. With a constant id, the * KnownTagCodec.INTERCEPTED test folds, leaving only the split-by-tags table load; an id set never @@ -1163,9 +1176,6 @@ public void setTag(final long tagId, final Object value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1180,9 +1190,6 @@ public void setTag(final long tagId, final CharSequence value) { } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1196,9 +1203,6 @@ public void setTag(final long tagId, final boolean value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1212,9 +1216,6 @@ public void setTag(final long tagId, final int value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1228,9 +1229,6 @@ public void setTag(final long tagId, final long value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1244,9 +1242,6 @@ public void setTag(final long tagId, final float value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1260,9 +1255,6 @@ public void setTag(final long tagId, final double value) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, value); } } @@ -1296,9 +1288,6 @@ public void setTag(TagMap.EntryReader entry) { private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } unsafeTags.set(tagId, box); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 2e224319f54..fa58ee35745 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -168,12 +168,16 @@ public boolean needsIntercept(Map map) { } /** - * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag. With a constant - * id, the {@link KnownTagCodec#INTERCEPTED} test folds to a constant, leaving only the - * split-by-tags table load at run time. {@code tagId} must be a known id. + * Whether {@link #interceptTag(DDSpanContext, long, Object)} may route the tag, or must see it to + * supersede a pending direction-scoped alias ({@link KnownTagCodec#HAS_SCOPED_ALIAS}). With a + * constant id, the bit test folds to a constant, leaving only the split-by-tags table load at run + * time. {@code tagId} must be a known id. */ public boolean needsIntercept(long tagId) { - return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); + // One mask test for both bits: a tag with a direction-scoped alias is routed here too, so the + // span can supersede a pending alias without adding code to its store fast path. + return (tagId & (KnownTagCodec.INTERCEPTED | KnownTagCodec.HAS_SCOPED_ALIAS)) != 0L + || isSplitServiceTag(tagId); } public boolean needsIntercept(String tag) { @@ -195,6 +199,10 @@ public boolean interceptTag(DDSpanContext span, String tag, Object value) { * {@code DDSpanContext.setTag}'s compiled code; {@code TagInterceptorInliningTest} pins that. */ public boolean interceptTag(DDSpanContext span, long tagId, Object value) { + if (KnownTagCodec.hasScopedAlias(tagId)) { + // Not a route: the tag is still stored. A write of it supersedes any pending alias. + span.supersedeAliases(tagId); + } switch (KnownTagCodec.serialNum(tagId)) { case KnownTags.RESOURCE_NAME_SERIAL_NUM: return interceptResourceName(span, value); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 36304a6b7b4..c23a76e4112 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -934,9 +934,16 @@ void exactlyTheTagsWithTheInterceptedBitHaveACase() throws IllegalAccessExceptio consumed |= interceptor.interceptTag(span, tagId, value); } - boolean hasCase = consumed || !mockingDetails(span).getInvocations().isEmpty(); + // Superseding a pending alias is not a case: the tag is still stored. + boolean hasCase = + consumed + || mockingDetails(span).getInvocations().stream() + .anyMatch(i -> !i.getMethod().getName().equals("supersedeAliases")); assertEquals(KnownTagCodec.isIntercepted(tagId), hasCase, field.getName()); - assertEquals(KnownTagCodec.isIntercepted(tagId), interceptor.needsIntercept(tagId)); + assertEquals( + KnownTagCodec.isIntercepted(tagId) || KnownTagCodec.hasScopedAlias(tagId), + interceptor.needsIntercept(tagId), + field.getName()); } } From 44c9c2f87b5c7543fa1bdd3b86b3e62b3bcbf740 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 14:49:49 -0400 Subject: [PATCH 28/35] Cover SpanTags and the KnownTagCodec direction helpers in internal-api internal-api's jacocoTestCoverageVerification failed. SpanTags had no internal-api tests (0% instructions and branches), and KnownTagCodec fell to 70% instructions and 50% branches against its 80%/70% thresholds. SpanTagsTest exercises get, remove, prepareWrite, prepareAliasWrite, isDirectionDependent, resolveScopedNames and openTelemetryTagOf across every direction, with the alias present and absent and the last-write-wins cases. KnownTagsTest covers the direction-scoped-name, flag and range helpers. SpanTags is now at 99%/96% and KnownTagCodec at 100%/94%. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/KnownTagsTest.java | 65 ++++ .../java/datadog/trace/api/SpanTagsTest.java | 290 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index 7ab62e60314..0d75314fd0b 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; /** * Checks the generated registry's name resolution and tag ID encoding. Datadog and OpenTelemetry @@ -167,6 +168,70 @@ void unknownNamesResolveToZero() { * but it is one tag either way: one id, one stored value. Only its OpenTelemetry name depends on * the span's direction. */ + @Test + void flagsAndRangeChecks() { + assertTrue(KnownTagCodec.isIntercepted(KnownTags.SPAN_KIND_ID)); + assertFalse(KnownTagCodec.isIntercepted(KnownTags.HTTP_ROUTE_ID)); + assertTrue(KnownTagCodec.isKnown(KnownTags.HTTP_ROUTE_ID)); + assertFalse(KnownTagCodec.isKnown(0L)); + assertFalse(KnownTagCodec.isKnown(KnownTagCodec.makeTagId(KnownTagCodec.serialLimit()))); + } + + @TableTest({ + "scenario | name | direction | expected ", + "client hostname | peer.hostname | 1 | server.address ", + "server hostname | http.hostname | 0 | server.address ", + "server client ip | network.client.ip | 0 | network.peer.address", + "client peer port | peer.port | 1 | server.port ", + "server peer port | peer.port | 0 | client.port " + }) + void aTagsDirectionScopedNameByDatadogName(String name, int direction, String expected) { + assertEquals(expected, KnownTagCodec.directionScopedNameOf(name, direction)); + assertEquals( + expected, KnownTagCodec.directionScopedNameOf(KnownTagCodec.keyOf(name), direction)); + } + + @TableTest({ + "scenario | name | direction", + "no direction | peer.hostname | -1 ", + "no alias in this direction | peer.hostname | 0 ", + "internal span | peer.port | 2 ", + "renamed in every direction | http.method | 1 ", + "custom tag | my.tag | 1 " + }) + void noDirectionScopedName(String name, int direction) { + assertNull(KnownTagCodec.directionScopedNameOf(name, direction)); + } + + @Test + void directionalKeyOfResolvesOnlyAliases() { + assertEquals( + KnownTags.PEER_HOSTNAME_ID, + KnownTagCodec.directionalKeyOf("server.address", DIRECTION_OUTBOUND)); + assertEquals(0L, KnownTagCodec.directionalKeyOf(Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertEquals( + KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL, + KnownTagCodec.directionalKeyOf("server.address", DIRECTION_UNKNOWN)); + } + + @Test + void theAliasFilterRejectsMostOtherNames() { + int rejected = 0; + String[] names = {"my.tag", "http.route", "component", "db.statement", "error.message"}; + for (String name : names) { + if (!KnownTagCodec.mayBeScopedAlias(name)) { + rejected++; + } + } + assertTrue(rejected > 0, "a 64-bit filter over 4 aliases should reject most other names"); + } + + @Test + void canonicalNames() { + assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName("http.request.method")); + assertEquals("my.tag", KnownTagCodec.canonicalTagName("my.tag")); + } + /** The direction role rides on the one lookup a write by name already does. */ @Test void theLookupCarriesTheDirectionRole() { diff --git a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java b/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java new file mode 100644 index 00000000000..18e48428610 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java @@ -0,0 +1,290 @@ +package datadog.trace.api; + +import static datadog.trace.api.KnownTagCodec.DIRECTION_INBOUND; +import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; +import static datadog.trace.api.KnownTagCodec.DIRECTION_OUTBOUND; +import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +/** + * {@link SpanTags}: a span's reading of direction-dependent names over a plain {@link TagMap}. The + * span-level behavior, with real spans, is in dd-trace-core's directional span tests. + */ +class SpanTagsTest { + @TableTest({ + "scenario | kind | direction", + "server | server | 0 ", + "consumer | consumer | 0 ", + "client | client | 1 ", + "producer | producer | 1 ", + "internal | internal | 2 ", + "broker | broker | -1 ", + "custom | custom | -1 " + }) + void theDirectionComesFromTheSpanKind(String kind, int direction) { + assertEquals(direction, SpanTags.directionOfKind(kind)); + + TagMap tags = TagMap.create(); + tags.set(Tags.SPAN_KIND, kind); + assertEquals(direction, SpanTags.directionOf(tags)); + } + + @Test + void noKindIsNoDirection() { + assertEquals(DIRECTION_UNKNOWN, SpanTags.directionOfKind(null)); + assertEquals(DIRECTION_UNKNOWN, SpanTags.directionOf(TagMap.create())); + } + + @TableTest({ + "scenario | tag | direction | otelName ", + "peer.port, client | peer.port | 1 | server.port ", + "peer.port, server | peer.port | 0 | client.port ", + "peer.port, internal | peer.port | 2 | peer.port ", + "peer.port, no direction | peer.port | -1 | peer.port ", + "peer.hostname, client | peer.hostname | 1 | server.address ", + "peer.hostname, server | peer.hostname | 0 | peer.hostname ", + "renamed in every direction | http.method | -1 | http.request.method", + "custom tag | my.tag | 1 | my.tag " + }) + void anEntryIsNamedForTheDirection(String tag, int direction, String otelName) { + TagMap tags = TagMap.create(); + tags.set(tag, "value"); + + assertEquals(otelName, SpanTags.openTelemetryTagOf(tags.getEntry(tag), direction)); + } + + @TableTest({ + "scenario | direction | alias | tag ", + "client: alias reads its tag | 1 | server.address | peer.hostname ", + "server: alias reads its tag | 0 | server.address | http.hostname ", + "server: ip alias reads its tag | 0 | network.peer.address | network.client.ip", + "client: port alias reads its tag | 1 | server.port | peer.port ", + "server: port alias reads its tag | 0 | client.port | peer.port " + }) + void anAliasAndItsTagReadAsOne(int direction, String alias, String tag) { + TagMap tags = TagMap.create(); + tags.set(tag, "stored"); + + assertEquals("stored", SpanTags.get(tags, alias, direction)); + assertEquals("stored", SpanTags.get(tags, tag, direction)); + assertEquals("stored", SpanTags.get(tags, KnownTagCodec.keyOf(tag), direction)); + + // A pending alias is the newer write, so it wins on read, by name or by id. + tags.set(alias, "pending"); + assertEquals("pending", SpanTags.get(tags, alias, direction)); + assertEquals("pending", SpanTags.get(tags, tag, direction)); + assertEquals("pending", SpanTags.get(tags, KnownTagCodec.keyOf(tag), direction)); + } + + @Test + void withNoDirectionAnAliasReadsOnlyAsWritten() { + TagMap tags = TagMap.create(); + tags.set(Tags.PEER_HOSTNAME, "stored"); + + assertNull(SpanTags.get(tags, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals("stored", SpanTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); + + tags.set("server.address", "pending"); + assertEquals("pending", SpanTags.get(tags, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + } + + @Test + void aMissReadsAsNull() { + TagMap tags = TagMap.create(); + + assertNull(SpanTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); + assertNull(SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(SpanTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull(SpanTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertNull(SpanTags.get(tags, "server.address", DIRECTION_OUTBOUND)); + // client.port names no tag on a client span + assertNull(SpanTags.get(tags, "client.port", DIRECTION_OUTBOUND)); + } + + @Test + void otherNamesReadAsWritten() { + TagMap tags = TagMap.create(); + tags.set("my.tag", "custom"); + tags.set(Tags.HTTP_METHOD, "GET"); + + assertEquals("custom", SpanTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); + assertEquals("GET", SpanTags.get(tags, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); + assertEquals("GET", SpanTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + } + + @Test + void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { + TagMap tags = TagMap.create(); + tags.set("server.address", "older"); + + long tagId = SpanTags.prepareAliasWrite(tags, "server.address", DIRECTION_OUTBOUND); + + assertEquals(KnownTags.PEER_HOSTNAME_ID, tagId); + assertNull(tags.getObject("server.address")); // the older pending value is dropped + } + + @TableTest({ + "scenario | name | direction", + "no direction yet | server.address | -1 ", + "no tag in the direction | client.port | 1 ", + "internal span | server.address | 2 ", + "not an alias | my.tag | 1 " + }) + void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction) { + TagMap tags = TagMap.create(); + tags.set(name, "pending"); + + assertEquals(0L, SpanTags.prepareAliasWrite(tags, name, direction)); + assertEquals("pending", tags.getObject(name)); + } + + @Test + void aTagWriteWithNoDirectionDropsEveryAliasThatCouldDenoteIt() { + TagMap tags = TagMap.create(); + tags.set("client.port", 1); + tags.set("server.port", 2); + + SpanTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); + + assertNull(tags.getObject("client.port")); + assertNull(tags.getObject("server.port")); + } + + @Test + void aTagWriteWithADirectionDropsOnlyItsAliasThere() { + TagMap tags = TagMap.create(); + tags.set("client.port", 1); // no tag on a client span: a custom tag, kept + tags.set("server.port", 2); + + SpanTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); + + assertEquals(1, tags.getObject("client.port")); + assertNull(tags.getObject("server.port")); + } + + @Test + void aTagWithoutAnAliasIsLeftAlone() { + TagMap tags = TagMap.create(); + tags.set("server.address", "pending"); + + SpanTags.prepareWrite(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); + SpanTags.prepareWrite(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); + + assertEquals("pending", tags.getObject("server.address")); + } + + @Test + void entriesThatDependOnDirectionAreRecognized() { + TagMap tags = TagMap.create(); + tags.set("server.address", "a"); + tags.set(Tags.PEER_HOSTNAME, "b"); + tags.set(Tags.HTTP_METHOD, "c"); + tags.set("my.tag", "d"); + + assertTrue(SpanTags.isDirectionDependent(tags.getEntry("server.address"))); + assertTrue(SpanTags.isDirectionDependent(tags.getEntry(Tags.PEER_HOSTNAME))); + assertFalse(SpanTags.isDirectionDependent(tags.getEntry(Tags.HTTP_METHOD))); + assertFalse(SpanTags.isDirectionDependent(tags.getEntry("my.tag"))); + } + + @Test + void removingAnAliasRemovesItsTag() { + TagMap tags = TagMap.create(); + tags.set(Tags.PEER_HOSTNAME, "stored"); + tags.set("server.address", "pending"); + + SpanTags.remove(tags, "server.address", DIRECTION_OUTBOUND); + + assertNull(tags.getObject(Tags.PEER_HOSTNAME)); + assertNull(tags.getObject("server.address")); + } + + @Test + void removingATagRemovesItsAlias() { + TagMap byName = TagMap.create(); + byName.set(Tags.HTTP_HOSTNAME, "stored"); + byName.set("server.address", "pending"); + SpanTags.remove(byName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); + assertNull(byName.getObject(Tags.HTTP_HOSTNAME)); + assertNull(byName.getObject("server.address")); + + TagMap byId = TagMap.create(); + byId.set(Tags.HTTP_HOSTNAME, "stored"); + byId.set("server.address", "pending"); + SpanTags.remove(byId, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); + assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); + assertNull(byId.getObject("server.address")); + } + + @Test + void removingWithNoDirectionRemovesOnlyTheName() { + TagMap tags = TagMap.create(); + tags.set(Tags.PEER_HOSTNAME, "stored"); + tags.set("server.address", "pending"); + tags.set("my.tag", "custom"); + + SpanTags.remove(tags, "server.address", DIRECTION_UNKNOWN); + SpanTags.remove(tags, "my.tag", DIRECTION_OUTBOUND); + SpanTags.remove(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); + + assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); + assertNull(tags.getObject("server.address")); + assertNull(tags.getObject("my.tag")); + } + + @TableTest({ + "scenario | direction | alias | tag ", + "client hostname | 1 | server.address | peer.hostname ", + "server hostname | 0 | server.address | http.hostname ", + "server client ip | 0 | network.peer.address | network.client.ip", + "client port | 1 | server.port | peer.port " + }) + void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String tag) { + TagMap tags = TagMap.create(); + tags.set(tag, "older"); + tags.set(alias, "newer"); + tags.set("my.tag", "custom"); + + SpanTags.resolveScopedNames(tags, direction); + + assertEquals("newer", tags.getObject(tag)); + assertNull(tags.getObject(alias)); + assertEquals("custom", tags.getObject("my.tag")); + } + + @Test + void resolvingLeavesAnAliasWithNoTagAsWritten() { + TagMap tags = TagMap.create(); + tags.set("client.port", 1234); + tags.set("server.address", "pending"); + + SpanTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); + assertEquals(1234, tags.getObject("client.port")); + assertEquals("pending", tags.getObject(Tags.PEER_HOSTNAME)); + + TagMap unknown = TagMap.create(); + unknown.set("server.address", "pending"); + SpanTags.resolveScopedNames(unknown, DIRECTION_UNKNOWN); + assertEquals("pending", unknown.getObject("server.address")); + } + + @Test + void resolvingWithNoAliasChangesNothing() { + TagMap tags = TagMap.create(); + tags.set(Tags.PEER_HOSTNAME, "stored"); + + SpanTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); + + assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); + assertEquals(1, tags.size()); + } +} From d3d6ee4eddf3d024283a750bf2f4e9a4ae394a1d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 15:10:09 -0400 Subject: [PATCH 29/35] Route alias-target writes only on spans holding a pending alias dc0672555a routed every write of a tag with a direction-scoped alias (peer.hostname, peer.port, ...) through interception: a second lock, and boxing for primitive writes. That's about 31 ns for setTag(peer.hostname). Each span now keeps a routing mask, routedTagFlags, which holds INTERCEPTED until an alias is stored as written; only then does it gain HAS_SCOPED_ALIAS. The id setters test it with the same single mask test as before (TagInterceptor.needsIntercept(tagId, flags)). So on a span that never held a pending alias, the common case, these writes take the plain store path, unboxed. The flag is sticky, set for any alias stored as written, so whenever a pending alias could exist the write is routed exactly as before. It's written under the tag lock and read without it: a setter that misses it is racing an unordered alias write. Adds SetTagBenchmark arms for the routed path and for a primitive peer.port write by id. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/SetTagBenchmark.java | 21 +++++- .../datadog/trace/core/DDSpanContext.java | 70 +++++++++++++++---- .../core/taginterceptor/TagInterceptor.java | 15 ++-- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java index 353bcd69c42..dfcbd2266d0 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java @@ -40,12 +40,15 @@ public class SetTagBenchmark { static final CoreTracer TRACER = CoreTracer.builder().build(); private DDSpan span; + private DDSpan spanWithPendingAlias; private TagMap tagMap; private long nonConstantTagId; @Setup public void setup() { span = (DDSpan) TRACER.startSpan("benchmark", "set.tag"); + spanWithPendingAlias = (DDSpan) TRACER.startSpan("benchmark", "set.tag"); + spanWithPendingAlias.setTag("server.address", "pending.example"); tagMap = TagMap.create(); nonConstantTagId = KnownTags.HTTP_ROUTE_ID; } @@ -53,6 +56,7 @@ public void setup() { @TearDown public void tearDown() { span.finish(); + spanWithPendingAlias.finish(); } @Benchmark @@ -90,12 +94,27 @@ public Object knownTag_byName() { return span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}"); } - /** A tag with a direction-scoped alias: the only names that pay for alias handling. */ + /** A tag with a direction-scoped alias, on a span that never held a pending alias. */ @Benchmark public Object aliasTargetTag_byName() { return span.setTag(KnownTags.PEER_HOSTNAME_NAME, "db.example"); } + /** + * The same write on a span that once held a pending alias (written before it had a kind), so the + * write takes the routed path that supersedes pending aliases. + */ + @Benchmark + public Object aliasTargetTag_withPendingAlias() { + return spanWithPendingAlias.setTag(KnownTags.PEER_HOSTNAME_NAME, "db.example"); + } + + /** A primitive write of a tag with an alias, by id: must stay unboxed on the plain path. */ + @Benchmark + public Object peerPort_byId() { + return span.setTag(KnownTags.PEER_PORT_ID, 5432); + } + @Benchmark public Object customTag_byName() { return span.setTag("my.custom.tag", "/users/{id}"); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 66c6a2f0646..33acd791e03 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -40,6 +40,7 @@ import datadog.trace.core.taginterceptor.TagInterceptor; import datadog.trace.core.tagprocessor.TagsPostProcessorFactory; import datadog.trace.util.TagsHelper; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.Closeable; import java.io.IOException; import java.util.Collections; @@ -146,6 +147,20 @@ public class DDSpanContext private volatile byte spanKindOrdinal = SPAN_KIND_UNSET; + /** + * The tag id flags the id setters route through {@link TagInterceptor}: {@link + * KnownTagCodec#INTERCEPTED}, plus {@link KnownTagCodec#HAS_SCOPED_ALIAS} once a direction-scoped + * alias has been stored as written (see {@link #notePendingAlias}). Until then -- almost always + * -- a write of a tag with an alias takes the plain store path: no second lock, no boxing. + * + *

Sticky: it is never cleared, so a span that once held a pending alias keeps routing those + * tags. Written under the {@code unsafeTags} lock but read without it, deliberately: a setter on + * another thread that has not yet seen the flag is racing the alias write, and the order of two + * unsynchronized writes is undefined anyway. The writing thread always sees its own update, so no + * value is lost relative to routing every such write. + */ + private int routedTagFlags = (int) KnownTagCodec.INTERCEPTED; + private CharSequence integrationName; private CharSequence serviceNameSource; @@ -1020,12 +1035,35 @@ public void setMetric(final TagMap.EntryReader entry) { private long writeKeyOf(String tag) { long key = KnownTagCodec.keyOrAliasOf(tag); if (key > 0) { - if (KnownTagCodec.hasScopedAlias(key)) { + if (KnownTagCodec.hasScopedAlias(key) && hasPendingAliases()) { SpanTags.prepareWrite(unsafeTags, key, tagDirection()); } return key; } - return key == 0 ? 0L : SpanTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); + if (key == 0) { + return 0L; + } + long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); + if (aliasedId == 0) { + notePendingAlias(); + } + return aliasedId; + } + + /** + * Notes that an alias is being stored as written, so later writes of a tag with an alias are + * routed to supersede it. Call while holding the {@code unsafeTags} lock. + */ + @SuppressFBWarnings( + value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", + justification = "A stale read only races an unordered alias write; see routedTagFlags") + private void notePendingAlias() { + routedTagFlags = (int) (KnownTagCodec.INTERCEPTED | KnownTagCodec.HAS_SCOPED_ALIAS); + } + + /** True once any alias has been stored as written; see {@link #routedTagFlags}. */ + private boolean hasPendingAliases() { + return (routedTagFlags & KnownTagCodec.HAS_SCOPED_ALIAS) != 0; } private void storeTag(String tag, Object value) { @@ -1060,7 +1098,7 @@ private void storeTag(String tag, double value) { private void storeEntry(TagMap.EntryReader entry) { long tagId = entry.tagId(); if (tagId != 0) { - if (KnownTagCodec.hasScopedAlias(tagId)) { + if (KnownTagCodec.hasScopedAlias(tagId) && hasPendingAliases()) { SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); } unsafeTags.set(entry); @@ -1069,6 +1107,8 @@ private void storeEntry(TagMap.EntryReader entry) { if (aliasedId != 0) { unsafeTags.set(aliasedId, entry.objectValue()); } else { + // An alias stored as written (or a filter false positive: then merely slower). + notePendingAlias(); unsafeTags.set(entry); } } else { @@ -1151,12 +1191,16 @@ public void setTag(final String tag, final String value) { /** * Removes the pending aliases a write of {@code tagId} supersedes. Called by {@link - * TagInterceptor} for a tag with {@link KnownTagCodec#HAS_SCOPED_ALIAS}, which it routes like an - * intercepted tag, so the id setters' fast path carries no alias handling at all. The tag itself - * is stored right after, under its own lock acquisition; a concurrent write of the alias from + * TagInterceptor} for a tag with {@link KnownTagCodec#HAS_SCOPED_ALIAS}, which the id setters + * route like an intercepted tag only once the span holds a pending alias ({@link + * #routedTagFlags}), so their fast path carries no alias handling at all. The tag itself is + * stored right after, under its own lock acquisition; a concurrent write of the alias from * another thread in between has no defined order anyway. */ public void supersedeAliases(long tagId) { + if (!hasPendingAliases()) { + return; // entry paths route on the constant mask; nothing to supersede + } synchronized (unsafeTags) { SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); } @@ -1173,7 +1217,7 @@ public void setTag(final long tagId, final Object value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId) + } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1187,7 +1231,7 @@ public void setTag(final long tagId, final CharSequence value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId) + } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { unsafeTags.set(tagId, value); @@ -1199,7 +1243,7 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1212,7 +1256,7 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1225,7 +1269,7 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1238,7 +1282,7 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1251,7 +1295,7 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index fa58ee35745..5bef9861e47 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -174,10 +174,17 @@ public boolean needsIntercept(Map map) { * time. {@code tagId} must be a known id. */ public boolean needsIntercept(long tagId) { - // One mask test for both bits: a tag with a direction-scoped alias is routed here too, so the - // span can supersede a pending alias without adding code to its store fast path. - return (tagId & (KnownTagCodec.INTERCEPTED | KnownTagCodec.HAS_SCOPED_ALIAS)) != 0L - || isSplitServiceTag(tagId); + return needsIntercept( + tagId, (int) (KnownTagCodec.INTERCEPTED | KnownTagCodec.HAS_SCOPED_ALIAS)); + } + + /** + * {@link #needsIntercept(long)} for a span that routes only the id flags in {@code routedFlags}: + * a span adds {@link KnownTagCodec#HAS_SCOPED_ALIAS} only once it holds a pending alias. One mask + * test, as for the constant mask. + */ + public boolean needsIntercept(long tagId, int routedFlags) { + return ((int) tagId & routedFlags) != 0 || isSplitServiceTag(tagId); } public boolean needsIntercept(String tag) { From 85c222daded61ddea472811bad7b7f41d7dd5776 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 15:40:25 -0400 Subject: [PATCH 30/35] Cover the remaining TagMap.Internals setKnown overloads jacocoTestCoverageVerification flagged TagMap.Internals at 0.6: the existing test exercised the CharSequence, int, long and boolean setters; this adds the Object, float and double ones. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/api/TagMapSetByIdTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 11e77d8eaed..4634a02da58 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -91,4 +91,21 @@ void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { assertEquals(KnownTags.HTTP_ROUTE_ID, trusted.getEntry(KnownTags.HTTP_ROUTE_NAME).tagId()); } + @Test + void theCoresTrustedSetsCoverTheRemainingValueShapes() { + Object component = new StringBuilder("jdbc"); // not a String: exercises the Object overload + TagMap checked = TagMap.create(); + checked.set(KnownTags.COMPONENT_ID, component); + checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); + + TagMap trusted = TagMap.create(); + trusted.internals().setKnown(KnownTags.COMPONENT_ID, component); + trusted.internals().setKnown(KnownTags.DB_POOL_NAME_ID, 1.5f); + trusted.internals().setKnown(KnownTags.DB_INSTANCE_ID, 2.25d); + + assertEquals(new HashMap<>(checked), new HashMap<>(trusted)); + assertEquals(1.5f, trusted.getEntry(KnownTags.DB_POOL_NAME_ID).floatValue()); + assertEquals(2.25d, trusted.getEntry(KnownTags.DB_INSTANCE_ID).doubleValue()); + } } From f08c1c361076264e3963180f115cbcb20ca26e99 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 15:53:17 -0400 Subject: [PATCH 31/35] Add datadog.apicontract to CODEOWNERS Restricted lives in the new datadog.apicontract package, which had no owner. Give it the same owner as its sibling datadog.perfcontract. Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4cc52bad3c8..3006ae3a6d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -88,6 +88,7 @@ /internal-api/src/*/*/datadog/trace/api/endpoint/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/env/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/function/ @DataDog/apm-java +/internal-api/src/*/*/datadog/apicontract/ @DataDog/apm-java /internal-api/src/*/*/datadog/perfcontract/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/intake/ @DataDog/apm-java /internal-api/src/*/*/datadog/trace/api/logging/ @DataDog/apm-java From 22d0fd135818e1163441c4fbd68ef9723c105220 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 16:34:44 -0400 Subject: [PATCH 32/35] Give peer.port one direction-neutral id peer.port was modeled as one tag per direction: PEER_PORT_INBOUND_ID and PEER_PORT_OUTBOUND_ID, sharing the Datadog name and marked with a SHARED_NAME id bit. Two ids under one name kept producing lookup and supersede bugs: keyOf("peer.port") resolved to neither id, so a value set by name and one set by id landed in different entries, and tombstones and getAndSet by name could not find what an id had stored. The generator now merges a name's per-direction declarations into one tag with one id (PEER_PORT_ID), and only its OpenTelemetry name depends on the span's direction: openTelemetryNameOf(id, direction) returns client.port inbound and server.port outbound. The SHARED_NAME bit, hasSharedName and isKeyableById go away (bit 0 is left unused), and keyOf("peer.port") returns PEER_PORT_ID. KnownTagCodec gains the DIRECTION_* constants and openTelemetryTagOf(id, direction). Direction-scoped OpenTelemetry names are still not resolved when written by name. This is the direction-neutral-id part of #12731's redesign; its direction-scoped aliases and read-time resolution are left to that PR. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 90 +++++++-------- .../buildlogic/tagRegistry/TagConventions.kt | 6 +- .../buildlogic/tagRegistry/TagRegistry.kt | 106 ++++++++++++------ .../tagRegistry/TagRegistryGenerator.kt | 21 ++-- .../TagRegistryGeneratorPluginTest.kt | 6 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 75 +++++++++---- .../datadog/trace/core/DDSpanContext.java | 19 ++-- .../core/taginterceptor/TagInterceptor.java | 3 +- .../trace/core/DDSpanSetTagByIdTest.java | 11 +- .../taginterceptor/TagInterceptorTest.java | 5 +- .../java/datadog/trace/api/KnownTagCodec.java | 77 ++++++++----- .../main/java/datadog/trace/api/TagMap.java | 7 +- .../java/datadog/trace/api/KnownTagsTest.java | 30 ++--- .../datadog/trace/api/TagMapSetByIdTest.java | 10 -- .../datadog/trace/api/TagMapTagHashTest.java | 2 +- tag-conventions.yaml | 19 ++-- 16 files changed, 272 insertions(+), 215 deletions(-) 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 f409bc18bdc..a68d39d6701 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 @@ -40,35 +40,25 @@ object KnownTagsEmitter { return u } - val nameOfConst = HashMap() - val idOfConst = HashMap() - val serialOfConst = HashMap() - val otelNameOfConst = HashMap() + val nameOfConst = HashMap() + val idOfConst = HashMap() + val serialOfConst = HashMap() + val otelNameOfConst = HashMap() for (t in reg.tags) { val base = sanitize(t.name) - // Shared Datadog names get no NAME constant because they identify several tags. - // Callers use a direction-specific ID (e.g. PEER_PORT_OUTBOUND_ID); nameOf emits the - // shared Datadog name as a literal. - if (t.sharedNameDirection == null) nameOfConst[t.identity] = unique(withSuffix(base, "_NAME")) - idOfConst[t.identity] = unique(withSuffix(base, "_ID")) - serialOfConst[t.identity] = unique(withSuffix(base, "_SERIAL_NUM")) + nameOfConst[t.ddName] = unique(withSuffix(base, "_NAME")) + idOfConst[t.ddName] = unique(withSuffix(base, "_ID")) + serialOfConst[t.ddName] = unique(withSuffix(base, "_SERIAL_NUM")) // Suffix the pre-suffix base (not nameC), same as the other three: suffixing an // already-suffixed identifier would produce a redundant compound like NAME_OTEL_NAME. - if (t.otelName != null) otelNameOfConst[t.identity] = unique(withSuffix(base, "_OTEL_NAME")) + if (t.otelName != null) otelNameOfConst[t.ddName] = unique(withSuffix(base, "_OTEL_NAME")) } - fun nameC(identity: TagConventions.TagIdentity) = nameOfConst.getValue(identity) - - // The expression nameOf returns: the NAME constant, or the shared name's literal. - fun nameExpr(t: TagRegistry.Tag) = nameOfConst[t.identity] ?: "\"${escape(t.ddName)}\"" - fun idC(identity: TagConventions.TagIdentity) = idOfConst.getValue(identity) - fun serialC(identity: TagConventions.TagIdentity) = serialOfConst.getValue(identity) - fun otelNameC(identity: TagConventions.TagIdentity) = otelNameOfConst.getValue(identity) - - val order = reg.tags.map { it.identity } // stable emit order - // keyOf has no direction argument, so omit shared names and let them resolve to 0 (unknown). - val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null }.map { it.identity } - // canonical name -> OpenTelemetry name, for the reverse (openTelemetryNameOf) switch. - val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.identity to it } }.toMap() + fun nameC(ddName: String) = nameOfConst.getValue(ddName) + fun idC(ddName: String) = idOfConst.getValue(ddName) + fun serialC(ddName: String) = serialOfConst.getValue(ddName) + fun otelNameC(ddName: String) = otelNameOfConst.getValue(ddName) + + val keyOfOrder = reg.tags.map { it.ddName } // stable emit order return buildString { // Public API first (name + encoded id couplets), so readers see the useful parts up top; the // serial ids and keyOf/resolver machinery follow below. Derivation is in the trailing comment. @@ -87,24 +77,16 @@ object KnownTagsEmitter { ) for (t in reg.tags) { - val direction = t.sharedNameDirection - if (direction == null) { - appendLine(" public static final String ${nameC(t.identity)} = \"${escape(t.ddName)}\";") - } else { - appendLine( - " /** {@code ${escape(t.ddName)}} on ${direction.yamlKey} spans. This ID identifies the direction; " + - "the Datadog name is shared across directions. */" - ) - } - appendLine(" public static final long ${idC(t.identity)} = ${hex(t.id)};") + appendLine(" public static final String ${nameC(t.ddName)} = \"${escape(t.ddName)}\";") + appendLine(" public static final long ${idC(t.ddName)} = ${hex(t.id)};") if (t.otelName != null) { - appendLine(" public static final String ${otelNameC(t.identity)} = \"${escape(t.otelName)}\";") + appendLine(" public static final String ${otelNameC(t.ddName)} = \"${escape(t.otelName)}\";") } 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)}") + for ((direction, otel) in t.scopedOtelNames) append(" -> ${escape(otel)} (${direction.yamlKey})") appendLine(" <${escape(t.required)}>") appendLine() } @@ -113,7 +95,7 @@ object KnownTagsEmitter { // this package (the resolver here, TagInterceptor in core) can name them as case labels. appendLine(" // ---- serial numbers ----") for (t in reg.tags) { - appendLine(" public static final int ${serialC(t.identity)} = ${t.serial};") + appendLine(" public static final int ${serialC(t.ddName)} = ${t.serial};") } // Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag. val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1 @@ -123,7 +105,7 @@ object KnownTagsEmitter { // canonical names. Sort by OTel name to keep output deterministic. val otelByCanonical = reg.tags - .mapNotNull { t -> t.otelName?.let { it to t.identity } } + .mapNotNull { t -> t.otelName?.let { it to t.ddName } } .sortedBy { it.first } // keyOf table (open-addressed, via StringIndex.EmbeddingSupport). Canonical names first, then @@ -179,7 +161,7 @@ object KnownTagsEmitter { """.trimIndent() ) for (t in reg.tags.sortedBy { it.serial }) { - appendLine(" ${nameExpr(t)},") + appendLine(" ${nameC(t.ddName)},") } appendLine( """ @@ -198,21 +180,29 @@ object KnownTagsEmitter { 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. + // openTelemetryNameOf: id -> OTel-namespace name on spans of a direction, null when + // the tag has none there. The caller (a serializer) owns any fall-back-to-Datadog-name + // policy. @Override - public String openTelemetryNameOf(long tagId) { + public String openTelemetryNameOf(long tagId, int direction) { switch (KnownTagCodec.serialNum(tagId)) { """.trimIndent() ) - for (identity in order) { - if (otelName[identity] == null) continue - appendLine( - """ - case ${serialC(identity)}: - return ${otelNameC(identity)}; - """.trimIndent() - ) + for (t in reg.tags) { + val result = + if (t.otelName != null) { + otelNameC(t.ddName) + } else if (t.scopedOtelNames.isNotEmpty()) { + // Renamed only on spans of some directions: elsewhere, and while the direction is + // unknown, the tag keeps its Datadog name. + t.scopedOtelNames.entries.reversed().fold("null") { rest, (direction, otel) -> + "direction == KnownTagCodec.DIRECTION_${direction.name} ? \"${escape(otel)}\" : $rest" + } + } else { + continue + } + appendLine("case ${serialC(t.ddName)}:") + appendLine(" return $result;") } appendLine( """ diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt index 7b1239fbc40..27ca3cfba0f 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt @@ -61,13 +61,9 @@ class TagConventions private constructor( val name: String get() = identity.label - /** The Datadog-namespace name, shared by the tags of a name declared per direction. */ + /** The Datadog-namespace name, shared by the declarations of a name declared per direction. */ val ddName: String get() = identity.ddName - - /** The direction of a tag declared per direction, or null for every other tag. */ - val sharedNameDirection: Direction? - get() = identity.direction } /** 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 983eaad5ba4..70b3f7d7d20 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 @@ -13,75 +13,73 @@ package datadog.buildlogic.tagRegistry * 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. + * + *

There is one tag, and one id, per Datadog name. A name the conventions declare once per + * direction, as `peer.port` is, is still one tag: only its OpenTelemetry name depends on the span's + * direction, so direction is applied when a tag is named, never when it is stored. */ class TagRegistry private constructor(val tags: List) { data class Tag( - val identity: TagConventions.TagIdentity, + val ddName: String, val type: String, 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, - /** - * The one direction [declaredOtelName] applies in, or null when it applies in every direction. A - * tag has one declaration, so its rename covers either every direction or exactly one. - */ - val otelDirection: TagConventions.Direction? = null, + /** 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 identity's label, as reports and constant names show it. */ - val name: String = identity.label - val ddName: String = identity.ddName - val sharedNameDirection: TagConventions.Direction? = identity.direction - - /** - * The rename included in the direction-free lookup tables, or null when absent or scoped - * to one direction. Scoped renames remain in `declaredOtelName` for later resolution. - */ - val otelName: String? = declaredOtelName.takeIf { otelDirection == null } + /** The tag as reports and generated constant names show it. */ + val name: String + get() = ddName } 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() + 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. + * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, other low bits and the reserved [47-32] window + * zero. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, sharedName: Boolean = false): Long { + fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): 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 } fun build(conv: TagConventions): TagRegistry { - val traceLevel = conv.traceLevelTags().map { it.identity }.toSet() - val renames = conv.otelMappings().associateBy { it.tag } + val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet() + val renames = conv.otelMappings().groupBy { it.tag.ddName } - // Stable order (by name) so serials -- and therefore ids -- are a pure function of the input. + // 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. val tags = - conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t -> + conv.allDeclaredTags().groupBy { it.ddName }.toSortedMap().values.mapIndexed { i, decls -> + val first = decls.first() + require(decls.all { it.type == first.type }) { + "'${first.ddName}' is declared per direction with different types: ${decls.map { it.type }}" + } val serial = FIRST_SERIAL + i - val isTraceLevel = t.identity in traceLevel - val isIntercepted = t.ddName in conv.interceptedNames + val isTraceLevel = first.ddName in traceLevel + val isIntercepted = first.ddName in conv.interceptedNames + val (otelName, scoped) = otelNamesOf(first.ddName, renames[first.ddName].orEmpty()) Tag( - t.identity, - t.type, - t.required, + first.ddName, + first.type, + strongestRequirement(decls.map { it.required }), serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null), - declaredOtelName = renames[t.identity]?.otelName, - otelDirection = renames[t.identity]?.direction, + id = encode(serial, isTraceLevel, isIntercepted), + otelName = otelName, + scopedOtelNames = scoped, ) } @@ -89,6 +87,39 @@ class TagRegistry private constructor(val tags: List) { return TagRegistry(tags) } + /** + * 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. + */ + private fun otelNamesOf( + ddName: String, + mappings: List, + ): Pair> { + val everywhere = mappings.filter { (it.direction ?: it.tag.direction) == null }.map { it.otelName }.distinct() + require(everywhere.size <= 1) { "'$ddName' has several direction-free OpenTelemetry names: $everywhere" } + if (everywhere.isNotEmpty()) { + require(mappings.all { it.otelName == everywhere[0] }) { + "'$ddName' has a direction-free OpenTelemetry name and a different one per direction" + } + return everywhere[0] to emptyMap() + } + val scoped = sortedMapOf() + for (m in mappings) { + val direction = m.direction ?: m.tag.direction!! + val prev = scoped.put(direction, m.otelName) + require(prev == null || prev == m.otelName) { + "'$ddName' has two OpenTelemetry names on ${direction.yamlKey} spans: '$prev' and '${m.otelName}'" + } + } + return null to scoped + } + + private val REQUIREMENT_STRENGTH = listOf("required", "conditional", "recommended", "optional") + + /** The strongest of a name's requirement levels across its declarations per direction. */ + private fun strongestRequirement(levels: List): String = + levels.minBy { REQUIREMENT_STRENGTH.indexOf(it).let { i -> if (i < 0) Int.MAX_VALUE else i } } + /** * Rejects OpenTelemetry names that collide with any Datadog name. Within each direction, * a rename must also belong to a single tag. Direction-free renames reserve their name @@ -99,8 +130,9 @@ class TagRegistry private constructor(val tags: List) { val canonical = tags.map { it.ddName }.toSet() val owner = HashMap, String>() for (t in tags) { - val otel = t.declaredOtelName ?: continue - for (direction in t.otelDirection?.let { listOf(it) } ?: TagConventions.Direction.entries) { + val byDirection = + t.otelName?.let { otel -> TagConventions.Direction.entries.associateWith { otel } } ?: t.scopedOtelNames + for ((direction, otel) in byDirection) { require(otel !in canonical) { "OpenTelemetry name '$otel' (of '${t.name}') collides with canonical tag name '$otel'" } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index 50960175cc0..c4e54eec0c8 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -102,23 +102,16 @@ object TagRegistryGenerator { """ # DIRECTION-SCOPED OPENTELEMETRY NAMES. Each applies only on spans of the given direction, so - # it is not in the tables above; name resolution does not use it until it knows the direction. + # it is not in the tables above: a tag is emitted under it when its span has that direction, + # but name resolution does not use it until it knows the direction. """.trimIndent() ) val scoped = - reg.tags.filter { it.otelDirection != null }.sortedWith(compareBy({ it.declaredOtelName }, { it.otelDirection })) - for (t in scoped) { - appendLine(" %-30s %-9s -> %s".format(Locale.ROOT, t.declaredOtelName, t.otelDirection!!.yamlKey, t.name)) - } - appendLine( - """ - - # SHARED DATADOG NAMES. One Datadog name for a tag per direction: emitting it needs no context, - # but resolving the name to a tag needs the span's direction, so keyOf does not resolve it yet. - """.trimIndent() - ) - for ((ddName, shared) in reg.tags.filter { it.sharedNameDirection != null }.groupBy { it.ddName }.toSortedMap()) { - appendLine(" %-30s -> %s".format(Locale.ROOT, ddName, shared.joinToString(", ") { it.name })) + reg.tags + .flatMap { t -> t.scopedOtelNames.map { (direction, otel) -> Triple(otel, direction, t.name) } } + .sortedWith(compareBy({ it.first }, { it.second })) + for ((otel, direction, canonical) in scoped) { + appendLine(" %-30s %-9s -> %s".format(Locale.ROOT, otel, direction.yamlKey, canonical)) } } } diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt index fa08bc5b177..3b676ad52e4 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt @@ -247,9 +247,13 @@ class TagRegistryGeneratorPluginTest { public final class KnownTagCodec { public interface Resolver { String nameOf(long id); - String openTelemetryNameOf(long id); + String openTelemetryNameOf(long id, int direction); long keyOf(String name); } + public static final int DIRECTION_UNKNOWN = -1; + public static final int DIRECTION_INBOUND = 0; + public static final int DIRECTION_OUTBOUND = 1; + public static final int DIRECTION_NONE = 2; public static int serialNum(long id) { return (int) (id >>> 48); } } """ diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index 16b899d19ab..a88f7509144 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -293,7 +293,10 @@ class TagRegistryGeneratorTest { TagRegistryGenerator.generate(yaml, output) val generated = contents(output) - assertThat(generated.getValue("java/datadog/trace/api/KnownTags.java")).doesNotContain("server.address") + val source = generated.getValue("java/datadog/trace/api/KnownTags.java") + assertThat(source).doesNotContain("HTTP_HOSTNAME_OTEL_NAME") + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).doesNotContain("server.address") + assertThat(source).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") assertThat(generated.getValue("tag-assignment.txt")) .containsPattern("server\\.address +inbound +-> http\\.hostname") } @@ -324,23 +327,21 @@ class TagRegistryGeneratorTest { val tags = TagRegistry.build(conventions).tags.associateBy { it.name } - assertThat(tags.getValue("http.hostname").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.address" to TagConventions.Direction.INBOUND) - assertThat(tags.getValue("peer.hostname").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.address" to TagConventions.Direction.OUTBOUND) - assertThat(tags.getValue("peer.port@outbound").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("server.port" to TagConventions.Direction.OUTBOUND) - assertThat(tags.getValue("peer.port@inbound").let { it.declaredOtelName to it.otelDirection }) - .isEqualTo("client.port" to TagConventions.Direction.INBOUND) - assertThat(tags.values.filter { it.name.startsWith("peer.port") }.map { it.ddName }) - .containsOnly("peer.port") + assertThat(tags.getValue("http.hostname").scopedOtelNames) + .isEqualTo(mapOf(TagConventions.Direction.INBOUND to "server.address")) + assertThat(tags.getValue("peer.hostname").scopedOtelNames) + .isEqualTo(mapOf(TagConventions.Direction.OUTBOUND to "server.address")) + assertThat(tags.getValue("peer.port").scopedOtelNames) + .isEqualTo( + mapOf(TagConventions.Direction.INBOUND to "client.port", TagConventions.Direction.OUTBOUND to "server.port") + ) assertThat(tags.values.map { it.otelName }).containsOnlyNulls() assertThat(conventions.resolve("http.client").map { it.name }).contains("peer.port@outbound") assertThat(conventions.resolve("http.server").map { it.name }).contains("peer.port@inbound") } @Test - fun `a Datadog name declared per direction is not resolvable without a direction`() { + fun `a Datadog name declared per direction is one tag`() { val yaml = directory.conventionsFile( """ span_types: @@ -358,12 +359,43 @@ class TagRegistryGeneratorTest { val generated = contents(output) val source = generated.getValue("java/datadog/trace/api/KnownTags.java") assertThat(source) - .contains("PEER_PORT_INBOUND_ID", "PEER_PORT_OUTBOUND_ID") - .contains("{@code peer.port} on outbound spans.", "{@code peer.port} on inbound spans.") - .doesNotContain("PEER_PORT_NAME", "PEER_PORT_INBOUND_NAME", "PEER_PORT_OUTBOUND_NAME") - assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).doesNotContain("PEER_PORT") - assertThat(generated.getValue("tag-assignment.txt")) - .containsPattern("peer\\.port +-> peer\\.port@inbound, peer\\.port@outbound") + .contains("PEER_PORT_NAME = \"peer.port\"", "PEER_PORT_ID") + .doesNotContain("PEER_PORT_INBOUND", "PEER_PORT_OUTBOUND", "shared") + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")).contains("PEER_PORT_NAME,") + assertThat(source.substringAfter("KEYOF_VALUES = {").substringBefore("};")).contains("PEER_PORT_ID,") + assertThat(generated.getValue("tag-assignment.txt").lines().filter { it.endsWith(" peer.port") }) + .hasSize(1) + } + + @Test + fun `a tag declared per direction is emitted under the OpenTelemetry name for the direction`() { + val yaml = directory.conventionsFile( + """ + span_types: + http.client: {span-kind: client, include: [outbound_peer]} + http.server: + span-kind: server + include: [inbound_peer] + tags: [{dd-name: http.hostname, otel-name: server.address}] + mixins: + outbound_peer: {span-kind: client, tags: [{dd-name: peer.port, type: int, otel-name: server.port}]} + inbound_peer: {span-kind: server, tags: [{dd-name: peer.port, type: int, otel-name: client.port}]} + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + val emit = source.substringAfter("public String openTelemetryNameOf(long tagId, int direction)") + assertThat(emit).contains( + "return direction == KnownTagCodec.DIRECTION_INBOUND ? \"client.port\" : " + + "direction == KnownTagCodec.DIRECTION_OUTBOUND ? \"server.port\" : null;" + ) + assertThat(emit).contains("return direction == KnownTagCodec.DIRECTION_INBOUND ? \"server.address\" : null;") + // Direction-scoped names are not resolved by name yet. + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")) + .doesNotContain("client.port", "server.port", "server.address") } @Test @@ -396,7 +428,7 @@ class TagRegistryGeneratorTest { val dbType = TagRegistry.build(conventions).tags.single() assertThat(dbType.otelName).isEqualTo("db.system") - assertThat(dbType.otelDirection).isNull() + assertThat(dbType.scopedOtelNames).isEmpty() } @TableTest( @@ -437,12 +469,13 @@ class TagRegistryGeneratorTest { """ ) - assertThat(TagRegistry.build(conventions).tags.map { it.identity }) + assertThat(conventions.allDeclaredTags().map { it.identity }) .containsExactlyInAnyOrder( TagConventions.TagIdentity("x", TagConventions.Direction.INBOUND), TagConventions.TagIdentity("x", TagConventions.Direction.OUTBOUND), TagConventions.TagIdentity("x@inbound"), ) + assertThat(TagRegistry.build(conventions).tags.map { it.ddName }).containsExactly("x", "x@inbound") } @Test @@ -460,7 +493,7 @@ class TagRegistryGeneratorTest { ) assertThat(TagRegistry.build(conventions).tags.map { it.name }) - .containsExactlyInAnyOrder("peer.port@inbound", "peer.port@outbound", "user@domain") + .containsExactlyInAnyOrder("peer.port", "user@domain") } @Test diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index e0c15e91cdf..47ccc0d1abc 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1007,7 +1007,7 @@ public void removeTag(String tag) { * @param tagId a {@code KnownTags.*_ID} constant */ public void removeTag(long tagId) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagId == KnownTags.SPAN_KIND_ID) { @@ -1065,11 +1065,10 @@ public void setTag(final String tag, final String value) { /* * Id-keyed setters, mirroring the String setters above. With a constant id, the * KnownTagCodec.INTERCEPTED test folds, leaving only the split-by-tags table load; an id set never - * takes the custom-tag path. An id that names no known tag, or whose name is shared across - * directions (see KnownTagCodec#isKeyableById), is ignored, like a null tag. + * takes the custom-tag path. An id that names no known tag is ignored, like a null tag. */ public void setTag(final long tagId, final Object value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { @@ -1083,7 +1082,7 @@ public void setTag(final long tagId, final Object value) { } public void setTag(final long tagId, final CharSequence value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (null == value) { @@ -1097,7 +1096,7 @@ public void setTag(final long tagId, final CharSequence value) { } public void setTag(final long tagId, final boolean value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1110,7 +1109,7 @@ public void setTag(final long tagId, final boolean value) { } public void setTag(final long tagId, final int value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1123,7 +1122,7 @@ public void setTag(final long tagId, final int value) { } public void setTag(final long tagId, final long value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1136,7 +1135,7 @@ public void setTag(final long tagId, final long value) { } public void setTag(final long tagId, final float value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { @@ -1149,7 +1148,7 @@ public void setTag(final long tagId, final float value) { } public void setTag(final long tagId, final double value) { - if (!KnownTagCodec.isKeyableById(tagId)) { + if (!KnownTagCodec.isKnown(tagId)) { return; } if (tagInterceptor.needsIntercept(tagId)) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 82a6cb16c06..d65866c385b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -95,8 +95,7 @@ public TagInterceptor( /** * Marks, by serial, each known tag that {@code names} names under any of its names, so a tag set - * by id or by its OpenTelemetry name matches too. A name declared per direction marks each - * direction's tag. + * by id or by its OpenTelemetry name matches too. */ private static boolean[] knownTagSerials(Set names) { boolean[] serials = new boolean[KnownTagCodec.serialLimit()]; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java index 34fc1bc2028..a07f15bd87b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSetTagByIdTest.java @@ -62,13 +62,16 @@ void clearingAnUnknownIdIsIgnored() { } @Test - void aSharedNameIdIsIgnoredAndLeavesTheNamedTag() { + void anIdReplacesAValueSetUnderTheName() { span.setTag("peer.port", 8080); - span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); - span.setTag(KnownTags.PEER_PORT_INBOUND_ID, (Object) null); + span.setTag(KnownTags.PEER_PORT_ID, 443); - assertEquals(8080, span.getTag("peer.port")); + assertEquals(443, span.getTag("peer.port")); + + span.setTag(KnownTags.PEER_PORT_ID, (Object) null); + + assertNull(span.getTag("peer.port")); } @Test diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 1b1a925b856..815fc53f2db 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -974,11 +974,10 @@ void splitByTagsMatchesACustomTagByName() { } @Test - void splitByTagsOnANameDeclaredPerDirectionMatchesEachDirection() { + void splitByTagsOnANameDeclaredPerDirectionMatchesItsOneTag() { TagInterceptor interceptor = splittingInterceptor(Tags.PEER_PORT); - assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_INBOUND_ID)); - assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_ID)); assertTrue(interceptor.needsIntercept(Tags.PEER_PORT)); } diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 314fe6d67dc..ad15e5a0bfa 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -15,9 +15,12 @@ public final class KnownTagCodec { * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] * are RESERVED and always zero here: they are the window the dense tag store uses for its * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is - * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is the - * SHARED_NAME bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for - * them. + * the trace/span LEVEL bit (set ⟹ trace-level), and bit 1 is the INTERCEPTED bit. Unknown + * (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for them. + * + *

There is one id per Datadog name. A tag the conventions declare once per direction, such as + * {@code peer.port}, is still one tag: only its OpenTelemetry name depends on the span's + * direction, so direction applies when a tag is named, never when it is stored. * *

The INTERCEPTED bit marks a tag TagInterceptor may route on the set-path, to a span field or * a sampling directive instead of tag storage. It is a hint, not a decision: the interceptor's @@ -59,27 +62,6 @@ public static boolean isIntercepted(long tagId) { return (tagId & INTERCEPTED) != 0L; } - /** - * SHARED_NAME bit (low-32 carve, bit 0): marks one direction of a tag declared once per direction - * under a shared Datadog name, such as {@code peer.port}. {@link #keyOf} resolves that name to - * neither id, so the name alone cannot find an entry stored under one of them. - */ - public static final long SHARED_NAME = 1L << 0; - - /** True if the tagId is one direction of a tag whose Datadog name is shared across directions. */ - public static boolean hasSharedName(long tagId) { - return (tagId & SHARED_NAME) != 0L; - } - - /** - * True if {@code tagId} can key a {@link TagMap} entry: a known tag whose name resolves back to - * it. A {@link #hasSharedName shared-name} id cannot, until name resolution knows the span's - * direction. Like {@link #isKnown}, this folds away for a constant id. - */ - public static boolean isKeyableById(long tagId) { - return isKnown(tagId) && !hasSharedName(tagId); - } - /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; @@ -95,12 +77,36 @@ public static long makeTagId(int serialNum) { return (long) serialNum << 48; } + /* + * A span's direction, which names the few tags whose OpenTelemetry name depends on it: peer.port + * is client.port on inbound spans and server.port on outbound ones. A tag is stored the same way + * in every direction; direction applies only when naming it. Pass one of these to + * openTelemetryTagOf(long, int). + */ + + /** Direction not known, for example a span with no kind: only names that need none apply. */ + public static final int DIRECTION_UNKNOWN = -1; + + /** Server and consumer spans, which receive a request or message. */ + public static final int DIRECTION_INBOUND = 0; + + /** Client and producer spans, which send a request or message. */ + public static final int DIRECTION_OUTBOUND = 1; + + /** Internal spans, which have no other end. */ + public static final int DIRECTION_NONE = 2; + + /** The registry's tables. Generated as {@code KnownTags.RESOLVER}; this class owns the policy. */ public interface Resolver { /** The tag's Datadog-namespace (canonical) name. */ String nameOf(long tagId); - /** The tag's OpenTelemetry-namespace name, or {@code null} when it declares none. */ - String openTelemetryNameOf(long tagId); + /** + * The tag's OpenTelemetry-namespace name on spans of {@code direction}, or {@code null} when it + * declares none there. {@link #DIRECTION_UNKNOWN} returns only a name that holds in every + * direction. + */ + String openTelemetryNameOf(long tagId, int direction); /** The id for {@code name} in ANY namespace (many→one), or 0 when it is not a known tag. */ long keyOf(String name); @@ -131,7 +137,7 @@ public static String datadogNameOf(long tagId) { * #openTelemetryTagOf} instead. */ public static String openTelemetryNameOf(long tagId) { - return Installed.RESOLVER.openTelemetryNameOf(tagId); + return Installed.RESOLVER.openTelemetryNameOf(tagId, DIRECTION_UNKNOWN); } /** @@ -143,10 +149,25 @@ public static String openTelemetryNameOf(long tagId) { *

This is the one place the pass-through policy lives, so no serializer re-decides it. Pair it * with {@link #datadogNameOf} for the same tag under the Datadog namespace; outbound naming is * per-namespace, never normalized to one of them. + * + *

Needs no direction for nearly every tag. The exception is a tag renamed in some directions + * only ({@code http.hostname} is {@code server.address} on inbound spans, {@code peer.port} is + * {@code client.port} inbound and {@code server.port} outbound): without a direction it passes + * through under its Datadog name. A caller naming a span's tags uses {@link + * #openTelemetryTagOf(long, int)}. */ public static String openTelemetryTagOf(long tagId) { + return openTelemetryTagOf(tagId, DIRECTION_UNKNOWN); + } + + /** + * {@link #openTelemetryTagOf(long)} on spans of {@code direction}, which also applies a tag's + * rename that holds only in that direction: {@code http.hostname} is {@code server.address} on + * inbound spans, but stays {@code http.hostname} on outbound ones. + */ + public static String openTelemetryTagOf(long tagId, int direction) { Resolver resolver = Installed.RESOLVER; - String otelName = resolver.openTelemetryNameOf(tagId); + String otelName = resolver.openTelemetryNameOf(tagId, direction); return otelName != null ? otelName : resolver.nameOf(tagId); } diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 0dbef18896b..088a6a281fa 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -472,11 +472,6 @@ private static String requireKnownName(long tagId) { if (name == null) { throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); } - if (KnownTagCodec.hasSharedName(tagId)) { - // Name-keyed access hashes the shared name as a custom tag, so it could never find an entry - // keyed by this id; set the tag by name until resolution knows the span's direction. - throw new IllegalArgumentException("tag id has a shared name, set it by name: " + name); - } return name; } @@ -1642,7 +1637,7 @@ private Internals(TagMap map) { /* * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has - * already checked KnownTagCodec.isKeyableById(tagId). + * already checked KnownTagCodec.isKnown(tagId). */ public void setKnown(long tagId, @Nonnull Object value) { this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index 08570b034fa..c12f7b446a7 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -160,20 +159,25 @@ void unknownNamesResolveToZero() { } /** - * {@code peer.port} means the server's port on outbound spans and the client's on inbound ones, - * so it is one tag per direction. Both share the Datadog name, which therefore resolves to no tag - * until name resolution knows the span's direction. + * {@code peer.port} is declared once per direction, but it is one tag with one id: only its + * OpenTelemetry name depends on the span's direction ({@code client.port} inbound, {@code + * server.port} outbound). */ @Test - void aNameDeclaredPerDirectionIsOneTagPerDirection() { - assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_INBOUND_ID)); - assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertNotEquals(KnownTags.PEER_PORT_INBOUND_ID, KnownTags.PEER_PORT_OUTBOUND_ID); - assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_INBOUND_ID)); - assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertFalse(KnownTagCodec.isKeyableById(KnownTags.PEER_PORT_OUTBOUND_ID)); - assertTrue(KnownTagCodec.isKeyableById(KnownTags.PEER_HOSTNAME_ID)); - assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); + void aNameDeclaredPerDirectionIsOneTag() { + assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.keyOf(Tags.PEER_PORT)); + assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_ID)); + assertNull(KnownTagCodec.openTelemetryNameOf(KnownTags.PEER_PORT_ID)); + assertEquals(Tags.PEER_PORT, KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID)); + assertEquals( + "client.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_INBOUND)); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND)); + assertEquals( + Tags.PEER_PORT, + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_NONE)); } @Test diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 9bc0edc80e9..967aa6eacea 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -73,16 +73,6 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } - @Test - void rejectsSharedNameIds() { - TagMap map = TagMap.create(); - assertThrows(IllegalArgumentException.class, () -> map.set(KnownTags.PEER_PORT_INBOUND_ID, 80)); - assertThrows( - IllegalArgumentException.class, () -> map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 443)); - assertThrows( - IllegalArgumentException.class, () -> map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID)); - } - @Test void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { TagMap checked = TagMap.create(); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index dd477db0eb9..3a9c6a585df 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -92,7 +92,7 @@ private static List knownTagIds() throws IllegalAccessException { for (Field field : KnownTags.class.getFields()) { if (field.getType() == long.class && Modifier.isStatic(field.getModifiers())) { long tagId = field.getLong(null); - if (KnownTagCodec.isKeyableById(tagId)) { + if (KnownTagCodec.isKnown(tagId)) { tagIds.add(tagId); } } diff --git a/tag-conventions.yaml b/tag-conventions.yaml index c4a5f9f9b70..b54462a6a62 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -42,13 +42,12 @@ # abstract type, a plain mixin) it applies in every direction; in a directional scope, only in that # direction. # A tag whose MEANING flips with direction, like peer.port (the server's port on an outbound span, the -# client's on an inbound one), is declared once per direction, each in a directional mixin. Each -# declaration becomes its own tag, with fixed names in both namespaces; the Datadog name is shared. -# (The generator labels these tags `@` in its reports; that label is not YAML -# syntax.) Any other repeated declaration is still an error. -# Until name resolution knows a span's direction, only renames that apply in every direction are used. -# A directional rename, including one under `span-kind: internal` (spans with no direction), is -# listed in tag-assignment.txt but not yet applied. +# client's on an inbound one), is declared once per direction, each in a directional mixin. The +# declarations merge into ONE tag with one id; only its OpenTelemetry name depends on the span's +# direction. Any other repeated declaration is still an error. +# Name resolution does not yet know a span's direction, so only renames that apply in every direction +# resolve a name to a tag. A directional rename, including one under `span-kind: internal` (spans with +# no direction), is applied only when a span's tags are emitted under OpenTelemetry names. # `span-kind-neutral: true` widens a directional rename to every direction -- the author's assertion # that OTel only uses that name for this tag (db.system, say, only ever names a database). It is # interim, and goes away once resolution is direction-aware. A rename on a concrete type with no @@ -162,9 +161,9 @@ mixins: - { dd-name: peer.ipv4, type: string } - { dd-name: peer.ipv6, type: string } - # outbound_peer / inbound_peer declare peer.port once per direction. `peer.port` is then a SHARED - # Datadog name for two tags (reported as peer.port@outbound and peer.port@inbound): emitting it needs no context, - # but resolving the bare name needs the span's direction. + # outbound_peer / inbound_peer declare peer.port once per direction. It is still one tag + # (PEER_PORT_ID); only its OpenTelemetry name depends on the span's direction: client.port on + # inbound spans, server.port on outbound ones. # outbound_peer — the other end of an outbound connection: the server. outbound_peer: From ba715dea365bedfcb7c5d2793462747b81e519c8 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 16:40:32 -0400 Subject: [PATCH 33/35] Name peer.port by the span's kind in OTLP export With peer.port now one direction-neutral tag, its OpenTelemetry name depends on the span: server.port on a client span, client.port on a server span. The OTLP JSON and protobuf writers compute the span's direction once from its span.kind and name each tag for it, so direction-scoped renames apply too: peer.hostname exports as server.address on a client span, and network.client.ip as network.peer.address on a server span. KnownTagCodec gains directionOf(TagMap) / directionOfKind(String), and TagMap.EntryReader gains openTelemetryTag(direction), standing in for the SpanTags helpers #12731 introduces. Ported from #12731's redesign. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/otlp/trace/OtlpTraceJson.java | 15 ++++++--- .../trace/core/otlp/trace/OtlpTraceProto.java | 15 ++++++--- .../trace/OtlpTraceJsonCollectorTest.java | 31 +++++++++++++++++++ .../java/datadog/trace/api/KnownTagCodec.java | 27 ++++++++++++++++ .../main/java/datadog/trace/api/TagMap.java | 10 ++++++ .../java/datadog/trace/api/KnownTagsTest.java | 15 +++++++++ 6 files changed, 105 insertions(+), 8 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java index 32877b15017..1575b26d228 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJson.java @@ -160,12 +160,16 @@ public static void writeSpanLink(JsonWriter writer, AgentSpanLink spanLink) { writer.endObject(); } - private static void writeSpanTag(JsonWriter writer, TagMap.EntryReader tagEntry) { + private static void writeSpanTag( + JsonWriter writer, Integer direction, TagMap.EntryReader tagEntry) { // OTLP is the OpenTelemetry wire format, but the rename itself is opt-in: emit the // OpenTelemetry-namespace name only when OTel semantics are enabled, else the entry's own - // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. + // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. A few + // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = - Config.get().isTraceOtelSemanticsEnabled() ? tagEntry.openTelemetryTag() : tagEntry.tag(); + Config.get().isTraceOtelSemanticsEnabled() + ? tagEntry.openTelemetryTag(direction) + : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: writeAttribute(writer, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue()); @@ -248,7 +252,10 @@ public void accept(Metadata metadata) { writeSpanTag(writer, PROCESS_TAGS_KEY, metadata.processTags()); } - metadata.getTags().forEach(writer, OtlpTraceJson::writeSpanTag); + // The direction is computed once per span; boxing -1..2 hits the Integer cache. + TagMap tags = metadata.getTags(); + Integer direction = KnownTagCodec.directionOf(tags); + tags.forEach(writer, direction, OtlpTraceJson::writeSpanTag); // reset for next span includeProcessTags = false; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java index 6aabcbc9428..ab64bde46cd 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProto.java @@ -224,13 +224,17 @@ public static void writeSpanId(StreamingBuffer buf, long spanId) { buf.putLong(spanId); } - private static void writeSpanTag(StreamingBuffer buf, TagMap.EntryReader tagEntry) { + private static void writeSpanTag( + StreamingBuffer buf, Integer direction, TagMap.EntryReader tagEntry) { writeTag(buf, 9, LEN_WIRE_TYPE); // OTLP is the OpenTelemetry wire format, but the rename itself is opt-in: emit the // OpenTelemetry-namespace name only when OTel semantics are enabled, else the entry's own - // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. + // (Datadog) name, so existing consumers keep seeing Datadog names until they opt in. A few + // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = - Config.get().isTraceOtelSemanticsEnabled() ? tagEntry.openTelemetryTag() : tagEntry.tag(); + Config.get().isTraceOtelSemanticsEnabled() + ? tagEntry.openTelemetryTag(direction) + : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: writeAttribute(buf, BOOLEAN_ATTRIBUTE, key, tagEntry.objectValue()); @@ -325,7 +329,10 @@ public void accept(Metadata metadata) { writeSpanTag(buf, PROCESS_TAGS_KEY, metadata.processTags()); } - metadata.getTags().forEach(buf, OtlpTraceProto::writeSpanTag); + // The direction is computed once per span; boxing -1..2 hits the Integer cache. + TagMap tags = metadata.getTags(); + Integer direction = KnownTagCodec.directionOf(tags); + tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag); // reset for next span includeProcessTags = false; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java index a0456e9d20e..811e3cdf32b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollectorTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test; import org.mockito.AdditionalAnswers; import org.mockito.MockedStatic; +import org.tabletest.junit.TableTest; /** * Tests for {@link OtlpTraceJsonCollector}, parsing the produced JSON back with {@link JsonMapper} @@ -117,6 +118,36 @@ void tagsAreEmittedUnderTheirDatadogNameWhenOtelSemanticsDisabled() throws IOExc "a tag the registry does not name passes through unchanged; got " + attrKeys); } + @TableTest({ + "scenario | kind | tag | otelName ", + "client hostname | client | peer.hostname | server.address ", + "server hostname | server | http.hostname | server.address ", + "server client ip | server | network.client.ip | network.peer.address", + "client peer port | client | peer.port | server.port ", + "server peer port | server | peer.port | client.port " + }) + void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( + String kind, String tag, String otelName) throws IOException { + Config realConfig = Config.get(); + try (MockedStatic configMock = mockStatic(Config.class)) { + Config config = mock(Config.class, AdditionalAnswers.delegatesTo(realConfig)); + when(config.isTraceOtelSemanticsEnabled()).thenReturn(true); + configMock.when(Config::get).thenReturn(config); + + AgentSpan agentSpan = TRACER.startSpan("test", "op.directional"); + agentSpan.setTag(SPAN_KIND, kind); + agentSpan.setTag(tag, "value"); + agentSpan.finish(); + + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) agentSpan)); + Set attrKeys = attributeKeys(onlySpan(collector.collectTraces())); + + assertTrue(attrKeys.contains(otelName), "expected " + otelName + "; got " + attrKeys); + assertFalse(attrKeys.contains(tag), "expected no " + tag + "; got " + attrKeys); + } + } + private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled) throws IOException { Config realConfig = Config.get(); diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index ad15e5a0bfa..80d63829564 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -1,5 +1,7 @@ package datadog.trace.api; +import datadog.trace.bootstrap.instrumentation.api.Tags; + /** * Resolves generated tag IDs and names. {@link KnownTags} owns the lookup tables; this class owns * the ID layout and namespace naming policy. @@ -96,6 +98,31 @@ public static long makeTagId(int serialNum) { /** Internal spans, which have no other end. */ public static final int DIRECTION_NONE = 2; + /** The direction of a span with these tags, from its {@code span.kind}. */ + public static int directionOf(TagMap tags) { + TagMap.Entry kind = tags.getEntry(KnownTags.SPAN_KIND_ID); + return kind == null ? DIRECTION_UNKNOWN : directionOfKind(kind.stringValue()); + } + + /** The direction of a span of {@code kind}; brokers and custom kinds have none. */ + public static int directionOfKind(String kind) { + if (kind == null) { + return DIRECTION_UNKNOWN; + } + switch (kind) { + case Tags.SPAN_KIND_SERVER: + case Tags.SPAN_KIND_CONSUMER: + return DIRECTION_INBOUND; + case Tags.SPAN_KIND_CLIENT: + case Tags.SPAN_KIND_PRODUCER: + return DIRECTION_OUTBOUND; + case Tags.SPAN_KIND_INTERNAL: + return DIRECTION_NONE; + default: + return DIRECTION_UNKNOWN; + } + } + /** The registry's tables. Generated as {@code KnownTags.RESOLVER}; this class owns the policy. */ public interface Resolver { /** The tag's Datadog-namespace (canonical) name. */ diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 088a6a281fa..fbbb985eaf6 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -197,6 +197,16 @@ default String openTelemetryTag() { return otelTag != null ? otelTag : tag(); } + /** + * {@link #openTelemetryTag()} on a span of {@code direction} (a {@code + * KnownTagCodec.DIRECTION_*} constant), which also applies a rename that holds only in that + * direction: {@code peer.port} is {@code server.port} on a client span. + */ + default String openTelemetryTag(int direction) { + long tagId = tagId(); + return tagId == 0 ? tag() : KnownTagCodec.openTelemetryTagOf(tagId, direction); + } + byte type(); boolean is(byte type); diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index c12f7b446a7..12305e3b680 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; /** * Checks the generated registry's name resolution and tag ID encoding. Datadog and OpenTelemetry @@ -180,6 +181,20 @@ void aNameDeclaredPerDirectionIsOneTag() { KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_NONE)); } + @TableTest({ + "scenario | kind | direction", + "server | server | 0 ", + "consumer | consumer | 0 ", + "client | client | 1 ", + "producer | producer | 1 ", + "internal | internal | 2 ", + "broker | broker | -1 ", + "custom | custom | -1 " + }) + void theDirectionOfASpanFollowsItsKind(String kind, int direction) { + assertEquals(direction, KnownTagCodec.directionOfKind(kind)); + } + @Test void unknownIdsResolveToNullName() { assertNull(KnownTagCodec.nameOf(0L)); From 584310cd1e9c1c44b8c5bbdd11c935aa32e8d98c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 11:23:46 -0400 Subject: [PATCH 34/35] Let C2 inline the constructors of TagMap's nested classes Entry's full constructor and the Internals and Ledger constructors were private, so javac (targeting Java 8, without nestmates) gave each one a synthetic access constructor taking the never-loaded class TagMap$1. C2 refuses to inline a call whose signature names an unloaded class ("unloaded signature classes"). So the Internals view the core creates for each id write could not be scalar-replaced, and every Entry built through that constructor went through a real call. Making the constructors package-private removes the accessors and TagMap$1. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagMap.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index fbbb985eaf6..8823ae61d48 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -457,7 +457,7 @@ private Entry(String tag, byte type, long prim, Object obj) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; build it from its id"; } - private Entry(long tagId, byte type, long prim, Object obj) { + Entry(long tagId, byte type, long prim, Object obj) { // Resolve the name once: it both validates the id and names the entry. super(requireKnownName(tagId)); this.tagHash = tagId; @@ -468,7 +468,7 @@ private Entry(long tagId, byte type, long prim, Object obj) { } /** {@code tagId} is a known id, or 0 for the custom tag {@code customTag}. */ - private Entry(long tagId, String customTag, byte type, long prim, Object obj) { + Entry(long tagId, String customTag, byte type, long prim, Object obj) { super(tagId != 0 ? KnownTagCodec.nameOf(tagId) : customTag); this.tagHash = tagId != 0 ? tagId : customHash(customTag); @@ -982,11 +982,11 @@ public static final class Ledger implements Iterable { int nextPos = 0; boolean containsRemovals = false; - private Ledger() { + Ledger() { this(8); } - private Ledger(int size) { + Ledger(int size) { this.entryChanges = new EntryChange[size]; } @@ -1641,7 +1641,7 @@ public Internals internals() { public static final class Internals { private final TagMap map; - private Internals(TagMap map) { + Internals(TagMap map) { this.map = map; } From 3842b16f12119aa323b7aca993973a3441c5975d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 11:43:16 -0400 Subject: [PATCH 35/35] Remove TagMap's remaining synthetic accessors and guard against them Several private members were used across TagMap's nested classes, so javac (targeting Java 8) generated access$ methods for them: Entry's *2Prim helpers and requireKnownName, TagMap.parentEntryVisible, TagMap.putEntry and the TagMap.buckets field. Each accessor adds a level of inlining depth. putEntry's accessor sits on Internals.setKnown, the core's trusted id-write path. All of these are now package-private. TagMapSyntheticAccessorTest fails if TagMap or any of its nested classes gains a synthetic accessor again. It is the prototype for the @NoSyntheticAccess contract (APMLP-1976). Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 18 +++---- .../api/TagMapSyntheticAccessorTest.java | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 8823ae61d48..359ba409b75 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -477,7 +477,7 @@ private Entry(String tag, byte type, long prim, Object obj) { this.rawObj = obj; } - private static String requireKnownName(long tagId) { + static String requireKnownName(long tagId) { String name = tagId == 0 ? null : KnownTagCodec.nameOf(tagId); if (name == null) { throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); @@ -927,7 +927,7 @@ public boolean equals(Object obj) { return this.tag.equals(that.tag) && this.objectValue().equals(that.objectValue()); } - private static long boolean2Prim(boolean value) { + static long boolean2Prim(boolean value) { return value ? 1L : 0L; } @@ -935,7 +935,7 @@ private static boolean prim2Boolean(long prim) { return (prim != 0L); } - private static long int2Prim(int value) { + static long int2Prim(int value) { return value; } @@ -943,7 +943,7 @@ private static int prim2Int(long prim) { return (int) prim; } - private static long long2Prim(long value) { + static long long2Prim(long value) { return value; } @@ -951,7 +951,7 @@ private static long prim2Long(long prim) { return prim; } - private static long float2Prim(float value) { + static long float2Prim(float value) { return Float.floatToIntBits(value); } @@ -959,7 +959,7 @@ private static float prim2Float(long prim) { return Float.intBitsToFloat((int) prim); } - private static long double2Prim(double value) { + static long double2Prim(double value) { return Double.doubleToRawLongBits(value); } @@ -1189,7 +1189,7 @@ public EntryChange next() { * removed from the collision chain. */ - private final Object[] buckets; + final Object[] buckets; private int size; private boolean frozen; @@ -1481,7 +1481,7 @@ private static Entry findInBucket(Object bucket, int hash, String tag) { * that level's same-index bucket, so each nearer level is probed at one bucket using {@code * parentEntry}'s cached hash (no re-hash, no full-map probe). */ - private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { + boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { int hash = parentEntry.hash(); String tag = parentEntry.tag; for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { @@ -1741,7 +1741,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { * for a prior-value lookup they discard; {@link #getAndSet(Entry)} layers the parent fallback on * top. */ - private Entry putEntry(@Nonnull Entry newEntry) { + Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); // Re-setting a key clears any read-through tombstone for it (the new value overrides the diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java new file mode 100644 index 00000000000..3042436cb54 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java @@ -0,0 +1,49 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * TagMap and its nested classes have no synthetic accessors. Targeting Java 8, javac reaches a + * private member of a nested or outer class through a generated {@code access$NNN} method, or a + * constructor taking a dummy {@code $1} class. C2 refuses to inline a call whose signature names a + * never-loaded class, and every accessor adds a level of inlining depth, so on TagMap's hot paths a + * member shared between nested classes must be package-private instead. Becomes a + * {@code @NoSyntheticAccess} contract check (APMLP-1976). + */ +class TagMapSyntheticAccessorTest { + @Test + void theTagMapFamilyHasNoSyntheticAccessors() { + List> classes = new ArrayList<>(); + for (Class root : Arrays.asList(TagMap.class)) { + collect(root, classes); + } + List accessors = new ArrayList<>(); + for (Class c : classes) { + for (Method method : c.getDeclaredMethods()) { + if (method.isSynthetic() && method.getName().startsWith("access$")) { + accessors.add(method.toString()); + } + } + for (Constructor constructor : c.getDeclaredConstructors()) { + if (constructor.isSynthetic()) { + accessors.add(constructor.toString()); + } + } + } + assertEquals(new ArrayList(), accessors); + } + + private static void collect(Class c, List> into) { + into.add(c); + for (Class nested : c.getDeclaredClasses()) { + collect(nested, into); + } + } +}