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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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/99] 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); + } + } +} From bb0bbb0eaabad9c55436278c4bfde14452347870 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 22:25:45 -0400 Subject: [PATCH 36/99] Cover TagMap.Internals, KnownTagCodec and EntryReader paths Brings the three classes back over jacoco's thresholds: the trusted setters' Object/float/double overloads, isIntercepted, isKnown's range edges, canonicalTagName, span-kind direction, and an entry's directional OpenTelemetry name. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/KnownTagsTest.java | 54 +++++++++++++++++++ .../datadog/trace/api/TagMapSetByIdTest.java | 18 +++++++ 2 files changed, 72 insertions(+) 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 12305e3b680..8562ef3c8bf 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -260,4 +260,58 @@ void serialEncodingRoundTrips() { assertEquals(263, KnownTagCodec.serialNum(id)); assertFalse(KnownTagCodec.isTraceLevel(id)); } + + @Test + void theInterceptedBitMarksTheTracerOverlayTags() { + assertTrue(KnownTagCodec.isIntercepted(KnownTags.SPAN_KIND_ID)); + assertFalse(KnownTagCodec.isIntercepted(KnownTags.HTTP_ROUTE_ID)); + } + + @Test + void isKnownRejectsTheNoTagSerialAndSerialsPastTheLimit() { + assertTrue(KnownTagCodec.isKnown(KnownTags.HTTP_ROUTE_ID)); + assertFalse(KnownTagCodec.isKnown(0L)); + assertFalse(KnownTagCodec.isKnown(KnownTagCodec.makeTagId(KnownTagCodec.serialLimit()))); + } + + @Test + void canonicalTagNameMapsAnOpenTelemetryRenameToItsDatadogName() { + assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(KnownTags.HTTP_METHOD_OTEL_NAME)); + assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(Tags.HTTP_METHOD)); + assertEquals("my.custom.tag", KnownTagCodec.canonicalTagName("my.custom.tag")); + } + + @TableTest({ + "scenario | kind | direction", + "no kind | | -1 ", + "server | 'server' | 0 ", + "consumer | 'consumer' | 0 ", + "client | 'client' | 1 ", + "producer | 'producer' | 1 ", + "internal | 'internal' | 2 ", + "broker | 'broker' | -1 " + }) + void directionOfKind(String kind, int direction) { + assertEquals(direction, KnownTagCodec.directionOfKind(kind)); + } + + @Test + void directionOfReadsTheMapsSpanKind() { + TagMap tags = TagMap.create(); + assertEquals(KnownTagCodec.DIRECTION_UNKNOWN, KnownTagCodec.directionOf(tags)); + + tags.set(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT); + assertEquals(KnownTagCodec.DIRECTION_OUTBOUND, KnownTagCodec.directionOf(tags)); + } + + @Test + void anEntrysDirectionalOpenTelemetryTagFallsBackToItsNameForACustomTag() { + TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); + assertEquals("my.custom.tag", custom.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); + + TagMap.Entry peerPort = TagMap.Entry.newAnyEntry(KnownTags.PEER_PORT_ID, 443); + assertEquals( + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND), + peerPort.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); + } } 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 967aa6eacea..4634a02da58 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -90,4 +90,22 @@ void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { assertEquals(new HashMap<>(checked), new HashMap<>(trusted)); 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 fb6410ebbc7e81189e9db728d40f38d6337531b4 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 23:00:58 -0400 Subject: [PATCH 37/99] Format the tag-registry generator to its spotless rules build-logic is a separate Gradle build, so the root spotlessApply never formats it. Three generator lines were wrapped where its Kotlin formatter keeps them on one line. Co-Authored-By: Claude Opus 5.5 --- .../datadog/buildlogic/tagRegistry/GenerateKnownTagsTask.kt | 3 +-- .../main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt | 3 +-- .../datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) 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 72c2dfc1514..4b96de6b26d 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 @@ -31,8 +31,7 @@ abstract class GenerateKnownTagsTask : DefaultTask() { @TaskAction fun generate() { val outDir = destinationDirectory.get().asFile - TagRegistryGenerator.generate( - tagConventionsFile.get().asFile, outDir, tracerOverlayFile.orNull?.asFile) + 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/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index 70b3f7d7d20..1900a5e8c56 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 @@ -117,8 +117,7 @@ class TagRegistry private constructor(val tags: List) { 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 } } + 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, 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 c4e54eec0c8..eaae7ccc1a6 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() From 0d7b7327742784315f3197f0605c1e8b74acc3e4 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 23:32:18 -0400 Subject: [PATCH 38/99] Drop #12817's direction stand-in tests, covered by SpanTagsTest #12817 tests KnownTagCodec.directionOf/directionOfKind and EntryReader.openTelemetryTag(int), stand-ins that this branch replaces with SpanTags. SpanTagsTest already covers the same cases. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/KnownTagsTest.java | 34 ------------------- 1 file changed, 34 deletions(-) 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 53c899af100..19e7dc5a72b 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -407,38 +407,4 @@ void canonicalTagNameMapsAnOpenTelemetryRenameToItsDatadogName() { assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(Tags.HTTP_METHOD)); assertEquals("my.custom.tag", KnownTagCodec.canonicalTagName("my.custom.tag")); } - - @TableTest({ - "scenario | kind | direction", - "no kind | | -1 ", - "server | 'server' | 0 ", - "consumer | 'consumer' | 0 ", - "client | 'client' | 1 ", - "producer | 'producer' | 1 ", - "internal | 'internal' | 2 ", - "broker | 'broker' | -1 " - }) - void directionOfKind(String kind, int direction) { - assertEquals(direction, KnownTagCodec.directionOfKind(kind)); - } - - @Test - void directionOfReadsTheMapsSpanKind() { - TagMap tags = TagMap.create(); - assertEquals(KnownTagCodec.DIRECTION_UNKNOWN, KnownTagCodec.directionOf(tags)); - - tags.set(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT); - assertEquals(KnownTagCodec.DIRECTION_OUTBOUND, KnownTagCodec.directionOf(tags)); - } - - @Test - void anEntrysDirectionalOpenTelemetryTagFallsBackToItsNameForACustomTag() { - TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); - assertEquals("my.custom.tag", custom.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); - - TagMap.Entry peerPort = TagMap.Entry.newAnyEntry(KnownTags.PEER_PORT_ID, 443); - assertEquals( - KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND), - peerPort.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); - } } From 97a508b1f5301d14c60dccd91118b09250176173 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 15:39:03 -0400 Subject: [PATCH 39/99] Order the tag-id carve bits by position Co-Authored-By: Claude Opus 5.5 --- .../kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 1900a5e8c56..248471c228a 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 @@ -39,12 +39,12 @@ 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 + const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE /** - * 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 + * Mirrors KnownTagCodec.makeTagId(serial) + intercepted() + traceLevel() -- must stay in sync. + * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits and the reserved [47-32] window * zero. */ fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long { From 1c88d5ef54f52b5849719cf656c1fd2cfdb67514 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 17:34:05 -0400 Subject: [PATCH 40/99] Re-apply split-by-tags language by its id resolveSplitByTagsPrecedence checked language through the custom-tag overload, which only matches names that resolve to no known tag. language is known, so the check was always false and a prototype's language was never re-applied over another split-by-tags candidate the prototype set. Check and read it by LANGUAGE_ID, as for component. Found by Codex, via a review comment from AlexeyKuznetsov-DD. Co-Authored-By: Claude Opus 5.5 --- .../core/taginterceptor/TagInterceptor.java | 7 +++-- .../taginterceptor/TagInterceptorTest.java | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 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 87f9438ffb6..b487d4665b7 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 @@ -2,6 +2,7 @@ import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; import static datadog.trace.api.KnownTags.COMPONENT_ID; +import static datadog.trace.api.KnownTags.LANGUAGE_ID; 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.ServiceNameSources.SPLIT_BY_SERVLET_CONTEXT; @@ -340,10 +341,10 @@ public void resolveSplitByTagsPrecedence(DDSpanContext span, TagMap prototypeTag return; } } - if (isSplitServiceTag(DDTags.LANGUAGE_TAG_KEY)) { - Object language = prototypeTags.getObject(DDTags.LANGUAGE_TAG_KEY); + if (isSplitServiceTag(LANGUAGE_ID)) { + TagMap.Entry language = prototypeTags.getEntry(LANGUAGE_ID); if (language != null) { - splitService(span, language); + splitService(span, language.objectValue()); } } } 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 533f35a6759..6cb01e76f08 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 @@ -30,6 +30,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; @@ -375,6 +376,36 @@ void splitByTagsDecoratorComponentOverwritesCandidateSetAtConstruction() { assertEquals("my-component", span.getServiceName()); } + /** + * Without a {@code component}, a prototype's {@code language} is re-applied after the prototype's + * tags are intercepted, so it wins over another split-by-tags candidate the prototype sets. + * Called directly: the prototype's map is intercepted in hash order, so a full application can't + * pin the order that needs this. + */ + @Test + void splitByTagsLanguageIsReappliedWithoutAComponent() { + TagInterceptor interceptor = + new TagInterceptor( + true, + "my-service", + new HashSet<>(Arrays.asList("tenant", DDTags.LANGUAGE_TAG_KEY)), + new RuleFlags(), + false); + CoreTracer tracer = + tracerBuilder().writer(new LoggingWriter()).tagInterceptor(interceptor).build(); + AgentSpan span = tracer.buildSpan("datadog", "some span").start(); + span.setTag("tenant", "acme"); // as if the prototype's tenant was intercepted last + assertEquals("acme", span.getServiceName()); + + TagMap prototypeTags = TagMap.create(); + prototypeTags.set(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE); + prototypeTags.set("tenant", "acme"); + interceptor.resolveSplitByTagsPrecedence((DDSpanContext) span.spanContext(), prototypeTags); + span.finish(); + + assertEquals(DDTags.LANGUAGE_TAG_VALUE, span.getServiceName()); + } + /** Separate tag writes stay last-writer-wins, whichever split-by-tags candidate they set. */ @Test void splitByTagsSeparateWritesAreLastWriterWins() { From 5ba0a09509ffb468a2ecb52015cc7f9e77710d16 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 17:34:07 -0400 Subject: [PATCH 41/99] Say what a tag id's intercepted bit does and doesn't decide Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/KnownTagCodec.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 80d63829564..605f7140d4c 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -7,8 +7,9 @@ * the ID layout and namespace naming policy. * *

Datadog and OpenTelemetry names can resolve to the same ID. {@link #canonicalTagName(String)} - * provides the Datadog key used by {@link TagMap}; the namespace readers choose output names. The - * ID does not determine whether a tag is intercepted or where it is stored. + * provides the Datadog key used by {@link TagMap}; the namespace readers choose output names. An ID + * marks a tag that may be intercepted ({@link #INTERCEPTED}), but {@code TagInterceptor} decides + * whether to consume the value. The ID says nothing about where the tag is stored. */ public final class KnownTagCodec { /* From 32ad2a3867023d311b5a143c234a59e3076b3cc9 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 16:05:30 -0400 Subject: [PATCH 42/99] Restore the conventions' one-tag wording for peer.port An earlier merge kept the comments from before peer.port got a single, direction-neutral id. The generated registry already has one PEER_PORT_ID; the comments now say so again. Co-Authored-By: Claude Opus 5.5 --- tag-conventions.yaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 306b3127299f66734dd4e9e9aae0360dae092de1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 16:14:31 -0400 Subject: [PATCH 43/99] Tidy the direction-resolution code from review - TagMap: getAndSet(String, ...) and put use the existing entry helpers again instead of inlining them; writeKeyOf is gone. - DDSpanContext: build the span-kind direction table from SpanTags.directionOfKind so the two can't drift, and keep the supersede-pending-aliases guard in one helper. - KnownTagCodec: drop keyOf(String, int), used only by tests, and the package-private lookup that duplicated keyOrAliasOf. The tests check the same behaviour through directionalKeyOf and keyOrAliasOf. - TagConventions: drop the unused sharedNameDirection. Co-Authored-By: Claude Opus 5.5 --- .../buildlogic/tagRegistry/TagConventions.kt | 4 -- .../datadog/trace/core/DDSpanContext.java | 41 +++++++------ .../java/datadog/trace/api/KnownTagCodec.java | 24 +------- .../main/java/datadog/trace/api/TagMap.java | 61 ++++++------------- .../java/datadog/trace/api/KnownTagsTest.java | 17 ++++-- 5 files changed, 55 insertions(+), 92 deletions(-) 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..387ef8a43a7 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 @@ -64,10 +64,6 @@ class TagConventions private constructor( /** The Datadog-namespace name, shared by the tags 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/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index cfe876c1edb..0cae585e73e 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 @@ -136,19 +136,18 @@ public class DDSpanContext /** * 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. + * server.address} is {@code peer.hostname} on a client span), derived from {@link + * SpanTags#directionOfKind} so the two can't drift. 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 static final int[] TAG_DIRECTION_OF_SPAN_KIND = tagDirectionOfSpanKind(); + + private static int[] tagDirectionOfSpanKind() { + int[] directions = new int[SPAN_KIND_VALUES.length]; + for (int i = 0; i < directions.length; i++) { + directions[i] = SpanTags.directionOfKind(SPAN_KIND_VALUES[i]); + } + return directions; + } private volatile byte spanKindOrdinal = SPAN_KIND_UNSET; @@ -1061,9 +1060,7 @@ public void setMetric(final TagMap.EntryReader entry) { private long writeKeyOf(String tag) { long key = KnownTagCodec.keyOrAliasOf(tag); if (key > 0) { - if (KnownTagCodec.hasScopedAlias(key) && hasPendingAliases()) { - SpanTags.prepareWrite(unsafeTags, key, tagDirection()); - } + supersedePendingAliases(key); return key; } if (key == 0) { @@ -1124,9 +1121,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) && hasPendingAliases()) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); - } + supersedePendingAliases(tagId); unsafeTags.set(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); @@ -1228,6 +1223,16 @@ public void supersedeAliases(long tagId) { return; // entry paths route on the constant mask; nothing to supersede } synchronized (unsafeTags) { + supersedePendingAliases(tagId); + } + } + + /** + * Removes the pending aliases a write of {@code tagId} supersedes, if it has an alias and the + * span holds any. Callers hold the {@code unsafeTags} lock. + */ + private void supersedePendingAliases(long tagId) { + if (KnownTagCodec.hasScopedAlias(tagId) && hasPendingAliases()) { SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); } } 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 cdc73065bb0..1105e186c41 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -227,27 +227,13 @@ public static boolean isKnown(long tagId) { /** * 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)}. + * depends on the span's direction also resolves to 0; see {@link #directionalKeyOf}. */ public static long keyOf(String 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; - } - /** * 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 @@ -323,14 +309,6 @@ private static long aliasMask() { } } - /** - * 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); - } - /** * The Datadog-namespace name to store {@code name} under: {@code name} itself when it is not a * known tag, otherwise the canonical Datadog name for whichever id it resolves to. A Datadog 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 9553fb2a8f2..9d69c99ee5a 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -295,7 +295,7 @@ public static final Entry create(@Nonnull String tag, double value) { /** {@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); + long key = KnownTagCodec.keyOrAliasOf(tag); if (key < 0) { throw new IllegalArgumentException( "'" @@ -1520,10 +1520,7 @@ boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { @Deprecated @Override public Object put(@Nonnull String tag, Object value) { - long tagId = writeKeyOf(tag); - TagMap.Entry entry = - this.getAndSet( - tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); + TagMap.Entry entry = this.getAndSet(anyEntryFor(tag, value)); return entry == null ? null : entry.objectValue(); } @@ -1540,16 +1537,10 @@ 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.) + * from its id or a custom tag from its name. (Entry's own name factories are custom-only.) A + * direction-scoped OpenTelemetry name ({@code server.address}) resolves to no id, so it is stored + * as written, a custom tag, and resolved by whoever knows the 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 static long writeKeyOf(String tag) { - return KnownTagCodec.keyOf(tag); - } static Entry anyEntryFor(String tag, Object value) { long tagId = KnownTagCodec.keyOf(tag); @@ -1587,7 +1578,7 @@ static Entry doubleEntryFor(String tag, double value) { } public void set(@Nonnull String tag, @Nonnull Object value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1596,7 +1587,7 @@ public void set(@Nonnull String tag, @Nonnull Object value) { } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1605,7 +1596,7 @@ public void set(@Nonnull String tag, @Nonnull CharSequence value) { } public void set(@Nonnull String tag, boolean value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1614,7 +1605,7 @@ public void set(@Nonnull String tag, boolean value) { } public void set(@Nonnull String tag, int value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1623,7 +1614,7 @@ public void set(@Nonnull String tag, int value) { } public void set(@Nonnull String tag, long value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1632,7 +1623,7 @@ public void set(@Nonnull String tag, long value) { } public void set(@Nonnull String tag, float value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1641,7 +1632,7 @@ public void set(@Nonnull String tag, float value) { } public void set(@Nonnull String tag, double value) { - long tagId = writeKeyOf(tag); + long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); } else { @@ -1869,45 +1860,31 @@ Entry putEntry(@Nonnull Entry newEntry) { } public Entry getAndSet(@Nonnull String tag, Object value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value)); + return this.getAndSet(anyEntryFor(tag, value)); } public Entry getAndSet(@Nonnull String tag, CharSequence value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value)); + return this.getAndSet(objectEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, boolean value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value)); + return this.getAndSet(booleanEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, int value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value)); + return this.getAndSet(intEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, long value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value)); + return this.getAndSet(longEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, float value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value)); + return this.getAndSet(floatEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, double value) { - long tagId = writeKeyOf(tag); - return this.getAndSet( - tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value)); + return this.getAndSet(doubleEntryFor(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 19e7dc5a72b..fcae594acbb 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -261,7 +261,8 @@ 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)); + // peer.port is not an alias: no direction changes which tag it names + assertEquals(0L, KnownTagCodec.directionalKeyOf(Tags.PEER_PORT, direction)); } } @@ -282,16 +283,22 @@ static Stream directionalNames() { @ParameterizedTest(name = "{0} on direction {1}") @MethodSource("directionalNames") void aDirectionalNameResolvesByDirection(String name, int direction, long expected) { - assertEquals(expected, KnownTagCodec.keyOf(name, direction)); + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + assertEquals(expected, tagId > 0 ? tagId : 0L); assertEquals(0L, KnownTagCodec.keyOf(name)); + assertEquals( + KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, KnownTagCodec.keyOrAliasOf(name)); } @Test void aDirectionFreeNameResolvesTheSameInEveryDirection() { + assertEquals(KnownTags.HTTP_METHOD_ID, KnownTagCodec.keyOf("http.request.method")); + assertEquals(KnownTags.PEER_HOSTNAME_ID, KnownTagCodec.keyOf(Tags.PEER_HOSTNAME)); 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)); + for (String name : + new String[] {"http.request.method", Tags.PEER_HOSTNAME, "my.custom.tag"}) { + assertEquals(0L, KnownTagCodec.directionalKeyOf(name, direction), name); + } } } From 96df27a6a33ab306c7e920c15ed01854c9363b00 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 17:21:07 -0400 Subject: [PATCH 44/99] Keep the alias slow paths to spans that really hold a pending alias - Serialization resolves pending aliases only on a span that stored one, instead of walking every tag of every span, and skips internal spans, where no alias names a tag. - A custom tag that merely passes the alias filter no longer marks the span as holding a pending alias. prepareAliasWrite now tells a real alias left pending (the registry's sentinel) from a name that is no alias at all, and only the former sets the flag. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 21 ++++++---- .../DDSpanDirectionalTagConsistencyTest.java | 39 +++++++++++++++++++ .../main/java/datadog/trace/api/SpanTags.java | 19 ++++----- .../java/datadog/trace/api/SpanTagsTest.java | 15 +++---- 4 files changed, 70 insertions(+), 24 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 0cae585e73e..f8a9836020b 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 @@ -1067,10 +1067,11 @@ private long writeKeyOf(String tag) { return 0L; } long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); - if (aliasedId == 0) { - notePendingAlias(); + if (aliasedId > 0) { + return aliasedId; } - return aliasedId; + notePendingAlias(); // the registry said alias, so this is the sentinel: stored as written + return 0L; } /** @@ -1085,7 +1086,7 @@ private void notePendingAlias() { } /** True once any alias has been stored as written; see {@link #routedTagFlags}. */ - private boolean hasPendingAliases() { + boolean hasPendingAliases() { return (routedTagFlags & KnownTagCodec.HAS_SCOPED_ALIAS) != 0; } @@ -1125,11 +1126,12 @@ private void storeEntry(TagMap.EntryReader entry) { unsafeTags.set(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); - if (aliasedId != 0) { + if (aliasedId > 0) { unsafeTags.set(aliasedId, entry.objectValue()); } else { - // An alias stored as written (or a filter false positive: then merely slower). - notePendingAlias(); + if (aliasedId < 0) { + notePendingAlias(); // a real alias, stored as written; 0 is a filter false positive + } unsafeTags.set(entry); } } else { @@ -1648,7 +1650,10 @@ void processTagsAndBaggage( // - 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()); + // Only a span that stored an alias as written has any; every other span skips the walk. + if (hasPendingAliases()) { + SpanTags.resolveScopedNames(unsafeTags, tagDirection()); + } // Tags TagsPostProcessorFactory.lazyProcessor().processTags(unsafeTags, this, restrictedSpan); 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 3d256ad8867..3e9435ef549 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 @@ -2,9 +2,12 @@ import static java.util.Collections.singletonMap; 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; +import datadog.trace.api.KnownTagCodec; import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -47,6 +50,42 @@ void aBulkWriteReplacesTheValueOfASharedName() { assertEquals(1, entriesNamed(span.getTags(), Tags.PEER_PORT)); } + /** + * The alias filter passes some custom names that aren't aliases. Seeding one must not mark the + * span as holding a pending alias, which would keep its aliased-tag writes and its serialization + * on the slow path for the span's whole life. + */ + @Test + void aCustomTagThatOnlyPassesTheAliasFilterLeavesTheSpanOnTheFastPath() { + String lookalike = "custom.tag18"; + assertTrue(KnownTagCodec.mayBeScopedAlias(lookalike), "precondition: a filter false positive"); + + DDSpan built = + (DDSpan) + tracer + .buildSpan("datadog", "built") + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) + .withTag(lookalike, "value") + .start(); + try { + assertFalse(built.spanContext().hasPendingAliases()); + assertEquals("value", built.getTag(lookalike)); + } finally { + built.finish(); + } + } + + @Test + void aRealAliasWrittenBeforeTheKindIsKnownMarksTheSpan() { + DDSpan built = + (DDSpan) tracer.buildSpan("datadog", "built").withTag("server.address", "host").start(); + try { + assertTrue(built.spanContext().hasPendingAliases()); + } finally { + built.finish(); + } + } + @Test void aBuilderTagWithASharedNameIsAKnownTag() { DDSpan built = 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 cac68efbc20..28efc723a35 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanTags.java +++ b/internal-api/src/main/java/datadog/trace/api/SpanTags.java @@ -76,17 +76,18 @@ public static String openTelemetryTagOf(TagMap.EntryReader entry, int direction) } /** - * 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. + * Prepares a write of {@code alias} to a span of {@code direction}. Returns the id of the tag to + * store the value under; {@link KnownTagCodec#NO_TAG_IN_DIRECTION_SENTINEL} when {@code alias} is + * a direction-scoped alias that can't resolve yet, so its value is stored as written, pending; or + * 0 when it is no alias at all, so its value is an ordinary custom tag. Callers that keep a + * pending-alias flag set it only for the sentinel, so a name that merely passed {@link + * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. */ 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 + if (tagId > 0) { + tags.remove(alias); // a value pending under the alias is older than this write } - tags.remove(alias); // a value pending under the alias is older than this write return tagId; } @@ -200,8 +201,8 @@ public static void remove(TagMap tags, long tagId, int 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; + if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { + return; // no alias names a tag on a span with no direction, or an internal one } List pending = null; for (TagMap.EntryReader entry : tags) { diff --git a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java b/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java index 18e48428610..a5d04978b32 100644 --- a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java @@ -133,17 +133,18 @@ void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { } @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 " + "scenario | name | direction | result", + "no direction yet | server.address | -1 | -3 ", + "no tag in the direction | client.port | 1 | -3 ", + "internal span | server.address | 2 | -3 ", + "not an alias | my.tag | 1 | 0 " }) - void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction) { + void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, long result) { TagMap tags = TagMap.create(); tags.set(name, "pending"); - assertEquals(0L, SpanTags.prepareAliasWrite(tags, name, direction)); + // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all + assertEquals(result, SpanTags.prepareAliasWrite(tags, name, direction)); assertEquals("pending", tags.getObject(name)); } From a5c65565d32d363ab6d2439b94d1138baccbb438 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 17:39:26 -0400 Subject: [PATCH 45/99] Make a removal before the span's kind supersede every meaning Removing a direction-dependent name before the span had a kind removed only its literal spelling, so an older value could resurface once the kind arrived: a removed server.address left peer.hostname visible on what became a client span, and removing peer.hostname left a pending server.address that then resolved to it. A removal now counts as a write: removing a tag also removes the pending aliases that could denote it, and removing an alias also removes every tag it could denote. Co-Authored-By: Claude Opus 5.5 --- .../DDSpanDirectionalTagConsistencyTest.java | 22 ++++++++++ .../main/java/datadog/trace/api/SpanTags.java | 35 ++++++++++++++-- .../java/datadog/trace/api/SpanTagsTest.java | 42 +++++++++++++++++-- 3 files changed, 92 insertions(+), 7 deletions(-) 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 3e9435ef549..c5e8b05a3d3 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 @@ -86,6 +86,28 @@ void aRealAliasWrittenBeforeTheKindIsKnownMarksTheSpan() { } } + /** A removal before the span has a kind must not let an older value resurface once it does. */ + @Test + void removingAnAliasBeforeTheKindIsKnownSticksOnceTheKindArrives() { + span.setTag(Tags.PEER_HOSTNAME, "older"); + span.setTag("server.address", (Object) null); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + assertNull(span.getTag(Tags.PEER_HOSTNAME)); + assertNull(span.getTag("server.address")); + } + + @Test + void removingATagBeforeTheKindIsKnownDropsAPendingAliasForIt() { + span.setTag("server.address", "pending"); + span.setTag(Tags.PEER_HOSTNAME, (Object) null); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + assertNull(span.getTag(Tags.PEER_HOSTNAME)); + } + @Test void aBuilderTagWithASharedNameIsAKnownTag() { DDSpan built = 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 28efc723a35..6325becab35 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanTags.java +++ b/internal-api/src/main/java/datadog/trace/api/SpanTags.java @@ -31,6 +31,10 @@ * resolved for emission. * * + *

A removal counts as a write. Before the span has a direction, removing a tag also removes the + * pending aliases that could denote it, and removing an alias also removes every tag it could + * denote, so a removed value can't resurface once the span learns its kind. + * *

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 @@ -173,6 +177,7 @@ public static Object get(TagMap tags, long tagId, int direction) { public static void remove(TagMap tags, String name, int direction) { tags.remove(name); if (direction == DIRECTION_UNKNOWN) { + removeEveryMeaningOf(tags, name); return; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); @@ -189,9 +194,33 @@ public static void remove(TagMap tags, String name, int 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 alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - if (alias != null) { - tags.remove(alias); + if (KnownTagCodec.hasScopedAlias(tagId)) { + removePendingAliases(tags, tagId, direction); + } + } + + /** + * A removal before the span has a direction counts as a write of every tag {@code name} could + * denote once it has one, so no older value can resurface when the kind arrives: removing a tag + * also removes every pending alias that could denote it, and removing an alias also removes every + * tag it could denote. Like the write rule, this can drop the other direction's tag. + */ + private static void removeEveryMeaningOf(TagMap tags, String name) { + long tagId = KnownTagCodec.keyOf(name); + if (tagId != 0) { + if (KnownTagCodec.hasScopedAlias(tagId)) { + removePendingAliases(tags, tagId, DIRECTION_UNKNOWN); + } + return; + } + if (!KnownTagCodec.mayBeScopedAlias(name)) { + return; + } + for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { + long aliasedId = KnownTagCodec.directionalKeyOf(name, direction); + if (aliasedId > 0) { + tags.getAndRemove(aliasedId); + } } } diff --git a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java b/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java index a5d04978b32..d4fe825e3f4 100644 --- a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java @@ -227,18 +227,15 @@ void removingATagRemovesItsAlias() { } @Test - void removingWithNoDirectionRemovesOnlyTheName() { + void removingATagWithNoAliasLeavesUnrelatedTags() { 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")); } @@ -288,4 +285,41 @@ void resolvingWithNoAliasChangesNothing() { assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertEquals(1, tags.size()); } + + @Test + void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { + TagMap tags = TagMap.create(); + tags.set(Tags.PEER_HOSTNAME, "older"); + + SpanTags.remove(tags, "server.address", DIRECTION_UNKNOWN); + + // once the span is a client, server.address names peer.hostname: the removal must stick + assertNull(SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(SpanTags.get(tags, "server.address", DIRECTION_OUTBOUND)); + } + + @Test + void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { + TagMap byName = TagMap.create(); + byName.set("server.address", "pending"); + SpanTags.remove(byName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); + assertNull(SpanTags.get(byName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + + TagMap byId = TagMap.create(); + byId.set("server.address", "pending"); + SpanTags.remove(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + assertNull(SpanTags.get(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + } + + @Test + void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { + TagMap tags = TagMap.create(); + tags.set(Tags.PEER_HOSTNAME, "kept"); + tags.set("my.tag", "gone"); + + SpanTags.remove(tags, "my.tag", DIRECTION_UNKNOWN); + + assertNull(tags.getObject("my.tag")); + assertEquals("kept", SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + } } From d7356bcfbda921a04a368fb66d4076c3562ddc82 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 19:30:04 -0400 Subject: [PATCH 46/99] Extend the pending-alias gate to reads, removals and entry routing - getTag of a tag with an alias probes for a pending alias only when the span holds one; SpanTags.get takes the span's flag. - Removal by name does one keyOrAliasOf and dispatches on it, with a new TagMap.removeResolved, instead of two registry lookups. - Entries are routed through interceptTag only for real routes; storeEntry already supersedes pending aliases. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 2 +- .../core/taginterceptor/TagInterceptor.java | 5 +- .../main/java/datadog/trace/api/SpanTags.java | 64 +++++++++++-------- .../main/java/datadog/trace/api/TagMap.java | 7 ++ 4 files changed, 50 insertions(+), 28 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 f8a9836020b..c90681df260 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 @@ -1560,7 +1560,7 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return SpanTags.get(unsafeTags, tag, tagDirection()); + return SpanTags.get(unsafeTags, tag, tagDirection(), hasPendingAliases()); } @Deprecated 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 861dc87d6f1..abe02184826 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 @@ -290,7 +290,10 @@ public boolean interceptTag(DDSpanContext span, long tagId, Object value) { public boolean interceptTag(DDSpanContext span, TagMap.EntryReader entry) { long tagId = entry.tagId(); if (tagId != 0) { - return needsIntercept(tagId) && interceptTag(span, tagId, entry.objectValue()); + // Only real routes: an entry that isn't routed is stored through DDSpanContext.storeEntry, + // which already supersedes any pending alias, so HAS_SCOPED_ALIAS needs no detour here. + return needsIntercept(tagId, (int) KnownTagCodec.INTERCEPTED) + && interceptTag(span, tagId, entry.objectValue()); } return isSplitServiceTag(entry.tag()) && splitService(span, entry.objectValue()); } 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 6325becab35..3735fc9dc4f 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanTags.java +++ b/internal-api/src/main/java/datadog/trace/api/SpanTags.java @@ -147,11 +147,21 @@ private static void removePendingAliases(TagMap tags, long tagId, int direction) * single lookup as {@link TagMap#getObject}. */ public static Object get(TagMap tags, String name, int direction) { + return get(tags, name, direction, true); + } + + /** + * {@link #get(TagMap, String, int)} for a caller that tracks whether {@code tags} can hold a + * pending alias at all. With {@code mayHavePendingAliases} false, a tag with an alias skips + * probing for one, so it costs a single lookup like any other tag. + */ + public static Object get(TagMap tags, String name, int direction, boolean mayHavePendingAliases) { long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { - return get(tags, key, direction); + return get(tags, key, direction, mayHavePendingAliases); } - TagMap.Entry asWritten = tags.getEntryResolved(0L, name); + TagMap.Entry asWritten = + key == 0 || mayHavePendingAliases ? tags.getEntryResolved(0L, name) : null; if (key == 0 || asWritten != null) { return asWritten == null ? null : asWritten.objectValue(); } @@ -162,7 +172,14 @@ public static Object get(TagMap tags, String name, int 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 (KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { + return get(tags, tagId, direction, true); + } + + /** {@link #get(TagMap, long, int)}; see {@link #get(TagMap, String, int, boolean)}. */ + public static Object get(TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { + if (mayHavePendingAliases + && KnownTagCodec.hasScopedAlias(tagId) + && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); Object pending = alias == null ? null : tags.getObject(alias); if (pending != null) { @@ -175,19 +192,23 @@ public static Object get(TagMap tags, long tagId, int direction) { /** 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); + // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. + long key = KnownTagCodec.keyOrAliasOf(name); + if (key > 0) { + remove(tags, key, direction); + return; + } + tags.removeResolved(0L, name); // a custom tag, or an alias stored as written + if (key == 0) { + return; + } if (direction == DIRECTION_UNKNOWN) { - removeEveryMeaningOf(tags, name); + removeEveryTagOfAlias(tags, name); return; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); if (tagId > 0) { tags.getAndRemove(tagId); - } else { - String alias = KnownTagCodec.directionScopedNameOf(name, direction); - if (alias != null) { - tags.remove(alias); - } } } @@ -200,24 +221,15 @@ public static void remove(TagMap tags, long tagId, int direction) { } /** - * A removal before the span has a direction counts as a write of every tag {@code name} could - * denote once it has one, so no older value can resurface when the kind arrives: removing a tag - * also removes every pending alias that could denote it, and removing an alias also removes every - * tag it could denote. Like the write rule, this can drop the other direction's tag. + * Removes every tag {@code alias} could denote, for a removal of the alias before the span has a + * direction. A removal counts as a write, so no older value can resurface when the kind arrives; + * like the write rule, this can drop the other direction's tag. (Removing a tag before then + * likewise removes the pending aliases that could denote it; see {@link #remove(TagMap, long, + * int)}.) */ - private static void removeEveryMeaningOf(TagMap tags, String name) { - long tagId = KnownTagCodec.keyOf(name); - if (tagId != 0) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, tagId, DIRECTION_UNKNOWN); - } - return; - } - if (!KnownTagCodec.mayBeScopedAlias(name)) { - return; - } + private static void removeEveryTagOfAlias(TagMap tags, String alias) { for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { - long aliasedId = KnownTagCodec.directionalKeyOf(name, direction); + long aliasedId = KnownTagCodec.directionalKeyOf(alias, direction); if (aliasedId > 0) { tags.getAndRemove(aliasedId); } 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 9d69c99ee5a..e6c03d04223 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1682,6 +1682,13 @@ public Entry getEntryResolved(long tagId, String tag) { return tagId > 0 ? this.getEntry(tagId) : this.getEntry(tag, Entry.customHash(tag)); } + /** The removal counterpart of {@link #setResolved(long, String, Object)}. */ + public Entry removeResolved(long tagId, String tag) { + this.checkWriteAccess(); + + return tagId > 0 ? this.getAndRemove(tagId) : this.getAndRemove(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; From c517b18fde54f6b7f807048de9d17ad9bbcadcb7 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 19:47:31 -0400 Subject: [PATCH 47/99] Encapsulate direction handling in a package-private DirectionalTags SpanTags was a public internal-api class, though only DDSpanContext used its span behavior. It is now DirectionalTags, package-private beside DDSpanContext in dd-trace-core, and reads everything through KnownTagCodec and TagMap. The naming facts other code needs go back to KnownTagCodec, where master had them: directionOf, directionOfKind, and a new isDirectionDependent for TagInterceptor. TagMap.EntryReader.openTelemetryTag(int) is restored, so the OTLP writers are unchanged from master. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 27 +-- .../datadog/trace/core/DirectionalTags.java | 82 ++------- .../trace/core/otlp/trace/OtlpTraceJson.java | 5 +- .../trace/core/otlp/trace/OtlpTraceProto.java | 5 +- .../core/taginterceptor/TagInterceptor.java | 7 +- .../DDSpanDirectionalTagConsistencyTest.java | 6 +- .../trace/core/DirectionalTagsTest.java | 158 ++++++------------ .../java/datadog/trace/api/KnownTagCodec.java | 41 +++++ .../main/java/datadog/trace/api/TagMap.java | 10 ++ .../trace/api/KnownTagCodecDirectionTest.java | 71 ++++++++ 10 files changed, 216 insertions(+), 196 deletions(-) rename internal-api/src/main/java/datadog/trace/api/SpanTags.java => dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java (75%) rename internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java => dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java (54%) create mode 100644 internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.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 c90681df260..e2b3b210350 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 @@ -18,7 +18,6 @@ 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; @@ -137,14 +136,14 @@ public class DDSpanContext /** * The direction each span kind ordinal reads direction-dependent tag names in ({@code * server.address} is {@code peer.hostname} on a client span), derived from {@link - * SpanTags#directionOfKind} so the two can't drift. Broker and custom kinds have none. + * DirectionalTags#directionOfKind} so the two can't drift. Broker and custom kinds have none. */ private static final int[] TAG_DIRECTION_OF_SPAN_KIND = tagDirectionOfSpanKind(); private static int[] tagDirectionOfSpanKind() { int[] directions = new int[SPAN_KIND_VALUES.length]; for (int i = 0; i < directions.length; i++) { - directions[i] = SpanTags.directionOfKind(SPAN_KIND_VALUES[i]); + directions[i] = KnownTagCodec.directionOfKind(SPAN_KIND_VALUES[i]); } return directions; } @@ -960,7 +959,9 @@ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); } - /** The direction this span reads direction-dependent tag names in; see {@link SpanTags}. */ + /** + * The direction this span reads direction-dependent tag names in; see {@link DirectionalTags}. + */ private int tagDirection() { return TAG_DIRECTION_OF_SPAN_KIND[spanKindOrdinal]; } @@ -1051,10 +1052,10 @@ public void setMetric(final TagMap.EntryReader entry) { * 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 + * Only those go through DirectionalTags, 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 + * even when the eventual kind maps it to the other tag (see DirectionalTags). Call while holding the * unsafeTags lock. */ private long writeKeyOf(String tag) { @@ -1066,7 +1067,7 @@ private long writeKeyOf(String tag) { if (key == 0) { return 0L; } - long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); + long aliasedId = DirectionalTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); if (aliasedId > 0) { return aliasedId; } @@ -1125,7 +1126,7 @@ private void storeEntry(TagMap.EntryReader entry) { supersedePendingAliases(tagId); unsafeTags.set(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { - long aliasedId = SpanTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); + long aliasedId = DirectionalTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); if (aliasedId > 0) { unsafeTags.set(aliasedId, entry.objectValue()); } else { @@ -1146,7 +1147,7 @@ public void removeTag(String tag) { } synchronized (unsafeTags) { // A direction-dependent name and the tag it denotes here are removed together. - SpanTags.remove(unsafeTags, tag, tagDirection()); + DirectionalTags.remove(unsafeTags, tag, tagDirection()); } } @@ -1164,7 +1165,7 @@ public void removeTag(long tagId) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (unsafeTags) { - SpanTags.remove(unsafeTags, tagId, tagDirection()); + DirectionalTags.remove(unsafeTags, tagId, tagDirection()); } } @@ -1235,7 +1236,7 @@ public void supersedeAliases(long tagId) { */ private void supersedePendingAliases(long tagId) { if (KnownTagCodec.hasScopedAlias(tagId) && hasPendingAliases()) { - SpanTags.prepareWrite(unsafeTags, tagId, tagDirection()); + DirectionalTags.prepareWrite(unsafeTags, tagId, tagDirection()); } } @@ -1560,7 +1561,7 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return SpanTags.get(unsafeTags, tag, tagDirection(), hasPendingAliases()); + return DirectionalTags.get(unsafeTags, tag, tagDirection(), hasPendingAliases()); } @Deprecated @@ -1652,7 +1653,7 @@ void processTagsAndBaggage( // Direction-scoped names: the span's kind is final now, so each resolves to its one tag. // Only a span that stored an alias as written has any; every other span skips the walk. if (hasPendingAliases()) { - SpanTags.resolveScopedNames(unsafeTags, tagDirection()); + DirectionalTags.resolveScopedNames(unsafeTags, tagDirection()); } // Tags diff --git a/internal-api/src/main/java/datadog/trace/api/SpanTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java similarity index 75% rename from internal-api/src/main/java/datadog/trace/api/SpanTags.java rename to dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index 3735fc9dc4f..bcf4665df10 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -1,11 +1,11 @@ -package datadog.trace.api; +package datadog.trace.core; 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 datadog.trace.api.KnownTagCodec; +import datadog.trace.api.TagMap; import java.util.ArrayList; import java.util.List; @@ -43,41 +43,7 @@ * ({@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}. */ - 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); - } +final class DirectionalTags { /** * Prepares a write of {@code alias} to a span of {@code direction}. Returns the id of the tag to @@ -87,7 +53,7 @@ public static String openTelemetryTagOf(TagMap.EntryReader entry, int direction) * pending-alias flag set it only for the sentinel, so a name that merely passed {@link * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. */ - public static long prepareAliasWrite(TagMap tags, String alias, int direction) { + static long prepareAliasWrite(TagMap tags, String alias, int direction) { long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { tags.remove(alias); // a value pending under the alias is older than this write @@ -100,27 +66,12 @@ public static long prepareAliasWrite(TagMap tags, String alias, int direction) { * 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) { + static void prepareWrite(TagMap tags, long tagId, int direction) { 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. @@ -146,7 +97,7 @@ private static void removePendingAliases(TagMap tags, long tagId, int direction) * 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) { + static Object get(TagMap tags, String name, int direction) { return get(tags, name, direction, true); } @@ -155,7 +106,7 @@ public static Object get(TagMap tags, String name, int direction) { * pending alias at all. With {@code mayHavePendingAliases} false, a tag with an alias skips * probing for one, so it costs a single lookup like any other tag. */ - public static Object get(TagMap tags, String name, int direction, boolean mayHavePendingAliases) { + static Object get(TagMap tags, String name, int direction, boolean mayHavePendingAliases) { long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { return get(tags, key, direction, mayHavePendingAliases); @@ -170,13 +121,16 @@ public static Object get(TagMap tags, String name, int direction, boolean mayHav 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) { + /** + * The value of the known tag {@code tagId} on a span of {@code direction}; see {@link + * #get(TagMap, String, int)}. + */ + static Object get(TagMap tags, long tagId, int direction) { return get(tags, tagId, direction, true); } /** {@link #get(TagMap, long, int)}; see {@link #get(TagMap, String, int, boolean)}. */ - public static Object get(TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { + static Object get(TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { if (mayHavePendingAliases && KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { @@ -191,7 +145,7 @@ public static Object get(TagMap tags, long tagId, int direction, boolean mayHave } /** 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) { + static void remove(TagMap tags, String name, int direction) { // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { @@ -213,7 +167,7 @@ public static void remove(TagMap tags, String name, int 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) { + static void remove(TagMap tags, long tagId, int direction) { tags.getAndRemove(tagId); if (KnownTagCodec.hasScopedAlias(tagId)) { removePendingAliases(tags, tagId, direction); @@ -241,7 +195,7 @@ private static void removeEveryTagOfAlias(TagMap tags, String alias) { * 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) { + static void resolveScopedNames(TagMap tags, int direction) { if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { return; // no alias names a tag on a span with no direction, or an internal one } @@ -263,5 +217,5 @@ public static void resolveScopedNames(TagMap tags, int direction) { } } - private SpanTags() {} + private DirectionalTags() {} } 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 be11a554822..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 @@ -28,7 +28,6 @@ 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; @@ -169,7 +168,7 @@ private static void writeSpanTag( // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = Config.get().isTraceOtelSemanticsEnabled() - ? SpanTags.openTelemetryTagOf(tagEntry, direction) + ? tagEntry.openTelemetryTag(direction) : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: @@ -255,7 +254,7 @@ public void accept(Metadata metadata) { // The direction is computed once per span; boxing -1..2 hits the Integer cache. TagMap tags = metadata.getTags(); - Integer direction = SpanTags.directionOf(tags); + Integer direction = KnownTagCodec.directionOf(tags); tags.forEach(writer, direction, OtlpTraceJson::writeSpanTag); // reset for next span 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 ac653536ee1..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 @@ -40,7 +40,6 @@ 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; @@ -234,7 +233,7 @@ private static void writeSpanTag( // OpenTelemetry names depend on the span's direction (peer.port is server.port on a client). String key = Config.get().isTraceOtelSemanticsEnabled() - ? SpanTags.openTelemetryTagOf(tagEntry, direction) + ? tagEntry.openTelemetryTag(direction) : tagEntry.tag(); switch (tagEntry.type()) { case TagMap.EntryReader.BOOLEAN: @@ -332,7 +331,7 @@ public void accept(Metadata metadata) { // The direction is computed once per span; boxing -1..2 hits the Integer cache. TagMap tags = metadata.getTags(); - Integer direction = SpanTags.directionOf(tags); + Integer direction = KnownTagCodec.directionOf(tags); tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag); // reset for next span 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 abe02184826..e54e8faf099 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 @@ -24,7 +24,6 @@ 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; @@ -142,12 +141,12 @@ private boolean isSplitServiceTag(String 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. + * holds a direction-dependent name, which the span resolves through {@code DirectionalTags}. + * 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) || SpanTags.isDirectionDependent(entry)) return true; + if (needsIntercept(entry) || KnownTagCodec.isDirectionDependent(entry)) return true; } return false; } 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 c5e8b05a3d3..b5a2493b717 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 @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.trace.api.KnownTagCodec; -import datadog.trace.api.SpanTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.ListWriter; @@ -163,13 +162,14 @@ void theLastWriteWinsBetweenATagAndItsAlias( // 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); + DirectionalTags.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. + * The accepted corner (see DirectionalTags): a pre-kind write of either possible tag drops the + * alias. */ @Test void aPreKindWriteOfTheOtherPossibleTagDropsAPendingAlias() { diff --git a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java similarity index 54% rename from internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java rename to dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java index d4fe825e3f4..4bdb3c2d004 100644 --- a/internal-api/src/test/java/datadog/trace/api/SpanTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java @@ -1,65 +1,24 @@ -package datadog.trace.api; +package datadog.trace.core; 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.api.KnownTagCodec; +import datadog.trace.api.KnownTags; +import datadog.trace.api.TagMap; 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. + * {@link DirectionalTags}: a span's reading of direction-dependent names over a plain {@link + * TagMap}. The span-level behavior, with real spans, is in the 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)); - } - +class DirectionalTagsTest { @TableTest({ "scenario | direction | alias | tag ", "client: alias reads its tag | 1 | server.address | peer.hostname ", @@ -72,15 +31,15 @@ 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)); + assertEquals("stored", DirectionalTags.get(tags, alias, direction)); + assertEquals("stored", DirectionalTags.get(tags, tag, direction)); + assertEquals("stored", DirectionalTags.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)); + assertEquals("pending", DirectionalTags.get(tags, alias, direction)); + assertEquals("pending", DirectionalTags.get(tags, tag, direction)); + assertEquals("pending", DirectionalTags.get(tags, KnownTagCodec.keyOf(tag), direction)); } @Test @@ -88,26 +47,27 @@ 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)); + assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals( + "stored", DirectionalTags.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)); + assertEquals("pending", DirectionalTags.get(tags, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", DirectionalTags.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)); + assertNull(DirectionalTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_OUTBOUND)); // client.port names no tag on a client span - assertNull(SpanTags.get(tags, "client.port", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, "client.port", DIRECTION_OUTBOUND)); } @Test @@ -116,9 +76,9 @@ void otherNamesReadAsWritten() { 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)); + assertEquals("custom", DirectionalTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); + assertEquals("GET", DirectionalTags.get(tags, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); + assertEquals("GET", DirectionalTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); } @Test @@ -126,7 +86,7 @@ void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { TagMap tags = TagMap.create(); tags.set("server.address", "older"); - long tagId = SpanTags.prepareAliasWrite(tags, "server.address", DIRECTION_OUTBOUND); + long tagId = DirectionalTags.prepareAliasWrite(tags, "server.address", DIRECTION_OUTBOUND); assertEquals(KnownTags.PEER_HOSTNAME_ID, tagId); assertNull(tags.getObject("server.address")); // the older pending value is dropped @@ -144,7 +104,7 @@ void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, tags.set(name, "pending"); // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all - assertEquals(result, SpanTags.prepareAliasWrite(tags, name, direction)); + assertEquals(result, DirectionalTags.prepareAliasWrite(tags, name, direction)); assertEquals("pending", tags.getObject(name)); } @@ -154,7 +114,7 @@ void aTagWriteWithNoDirectionDropsEveryAliasThatCouldDenoteIt() { tags.set("client.port", 1); tags.set("server.port", 2); - SpanTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); assertNull(tags.getObject("client.port")); assertNull(tags.getObject("server.port")); @@ -166,7 +126,7 @@ void aTagWriteWithADirectionDropsOnlyItsAliasThere() { 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); + DirectionalTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); assertEquals(1, tags.getObject("client.port")); assertNull(tags.getObject("server.port")); @@ -177,33 +137,19 @@ 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); + DirectionalTags.prepareWrite(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); + DirectionalTags.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); + DirectionalTags.remove(tags, "server.address", DIRECTION_OUTBOUND); assertNull(tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("server.address")); @@ -214,14 +160,14 @@ 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); + DirectionalTags.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); + DirectionalTags.remove(byId, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); assertNull(byId.getObject("server.address")); } @@ -232,8 +178,8 @@ void removingATagWithNoAliasLeavesUnrelatedTags() { tags.set(Tags.PEER_HOSTNAME, "stored"); tags.set("my.tag", "custom"); - SpanTags.remove(tags, "my.tag", DIRECTION_OUTBOUND); - SpanTags.remove(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); + DirectionalTags.remove(tags, "my.tag", DIRECTION_OUTBOUND); + DirectionalTags.remove(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("my.tag")); @@ -252,7 +198,7 @@ void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String t tags.set(alias, "newer"); tags.set("my.tag", "custom"); - SpanTags.resolveScopedNames(tags, direction); + DirectionalTags.resolveScopedNames(tags, direction); assertEquals("newer", tags.getObject(tag)); assertNull(tags.getObject(alias)); @@ -265,13 +211,13 @@ void resolvingLeavesAnAliasWithNoTagAsWritten() { tags.set("client.port", 1234); tags.set("server.address", "pending"); - SpanTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); + DirectionalTags.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); + DirectionalTags.resolveScopedNames(unknown, DIRECTION_UNKNOWN); assertEquals("pending", unknown.getObject("server.address")); } @@ -280,7 +226,7 @@ void resolvingWithNoAliasChangesNothing() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "stored"); - SpanTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); + DirectionalTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertEquals(1, tags.size()); @@ -291,24 +237,24 @@ void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "older"); - SpanTags.remove(tags, "server.address", DIRECTION_UNKNOWN); + DirectionalTags.remove(tags, "server.address", DIRECTION_UNKNOWN); // once the span is a client, server.address names peer.hostname: the removal must stick - assertNull(SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(SpanTags.get(tags, "server.address", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_OUTBOUND)); } @Test void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { TagMap byName = TagMap.create(); byName.set("server.address", "pending"); - SpanTags.remove(byName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); - assertNull(SpanTags.get(byName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + DirectionalTags.remove(byName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); + assertNull(DirectionalTags.get(byName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); TagMap byId = TagMap.create(); byId.set("server.address", "pending"); - SpanTags.remove(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); - assertNull(SpanTags.get(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + DirectionalTags.remove(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + assertNull(DirectionalTags.get(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); } @Test @@ -317,9 +263,9 @@ void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { tags.set(Tags.PEER_HOSTNAME, "kept"); tags.set("my.tag", "gone"); - SpanTags.remove(tags, "my.tag", DIRECTION_UNKNOWN); + DirectionalTags.remove(tags, "my.tag", DIRECTION_UNKNOWN); assertNull(tags.getObject("my.tag")); - assertEquals("kept", SpanTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertEquals("kept", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); } } 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 1105e186c41..2c2ef36b022 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. @@ -69,6 +71,45 @@ public static boolean isIntercepted(long tagId) { */ public static final long HAS_SCOPED_ALIAS = 1L << 0; + /** 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; + } + } + + /** + * True if a span must apply its direction to {@code entry}: a known tag with {@link + * #HAS_SCOPED_ALIAS}, or a direction-scoped alias stored as written. A mask test for a known tag; + * a filter test, then one lookup, for a custom one. + */ + public static boolean isDirectionDependent(TagMap.EntryReader entry) { + long tagId = entry.tagId(); + if (tagId != 0) { + return hasScopedAlias(tagId); + } + String tag = entry.tag(); + return mayBeScopedAlias(tag) && keyOrAliasOf(tag) == DIRECTION_SCOPED_OTEL_NAME_SENTINEL; + } + /** 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; 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 e6c03d04223..7897933584e 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/KnownTagCodecDirectionTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.java new file mode 100644 index 00000000000..0c8e0a6791c --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.java @@ -0,0 +1,71 @@ +package datadog.trace.api; + +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.assertTrue; + +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +/** + * {@link KnownTagCodec}'s direction helpers: a span's direction, and the direction-dependent names. + */ +class KnownTagCodecDirectionTest { + @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, KnownTagCodec.directionOfKind(kind)); + + TagMap tags = TagMap.create(); + tags.set(Tags.SPAN_KIND, kind); + assertEquals(direction, KnownTagCodec.directionOf(tags)); + } + + @Test + void noKindIsNoDirection() { + assertEquals(DIRECTION_UNKNOWN, KnownTagCodec.directionOfKind(null)); + assertEquals(DIRECTION_UNKNOWN, KnownTagCodec.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, tags.getEntry(tag).openTelemetryTag(direction)); + } + + @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(KnownTagCodec.isDirectionDependent(tags.getEntry("server.address"))); + assertTrue(KnownTagCodec.isDirectionDependent(tags.getEntry(Tags.PEER_HOSTNAME))); + assertFalse(KnownTagCodec.isDirectionDependent(tags.getEntry(Tags.HTTP_METHOD))); + assertFalse(KnownTagCodec.isDirectionDependent(tags.getEntry("my.tag"))); + } +} From ab4b1ace98abf097052d70b07ebae0ce133c7065 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 20:17:55 -0400 Subject: [PATCH 48/99] Write and read known tags by id without building an Entry per call - TagMap.setResolved now calls set(tagId, ...) for a known id, as set(String, ...) already does, so every name-keyed span write takes the id path. Same result on today's store; a store that keeps known tags without an Entry per tag no longer allocates one here. - TagMap gains a package-private getKnownValue(long), exposed to the core as Internals.getKnownValue. DirectionalTags' id reads and KnownTagCodec.directionOf use it instead of getEntry(id).objectValue(). Both pulled forward from the dense store work (#12856) so it overrides them rather than reworking these call sites. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DirectionalTags.java | 6 +- .../java/datadog/trace/api/KnownTagCodec.java | 4 +- .../main/java/datadog/trace/api/TagMap.java | 60 +++++++++++++++---- .../datadog/trace/api/TagMapSetByIdTest.java | 33 ++++++++++ 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index bcf4665df10..c2ace4af8d1 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -117,8 +117,7 @@ static Object get(TagMap tags, String name, int direction, boolean mayHavePendin 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(); + return tagId > 0 ? tags.internals().getKnownValue(tagId) : null; } /** @@ -140,8 +139,7 @@ static Object get(TagMap tags, long tagId, int direction, boolean mayHavePending return pending; } } - TagMap.Entry tag = tags.getEntry(tagId); - return tag == null ? null : tag.objectValue(); + return tags.internals().getKnownValue(tagId); } /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ 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 2c2ef36b022..1e28d4841d2 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -73,8 +73,8 @@ public static boolean isIntercepted(long tagId) { /** 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()); + Object kind = tags.getKnownValue(KnownTags.SPAN_KIND_ID); + return kind == null ? DIRECTION_UNKNOWN : directionOfKind(kind.toString()); } /** The direction of a span of {@code kind}; brokers and custom kinds have none. */ 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 7897933584e..0f1f71fc8bd 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1657,34 +1657,59 @@ public void set(@Nonnull String tag, double value) { * 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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(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)); + if (tagId > 0) { + this.set(tagId, value); + } else { + this.putEntry(Entry.newDoubleEntry(tag, value)); + } } /** The read counterpart of {@link #setResolved(long, String, Object)}. */ @@ -1758,6 +1783,21 @@ public void setKnown(long tagId, float value) { public void setKnown(long tagId, double value) { this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } + + /** + * The value of the known tag {@code tagId}, boxed if necessary, or null: {@code + * getEntry(tagId)}'s value, including read-through, for a caller that needs only the value. A + * store that keeps known tags without an Entry per tag can answer without building one. + */ + public Object getKnownValue(long tagId) { + return this.map.getKnownValue(tagId); + } + } + + /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ + Object getKnownValue(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.objectValue(); } public void set(long tagId, @Nonnull Object 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 4634a02da58..e5463372094 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -73,6 +73,39 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } + @Test + void theCoresKnownValueReadMatchesTheEntrysValue() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + TagMap tags = TagMap.createFromParent(parent.freeze()); + tags.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + + assertEquals("/users/{id}", tags.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals( + 200, tags.internals().getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); // read-through + assertNull(tags.internals().getKnownValue(KnownTags.HTTP_METHOD_ID)); + } + + @Test + void aResolvedWriteOfAKnownTagStoresWhatAnIdWriteStores() { + TagMap byId = TagMap.create(); + byId.set(KnownTags.HTTP_ROUTE_ID, "/a"); + byId.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + byId.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + byId.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + + TagMap resolved = TagMap.create(); + resolved.setResolved(KnownTags.HTTP_ROUTE_ID, KnownTags.HTTP_ROUTE_NAME, "/a"); + resolved.setResolved(KnownTags.HTTP_STATUS_CODE_ID, "ignored", 200); + resolved.setResolved(KnownTags.HTTP_RESEND_COUNT_ID, "ignored", 2L); + resolved.setResolved(KnownTags.DD_PROFILING_ENABLED_ID, "ignored", true); + resolved.setResolved(0L, "my.custom.tag", "custom"); + + assertEquals("custom", resolved.getObject("my.custom.tag")); + resolved.remove("my.custom.tag"); + assertEquals(new HashMap<>(byId), new HashMap<>(resolved)); + } + @Test void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { TagMap checked = TagMap.create(); From 351145d8deed27472a8d75966e302a7ee847b597 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 22:54:09 -0400 Subject: [PATCH 49/99] Resolve aliases when the kind is set, and route them as their tag - Pending aliases fold into their tags as soon as the span learns its kind, in setSpanKindOrdinal, instead of at serialization. Readers that run first, such as peer service naming in processServiceTags, now see the tag. The serialization-time pass is gone. - Applying a prototype checks presence by the tag an entry resolves to, so an alias default can't overwrite an explicit value of its tag. - A name-keyed or bulk write of an alias, once the span has a direction, is intercepted as the tag it denotes, so split-by-tags on peer.hostname sees server.address. Accepted corner, documented on DirectionalTags: an alias written before the kind is resolved without passing through the interceptor again. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 91 +++++++++++++++---- .../datadog/trace/core/DirectionalTags.java | 4 + .../DDSpanDirectionalTagConsistencyTest.java | 27 ++++++ .../taginterceptor/TagInterceptorTest.java | 18 ++++ 4 files changed, 120 insertions(+), 20 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 e2b3b210350..e27656b7cf6 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 @@ -700,11 +700,10 @@ private void seedAbsentTags(final TagMap map) { map.forEach( this, (ctx, tagEntry) -> { - final String tag = tagEntry.tag(); - if (ctx.unsafeTags.containsKey(tag)) { + if (ctx.isPresent(tagEntry)) { return; } - if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { + if (!ctx.interceptEntry(tagEntry)) { ctx.storeEntry(tagEntry); } }); @@ -957,6 +956,14 @@ static boolean tagEquals(String tagValue, String tagLiteral) { */ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); + if (hasPendingAliases()) { + // The span now has a direction: fold pending aliases into the tags they denote, so every + // later reader (peer service naming, stats, sampling rules, serialization) sees them + // resolved. + synchronized (unsafeTags) { + DirectionalTags.resolveScopedNames(unsafeTags, tagDirection()); + } + } } /** @@ -1181,11 +1188,56 @@ public void removeTag(long tagId) { * @param tag The tag name. * @param value The nullable tag value. */ + /** + * The id a name-keyed write of {@code tag} goes through: the tag's own id; for a direction-scoped + * alias ({@code server.address}) once the span has a direction, the tag it denotes; else 0. So an + * alias takes the id path, including interception (split-by-tags on {@code peer.hostname} sees a + * {@code server.address} write on a client span). + */ + private long idForWrite(String tag) { + long tagId = KnownTagCodec.keyOf(tag); + if (tagId == 0 && KnownTagCodec.mayBeScopedAlias(tag)) { + long aliasedId = KnownTagCodec.directionalKeyOf(tag, tagDirection()); + if (aliasedId > 0) { + return aliasedId; + } + } + return tagId; + } + + /** + * {@link TagInterceptor#interceptTag(DDSpanContext, TagMap.EntryReader)} for an entry about to be + * stored through {@link #storeEntry}, which resolves a direction-scoped alias to its tag: such an + * entry is intercepted as that tag, so routing sees the tag it is stored as. + */ + private boolean interceptEntry(TagMap.EntryReader entry) { + if (entry.tagId() == 0 && KnownTagCodec.mayBeScopedAlias(entry.tag())) { + long aliasedId = KnownTagCodec.directionalKeyOf(entry.tag(), tagDirection()); + if (aliasedId > 0) { + return tagInterceptor.needsIntercept(aliasedId, (int) KnownTagCodec.INTERCEPTED) + && tagInterceptor.interceptTag(this, aliasedId, entry.objectValue()); + } + } + return tagInterceptor.interceptTag(this, entry); + } + + /** + * Whether {@code entry}'s tag is already on this span, by the tag it denotes here: a + * direction-scoped alias is present if its tag is, and a tag if a pending alias for it is. + */ + private boolean isPresent(TagMap.EntryReader entry) { + if (KnownTagCodec.isDirectionDependent(entry)) { + return DirectionalTags.get(unsafeTags, entry.tag(), tagDirection(), hasPendingAliases()) + != null; + } + return unsafeTags.containsKey(entry.tag()); + } + public void setTag(final String tag, final Object value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (null == value) { @@ -1201,7 +1253,7 @@ public void setTag(final String tag, final String value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (null == value) { @@ -1344,7 +1396,7 @@ public void setTag(TagMap.EntryReader entry) { } // the interceptor prechecks the entry, to avoid boxing - if (!tagInterceptor.interceptTag(this, entry)) { + if (!interceptEntry(entry)) { synchronized (unsafeTags) { storeEntry(entry); } @@ -1375,7 +1427,7 @@ public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) @@ -1390,7 +1442,7 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) @@ -1405,7 +1457,7 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) @@ -1420,7 +1472,7 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) @@ -1435,7 +1487,7 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - final long tagId = KnownTagCodec.keyOf(tag); + final long tagId = idForWrite(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) @@ -1463,7 +1515,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { map.forEach( this, (ctx, tagEntry) -> { - if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { + if (!ctx.interceptEntry(tagEntry)) { ctx.storeEntry(tagEntry); } }); @@ -1491,7 +1543,7 @@ void setAllTags(final TagMap.Ledger ledger) { } else { TagMap.Entry entry = (TagMap.Entry) entryChange; - if (!tagInterceptor.interceptTag(this, entry)) { + if (!interceptEntry(entry)) { storeEntry(entry); } } @@ -1507,7 +1559,12 @@ void setAllTags(final Map map) { } else if (!map.isEmpty()) { synchronized (unsafeTags) { for (final Map.Entry tag : map.entrySet()) { - if (!tagInterceptor.interceptTag(this, tag.getKey(), tag.getValue())) { + long tagId = idForWrite(tag.getKey()); + boolean intercepted = + tagId != 0 + ? tagInterceptor.interceptTag(this, tagId, tag.getValue()) + : tagInterceptor.interceptCustomTag(this, tag.getKey(), tag.getValue()); + if (!intercepted) { unsafeSetTag(tag.getKey(), tag.getValue()); } } @@ -1650,12 +1707,6 @@ 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. - // Only a span that stored an alias as written has any; every other span skips the walk. - if (hasPendingAliases()) { - DirectionalTags.resolveScopedNames(unsafeTags, tagDirection()); - } - // Tags TagsPostProcessorFactory.lazyProcessor().processTags(unsafeTags, this, restrictedSpan); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index c2ace4af8d1..df26487061a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -42,6 +42,10 @@ * 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}). + * + *

One more: an alias written before the span has a direction is resolved when the kind arrives, + * without passing through {@code TagInterceptor} again, so split-by-tags on its tag doesn't see it. + * An alias written once the direction is known is intercepted as its tag. */ final class DirectionalTags { 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 b5a2493b717..ebad3f8393b 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 @@ -9,6 +9,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.ListWriter; import java.util.concurrent.CountDownLatch; @@ -107,6 +108,32 @@ void removingATagBeforeTheKindIsKnownDropsAPendingAliasForIt() { assertNull(span.getTag(Tags.PEER_HOSTNAME)); } + /** + * A pending alias is folded into its tag as soon as the span learns its kind, not at + * serialization, so readers that run before then (peer service naming, stats) see the tag. + */ + @Test + void aPendingAliasIsResolvedAsSoonAsTheKindIsKnown() { + span.setTag("server.address", "db.example.com"); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + TagMap stored = span.getTags(); + assertEquals("db.example.com", stored.getObject(Tags.PEER_HOSTNAME)); + assertNull(stored.getObject("server.address")); + } + + /** A prototype's alias default must not overwrite an explicit value of the tag it denotes. */ + @Test + void aPrototypeAliasDefaultDoesNotClobberAnExplicitTag() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag(Tags.PEER_HOSTNAME, "explicit"); + + span.apply(SpanPrototype.builder().initTag("server.address", "from-prototype").build()); + + assertEquals("explicit", span.getTag(Tags.PEER_HOSTNAME)); + } + @Test void aBuilderTagWithASharedNameIsAKnownTag() { DDSpan built = 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 e6d088a99dc..3164432f90d 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 @@ -421,6 +421,24 @@ void splitByTagsSeparateWritesAreLastWriterWins() { assertEquals(DDTags.LANGUAGE_TAG_VALUE, span.getServiceName()); } + /** Split-by-tags on a tag also splits on its direction-scoped alias, by name or in bulk. */ + @Test + void splitByTagsSeesAnAliasAsTheTagItDenotes() { + CoreTracer tracer = createSplittingTracer(Tags.PEER_HOSTNAME); + + AgentSpan byName = tracer.buildSpan("datadog", "by name").start(); + byName.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + byName.setTag("server.address", "by-name-host"); + byName.finish(); + assertEquals("by-name-host", byName.getServiceName()); + + AgentSpan inBulk = tracer.buildSpan("datadog", "in bulk").start(); + inBulk.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + inBulk.setAllTags(Collections.singletonMap("server.address", "bulk-host")); + inBulk.finish(); + assertEquals("bulk-host", inBulk.getServiceName()); + } + @Test void setResourceName() throws Exception { ListWriter writer = new ListWriter(); From 6aa6d4f1e2ce52e6484bb30629484a85763fe689 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 00:12:11 -0400 Subject: [PATCH 50/99] Close three more gaps between the write paths and direction handling - Builder ledger removals go through DirectionalTags.remove, as removeTag does, so removing an alias also removes the tag a direction-aware write stored it as. - Split-by-tags that names an alias itself keeps that alias on the custom path, so the configured name still matches after aliases started routing as the tag they denote. - setSpanKindOrdinal checks for pending aliases under the lock alias registration takes, so a concurrent alias write can't be left unresolved. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 30 ++++++++++++++----- .../DDSpanDirectionalTagConsistencyTest.java | 19 ++++++++++++ .../taginterceptor/TagInterceptorTest.java | 21 +++++++++++++ 3 files changed, 62 insertions(+), 8 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 e27656b7cf6..c7d8b91429f 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 @@ -956,11 +956,13 @@ static boolean tagEquals(String tagValue, String tagLiteral) { */ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); - if (hasPendingAliases()) { - // The span now has a direction: fold pending aliases into the tags they denote, so every - // later reader (peer service naming, stats, sampling rules, serialization) sees them - // resolved. - synchronized (unsafeTags) { + // Checked under the lock alias registration takes: a writer that read no direction registers + // its pending alias before this sees the flag, or reads the new kind and resolves at write. + synchronized (unsafeTags) { + if (hasPendingAliases()) { + // The span now has a direction: fold pending aliases into the tags they denote, so every + // later reader (peer service naming, stats, sampling rules, serialization) sees them + // resolved. DirectionalTags.resolveScopedNames(unsafeTags, tagDirection()); } } @@ -1196,7 +1198,7 @@ public void removeTag(long tagId) { */ private long idForWrite(String tag) { long tagId = KnownTagCodec.keyOf(tag); - if (tagId == 0 && KnownTagCodec.mayBeScopedAlias(tag)) { + if (tagId == 0 && KnownTagCodec.mayBeScopedAlias(tag) && !splitsOnAliasName(tag)) { long aliasedId = KnownTagCodec.directionalKeyOf(tag, tagDirection()); if (aliasedId > 0) { return aliasedId; @@ -1205,13 +1207,24 @@ private long idForWrite(String tag) { return tagId; } + /** + * Whether split-by-tags names {@code alias} itself ({@code + * dd.trace.split-by-tags=server.address}). Such a write stays on the custom path, where the + * interceptor matches the configured name, instead of being routed as the tag the alias denotes. + */ + private boolean splitsOnAliasName(String alias) { + return tagInterceptor.needsInterceptCustomTag(alias); + } + /** * {@link TagInterceptor#interceptTag(DDSpanContext, TagMap.EntryReader)} for an entry about to be * stored through {@link #storeEntry}, which resolves a direction-scoped alias to its tag: such an * entry is intercepted as that tag, so routing sees the tag it is stored as. */ private boolean interceptEntry(TagMap.EntryReader entry) { - if (entry.tagId() == 0 && KnownTagCodec.mayBeScopedAlias(entry.tag())) { + if (entry.tagId() == 0 + && KnownTagCodec.mayBeScopedAlias(entry.tag()) + && !splitsOnAliasName(entry.tag())) { long aliasedId = KnownTagCodec.directionalKeyOf(entry.tag(), tagDirection()); if (aliasedId > 0) { return tagInterceptor.needsIntercept(aliasedId, (int) KnownTagCodec.INTERCEPTED) @@ -1539,7 +1552,8 @@ void setAllTags(final TagMap.Ledger ledger) { // mirror removeTag(String): keep the cached ordinal in sync with unsafeTags spanKindOrdinal = SPAN_KIND_UNSET; } - unsafeTags.remove(tag); + // as removeTag(String): a direction-dependent name and the tag it denotes go together + DirectionalTags.remove(unsafeTags, tag, tagDirection()); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; 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 ebad3f8393b..1fbb290d8b8 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 @@ -134,6 +134,25 @@ void aPrototypeAliasDefaultDoesNotClobberAnExplicitTag() { assertEquals("explicit", span.getTag(Tags.PEER_HOSTNAME)); } + /** A builder removal of an alias also removes the tag a direction-aware write stored it as. */ + @Test + void aBuilderRemovalOfAnAliasRemovesTheTagItWasStoredAs() { + DDSpan built = + (DDSpan) + tracer + .buildSpan("datadog", "built") + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) + .withTag("server.address", "host") + .withTag("server.address", (Object) null) + .start(); + try { + assertNull(built.getTag(Tags.PEER_HOSTNAME)); + assertNull(built.getTags().getObject(Tags.PEER_HOSTNAME)); + } finally { + built.finish(); + } + } + @Test void aBuilderTagWithASharedNameIsAKnownTag() { DDSpan built = 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 3164432f90d..a8211f74459 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 @@ -439,6 +439,27 @@ void splitByTagsSeesAnAliasAsTheTagItDenotes() { assertEquals("bulk-host", inBulk.getServiceName()); } + /** Split-by-tags that names the alias itself still splits on it, by name and from the builder. */ + @Test + void splitByTagsOnAnAliasNameStillSplitsOnIt() { + CoreTracer tracer = createSplittingTracer("server.address"); + + AgentSpan byName = tracer.buildSpan("datadog", "by name").start(); + byName.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + byName.setTag("server.address", "by-name-host"); + byName.finish(); + assertEquals("by-name-host", byName.getServiceName()); + + AgentSpan built = + tracer + .buildSpan("datadog", "built") + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) + .withTag("server.address", "built-host") + .start(); + built.finish(); + assertEquals("built-host", built.getServiceName()); + } + @Test void setResourceName() throws Exception { ListWriter writer = new ListWriter(); From 1c92947b1d31fdbcf339abaf2a4dd908d9e350d1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 08:37:20 -0400 Subject: [PATCH 51/99] Split TagMap into a view over an id store and a name store TagMap now holds its tags in two internal stores: an IdStore for known tags by id and a name store for custom tags by name. In hash mode, the only mode so far, one HashStore serves as both, so the split adds no object per tag. The bucket structure, read-through to frozen parents and removal tombstones move into HashStore unchanged; TagMap delegates to it. The public API and behavior are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/HashStore.java | 861 +++++++++++++++++ .../main/java/datadog/trace/api/IdStore.java | 44 + .../main/java/datadog/trace/api/TagMap.java | 881 +++--------------- 3 files changed, 1019 insertions(+), 767 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/HashStore.java create mode 100644 internal-api/src/main/java/datadog/trace/api/IdStore.java diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java new file mode 100644 index 00000000000..3b364db7554 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -0,0 +1,861 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.BucketGroup; +import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * TagMap's hash store: a fixed-size bucket array of shareable, immutable {@link Entry} objects, + * keyed by the 64-bit tag hash (a known tag's id, or a custom tag's name hash in the low 32 bits; + * the two never collide, see {@link Entry#tagHashOf}). + * + *

It is always a TagMap's name store, holding the custom tags. In hash mode it is also the map's + * {@link IdStore}, holding the known tags too -- one object serves both roles, so splitting the + * stores costs no extra allocation. No locking: guarded by the owner's lock. + * + *

For memory efficiency, a bucket holds a lone Entry directly. Because Entries are shared + * between maps, they cannot form a linked list to handle collisions; colliding entries share a + * fixed-size {@link BucketGroup} instead, chained when one fills. Removals are assumed rare, so + * groups are never consolidated, but an emptied group is unlinked from its chain. + */ +final class HashStore extends IdStore { + final Object[] buckets; + int size; + + /** + * Optional frozen parent store for read-through. When non-null, reads that miss the local buckets + * fall through to the parent chain, nearest-level-wins (a local entry shadows the parent's, a + * nearer ancestor shadows a farther one). The chain mirrors the owning TagMaps' parents. Not + * final only so {@link #clear()} can detach it (to null); it is otherwise fixed at construction + * and never re-pointed. + */ + HashStore parent; + + /** + * Parent keys removed locally (read-through tombstones), by canonical name. Lazily allocated on + * the first such removal; {@code null} both means "no tombstones" and serves as the gate that + * keeps the hot paths untouched. Only meaningful when {@link #parent} != null. A tombstone stops + * read-through fall-through for its key, so a key removed from a child no longer reads through to + * the parent. Kept off the bucket structure deliberately -- it is shape-agnostic (bare-Entry vs + * BucketGroup) and rare, so it costs a lazy allocation on removal rather than complicating the + * hot bucket code. + */ + private Set removedFromParent; + + /** A fresh, empty store reading through to {@code parent} (may be null). */ + HashStore(HashStore parent) { + // needs to be a power of 2 for bucket masking calculation to work as intended + this.buckets = new Object[1 << 4]; + this.size = 0; + this.parent = parent; + } + + /** A parentless store over {@code buckets}; reads no statics, so TagMap.EMPTY can use it. */ + HashStore(Object[] buckets, int size) { + this.buckets = buckets; + this.size = size; + this.parent = null; + } + + /** Exact count, including the visible part of the read-through chain. */ + int size() { + HashStore parent = this.parent; + return parent == null ? this.size : this.size + this.visibleParentCount(); + } + + /** + * Exact count of ancestor entries not shadowed by a nearer level or tombstoned (the read-through + * addition). Walks the ancestor chain iteratively, nearest-level-wins. + */ + private int visibleParentCount() { + int count = 0; + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object parentBucket = parentBuckets[i]; + if (parentBucket instanceof Entry) { + if (parentEntryVisible((Entry) parentBucket, ancestor)) count++; + } else if (parentBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) parentBucket; + curGroup != null; + curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry parentEntry = curGroup._entryAt(j); + if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) count++; + } + } + } + } + } + return count; + } + + boolean isEmpty() { + if (this.size != 0) { + return false; + } + HashStore parent = this.parent; + if (parent == null) { + return true; + } + if (this.removedFromParent == null) { + // A live parent is never definitely-empty: TagMap.createFromParent drops an empty parent (and + // copy() only forwards an already-vetted parent), and the parent is frozen so it can't become + // empty. So with no local entries and no tombstones, the parent contributes at least one + // visible entry -> this store is non-empty. + return false; + } + // size == 0 with tombstones (rare): empty iff every visible ancestor entry is tombstoned + return this.visibleParentCount() == 0; + } + + /** Cheap: empty iff no level in the chain holds a local entry (ignores shadowing/tombstones). */ + boolean isDefinitelyEmpty() { + for (HashStore level = this; level != null; level = level.parent) { + if (level.size != 0) { + return false; + } + } + return true; + } + + /** Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. */ + int estimateSize() { + int total = 0; + for (HashStore level = this; level != null; level = level.parent) { + total += level.size; + } + return total; + } + + /* + * Known tags, by id (the IdStore role). The id is validated as the TagMap id-keyed API requires. + */ + + @Override + Entry get(long tagId) { + return this.get(Entry.requireKnownName(tagId), tagId); + } + + @Override + Object getKnownValue(long tagId) { + Entry entry = this.get(tagId); + return entry == null ? null : entry.objectValue(); + } + + @Override + Entry remove(long tagId) { + return this.remove(Entry.requireKnownName(tagId), tagId); + } + + /* + * Custom tags, by name (the name-store role). The name must not be a known tag's: a caller + * resolves a name once and routes a known tag by its id instead (see TagRouting). + */ + + /** The custom tag {@code tag}'s entry, reading through to the parent chain. */ + Entry get(String tag) { + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; look it up by its id"; + return this.get(tag, Entry.customHash(tag)); + } + + /** Removes the custom tag {@code tag}; see {@link #remove(String, long)}. */ + Entry remove(String tag) { + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; remove it by its id"; + return this.remove(tag, Entry.customHash(tag)); + } + + /* + * Either kind, by the entry's own identity (its canonical name and tag hash). + */ + + /** The entry for {@code (canonicalTag, tagHash)}: local first, then the read-through chain. */ + private Entry get(String canonicalTag, long tagHash) { + Entry local = this.getLocal(canonicalTag, tagHash); + if (local != null) { + // Local entry shadows the parent (local-wins) -- unchanged hot path. + return local; + } + // Read-through: miss locally, defer to the frozen parent. + // The tombstone check lives only here, on the cold miss+parent path -- the hot local hit above + // never touches it. + HashStore parent = this.parent; + if (parent == null) { + return null; + } + if (this.removedFromParent != null && this.removedFromParent.contains(canonicalTag)) { + return null; // tombstoned: removed locally, do not read through + } + return parent.get(canonicalTag, tagHash); + } + + /** + * Inserts or replaces a local entry, returning the replaced local Entry (or null if none). Does + * NOT consult the read-through parent -- set paths use this so they never pay for a prior-value + * lookup they discard; {@link #getAndPut(Entry)} layers the parent fallback on top. + */ + @Override + Entry put(Entry newEntry) { + // 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) { + this.removedFromParent.remove(newEntry.tag); + } + + Object[] thisBuckets = this.buckets; + + int newHash = newEntry.hash(); + int bucketIndex = newHash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + if (bucket == null) { + thisBuckets[bucketIndex] = newEntry; + + this.size += 1; + return null; + } else if (bucket instanceof Entry) { + Entry existingEntry = (Entry) bucket; + if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { + thisBuckets[bucketIndex] = newEntry; + + // replaced existing entry - no size change + return existingEntry; + } else { + thisBuckets[bucketIndex] = + new BucketGroup(existingEntry.hash(), existingEntry, newHash, newEntry); + + this.size += 1; + return null; + } + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + BucketGroup containingGroup = lastGroup.findContainingGroupInChain(newHash, newEntry.tag); + if (containingGroup != null) { + // replaced existing entry - no size change + return containingGroup._replace(newHash, newEntry); + } + + if (!lastGroup.insertInChain(newHash, newEntry)) { + thisBuckets[bucketIndex] = new BucketGroup(newHash, newEntry, lastGroup); + } + this.size += 1; + return null; + } + + // unreachable + return null; + } + + /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ + @Override + Entry getAndPut(Entry newEntry) { + // Capture whether the key was tombstoned BEFORE put clears it: a tombstoned key had no visible + // prior value (it was removed), so getAndPut must report null rather than the parent's. + boolean wasTombstoned = + this.removedFromParent != null && this.removedFromParent.contains(newEntry.tag); + + Entry priorLocal = this.put(newEntry); + if (priorLocal != null) { + return priorLocal; // replaced a local entry -> that is the prior value + } + // No local entry was replaced. The prior visible value, if any, was the parent's -- unless the + // key was tombstoned (then it was not visible). + if (wasTombstoned || this.parent == null) { + return null; + } + // By the entry's own identity, not its name: the name would only resolve back to the same hash. + return this.parent.get(newEntry.tag, newEntry.tagHash); + } + + /** + * Removes {@code (canonicalTag, tagHash)}, returning the prior visible entry. Removing a key the + * parent chain still exposes also installs a tombstone, so it stops reading through. + */ + private Entry remove(String canonicalTag, long tagHash) { + Entry localRemoved = this.removeLocal(canonicalTag, tagHash); + + HashStore parent = this.parent; + if (parent != null) { + // Read-through: if the parent still exposes this key, removing it must also hide it from + // fall-through -- install a tombstone. The prior *visible* value (Map.remove contract) is the + // local entry if there was one, otherwise the parent's (which we now hide). Rare path (only + // when removing a parent-exposed key). + boolean alreadyTombstoned = + this.removedFromParent != null && this.removedFromParent.contains(canonicalTag); + if (!alreadyTombstoned) { + Entry parentEntry = parent.get(canonicalTag, tagHash); + if (parentEntry != null) { + if (this.removedFromParent == null) { + // Small initial capacity: this set is rare and almost always holds only a handful of + // tombstoned keys, so the default 16-bucket HashSet table would be wasteful. + this.removedFromParent = new HashSet<>(4); + } + this.removedFromParent.add(canonicalTag); + return localRemoved != null ? localRemoved : parentEntry; + } + } + } + return localRemoved; + } + + /** + * Whether an entry that lives at ancestor level {@code fromAncestor} is visible through this + * leaf: not shadowed and not tombstoned by any nearer level (the leaf, or a closer ancestor). + * Nearest-level-wins. This mirrors what {@link #get(String, long)}'s recursion applies as it + * descends -- each nearer level contributes both its own local entries (shadowing) and its own + * read-through removals (tombstones). A non-leaf level can carry tombstones too: a map may remove + * an inherited key and then itself be frozen and reused as a parent. Exploits universal hashing + * -- the only local entry that could shadow {@code parentEntry} at a level is in 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). + */ + boolean parentEntryVisible(Entry parentEntry, HashStore fromAncestor) { + int hash = parentEntry.hash(); + String tag = parentEntry.tag; + for (HashStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { + // tombstoned by a nearer level (its own read-through removal hides the deeper entry) + if (nearer.removedFromParent != null && nearer.removedFromParent.contains(tag)) { + return false; + } + // shadowed by a nearer level's local entry + Object[] nearerBuckets = nearer.buckets; + Object nearerBucket = nearerBuckets[hash & (nearerBuckets.length - 1)]; + if (findInBucket(nearerBucket, hash, tag) != null) { + return false; + } + } + return true; + } + + /** Looks up an entry in this store's own buckets only -- no read-through to the parent. */ + private Entry getLocal(String tag, long tagHash) { + Object[] thisBuckets = this.buckets; + int hash = Entry.bucketHash(tagHash); + return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag); + } + + /** + * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). + */ + private static Entry findInBucket(Object bucket, int hash, String tag) { + if (bucket instanceof Entry) { + Entry tagEntry = (Entry) bucket; + return tagEntry.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; + } else if (bucket instanceof BucketGroup) { + return ((BucketGroup) bucket).findInChain(hash, tag); + } + return null; + } + + /** Removes an entry from this store's own buckets only -- no parent/tombstone handling. */ + private Entry removeLocal(String tag, long tagHash) { + Object[] thisBuckets = this.buckets; + + int hash = Entry.bucketHash(tagHash); + int bucketIndex = hash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + // null bucket case - do nothing + if (bucket instanceof Entry) { + Entry existingEntry = (Entry) bucket; + if (existingEntry.hash() == hash && existingEntry.matches(tag)) { + thisBuckets[bucketIndex] = null; + + this.size -= 1; + return existingEntry; + } else { + return null; + } + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + BucketGroup containingGroup = lastGroup.findContainingGroupInChain(hash, tag); + if (containingGroup == null) { + return null; + } + + Entry existingEntry = containingGroup._remove(hash, tag); + if (containingGroup._isEmpty()) { + thisBuckets[bucketIndex] = lastGroup.removeGroupInChain(containingGroup); + } + + this.size -= 1; + return existingEntry; + } + return null; + } + + /** + * An independently mutable copy: the same (frozen, shared) parent, this store's own entries + * (shared Entry objects) and its tombstones. + */ + HashStore copy() { + HashStore copy = new HashStore(this.parent); + copy.putAllIntoEmpty(this); + if (this.removedFromParent != null) { + copy.removedFromParent = new HashSet<>(this.removedFromParent); + } + return copy; + } + + /** + * Copies {@code that}'s own entries into this store, sharing the Entry objects. Since stores + * don't support expansion, buckets are perfectly aligned. + */ + void putAllLocal(HashStore that) { + if (this.size == 0) { + this.putAllIntoEmpty(that); + } else { + this.putAllMerge(that); + } + } + + private void putAllMerge(HashStore that) { + Object[] thisBuckets = this.buckets; + Object[] thatBuckets = that.buckets; + + // Since TagMap-s don't support expansion, buckets are perfectly aligned + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check + // elimination + for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { + Object thatBucket = thatBuckets[i]; + + // if nothing incoming, nothing to do + if (thatBucket == null) continue; + + Object thisBucket = thisBuckets[i]; + if (thisBucket == null) { + // This bucket is null, easy case + // Either copy over the sole entry or clone the BucketGroup chain + + if (thatBucket instanceof Entry) { + thisBuckets[i] = thatBucket; + this.size += 1; + } else if (thatBucket instanceof BucketGroup) { + BucketGroup thatGroup = (BucketGroup) thatBucket; + + BucketGroup thisNewGroup = thatGroup.cloneChain(); + thisBuckets[i] = thisNewGroup; + this.size += thisNewGroup.sizeInChain(); + } + } else if (thisBucket instanceof Entry) { + // This bucket is a single entry, medium complexity case + // If other side is an Entry - just merge the entries into a bucket + // If other side is a BucketGroup - then clone the group and insert the entry normally into + // the cloned group + + Entry thisEntry = (Entry) thisBucket; + int thisHash = thisEntry.hash(); + + if (thatBucket instanceof Entry) { + Entry thatEntry = (Entry) thatBucket; + int thatHash = thatEntry.hash(); + + if (thisHash == thatHash && thisEntry.matches(thatEntry.tag())) { + thisBuckets[i] = thatEntry; + // replacing entry, no size change + } else { + thisBuckets[i] = + new BucketGroup( + thisHash, thisEntry, + thatHash, thatEntry); + this.size += 1; + } + } else if (thatBucket instanceof BucketGroup) { + BucketGroup thatGroup = (BucketGroup) thatBucket; + + // Clone the other group, then place this entry into that group + BucketGroup thisNewGroup = thatGroup.cloneChain(); + int thisNewGroupSize = thisNewGroup.sizeInChain(); + + Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); + if (incomingEntry != null) { + // there's already an entry w/ the same tag from the incoming TagMap + // incoming entry clobbers the existing try, so we're done + thisBuckets[i] = thisNewGroup; + + // overlapping group - subtract one for clobbered existing entry + this.size += thisNewGroupSize - 1; + } else if (thisNewGroup.insertInChain(thisHash, thisEntry)) { + // able to add thisEntry into the existing groups + thisBuckets[i] = thisNewGroup; + + // non overlapping group - existing entry already accounted for in this.size + this.size += thisNewGroupSize; + } else { + // unable to add into the existing groups + thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup); + + // non overlapping group - existing entry already accounted for in this.size + this.size += thisNewGroupSize; + } + } + } else if (thisBucket instanceof BucketGroup) { + // This bucket is a BucketGroup, medium to hard case + // If the other side is an entry, just normal insertion procedure - no cloning required + BucketGroup thisGroup = (BucketGroup) thisBucket; + + if (thatBucket instanceof Entry) { + Entry thatEntry = (Entry) thatBucket; + int thatHash = thatEntry.hash(); + + if (thisGroup.replaceInChain(thatHash, thatEntry) != null) { + // replaced existing entry no size change + } else if (thisGroup.insertInChain(thatHash, thatEntry)) { + this.size += 1; + } else { + thisBuckets[i] = new BucketGroup(thatHash, thatEntry, thisGroup); + this.size += 1; + } + } else if (thatBucket instanceof BucketGroup) { + // Most complicated case - need to walk that bucket group chain and update this chain + BucketGroup thatGroup = (BucketGroup) thatBucket; + + // Taking the easy / expensive way out for updating size + int thisPrevGroupSize = thisGroup.sizeInChain(); + + BucketGroup thisNewGroup = thisGroup.replaceOrInsertAllInChain(thatGroup); + int thisNewGroupSize = thisNewGroup.sizeInChain(); + + thisBuckets[i] = thisNewGroup; + this.size += (thisNewGroupSize - thisPrevGroupSize); + } + } + } + } + + /* + * Specially optimized version of putAll for the common case of destination store being empty + */ + void putAllIntoEmpty(HashStore that) { + Object[] thisBuckets = this.buckets; + Object[] thatBuckets = that.buckets; + + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check + // elimination + for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { + Object thatBucket = thatBuckets[i]; + + // faster to explicitly null check first, then do instanceof + if (thatBucket == null) { + // do nothing + } else if (thatBucket instanceof BucketGroup) { + // if it is a BucketGroup, then need to clone + BucketGroup thatGroup = (BucketGroup) thatBucket; + + thisBuckets[i] = thatGroup.cloneChain(); + } else { // if ( thatBucket instanceof Entry ) + thisBuckets[i] = thatBucket; + } + } + this.size = that.size; + } + + void fillMapLocal(Map map) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + map.put(thisEntry.tag, thisEntry.objectValue()); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.fillMapFromChain(map); + } + } + } + + void fillStringMapLocal(Map stringMap) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + stringMap.put(thisEntry.tag, thisEntry.stringValue()); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.fillStringMapFromChain(stringMap); + } + } + } + + /** Visits the local entries, then the visible read-through entries. */ + void forEach(Consumer consumer) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + consumer.accept(thisEntry); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.forEachInChain(consumer); + } + } + + // read-through: parent entries not shadowed locally or tombstoned. Kept out of line so the + // common parent == null path stays small / inlinable. + if (this.parent != null) { + this.forEachParent(consumer); + } + } + + private void forEachParent(Consumer consumer) { + // Walk the ancestor chain, nearest first. Each entry is emitted once, by the nearest level that + // defines its key, when not shadowed by a nearer level and not tombstoned. + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object parentBucket = parentBuckets[i]; + if (parentBucket instanceof Entry) { + Entry parentEntry = (Entry) parentBucket; + if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(parentEntry); + } else if (parentBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) parentBucket; + curGroup != null; + curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry parentEntry = curGroup._entryAt(j); + if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) + consumer.accept(parentEntry); + } + } + } + } + } + } + + void forEach(T thisObj, BiConsumer consumer) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + consumer.accept(thisObj, thisEntry); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.forEachInChain(thisObj, consumer); + } + } + + // read-through: parent entries not shadowed locally or tombstoned (kept out of line). + if (this.parent != null) { + this.forEachParent(thisObj, consumer); + } + } + + private void forEachParent(T thisObj, BiConsumer consumer) { + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object parentBucket = parentBuckets[i]; + if (parentBucket instanceof Entry) { + Entry parentEntry = (Entry) parentBucket; + if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(thisObj, parentEntry); + } else if (parentBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) parentBucket; + curGroup != null; + curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry parentEntry = curGroup._entryAt(j); + if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { + consumer.accept(thisObj, parentEntry); + } + } + } + } + } + } + } + + void forEach(T thisObj, U otherObj, TriConsumer consumer) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + consumer.accept(thisObj, otherObj, thisEntry); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.forEachInChain(thisObj, otherObj, consumer); + } + } + + // read-through: parent entries not shadowed locally or tombstoned (kept out of line). + if (this.parent != null) { + this.forEachParent(thisObj, otherObj, consumer); + } + } + + private void forEachParent( + T thisObj, U otherObj, TriConsumer consumer) { + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object parentBucket = parentBuckets[i]; + if (parentBucket instanceof Entry) { + Entry parentEntry = (Entry) parentBucket; + if (parentEntryVisible(parentEntry, ancestor)) + consumer.accept(thisObj, otherObj, parentEntry); + } else if (parentBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) parentBucket; + curGroup != null; + curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry parentEntry = curGroup._entryAt(j); + if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { + consumer.accept(thisObj, otherObj, parentEntry); + } + } + } + } + } + } + } + + /** + * Removes every entry, including any inherited through read-through. Detaching the parent (rather + * than tombstoning every inherited key) is simpler and cheaper, and leaves an empty, parent-less + * store. Detach is one-way -- the parent is never re-pointed. + */ + void clear() { + Arrays.fill(this.buckets, null); + this.size = 0; + this.parent = null; + this.removedFromParent = null; + } + + void checkIntegrity() { + // Decided to use if ( cond ) throw new IllegalStateException rather than assert + // That was done to avoid the extra static initialization needed for an assertion + // While that's probably an unnecessary optimization, this method is only called in tests + + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + int thisHash = thisEntry.hash(); + + int expectedBucket = thisHash & (thisBuckets.length - 1); + if (expectedBucket != i) { + throw new IllegalStateException("incorrect bucket"); + } + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry thisEntry = curGroup._entryAt(i); + if (thisEntry == null) continue; + + int thisHash = thisEntry.hash(); + assert curGroup._hashAt(i) == thisHash; + + int expectedBucket = thisHash & (thisBuckets.length - 1); + if (expectedBucket != i) { + throw new IllegalStateException("incorrect bucket"); + } + } + } + } + } + + if (this.size != this.computeSize()) { + throw new IllegalStateException("incorrect size"); + } + // Local-structure invariant: the size counter's emptiness must match the local buckets. + if ((this.size == 0) != this.checkIfEmpty()) { + throw new IllegalStateException("incorrect empty status"); + } + } + + int computeSize() { + Object[] thisBuckets = this.buckets; + + int size = 0; + for (int i = 0; i < thisBuckets.length; ++i) { + Object curBucket = thisBuckets[i]; + + if (curBucket instanceof Entry) { + size += 1; + } else if (curBucket instanceof BucketGroup) { + BucketGroup curGroup = (BucketGroup) curBucket; + size += curGroup.sizeInChain(); + } + } + return size; + } + + boolean checkIfEmpty() { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object curBucket = thisBuckets[i]; + + if (curBucket instanceof Entry) { + return false; + } else if (curBucket instanceof BucketGroup) { + BucketGroup curGroup = (BucketGroup) curBucket; + if (!curGroup.isEmptyChain()) return false; + } + } + + return true; + } + + /** The bucket layout, for deep debugging. */ + String toInternalString() { + Object[] thisBuckets = this.buckets; + + StringBuilder ledger = new StringBuilder(128); + for (int i = 0; i < thisBuckets.length; ++i) { + ledger.append('[').append(i).append("] = "); + + Object thisBucket = thisBuckets[i]; + if (thisBucket == null) { + ledger.append("null"); + } else if (thisBucket instanceof Entry) { + ledger.append('{').append(thisBucket).append('}'); + } else if (thisBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) thisBucket; + curGroup != null; + curGroup = curGroup.prev) { + ledger.append(curGroup).append(" -> "); + } + } + ledger.append('\n'); + } + return ledger.toString(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java new file mode 100644 index 00000000000..51542ebb081 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -0,0 +1,44 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.Entry; + +/** + * The known-tag half of a {@link TagMap}: tags keyed by their {@link KnownTags} id. Every method + * takes an id or an Entry already built from one -- never a name, so nothing here can be handed a + * name that a caller has not yet resolved. + * + *

Each implementation is its own class, chosen when the owning TagMap is built. In hash mode the + * IdStore is the {@link HashStore} that also holds the custom tags, so the split costs no extra + * object. No locking: guarded by the owner's lock. + */ +abstract class IdStore { + /** + * The entry for the known tag {@code tagId}, reading through to the parent chain, or null. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + abstract Entry get(long tagId); + + /** + * {@code get(tagId)}'s value, boxed if necessary, or null -- for a caller that needs only the + * value, so a store that keeps known tags without an Entry per tag can answer without one. + */ + abstract Object getKnownValue(long tagId); + + /** + * Inserts or replaces a known tag's entry, returning the local entry it replaced (or null). Does + * not consult the read-through parent. + */ + abstract Entry put(Entry knownEntry); + + /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ + abstract Entry getAndPut(Entry knownEntry); + + /** + * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is + * hidden by a tombstone. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + abstract Entry remove(long 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 0f1f71fc8bd..58b974b0739 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -9,7 +9,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; @@ -1197,50 +1196,34 @@ public EntryChange next() { } /* - * For memory efficiency, TagMap uses a rather complicated bucket system. - *

- * When there is only a single Entry in a particular bucket, the Entry is stored into the bucket directly. - *

- * Because the Entry objects can be shared between multiple TagMaps, the Entry objects cannot - * directly form a linked list to handle collisions. - *

- * Instead when multiple entries collide in the same bucket, a BucketGroup is formed to hold multiple entries. - * But a BucketGroup is only formed when a collision occurs to keep allocation low in the common case of no collisions. - *

- * For efficiency, BucketGroups are a fixed size, so when a BucketGroup fills up another BucketGroup is formed - * to hold the additional Entry-s. And the BucketGroup-s are connected via a linked list instead of the Entry-s. - *

- * This does introduce some inefficiencies when Entry-s are removed. - * The assumption is that removals are rare, so BucketGroups are never consolidated. - * However as a precaution if a BucketGroup becomes completely empty, then that BucketGroup will be - * removed from the collision chain. + * A TagMap is a view over a pair of stores: an IdStore holding the known tags by id, and a name + * store holding the custom tags by name. Their key spaces are disjoint, so each store reads + * through to its own parent chain and keeps its own tombstones, and precedence never crosses + * stores. + * + * In hash mode -- the only mode so far -- one HashStore serves as both (ids == names), so the + * split costs no extra object. A whole-map operation (size, iteration, copy, putAll, clear) runs + * on that one store and so covers both halves; a separate IdStore implementation would have to + * visit ids as well. */ - final Object[] buckets; - private int size; + /** The known tags, by id. In hash mode the same object as {@link #names}. */ + final IdStore ids; + + /** The custom tags, by name; in hash mode it holds the known tags too. */ + final HashStore names; + private boolean frozen; /** - * Optional frozen parent for read-through. When non-null, reads that miss the local buckets fall - * through to the parent chain, nearest-level-wins (a local entry shadows the parent's, a nearer - * ancestor shadows a farther one). Parents may themselves have parents, so this is a chain (e.g. - * baggage layered over trace tags). Must be frozen when attached, so it is safely shareable. Not - * final only so {@link #clear()} can detach it (to null); it is otherwise fixed at construction - * and never re-pointed. Package-visible so same-package tests can assert attach/detach directly. + * Optional frozen parent for read-through. Reads that miss locally fall through to the parent + * chain (each store reads through to the parent's matching store); see {@link HashStore#parent}. + * Must be frozen when attached, so it is safely shareable. Not final only so {@link #clear()} can + * detach it (to null); it is otherwise fixed at construction and never re-pointed. + * Package-visible so same-package tests can assert attach/detach directly. */ @VisibleForTesting TagMap parent; - /** - * Parent keys removed locally (read-through tombstones). Lazily allocated on the first such - * removal; {@code null} both means "no tombstones" and serves as the gate that keeps the hot - * paths untouched. Only meaningful when {@link #parent} != null. A tombstone stops read-through - * fall-through for its key, so a key removed from a child no longer reads through to the parent. - * Kept off the bucket structure deliberately — it is shape-agnostic (bare-Entry vs BucketGroup) - * and rare, so it costs a lazy allocation on removal rather than complicating the hot bucket - * code. - */ - private Set removedFromParent; - public TagMap() { this((TagMap) null); } @@ -1251,17 +1234,25 @@ public TagMap() { * optimizations can treat it as fixed. */ private TagMap(TagMap parent) { - // needs to be a power of 2 for bucket masking calculation to work as intended - this.buckets = new Object[1 << 4]; - this.size = 0; + this(parent, new HashStore(parent == null ? null : parent.names)); + } + + /** + * A mutable map over {@code store}, which reads through to {@code parent}'s store. Where the + * store mode is chosen: hash mode uses the one store for both halves. + */ + private TagMap(TagMap parent, HashStore store) { + this.ids = store; + this.names = store; this.frozen = false; this.parent = parent; } /** Used for inexpensive immutable */ private TagMap(Object[] buckets, int size) { - this.buckets = buckets; - this.size = size; + HashStore store = new HashStore(buckets, size); + this.ids = store; + this.names = store; this.frozen = true; this.parent = null; } @@ -1269,75 +1260,23 @@ private TagMap(Object[] buckets, int size) { @Override public int size() { // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. - TagMap parent = this.parent; - return parent == null ? this.size : this.size + this.visibleParentCount(); - } - - /** - * Exact count of ancestor entries not shadowed by a nearer level or tombstoned (the read-through - * addition). Walks the ancestor chain iteratively, nearest-level-wins. - */ - private int visibleParentCount() { - int count = 0; - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - if (parentEntryVisible((Entry) parentBucket, ancestor)) count++; - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) count++; - } - } - } - } - } - return count; + return this.names.size(); } @Override public boolean isEmpty() { // Exact (Map contract). Under read-through resolves the parent; prefer isDefinitelyEmpty(). - if (this.size != 0) { - return false; - } - TagMap parent = this.parent; - if (parent == null) { - return true; - } - if (this.removedFromParent == null) { - // A live parent is never definitely-empty: createFromParent drops an empty parent (and copy() - // only forwards an already-vetted parent), and the parent is frozen so it can't become empty. - // So with no local entries and no tombstones, the parent contributes at least one visible - // entry -> this map is non-empty. - return false; - } - // size == 0 with tombstones (rare): empty iff every visible ancestor entry is tombstoned - return this.visibleParentCount() == 0; + return this.names.isEmpty(); } public boolean isDefinitelyEmpty() { // Cheap: empty iff no level in the chain holds a local entry (ignores shadowing/tombstones). - for (TagMap level = this; level != null; level = level.parent) { - if (level.size != 0) { - return false; - } - } - return true; + return this.names.isDefinitelyEmpty(); } public int estimateSize() { // Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. - int total = 0; - for (TagMap level = this; level != null; level = level.parent) { - total += level.size; - } - return total; + return this.names.estimateSize(); } @Deprecated @@ -1446,85 +1385,14 @@ public Set> entrySet() { 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. + // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. long tagId = KnownTagCodec.keyOf(tag); - return tagId == 0 - ? this.getEntry(tag, Entry.customHash(tag)) - : this.getEntry(KnownTagCodec.nameOf(tagId), tagId); + return tagId == 0 ? this.names.get(tag) : this.ids.get(tagId); } /** The entry for a known tag id. */ 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; - } - // Read-through: miss locally, defer to the frozen parent. Single-parent in phase 1. - // The tombstone check lives only here, on the cold miss+parent path — the hot local hit above - // never touches it. - TagMap parent = this.parent; - if (parent == null) { - return null; - } - if (this.removedFromParent != null && this.removedFromParent.contains(canonicalTag)) { - return null; // tombstoned: removed locally, do not read through - } - 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, long tagHash) { - Object[] thisBuckets = this.buckets; - int hash = Entry.bucketHash(tagHash); - return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag); - } - - /** - * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). - */ - private static Entry findInBucket(Object bucket, int hash, String tag) { - if (bucket instanceof Entry) { - Entry tagEntry = (Entry) bucket; - return tagEntry.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; - } else if (bucket instanceof BucketGroup) { - return ((BucketGroup) bucket).findInChain(hash, tag); - } - return null; - } - - /** - * Whether an entry that lives at ancestor level {@code fromAncestor} is visible through this - * leaf: not shadowed and not tombstoned by any nearer level (the leaf, or a closer ancestor). - * Nearest-level-wins. This mirrors what {@link #getEntry(String)}'s recursion applies as it - * descends -- each nearer level contributes both its own local entries (shadowing) and its own - * read-through removals (tombstones). A non-leaf level can carry tombstones too: a map may remove - * an inherited key and then itself be frozen and reused as a parent. Exploits universal hashing — - * by {@code _hash} the only local entry that could shadow {@code parentEntry} at a level is in - * 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). - */ - boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { - int hash = parentEntry.hash(); - String tag = parentEntry.tag; - for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - // tombstoned by a nearer level (its own read-through removal hides the deeper entry) - if (nearer.removedFromParent != null && nearer.removedFromParent.contains(tag)) { - return false; - } - // shadowed by a nearer level's local entry - Object[] nearerBuckets = nearer.buckets; - Object nearerBucket = nearerBuckets[hash & (nearerBuckets.length - 1)]; - if (findInBucket(nearerBucket, hash, tag) != null) { - return false; - } - } - return true; + return this.ids.get(tagId); } @Deprecated @@ -1592,7 +1460,7 @@ public void set(@Nonnull String tag, @Nonnull Object value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newAnyEntry(tag, value)); + this.putCustom(Entry.newAnyEntry(tag, value)); } } @@ -1601,7 +1469,7 @@ public void set(@Nonnull String tag, @Nonnull CharSequence value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newObjectEntry(tag, value)); + this.putCustom(Entry.newObjectEntry(tag, value)); } } @@ -1610,7 +1478,7 @@ public void set(@Nonnull String tag, boolean value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newBooleanEntry(tag, value)); + this.putCustom(Entry.newBooleanEntry(tag, value)); } } @@ -1619,7 +1487,7 @@ public void set(@Nonnull String tag, int value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newIntEntry(tag, value)); + this.putCustom(Entry.newIntEntry(tag, value)); } } @@ -1628,7 +1496,7 @@ public void set(@Nonnull String tag, long value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newLongEntry(tag, value)); + this.putCustom(Entry.newLongEntry(tag, value)); } } @@ -1637,7 +1505,7 @@ public void set(@Nonnull String tag, float value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newFloatEntry(tag, value)); + this.putCustom(Entry.newFloatEntry(tag, value)); } } @@ -1646,7 +1514,7 @@ public void set(@Nonnull String tag, double value) { if (tagId != 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newDoubleEntry(tag, value)); + this.putCustom(Entry.newDoubleEntry(tag, value)); } } @@ -1660,7 +1528,7 @@ public void setResolved(long tagId, @Nonnull String tag, @Nonnull Object value) if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newAnyEntry(tag, value)); + this.putCustom(Entry.newAnyEntry(tag, value)); } } @@ -1668,7 +1536,7 @@ public void setResolved(long tagId, @Nonnull String tag, @Nonnull CharSequence v if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newObjectEntry(tag, value)); + this.putCustom(Entry.newObjectEntry(tag, value)); } } @@ -1676,7 +1544,7 @@ public void setResolved(long tagId, @Nonnull String tag, boolean value) { if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newBooleanEntry(tag, value)); + this.putCustom(Entry.newBooleanEntry(tag, value)); } } @@ -1684,7 +1552,7 @@ public void setResolved(long tagId, @Nonnull String tag, int value) { if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newIntEntry(tag, value)); + this.putCustom(Entry.newIntEntry(tag, value)); } } @@ -1692,7 +1560,7 @@ public void setResolved(long tagId, @Nonnull String tag, long value) { if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newLongEntry(tag, value)); + this.putCustom(Entry.newLongEntry(tag, value)); } } @@ -1700,7 +1568,7 @@ public void setResolved(long tagId, @Nonnull String tag, float value) { if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newFloatEntry(tag, value)); + this.putCustom(Entry.newFloatEntry(tag, value)); } } @@ -1708,20 +1576,20 @@ public void setResolved(long tagId, @Nonnull String tag, double value) { if (tagId > 0) { this.set(tagId, value); } else { - this.putEntry(Entry.newDoubleEntry(tag, value)); + this.putCustom(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)); + return tagId > 0 ? this.ids.get(tagId) : this.names.get(tag); } /** The removal counterpart of {@link #setResolved(long, String, Object)}. */ public Entry removeResolved(long tagId, String tag) { this.checkWriteAccess(); - return tagId > 0 ? this.getAndRemove(tagId) : this.getAndRemove(tag, Entry.customHash(tag)); + return tagId > 0 ? this.ids.remove(tagId) : this.names.remove(tag); } /* @@ -1755,33 +1623,33 @@ public static final class Internals { * already checked KnownTagCodec.isKnown(tagId). */ public void setKnown(long tagId, @Nonnull Object value) { - this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); + this.map.putKnown(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)); + this.map.putKnown(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); } public void setKnown(long tagId, boolean value) { - this.map.putEntry( + this.map.putKnown( 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)); + this.map.putKnown(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)); + this.map.putKnown(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)); + this.map.putKnown(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)); + this.map.putKnown(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } /** @@ -1796,36 +1664,35 @@ public Object getKnownValue(long tagId) { /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ Object getKnownValue(long tagId) { - Entry entry = this.getEntry(tagId); - return entry == null ? null : entry.objectValue(); + return this.ids.getKnownValue(tagId); } public void set(long tagId, @Nonnull Object value) { - this.putEntry(Entry.newAnyEntry(tagId, value)); + this.putKnown(Entry.newAnyEntry(tagId, value)); } public void set(long tagId, @Nonnull CharSequence value) { - this.putEntry(Entry.newObjectEntry(tagId, value)); + this.putKnown(Entry.newObjectEntry(tagId, value)); } public void set(long tagId, boolean value) { - this.putEntry(Entry.newBooleanEntry(tagId, value)); + this.putKnown(Entry.newBooleanEntry(tagId, value)); } public void set(long tagId, int value) { - this.putEntry(Entry.newIntEntry(tagId, value)); + this.putKnown(Entry.newIntEntry(tagId, value)); } public void set(long tagId, long value) { - this.putEntry(Entry.newLongEntry(tagId, value)); + this.putKnown(Entry.newLongEntry(tagId, value)); } public void set(long tagId, float value) { - this.putEntry(Entry.newFloatEntry(tagId, value)); + this.putKnown(Entry.newFloatEntry(tagId, value)); } public void set(long tagId, double value) { - this.putEntry(Entry.newDoubleEntry(tagId, value)); + this.putKnown(Entry.newDoubleEntry(tagId, value)); } /** @@ -1838,22 +1705,9 @@ public Entry getAndSet(@Nullable Entry newEntry) { if (newEntry == null) { return null; } - // Capture whether the key was tombstoned BEFORE putEntry clears it: a tombstoned key had no - // visible prior value (it was removed), so getAndSet must report null rather than the parent's. - boolean wasTombstoned = - this.removedFromParent != null && this.removedFromParent.contains(newEntry.tag); + this.checkWriteAccess(); - Entry priorLocal = this.putEntry(newEntry); - if (priorLocal != null) { - return priorLocal; // replaced a local entry -> that is the prior value - } - // No local entry was replaced. The prior visible value, if any, was the parent's -- unless the - // key was tombstoned (then it was not visible). set(...) skips this via putEntry (no prior). - if (wasTombstoned || this.parent == null) { - return null; - } - // By the entry's own identity, not its name: the name would only resolve back to the same hash. - return this.parent.getEntry(newEntry.tag, newEntry.tagHash); + return newEntry.tagId() != 0 ? this.ids.getAndPut(newEntry) : this.names.getAndPut(newEntry); } /** @@ -1865,55 +1719,21 @@ public Entry getAndSet(@Nullable 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 - // removal). Gated on the lazy field, so this is a no-op for the common no-tombstone case. - if (this.removedFromParent != null) { - this.removedFromParent.remove(newEntry.tag); - } - - Object[] thisBuckets = this.buckets; - - int newHash = newEntry.hash(); - int bucketIndex = newHash & (thisBuckets.length - 1); - - Object bucket = thisBuckets[bucketIndex]; - if (bucket == null) { - thisBuckets[bucketIndex] = newEntry; - - this.size += 1; - return null; - } else if (bucket instanceof Entry) { - Entry existingEntry = (Entry) bucket; - if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { - thisBuckets[bucketIndex] = newEntry; - - // replaced existing entry - no size change - return existingEntry; - } else { - thisBuckets[bucketIndex] = - new BucketGroup(existingEntry.hash(), existingEntry, newHash, newEntry); + return newEntry.tagId() != 0 ? this.ids.put(newEntry) : this.names.put(newEntry); + } - this.size += 1; - return null; - } - } else if (bucket instanceof BucketGroup) { - BucketGroup lastGroup = (BucketGroup) bucket; + /** {@link #putEntry} for an entry known to be a known tag's. */ + void putKnown(Entry knownEntry) { + this.checkWriteAccess(); - BucketGroup containingGroup = lastGroup.findContainingGroupInChain(newHash, newEntry.tag); - if (containingGroup != null) { - // replaced existing entry - no size change - return containingGroup._replace(newHash, newEntry); - } + this.ids.put(knownEntry); + } - if (!lastGroup.insertInChain(newHash, newEntry)) { - thisBuckets[bucketIndex] = new BucketGroup(newHash, newEntry, lastGroup); - } - this.size += 1; - return null; - } + /** {@link #putEntry} for an entry known to be a custom tag's. */ + void putCustom(Entry customEntry) { + this.checkWriteAccess(); - // unreachable - return null; + this.names.put(customEntry); } public Entry getAndSet(@Nonnull String tag, Object value) { @@ -1976,158 +1796,13 @@ public void putAll(TagMap that) { private void putAllOptimizedMap(TagMap that) { if (that.parent != null) { - // read-through source: the bucket-copy paths below only see that's local entries, so they - // would drop entries visible only through that's ancestor chain (and ignore its tombstones). + // read-through source: the bucket-copy paths only see that's local entries, so they would + // drop entries visible only through that's ancestor chain (and ignore its tombstones). // Union-copy the full visible set instead -- still shares the source Entry objects. that.forEach(this, (self, entry) -> self.set(entry)); return; } - if (this.size == 0) { - this.putAllIntoEmptyMap(that); - } else { - this.putAllMerge(that); - } - } - - private void putAllMerge(TagMap that) { - Object[] thisBuckets = this.buckets; - Object[] thatBuckets = that.buckets; - - // Since TagMap-s don't support expansion, buckets are perfectly aligned - // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check - // elimination - for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { - Object thatBucket = thatBuckets[i]; - - // if nothing incoming, nothing to do - if (thatBucket == null) continue; - - Object thisBucket = thisBuckets[i]; - if (thisBucket == null) { - // This bucket is null, easy case - // Either copy over the sole entry or clone the BucketGroup chain - - if (thatBucket instanceof Entry) { - thisBuckets[i] = thatBucket; - this.size += 1; - } else if (thatBucket instanceof BucketGroup) { - BucketGroup thatGroup = (BucketGroup) thatBucket; - - BucketGroup thisNewGroup = thatGroup.cloneChain(); - thisBuckets[i] = thisNewGroup; - this.size += thisNewGroup.sizeInChain(); - } - } else if (thisBucket instanceof Entry) { - // This bucket is a single entry, medium complexity case - // If other side is an Entry - just merge the entries into a bucket - // If other side is a BucketGroup - then clone the group and insert the entry normally into - // the cloned group - - Entry thisEntry = (Entry) thisBucket; - int thisHash = thisEntry.hash(); - - if (thatBucket instanceof Entry) { - Entry thatEntry = (Entry) thatBucket; - int thatHash = thatEntry.hash(); - - if (thisHash == thatHash && thisEntry.matches(thatEntry.tag())) { - thisBuckets[i] = thatEntry; - // replacing entry, no size change - } else { - thisBuckets[i] = - new BucketGroup( - thisHash, thisEntry, - thatHash, thatEntry); - this.size += 1; - } - } else if (thatBucket instanceof BucketGroup) { - BucketGroup thatGroup = (BucketGroup) thatBucket; - - // Clone the other group, then place this entry into that group - BucketGroup thisNewGroup = thatGroup.cloneChain(); - int thisNewGroupSize = thisNewGroup.sizeInChain(); - - Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); - if (incomingEntry != null) { - // there's already an entry w/ the same tag from the incoming TagMap - // incoming entry clobbers the existing try, so we're done - thisBuckets[i] = thisNewGroup; - - // overlapping group - subtract one for clobbered existing entry - this.size += thisNewGroupSize - 1; - } else if (thisNewGroup.insertInChain(thisHash, thisEntry)) { - // able to add thisEntry into the existing groups - thisBuckets[i] = thisNewGroup; - - // non overlapping group - existing entry already accounted for in this.size - this.size += thisNewGroupSize; - } else { - // unable to add into the existing groups - thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup); - - // non overlapping group - existing entry already accounted for in this.size - this.size += thisNewGroupSize; - } - } - } else if (thisBucket instanceof BucketGroup) { - // This bucket is a BucketGroup, medium to hard case - // If the other side is an entry, just normal insertion procedure - no cloning required - BucketGroup thisGroup = (BucketGroup) thisBucket; - - if (thatBucket instanceof Entry) { - Entry thatEntry = (Entry) thatBucket; - int thatHash = thatEntry.hash(); - - if (thisGroup.replaceInChain(thatHash, thatEntry) != null) { - // replaced existing entry no size change - } else if (thisGroup.insertInChain(thatHash, thatEntry)) { - this.size += 1; - } else { - thisBuckets[i] = new BucketGroup(thatHash, thatEntry, thisGroup); - this.size += 1; - } - } else if (thatBucket instanceof BucketGroup) { - // Most complicated case - need to walk that bucket group chain and update this chain - BucketGroup thatGroup = (BucketGroup) thatBucket; - - // Taking the easy / expensive way out for updating size - int thisPrevGroupSize = thisGroup.sizeInChain(); - - BucketGroup thisNewGroup = thisGroup.replaceOrInsertAllInChain(thatGroup); - int thisNewGroupSize = thisNewGroup.sizeInChain(); - - thisBuckets[i] = thisNewGroup; - this.size += (thisNewGroupSize - thisPrevGroupSize); - } - } - } - } - - /* - * Specially optimized version of putAll for the common case of destination map being empty - */ - private void putAllIntoEmptyMap(TagMap that) { - Object[] thisBuckets = this.buckets; - Object[] thatBuckets = that.buckets; - - // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check - // elimination - for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { - Object thatBucket = thatBuckets[i]; - - // faster to explicitly null check first, then do instanceof - if (thatBucket == null) { - // do nothing - } else if (thatBucket instanceof BucketGroup) { - // if it is a BucketGroup, then need to clone - BucketGroup thatGroup = (BucketGroup) thatBucket; - - thisBuckets[i] = thatGroup.cloneChain(); - } else { // if ( thatBucket instanceof Entry ) - thisBuckets[i] = thatBucket; - } - } - this.size = that.size; + this.names.putAllLocal(that.names); } public void fillMap(Map map) { @@ -2138,22 +1813,7 @@ public void fillMap(Map map) { this.forEach(map, (m, entry) -> m.put(entry.tag(), entry.objectValue())); return; } - - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - map.put(thisEntry.tag, thisEntry.objectValue()); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.fillMapFromChain(map); - } - } + this.names.fillMapLocal(map); } public void fillStringMap(Map stringMap) { @@ -2163,22 +1823,7 @@ public void fillStringMap(Map stringMap) { this.forEach(stringMap, (m, entry) -> m.put(entry.tag(), entry.stringValue())); return; } - - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - stringMap.put(thisEntry.tag, thisEntry.stringValue()); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.fillStringMapFromChain(stringMap); - } - } + this.names.fillStringMapLocal(stringMap); } @Override @@ -2197,95 +1842,23 @@ public Entry getAndRemove(String tag) { this.checkWriteAccess(); // 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. + // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. long tagId = KnownTagCodec.keyOf(tag); - return tagId == 0 - ? this.getAndRemove(tag, Entry.customHash(tag)) - : this.getAndRemove(KnownTagCodec.nameOf(tagId), tagId); + return tagId == 0 ? this.names.remove(tag) : this.ids.remove(tagId); } /** 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); - } - - private Entry getAndRemove(String canonicalTag, long tagHash) { - Entry localRemoved = this.removeLocal(canonicalTag, tagHash); - - TagMap parent = this.parent; - if (parent != null) { - // Read-through: if the parent still exposes this key, removing it must also hide it from - // fall-through — install a tombstone. The prior *visible* value (Map.remove contract) is the - // local entry if there was one, otherwise the parent's (which we now hide). Single-parent in - // phase 1; rare path (only when removing a parent-exposed key). - boolean alreadyTombstoned = - this.removedFromParent != null && this.removedFromParent.contains(canonicalTag); - if (!alreadyTombstoned) { - 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 - // tombstoned keys, so the default 16-bucket HashSet table would be wasteful. - this.removedFromParent = new HashSet<>(4); - } - this.removedFromParent.add(canonicalTag); - return localRemoved != null ? localRemoved : parentEntry; - } - } - } - return localRemoved; - } - - /** Removes an entry from this map's own buckets only — no parent/tombstone handling. */ - private Entry removeLocal(String tag, long tagHash) { - Object[] thisBuckets = this.buckets; - - int hash = Entry.bucketHash(tagHash); - int bucketIndex = hash & (thisBuckets.length - 1); - - Object bucket = thisBuckets[bucketIndex]; - // null bucket case - do nothing - if (bucket instanceof Entry) { - Entry existingEntry = (Entry) bucket; - if (existingEntry.hash() == hash && existingEntry.matches(tag)) { - thisBuckets[bucketIndex] = null; - - this.size -= 1; - return existingEntry; - } else { - return null; - } - } else if (bucket instanceof BucketGroup) { - BucketGroup lastGroup = (BucketGroup) bucket; - - BucketGroup containingGroup = lastGroup.findContainingGroupInChain(hash, tag); - if (containingGroup == null) { - return null; - } - - Entry existingEntry = containingGroup._remove(hash, tag); - if (containingGroup._isEmpty()) { - this.buckets[bucketIndex] = lastGroup.removeGroupInChain(containingGroup); - } - - this.size -= 1; - return existingEntry; - } - return null; + return this.ids.remove(tagId); } public TagMap copy() { - // Construct with the same (frozen, shared) parent up front — the parent is fixed at - // construction. putAll then clones this map's own (local) buckets + size. The copy stays - // independently mutable (writes land on its local buckets, never the shared parent). - TagMap copy = new TagMap(this.parent); - copy.putAllIntoEmptyMap(this); - if (this.removedFromParent != null) { - copy.removedFromParent = new HashSet<>(this.removedFromParent); - } - return copy; + // The same (frozen, shared) parent, fixed at construction, over a copy of this map's own + // entries and tombstones. The copy stays independently mutable (writes land on its local + // store, never the shared parent). + return new TagMap(this.parent, this.names.copy()); } public TagMap immutableCopy() { @@ -2307,161 +1880,25 @@ public Stream stream() { @Override public void forEach(Consumer consumer) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(consumer); - } - } - - // read-through: parent entries not shadowed locally or tombstoned. Kept out of line so the - // common parent == null path stays byte-identical to before (small / inlinable). - if (this.parent != null) { - this.forEachParent(consumer); - } - } - - private void forEachParent(Consumer consumer) { - // Walk the ancestor chain, nearest first. Each entry is emitted once, by the nearest level that - // defines its key, when not shadowed by a nearer level and not tombstoned. - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) - consumer.accept(parentEntry); - } - } - } - } - } + this.names.forEach(consumer); } public void forEach(T thisObj, BiConsumer consumer) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisObj, thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(thisObj, consumer); - } - } - - // read-through: parent entries not shadowed locally or tombstoned (kept out of line). - if (this.parent != null) { - this.forEachParent(thisObj, consumer); - } - } - - private void forEachParent(T thisObj, BiConsumer consumer) { - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(thisObj, parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { - consumer.accept(thisObj, parentEntry); - } - } - } - } - } - } + this.names.forEach(thisObj, consumer); } public void forEach( T thisObj, U otherObj, TriConsumer consumer) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisObj, otherObj, thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(thisObj, otherObj, consumer); - } - } - - // read-through: parent entries not shadowed locally or tombstoned (kept out of line). - if (this.parent != null) { - this.forEachParent(thisObj, otherObj, consumer); - } - } - - private void forEachParent( - T thisObj, U otherObj, TriConsumer consumer) { - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) - consumer.accept(thisObj, otherObj, parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { - consumer.accept(thisObj, otherObj, parentEntry); - } - } - } - } - } - } + this.names.forEach(thisObj, otherObj, consumer); } public void clear() { this.checkWriteAccess(); - Arrays.fill(this.buckets, null); - this.size = 0; - // clear() removes ALL mappings, including any inherited through read-through. Detaching the - // parent (rather than tombstoning every inherited key) is simpler and cheaper, and leaves an - // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. + // clear() removes ALL mappings, including any inherited through read-through: the store + // detaches its parent, and so does the view. + this.names.clear(); this.parent = null; - this.removedFromParent = null; } public TagMap freeze() { @@ -2479,85 +1916,15 @@ public void checkWriteAccess() { } void checkIntegrity() { - // Decided to use if ( cond ) throw new IllegalStateException rather than assert - // That was done to avoid the extra static initialization needed for an assertion - // While that's probably an unnecessary optimization, this method is only called in tests - - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - int thisHash = thisEntry.hash(); - - int expectedBucket = thisHash & (thisBuckets.length - 1); - if (expectedBucket != i) { - throw new IllegalStateException("incorrect bucket"); - } - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry thisEntry = curGroup._entryAt(i); - if (thisEntry == null) continue; - - int thisHash = thisEntry.hash(); - assert curGroup._hashAt(i) == thisHash; - - int expectedBucket = thisHash & (thisBuckets.length - 1); - if (expectedBucket != i) { - throw new IllegalStateException("incorrect bucket"); - } - } - } - } - } - - if (this.size != this.computeSize()) { - throw new IllegalStateException("incorrect size"); - } - // Local-structure invariant: the size counter's emptiness must match the local buckets. Uses - // the local (this.size == 0), NOT isEmpty(), which under read-through resolves the parent too. - if ((this.size == 0) != this.checkIfEmpty()) { - throw new IllegalStateException("incorrect empty status"); - } + this.names.checkIntegrity(); } int computeSize() { - Object[] thisBuckets = this.buckets; - - int size = 0; - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; - - if (curBucket instanceof Entry) { - size += 1; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - size += curGroup.sizeInChain(); - } - } - return size; + return this.names.computeSize(); } boolean checkIfEmpty() { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; - - if (curBucket instanceof Entry) { - return false; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - if (!curGroup.isEmptyChain()) return false; - } - } - - return true; + return this.names.checkIfEmpty(); } @Override @@ -2615,35 +1982,15 @@ String toPrettyString() { * debugging */ String toInternalString() { - Object[] thisBuckets = this.buckets; - - StringBuilder ledger = new StringBuilder(128); - for (int i = 0; i < thisBuckets.length; ++i) { - ledger.append('[').append(i).append("] = "); - - Object thisBucket = thisBuckets[i]; - if (thisBucket == null) { - ledger.append("null"); - } else if (thisBucket instanceof Entry) { - ledger.append('{').append(thisBucket).append('}'); - } else if (thisBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) thisBucket; - curGroup != null; - curGroup = curGroup.prev) { - ledger.append(curGroup).append(" -> "); - } - } - ledger.append('\n'); - } - return ledger.toString(); + return this.names.toInternalString(); } abstract static class IteratorBase { - private final TagMap map; + private final HashStore store; - // the level whose buckets are currently being walked: the leaf (map) first, then each ancestor - // in turn (read-through union). map == level means we're on the leaf's own entries. - private TagMap level; + // the level whose buckets are currently being walked: the leaf (store) first, then each + // ancestor in turn (read-through union). store == level means we're on the leaf's own entries. + private HashStore level; private Object[] buckets; private Entry nextEntry; @@ -2654,9 +2001,9 @@ abstract static class IteratorBase { private int groupIndex = 0; IteratorBase(TagMap map) { - this.map = map; - this.level = map; - this.buckets = map.buckets; + this.store = map.names; + this.level = this.store; + this.buckets = this.store.buckets; } public final boolean hasNext() { @@ -2696,7 +2043,7 @@ private final Entry advance() { if (tagEntry != null) { // leaf entries emit as-is; ancestor entries only if visible from the leaf -- not shadowed // by a nearer level and not tombstoned. (parentEntryVisible walks the nearer levels.) - if (this.level == this.map || this.map.parentEntryVisible(tagEntry, this.level)) { + if (this.level == this.store || this.store.parentEntryVisible(tagEntry, this.level)) { return tagEntry; } continue; // ancestor entry shadowed/tombstoned -> skip From 4d6f7d08ec8482b744c6b422cfce4a3affbb737f Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 08:39:06 -0400 Subject: [PATCH 52/99] Route TagMap's name-keyed operations through one resolver TagRouting resolves a tag name once, by KnownTagCodec.keyOrAliasOf, and routes the operation to the store that holds it: a known tag to the id store by its id, anything else to the name store by its name. TagMap's name-keyed getters, setters, removals and entry factories, and its resolved variants, all go through it, so name-to-id resolution lives in one place. A direction-scoped alias is still stored as a custom tag. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 122 ++++++++++-------- .../java/datadog/trace/api/TagRouting.java | 52 ++++++++ 2 files changed, 117 insertions(+), 57 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/TagRouting.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 58b974b0739..bafa91d244a 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1386,8 +1386,7 @@ public Set> entrySet() { public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. - long tagId = KnownTagCodec.keyOf(tag); - return tagId == 0 ? this.names.get(tag) : this.ids.get(tagId); + return TagRouting.get(this.ids, this.names, TagRouting.keyOf(tag), tag); } /** The entry for a known tag id. */ @@ -1421,98 +1420,108 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { */ static Entry anyEntryFor(String tag, Object value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) ? Entry.newAnyEntry(key, 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); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) + ? Entry.newObjectEntry(key, 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); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) + ? Entry.newBooleanEntry(key, 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); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) ? Entry.newIntEntry(key, 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); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) + ? Entry.newLongEntry(key, 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); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) + ? Entry.newFloatEntry(key, 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); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) + ? Entry.newDoubleEntry(key, value) + : Entry.newDoubleEntry(tag, value); } public void set(@Nonnull String tag, @Nonnull Object value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newAnyEntry(key, value)); } else { this.putCustom(Entry.newAnyEntry(tag, value)); } } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newObjectEntry(key, value)); } else { this.putCustom(Entry.newObjectEntry(tag, value)); } } public void set(@Nonnull String tag, boolean value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newBooleanEntry(key, value)); } else { this.putCustom(Entry.newBooleanEntry(tag, value)); } } public void set(@Nonnull String tag, int value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newIntEntry(key, value)); } else { this.putCustom(Entry.newIntEntry(tag, value)); } } public void set(@Nonnull String tag, long value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newLongEntry(key, value)); } else { this.putCustom(Entry.newLongEntry(tag, value)); } } public void set(@Nonnull String tag, float value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newFloatEntry(key, value)); } else { this.putCustom(Entry.newFloatEntry(tag, value)); } } public void set(@Nonnull String tag, double value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = TagRouting.keyOf(tag); + if (TagRouting.isKnown(key)) { + this.putKnown(Entry.newDoubleEntry(key, value)); } else { this.putCustom(Entry.newDoubleEntry(tag, value)); } @@ -1525,56 +1534,56 @@ public void set(@Nonnull String tag, double value) { * second lookup. */ public void setResolved(long tagId, @Nonnull String tag, @Nonnull Object value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newAnyEntry(tagId, value)); } else { this.putCustom(Entry.newAnyEntry(tag, value)); } } public void setResolved(long tagId, @Nonnull String tag, @Nonnull CharSequence value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newObjectEntry(tagId, value)); } else { this.putCustom(Entry.newObjectEntry(tag, value)); } } public void setResolved(long tagId, @Nonnull String tag, boolean value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newBooleanEntry(tagId, value)); } else { this.putCustom(Entry.newBooleanEntry(tag, value)); } } public void setResolved(long tagId, @Nonnull String tag, int value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newIntEntry(tagId, value)); } else { this.putCustom(Entry.newIntEntry(tag, value)); } } public void setResolved(long tagId, @Nonnull String tag, long value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newLongEntry(tagId, value)); } else { this.putCustom(Entry.newLongEntry(tag, value)); } } public void setResolved(long tagId, @Nonnull String tag, float value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newFloatEntry(tagId, value)); } else { this.putCustom(Entry.newFloatEntry(tag, value)); } } public void setResolved(long tagId, @Nonnull String tag, double value) { - if (tagId > 0) { - this.set(tagId, value); + if (TagRouting.isKnown(tagId)) { + this.putKnown(Entry.newDoubleEntry(tagId, value)); } else { this.putCustom(Entry.newDoubleEntry(tag, value)); } @@ -1582,14 +1591,14 @@ public void setResolved(long tagId, @Nonnull String tag, double value) { /** The read counterpart of {@link #setResolved(long, String, Object)}. */ public Entry getEntryResolved(long tagId, String tag) { - return tagId > 0 ? this.ids.get(tagId) : this.names.get(tag); + return TagRouting.get(this.ids, this.names, tagId, tag); } /** The removal counterpart of {@link #setResolved(long, String, Object)}. */ public Entry removeResolved(long tagId, String tag) { this.checkWriteAccess(); - return tagId > 0 ? this.ids.remove(tagId) : this.names.remove(tag); + return TagRouting.remove(this.ids, this.names, tagId, tag); } /* @@ -1707,7 +1716,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { } this.checkWriteAccess(); - return newEntry.tagId() != 0 ? this.ids.getAndPut(newEntry) : this.names.getAndPut(newEntry); + return TagRouting.getAndPut(this.ids, this.names, newEntry); } /** @@ -1719,7 +1728,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); - return newEntry.tagId() != 0 ? this.ids.put(newEntry) : this.names.put(newEntry); + return TagRouting.put(this.ids, this.names, newEntry); } /** {@link #putEntry} for an entry known to be a known tag's. */ @@ -1843,8 +1852,7 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. - long tagId = KnownTagCodec.keyOf(tag); - return tagId == 0 ? this.names.remove(tag) : this.ids.remove(tagId); + return TagRouting.remove(this.ids, this.names, TagRouting.keyOf(tag), tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java new file mode 100644 index 00000000000..7cd3b2ef0c5 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -0,0 +1,52 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.Entry; + +/** + * Where a tag name is resolved and an operation is routed to the store that holds it: a known tag + * to the {@link IdStore} by its id, a custom tag to the name store by its name. The one place a + * TagMap turns a name into an id, so the stores themselves never see a name that might be a known + * tag's. + * + *

A name resolves once, by {@link #keyOf}, to a key: a known tag's id ({@code > 0}), 0 for a + * custom tag, or a negative sentinel for a direction-scoped OpenTelemetry alias ({@code + * server.address}), whose tag depends on the span's direction. An alias is the caller's to decide: + * the routing methods treat any key that is not an id as custom, which is what a TagMap -- it has + * no direction -- does with an alias; a caller that knows the direction resolves the alias to its + * tag first. + * + *

Static, so a span can route to its stores the same way a TagMap does. + */ +final class TagRouting { + private TagRouting() {} + + /** {@code tag}'s key: its id when known, 0 when custom, negative for a direction-scoped alias. */ + static long keyOf(String tag) { + return KnownTagCodec.keyOrAliasOf(tag); + } + + /** Whether {@code key} is a known tag's id, as opposed to a custom tag or an alias. */ + static boolean isKnown(long key) { + return key > 0; + } + + /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ + static Entry get(IdStore ids, HashStore names, long key, String tag) { + return isKnown(key) ? ids.get(key) : names.get(tag); + } + + /** Removes {@code tag}, already resolved to {@code key}, returning the prior visible entry. */ + static Entry remove(IdStore ids, HashStore names, long key, String tag) { + return isKnown(key) ? ids.remove(key) : names.remove(tag); + } + + /** Puts {@code entry} into the store for its kind, returning the local entry it replaced. */ + static Entry put(IdStore ids, HashStore names, Entry entry) { + return entry.tagId() != 0 ? ids.put(entry) : names.put(entry); + } + + /** {@link #put}, returning the prior visible entry. */ + static Entry getAndPut(IdStore ids, HashStore names, Entry entry) { + return entry.tagId() != 0 ? ids.getAndPut(entry) : names.getAndPut(entry); + } +} From d2261f3341e9d07060dd055ca2d0e60506457fef Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 08:42:01 -0400 Subject: [PATCH 53/99] Clarify what the store split costs and where a dense mode would plug in Co-Authored-By: Claude Opus 5.5 --- internal-api/src/main/java/datadog/trace/api/TagMap.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 bafa91d244a..9cbcdcef61f 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1201,10 +1201,11 @@ public EntryChange next() { * through to its own parent chain and keeps its own tombstones, and precedence never crosses * stores. * - * In hash mode -- the only mode so far -- one HashStore serves as both (ids == names), so the - * split costs no extra object. A whole-map operation (size, iteration, copy, putAll, clear) runs - * on that one store and so covers both halves; a separate IdStore implementation would have to - * visit ids as well. + * In hash mode -- the only mode so far -- one HashStore serves as both (ids == names), so a map + * is the view plus one store, and no tag costs more than before. A whole-map operation (size, + * iteration, copy, putAll, clear) runs on that one store and so covers both halves; a separate + * IdStore implementation (a dense store) would have to visit ids as well, and would be chosen, + * by a global mode, in the constructor that pairs the stores. */ /** The known tags, by id. In hash mode the same object as {@link #names}. */ From e605c603a85fbc7e17616d79f8ffc0baad2e7244 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 10:19:34 -0400 Subject: [PATCH 54/99] Let the span own its tag stores and route to them directly DDSpanContext now owns a HashStore (and, outside hash mode, an IdStore) instead of a TagMap, created when the span is constructed and read through to the frozen tracer-level tags. Every tag write and read goes through TagRouting under one lock, the HashStore itself. Direction, alias, interception and pending-alias handling stay in DDSpanContext and DirectionalTags, now applied to the stores. HashStore is no longer an IdStore: it is the name-keyed hash, which also holds known tags by id, and exposes an IdStore adapter over them. The static HASH_MODE flag lets hash mode call the hash directly, so a span needs no adapter object. Internal readers (tag post-processors, the serializer) get a short-lived TagMap view over the span's stores whose name-keyed reads honor the span's direction; getTags() is still a snapshot copy. TagMap.Internals is gone: the store API is the direct path. Its tests move to TagStoresTest. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 216 ++++++++------ .../datadog/trace/core/DirectionalTags.java | 105 ++++--- .../DDSpanDirectionalTagConsistencyTest.java | 2 +- .../trace/core/DirectionalTagsTest.java | 145 ++++++--- .../forbiddenApiFilters/instrumentation.txt | 16 +- .../java/datadog/trace/api/HashIdStore.java | 72 +++++ .../java/datadog/trace/api/HashStore.java | 135 +++++++-- .../main/java/datadog/trace/api/IdStore.java | 44 ++- .../main/java/datadog/trace/api/TagMap.java | 164 ++++++----- .../java/datadog/trace/api/TagRouting.java | 252 ++++++++++++++-- .../datadog/trace/api/TagMapSetByIdTest.java | 43 +-- .../java/datadog/trace/api/TagStoresTest.java | 274 ++++++++++++++++++ 12 files changed, 1104 insertions(+), 364 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/HashIdStore.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagStoresTest.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 c7d8b91429f..97d76426f63 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 @@ -15,10 +15,13 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; +import datadog.trace.api.HashStore; +import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; import datadog.trace.api.TagMap; +import datadog.trace.api.TagRouting; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; import datadog.trace.api.config.TracerConfig; @@ -69,7 +72,11 @@ * the associated Span instance */ public class DDSpanContext - implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext { + implements AgentSpanContext, + RequestContext, + TraceSegment, + ProfilerContext, + TagMap.DirectionSource { private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class); public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -157,7 +164,7 @@ private static int[] tagDirectionOfSpanKind() { * -- 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 + * tags. Written under the {@code tagStore} 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. @@ -175,8 +182,15 @@ private static int[] tagDirectionOfSpanKind() { * threads, they will rarely, if ever, be read and modified concurrently by multiple threads but * rather read and accessed in a serial fashion on thread after thread. The synchronization can * then be wrapped around bulk operations to minimize the costly atomic operations. + * + *

The span owns its tag stores directly: {@code tagStore} holds the custom tags, and the known + * tags too in hash mode; {@code tagIds} holds the known tags otherwise (null in hash mode). Every + * access goes through {@link TagRouting}, under one lock that guards both: {@code tagStore} + * itself. Internal readers get a {@link #tagView() view}. */ - private final TagMap unsafeTags; + private final HashStore tagStore; + + private final IdStore tagIds; /** The service name is required, otherwise the span are dropped by the agent */ private volatile String serviceName; @@ -441,13 +455,9 @@ public DDSpanContext( assert pathwayContext != null; this.pathwayContext = pathwayContext; - // The +1 is the magic number from the tags below that we set at the end, - // and "* 4 / 3" is to make sure that we don't resize immediately - final int capacity = Math.max((tagsSize <= 0 ? 3 : (tagsSize + 1)) * 4 / 3, 8); - this.unsafeTags = - readThroughParent != null - ? TagMap.createFromParent(readThroughParent) - : TagMap.create(capacity); + // Reads through to the frozen tracer-level tags, if any. + this.tagStore = new HashStore(TagMap.readThroughStore(readThroughParent)); + this.tagIds = TagRouting.idStoreFor(this.tagStore); // must set this before setting the service and resource names below this.profilingContextIntegration = profilingContextIntegration; @@ -696,7 +706,7 @@ private void seedAbsentTags(final TagMap map) { if (map == null) { return; } - synchronized (unsafeTags) { + synchronized (tagStore) { map.forEach( this, (ctx, tagEntry) -> { @@ -829,7 +839,7 @@ public int getSamplingPriority() { } public void setSpanSamplingPriority(double rate, int limit) { - synchronized (unsafeTags) { + synchronized (tagStore) { unsafeSetTag(SPAN_SAMPLING_MECHANISM_TAG, SamplingMechanism.SPAN_SAMPLING_RATE); unsafeSetTag(SPAN_SAMPLING_RULE_RATE_TAG, rate); if (limit != Integer.MAX_VALUE) { @@ -958,20 +968,29 @@ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); // Checked under the lock alias registration takes: a writer that read no direction registers // its pending alias before this sees the flag, or reads the new kind and resolves at write. - synchronized (unsafeTags) { + synchronized (tagStore) { if (hasPendingAliases()) { // The span now has a direction: fold pending aliases into the tags they denote, so every // later reader (peer service naming, stats, sampling rules, serialization) sees them // resolved. - DirectionalTags.resolveScopedNames(unsafeTags, tagDirection()); + DirectionalTags.resolveScopedNames(tagIds, tagStore, tagDirection()); } } } + /** + * A live view of this span's tag stores, for its internal readers: post-processors and the + * serializer. Use it only under the {@code tagStore} lock, and only while holding it. + */ + private TagMap tagView() { + return TagMap.viewOf(tagIds, tagStore, this); + } + /** * The direction this span reads direction-dependent tag names in; see {@link DirectionalTags}. */ - private int tagDirection() { + @Override + public int tagDirection() { return TAG_DIRECTION_OF_SPAN_KIND[spanKindOrdinal]; } @@ -1006,8 +1025,9 @@ public String getSpanKindString() { return SPAN_KIND_VALUES[ordinal]; } // UNSET or CUSTOM -- fall through to tag map - synchronized (unsafeTags) { - return unsafeTags.getString(Tags.SPAN_KIND); + synchronized (tagStore) { + TagMap.Entry kind = TagRouting.getById(tagIds, tagStore, KnownTags.SPAN_KIND_ID); + return kind == null ? null : kind.stringValue(); } } @@ -1018,31 +1038,31 @@ public void setOrigin(final CharSequence origin) { } public void setMetric(final CharSequence key, final Number value) { - synchronized (unsafeTags) { + synchronized (tagStore) { unsafeSetTag(key.toString(), value); } } public void setMetric(final CharSequence key, final int value) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(key.toString(), value); } } @@ -1052,7 +1072,7 @@ public void setMetric(final TagMap.EntryReader entry) { return; } - synchronized (unsafeTags) { + synchronized (tagStore) { storeEntry(entry); } } @@ -1065,7 +1085,7 @@ public void setMetric(final TagMap.EntryReader entry) { * 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 DirectionalTags). Call while holding the - * unsafeTags lock. + * tagStore lock. */ private long writeKeyOf(String tag) { long key = KnownTagCodec.keyOrAliasOf(tag); @@ -1076,7 +1096,7 @@ private long writeKeyOf(String tag) { if (key == 0) { return 0L; } - long aliasedId = DirectionalTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); + long aliasedId = DirectionalTags.prepareAliasWrite(tagIds, tagStore, tag, tagDirection()); if (aliasedId > 0) { return aliasedId; } @@ -1086,7 +1106,7 @@ private long writeKeyOf(String tag) { /** * 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. + * routed to supersede it. Call while holding the {@code tagStore} lock. */ @SuppressFBWarnings( value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", @@ -1096,36 +1116,37 @@ private void notePendingAlias() { } /** True once any alias has been stored as written; see {@link #routedTagFlags}. */ - boolean hasPendingAliases() { + @Override + public boolean hasPendingAliases() { return (routedTagFlags & KnownTagCodec.HAS_SCOPED_ALIAS) != 0; } private void storeTag(String tag, Object value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } private void storeTag(String tag, CharSequence value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } private void storeTag(String tag, boolean value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } private void storeTag(String tag, int value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } private void storeTag(String tag, long value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } private void storeTag(String tag, float value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } private void storeTag(String tag, double value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); + TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); } /** {@link #storeTag} for an entry, which carries its id: no lookup unless it may be an alias. */ @@ -1133,30 +1154,39 @@ private void storeEntry(TagMap.EntryReader entry) { long tagId = entry.tagId(); if (tagId != 0) { supersedePendingAliases(tagId); - unsafeTags.set(entry); + putEntry(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { - long aliasedId = DirectionalTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); + long aliasedId = + DirectionalTags.prepareAliasWrite(tagIds, tagStore, entry.tag(), tagDirection()); if (aliasedId > 0) { - unsafeTags.set(aliasedId, entry.objectValue()); + TagRouting.setKnown(tagIds, tagStore, aliasedId, entry.objectValue()); } else { if (aliasedId < 0) { notePendingAlias(); // a real alias, stored as written; 0 is a filter false positive } - unsafeTags.set(entry); + putEntry(entry); } } else { - unsafeTags.set(entry); + putEntry(entry); + } + } + + /** Stores {@code entry} as is, sharing its Entry; a reader with no entry is a no-op. */ + private void putEntry(TagMap.EntryReader entry) { + TagMap.Entry e = entry.entry(); + if (e != null) { + TagRouting.put(tagIds, tagStore, e); } } public void removeTag(String tag) { if (tagEquals(tag, Tags.SPAN_KIND)) { - // Clear the cached ordinal; unsafeTags still needs to be updated below. + // Clear the cached ordinal; the tag store still needs to be updated below. spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (unsafeTags) { + synchronized (tagStore) { // A direction-dependent name and the tag it denotes here are removed together. - DirectionalTags.remove(unsafeTags, tag, tagDirection()); + DirectionalTags.remove(tagIds, tagStore, tag, tagDirection()); } } @@ -1173,8 +1203,8 @@ public void removeTag(long tagId) { if (tagId == KnownTags.SPAN_KIND_ID) { spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (unsafeTags) { - DirectionalTags.remove(unsafeTags, tagId, tagDirection()); + synchronized (tagStore) { + DirectionalTags.remove(tagIds, tagStore, tagId, tagDirection()); } } @@ -1240,10 +1270,11 @@ private boolean interceptEntry(TagMap.EntryReader entry) { */ private boolean isPresent(TagMap.EntryReader entry) { if (KnownTagCodec.isDirectionDependent(entry)) { - return DirectionalTags.get(unsafeTags, entry.tag(), tagDirection(), hasPendingAliases()) + return DirectionalTags.get(tagIds, tagStore, entry.tag(), tagDirection(), hasPendingAliases()) != null; } - return unsafeTags.containsKey(entry.tag()); + String tag = entry.tag(); + return TagRouting.get(tagIds, tagStore, TagRouting.keyOf(tag), tag) != null; } public void setTag(final String tag, final Object value) { @@ -1256,7 +1287,7 @@ public void setTag(final String tag, final Object value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1272,7 +1303,7 @@ public void setTag(final String tag, final String value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1290,18 +1321,18 @@ public void supersedeAliases(long tagId) { if (!hasPendingAliases()) { return; // entry paths route on the constant mask; nothing to supersede } - synchronized (unsafeTags) { + synchronized (tagStore) { supersedePendingAliases(tagId); } } /** * Removes the pending aliases a write of {@code tagId} supersedes, if it has an alias and the - * span holds any. Callers hold the {@code unsafeTags} lock. + * span holds any. Callers hold the {@code tagStore} lock. */ private void supersedePendingAliases(long tagId) { if (KnownTagCodec.hasScopedAlias(tagId) && hasPendingAliases()) { - DirectionalTags.prepareWrite(unsafeTags, tagId, tagDirection()); + DirectionalTags.prepareWrite(tagIds, tagStore, tagId, tagDirection()); } } @@ -1318,8 +1349,8 @@ public void setTag(final long tagId, final Object value) { removeTag(tagId); } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1332,8 +1363,8 @@ public void setTag(final long tagId, final CharSequence value) { removeTag(tagId); } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1345,8 +1376,8 @@ public void setTag(final long tagId, final boolean value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1358,8 +1389,8 @@ public void setTag(final long tagId, final int value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1371,8 +1402,8 @@ public void setTag(final long tagId, final long value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1384,8 +1415,8 @@ public void setTag(final long tagId, final float value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1397,8 +1428,8 @@ public void setTag(final long tagId, final double value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { this.setBox(tagId, value); } else { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, value); } } } @@ -1410,7 +1441,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!interceptEntry(entry)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeEntry(entry); } } @@ -1430,8 +1461,8 @@ public void setTag(TagMap.EntryReader entry) { */ private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); + synchronized (tagStore) { + TagRouting.setKnown(tagIds, tagStore, tagId, box); } } } @@ -1445,7 +1476,7 @@ public void setTag(final String tag, final boolean value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1460,7 +1491,7 @@ public void setTag(final String tag, final int value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1475,7 +1506,7 @@ public void setTag(final String tag, final long value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1490,7 +1521,7 @@ public void setTag(final String tag, final float value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1505,7 +1536,7 @@ public void setTag(final String tag, final double value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (unsafeTags) { + synchronized (tagStore) { storeTag(tag, value); } } @@ -1520,7 +1551,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { return; } - synchronized (unsafeTags) { + synchronized (tagStore) { if (needsIntercept) { // forEach out-performs the iterator of TagMap // Taking advantage of ability to pass through other context arguments @@ -1534,7 +1565,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { }); } else { // needsIntercept(TagMap) is also true for a map with a direction-dependent name. - unsafeTags.putAll(map); + TagRouting.putAll(tagIds, tagStore, map); } } } @@ -1544,16 +1575,16 @@ void setAllTags(final TagMap.Ledger ledger) { return; } - synchronized (unsafeTags) { + synchronized (tagStore) { for (final TagMap.EntryChange entryChange : ledger) { String tag = entryChange.tag(); if (entryChange.isRemoval()) { if (tagEquals(tag, Tags.SPAN_KIND)) { - // mirror removeTag(String): keep the cached ordinal in sync with unsafeTags + // mirror removeTag(String): keep the cached ordinal in sync with the tags spanKindOrdinal = SPAN_KIND_UNSET; } // as removeTag(String): a direction-dependent name and the tag it denotes go together - DirectionalTags.remove(unsafeTags, tag, tagDirection()); + DirectionalTags.remove(tagIds, tagStore, tag, tagDirection()); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; @@ -1571,7 +1602,7 @@ void setAllTags(final Map map) { } else if (map instanceof TagMap) { setAllTags((TagMap) map); } else if (!map.isEmpty()) { - synchronized (unsafeTags) { + synchronized (tagStore) { for (final Map.Entry tag : map.entrySet()) { long tagId = idForWrite(tag.getKey()); boolean intercepted = @@ -1616,7 +1647,7 @@ Object getTag(final String key) { return getSpanKindString(); default: Object value; - synchronized (unsafeTags) { + synchronized (tagStore) { value = unsafeGetTag(key); } // maintain previously observable type of http url :| @@ -1632,13 +1663,13 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return DirectionalTags.get(unsafeTags, tag, tagDirection(), hasPendingAliases()); + return DirectionalTags.get(tagIds, tagStore, tag, tagDirection(), hasPendingAliases()); } @Deprecated public TagMap getTags() { - synchronized (unsafeTags) { - TagMap tags = unsafeTags.copy(); + synchronized (tagStore) { + TagMap tags = tagView().copy(); tags.put(DDTags.THREAD_ID, threadId); // maintain previously observable type of the thread name :| @@ -1690,8 +1721,8 @@ public void setMetaStruct(final String field, final T value) { } void earlyProcessTags(AppendableSpanLinks links) { - synchronized (unsafeTags) { - TagsPostProcessorFactory.eagerProcessor().processTags(unsafeTags, this, links); + synchronized (tagStore) { + TagsPostProcessorFactory.eagerProcessor().processTags(tagView(), this, links); } } @@ -1720,15 +1751,18 @@ void processTagsAndBaggage( // This is a compromise to avoid... // - 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) { + synchronized (tagStore) { + // One live view of the tag stores, for the post-processors and the serializer + TagMap tags = tagView(); + // Tags - TagsPostProcessorFactory.lazyProcessor().processTags(unsafeTags, this, restrictedSpan); + TagsPostProcessorFactory.lazyProcessor().processTags(tags, this, restrictedSpan); // Links if (injectLinksAsTags) { String linksTag = DDSpanLink.toTag(restrictedSpan.getLinks()); if (linksTag != null) { - unsafeTags.set(SPAN_LINKS, linksTag); + tags.set(SPAN_LINKS, linksTag); } } @@ -1737,7 +1771,7 @@ void processTagsAndBaggage( if (injectEventsAsTags) { String eventsTag = spanEventsToTag(events); if (eventsTag != null) { - unsafeTags.set(SPAN_EVENTS, eventsTag); + tags.set(SPAN_EVENTS, eventsTag); } } @@ -1762,7 +1796,7 @@ void processTagsAndBaggage( new Metadata( threadId, threadName, - unsafeTags, + tags, baggageItemsWithPropagationTags, getSamplingPriority(), measured, @@ -1925,7 +1959,7 @@ public String toString() { s.append(" *measured*"); } - synchronized (unsafeTags) { + synchronized (tagStore) { s.append(" tags=").append(new TreeMap<>(getTags())); } return s.toString(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index df26487061a..09dd5e0dd84 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -4,15 +4,20 @@ import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; +import datadog.trace.api.HashStore; +import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.TagMap; +import datadog.trace.api.TagRouting; 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 written, read, removed or emitted. + * A span's reading of the few tag names that depend on its direction. The tag stores ({@link + * HashStore}, {@link IdStore}) keep every name as written and know nothing of span kinds -- baggage + * is a TagMap too -- so the direction is applied here, by the span layer, to the span's stores when + * a tag is written, read, removed or emitted. The read rule itself is {@link TagRouting}'s, so a + * span's {@link TagMap#viewOf view} reads the same way. * *

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 @@ -57,7 +62,7 @@ final class DirectionalTags { * pending-alias flag set it only for the sentinel, so a name that merely passed {@link * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. */ - static long prepareAliasWrite(TagMap tags, String alias, int direction) { + static long prepareAliasWrite(IdStore ids, HashStore tags, String alias, int direction) { long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { tags.remove(alias); // a value pending under the alias is older than this write @@ -70,9 +75,9 @@ static long prepareAliasWrite(TagMap tags, String alias, int direction) { * 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. */ - static void prepareWrite(TagMap tags, long tagId, int direction) { + static void prepareWrite(IdStore ids, HashStore tags, long tagId, int direction) { if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, tagId, direction); + removePendingAliases(ids, tags, tagId, direction); } } @@ -80,7 +85,7 @@ static void prepareWrite(TagMap tags, long tagId, int 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. */ - private static void removePendingAliases(TagMap tags, long tagId, int direction) { + private static void removePendingAliases(IdStore ids, HashStore tags, long tagId, int direction) { if (direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); if (alias != null) { @@ -101,78 +106,68 @@ private static void removePendingAliases(TagMap tags, long tagId, int direction) * reads its alias; a pending alias, being the newer write, wins. Every other name costs the same * single lookup as {@link TagMap#getObject}. */ - static Object get(TagMap tags, String name, int direction) { - return get(tags, name, direction, true); + static Object get(IdStore ids, HashStore tags, String name, int direction) { + return get(ids, tags, name, direction, true); } /** - * {@link #get(TagMap, String, int)} for a caller that tracks whether {@code tags} can hold a - * pending alias at all. With {@code mayHavePendingAliases} false, a tag with an alias skips - * probing for one, so it costs a single lookup like any other tag. + * {@link #get(IdStore, HashStore, String, int)} for a caller that tracks whether {@code tags} can + * hold a pending alias at all. With {@code mayHavePendingAliases} false, a tag with an alias + * skips probing for one, so it costs a single lookup like any other tag. */ - static Object get(TagMap tags, String name, int direction, boolean mayHavePendingAliases) { - long key = KnownTagCodec.keyOrAliasOf(name); - if (key > 0) { - return get(tags, key, direction, mayHavePendingAliases); - } - TagMap.Entry asWritten = - key == 0 || mayHavePendingAliases ? tags.getEntryResolved(0L, name) : null; - if (key == 0 || asWritten != null) { - return asWritten == null ? null : asWritten.objectValue(); - } - long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return tagId > 0 ? tags.internals().getKnownValue(tagId) : null; + static Object get( + IdStore ids, HashStore tags, String name, int direction, boolean mayHavePendingAliases) { + TagMap.Entry entry = + TagRouting.getEntry( + ids, tags, TagRouting.keyOf(name), name, direction, mayHavePendingAliases); + return entry == null ? null : entry.objectValue(); } /** * The value of the known tag {@code tagId} on a span of {@code direction}; see {@link - * #get(TagMap, String, int)}. + * #get(IdStore, HashStore, String, int)}. */ - static Object get(TagMap tags, long tagId, int direction) { - return get(tags, tagId, direction, true); + static Object get(IdStore ids, HashStore tags, long tagId, int direction) { + return get(ids, tags, tagId, direction, true); } - /** {@link #get(TagMap, long, int)}; see {@link #get(TagMap, String, int, boolean)}. */ - static Object get(TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { - if (mayHavePendingAliases - && KnownTagCodec.hasScopedAlias(tagId) - && direction != DIRECTION_UNKNOWN) { - String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - Object pending = alias == null ? null : tags.getObject(alias); - if (pending != null) { - return pending; - } - } - return tags.internals().getKnownValue(tagId); + /** + * {@link #get(IdStore, HashStore, long, int)}; see {@link #get(IdStore, HashStore, String, int, + * boolean)}. + */ + static Object get( + IdStore ids, HashStore tags, long tagId, int direction, boolean mayHavePendingAliases) { + TagMap.Entry entry = TagRouting.getById(ids, tags, tagId, direction, mayHavePendingAliases); + return entry == null ? null : entry.objectValue(); } /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ - static void remove(TagMap tags, String name, int direction) { + static void remove(IdStore ids, HashStore tags, String name, int direction) { // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { - remove(tags, key, direction); + remove(ids, tags, key, direction); return; } - tags.removeResolved(0L, name); // a custom tag, or an alias stored as written + tags.remove(name); // a custom tag, or an alias stored as written if (key == 0) { return; } if (direction == DIRECTION_UNKNOWN) { - removeEveryTagOfAlias(tags, name); + removeEveryTagOfAlias(ids, tags, name); return; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); if (tagId > 0) { - tags.getAndRemove(tagId); + TagRouting.removeById(ids, tags, tagId); } } /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ - static void remove(TagMap tags, long tagId, int direction) { - tags.getAndRemove(tagId); + static void remove(IdStore ids, HashStore tags, long tagId, int direction) { + TagRouting.removeById(ids, tags, tagId); if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, tagId, direction); + removePendingAliases(ids, tags, tagId, direction); } } @@ -180,14 +175,14 @@ static void remove(TagMap tags, long tagId, int direction) { * Removes every tag {@code alias} could denote, for a removal of the alias before the span has a * direction. A removal counts as a write, so no older value can resurface when the kind arrives; * like the write rule, this can drop the other direction's tag. (Removing a tag before then - * likewise removes the pending aliases that could denote it; see {@link #remove(TagMap, long, - * int)}.) + * likewise removes the pending aliases that could denote it; see {@link #remove(IdStore, + * HashStore, long, int)}.) */ - private static void removeEveryTagOfAlias(TagMap tags, String alias) { + private static void removeEveryTagOfAlias(IdStore ids, HashStore tags, String alias) { for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { long aliasedId = KnownTagCodec.directionalKeyOf(alias, direction); if (aliasedId > 0) { - tags.getAndRemove(aliasedId); + TagRouting.removeById(ids, tags, aliasedId); } } } @@ -197,12 +192,13 @@ private static void removeEveryTagOfAlias(TagMap tags, String alias) { * 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. */ - static void resolveScopedNames(TagMap tags, int direction) { + static void resolveScopedNames(IdStore ids, HashStore tags, int direction) { if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { return; // no alias names a tag on a span with no direction, or an internal one } List pending = null; - for (TagMap.EntryReader entry : tags) { + // Pending aliases are custom tags, so they are all in the HashStore. + for (TagMap.EntryReader entry : TagMap.viewOf(ids, tags, null)) { if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { if (pending == null) { pending = new ArrayList<>(2); @@ -215,7 +211,8 @@ static void resolveScopedNames(TagMap tags, int direction) { } for (TagMap.Entry entry : pending) { tags.remove(entry.tag()); - tags.set(KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); + TagRouting.setKnown( + ids, tags, KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); } } 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 1fbb290d8b8..fba685be7c2 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 @@ -208,7 +208,7 @@ void theLastWriteWinsBetweenATagAndItsAlias( // And when the span is emitted: the pending alias, if any, is folded into the tag. TagMap emitted = span.getTags().copy(); - DirectionalTags.resolveScopedNames(emitted, direction); + DirectionalTags.resolveScopedNames(emitted.idStore(), emitted.hashStore(), direction); assertEquals("second.example", emitted.getObject(tag)); assertNull(emitted.getObject("server.address")); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java index 4bdb3c2d004..f6aeba7be15 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java @@ -31,15 +31,20 @@ void anAliasAndItsTagReadAsOne(int direction, String alias, String tag) { TagMap tags = TagMap.create(); tags.set(tag, "stored"); - assertEquals("stored", DirectionalTags.get(tags, alias, direction)); - assertEquals("stored", DirectionalTags.get(tags, tag, direction)); - assertEquals("stored", DirectionalTags.get(tags, KnownTagCodec.keyOf(tag), direction)); + assertEquals("stored", DirectionalTags.get(tags.idStore(), tags.hashStore(), alias, direction)); + assertEquals("stored", DirectionalTags.get(tags.idStore(), tags.hashStore(), tag, direction)); + assertEquals( + "stored", + DirectionalTags.get(tags.idStore(), tags.hashStore(), 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", DirectionalTags.get(tags, alias, direction)); - assertEquals("pending", DirectionalTags.get(tags, tag, direction)); - assertEquals("pending", DirectionalTags.get(tags, KnownTagCodec.keyOf(tag), direction)); + assertEquals( + "pending", DirectionalTags.get(tags.idStore(), tags.hashStore(), alias, direction)); + assertEquals("pending", DirectionalTags.get(tags.idStore(), tags.hashStore(), tag, direction)); + assertEquals( + "pending", + DirectionalTags.get(tags.idStore(), tags.hashStore(), KnownTagCodec.keyOf(tag), direction)); } @Test @@ -47,27 +52,47 @@ void withNoDirectionAnAliasReadsOnlyAsWritten() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "stored"); - assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_UNKNOWN)); - assertEquals("stored", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertNull( + DirectionalTags.get(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_UNKNOWN)); assertEquals( - "stored", DirectionalTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); + "stored", + DirectionalTags.get( + tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals( + "stored", + DirectionalTags.get( + tags.idStore(), tags.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); tags.set("server.address", "pending"); - assertEquals("pending", DirectionalTags.get(tags, "server.address", DIRECTION_UNKNOWN)); - assertEquals("stored", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals( + "pending", + DirectionalTags.get(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_UNKNOWN)); + assertEquals( + "stored", + DirectionalTags.get( + tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); } @Test void aMissReadsAsNull() { TagMap tags = TagMap.create(); - assertNull(DirectionalTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get( + tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get( + tags.idStore(), tags.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get( + tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get( + tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND)); // client.port names no tag on a client span - assertNull(DirectionalTags.get(tags, "client.port", DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get(tags.idStore(), tags.hashStore(), "client.port", DIRECTION_OUTBOUND)); } @Test @@ -76,9 +101,17 @@ void otherNamesReadAsWritten() { tags.set("my.tag", "custom"); tags.set(Tags.HTTP_METHOD, "GET"); - assertEquals("custom", DirectionalTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); - assertEquals("GET", DirectionalTags.get(tags, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); - assertEquals("GET", DirectionalTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertEquals( + "custom", + DirectionalTags.get(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_OUTBOUND)); + assertEquals( + "GET", + DirectionalTags.get( + tags.idStore(), tags.hashStore(), Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); + assertEquals( + "GET", + DirectionalTags.get( + tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); } @Test @@ -86,7 +119,9 @@ void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { TagMap tags = TagMap.create(); tags.set("server.address", "older"); - long tagId = DirectionalTags.prepareAliasWrite(tags, "server.address", DIRECTION_OUTBOUND); + long tagId = + DirectionalTags.prepareAliasWrite( + tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND); assertEquals(KnownTags.PEER_HOSTNAME_ID, tagId); assertNull(tags.getObject("server.address")); // the older pending value is dropped @@ -104,7 +139,9 @@ void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, tags.set(name, "pending"); // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all - assertEquals(result, DirectionalTags.prepareAliasWrite(tags, name, direction)); + assertEquals( + result, + DirectionalTags.prepareAliasWrite(tags.idStore(), tags.hashStore(), name, direction)); assertEquals("pending", tags.getObject(name)); } @@ -114,7 +151,8 @@ void aTagWriteWithNoDirectionDropsEveryAliasThatCouldDenoteIt() { tags.set("client.port", 1); tags.set("server.port", 2); - DirectionalTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite( + tags.idStore(), tags.hashStore(), KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); assertNull(tags.getObject("client.port")); assertNull(tags.getObject("server.port")); @@ -126,7 +164,8 @@ void aTagWriteWithADirectionDropsOnlyItsAliasThere() { tags.set("client.port", 1); // no tag on a client span: a custom tag, kept tags.set("server.port", 2); - DirectionalTags.prepareWrite(tags, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); + DirectionalTags.prepareWrite( + tags.idStore(), tags.hashStore(), KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); assertEquals(1, tags.getObject("client.port")); assertNull(tags.getObject("server.port")); @@ -137,8 +176,10 @@ void aTagWithoutAnAliasIsLeftAlone() { TagMap tags = TagMap.create(); tags.set("server.address", "pending"); - DirectionalTags.prepareWrite(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); - DirectionalTags.prepareWrite(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); + DirectionalTags.prepareWrite( + tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite( + tags.idStore(), tags.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); assertEquals("pending", tags.getObject("server.address")); } @@ -149,7 +190,7 @@ void removingAnAliasRemovesItsTag() { tags.set(Tags.PEER_HOSTNAME, "stored"); tags.set("server.address", "pending"); - DirectionalTags.remove(tags, "server.address", DIRECTION_OUTBOUND); + DirectionalTags.remove(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND); assertNull(tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("server.address")); @@ -160,14 +201,16 @@ void removingATagRemovesItsAlias() { TagMap byName = TagMap.create(); byName.set(Tags.HTTP_HOSTNAME, "stored"); byName.set("server.address", "pending"); - DirectionalTags.remove(byName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); + DirectionalTags.remove( + byName.idStore(), byName.hashStore(), 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"); - DirectionalTags.remove(byId, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); + DirectionalTags.remove( + byId.idStore(), byId.hashStore(), KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); assertNull(byId.getObject("server.address")); } @@ -178,8 +221,9 @@ void removingATagWithNoAliasLeavesUnrelatedTags() { tags.set(Tags.PEER_HOSTNAME, "stored"); tags.set("my.tag", "custom"); - DirectionalTags.remove(tags, "my.tag", DIRECTION_OUTBOUND); - DirectionalTags.remove(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); + DirectionalTags.remove(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_OUTBOUND); + DirectionalTags.remove( + tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("my.tag")); @@ -198,7 +242,7 @@ void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String t tags.set(alias, "newer"); tags.set("my.tag", "custom"); - DirectionalTags.resolveScopedNames(tags, direction); + DirectionalTags.resolveScopedNames(tags.idStore(), tags.hashStore(), direction); assertEquals("newer", tags.getObject(tag)); assertNull(tags.getObject(alias)); @@ -211,13 +255,13 @@ void resolvingLeavesAnAliasWithNoTagAsWritten() { tags.set("client.port", 1234); tags.set("server.address", "pending"); - DirectionalTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); + DirectionalTags.resolveScopedNames(tags.idStore(), tags.hashStore(), DIRECTION_OUTBOUND); assertEquals(1234, tags.getObject("client.port")); assertEquals("pending", tags.getObject(Tags.PEER_HOSTNAME)); TagMap unknown = TagMap.create(); unknown.set("server.address", "pending"); - DirectionalTags.resolveScopedNames(unknown, DIRECTION_UNKNOWN); + DirectionalTags.resolveScopedNames(unknown.idStore(), unknown.hashStore(), DIRECTION_UNKNOWN); assertEquals("pending", unknown.getObject("server.address")); } @@ -226,7 +270,7 @@ void resolvingWithNoAliasChangesNothing() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "stored"); - DirectionalTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); + DirectionalTags.resolveScopedNames(tags.idStore(), tags.hashStore(), DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertEquals(1, tags.size()); @@ -237,24 +281,34 @@ void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "older"); - DirectionalTags.remove(tags, "server.address", DIRECTION_UNKNOWN); + DirectionalTags.remove(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_UNKNOWN); // once the span is a client, server.address names peer.hostname: the removal must stick - assertNull(DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get( + tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get( + tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND)); } @Test void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { TagMap byName = TagMap.create(); byName.set("server.address", "pending"); - DirectionalTags.remove(byName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); - assertNull(DirectionalTags.get(byName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + DirectionalTags.remove( + byName.idStore(), byName.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); + assertNull( + DirectionalTags.get( + byName.idStore(), byName.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); TagMap byId = TagMap.create(); byId.set("server.address", "pending"); - DirectionalTags.remove(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); - assertNull(DirectionalTags.get(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + DirectionalTags.remove( + byId.idStore(), byId.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + assertNull( + DirectionalTags.get( + byId.idStore(), byId.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); } @Test @@ -263,9 +317,12 @@ void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { tags.set(Tags.PEER_HOSTNAME, "kept"); tags.set("my.tag", "gone"); - DirectionalTags.remove(tags, "my.tag", DIRECTION_UNKNOWN); + DirectionalTags.remove(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_UNKNOWN); assertNull(tags.getObject("my.tag")); - assertEquals("kept", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertEquals( + "kept", + DirectionalTags.get( + tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); } } diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 5ffb1f7868d..5d049ffacb2 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -3,7 +3,15 @@ @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() +# A span's tag stores, and the routing over them, skip checks the tracer core has already made; +# they are @Restricted to the core (see datadog.apicontract.Restricted). Instrumentation uses +# TagMap's checked methods. +@defaultMessage Tag stores are restricted to the tracer core; use TagMap's checked methods instead. +datadog.trace.api.HashStore +datadog.trace.api.IdStore +datadog.trace.api.TagRouting +datadog.trace.api.TagMap$DirectionSource +datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,datadog.trace.api.TagMap$DirectionSource) +datadog.trace.api.TagMap#readThroughStore(datadog.trace.api.TagMap) +datadog.trace.api.TagMap#hashStore() +datadog.trace.api.TagMap#idStore() diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java new file mode 100644 index 00000000000..2b4a87ab39a --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -0,0 +1,72 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.Entry; + +/** The {@link IdStore} view of a {@link HashStore}'s known tags; see {@link HashStore#idStore}. */ +final class HashIdStore extends IdStore { + private final HashStore hash; + + HashIdStore(HashStore hash) { + this.hash = hash; + } + + @Override + public Entry get(long tagId) { + return this.hash.getById(tagId); + } + + @Override + public Object getKnownValue(long tagId) { + return this.hash.getKnownValue(tagId); + } + + @Override + public void set(long tagId, Object value) { + this.hash.setKnown(tagId, value); + } + + @Override + public void set(long tagId, CharSequence value) { + this.hash.setKnown(tagId, value); + } + + @Override + public void set(long tagId, boolean value) { + this.hash.setKnown(tagId, value); + } + + @Override + public void set(long tagId, int value) { + this.hash.setKnown(tagId, value); + } + + @Override + public void set(long tagId, long value) { + this.hash.setKnown(tagId, value); + } + + @Override + public void set(long tagId, float value) { + this.hash.setKnown(tagId, value); + } + + @Override + public void set(long tagId, double value) { + this.hash.setKnown(tagId, value); + } + + @Override + public Entry put(Entry knownEntry) { + return this.hash.put(knownEntry); + } + + @Override + public Entry getAndPut(Entry knownEntry) { + return this.hash.getAndPut(knownEntry); + } + + @Override + public Entry remove(long tagId) { + return this.hash.removeById(tagId); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 3b364db7554..68f0972d157 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -1,5 +1,6 @@ package datadog.trace.api; +import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.BucketGroup; import datadog.trace.api.TagMap.Entry; import datadog.trace.api.TagMap.EntryReader; @@ -16,16 +17,22 @@ * keyed by the 64-bit tag hash (a known tag's id, or a custom tag's name hash in the low 32 bits; * the two never collide, see {@link Entry#tagHashOf}). * - *

It is always a TagMap's name store, holding the custom tags. In hash mode it is also the map's - * {@link IdStore}, holding the known tags too -- one object serves both roles, so splitting the - * stores costs no extra allocation. No locking: guarded by the owner's lock. + *

It is the name store, holding the custom tags by name. Through the tag-hash scheme it can hold + * known tags too, by id: in hash mode it holds both, and {@link #idStore()} adapts its known-tag + * half to the {@link IdStore} API. Other IdStore implementations (a dense store) are separate + * classes. No locking: guarded by the owner's lock. + * + *

Owned by a {@link TagMap}, or directly by a span in the tracer core, which routes to it with + * {@link TagRouting}. Name-keyed methods take only custom tag names: a known tag is resolved to its + * id first, and goes through the id-keyed methods. * *

For memory efficiency, a bucket holds a lone Entry directly. Because Entries are shared * between maps, they cannot form a linked list to handle collisions; colliding entries share a * fixed-size {@link BucketGroup} instead, chained when one fills. Removals are assumed rare, so * groups are never consolidated, but an emptied group is unlinked from its chain. */ -final class HashStore extends IdStore { +@Restricted(allowedIn = {"datadog.trace.core"}) +public final class HashStore { final Object[] buckets; int size; @@ -49,8 +56,16 @@ final class HashStore extends IdStore { */ private Set removedFromParent; - /** A fresh, empty store reading through to {@code parent} (may be null). */ - HashStore(HashStore parent) { + /** A fresh, empty store with no parent. */ + public HashStore() { + this((HashStore) null); + } + + /** + * A fresh, empty store reading through to {@code parent} (may be null), which must be frozen -- + * see {@link TagMap#readThroughStore}. + */ + public HashStore(HashStore parent) { // needs to be a power of 2 for bucket masking calculation to work as intended this.buckets = new Object[1 << 4]; this.size = 0; @@ -135,43 +150,122 @@ int estimateSize() { return total; } + /** A new {@link IdStore} over this store's known tags. */ + public IdStore idStore() { + return new HashIdStore(this); + } + /* - * Known tags, by id (the IdStore role). The id is validated as the TagMap id-keyed API requires. + * Known tags, by id. */ - @Override - Entry get(long tagId) { + /** + * The known tag {@code tagId}'s entry, reading through to the parent chain. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + public Entry getById(long tagId) { return this.get(Entry.requireKnownName(tagId), tagId); } - @Override - Object getKnownValue(long tagId) { - Entry entry = this.get(tagId); + /** {@link #getById}'s value, boxed if necessary, or null. */ + public Object getKnownValue(long tagId) { + Entry entry = this.getById(tagId); return entry == null ? null : entry.objectValue(); } - @Override - Entry remove(long tagId) { + /** + * Removes the known tag {@code tagId}, returning the prior visible entry. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + public Entry removeById(long tagId) { return this.remove(Entry.requireKnownName(tagId), tagId); } + /* + * Sets a known tag by id without validating it: the caller has checked KnownTagCodec.isKnown. + */ + + public void setKnown(long tagId, Object value) { + this.put(new Entry(tagId, null, Entry.ANY, 0L, value)); + } + + public void setKnown(long tagId, CharSequence value) { + this.put(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + } + + public void setKnown(long tagId, boolean value) { + this.put( + new Entry( + tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + } + + public void setKnown(long tagId, int value) { + this.put(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + } + + public void setKnown(long tagId, long value) { + this.put(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + } + + public void setKnown(long tagId, float value) { + this.put(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + } + + public void setKnown(long tagId, double value) { + this.put(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + } + /* * Custom tags, by name (the name-store role). The name must not be a known tag's: a caller * resolves a name once and routes a known tag by its id instead (see TagRouting). */ /** The custom tag {@code tag}'s entry, reading through to the parent chain. */ - Entry get(String tag) { + public Entry get(String tag) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; look it up by its id"; return this.get(tag, Entry.customHash(tag)); } /** Removes the custom tag {@code tag}; see {@link #remove(String, long)}. */ - Entry remove(String tag) { + public Entry remove(String tag) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; remove it by its id"; return this.remove(tag, Entry.customHash(tag)); } + /* + * Sets the custom tag {@code tag}, which must not be a known tag's name. + */ + + public void set(String tag, Object value) { + this.put(Entry.newAnyEntry(tag, value)); + } + + public void set(String tag, CharSequence value) { + this.put(Entry.newObjectEntry(tag, value)); + } + + public void set(String tag, boolean value) { + this.put(Entry.newBooleanEntry(tag, value)); + } + + public void set(String tag, int value) { + this.put(Entry.newIntEntry(tag, value)); + } + + public void set(String tag, long value) { + this.put(Entry.newLongEntry(tag, value)); + } + + public void set(String tag, float value) { + this.put(Entry.newFloatEntry(tag, value)); + } + + public void set(String tag, double value) { + this.put(Entry.newDoubleEntry(tag, value)); + } + /* * Either kind, by the entry's own identity (its canonical name and tag hash). */ @@ -199,10 +293,10 @@ private Entry get(String canonicalTag, long tagHash) { /** * Inserts or replaces a local entry, returning the replaced local Entry (or null if none). Does * NOT consult the read-through parent -- set paths use this so they never pay for a prior-value - * lookup they discard; {@link #getAndPut(Entry)} layers the parent fallback on top. + * lookup they discard; {@link #getAndPut(Entry)} layers the parent fallback on top. Takes either + * kind of entry: the entry already carries its id or its name. */ - @Override - Entry put(Entry newEntry) { + public Entry put(Entry newEntry) { // 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) { @@ -255,8 +349,7 @@ Entry put(Entry newEntry) { } /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - @Override - Entry getAndPut(Entry newEntry) { + public Entry getAndPut(Entry newEntry) { // Capture whether the key was tombstoned BEFORE put clears it: a tombstoned key had no visible // prior value (it was removed), so getAndPut must report null rather than the parent's. boolean wasTombstoned = diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 51542ebb081..ad65da8380c 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -1,38 +1,58 @@ package datadog.trace.api; +import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.Entry; /** - * The known-tag half of a {@link TagMap}: tags keyed by their {@link KnownTags} id. Every method - * takes an id or an Entry already built from one -- never a name, so nothing here can be handed a - * name that a caller has not yet resolved. + * Known tags keyed by their {@link KnownTags} id. Every method takes an id, or an Entry already + * built from one -- never a name, so nothing here can be handed a name that a caller has not yet + * resolved. * - *

Each implementation is its own class, chosen when the owning TagMap is built. In hash mode the - * IdStore is the {@link HashStore} that also holds the custom tags, so the split costs no extra - * object. No locking: guarded by the owner's lock. + *

Each implementation is its own class: today the adapter {@link HashStore#idStore()} returns + * over a hash store's known tags; a dense store later. In hash mode ({@link TagRouting#HASH_MODE}) + * callers reach the hash store directly instead. No locking: guarded by the owner's lock. */ -abstract class IdStore { +@Restricted(allowedIn = {"datadog.trace.core"}) +public abstract class IdStore { /** * The entry for the known tag {@code tagId}, reading through to the parent chain, or null. * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - abstract Entry get(long tagId); + public abstract Entry get(long tagId); /** * {@code get(tagId)}'s value, boxed if necessary, or null -- for a caller that needs only the * value, so a store that keeps known tags without an Entry per tag can answer without one. */ - abstract Object getKnownValue(long tagId); + public abstract Object getKnownValue(long tagId); + + /* + * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. + */ + + public abstract void set(long tagId, Object value); + + public abstract void set(long tagId, CharSequence value); + + public abstract void set(long tagId, boolean value); + + public abstract void set(long tagId, int value); + + public abstract void set(long tagId, long value); + + public abstract void set(long tagId, float value); + + public abstract void set(long tagId, double value); /** * Inserts or replaces a known tag's entry, returning the local entry it replaced (or null). Does * not consult the read-through parent. */ - abstract Entry put(Entry knownEntry); + public abstract Entry put(Entry knownEntry); /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - abstract Entry getAndPut(Entry knownEntry); + public abstract Entry getAndPut(Entry knownEntry); /** * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is @@ -40,5 +60,5 @@ abstract class IdStore { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - abstract Entry remove(long tagId); + public abstract Entry remove(long 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 9cbcdcef61f..6a46bb27238 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1,7 +1,6 @@ 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; @@ -1208,12 +1207,18 @@ public EntryChange next() { * by a global mode, in the constructor that pairs the stores. */ - /** The known tags, by id. In hash mode the same object as {@link #names}. */ + /** The known tags, by id; null in hash mode, where {@link #names} holds them. */ final IdStore ids; /** The custom tags, by name; in hash mode it holds the known tags too. */ final HashStore names; + /** + * For a span's view ({@link #viewOf}), the span, which says what direction-scoped names read as; + * null for a TagMap of its own, which reads every name as written. + */ + final DirectionSource direction; + private boolean frozen; /** @@ -1243,17 +1248,80 @@ private TagMap(TagMap parent) { * store mode is chosen: hash mode uses the one store for both halves. */ private TagMap(TagMap parent, HashStore store) { - this.ids = store; + this.ids = TagRouting.idStoreFor(store); this.names = store; + this.direction = null; this.frozen = false; this.parent = parent; } + /** A live view over stores the caller owns; see {@link #viewOf}. */ + private TagMap(IdStore ids, HashStore names, DirectionSource direction) { + this.ids = ids; + this.names = names; + this.direction = direction; + this.frozen = false; + this.parent = null; + } + + /** + * What direction-scoped names read as on a span: its direction (a {@code + * KnownTagCodec.DIRECTION_*} constant), and whether it may hold an alias stored as written. + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public interface DirectionSource { + int tagDirection(); + + boolean hasPendingAliases(); + } + + /** + * A live, mutable TagMap over a span's stores, for the span's internal readers (tag + * post-processors, serializers). It owns nothing: reads and writes go straight to the stores, and + * name-keyed reads honor the span's direction. Not thread-safe; use it under the lock that guards + * the stores, and only as long as the caller holds it. Small, so a local view is scalar-replaced. + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public static TagMap viewOf(IdStore ids, HashStore names, DirectionSource direction) { + return new TagMap(ids, names, direction); + } + + /** + * The store a span's HashStore reads through to for the tags in {@code parent}, or null when + * there is nothing to read through to; the stores-level {@link #createFromParent}. + * + * @throws IllegalStateException if {@code parent} is not frozen + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public static HashStore readThroughStore(TagMap parent) { + if (parent == null) { + return null; + } + if (!parent.frozen) { + throw new IllegalStateException("read-through parent must be frozen"); + } + // An empty parent is dropped, as createFromParent does. + return parent.isEmpty() ? null : parent.names; + } + + /** This map's HashStore. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public HashStore hashStore() { + return this.names; + } + + /** This map's IdStore, null in hash mode; see {@link TagRouting#idStoreFor}. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public IdStore idStore() { + return this.ids; + } + /** Used for inexpensive immutable */ private TagMap(Object[] buckets, int size) { HashStore store = new HashStore(buckets, size); - this.ids = store; + this.ids = TagRouting.idStoreFor(store); this.names = store; + this.direction = null; this.frozen = true; this.parent = null; } @@ -1387,12 +1455,18 @@ public Set> entrySet() { public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. - return TagRouting.get(this.ids, this.names, TagRouting.keyOf(tag), tag); + long key = TagRouting.keyOf(tag); + DirectionSource direction = this.direction; + if (direction == null || key == 0) { + return TagRouting.get(this.ids, this.names, key, tag); + } + return TagRouting.getEntry( + this.ids, this.names, key, tag, direction.tagDirection(), direction.hasPendingAliases()); } /** The entry for a known tag id. */ public Entry getEntry(long tagId) { - return this.ids.get(tagId); + return TagRouting.getById(this.ids, this.names, tagId); } @Deprecated @@ -1607,74 +1681,10 @@ public Entry removeResolved(long tagId, String tag) { * 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: use it within - * the expression ({@code map.internals().setKnown(...)}) rather than storing it. - */ - @NoEscape - public static final class Internals { - private final TagMap map; - - 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.isKnown(tagId). - */ - public void setKnown(long tagId, @Nonnull Object value) { - this.map.putKnown(new Entry(tagId, null, Entry.ANY, 0L, value)); - } - - public void setKnown(long tagId, @Nonnull CharSequence value) { - this.map.putKnown(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); - } - - public void setKnown(long tagId, boolean value) { - this.map.putKnown( - new Entry( - tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); - } - - public void setKnown(long tagId, int value) { - this.map.putKnown(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); - } - - public void setKnown(long tagId, long value) { - this.map.putKnown(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); - } - - public void setKnown(long tagId, float value) { - this.map.putKnown(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); - } - - public void setKnown(long tagId, double value) { - this.map.putKnown(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); - } - - /** - * The value of the known tag {@code tagId}, boxed if necessary, or null: {@code - * getEntry(tagId)}'s value, including read-through, for a caller that needs only the value. A - * store that keeps known tags without an Entry per tag can answer without building one. - */ - public Object getKnownValue(long tagId) { - return this.map.getKnownValue(tagId); - } - } - /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ + /** {@code getEntry(tagId)}'s value, or null, for a caller that needs only the value. */ Object getKnownValue(long tagId) { - return this.ids.getKnownValue(tagId); + return TagRouting.getKnownValue(this.ids, this.names, tagId); } public void set(long tagId, @Nonnull Object value) { @@ -1736,7 +1746,7 @@ Entry putEntry(@Nonnull Entry newEntry) { void putKnown(Entry knownEntry) { this.checkWriteAccess(); - this.ids.put(knownEntry); + TagRouting.put(this.ids, this.names, knownEntry); } /** {@link #putEntry} for an entry known to be a custom tag's. */ @@ -1805,7 +1815,7 @@ public void putAll(TagMap that) { } private void putAllOptimizedMap(TagMap that) { - if (that.parent != null) { + if (that.names.parent != null) { // read-through source: the bucket-copy paths only see that's local entries, so they would // drop entries visible only through that's ancestor chain (and ignore its tombstones). // Union-copy the full visible set instead -- still shares the source Entry objects. @@ -1816,7 +1826,7 @@ private void putAllOptimizedMap(TagMap that) { } public void fillMap(Map map) { - if (this.parent != null) { + if (this.names.parent != null) { // read-through source: the bucket-only loop below sees just local entries, so it would drop // tags visible only through the parent chain (and ignore shadowing/tombstones). Walk the full // visible union instead -- mirrors putAllOptimizedMap. @@ -1827,7 +1837,7 @@ public void fillMap(Map map) { } public void fillStringMap(Map stringMap) { - if (this.parent != null) { + if (this.names.parent != null) { // read-through source: walk the visible union so parent-only tags aren't dropped -- see // fillMap. this.forEach(stringMap, (m, entry) -> m.put(entry.tag(), entry.stringValue())); @@ -1860,7 +1870,7 @@ public Entry getAndRemove(String tag) { public Entry getAndRemove(long tagId) { this.checkWriteAccess(); - return this.ids.remove(tagId); + return TagRouting.removeById(this.ids, this.names, tagId); } public TagMap copy() { diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index 7cd3b2ef0c5..f7bf5308f72 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -1,52 +1,264 @@ package datadog.trace.api; +import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; + +import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.Entry; /** * Where a tag name is resolved and an operation is routed to the store that holds it: a known tag - * to the {@link IdStore} by its id, a custom tag to the name store by its name. The one place a - * TagMap turns a name into an id, so the stores themselves never see a name that might be a known - * tag's. + * to the {@link IdStore} by its id, a custom tag to the {@link HashStore} by its name. The one + * place a name becomes an id, so the stores themselves never see a name that might be a known + * tag's. Shared by {@link TagMap} and by the tracer core's spans, which own their stores directly. * *

A name resolves once, by {@link #keyOf}, to a key: a known tag's id ({@code > 0}), 0 for a * custom tag, or a negative sentinel for a direction-scoped OpenTelemetry alias ({@code * server.address}), whose tag depends on the span's direction. An alias is the caller's to decide: - * the routing methods treat any key that is not an id as custom, which is what a TagMap -- it has - * no direction -- does with an alias; a caller that knows the direction resolves the alias to its - * tag first. + * the key-taking methods treat any key that is not an id as custom, which is what a TagMap does + * with an alias, having no direction. {@link #getEntry(IdStore, HashStore, long, String, int, + * boolean)} reads an alias by a span's direction. * - *

Static, so a span can route to its stores the same way a TagMap does. + *

In {@link #HASH_MODE} known tags live in the HashStore too, and the id-keyed methods go to it + * directly: the flag is a constant, so the IdStore argument folds away and may be null. */ -final class TagRouting { +@Restricted(allowedIn = {"datadog.trace.core"}) +public final class TagRouting { + /** + * True while known tags are kept in the HashStore: id-keyed operations call it directly rather + * than through an {@link IdStore}, so an owner needs no IdStore object. False routes them through + * the IdStore -- the shape a dense store needs. + */ + public static final boolean HASH_MODE = true; + private TagRouting() {} + /** The IdStore an owner of {@code hash} pairs with it: none in hash mode. */ + public static IdStore idStoreFor(HashStore hash) { + return HASH_MODE ? null : hash.idStore(); + } + /** {@code tag}'s key: its id when known, 0 when custom, negative for a direction-scoped alias. */ - static long keyOf(String tag) { + public static long keyOf(String tag) { return KnownTagCodec.keyOrAliasOf(tag); } /** Whether {@code key} is a known tag's id, as opposed to a custom tag or an alias. */ - static boolean isKnown(long key) { + public static boolean isKnown(long key) { return key > 0; } - /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ - static Entry get(IdStore ids, HashStore names, long key, String tag) { - return isKnown(key) ? ids.get(key) : names.get(tag); + /* + * By id. + */ + + /** The known tag {@code tagId}'s entry; throws for an id that names no known tag. */ + public static Entry getById(IdStore ids, HashStore hash, long tagId) { + return HASH_MODE ? hash.getById(tagId) : ids.get(tagId); } - /** Removes {@code tag}, already resolved to {@code key}, returning the prior visible entry. */ - static Entry remove(IdStore ids, HashStore names, long key, String tag) { - return isKnown(key) ? ids.remove(key) : names.remove(tag); + /** {@link #getById}'s value, or null. */ + public static Object getKnownValue(IdStore ids, HashStore hash, long tagId) { + return HASH_MODE ? hash.getKnownValue(tagId) : ids.getKnownValue(tagId); + } + + /** Removes the known tag {@code tagId}; throws for an id that names no known tag. */ + public static Entry removeById(IdStore ids, HashStore hash, long tagId) { + return HASH_MODE ? hash.removeById(tagId) : ids.remove(tagId); + } + + /* + * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. + */ + + public static void setKnown(IdStore ids, HashStore hash, long tagId, Object value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } + } + + public static void setKnown(IdStore ids, HashStore hash, long tagId, CharSequence value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } } + public static void setKnown(IdStore ids, HashStore hash, long tagId, boolean value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } + } + + public static void setKnown(IdStore ids, HashStore hash, long tagId, int value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } + } + + public static void setKnown(IdStore ids, HashStore hash, long tagId, long value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } + } + + public static void setKnown(IdStore ids, HashStore hash, long tagId, float value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } + } + + public static void setKnown(IdStore ids, HashStore hash, long tagId, double value) { + if (HASH_MODE) { + hash.setKnown(tagId, value); + } else { + ids.set(tagId, value); + } + } + + /* + * By entry, which already carries its id or its name. + */ + /** Puts {@code entry} into the store for its kind, returning the local entry it replaced. */ - static Entry put(IdStore ids, HashStore names, Entry entry) { - return entry.tagId() != 0 ? ids.put(entry) : names.put(entry); + public static Entry put(IdStore ids, HashStore hash, Entry entry) { + return HASH_MODE || entry.tagId() == 0 ? hash.put(entry) : ids.put(entry); } /** {@link #put}, returning the prior visible entry. */ - static Entry getAndPut(IdStore ids, HashStore names, Entry entry) { - return entry.tagId() != 0 ? ids.getAndPut(entry) : names.getAndPut(entry); + public static Entry getAndPut(IdStore ids, HashStore hash, Entry entry) { + return HASH_MODE || entry.tagId() == 0 ? hash.getAndPut(entry) : ids.getAndPut(entry); + } + + /* + * By name, already resolved to its key by keyOf. A key that is not an id is a custom tag. + */ + + /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ + public static Entry get(IdStore ids, HashStore hash, long key, String tag) { + return isKnown(key) ? getById(ids, hash, key) : hash.get(tag); + } + + /** Removes {@code tag}, already resolved to {@code key}, returning the prior visible entry. */ + public static Entry remove(IdStore ids, HashStore hash, long key, String tag) { + return isKnown(key) ? removeById(ids, hash, key) : hash.remove(tag); + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, Object value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, CharSequence value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, boolean value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, int value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, long value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, float value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + public static void set(IdStore ids, HashStore hash, long key, String tag, double value) { + if (isKnown(key)) { + setKnown(ids, hash, key, value); + } else { + hash.set(tag, value); + } + } + + /** + * Copies every entry visible in {@code source} into the stores, sharing the Entry objects; see + * {@link TagMap#putAll(TagMap)}. + */ + public static void putAll(IdStore ids, HashStore hash, TagMap source) { + TagMap.viewOf(ids, hash, null).putAll(source); + } + + /* + * By name, on a span of a direction. A direction-scoped alias reads the tag it denotes, and a tag + * reads a pending alias (one stored as written, before the span had a direction), which, being + * the newer write, wins. + */ + + /** + * The entry {@code name}, resolved to {@code key}, reads on a span of {@code direction}. With + * {@code mayHavePendingAliases} false, a tag with an alias skips probing for one, so it costs a + * single lookup like any other tag. + */ + public static Entry getEntry( + IdStore ids, + HashStore hash, + long key, + String name, + int direction, + boolean mayHavePendingAliases) { + if (isKnown(key)) { + return getById(ids, hash, key, direction, mayHavePendingAliases); + } + Entry asWritten = key == 0 || mayHavePendingAliases ? hash.get(name) : null; + if (key == 0 || asWritten != null) { + return asWritten; + } + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + return isKnown(tagId) ? getById(ids, hash, tagId) : null; + } + + /** The entry the known tag {@code tagId} reads on a span of {@code direction}; see above. */ + public static Entry getById( + IdStore ids, HashStore hash, long tagId, int direction, boolean mayHavePendingAliases) { + if (mayHavePendingAliases + && KnownTagCodec.hasScopedAlias(tagId) + && direction != DIRECTION_UNKNOWN) { + String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); + Entry pending = alias == null ? null : hash.get(alias); + if (pending != null) { + return pending; + } + } + return getById(ids, hash, tagId); } } 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 e5463372094..bfbfaa45234 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -80,10 +80,9 @@ void theCoresKnownValueReadMatchesTheEntrysValue() { TagMap tags = TagMap.createFromParent(parent.freeze()); tags.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); - assertEquals("/users/{id}", tags.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals( - 200, tags.internals().getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); // read-through - assertNull(tags.internals().getKnownValue(KnownTags.HTTP_METHOD_ID)); + assertEquals("/users/{id}", tags.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(200, tags.getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); // read-through + assertNull(tags.getKnownValue(KnownTags.HTTP_METHOD_ID)); } @Test @@ -105,40 +104,4 @@ void aResolvedWriteOfAKnownTagStoresWhatAnIdWriteStores() { resolved.remove("my.custom.tag"); assertEquals(new HashMap<>(byId), new HashMap<>(resolved)); } - - @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()); - } - - @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()); - } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java new file mode 100644 index 00000000000..3020a630502 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -0,0 +1,274 @@ +package datadog.trace.api; + +import static datadog.trace.api.KnownTagCodec.DIRECTION_INBOUND; +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +/** + * The tag stores a span owns directly ({@link HashStore}, its {@link IdStore} adapter), the {@link + * TagRouting} over them, and the {@link TagMap} view a span hands its internal readers. + */ +class TagStoresTest { + @Test + void trustedKnownSetsStoreWhatTheCheckedSetsStore() { + Object component = new StringBuilder("jdbc"); // not a String: exercises the Object overload + 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); + checked.set(KnownTags.COMPONENT_ID, component); + checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); + + HashStore hash = new HashStore(); + hash.setKnown(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); + hash.setKnown(KnownTags.HTTP_STATUS_CODE_ID, 200); + hash.setKnown(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + hash.setKnown(KnownTags.DD_PROFILING_ENABLED_ID, true); + hash.setKnown(KnownTags.COMPONENT_ID, component); + hash.setKnown(KnownTags.DB_POOL_NAME_ID, 1.5f); + hash.setKnown(KnownTags.DB_INSTANCE_ID, 2.25d); + + assertEquals(new HashMap<>(checked), new HashMap<>(TagMap.viewOf(null, hash, null))); + assertEquals(KnownTags.HTTP_ROUTE_ID, hash.getById(KnownTags.HTTP_ROUTE_ID).tagId()); + assertEquals(1.5f, hash.getById(KnownTags.DB_POOL_NAME_ID).floatValue()); + } + + @Test + void theIdStoreAdapterReadsAndWritesTheHashStoresKnownTags() { + HashStore hash = new HashStore(); + IdStore ids = hash.idStore(); + ids.set(KnownTags.HTTP_ROUTE_ID, (Object) "/a"); + ids.set(KnownTags.COMPONENT_ID, "jdbc"); + ids.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + ids.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + ids.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + ids.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + ids.set(KnownTags.DB_INSTANCE_ID, 2.25d); + + assertEquals("/a", hash.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("jdbc", ids.get(KnownTags.COMPONENT_ID).stringValue()); + assertEquals(true, ids.getKnownValue(KnownTags.DD_PROFILING_ENABLED_ID)); + assertEquals(200, ids.getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); + assertEquals(2L, ids.getKnownValue(KnownTags.HTTP_RESEND_COUNT_ID)); + assertEquals(1.5f, ids.getKnownValue(KnownTags.DB_POOL_NAME_ID)); + assertEquals(2.25d, ids.getKnownValue(KnownTags.DB_INSTANCE_ID)); + + TagMap.Entry route = TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/b"); + assertEquals("/a", ids.put(route).stringValue()); + assertEquals( + "/b", ids.getAndPut(TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/c")).stringValue()); + assertEquals("/c", ids.remove(KnownTags.HTTP_ROUTE_ID).stringValue()); + assertNull(hash.getById(KnownTags.HTTP_ROUTE_ID)); + assertThrows(IllegalArgumentException.class, () -> ids.get(0L)); + } + + @Test + void customTagsAreKeptByName() { + HashStore hash = new HashStore(); + hash.set("a", (Object) "x"); + hash.set("b", "y"); + hash.set("c", true); + hash.set("d", 1); + hash.set("e", 2L); + hash.set("f", 3f); + hash.set("g", 4d); + + assertEquals("x", hash.get("a").objectValue()); + assertEquals("y", hash.get("b").stringValue()); + assertTrue(hash.get("c").booleanValue()); + assertEquals(1, hash.get("d").intValue()); + assertEquals(2L, hash.get("e").longValue()); + assertEquals(3f, hash.get("f").floatValue()); + assertEquals(4d, hash.get("g").doubleValue()); + assertEquals("x", hash.remove("a").objectValue()); + assertNull(hash.get("a")); + } + + @Test + void aNameStoreRejectsAKnownTagsName() { + HashStore hash = new HashStore(); + assertThrows(AssertionError.class, () -> hash.get(KnownTags.HTTP_ROUTE_NAME)); + assertThrows(AssertionError.class, () -> hash.remove(KnownTags.HTTP_ROUTE_NAME)); + } + + @Test + void routingSendsKnownTagsByIdAndEverythingElseByName() { + HashStore hash = new HashStore(); + IdStore ids = TagRouting.idStoreFor(hash); + + long route = TagRouting.keyOf(KnownTags.HTTP_ROUTE_NAME); + long custom = TagRouting.keyOf("my.tag"); + long alias = TagRouting.keyOf("server.address"); + assertEquals(KnownTags.HTTP_ROUTE_ID, route); + assertEquals(0L, custom); + assertTrue(alias < 0); + + TagRouting.set(ids, hash, route, KnownTags.HTTP_ROUTE_NAME, (Object) "/a"); + TagRouting.set(ids, hash, custom, "my.tag", (Object) "custom"); + TagRouting.set(ids, hash, alias, "server.address", (Object) "as-written"); + + assertEquals("/a", TagRouting.get(ids, hash, route, KnownTags.HTTP_ROUTE_NAME).stringValue()); + assertEquals("/a", TagRouting.getKnownValue(ids, hash, KnownTags.HTTP_ROUTE_ID)); + assertEquals("custom", hash.get("my.tag").stringValue()); + assertEquals("as-written", hash.get("server.address").stringValue()); // an alias is custom + + assertNotNull(TagRouting.remove(ids, hash, route, KnownTags.HTTP_ROUTE_NAME)); + assertNotNull(TagRouting.remove(ids, hash, custom, "my.tag")); + assertNull(TagRouting.getById(ids, hash, KnownTags.HTTP_ROUTE_ID)); + assertNull(hash.get("my.tag")); + } + + @Test + void routingSetsEveryValueShapeByKey() { + HashStore hash = new HashStore(); + IdStore ids = TagRouting.idStoreFor(hash); + TagRouting.set(ids, hash, KnownTags.COMPONENT_ID, "component", "jdbc"); + TagRouting.set(ids, hash, KnownTags.DD_PROFILING_ENABLED_ID, "x", true); + TagRouting.set(ids, hash, KnownTags.HTTP_STATUS_CODE_ID, "x", 200); + TagRouting.set(ids, hash, KnownTags.HTTP_RESEND_COUNT_ID, "x", 2L); + TagRouting.set(ids, hash, KnownTags.DB_POOL_NAME_ID, "x", 1.5f); + TagRouting.set(ids, hash, KnownTags.DB_INSTANCE_ID, "x", 2.25d); + TagRouting.set(ids, hash, 0L, "s", "v"); + TagRouting.set(ids, hash, 0L, "b", true); + TagRouting.set(ids, hash, 0L, "i", 1); + TagRouting.set(ids, hash, 0L, "l", 2L); + TagRouting.set(ids, hash, 0L, "f", 3f); + TagRouting.set(ids, hash, 0L, "d", 4d); + + TagMap expected = TagMap.create(); + expected.set(KnownTags.COMPONENT_ID, "jdbc"); + expected.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + expected.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + expected.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + expected.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + expected.set(KnownTags.DB_INSTANCE_ID, 2.25d); + expected.set("s", "v"); + expected.set("b", true); + expected.set("i", 1); + expected.set("l", 2L); + expected.set("f", 3f); + expected.set("d", 4d); + assertEquals(new HashMap<>(expected), new HashMap<>(TagMap.viewOf(ids, hash, null))); + } + + @Test + void routingPutsEntriesByTheirKind() { + HashStore hash = new HashStore(); + IdStore ids = TagRouting.idStoreFor(hash); + assertNull(TagRouting.put(ids, hash, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/a"))); + assertNull(TagRouting.getAndPut(ids, hash, TagMap.Entry.create("my.tag", "x"))); + assertEquals( + "/a", + TagRouting.getAndPut(ids, hash, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/b")) + .stringValue()); + assertEquals("x", TagRouting.put(ids, hash, TagMap.Entry.create("my.tag", "y")).stringValue()); + } + + @Test + void putAllCopiesTheVisibleTags() { + TagMap parent = TagMap.create(); + parent.set("inherited", "p"); + TagMap source = TagMap.createFromParent(parent.freeze()); + source.set(KnownTags.HTTP_ROUTE_ID, "/a"); + + HashStore hash = new HashStore(); + TagRouting.putAll(TagRouting.idStoreFor(hash), hash, source); + assertEquals("p", hash.get("inherited").stringValue()); + assertEquals("/a", hash.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + } + + @Test + void aViewReadsAnAliasByTheSpansDirection() { + HashStore hash = new HashStore(); + hash.setKnown(KnownTags.HTTP_HOSTNAME_ID, "inbound.example"); + hash.setKnown(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); + + TagMap inbound = TagMap.viewOf(null, hash, direction(DIRECTION_INBOUND, false)); + TagMap outbound = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, false)); + TagMap plain = TagMap.viewOf(null, hash, null); + + assertEquals("inbound.example", inbound.getString("server.address")); + assertEquals("outbound.example", outbound.getString("server.address")); + assertNull(plain.getString("server.address")); // a TagMap of its own reads names as written + assertEquals("inbound.example", outbound.getString(KnownTags.HTTP_HOSTNAME_NAME)); + } + + @Test + void aViewReadsAPendingAliasOverItsTag() { + HashStore hash = new HashStore(); + hash.setKnown(KnownTags.PEER_HOSTNAME_ID, "older"); + hash.set("server.address", "pending"); + + TagMap pending = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, true)); + TagMap noPending = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, false)); + TagMap unknown = TagMap.viewOf(null, hash, direction(DIRECTION_UNKNOWN, true)); + + assertEquals("pending", pending.getString(KnownTags.PEER_HOSTNAME_NAME)); + assertEquals("pending", pending.getString("server.address")); + assertEquals("older", noPending.getString(KnownTags.PEER_HOSTNAME_NAME)); + assertEquals("older", unknown.getString(KnownTags.PEER_HOSTNAME_NAME)); + assertEquals("pending", unknown.getString("server.address")); + assertNull(noPending.getString("client.port")); // no tag in this direction + } + + @Test + void aViewIsLiveOverTheStores() { + HashStore hash = new HashStore(); + TagMap view = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, false)); + view.set(KnownTags.HTTP_ROUTE_NAME, "/a"); + view.set("my.tag", "x"); + assertEquals("/a", hash.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("x", hash.get("my.tag").stringValue()); + + TagMap copy = view.copy(); + view.remove("my.tag"); + assertEquals("x", copy.getString("my.tag")); // a copy is a TagMap of its own + assertFalse(view.containsKey("my.tag")); + assertSame(hash, view.hashStore()); + } + + @Test + void readThroughStoreMirrorsCreateFromParent() { + assertNull(TagMap.readThroughStore(null)); + assertThrows(IllegalStateException.class, () -> TagMap.readThroughStore(TagMap.create())); + assertNull(TagMap.readThroughStore(TagMap.create().freeze())); + + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.freeze(); + HashStore parentStore = TagMap.readThroughStore(parent); + assertSame(parent.hashStore(), parentStore); + + HashStore child = new HashStore(parentStore); + assertEquals("/inherited", child.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + child.removeById(KnownTags.HTTP_ROUTE_ID); // tombstones the inherited tag + assertNull(child.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/inherited", parent.getString(KnownTags.HTTP_ROUTE_NAME)); + } + + private static TagMap.DirectionSource direction(int direction, boolean pendingAliases) { + return new TagMap.DirectionSource() { + @Override + public int tagDirection() { + return direction; + } + + @Override + public boolean hasPendingAliases() { + return pendingAliases; + } + }; + } +} From 7d80723b49d7bd15bb18e184a4aa17b7a0fd5b36 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 10:21:16 -0400 Subject: [PATCH 55/99] Add a benchmark of reading tags back from a live span Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/GetTagBenchmark.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java new file mode 100644 index 00000000000..d0e1f4ef64a --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java @@ -0,0 +1,79 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import datadog.trace.api.KnownTags; +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 reading tags back from a live span, by how the tag is named: a known tag's name, a + * custom name, a direction-scoped alias, and a miss. {@code getTag} takes the span's tag lock; + * {@code unsafeGetTag} reads without it, as the tracer's internal readers do. + */ +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@Fork(value = 3) +public class GetTagBenchmark { + static final CoreTracer TRACER = CoreTracer.builder().build(); + + private DDSpan span; + + @Setup + public void setup() { + span = (DDSpan) TRACER.startSpan("benchmark", "get.tag"); + span.setTag("span.kind", "client"); + span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}"); + span.setTag(KnownTags.PEER_HOSTNAME_NAME, "db.example"); + span.setTag(KnownTags.HTTP_STATUS_CODE_NAME, 200); + span.setTag("my.custom.tag", "custom"); + span.setTag("my.other.tag", 42); + } + + @TearDown + public void tearDown() { + span.finish(); + } + + @Benchmark + public Object knownTag_getTag() { + return span.getTag(KnownTags.HTTP_ROUTE_NAME); + } + + @Benchmark + public Object customTag_getTag() { + return span.getTag("my.custom.tag"); + } + + @Benchmark + public Object aliasName_getTag() { + return span.getTag("server.address"); + } + + @Benchmark + public Object missingTag_getTag() { + return span.getTag("not.there"); + } + + @Benchmark + public Object knownTag_unsafeGetTag() { + return span.unsafeGetTag(KnownTags.HTTP_ROUTE_NAME); + } + + @Benchmark + public Object customTag_unsafeGetTag() { + return span.unsafeGetTag("my.custom.tag"); + } +} From 5f7458b99728d8cb3a6be3e1ea6c7dff5c6bc15a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 12:29:46 -0400 Subject: [PATCH 56/99] Copy tracer-level tags into a span's store without a view TagRouting.putAll built a TagMap view to reuse TagMap.putAll, but putAll is too big to inline, so the view escaped and cost an allocation on the span's tag-seeding path. In hash mode it now copies into the HashStore directly. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagRouting.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index f7bf5308f72..b0ff5a74578 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -215,7 +215,15 @@ public static void set(IdStore ids, HashStore hash, long key, String tag, double * {@link TagMap#putAll(TagMap)}. */ public static void putAll(IdStore ids, HashStore hash, TagMap source) { - TagMap.viewOf(ids, hash, null).putAll(source); + if (!HASH_MODE) { + TagMap.viewOf(ids, hash, null).putAll(source); + } else if (source.names.parent != null) { + // read-through source: copy the full visible set, as TagMap.putAll does + source.forEach(hash, (target, entry) -> target.put(entry.entry())); + } else { + // No view here: putAll is too big to inline, so a view would escape and be allocated. + hash.putAllLocal(source.names); + } } /* From ac7a00da093dffdca393200a51797846ebb8147d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 12:58:28 -0400 Subject: [PATCH 57/99] Cache the span's view of its tag stores The view handed to the post-processor chain escapes, so building one per use cost an allocation at trace write and another at serialization. The span now creates one lazily, under its tag lock, and reuses it. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/DDSpanContext.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 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 97d76426f63..d28bf36dd27 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 @@ -192,6 +192,13 @@ private static int[] tagDirectionOfSpanKind() { private final IdStore tagIds; + /** + * This span's one {@link #tagView() view} of its tag stores, created on first use under the + * {@code tagStore} lock and reused by every internal reader. The view holds no state of its own + * beyond the stores and this span (its direction source), so it stays live and correct. + */ + private TagMap tagView; + /** The service name is required, otherwise the span are dropped by the agent */ private volatile String serviceName; @@ -980,10 +987,15 @@ public void setSpanKindOrdinal(String kind) { /** * A live view of this span's tag stores, for its internal readers: post-processors and the - * serializer. Use it only under the {@code tagStore} lock, and only while holding it. + * serializer. Call it, and use the view, only under the {@code tagStore} lock. */ private TagMap tagView() { - return TagMap.viewOf(tagIds, tagStore, this); + TagMap view = this.tagView; + if (view == null) { + // a view handed to the post-processor chain escapes, so allocate it once per span + this.tagView = view = TagMap.viewOf(tagIds, tagStore, this); + } + return view; } /** From 2d0ef38249f2163eee309bf9f2f3befe048d9df3 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 13:13:45 -0400 Subject: [PATCH 58/99] Keep a span's pending aliases in their own holder A direction-scoped alias written before the span has a kind is no longer stored in the span's HashStore. It goes into a small holder that DirectionalTags owns, which stays null until the first such write, so the HashStore holds only custom tags. Kind-set resolution walks only the holder, and once every alias has resolved the holder is dropped, so the span returns to the fast path rather than staying flagged for life. The id setters' routing gate now tests the holder for null instead of reading a sticky flag. Reads of a tag with an alias probe the holder only when there is one. Last-write-wins and the removal rules are unchanged. Emitting the span's tags (trace-write and serialization processing, and the getTags snapshot) copies the holder's entries in under their literal names, so a span that never learns its kind still emits them. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 184 ++++++++---- .../datadog/trace/core/DirectionalTags.java | 264 +++++++++++++++--- 2 files changed, 359 insertions(+), 89 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 d28bf36dd27..fa732837a20 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 @@ -47,7 +47,6 @@ 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; @@ -158,18 +157,17 @@ private static int[] tagDirectionOfSpanKind() { 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. + * The direction-scoped aliases this span stored as written, with their values (see {@link + * DirectionalTags}): null until the first one, and again once the span's kind resolves them all. + * While null -- 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 tagStore} 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. + *

Written under the {@code tagStore} lock. {@link #routedTagFlags()} reads it without the + * lock, deliberately: a setter on another thread that has not yet seen the holder 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 DirectionalTags.PendingAliases pendingAliases; private CharSequence integrationName; private CharSequence serviceNameSource; @@ -976,15 +974,29 @@ public void setSpanKindOrdinal(String kind) { // Checked under the lock alias registration takes: a writer that read no direction registers // its pending alias before this sees the flag, or reads the new kind and resolves at write. synchronized (tagStore) { - if (hasPendingAliases()) { + DirectionalTags.PendingAliases pending = pendingAliases; + if (pending != null) { // The span now has a direction: fold pending aliases into the tags they denote, so every // later reader (peer service naming, stats, sampling rules, serialization) sees them - // resolved. - DirectionalTags.resolveScopedNames(tagIds, tagStore, tagDirection()); + // resolved. Once none is left, the span is back on the fast path. + pendingAliases = DirectionalTags.resolvePending(tagIds, tagStore, pending, tagDirection()); } } } + /** + * {@link #tagView()} for emitting the span's tags: first copies any pending aliases into the + * HashStore under their literal names, so a span that never learns its kind still emits them. The + * holder stays the span's record of them. Call under the {@code tagStore} lock. + */ + private TagMap emittableTags() { + DirectionalTags.PendingAliases pending = pendingAliases; + if (pending != null) { + DirectionalTags.copyPending(pending, tagStore); + } + return tagView(); + } + /** * A live view of this span's tag stores, for its internal readers: post-processors and the * serializer. Call it, and use the view, only under the {@code tagStore} lock. @@ -1108,57 +1120,109 @@ private long writeKeyOf(String tag) { if (key == 0) { return 0L; } - long aliasedId = DirectionalTags.prepareAliasWrite(tagIds, tagStore, tag, tagDirection()); + long aliasedId = + DirectionalTags.prepareAliasWrite(tagIds, tagStore, pendingAliases, tag, tagDirection()); if (aliasedId > 0) { + dropPendingAliasesIfEmpty(); return aliasedId; } - notePendingAlias(); // the registry said alias, so this is the sentinel: stored as written - return 0L; + return PENDING_ALIAS; // the registry said alias, so this is the sentinel: stored as written + } + + /** {@link #writeKeyOf}'s answer for an alias that is stored as written, pending. */ + private static final long PENDING_ALIAS = -1L; + + /** Stores {@code value} under {@code alias} as written, pending. Call under the lock. */ + private void storePendingAlias(String alias, Object value) { + pendingAliases = DirectionalTags.addPending(pendingAliases, alias, value); + } + + /** Drops the pending-alias holder once empty, so the span is back on the fast path. */ + private void dropPendingAliasesIfEmpty() { + DirectionalTags.PendingAliases pending = pendingAliases; + if (pending != null && pending.isEmpty()) { + pendingAliases = null; + } } /** - * 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 tagStore} lock. + * The tag id flags the id setters route through {@link TagInterceptor}: {@link + * KnownTagCodec#INTERCEPTED}, plus {@link KnownTagCodec#HAS_SCOPED_ALIAS} while the span holds a + * pending alias, so a write of a tag with an alias is routed to supersede it. */ - @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); + private int routedTagFlags() { + return pendingAliases == null + ? (int) KnownTagCodec.INTERCEPTED + : (int) (KnownTagCodec.INTERCEPTED | KnownTagCodec.HAS_SCOPED_ALIAS); } - /** True once any alias has been stored as written; see {@link #routedTagFlags}. */ + /** True while the span holds an alias stored as written; see {@link #pendingAliases}. */ @Override public boolean hasPendingAliases() { - return (routedTagFlags & KnownTagCodec.HAS_SCOPED_ALIAS) != 0; + return pendingAliases != null; } private void storeTag(String tag, Object value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } private void storeTag(String tag, CharSequence value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } private void storeTag(String tag, boolean value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } private void storeTag(String tag, int value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } private void storeTag(String tag, long value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } private void storeTag(String tag, float value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } private void storeTag(String tag, double value) { - TagRouting.set(tagIds, tagStore, writeKeyOf(tag), tag, value); + long key = writeKeyOf(tag); + if (key == PENDING_ALIAS) { + storePendingAlias(tag, value); + } else { + TagRouting.set(tagIds, tagStore, key, tag, value); + } } /** {@link #storeTag} for an entry, which carries its id: no lookup unless it may be an alias. */ @@ -1169,14 +1233,15 @@ private void storeEntry(TagMap.EntryReader entry) { putEntry(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { long aliasedId = - DirectionalTags.prepareAliasWrite(tagIds, tagStore, entry.tag(), tagDirection()); + DirectionalTags.prepareAliasWrite( + tagIds, tagStore, pendingAliases, entry.tag(), tagDirection()); if (aliasedId > 0) { + dropPendingAliasesIfEmpty(); TagRouting.setKnown(tagIds, tagStore, aliasedId, entry.objectValue()); + } else if (aliasedId < 0) { + storePendingAlias(entry.tag(), entry.objectValue()); // a real alias, stored as written } else { - if (aliasedId < 0) { - notePendingAlias(); // a real alias, stored as written; 0 is a filter false positive - } - putEntry(entry); + putEntry(entry); // 0 is a filter false positive: an ordinary custom tag } } else { putEntry(entry); @@ -1198,7 +1263,8 @@ public void removeTag(String tag) { } synchronized (tagStore) { // A direction-dependent name and the tag it denotes here are removed together. - DirectionalTags.remove(tagIds, tagStore, tag, tagDirection()); + DirectionalTags.remove(tagIds, tagStore, pendingAliases, tag, tagDirection()); + dropPendingAliasesIfEmpty(); } } @@ -1216,7 +1282,8 @@ public void removeTag(long tagId) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (tagStore) { - DirectionalTags.remove(tagIds, tagStore, tagId, tagDirection()); + DirectionalTags.remove(tagIds, tagStore, pendingAliases, tagId, tagDirection()); + dropPendingAliasesIfEmpty(); } } @@ -1282,7 +1349,7 @@ private boolean interceptEntry(TagMap.EntryReader entry) { */ private boolean isPresent(TagMap.EntryReader entry) { if (KnownTagCodec.isDirectionDependent(entry)) { - return DirectionalTags.get(tagIds, tagStore, entry.tag(), tagDirection(), hasPendingAliases()) + return DirectionalTags.get(tagIds, tagStore, pendingAliases, entry.tag(), tagDirection()) != null; } String tag = entry.tag(); @@ -1325,7 +1392,7 @@ 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 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 + * #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. */ @@ -1343,8 +1410,10 @@ public void supersedeAliases(long tagId) { * span holds any. Callers hold the {@code tagStore} lock. */ private void supersedePendingAliases(long tagId) { - if (KnownTagCodec.hasScopedAlias(tagId) && hasPendingAliases()) { - DirectionalTags.prepareWrite(tagIds, tagStore, tagId, tagDirection()); + DirectionalTags.PendingAliases pending = pendingAliases; + if (pending != null && KnownTagCodec.hasScopedAlias(tagId)) { + DirectionalTags.prepareWrite(tagIds, tagStore, pending, tagId, tagDirection()); + dropPendingAliasesIfEmpty(); } } @@ -1359,7 +1428,7 @@ public void setTag(final long tagId, final Object value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) + } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (tagStore) { TagRouting.setKnown(tagIds, tagStore, tagId, value); @@ -1373,7 +1442,7 @@ public void setTag(final long tagId, final CharSequence value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) + } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (tagStore) { TagRouting.setKnown(tagIds, tagStore, tagId, value); @@ -1385,7 +1454,7 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { synchronized (tagStore) { @@ -1398,7 +1467,7 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { synchronized (tagStore) { @@ -1411,7 +1480,7 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { synchronized (tagStore) { @@ -1424,7 +1493,7 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { synchronized (tagStore) { @@ -1437,7 +1506,7 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { synchronized (tagStore) { @@ -1596,7 +1665,8 @@ void setAllTags(final TagMap.Ledger ledger) { spanKindOrdinal = SPAN_KIND_UNSET; } // as removeTag(String): a direction-dependent name and the tag it denotes go together - DirectionalTags.remove(tagIds, tagStore, tag, tagDirection()); + DirectionalTags.remove(tagIds, tagStore, pendingAliases, tag, tagDirection()); + dropPendingAliasesIfEmpty(); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; @@ -1675,13 +1745,17 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return DirectionalTags.get(tagIds, tagStore, tag, tagDirection(), hasPendingAliases()); + return DirectionalTags.get(tagIds, tagStore, pendingAliases, tag, tagDirection()); } @Deprecated public TagMap getTags() { synchronized (tagStore) { TagMap tags = tagView().copy(); + DirectionalTags.PendingAliases pending = pendingAliases; + if (pending != null) { + DirectionalTags.copyPending(pending, tags.hashStore()); // under their literal names + } tags.put(DDTags.THREAD_ID, threadId); // maintain previously observable type of the thread name :| @@ -1734,7 +1808,7 @@ public void setMetaStruct(final String field, final T value) { void earlyProcessTags(AppendableSpanLinks links) { synchronized (tagStore) { - TagsPostProcessorFactory.eagerProcessor().processTags(tagView(), this, links); + TagsPostProcessorFactory.eagerProcessor().processTags(emittableTags(), this, links); } } @@ -1765,7 +1839,7 @@ void processTagsAndBaggage( // - implementing an interface to read the spans that require making the read method public synchronized (tagStore) { // One live view of the tag stores, for the post-processors and the serializer - TagMap tags = tagView(); + TagMap tags = emittableTags(); // Tags TagsPostProcessorFactory.lazyProcessor().processTags(tags, this, restrictedSpan); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index 09dd5e0dd84..32b233a00f4 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -10,6 +10,7 @@ import datadog.trace.api.TagMap; import datadog.trace.api.TagRouting; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -48,24 +49,101 @@ * ({@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}). * + *

A span keeps its pending aliases -- the aliases it stored as written -- in a {@link + * PendingAliases} holder, not in its stores: null until the first such write, so a span that never + * writes one pays nothing, and dropped once they are all resolved. The holder is copied into the + * span's HashStore when its tags are emitted, so an alias that never resolves is emitted under its + * literal name. The store-only methods below, which take no holder, read and resolve aliases stored + * as written in the HashStore itself, as a TagMap holds them. + * *

One more: an alias written before the span has a direction is resolved when the kind arrives, * without passing through {@code TagInterceptor} again, so split-by-tags on its tag doesn't see it. * An alias written once the direction is known is intercepted as its tag. */ final class DirectionalTags { + /** + * A span's pending aliases: the direction-scoped aliases it stored as written, with their values, + * in write order. Usually one or two. Guarded by the span's tag lock. + */ + static final class PendingAliases { + private String[] names = new String[2]; + private Object[] values = new Object[2]; + private int size; + + /** The value pending under {@code alias}, or null. */ + Object get(String alias) { + for (int i = 0; i < size; i++) { + if (names[i].equals(alias)) { + return values[i]; + } + } + return null; + } + + void put(String alias, Object value) { + for (int i = 0; i < size; i++) { + if (names[i].equals(alias)) { + values[i] = value; + return; + } + } + if (size == names.length) { + names = Arrays.copyOf(names, size * 2); + values = Arrays.copyOf(values, size * 2); + } + names[size] = alias; + values[size] = value; + size++; + } + + void remove(String alias) { + for (int i = 0; i < size; i++) { + if (names[i].equals(alias)) { + int moved = size - i - 1; + System.arraycopy(names, i + 1, names, i, moved); + System.arraycopy(values, i + 1, values, i, moved); + size--; + names[size] = null; + values[size] = null; + return; + } + } + } + + boolean isEmpty() { + return size == 0; + } + } + + /** {@code pending} with {@code alias} pending under {@code value}, created if null. */ + static PendingAliases addPending(PendingAliases pending, String alias, Object value) { + if (pending == null) { + pending = new PendingAliases(); + } + pending.put(alias, value); + return pending; + } + /** * Prepares a write of {@code alias} to a span of {@code direction}. Returns the id of the tag to * store the value under; {@link KnownTagCodec#NO_TAG_IN_DIRECTION_SENTINEL} when {@code alias} is * a direction-scoped alias that can't resolve yet, so its value is stored as written, pending; or - * 0 when it is no alias at all, so its value is an ordinary custom tag. Callers that keep a - * pending-alias flag set it only for the sentinel, so a name that merely passed {@link + * 0 when it is no alias at all, so its value is an ordinary custom tag. Callers that keep pending + * aliases keep one only for the sentinel, so a name that merely passed {@link * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. */ static long prepareAliasWrite(IdStore ids, HashStore tags, String alias, int direction) { + return prepareAliasWrite(ids, tags, null, alias, direction); + } + + /** {@link #prepareAliasWrite(IdStore, HashStore, String, int)} for a span's stores. */ + static long prepareAliasWrite( + IdStore ids, HashStore tags, PendingAliases pending, String alias, int direction) { long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { - tags.remove(alias); // a value pending under the alias is older than this write + // a value pending under the alias is older than this write + removeAsWritten(tags, pending, alias); } return tagId; } @@ -76,8 +154,14 @@ static long prepareAliasWrite(IdStore ids, HashStore tags, String alias, int dir * supersedes. A caller with a constant id that lacks the bit can skip the call. */ static void prepareWrite(IdStore ids, HashStore tags, long tagId, int direction) { + prepareWrite(ids, tags, null, tagId, direction); + } + + /** {@link #prepareWrite(IdStore, HashStore, long, int)} for a span's stores. */ + static void prepareWrite( + IdStore ids, HashStore tags, PendingAliases pending, long tagId, int direction) { if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(ids, tags, tagId, direction); + removePendingAliases(tags, pending, tagId, direction); } } @@ -85,42 +169,90 @@ static void prepareWrite(IdStore ids, HashStore tags, long tagId, int 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. */ - private static void removePendingAliases(IdStore ids, HashStore tags, long tagId, int direction) { + private static void removePendingAliases( + HashStore tags, PendingAliases pending, long tagId, int direction) { if (direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); if (alias != null) { - tags.remove(alias); + removeAsWritten(tags, pending, alias); } return; } for (int d = DIRECTION_INBOUND; d <= DIRECTION_NONE; d++) { String alias = KnownTagCodec.directionScopedNameOf(tagId, d); if (alias != null) { - tags.remove(alias); + removeAsWritten(tags, pending, alias); } } } /** - * 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 the same - * single lookup as {@link TagMap#getObject}. + * Removes {@code alias} as written: from the holder, and from the HashStore, which holds it once + * emitted, or when inherited or written through a view. + */ + private static void removeAsWritten(HashStore tags, PendingAliases pending, String alias) { + tags.remove(alias); + if (pending != null) { + pending.remove(alias); + } + } + + /** + * The value {@code alias} holds as written: in the holder when there is one, else -- with {@code + * inStore} -- in the HashStore. + */ + private static Object asWritten( + HashStore tags, PendingAliases pending, boolean inStore, String alias) { + if (pending != null) { + return pending.get(alias); + } + if (inStore) { + TagMap.Entry entry = tags.get(alias); + return entry == null ? null : entry.objectValue(); + } + return null; + } + + /** + * The value of {@code name} on a span of {@code direction}, for stores that keep aliases as + * written. An alias reads its tag, and a tag reads its alias; a pending alias, being the newer + * write, wins. Every other name costs the same single lookup as {@link TagMap#getObject}. */ static Object get(IdStore ids, HashStore tags, String name, int direction) { - return get(ids, tags, name, direction, true); + return get(ids, tags, null, true, name, direction); } /** - * {@link #get(IdStore, HashStore, String, int)} for a caller that tracks whether {@code tags} can - * hold a pending alias at all. With {@code mayHavePendingAliases} false, a tag with an alias - * skips probing for one, so it costs a single lookup like any other tag. + * {@link #get(IdStore, HashStore, String, int)} for a span, whose pending aliases are in {@code + * pending}. With no holder, a tag with an alias skips probing for one, so it costs a single + * lookup like any other tag. */ static Object get( - IdStore ids, HashStore tags, String name, int direction, boolean mayHavePendingAliases) { - TagMap.Entry entry = - TagRouting.getEntry( - ids, tags, TagRouting.keyOf(name), name, direction, mayHavePendingAliases); - return entry == null ? null : entry.objectValue(); + IdStore ids, HashStore tags, PendingAliases pending, String name, int direction) { + return get(ids, tags, pending, false, name, direction); + } + + private static Object get( + IdStore ids, + HashStore tags, + PendingAliases pending, + boolean inStore, + String name, + int direction) { + long key = TagRouting.keyOf(name); + if (key > 0) { + return get(ids, tags, pending, inStore, key, direction); + } + if (key == 0) { + TagMap.Entry entry = tags.get(name); + return entry == null ? null : entry.objectValue(); + } + Object written = asWritten(tags, pending, inStore, name); + if (written != null) { + return written; + } + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + return tagId > 0 ? TagRouting.getKnownValue(ids, tags, tagId) : null; } /** @@ -128,31 +260,53 @@ static Object get( * #get(IdStore, HashStore, String, int)}. */ static Object get(IdStore ids, HashStore tags, long tagId, int direction) { - return get(ids, tags, tagId, direction, true); + return get(ids, tags, null, true, tagId, direction); } - /** - * {@link #get(IdStore, HashStore, long, int)}; see {@link #get(IdStore, HashStore, String, int, - * boolean)}. - */ + /** {@link #get(IdStore, HashStore, long, int)} for a span; see above. */ static Object get( - IdStore ids, HashStore tags, long tagId, int direction, boolean mayHavePendingAliases) { - TagMap.Entry entry = TagRouting.getById(ids, tags, tagId, direction, mayHavePendingAliases); - return entry == null ? null : entry.objectValue(); + IdStore ids, HashStore tags, PendingAliases pending, long tagId, int direction) { + return get(ids, tags, pending, false, tagId, direction); + } + + private static Object get( + IdStore ids, + HashStore tags, + PendingAliases pending, + boolean inStore, + long tagId, + int direction) { + if ((pending != null || inStore) + && KnownTagCodec.hasScopedAlias(tagId) + && direction != DIRECTION_UNKNOWN) { + String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); + Object written = alias == null ? null : asWritten(tags, pending, inStore, alias); + if (written != null) { + return written; + } + } + return TagRouting.getKnownValue(ids, tags, tagId); } /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ static void remove(IdStore ids, HashStore tags, String name, int direction) { + remove(ids, tags, null, name, direction); + } + + /** {@link #remove(IdStore, HashStore, String, int)} for a span's stores. */ + static void remove( + IdStore ids, HashStore tags, PendingAliases pending, String name, int direction) { // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { - remove(ids, tags, key, direction); + remove(ids, tags, pending, key, direction); return; } - tags.remove(name); // a custom tag, or an alias stored as written if (key == 0) { + tags.remove(name); // a custom tag return; } + removeAsWritten(tags, pending, name); if (direction == DIRECTION_UNKNOWN) { removeEveryTagOfAlias(ids, tags, name); return; @@ -165,9 +319,15 @@ static void remove(IdStore ids, HashStore tags, String name, int direction) { /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ static void remove(IdStore ids, HashStore tags, long tagId, int direction) { + remove(ids, tags, null, tagId, direction); + } + + /** {@link #remove(IdStore, HashStore, long, int)} for a span's stores. */ + static void remove( + IdStore ids, HashStore tags, PendingAliases pending, long tagId, int direction) { TagRouting.removeById(ids, tags, tagId); if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(ids, tags, tagId, direction); + removePendingAliases(tags, pending, tagId, direction); } } @@ -188,16 +348,17 @@ private static void removeEveryTagOfAlias(IdStore ids, HashStore tags, String al } /** - * 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. + * Resolves each alias stored as written in {@code tags} 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. */ static void resolveScopedNames(IdStore ids, HashStore tags, int direction) { if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { return; // no alias names a tag on a span with no direction, or an internal one } List pending = null; - // Pending aliases are custom tags, so they are all in the HashStore. + // Aliases stored as written are custom tags, so they are all in the HashStore. for (TagMap.EntryReader entry : TagMap.viewOf(ids, tags, null)) { if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { if (pending == null) { @@ -216,5 +377,40 @@ static void resolveScopedNames(IdStore ids, HashStore tags, int direction) { } } + /** + * {@link #resolveScopedNames(IdStore, HashStore, int)} for a span: resolves its pending aliases, + * once it has a direction, into the tags they denote, and returns the holder of those left + * pending, or null when none are. + */ + static PendingAliases resolvePending( + IdStore ids, HashStore tags, PendingAliases pending, int direction) { + if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { + return pending; // no alias names a tag on a span with no direction, or an internal one + } + for (int i = 0; i < pending.size; ) { + String alias = pending.names[i]; + long tagId = KnownTagCodec.directionalKeyOf(alias, direction); + if (tagId > 0) { + Object value = pending.values[i]; + tags.remove(alias); // a copy emitted earlier + pending.remove(alias); + TagRouting.setKnown(ids, tags, tagId, value); + } else { + i++; + } + } + return pending.isEmpty() ? null : pending; + } + + /** + * Copies the pending aliases into {@code tags} as custom tags under their literal names, so that + * emitting the span's tags includes them. The holder stays the span's record of them. + */ + static void copyPending(PendingAliases pending, HashStore tags) { + for (int i = 0; i < pending.size; i++) { + tags.set(pending.names[i], pending.values[i]); + } + } + private DirectionalTags() {} } From f7999bd4ee956bd8c97b32c6e05b0394cd5e497f Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 21:31:36 -0400 Subject: [PATCH 59/99] Color known tags into co-occurrence slots in their ids The tag-registry generator now assigns each known tag a slot in id bits [47-32] by graph coloring: each concrete span type's resolved tags, and the trace-level tags, form a clique and get distinct slots. So 1L << slotOf(id) is an exact presence bit for a span's tags, and one long holds them all. Tags on no modeled span type get NO_SLOT, which a long shift maps onto bit 63; real slots are capped below it. KnownTagCodec gains SLOT_SHIFT, NO_SLOT, slotOf, hasSlot and slotCount. Nothing in the tracer reads slots yet: this is split out of the dense store (#12856) so the store, the span builder's tag ledger and a map-level routing summary can each build on it. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 4 + .../buildlogic/tagRegistry/TagRegistry.kt | 81 ++++++++++++++++--- .../tagRegistry/TagRegistryGenerator.kt | 5 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 44 +++++++++- .../java/datadog/trace/api/KnownTagCodec.java | 52 +++++++++--- .../trace/api/KnownTagCodecSlotTest.java | 77 ++++++++++++++++++ 6 files changed, 234 insertions(+), 29 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.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 c88b895281b..2085b062049 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 @@ -96,6 +96,7 @@ object KnownTagsEmitter { appendLine(" public static final String ${otelNameC(t.ddName)} = \"${escape(t.otelName)}\";") } append("// makeTagId(serial=${t.serial})") + append(if (t.slot == TagRegistry.NO_SLOT) " + no slot" else " + slot=${t.slot}") if (t.traceLevel) append(" + trace-level") if (t.intercepted) append(" + intercepted") if (t.scopedOtelNames.isNotEmpty()) append(" + scoped-alias") @@ -114,6 +115,9 @@ object KnownTagsEmitter { // Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag. val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1 appendLine(" public static final int SERIAL_LIMIT = $serialLimit;") + // Slots in use: every slotted tag's slot is below SLOT_COUNT. + val slotCount = (reg.tags.filter { it.slot != TagRegistry.NO_SLOT }.maxOfOrNull { it.slot } ?: -1) + 1 + appendLine(" public static final int SLOT_COUNT = $slotCount;") // OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all // canonical names. Sort by OTel name to keep output deterministic. diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index 1a46bea5a4d..5f1a485a4e4 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -2,12 +2,20 @@ package datadog.buildlogic.tagRegistry /** * Assigns tag ids from a parsed [TagConventions]. The id encoding mirrors KnownTagCodec: [63-48 - * serial][47-32 reserved][31-0 flags]. + * serial][47-32 slot][31-0 flags]. * - *

An id is a globally unique serial plus two classification bits: trace-level and intercepted. It - * carries no storage-layout coordinate -- bits [47-32] are held vacant for the co-occurrence slot - * that the dense tag store assigns by graph coloring, which lands with the dense store itself. - * Nothing here needs to know how (or whether) a tag is stored. + *

An id is a globally unique serial, a slot, and two classification bits: trace-level and + * intercepted. + * + *

The slot comes from GRAPH COLORING the tag co-occurrence graph. Each concrete span type's + * resolved tags appear together on one span, so they form a clique and need distinct slots; the + * trace-level tier is its own clique (it lives in a separate TagMap). Tags that never share a clique + * may share a slot, so the slot count stays bounded by the largest clique, small enough for one + * occupancy `long`. Every tag resolved onto a span type, or trace-level, is slotted whatever its + * requirement level; a tag on no modeled span type (the tracer overlay's routing-only keys, tags of + * unmodeled span types) gets [NO_SLOT]. Nothing in the tracer reads slots yet: they are for + * per-span structures that want an exact, compact presence word, such as a dense tag store or a + * span builder's tag ledger. * *

The intercepted bit marks a tag the tracer may route on the set-path (to a span field or a * sampling directive instead of tag storage), as listed by the tracer overlay. A setter called with @@ -26,6 +34,8 @@ class TagRegistry private constructor(val tags: List) { val serial: Int, val traceLevel: Boolean, val intercepted: Boolean, + /** The tag's slot, or [NO_SLOT] for a tag on no modeled span type. */ + val slot: Int, val id: Long, /** The tag's OpenTelemetry name in every direction; null when not renamed, or renamed per direction. */ val otelName: String? = null, @@ -43,6 +53,10 @@ class TagRegistry private constructor(val tags: List) { const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE + const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT + const val NO_SLOT = 0xFFFF // all-ones slot: no modeled span type; mirrors KnownTagCodec.NO_SLOT + const val MAX_SLOTS = 63 // fits one occupancy long; bit 63 is left for unslotted tags to share + /** * 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. @@ -50,17 +64,24 @@ class TagRegistry private constructor(val tags: List) { const val SERIAL_CAPACITY = 1 shl 15 /** - * Mirrors KnownTagCodec.makeTagId(serial) + intercepted() + traceLevel() -- must stay in sync. + * Mirrors KnownTagCodec's layout -- must stay in sync: serial at [63-48], slot at [47-32], * HAS_SCOPED_ALIAS at bit 0 (the tag has an OpenTelemetry name in some directions only), - * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, 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. + * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits zero. Every id is made here, and the + * range check makes each one positive by construction. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, hasScopedAlias: Boolean = false): Long { + fun encode( + serial: Int, + traceLevel: Boolean, + intercepted: Boolean, + hasScopedAlias: Boolean = false, + slot: Int = NO_SLOT, + ): 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 + require(slot == NO_SLOT || slot in 0 until MAX_SLOTS) { "slot $slot is outside [0, $MAX_SLOTS)" } + var id = (serial.toLong() shl 48) or (slot.toLong() shl SLOT_SHIFT) if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED if (hasScopedAlias) id = id or HAS_SCOPED_ALIAS @@ -70,6 +91,7 @@ class TagRegistry private constructor(val tags: List) { fun build(conv: TagConventions): TagRegistry { val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet() val renames = conv.otelMappings().groupBy { it.tag.ddName } + val slots = colorSlots(conv) // One tag per Datadog name, in a stable order (by name) so serials -- and therefore ids -- are // a pure function of the input. A name's declarations per direction merge into its one tag. @@ -83,6 +105,7 @@ class TagRegistry private constructor(val tags: List) { val isTraceLevel = first.ddName in traceLevel val isIntercepted = first.ddName in conv.interceptedNames val (otelName, scoped) = otelNamesOf(first.ddName, renames[first.ddName].orEmpty()) + val slot = slots[first.ddName] ?: NO_SLOT Tag( first.ddName, first.type, @@ -90,7 +113,8 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted, scoped.isNotEmpty()), + slot, + id = encode(serial, isTraceLevel, isIntercepted, scoped.isNotEmpty(), slot), otelName = otelName, scopedOtelNames = scoped, ) @@ -100,6 +124,41 @@ class TagRegistry private constructor(val tags: List) { return TagRegistry(tags) } + /** + * Colors the co-occurrence graph of slotted tags: tags resolved onto one concrete span type, or + * both trace-level, must get distinct slots. Greedy, most-constrained first (by clique count, + * then name), so the result is a pure function of the conventions. A name declared per direction + * is one tag, so it is one vertex. + */ + private fun colorSlots(conv: TagConventions): Map { + val cliques = ArrayList>() + for (type in conv.concreteTypes()) { + cliques.add(conv.resolve(type).map { it.ddName }.toSet()) + } + cliques.add(conv.traceLevelTags().map { it.ddName }.toSet()) + + val neighbors = HashMap>() + for (clique in cliques) { + for (a in clique) { + val adjacent = neighbors.getOrPut(a) { HashSet() } + for (b in clique) if (a != b) adjacent.add(b) + } + } + val cliqueCount = neighbors.keys.associateWith { tag -> cliques.count { tag in it } } + val order = neighbors.keys.sortedWith(compareByDescending { cliqueCount.getValue(it) }.thenBy { it }) + val slots = HashMap() + for (tag in order) { + val taken = neighbors.getValue(tag).mapNotNull { slots[it] }.toSet() + var slot = 0 + while (slot in taken) slot++ + require(slot < MAX_SLOTS) { + "coloring needs more than $MAX_SLOTS slots at '$tag'; one occupancy long holds $MAX_SLOTS" + } + slots[tag] = slot + } + return slots + } + /** * A tag's renames, as one name for every direction or one name per direction. The rename of a * declaration made per direction applies in that declaration's direction. 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 f2a44a962aa..2477a7b56f4 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 @@ -70,16 +70,17 @@ object TagRegistryGenerator { """ # Tag id assignment. tags=${reg.tags.size} - # TAGS serial lvl int id required name + # TAGS serial lvl int slot id required name """.trimIndent() ) for (t in reg.tags) { appendLine( - " %6d %s %s %-18s %-12s %s".format( + " %6d %s %s %4s %-18s %-12s %s".format( Locale.ROOT, t.serial, if (t.traceLevel) "T" else "-", if (t.intercepted) "I" else "-", + if (t.slot == TagRegistry.NO_SLOT) "-" else t.slot.toString(), "0x%016X".format(Locale.ROOT, t.id), t.required, t.name 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 2169afd1908..0ea513e0ae9 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 @@ -558,9 +558,9 @@ class TagRegistryGeneratorTest { val knownTags = contents(output).getValue("java/datadog/trace/api/KnownTags.java") assertThat(knownTags) .contains( - "COMPONENT_ID = 0x0001000000000000L", - "RESOURCE_NAME_ID = 0x0002000000000002L", - "SERVICE_ID = 0x0003000000000002L", + "COMPONENT_ID = 0x0001FFFF00000000L", + "RESOURCE_NAME_ID = 0x0002FFFF00000002L", + "SERVICE_ID = 0x0003FFFF00000002L", "public static final int SERVICE_SERIAL_NUM = 3;", "public static final int SERIAL_LIMIT = 4;" ) @@ -584,6 +584,44 @@ class TagRegistryGeneratorTest { .withMessageContaining(message) } + @Test + fun `tags on one span type get distinct slots and tags that never co-occur share one`() { + val yaml = directory.conventionsFile( + """ + trace_level: + tags: [{dd-name: env, required: required}] + span_types: + base: + abstract: true + tags: [{dd-name: service, required: required}] + http.server: + extends: base + span-kind: server + tags: [{dd-name: http.route, required: required}] + db.client: + extends: base + span-kind: client + tags: [{dd-name: db.type, required: required}, {dd-name: db.user}] + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + // service is on both span types, so it takes slot 0; db.type and db.user share db.client, so + // they take distinct slots, and http.route -- never on a span type with either -- reuses one of + // them; env is trace-level (its own TagMap) and reuses slot 0. Every level is slotted. + assertThat(contents(output).getValue("java/datadog/trace/api/KnownTags.java")) + .contains( + "DB_TYPE_ID = 0x0001000100000000L", + "DB_USER_ID = 0x0002000200000000L", + "ENV_ID = 0x0003000000000004L", + "HTTP_ROUTE_ID = 0x0004000100000000L", + "SERVICE_ID = 0x0005000000000000L", + "public static final int SLOT_COUNT = 3;" + ) + } + private fun tagConventions(@Language("yaml") yamlText: String) = TagConventions.parse( ObjectMapper(YAMLFactory()).readValue( directory.conventionsFile(yamlText), 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 1e28d4841d2..67c3f94573a 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -13,15 +13,15 @@ */ public final class KnownTagCodec { /* - * 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 - * 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), and bit 1 is the INTERCEPTED bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for - * them. + * tagId bit layout: [63-48 serialNum (16 bits, 15 used: bit 63 is kept clear)] [47-32 slot] + * [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] hold the tag's slot ({@link + * #slotOf}), assigned by graph coloring so tags on one span type never share a slot, or {@link + * #NO_SLOT} for a tag on no modeled span type. 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 HAS_SCOPED_ALIAS. + * 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 * a sampling directive instead of tag storage. It is a hint, not a decision: the interceptor's @@ -63,6 +63,32 @@ public static boolean isIntercepted(long tagId) { return (tagId & INTERCEPTED) != 0L; } + /** Bit position of the slot: bits [47-32] of an id. */ + public static final int SLOT_SHIFT = 32; + + /** The slot of a known tag on no modeled span type. */ + public static final int NO_SLOT = 0xFFFF; + + /** + * The tag's slot, or {@link #NO_SLOT}. The generator colors tags that appear together on a span + * type with distinct slots, so a span's slotted tags never collide: {@code 1L << slotOf(id)} is + * an exact presence bit for them. Every tag without a slot shares bit 63, which no real slot + * uses. + */ + public static int slotOf(long tagId) { + return (int) (tagId >>> SLOT_SHIFT) & 0xFFFF; + } + + /** True if the tag has a slot, below {@link #slotCount()}. */ + public static boolean hasSlot(long tagId) { + return slotOf(tagId) != NO_SLOT; + } + + /** Slots in use: every slotted tag's slot is below this. */ + public static int slotCount() { + return KnownTags.SLOT_COUNT; + } + /** * 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 @@ -121,10 +147,10 @@ public static long traceLevel(long tagId) { } /** - * Builds a tagId from its {@code serialNum} (globally unique per known tag). The reserved [47-32] - * window and the low 32 bits are zero, so the id is fully determined by the serial — the - * generator emits it as a literal. Inverse of {@link #serialNum}. Intended for the code generator - * and tests. + * Builds an id carrying only {@code serialNum}: slot 0 and no flags, so it equals a tag's real id + * only when that tag has neither. Enough for serial-keyed lookups such as {@link #nameOf}; + * compare against a {@code KnownTags} constant, never against this. Inverse of {@link + * #serialNum}. Intended for tests and serial-keyed lookups. */ public static long makeTagId(int serialNum) { return (long) serialNum << 48; diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java new file mode 100644 index 00000000000..4dd09044be8 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -0,0 +1,77 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** {@link KnownTagCodec}'s slot: bits [47-32] of an id, assigned by graph coloring. */ +class KnownTagCodecSlotTest { + @Test + void everySlotIsBelowTheSlotCount() throws IllegalAccessException { + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) { + continue; + } + long tagId = field.getLong(null); + if (KnownTagCodec.hasSlot(tagId)) { + assertTrue(KnownTagCodec.slotOf(tagId) < KnownTagCodec.slotCount(), field.getName()); + } else { + assertEquals(KnownTagCodec.NO_SLOT, KnownTagCodec.slotOf(tagId), field.getName()); + } + } + } + + @Test + void tagsOnAnHttpServerSpanHaveDistinctSlots() { + long[] httpServerTags = { + KnownTags.SERVICE_ID, + KnownTags.COMPONENT_ID, + KnownTags.SPAN_KIND_ID, + KnownTags.HTTP_METHOD_ID, + KnownTags.HTTP_URL_ID, + KnownTags.HTTP_STATUS_CODE_ID, + KnownTags.HTTP_ROUTE_ID, + KnownTags.HTTP_HOSTNAME_ID, + }; + Set slots = new HashSet<>(); + for (long tagId : httpServerTags) { + assertTrue(KnownTagCodec.hasSlot(tagId), Long.toHexString(tagId)); + assertTrue(slots.add(KnownTagCodec.slotOf(tagId)), Long.toHexString(tagId)); + } + } + + @Test + void anOptionalTagOnASpanTypeHasASlot() { + assertTrue(KnownTagCodec.hasSlot(KnownTags.PEER_PORT_ID)); + assertTrue(KnownTagCodec.hasSlot(KnownTags.DB_POOL_NAME_ID)); + } + + @Test + void aTagOnNoSpanTypeHasNoSlot() { + // the tracer overlay's routing-only keys appear on no modeled span type + assertFalse(KnownTagCodec.hasSlot(KnownTags.RESOURCE_NAME_ID)); + } + + @Test + void theSlotDoesNotChangeTheSerialOrFlags() { + long tagId = KnownTags.HTTP_METHOD_ID; + + assertTrue(KnownTagCodec.hasSlot(tagId)); + assertEquals(KnownTags.HTTP_METHOD_NAME, KnownTagCodec.nameOf(tagId)); + assertTrue(KnownTagCodec.isIntercepted(tagId)); + assertFalse(KnownTagCodec.isTraceLevel(tagId)); + } + + @Test + void anUnslottedTagSharesTheReservedTopBit() { + // An occupancy bit of 1L << slotOf(id) needs no branch for NO_SLOT: a long shift uses only the + // low 6 bits of its count, so 0xFFFF lands on bit 63, which no real slot uses + assertEquals(1L << 63, 1L << KnownTagCodec.slotOf(KnownTags.RESOURCE_NAME_ID)); + assertTrue(KnownTagCodec.slotCount() < 63, "real slots must stay below the shared bit 63"); + } +} From 0475a1fab17cc9ec3a38f5ca81f33e3f5046fec7 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 22:36:09 -0400 Subject: [PATCH 60/99] Treat a custom tag's zero id as unslotted hasSlot(0L) decoded slot 0, so the id keyOf gives a custom tag looked like the known tag in slot 0. It now returns false for 0L. The slot-count test also allows the full 63 slots the generator permits. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/KnownTagCodec.java | 7 +++++-- .../java/datadog/trace/api/KnownTagCodecSlotTest.java | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) 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 67c3f94573a..e0bbc6e66d9 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -79,9 +79,12 @@ public static int slotOf(long tagId) { return (int) (tagId >>> SLOT_SHIFT) & 0xFFFF; } - /** True if the tag has a slot, below {@link #slotCount()}. */ + /** + * True if the tag has a slot, below {@link #slotCount()}. False for {@code 0L}, the id {@link + * #keyOf} gives a custom tag, which would otherwise decode as slot 0. + */ public static boolean hasSlot(long tagId) { - return slotOf(tagId) != NO_SLOT; + return tagId != 0L && slotOf(tagId) != NO_SLOT; } /** Slots in use: every slotted tag's slot is below this. */ diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java index 4dd09044be8..ded02b397e5 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -67,11 +67,18 @@ void theSlotDoesNotChangeTheSerialOrFlags() { assertFalse(KnownTagCodec.isTraceLevel(tagId)); } + @Test + void aCustomTagHasNoSlot() { + // keyOf gives a custom tag the id 0L, whose slot bits decode as slot 0 + assertEquals(0L, KnownTagCodec.keyOf("my.custom.tag")); + assertFalse(KnownTagCodec.hasSlot(0L)); + } + @Test void anUnslottedTagSharesTheReservedTopBit() { // An occupancy bit of 1L << slotOf(id) needs no branch for NO_SLOT: a long shift uses only the // low 6 bits of its count, so 0xFFFF lands on bit 63, which no real slot uses assertEquals(1L << 63, 1L << KnownTagCodec.slotOf(KnownTags.RESOURCE_NAME_ID)); - assertTrue(KnownTagCodec.slotCount() < 63, "real slots must stay below the shared bit 63"); + assertTrue(KnownTagCodec.slotCount() <= 63, "real slots must stay below the shared bit 63"); } } From 8ed88c9ace82f0438e1657caa2a3678599b06292 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 22:51:10 -0400 Subject: [PATCH 61/99] Say a slot bit is only a may-be-present signal A span can still hold two tags that share a slot, so slotOf's presence bit is exact only when it is clear. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/KnownTagCodec.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 e0bbc6e66d9..6528a67c7ce 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -71,9 +71,10 @@ public static boolean isIntercepted(long tagId) { /** * The tag's slot, or {@link #NO_SLOT}. The generator colors tags that appear together on a span - * type with distinct slots, so a span's slotted tags never collide: {@code 1L << slotOf(id)} is - * an exact presence bit for them. Every tag without a slot shares bit 63, which no real slot - * uses. + * type with distinct slots, so when instrumentation follows its span type, {@code 1L << + * slotOf(id)} gives each of a span's tags its own presence bit. A span can still hold two tags + * that share a slot, so a clear bit means the tag is absent, but a set bit only means it may be + * present. Every tag without a slot shares bit 63, which no real slot uses. */ public static int slotOf(long tagId) { return (int) (tagId >>> SLOT_SHIFT) & 0xFFFF; From 31bf06ee78fb0f6f8c060dccec6c8396c89b2be9 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 14:11:26 -0400 Subject: [PATCH 62/99] Give known and custom tags separate hash stores Each half of a TagMap now has its own bucket array. HashIdStore holds the known tags by id, with a count, a final parent, and slot masks from the tag ids' co-occurrence slots: a clear occupancy bit is a definite miss, and the parent's slots let a read-through miss stop early. Removing an inherited known tag puts that tag's shared, immutable tombstone entry in the local buckets, so it allocates nothing. HashStore holds only custom tags and is created on the first custom write; until then a child borrows its parent's custom store read-only. The raw bucket mechanics move into a stateless HashSupport whose operations report what they did through return values. HASH_MODE is gone. A span now keeps its tags in a TagMap it creates with the span (TagMap.forSpan), locks on it, and routes to it through TagRouting; that TagMap is what its internal readers get, replacing the cached view. DirectionalTags' store-only methods are removed, and its test now exercises the methods the span uses. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 203 ++-- .../datadog/trace/core/DirectionalTags.java | 203 +--- .../DDSpanDirectionalTagConsistencyTest.java | 5 +- .../trace/core/DirectionalTagsTest.java | 248 +++-- .../forbiddenApiFilters/instrumentation.txt | 15 +- .../java/datadog/trace/api/HashIdStore.java | 322 ++++++- .../java/datadog/trace/api/HashStore.java | 883 +++--------------- .../java/datadog/trace/api/HashSupport.java | 403 ++++++++ .../main/java/datadog/trace/api/IdStore.java | 130 ++- .../main/java/datadog/trace/api/TagMap.java | 450 +++++---- .../java/datadog/trace/api/TagRouting.java | 215 ++--- .../java/datadog/trace/api/TagStoresTest.java | 577 ++++++++---- 12 files changed, 1933 insertions(+), 1721 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/HashSupport.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 fa732837a20..78ab335de29 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 @@ -15,8 +15,6 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; -import datadog.trace.api.HashStore; -import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; @@ -47,6 +45,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; @@ -70,6 +69,11 @@ * across Span boundaries and (2) any Datadog fields that are needed to identify or contextualize * the associated Span instance */ +@SuppressFBWarnings( + value = "USO_UNSAFE_ACCESSIBLE_OBJECT_SYNCHRONIZATION", + justification = + "The tags lock is the span's TagMap, handed only to the tracer's internal readers, which" + + " run under it; a separate lock object would cost an allocation per span") public class DDSpanContext implements AgentSpanContext, RequestContext, @@ -162,9 +166,9 @@ private static int[] tagDirectionOfSpanKind() { * While null -- almost always -- a write of a tag with an alias takes the plain store path: no * second lock, no boxing. * - *

Written under the {@code tagStore} lock. {@link #routedTagFlags()} reads it without the - * lock, deliberately: a setter on another thread that has not yet seen the holder is racing the - * alias write, and the order of two unsynchronized writes is undefined anyway. The writing thread + *

Written under the {@code tags} lock. {@link #routedTagFlags()} reads it without the lock, + * deliberately: a setter on another thread that has not yet seen the holder 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 DirectionalTags.PendingAliases pendingAliases; @@ -181,21 +185,13 @@ private static int[] tagDirectionOfSpanKind() { * rather read and accessed in a serial fashion on thread after thread. The synchronization can * then be wrapped around bulk operations to minimize the costly atomic operations. * - *

The span owns its tag stores directly: {@code tagStore} holds the custom tags, and the known - * tags too in hash mode; {@code tagIds} holds the known tags otherwise (null in hash mode). Every - * access goes through {@link TagRouting}, under one lock that guards both: {@code tagStore} - * itself. Internal readers get a {@link #tagView() view}. + *

The span's tags live in a {@link TagMap} it creates with the span ({@link TagMap#forSpan}), + * which holds an id store for the known tags and, from the first custom write, a custom store. + * The span routes every access to it through {@link TagRouting}, under one lock that guards both + * stores: {@code tags} itself. The same TagMap, whose name-keyed reads honor the span's + * direction, is what the span's internal readers (post-processors, the serializer) get. */ - private final HashStore tagStore; - - private final IdStore tagIds; - - /** - * This span's one {@link #tagView() view} of its tag stores, created on first use under the - * {@code tagStore} lock and reused by every internal reader. The view holds no state of its own - * beyond the stores and this span (its direction source), so it stays live and correct. - */ - private TagMap tagView; + private final TagMap tags; /** The service name is required, otherwise the span are dropped by the agent */ private volatile String serviceName; @@ -461,8 +457,7 @@ public DDSpanContext( this.pathwayContext = pathwayContext; // Reads through to the frozen tracer-level tags, if any. - this.tagStore = new HashStore(TagMap.readThroughStore(readThroughParent)); - this.tagIds = TagRouting.idStoreFor(this.tagStore); + this.tags = TagMap.forSpan(readThroughParent, this); // must set this before setting the service and resource names below this.profilingContextIntegration = profilingContextIntegration; @@ -711,7 +706,7 @@ private void seedAbsentTags(final TagMap map) { if (map == null) { return; } - synchronized (tagStore) { + synchronized (tags) { map.forEach( this, (ctx, tagEntry) -> { @@ -844,7 +839,7 @@ public int getSamplingPriority() { } public void setSpanSamplingPriority(double rate, int limit) { - synchronized (tagStore) { + synchronized (tags) { unsafeSetTag(SPAN_SAMPLING_MECHANISM_TAG, SamplingMechanism.SPAN_SAMPLING_RATE); unsafeSetTag(SPAN_SAMPLING_RULE_RATE_TAG, rate); if (limit != Integer.MAX_VALUE) { @@ -973,41 +968,28 @@ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); // Checked under the lock alias registration takes: a writer that read no direction registers // its pending alias before this sees the flag, or reads the new kind and resolves at write. - synchronized (tagStore) { + synchronized (tags) { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { // The span now has a direction: fold pending aliases into the tags they denote, so every // later reader (peer service naming, stats, sampling rules, serialization) sees them // resolved. Once none is left, the span is back on the fast path. - pendingAliases = DirectionalTags.resolvePending(tagIds, tagStore, pending, tagDirection()); + pendingAliases = DirectionalTags.resolvePending(tags, pending, tagDirection()); } } } /** - * {@link #tagView()} for emitting the span's tags: first copies any pending aliases into the - * HashStore under their literal names, so a span that never learns its kind still emits them. The - * holder stays the span's record of them. Call under the {@code tagStore} lock. + * This span's tags, for emitting them: first copies any pending aliases into the HashStore under + * their literal names, so a span that never learns its kind still emits them. The holder stays + * the span's record of them. Call under the {@code tags} lock. */ private TagMap emittableTags() { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { - DirectionalTags.copyPending(pending, tagStore); - } - return tagView(); - } - - /** - * A live view of this span's tag stores, for its internal readers: post-processors and the - * serializer. Call it, and use the view, only under the {@code tagStore} lock. - */ - private TagMap tagView() { - TagMap view = this.tagView; - if (view == null) { - // a view handed to the post-processor chain escapes, so allocate it once per span - this.tagView = view = TagMap.viewOf(tagIds, tagStore, this); + DirectionalTags.copyPending(pending, tags); } - return view; + return tags; } /** @@ -1049,8 +1031,8 @@ public String getSpanKindString() { return SPAN_KIND_VALUES[ordinal]; } // UNSET or CUSTOM -- fall through to tag map - synchronized (tagStore) { - TagMap.Entry kind = TagRouting.getById(tagIds, tagStore, KnownTags.SPAN_KIND_ID); + synchronized (tags) { + TagMap.Entry kind = TagRouting.getById(tags, KnownTags.SPAN_KIND_ID); return kind == null ? null : kind.stringValue(); } } @@ -1062,31 +1044,31 @@ public void setOrigin(final CharSequence origin) { } public void setMetric(final CharSequence key, final Number value) { - synchronized (tagStore) { + synchronized (tags) { unsafeSetTag(key.toString(), value); } } public void setMetric(final CharSequence key, final int value) { - synchronized (tagStore) { + synchronized (tags) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { - synchronized (tagStore) { + synchronized (tags) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { - synchronized (tagStore) { + synchronized (tags) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { - synchronized (tagStore) { + synchronized (tags) { storeTag(key.toString(), value); } } @@ -1096,7 +1078,7 @@ public void setMetric(final TagMap.EntryReader entry) { return; } - synchronized (tagStore) { + synchronized (tags) { storeEntry(entry); } } @@ -1109,7 +1091,7 @@ public void setMetric(final TagMap.EntryReader entry) { * 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 DirectionalTags). Call while holding the - * tagStore lock. + * tags lock. */ private long writeKeyOf(String tag) { long key = KnownTagCodec.keyOrAliasOf(tag); @@ -1120,8 +1102,7 @@ private long writeKeyOf(String tag) { if (key == 0) { return 0L; } - long aliasedId = - DirectionalTags.prepareAliasWrite(tagIds, tagStore, pendingAliases, tag, tagDirection()); + long aliasedId = DirectionalTags.prepareAliasWrite(tags, pendingAliases, tag, tagDirection()); if (aliasedId > 0) { dropPendingAliasesIfEmpty(); return aliasedId; @@ -1167,7 +1148,7 @@ private void storeTag(String tag, Object value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1176,7 +1157,7 @@ private void storeTag(String tag, CharSequence value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1185,7 +1166,7 @@ private void storeTag(String tag, boolean value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1194,7 +1175,7 @@ private void storeTag(String tag, int value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1203,7 +1184,7 @@ private void storeTag(String tag, long value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1212,7 +1193,7 @@ private void storeTag(String tag, float value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1221,7 +1202,7 @@ private void storeTag(String tag, double value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tagIds, tagStore, key, tag, value); + TagRouting.set(tags, key, tag, value); } } @@ -1233,11 +1214,10 @@ private void storeEntry(TagMap.EntryReader entry) { putEntry(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { long aliasedId = - DirectionalTags.prepareAliasWrite( - tagIds, tagStore, pendingAliases, entry.tag(), tagDirection()); + DirectionalTags.prepareAliasWrite(tags, pendingAliases, entry.tag(), tagDirection()); if (aliasedId > 0) { dropPendingAliasesIfEmpty(); - TagRouting.setKnown(tagIds, tagStore, aliasedId, entry.objectValue()); + TagRouting.setKnown(tags, aliasedId, entry.objectValue()); } else if (aliasedId < 0) { storePendingAlias(entry.tag(), entry.objectValue()); // a real alias, stored as written } else { @@ -1252,7 +1232,7 @@ private void storeEntry(TagMap.EntryReader entry) { private void putEntry(TagMap.EntryReader entry) { TagMap.Entry e = entry.entry(); if (e != null) { - TagRouting.put(tagIds, tagStore, e); + TagRouting.put(tags, e); } } @@ -1261,9 +1241,9 @@ public void removeTag(String tag) { // Clear the cached ordinal; the tag store still needs to be updated below. spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (tagStore) { + synchronized (tags) { // A direction-dependent name and the tag it denotes here are removed together. - DirectionalTags.remove(tagIds, tagStore, pendingAliases, tag, tagDirection()); + DirectionalTags.remove(tags, pendingAliases, tag, tagDirection()); dropPendingAliasesIfEmpty(); } } @@ -1281,8 +1261,8 @@ public void removeTag(long tagId) { if (tagId == KnownTags.SPAN_KIND_ID) { spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (tagStore) { - DirectionalTags.remove(tagIds, tagStore, pendingAliases, tagId, tagDirection()); + synchronized (tags) { + DirectionalTags.remove(tags, pendingAliases, tagId, tagDirection()); dropPendingAliasesIfEmpty(); } } @@ -1349,11 +1329,10 @@ private boolean interceptEntry(TagMap.EntryReader entry) { */ private boolean isPresent(TagMap.EntryReader entry) { if (KnownTagCodec.isDirectionDependent(entry)) { - return DirectionalTags.get(tagIds, tagStore, pendingAliases, entry.tag(), tagDirection()) - != null; + return DirectionalTags.get(tags, pendingAliases, entry.tag(), tagDirection()) != null; } String tag = entry.tag(); - return TagRouting.get(tagIds, tagStore, TagRouting.keyOf(tag), tag) != null; + return TagRouting.get(tags, TagRouting.keyOf(tag), tag) != null; } public void setTag(final String tag, final Object value) { @@ -1366,7 +1345,7 @@ public void setTag(final String tag, final Object value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1382,7 +1361,7 @@ public void setTag(final String tag, final String value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1400,19 +1379,19 @@ public void supersedeAliases(long tagId) { if (!hasPendingAliases()) { return; // entry paths route on the constant mask; nothing to supersede } - synchronized (tagStore) { + synchronized (tags) { supersedePendingAliases(tagId); } } /** * Removes the pending aliases a write of {@code tagId} supersedes, if it has an alias and the - * span holds any. Callers hold the {@code tagStore} lock. + * span holds any. Callers hold the {@code tags} lock. */ private void supersedePendingAliases(long tagId) { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null && KnownTagCodec.hasScopedAlias(tagId)) { - DirectionalTags.prepareWrite(tagIds, tagStore, pending, tagId, tagDirection()); + DirectionalTags.prepareWrite(tags, pending, tagId, tagDirection()); dropPendingAliasesIfEmpty(); } } @@ -1430,8 +1409,8 @@ public void setTag(final long tagId, final Object value) { removeTag(tagId); } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1444,8 +1423,8 @@ public void setTag(final long tagId, final CharSequence value) { removeTag(tagId); } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1457,8 +1436,8 @@ public void setTag(final long tagId, final boolean value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1470,8 +1449,8 @@ public void setTag(final long tagId, final int value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1483,8 +1462,8 @@ public void setTag(final long tagId, final long value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1496,8 +1475,8 @@ public void setTag(final long tagId, final float value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1509,8 +1488,8 @@ public void setTag(final long tagId, final double value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, value); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, value); } } } @@ -1522,7 +1501,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!interceptEntry(entry)) { - synchronized (tagStore) { + synchronized (tags) { storeEntry(entry); } } @@ -1542,8 +1521,8 @@ public void setTag(TagMap.EntryReader entry) { */ private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (tagStore) { - TagRouting.setKnown(tagIds, tagStore, tagId, box); + synchronized (tags) { + TagRouting.setKnown(tags, tagId, box); } } } @@ -1557,7 +1536,7 @@ public void setTag(final String tag, final boolean value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1572,7 +1551,7 @@ public void setTag(final String tag, final int value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1587,7 +1566,7 @@ public void setTag(final String tag, final long value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1602,7 +1581,7 @@ public void setTag(final String tag, final float value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1617,7 +1596,7 @@ public void setTag(final String tag, final double value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagStore) { + synchronized (tags) { storeTag(tag, value); } } @@ -1632,7 +1611,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { return; } - synchronized (tagStore) { + synchronized (tags) { if (needsIntercept) { // forEach out-performs the iterator of TagMap // Taking advantage of ability to pass through other context arguments @@ -1646,7 +1625,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { }); } else { // needsIntercept(TagMap) is also true for a map with a direction-dependent name. - TagRouting.putAll(tagIds, tagStore, map); + tags.putAll(map); } } } @@ -1656,7 +1635,7 @@ void setAllTags(final TagMap.Ledger ledger) { return; } - synchronized (tagStore) { + synchronized (tags) { for (final TagMap.EntryChange entryChange : ledger) { String tag = entryChange.tag(); if (entryChange.isRemoval()) { @@ -1665,7 +1644,7 @@ void setAllTags(final TagMap.Ledger ledger) { spanKindOrdinal = SPAN_KIND_UNSET; } // as removeTag(String): a direction-dependent name and the tag it denotes go together - DirectionalTags.remove(tagIds, tagStore, pendingAliases, tag, tagDirection()); + DirectionalTags.remove(tags, pendingAliases, tag, tagDirection()); dropPendingAliasesIfEmpty(); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; @@ -1684,7 +1663,7 @@ void setAllTags(final Map map) { } else if (map instanceof TagMap) { setAllTags((TagMap) map); } else if (!map.isEmpty()) { - synchronized (tagStore) { + synchronized (tags) { for (final Map.Entry tag : map.entrySet()) { long tagId = idForWrite(tag.getKey()); boolean intercepted = @@ -1729,7 +1708,7 @@ Object getTag(final String key) { return getSpanKindString(); default: Object value; - synchronized (tagStore) { + synchronized (tags) { value = unsafeGetTag(key); } // maintain previously observable type of http url :| @@ -1745,16 +1724,16 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return DirectionalTags.get(tagIds, tagStore, pendingAliases, tag, tagDirection()); + return DirectionalTags.get(tags, pendingAliases, tag, tagDirection()); } @Deprecated public TagMap getTags() { - synchronized (tagStore) { - TagMap tags = tagView().copy(); + synchronized (tags) { + TagMap tags = this.tags.copy(); DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { - DirectionalTags.copyPending(pending, tags.hashStore()); // under their literal names + DirectionalTags.copyPending(pending, tags); // under their literal names } tags.put(DDTags.THREAD_ID, threadId); @@ -1807,7 +1786,7 @@ public void setMetaStruct(final String field, final T value) { } void earlyProcessTags(AppendableSpanLinks links) { - synchronized (tagStore) { + synchronized (tags) { TagsPostProcessorFactory.eagerProcessor().processTags(emittableTags(), this, links); } } @@ -1837,7 +1816,7 @@ void processTagsAndBaggage( // This is a compromise to avoid... // - 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 (tagStore) { + synchronized (tags) { // One live view of the tag stores, for the post-processors and the serializer TagMap tags = emittableTags(); @@ -2045,7 +2024,7 @@ public String toString() { s.append(" *measured*"); } - synchronized (tagStore) { + synchronized (tags) { s.append(" tags=").append(new TreeMap<>(getTags())); } return s.toString(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index 32b233a00f4..ccd26ebf449 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -4,14 +4,10 @@ import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; -import datadog.trace.api.HashStore; -import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.TagMap; import datadog.trace.api.TagRouting; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; /** * A span's reading of the few tag names that depend on its direction. The tag stores ({@link @@ -67,9 +63,9 @@ final class DirectionalTags { * in write order. Usually one or two. Guarded by the span's tag lock. */ static final class PendingAliases { - private String[] names = new String[2]; - private Object[] values = new Object[2]; - private int size; + String[] names = new String[2]; + Object[] values = new Object[2]; + int size; /** The value pending under {@code alias}, or null. */ Object get(String alias) { @@ -128,18 +124,12 @@ static PendingAliases addPending(PendingAliases pending, String alias, Object va /** * Prepares a write of {@code alias} to a span of {@code direction}. Returns the id of the tag to * store the value under; {@link KnownTagCodec#NO_TAG_IN_DIRECTION_SENTINEL} when {@code alias} is - * a direction-scoped alias that can't resolve yet, so its value is stored as written, pending; or - * 0 when it is no alias at all, so its value is an ordinary custom tag. Callers that keep pending - * aliases keep one only for the sentinel, so a name that merely passed {@link + * a direction-scoped alias that can't resolve yet, so its value is kept as written, pending; or 0 + * when it is no alias at all, so its value is an ordinary custom tag. A span keeps a pending + * alias only for the sentinel, so a name that merely passed {@link * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. */ - static long prepareAliasWrite(IdStore ids, HashStore tags, String alias, int direction) { - return prepareAliasWrite(ids, tags, null, alias, direction); - } - - /** {@link #prepareAliasWrite(IdStore, HashStore, String, int)} for a span's stores. */ - static long prepareAliasWrite( - IdStore ids, HashStore tags, PendingAliases pending, String alias, int direction) { + static long prepareAliasWrite(TagMap tags, PendingAliases pending, String alias, int direction) { long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { // a value pending under the alias is older than this write @@ -153,13 +143,7 @@ static long prepareAliasWrite( * 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. */ - static void prepareWrite(IdStore ids, HashStore tags, long tagId, int direction) { - prepareWrite(ids, tags, null, tagId, direction); - } - - /** {@link #prepareWrite(IdStore, HashStore, long, int)} for a span's stores. */ - static void prepareWrite( - IdStore ids, HashStore tags, PendingAliases pending, long tagId, int direction) { + static void prepareWrite(TagMap tags, PendingAliases pending, long tagId, int direction) { if (KnownTagCodec.hasScopedAlias(tagId)) { removePendingAliases(tags, pending, tagId, direction); } @@ -170,7 +154,7 @@ static void prepareWrite( * or, with no direction yet, any it could have. */ private static void removePendingAliases( - HashStore tags, PendingAliases pending, long tagId, int direction) { + TagMap tags, PendingAliases pending, long tagId, int direction) { if (direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); if (alias != null) { @@ -187,145 +171,80 @@ private static void removePendingAliases( } /** - * Removes {@code alias} as written: from the holder, and from the HashStore, which holds it once - * emitted, or when inherited or written through a view. + * Removes {@code alias} as written: from the holder, and from the custom store, which holds it + * once emitted, or when inherited or written through the span's TagMap. */ - private static void removeAsWritten(HashStore tags, PendingAliases pending, String alias) { - tags.remove(alias); + private static void removeAsWritten(TagMap tags, PendingAliases pending, String alias) { + TagRouting.removeCustom(tags, alias); if (pending != null) { pending.remove(alias); } } /** - * The value {@code alias} holds as written: in the holder when there is one, else -- with {@code - * inStore} -- in the HashStore. - */ - private static Object asWritten( - HashStore tags, PendingAliases pending, boolean inStore, String alias) { - if (pending != null) { - return pending.get(alias); - } - if (inStore) { - TagMap.Entry entry = tags.get(alias); - return entry == null ? null : entry.objectValue(); - } - return null; - } - - /** - * The value of {@code name} on a span of {@code direction}, for stores that keep aliases as - * written. An alias reads its tag, and a tag reads its alias; a pending alias, being the newer - * write, wins. Every other name costs the same single lookup as {@link TagMap#getObject}. - */ - static Object get(IdStore ids, HashStore tags, String name, int direction) { - return get(ids, tags, null, true, name, direction); - } - - /** - * {@link #get(IdStore, HashStore, String, int)} for a span, whose pending aliases are in {@code - * pending}. With no holder, a tag with an alias skips probing for one, so it costs a single + * The value of {@code name} on a span of {@code direction}, whose pending aliases are in {@code + * pending}. An alias reads its tag, and a tag reads its alias; a pending alias, being the newer + * write, wins. With no holder, a tag with an alias skips probing for one, so it costs a single * lookup like any other tag. */ - static Object get( - IdStore ids, HashStore tags, PendingAliases pending, String name, int direction) { - return get(ids, tags, pending, false, name, direction); - } - - private static Object get( - IdStore ids, - HashStore tags, - PendingAliases pending, - boolean inStore, - String name, - int direction) { + static Object get(TagMap tags, PendingAliases pending, String name, int direction) { long key = TagRouting.keyOf(name); if (key > 0) { - return get(ids, tags, pending, inStore, key, direction); + return get(tags, pending, key, direction); } if (key == 0) { - TagMap.Entry entry = tags.get(name); + TagMap.Entry entry = TagRouting.getCustom(tags, name); return entry == null ? null : entry.objectValue(); } - Object written = asWritten(tags, pending, inStore, name); + Object written = pending == null ? null : pending.get(name); if (written != null) { return written; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return tagId > 0 ? TagRouting.getKnownValue(ids, tags, tagId) : null; + return tagId > 0 ? TagRouting.getKnownValue(tags, tagId) : null; } /** * The value of the known tag {@code tagId} on a span of {@code direction}; see {@link - * #get(IdStore, HashStore, String, int)}. + * #get(TagMap, PendingAliases, String, int)}. */ - static Object get(IdStore ids, HashStore tags, long tagId, int direction) { - return get(ids, tags, null, true, tagId, direction); - } - - /** {@link #get(IdStore, HashStore, long, int)} for a span; see above. */ - static Object get( - IdStore ids, HashStore tags, PendingAliases pending, long tagId, int direction) { - return get(ids, tags, pending, false, tagId, direction); - } - - private static Object get( - IdStore ids, - HashStore tags, - PendingAliases pending, - boolean inStore, - long tagId, - int direction) { - if ((pending != null || inStore) - && KnownTagCodec.hasScopedAlias(tagId) - && direction != DIRECTION_UNKNOWN) { + static Object get(TagMap tags, PendingAliases pending, long tagId, int direction) { + if (pending != null && KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - Object written = alias == null ? null : asWritten(tags, pending, inStore, alias); + Object written = alias == null ? null : pending.get(alias); if (written != null) { return written; } } - return TagRouting.getKnownValue(ids, tags, tagId); + return TagRouting.getKnownValue(tags, tagId); } /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ - static void remove(IdStore ids, HashStore tags, String name, int direction) { - remove(ids, tags, null, name, direction); - } - - /** {@link #remove(IdStore, HashStore, String, int)} for a span's stores. */ - static void remove( - IdStore ids, HashStore tags, PendingAliases pending, String name, int direction) { + static void remove(TagMap tags, PendingAliases pending, String name, int direction) { // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { - remove(ids, tags, pending, key, direction); + remove(tags, pending, key, direction); return; } if (key == 0) { - tags.remove(name); // a custom tag + TagRouting.removeCustom(tags, name); // a custom tag return; } removeAsWritten(tags, pending, name); if (direction == DIRECTION_UNKNOWN) { - removeEveryTagOfAlias(ids, tags, name); + removeEveryTagOfAlias(tags, name); return; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); if (tagId > 0) { - TagRouting.removeById(ids, tags, tagId); + TagRouting.removeById(tags, tagId); } } /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ - static void remove(IdStore ids, HashStore tags, long tagId, int direction) { - remove(ids, tags, null, tagId, direction); - } - - /** {@link #remove(IdStore, HashStore, long, int)} for a span's stores. */ - static void remove( - IdStore ids, HashStore tags, PendingAliases pending, long tagId, int direction) { - TagRouting.removeById(ids, tags, tagId); + static void remove(TagMap tags, PendingAliases pending, long tagId, int direction) { + TagRouting.removeById(tags, tagId); if (KnownTagCodec.hasScopedAlias(tagId)) { removePendingAliases(tags, pending, tagId, direction); } @@ -335,55 +254,25 @@ static void remove( * Removes every tag {@code alias} could denote, for a removal of the alias before the span has a * direction. A removal counts as a write, so no older value can resurface when the kind arrives; * like the write rule, this can drop the other direction's tag. (Removing a tag before then - * likewise removes the pending aliases that could denote it; see {@link #remove(IdStore, - * HashStore, long, int)}.) + * likewise removes the pending aliases that could denote it; see {@link #remove(TagMap, + * PendingAliases, long, int)}.) */ - private static void removeEveryTagOfAlias(IdStore ids, HashStore tags, String alias) { + private static void removeEveryTagOfAlias(TagMap tags, String alias) { for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { long aliasedId = KnownTagCodec.directionalKeyOf(alias, direction); if (aliasedId > 0) { - TagRouting.removeById(ids, tags, aliasedId); + TagRouting.removeById(tags, aliasedId); } } } /** - * Resolves each alias stored as written in {@code tags} 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. - */ - static void resolveScopedNames(IdStore ids, HashStore tags, int direction) { - if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { - return; // no alias names a tag on a span with no direction, or an internal one - } - List pending = null; - // Aliases stored as written are custom tags, so they are all in the HashStore. - for (TagMap.EntryReader entry : TagMap.viewOf(ids, tags, null)) { - if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { - if (pending == null) { - pending = new ArrayList<>(2); - } - pending.add(entry.entry()); - } - } - if (pending == null) { - return; - } - for (TagMap.Entry entry : pending) { - tags.remove(entry.tag()); - TagRouting.setKnown( - ids, tags, KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); - } - } - - /** - * {@link #resolveScopedNames(IdStore, HashStore, int)} for a span: resolves its pending aliases, - * once it has a direction, into the tags they denote, and returns the holder of those left - * pending, or null when none are. + * Resolves a span's pending aliases, once it has a direction, into the tags they denote: the + * alias, being the newer write, overwrites its tag. Returns the holder of those left pending -- + * an alias with no tag in {@code direction}, or any while the direction is unknown or none -- or + * null when none are. */ - static PendingAliases resolvePending( - IdStore ids, HashStore tags, PendingAliases pending, int direction) { + static PendingAliases resolvePending(TagMap tags, PendingAliases pending, int direction) { if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { return pending; // no alias names a tag on a span with no direction, or an internal one } @@ -392,9 +281,9 @@ static PendingAliases resolvePending( long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { Object value = pending.values[i]; - tags.remove(alias); // a copy emitted earlier + TagRouting.removeCustom(tags, alias); // a copy emitted earlier pending.remove(alias); - TagRouting.setKnown(ids, tags, tagId, value); + TagRouting.setKnown(tags, tagId, value); } else { i++; } @@ -406,9 +295,9 @@ static PendingAliases resolvePending( * Copies the pending aliases into {@code tags} as custom tags under their literal names, so that * emitting the span's tags includes them. The holder stays the span's record of them. */ - static void copyPending(PendingAliases pending, HashStore tags) { + static void copyPending(PendingAliases pending, TagMap tags) { for (int i = 0; i < pending.size; i++) { - tags.set(pending.names[i], pending.values[i]); + TagRouting.setCustom(tags, pending.names[i], pending.values[i]); } } 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 fba685be7c2..59fab946f56 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 @@ -206,9 +206,8 @@ void theLastWriteWinsBetweenATagAndItsAlias( 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(); - DirectionalTags.resolveScopedNames(emitted.idStore(), emitted.hashStore(), direction); + // And when the span is emitted: the pending alias, if any, is already folded into the tag. + TagMap emitted = span.getTags(); assertEquals("second.example", emitted.getObject(tag)); assertNull(emitted.getObject("server.address")); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java index f6aeba7be15..6c58fdfce3e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java @@ -6,19 +6,27 @@ import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.core.DirectionalTags.PendingAliases; import org.junit.jupiter.api.Test; import org.tabletest.junit.TableTest; /** - * {@link DirectionalTags}: a span's reading of direction-dependent names over a plain {@link - * TagMap}. The span-level behavior, with real spans, is in the directional span tests. + * {@link DirectionalTags}: a span's reading of direction-dependent names over its tags and its + * pending aliases, with no span. The span-level behavior, with real spans, is in the directional + * span tests. */ class DirectionalTagsTest { + private static PendingAliases pending(String alias, Object value) { + return DirectionalTags.addPending(null, alias, value); + } + @TableTest({ "scenario | direction | alias | tag ", "client: alias reads its tag | 1 | server.address | peer.hostname ", @@ -31,20 +39,16 @@ void anAliasAndItsTagReadAsOne(int direction, String alias, String tag) { TagMap tags = TagMap.create(); tags.set(tag, "stored"); - assertEquals("stored", DirectionalTags.get(tags.idStore(), tags.hashStore(), alias, direction)); - assertEquals("stored", DirectionalTags.get(tags.idStore(), tags.hashStore(), tag, direction)); - assertEquals( - "stored", - DirectionalTags.get(tags.idStore(), tags.hashStore(), KnownTagCodec.keyOf(tag), direction)); + assertEquals("stored", DirectionalTags.get(tags, null, alias, direction)); + assertEquals("stored", DirectionalTags.get(tags, null, tag, direction)); + assertEquals("stored", DirectionalTags.get(tags, null, 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"); + PendingAliases pending = pending(alias, "pending"); + assertEquals("pending", DirectionalTags.get(tags, pending, alias, direction)); + assertEquals("pending", DirectionalTags.get(tags, pending, tag, direction)); assertEquals( - "pending", DirectionalTags.get(tags.idStore(), tags.hashStore(), alias, direction)); - assertEquals("pending", DirectionalTags.get(tags.idStore(), tags.hashStore(), tag, direction)); - assertEquals( - "pending", - DirectionalTags.get(tags.idStore(), tags.hashStore(), KnownTagCodec.keyOf(tag), direction)); + "pending", DirectionalTags.get(tags, pending, KnownTagCodec.keyOf(tag), direction)); } @Test @@ -52,47 +56,29 @@ void withNoDirectionAnAliasReadsOnlyAsWritten() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "stored"); - assertNull( - DirectionalTags.get(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_UNKNOWN)); + assertNull(DirectionalTags.get(tags, null, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); assertEquals( - "stored", - DirectionalTags.get( - tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); - assertEquals( - "stored", - DirectionalTags.get( - tags.idStore(), tags.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); + "stored", DirectionalTags.get(tags, null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); - tags.set("server.address", "pending"); + PendingAliases pending = pending("server.address", "pending"); assertEquals( - "pending", - DirectionalTags.get(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_UNKNOWN)); + "pending", DirectionalTags.get(tags, pending, "server.address", DIRECTION_UNKNOWN)); assertEquals( - "stored", - DirectionalTags.get( - tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + "stored", DirectionalTags.get(tags, pending, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); } @Test void aMissReadsAsNull() { TagMap tags = TagMap.create(); - assertNull(DirectionalTags.get(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get( - tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get( - tags.idStore(), tags.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get( - tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get( - tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, "my.tag", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, "server.address", DIRECTION_OUTBOUND)); // client.port names no tag on a client span - assertNull( - DirectionalTags.get(tags.idStore(), tags.hashStore(), "client.port", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, "client.port", DIRECTION_OUTBOUND)); } @Test @@ -101,30 +87,22 @@ void otherNamesReadAsWritten() { tags.set("my.tag", "custom"); tags.set(Tags.HTTP_METHOD, "GET"); + assertEquals("custom", DirectionalTags.get(tags, null, "my.tag", DIRECTION_OUTBOUND)); + assertEquals("GET", DirectionalTags.get(tags, null, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); assertEquals( - "custom", - DirectionalTags.get(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_OUTBOUND)); - assertEquals( - "GET", - DirectionalTags.get( - tags.idStore(), tags.hashStore(), Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); - assertEquals( - "GET", - DirectionalTags.get( - tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + "GET", DirectionalTags.get(tags, null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); } @Test void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { TagMap tags = TagMap.create(); - tags.set("server.address", "older"); + PendingAliases pending = pending("server.address", "older"); long tagId = - DirectionalTags.prepareAliasWrite( - tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND); + DirectionalTags.prepareAliasWrite(tags, pending, "server.address", DIRECTION_OUTBOUND); assertEquals(KnownTags.PEER_HOSTNAME_ID, tagId); - assertNull(tags.getObject("server.address")); // the older pending value is dropped + assertTrue(pending.isEmpty()); // the older pending value is dropped } @TableTest({ @@ -136,83 +114,87 @@ void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { }) void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, long result) { TagMap tags = TagMap.create(); - tags.set(name, "pending"); + PendingAliases pending = pending(name, "pending"); // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all - assertEquals( - result, - DirectionalTags.prepareAliasWrite(tags.idStore(), tags.hashStore(), name, direction)); - assertEquals("pending", tags.getObject(name)); + assertEquals(result, DirectionalTags.prepareAliasWrite(tags, pending, name, direction)); + assertEquals("pending", pending.get(name)); } @Test void aTagWriteWithNoDirectionDropsEveryAliasThatCouldDenoteIt() { TagMap tags = TagMap.create(); - tags.set("client.port", 1); - tags.set("server.port", 2); + PendingAliases pending = pending("client.port", 1); + pending.put("server.port", 2); - DirectionalTags.prepareWrite( - tags.idStore(), tags.hashStore(), KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite(tags, pending, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); - assertNull(tags.getObject("client.port")); - assertNull(tags.getObject("server.port")); + assertTrue(pending.isEmpty()); } @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); + PendingAliases pending = pending("client.port", 1); // no tag on a client span: kept + pending.put("server.port", 2); - DirectionalTags.prepareWrite( - tags.idStore(), tags.hashStore(), KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); + DirectionalTags.prepareWrite(tags, pending, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); - assertEquals(1, tags.getObject("client.port")); - assertNull(tags.getObject("server.port")); + assertEquals(1, pending.get("client.port")); + assertNull(pending.get("server.port")); } @Test void aTagWithoutAnAliasIsLeftAlone() { TagMap tags = TagMap.create(); - tags.set("server.address", "pending"); + PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.prepareWrite( - tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); - DirectionalTags.prepareWrite( - tags.idStore(), tags.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); + DirectionalTags.prepareWrite(tags, pending, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite(tags, pending, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); - assertEquals("pending", tags.getObject("server.address")); + assertEquals("pending", pending.get("server.address")); } @Test void removingAnAliasRemovesItsTag() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "stored"); - tags.set("server.address", "pending"); + PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.remove(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND); + DirectionalTags.remove(tags, pending, "server.address", DIRECTION_OUTBOUND); assertNull(tags.getObject(Tags.PEER_HOSTNAME)); - assertNull(tags.getObject("server.address")); + assertTrue(pending.isEmpty()); } @Test void removingATagRemovesItsAlias() { TagMap byName = TagMap.create(); byName.set(Tags.HTTP_HOSTNAME, "stored"); - byName.set("server.address", "pending"); - DirectionalTags.remove( - byName.idStore(), byName.hashStore(), Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); + PendingAliases pendingByName = pending("server.address", "pending"); + DirectionalTags.remove(byName, pendingByName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); assertNull(byName.getObject(Tags.HTTP_HOSTNAME)); - assertNull(byName.getObject("server.address")); + assertTrue(pendingByName.isEmpty()); TagMap byId = TagMap.create(); byId.set(Tags.HTTP_HOSTNAME, "stored"); - byId.set("server.address", "pending"); - DirectionalTags.remove( - byId.idStore(), byId.hashStore(), KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); + PendingAliases pendingById = pending("server.address", "pending"); + DirectionalTags.remove(byId, pendingById, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); - assertNull(byId.getObject("server.address")); + assertTrue(pendingById.isEmpty()); + } + + @Test + void removingAnAliasAlsoRemovesACopyEmittedEarlier() { + TagMap tags = TagMap.create(); + PendingAliases pending = pending("server.address", "pending"); + DirectionalTags.copyPending(pending, tags); + assertEquals("pending", tags.getObject("server.address")); + + DirectionalTags.remove(tags, pending, "server.address", DIRECTION_UNKNOWN); + + assertNull(tags.getObject("server.address")); + assertTrue(pending.isEmpty()); } @Test @@ -221,9 +203,8 @@ void removingATagWithNoAliasLeavesUnrelatedTags() { tags.set(Tags.PEER_HOSTNAME, "stored"); tags.set("my.tag", "custom"); - DirectionalTags.remove(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_OUTBOUND); - DirectionalTags.remove( - tags.idStore(), tags.hashStore(), KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); + DirectionalTags.remove(tags, null, "my.tag", DIRECTION_OUTBOUND); + DirectionalTags.remove(tags, null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("my.tag")); @@ -239,10 +220,9 @@ void removingATagWithNoAliasLeavesUnrelatedTags() { 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"); - DirectionalTags.resolveScopedNames(tags.idStore(), tags.hashStore(), direction); + assertNull(DirectionalTags.resolvePending(tags, pending(alias, "newer"), direction)); assertEquals("newer", tags.getObject(tag)); assertNull(tags.getObject(alias)); @@ -250,29 +230,29 @@ void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String t } @Test - void resolvingLeavesAnAliasWithNoTagAsWritten() { + void resolvingLeavesAnAliasWithNoTagPending() { TagMap tags = TagMap.create(); - tags.set("client.port", 1234); - tags.set("server.address", "pending"); + PendingAliases pending = pending("client.port", 1234); + pending.put("server.address", "pending"); - DirectionalTags.resolveScopedNames(tags.idStore(), tags.hashStore(), DIRECTION_OUTBOUND); - assertEquals(1234, tags.getObject("client.port")); + assertSame(pending, DirectionalTags.resolvePending(tags, pending, DIRECTION_OUTBOUND)); + assertEquals(1234, pending.get("client.port")); + assertNull(pending.get("server.address")); assertEquals("pending", tags.getObject(Tags.PEER_HOSTNAME)); - TagMap unknown = TagMap.create(); - unknown.set("server.address", "pending"); - DirectionalTags.resolveScopedNames(unknown.idStore(), unknown.hashStore(), DIRECTION_UNKNOWN); - assertEquals("pending", unknown.getObject("server.address")); + PendingAliases unknown = pending("server.address", "pending"); + assertSame(unknown, DirectionalTags.resolvePending(tags, unknown, DIRECTION_UNKNOWN)); + assertEquals("pending", unknown.get("server.address")); } @Test - void resolvingWithNoAliasChangesNothing() { + void anAliasThatNeverResolvesIsEmittedUnderItsLiteralName() { TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "stored"); + PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.resolveScopedNames(tags.idStore(), tags.hashStore(), DIRECTION_OUTBOUND); + DirectionalTags.copyPending(pending, tags); - assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); + assertEquals("pending", tags.getObject("server.address")); assertEquals(1, tags.size()); } @@ -281,34 +261,27 @@ void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "older"); - DirectionalTags.remove(tags.idStore(), tags.hashStore(), "server.address", DIRECTION_UNKNOWN); + DirectionalTags.remove(tags, null, "server.address", DIRECTION_UNKNOWN); // once the span is a client, server.address names peer.hostname: the removal must stick - assertNull( - DirectionalTags.get( - tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get( - tags.idStore(), tags.hashStore(), "server.address", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(tags, null, "server.address", DIRECTION_OUTBOUND)); } @Test void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { TagMap byName = TagMap.create(); - byName.set("server.address", "pending"); - DirectionalTags.remove( - byName.idStore(), byName.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); - assertNull( - DirectionalTags.get( - byName.idStore(), byName.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + PendingAliases pendingByName = pending("server.address", "pending"); + DirectionalTags.remove(byName, pendingByName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); + assertTrue(pendingByName.isEmpty()); + assertNull(DirectionalTags.get(byName, pendingByName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); TagMap byId = TagMap.create(); - byId.set("server.address", "pending"); - DirectionalTags.remove( - byId.idStore(), byId.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + PendingAliases pendingById = pending("server.address", "pending"); + DirectionalTags.remove(byId, pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + assertTrue(pendingById.isEmpty()); assertNull( - DirectionalTags.get( - byId.idStore(), byId.hashStore(), KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + DirectionalTags.get(byId, pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); } @Test @@ -317,12 +290,23 @@ void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { tags.set(Tags.PEER_HOSTNAME, "kept"); tags.set("my.tag", "gone"); - DirectionalTags.remove(tags.idStore(), tags.hashStore(), "my.tag", DIRECTION_UNKNOWN); + DirectionalTags.remove(tags, null, "my.tag", DIRECTION_UNKNOWN); assertNull(tags.getObject("my.tag")); - assertEquals( - "kept", - DirectionalTags.get( - tags.idStore(), tags.hashStore(), Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertEquals("kept", DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + } + + @Test + void thePendingHolderGrowsAndKeepsWriteOrder() { + PendingAliases pending = pending("a", 1); + pending.put("b", 2); + pending.put("c", 3); + pending.put("a", 4); // replaces in place + pending.remove("b"); + pending.remove("missing"); + + assertEquals(4, pending.get("a")); + assertNull(pending.get("b")); + assertEquals(3, pending.get("c")); } } diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 5d049ffacb2..55187bf9e9e 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -3,15 +3,10 @@ @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) -# A span's tag stores, and the routing over them, skip checks the tracer core has already made; -# they are @Restricted to the core (see datadog.apicontract.Restricted). Instrumentation uses -# TagMap's checked methods. -@defaultMessage Tag stores are restricted to the tracer core; use TagMap's checked methods instead. -datadog.trace.api.HashStore -datadog.trace.api.IdStore +# A span's tags, and the routing over them, skip checks the tracer core has already made; they are +# @Restricted to the core (see datadog.apicontract.Restricted). Instrumentation uses TagMap's +# checked methods. +@defaultMessage Tag routing is restricted to the tracer core; use TagMap's checked methods instead. datadog.trace.api.TagRouting datadog.trace.api.TagMap$DirectionSource -datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,datadog.trace.api.TagMap$DirectionSource) -datadog.trace.api.TagMap#readThroughStore(datadog.trace.api.TagMap) -datadog.trace.api.TagMap#hashStore() -datadog.trace.api.TagMap#idStore() +datadog.trace.api.TagMap#forSpan(datadog.trace.api.TagMap,datadog.trace.api.TagMap$DirectionSource) diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 2b4a87ab39a..cb6a2c984ba 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -1,72 +1,338 @@ package datadog.trace.api; +import datadog.trace.api.TagMap.BucketGroup; import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; -/** The {@link IdStore} view of a {@link HashStore}'s known tags; see {@link HashStore#idStore}. */ +/** + * The hash {@link IdStore}: known tags in their own bucket array ({@link HashSupport}), keyed by + * id, reading through to the frozen parent's id store. + * + *

A read checks the slot masks before probing: a clear local bit skips the buckets, and a clear + * inherited bit skips the read-through. Removing an inherited tag puts that tag's shared tombstone + * ({@link Entry#tombstoneOf}) in the local buckets, so it allocates nothing, and sets its slot in + * the tombstone mask; the bucket entry is the exact state, the mask the hint. A tombstone is never + * counted, iterated or returned. + */ final class HashIdStore extends IdStore { - private final HashStore hash; + final Object[] buckets; - HashIdStore(HashStore hash) { - this.hash = hash; + /** Live local entries, not counting tombstones. */ + int count; + + /** The frozen parent's id store, or null. */ + final HashIdStore parent; + + HashIdStore(IdStore parent) { + super(parent); + this.buckets = HashSupport.newBuckets(); + this.parent = (HashIdStore) parent; + } + + /** A parentless store over {@code buckets}; reads no statics, so TagMap.EMPTY can use it. */ + HashIdStore(Object[] buckets, int count) { + super(null); + this.buckets = buckets; + this.count = count; + this.parent = null; + } + + @Override + Entry get(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw notKnown(tagId); + } + return this.getVisible(tagId, slotBit(tagId)); + } + + /** {@link #get} for a validated id and its slot bit. */ + private Entry getVisible(long tagId, long bit) { + if (this.mayHaveLocal(bit)) { + Entry local = + HashSupport.find(this.buckets, Entry.bucketHash(tagId), KnownTagCodec.nameOf(tagId)); + if (local != null) { + return local.tombstone ? null : local; + } + } + return this.mayInherit(bit) ? this.parent.getVisible(tagId, bit) : null; + } + + @Override + Object getKnownValue(long tagId) { + Entry entry = this.get(tagId); + return entry == null ? null : entry.objectValue(); + } + + @Override + void set(long tagId, Object value) { + this.put(new Entry(tagId, null, Entry.ANY, 0L, value)); + } + + @Override + void set(long tagId, CharSequence value) { + this.put(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + } + + @Override + void set(long tagId, boolean value) { + this.put( + new Entry( + tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + } + + @Override + void set(long tagId, int value) { + this.put(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + } + + @Override + void set(long tagId, long value) { + this.put(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + } + + @Override + void set(long tagId, float value) { + this.put(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + } + + @Override + void set(long tagId, double value) { + this.put(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } @Override - public Entry get(long tagId) { - return this.hash.getById(tagId); + Entry put(Entry knownEntry) { + Entry replaced = HashSupport.put(this.buckets, knownEntry); + this.occupancy |= slotBit(knownEntry.tagHash); + if (replaced == null || replaced.tombstone) { + this.count++; // a tombstone replaced is a removal undone: no prior local entry + return null; + } + return replaced; } @Override - public Object getKnownValue(long tagId) { - return this.hash.getKnownValue(tagId); + Entry getAndPut(Entry knownEntry) { + long tagId = knownEntry.tagHash; + long bit = slotBit(tagId); + Entry replaced = HashSupport.put(this.buckets, knownEntry); + this.occupancy |= bit; + if (replaced != null && !replaced.tombstone) { + return replaced; // replaced a local entry -> that is the prior value + } + this.count++; + if (replaced != null || !this.mayInherit(bit)) { + return null; // it was tombstoned, so not visible, or nothing to inherit + } + return this.parent.getVisible(tagId, bit); } @Override - public void set(long tagId, Object value) { - this.hash.setKnown(tagId, value); + Entry remove(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw notKnown(tagId); + } + long bit = slotBit(tagId); + int hash = Entry.bucketHash(tagId); + String name = KnownTagCodec.nameOf(tagId); + + Entry local = this.mayHaveLocal(bit) ? HashSupport.find(this.buckets, hash, name) : null; + if (local != null) { + if (local.tombstone) { + return null; // already removed + } + HashSupport.remove(this.buckets, hash, name); + this.count--; + } + if (this.mayInherit(bit)) { + // Read-through: hide an inherited value with a tombstone. The prior *visible* value is the + // local entry if there was one, otherwise the inherited one. + Entry inherited = this.parent.getVisible(tagId, bit); + if (inherited != null) { + HashSupport.put(this.buckets, Entry.tombstoneOf(tagId)); + this.tombstoneMask |= bit; + return local != null ? local : inherited; + } + } + return local; } @Override - public void set(long tagId, CharSequence value) { - this.hash.setKnown(tagId, value); + int size() { + return this.parent == null ? this.count : this.count + this.visibleParentCount(); + } + + /** Ancestor entries not shadowed or tombstoned by a nearer level, nearest-level-wins. */ + private int visibleParentCount() { + int[] count = new int[1]; + this.forEachParent(count, null, (c, unused, entry) -> c[0]++); + return count[0]; } @Override - public void set(long tagId, boolean value) { - this.hash.setKnown(tagId, value); + boolean isEmpty() { + if (this.count != 0) { + return false; + } + HashIdStore parent = this.parent; + if (parent == null) { + return true; + } + if (this.tombstoneMask == 0L) { + return parent.isEmpty(); // nothing hidden: the parent's tags are all visible + } + return this.visibleParentCount() == 0; + } + + @Override + boolean isDefinitelyEmpty() { + for (HashIdStore level = this; level != null; level = level.parent) { + if (level.count != 0) { + return false; + } + } + return true; + } + + @Override + int estimateSize() { + int total = 0; + for (HashIdStore level = this; level != null; level = level.parent) { + total += level.count; + } + return total; + } + + @Override + boolean hasParent() { + return this.parent != null; + } + + /** + * Whether {@code parentEntry}, from the ancestor level {@code fromAncestor}, is visible through + * this leaf: no nearer level holds an entry or a tombstone for it. A nearer level whose masks + * rule the slot out is skipped without a probe. + */ + boolean parentEntryVisible(Entry parentEntry, HashIdStore fromAncestor) { + long bit = slotBit(parentEntry.tagHash); + int hash = parentEntry.hash(); + String tag = parentEntry.tag; + for (HashIdStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { + if (nearer.mayHaveLocal(bit) && HashSupport.find(nearer.buckets, hash, tag) != null) { + return false; + } + } + return true; + } + + @Override + void forEach(Consumer consumer) { + HashSupport.forEach(this.buckets, consumer); + if (this.parent != null) { + this.forEachParent(consumer, null, (c, unused, entry) -> c.accept(entry)); + } + } + + @Override + void forEach(T thisObj, BiConsumer consumer) { + HashSupport.forEach(this.buckets, thisObj, consumer); + if (this.parent != null) { + this.forEachParent(thisObj, consumer, (t, c, entry) -> c.accept(t, entry)); + } + } + + @Override + void forEach(T thisObj, U otherObj, TriConsumer consumer) { + HashSupport.forEach(this.buckets, thisObj, otherObj, consumer); + if (this.parent != null) { + this.forEachParent(thisObj, otherObj, consumer); + } + } + + private void forEachParent( + T thisObj, U otherObj, TriConsumer consumer) { + for (HashIdStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } + } + } + } + } } @Override - public void set(long tagId, int value) { - this.hash.setKnown(tagId, value); + IdStore copy() { + HashIdStore copy = new HashIdStore(this.parent); + HashSupport.copyInto(copy.buckets, this.buckets); + copy.count = this.count; + copy.occupancy = this.occupancy; + copy.tombstoneMask = this.tombstoneMask; + return copy; } @Override - public void set(long tagId, long value) { - this.hash.setKnown(tagId, value); + void putAllLocal(IdStore other) { + HashIdStore that = (HashIdStore) other; + if (that.count == 0) { + return; + } + if (this.tombstoneMask != 0L) { + // A bucket merge would count a replaced tombstone as a replacement, so put one at a time. + HashSupport.forEach(that.buckets, this, (self, entry) -> self.put(entry.entry())); + return; + } + if (this.count == 0) { + HashSupport.copyInto(this.buckets, that.buckets); + this.count = that.count; + } else { + this.count += HashSupport.merge(this.buckets, that.buckets); + } + this.occupancy |= that.occupancy; } @Override - public void set(long tagId, float value) { - this.hash.setKnown(tagId, value); + void fillMapLocal(Map map) { + HashSupport.fillMap(this.buckets, map); } @Override - public void set(long tagId, double value) { - this.hash.setKnown(tagId, value); + void fillStringMapLocal(Map map) { + HashSupport.fillStringMap(this.buckets, map); } @Override - public Entry put(Entry knownEntry) { - return this.hash.put(knownEntry); + void checkIntegrity() { + HashSupport.checkBuckets(this.buckets); + if (this.count != this.computeSize()) { + throw new IllegalStateException("incorrect size"); + } } @Override - public Entry getAndPut(Entry knownEntry) { - return this.hash.getAndPut(knownEntry); + int computeSize() { + return HashSupport.count(this.buckets); } @Override - public Entry remove(long tagId) { - return this.hash.removeById(tagId); + String toInternalString() { + return HashSupport.toInternalString(this.buckets); } } diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 68f0972d157..208a54731ec 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -1,11 +1,9 @@ package datadog.trace.api; -import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.BucketGroup; import datadog.trace.api.TagMap.Entry; import datadog.trace.api.TagMap.EntryReader; import datadog.trace.api.function.TriConsumer; -import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -13,107 +11,50 @@ import java.util.function.Consumer; /** - * TagMap's hash store: a fixed-size bucket array of shareable, immutable {@link Entry} objects, - * keyed by the 64-bit tag hash (a known tag's id, or a custom tag's name hash in the low 32 bits; - * the two never collide, see {@link Entry#tagHashOf}). + * The custom-tag half of a {@link TagMap}: custom tags by name, in their own bucket array ({@link + * HashSupport}), reading through to the frozen parent's custom store. Name-keyed methods take only + * custom tag names (asserted): a known tag is resolved to its id first, and goes to the {@link + * IdStore}. * - *

It is the name store, holding the custom tags by name. Through the tag-hash scheme it can hold - * known tags too, by id: in hash mode it holds both, and {@link #idStore()} adapts its known-tag - * half to the {@link IdStore} API. Other IdStore implementations (a dense store) are separate - * classes. No locking: guarded by the owner's lock. - * - *

Owned by a {@link TagMap}, or directly by a span in the tracer core, which routes to it with - * {@link TagRouting}. Name-keyed methods take only custom tag names: a known tag is resolved to its - * id first, and goes through the id-keyed methods. - * - *

For memory efficiency, a bucket holds a lone Entry directly. Because Entries are shared - * between maps, they cannot form a linked list to handle collisions; colliding entries share a - * fixed-size {@link BucketGroup} instead, chained when one fills. Removals are assumed rare, so - * groups are never consolidated, but an emptied group is unlinked from its chain. + *

Created lazily by its owner on the first custom write; until then the owner borrows its + * parent's custom store, if any, read-only. No locking: guarded by the owner's lock. */ -@Restricted(allowedIn = {"datadog.trace.core"}) -public final class HashStore { +final class HashStore { final Object[] buckets; - int size; - /** - * Optional frozen parent store for read-through. When non-null, reads that miss the local buckets - * fall through to the parent chain, nearest-level-wins (a local entry shadows the parent's, a - * nearer ancestor shadows a farther one). The chain mirrors the owning TagMaps' parents. Not - * final only so {@link #clear()} can detach it (to null); it is otherwise fixed at construction - * and never re-pointed. - */ - HashStore parent; + /** Local entries. */ + int count; + + /** The frozen parent's custom store, or null. */ + final HashStore parent; /** - * Parent keys removed locally (read-through tombstones), by canonical name. Lazily allocated on - * the first such removal; {@code null} both means "no tombstones" and serves as the gate that - * keeps the hot paths untouched. Only meaningful when {@link #parent} != null. A tombstone stops - * read-through fall-through for its key, so a key removed from a child no longer reads through to - * the parent. Kept off the bucket structure deliberately -- it is shape-agnostic (bare-Entry vs - * BucketGroup) and rare, so it costs a lazy allocation on removal rather than complicating the - * hot bucket code. + * Parent keys removed locally (read-through tombstones). Lazily allocated on the first such + * removal; {@code null} both means "no tombstones" and serves as the gate that keeps the hot + * paths untouched. A tombstone stops read-through for its key. */ private Set removedFromParent; - /** A fresh, empty store with no parent. */ - public HashStore() { - this((HashStore) null); - } - - /** - * A fresh, empty store reading through to {@code parent} (may be null), which must be frozen -- - * see {@link TagMap#readThroughStore}. - */ - public HashStore(HashStore parent) { - // needs to be a power of 2 for bucket masking calculation to work as intended - this.buckets = new Object[1 << 4]; - this.size = 0; + /** A fresh, empty store reading through to {@code parent} (may be null), which is frozen. */ + HashStore(HashStore parent) { + this.buckets = HashSupport.newBuckets(); this.parent = parent; } - /** A parentless store over {@code buckets}; reads no statics, so TagMap.EMPTY can use it. */ - HashStore(Object[] buckets, int size) { - this.buckets = buckets; - this.size = size; - this.parent = null; - } - /** Exact count, including the visible part of the read-through chain. */ int size() { - HashStore parent = this.parent; - return parent == null ? this.size : this.size + this.visibleParentCount(); + return this.parent == null ? this.count : this.count + this.visibleParentCount(); } - /** - * Exact count of ancestor entries not shadowed by a nearer level or tombstoned (the read-through - * addition). Walks the ancestor chain iteratively, nearest-level-wins. - */ + /** Ancestor entries not shadowed or tombstoned by a nearer level, nearest-level-wins. */ private int visibleParentCount() { - int count = 0; - for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - if (parentEntryVisible((Entry) parentBucket, ancestor)) count++; - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) count++; - } - } - } - } - } - return count; + int[] count = new int[1]; + this.forEachParent(count, null, (c, unused, entry) -> c[0]++); + return count[0]; } boolean isEmpty() { - if (this.size != 0) { + if (this.count != 0) { return false; } HashStore parent = this.parent; @@ -121,235 +62,102 @@ boolean isEmpty() { return true; } if (this.removedFromParent == null) { - // A live parent is never definitely-empty: TagMap.createFromParent drops an empty parent (and - // copy() only forwards an already-vetted parent), and the parent is frozen so it can't become - // empty. So with no local entries and no tombstones, the parent contributes at least one - // visible entry -> this store is non-empty. - return false; + return parent.isEmpty(); // nothing hidden: the parent's tags are all visible } - // size == 0 with tombstones (rare): empty iff every visible ancestor entry is tombstoned return this.visibleParentCount() == 0; } - /** Cheap: empty iff no level in the chain holds a local entry (ignores shadowing/tombstones). */ + /** Cheap: no level of the chain holds a local entry (ignores shadowing and tombstones). */ boolean isDefinitelyEmpty() { for (HashStore level = this; level != null; level = level.parent) { - if (level.size != 0) { + if (level.count != 0) { return false; } } return true; } - /** Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. */ + /** Upper bound: the local counts of every level. */ int estimateSize() { int total = 0; for (HashStore level = this; level != null; level = level.parent) { - total += level.size; + total += level.count; } return total; } - /** A new {@link IdStore} over this store's known tags. */ - public IdStore idStore() { - return new HashIdStore(this); - } - - /* - * Known tags, by id. - */ - - /** - * The known tag {@code tagId}'s entry, reading through to the parent chain. - * - * @throws IllegalArgumentException if {@code tagId} names no known tag - */ - public Entry getById(long tagId) { - return this.get(Entry.requireKnownName(tagId), tagId); - } - - /** {@link #getById}'s value, boxed if necessary, or null. */ - public Object getKnownValue(long tagId) { - Entry entry = this.getById(tagId); - return entry == null ? null : entry.objectValue(); - } - - /** - * Removes the known tag {@code tagId}, returning the prior visible entry. - * - * @throws IllegalArgumentException if {@code tagId} names no known tag - */ - public Entry removeById(long tagId) { - return this.remove(Entry.requireKnownName(tagId), tagId); - } - - /* - * Sets a known tag by id without validating it: the caller has checked KnownTagCodec.isKnown. - */ - - public void setKnown(long tagId, Object value) { - this.put(new Entry(tagId, null, Entry.ANY, 0L, value)); - } - - public void setKnown(long tagId, CharSequence value) { - this.put(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); - } - - public void setKnown(long tagId, boolean value) { - this.put( - new Entry( - tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); - } - - public void setKnown(long tagId, int value) { - this.put(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); - } - - public void setKnown(long tagId, long value) { - this.put(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); - } - - public void setKnown(long tagId, float value) { - this.put(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); - } - - public void setKnown(long tagId, double value) { - this.put(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); - } - - /* - * Custom tags, by name (the name-store role). The name must not be a known tag's: a caller - * resolves a name once and routes a known tag by its id instead (see TagRouting). - */ - /** The custom tag {@code tag}'s entry, reading through to the parent chain. */ - public Entry get(String tag) { + Entry get(String tag) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; look it up by its id"; - return this.get(tag, Entry.customHash(tag)); + return this.get(tag, Entry.bucketHash(Entry.customHash(tag))); } - /** Removes the custom tag {@code tag}; see {@link #remove(String, long)}. */ - public Entry remove(String tag) { - assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; remove it by its id"; - return this.remove(tag, Entry.customHash(tag)); + private Entry get(String tag, int hash) { + Entry local = HashSupport.find(this.buckets, hash, tag); + if (local != null) { + return local; + } + HashStore parent = this.parent; + if (parent == null) { + return null; + } + if (this.removedFromParent != null && this.removedFromParent.contains(tag)) { + return null; // tombstoned: removed locally, do not read through + } + return parent.get(tag, hash); } /* * Sets the custom tag {@code tag}, which must not be a known tag's name. */ - public void set(String tag, Object value) { + void set(String tag, Object value) { this.put(Entry.newAnyEntry(tag, value)); } - public void set(String tag, CharSequence value) { + void set(String tag, CharSequence value) { this.put(Entry.newObjectEntry(tag, value)); } - public void set(String tag, boolean value) { + void set(String tag, boolean value) { this.put(Entry.newBooleanEntry(tag, value)); } - public void set(String tag, int value) { + void set(String tag, int value) { this.put(Entry.newIntEntry(tag, value)); } - public void set(String tag, long value) { + void set(String tag, long value) { this.put(Entry.newLongEntry(tag, value)); } - public void set(String tag, float value) { + void set(String tag, float value) { this.put(Entry.newFloatEntry(tag, value)); } - public void set(String tag, double value) { + void set(String tag, double value) { this.put(Entry.newDoubleEntry(tag, value)); } - /* - * Either kind, by the entry's own identity (its canonical name and tag hash). - */ - - /** The entry for {@code (canonicalTag, tagHash)}: local first, then the read-through chain. */ - private Entry get(String canonicalTag, long tagHash) { - Entry local = this.getLocal(canonicalTag, tagHash); - if (local != null) { - // Local entry shadows the parent (local-wins) -- unchanged hot path. - return local; - } - // Read-through: miss locally, defer to the frozen parent. - // The tombstone check lives only here, on the cold miss+parent path -- the hot local hit above - // never touches it. - HashStore parent = this.parent; - if (parent == null) { - return null; - } - if (this.removedFromParent != null && this.removedFromParent.contains(canonicalTag)) { - return null; // tombstoned: removed locally, do not read through - } - return parent.get(canonicalTag, tagHash); - } - /** - * Inserts or replaces a local entry, returning the replaced local Entry (or null if none). Does - * NOT consult the read-through parent -- set paths use this so they never pay for a prior-value - * lookup they discard; {@link #getAndPut(Entry)} layers the parent fallback on top. Takes either - * kind of entry: the entry already carries its id or its name. + * Inserts or replaces a custom tag's entry, returning the local entry it replaced (or null). Does + * not consult the read-through parent. */ - public Entry put(Entry newEntry) { + Entry put(Entry newEntry) { // 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) { this.removedFromParent.remove(newEntry.tag); } - - Object[] thisBuckets = this.buckets; - - int newHash = newEntry.hash(); - int bucketIndex = newHash & (thisBuckets.length - 1); - - Object bucket = thisBuckets[bucketIndex]; - if (bucket == null) { - thisBuckets[bucketIndex] = newEntry; - - this.size += 1; - return null; - } else if (bucket instanceof Entry) { - Entry existingEntry = (Entry) bucket; - if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { - thisBuckets[bucketIndex] = newEntry; - - // replaced existing entry - no size change - return existingEntry; - } else { - thisBuckets[bucketIndex] = - new BucketGroup(existingEntry.hash(), existingEntry, newHash, newEntry); - - this.size += 1; - return null; - } - } else if (bucket instanceof BucketGroup) { - BucketGroup lastGroup = (BucketGroup) bucket; - - BucketGroup containingGroup = lastGroup.findContainingGroupInChain(newHash, newEntry.tag); - if (containingGroup != null) { - // replaced existing entry - no size change - return containingGroup._replace(newHash, newEntry); - } - - if (!lastGroup.insertInChain(newHash, newEntry)) { - thisBuckets[bucketIndex] = new BucketGroup(newHash, newEntry, lastGroup); - } - this.size += 1; - return null; + Entry replaced = HashSupport.put(this.buckets, newEntry); + if (replaced == null) { + this.count++; } - - // unreachable - return null; + return replaced; } /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - public Entry getAndPut(Entry newEntry) { + Entry getAndPut(Entry newEntry) { // Capture whether the key was tombstoned BEFORE put clears it: a tombstoned key had no visible // prior value (it was removed), so getAndPut must report null rather than the parent's. boolean wasTombstoned = @@ -357,41 +165,39 @@ public Entry getAndPut(Entry newEntry) { Entry priorLocal = this.put(newEntry); if (priorLocal != null) { - return priorLocal; // replaced a local entry -> that is the prior value + return priorLocal; } - // No local entry was replaced. The prior visible value, if any, was the parent's -- unless the - // key was tombstoned (then it was not visible). if (wasTombstoned || this.parent == null) { return null; } - // By the entry's own identity, not its name: the name would only resolve back to the same hash. - return this.parent.get(newEntry.tag, newEntry.tagHash); + return this.parent.get(newEntry.tag, newEntry.hash()); } /** - * Removes {@code (canonicalTag, tagHash)}, returning the prior visible entry. Removing a key the + * Removes the custom tag {@code tag}, returning the prior visible entry. Removing a key the * parent chain still exposes also installs a tombstone, so it stops reading through. */ - private Entry remove(String canonicalTag, long tagHash) { - Entry localRemoved = this.removeLocal(canonicalTag, tagHash); + Entry remove(String tag) { + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; remove it by its id"; + int hash = Entry.bucketHash(Entry.customHash(tag)); + Entry localRemoved = HashSupport.remove(this.buckets, hash, tag); + if (localRemoved != null) { + this.count--; + } HashStore parent = this.parent; if (parent != null) { - // Read-through: if the parent still exposes this key, removing it must also hide it from - // fall-through -- install a tombstone. The prior *visible* value (Map.remove contract) is the - // local entry if there was one, otherwise the parent's (which we now hide). Rare path (only - // when removing a parent-exposed key). boolean alreadyTombstoned = - this.removedFromParent != null && this.removedFromParent.contains(canonicalTag); + this.removedFromParent != null && this.removedFromParent.contains(tag); if (!alreadyTombstoned) { - Entry parentEntry = parent.get(canonicalTag, tagHash); + Entry parentEntry = parent.get(tag, hash); if (parentEntry != null) { if (this.removedFromParent == null) { // Small initial capacity: this set is rare and almost always holds only a handful of // tombstoned keys, so the default 16-bucket HashSet table would be wasteful. this.removedFromParent = new HashSet<>(4); } - this.removedFromParent.add(canonicalTag); + this.removedFromParent.add(tag); return localRemoved != null ? localRemoved : parentEntry; } } @@ -400,409 +206,40 @@ private Entry remove(String canonicalTag, long tagHash) { } /** - * Whether an entry that lives at ancestor level {@code fromAncestor} is visible through this - * leaf: not shadowed and not tombstoned by any nearer level (the leaf, or a closer ancestor). - * Nearest-level-wins. This mirrors what {@link #get(String, long)}'s recursion applies as it - * descends -- each nearer level contributes both its own local entries (shadowing) and its own - * read-through removals (tombstones). A non-leaf level can carry tombstones too: a map may remove - * an inherited key and then itself be frozen and reused as a parent. Exploits universal hashing - * -- the only local entry that could shadow {@code parentEntry} at a level is in 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). + * Whether {@code parentEntry}, from the ancestor level {@code fromAncestor}, is visible through + * this leaf: not shadowed and not tombstoned by any nearer level. */ boolean parentEntryVisible(Entry parentEntry, HashStore fromAncestor) { int hash = parentEntry.hash(); String tag = parentEntry.tag; for (HashStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - // tombstoned by a nearer level (its own read-through removal hides the deeper entry) if (nearer.removedFromParent != null && nearer.removedFromParent.contains(tag)) { return false; } - // shadowed by a nearer level's local entry - Object[] nearerBuckets = nearer.buckets; - Object nearerBucket = nearerBuckets[hash & (nearerBuckets.length - 1)]; - if (findInBucket(nearerBucket, hash, tag) != null) { + if (HashSupport.find(nearer.buckets, hash, tag) != null) { return false; } } return true; } - /** Looks up an entry in this store's own buckets only -- no read-through to the parent. */ - private Entry getLocal(String tag, long tagHash) { - Object[] thisBuckets = this.buckets; - int hash = Entry.bucketHash(tagHash); - return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag); - } - - /** - * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). - */ - private static Entry findInBucket(Object bucket, int hash, String tag) { - if (bucket instanceof Entry) { - Entry tagEntry = (Entry) bucket; - return tagEntry.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; - } else if (bucket instanceof BucketGroup) { - return ((BucketGroup) bucket).findInChain(hash, tag); - } - return null; - } - - /** Removes an entry from this store's own buckets only -- no parent/tombstone handling. */ - private Entry removeLocal(String tag, long tagHash) { - Object[] thisBuckets = this.buckets; - - int hash = Entry.bucketHash(tagHash); - int bucketIndex = hash & (thisBuckets.length - 1); - - Object bucket = thisBuckets[bucketIndex]; - // null bucket case - do nothing - if (bucket instanceof Entry) { - Entry existingEntry = (Entry) bucket; - if (existingEntry.hash() == hash && existingEntry.matches(tag)) { - thisBuckets[bucketIndex] = null; - - this.size -= 1; - return existingEntry; - } else { - return null; - } - } else if (bucket instanceof BucketGroup) { - BucketGroup lastGroup = (BucketGroup) bucket; - - BucketGroup containingGroup = lastGroup.findContainingGroupInChain(hash, tag); - if (containingGroup == null) { - return null; - } - - Entry existingEntry = containingGroup._remove(hash, tag); - if (containingGroup._isEmpty()) { - thisBuckets[bucketIndex] = lastGroup.removeGroupInChain(containingGroup); - } - - this.size -= 1; - return existingEntry; - } - return null; - } - - /** - * An independently mutable copy: the same (frozen, shared) parent, this store's own entries - * (shared Entry objects) and its tombstones. - */ - HashStore copy() { - HashStore copy = new HashStore(this.parent); - copy.putAllIntoEmpty(this); - if (this.removedFromParent != null) { - copy.removedFromParent = new HashSet<>(this.removedFromParent); - } - return copy; - } - - /** - * Copies {@code that}'s own entries into this store, sharing the Entry objects. Since stores - * don't support expansion, buckets are perfectly aligned. - */ - void putAllLocal(HashStore that) { - if (this.size == 0) { - this.putAllIntoEmpty(that); - } else { - this.putAllMerge(that); - } - } - - private void putAllMerge(HashStore that) { - Object[] thisBuckets = this.buckets; - Object[] thatBuckets = that.buckets; - - // Since TagMap-s don't support expansion, buckets are perfectly aligned - // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check - // elimination - for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { - Object thatBucket = thatBuckets[i]; - - // if nothing incoming, nothing to do - if (thatBucket == null) continue; - - Object thisBucket = thisBuckets[i]; - if (thisBucket == null) { - // This bucket is null, easy case - // Either copy over the sole entry or clone the BucketGroup chain - - if (thatBucket instanceof Entry) { - thisBuckets[i] = thatBucket; - this.size += 1; - } else if (thatBucket instanceof BucketGroup) { - BucketGroup thatGroup = (BucketGroup) thatBucket; - - BucketGroup thisNewGroup = thatGroup.cloneChain(); - thisBuckets[i] = thisNewGroup; - this.size += thisNewGroup.sizeInChain(); - } - } else if (thisBucket instanceof Entry) { - // This bucket is a single entry, medium complexity case - // If other side is an Entry - just merge the entries into a bucket - // If other side is a BucketGroup - then clone the group and insert the entry normally into - // the cloned group - - Entry thisEntry = (Entry) thisBucket; - int thisHash = thisEntry.hash(); - - if (thatBucket instanceof Entry) { - Entry thatEntry = (Entry) thatBucket; - int thatHash = thatEntry.hash(); - - if (thisHash == thatHash && thisEntry.matches(thatEntry.tag())) { - thisBuckets[i] = thatEntry; - // replacing entry, no size change - } else { - thisBuckets[i] = - new BucketGroup( - thisHash, thisEntry, - thatHash, thatEntry); - this.size += 1; - } - } else if (thatBucket instanceof BucketGroup) { - BucketGroup thatGroup = (BucketGroup) thatBucket; - - // Clone the other group, then place this entry into that group - BucketGroup thisNewGroup = thatGroup.cloneChain(); - int thisNewGroupSize = thisNewGroup.sizeInChain(); - - Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); - if (incomingEntry != null) { - // there's already an entry w/ the same tag from the incoming TagMap - // incoming entry clobbers the existing try, so we're done - thisBuckets[i] = thisNewGroup; - - // overlapping group - subtract one for clobbered existing entry - this.size += thisNewGroupSize - 1; - } else if (thisNewGroup.insertInChain(thisHash, thisEntry)) { - // able to add thisEntry into the existing groups - thisBuckets[i] = thisNewGroup; - - // non overlapping group - existing entry already accounted for in this.size - this.size += thisNewGroupSize; - } else { - // unable to add into the existing groups - thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup); - - // non overlapping group - existing entry already accounted for in this.size - this.size += thisNewGroupSize; - } - } - } else if (thisBucket instanceof BucketGroup) { - // This bucket is a BucketGroup, medium to hard case - // If the other side is an entry, just normal insertion procedure - no cloning required - BucketGroup thisGroup = (BucketGroup) thisBucket; - - if (thatBucket instanceof Entry) { - Entry thatEntry = (Entry) thatBucket; - int thatHash = thatEntry.hash(); - - if (thisGroup.replaceInChain(thatHash, thatEntry) != null) { - // replaced existing entry no size change - } else if (thisGroup.insertInChain(thatHash, thatEntry)) { - this.size += 1; - } else { - thisBuckets[i] = new BucketGroup(thatHash, thatEntry, thisGroup); - this.size += 1; - } - } else if (thatBucket instanceof BucketGroup) { - // Most complicated case - need to walk that bucket group chain and update this chain - BucketGroup thatGroup = (BucketGroup) thatBucket; - - // Taking the easy / expensive way out for updating size - int thisPrevGroupSize = thisGroup.sizeInChain(); - - BucketGroup thisNewGroup = thisGroup.replaceOrInsertAllInChain(thatGroup); - int thisNewGroupSize = thisNewGroup.sizeInChain(); - - thisBuckets[i] = thisNewGroup; - this.size += (thisNewGroupSize - thisPrevGroupSize); - } - } - } - } - - /* - * Specially optimized version of putAll for the common case of destination store being empty - */ - void putAllIntoEmpty(HashStore that) { - Object[] thisBuckets = this.buckets; - Object[] thatBuckets = that.buckets; - - // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check - // elimination - for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { - Object thatBucket = thatBuckets[i]; - - // faster to explicitly null check first, then do instanceof - if (thatBucket == null) { - // do nothing - } else if (thatBucket instanceof BucketGroup) { - // if it is a BucketGroup, then need to clone - BucketGroup thatGroup = (BucketGroup) thatBucket; - - thisBuckets[i] = thatGroup.cloneChain(); - } else { // if ( thatBucket instanceof Entry ) - thisBuckets[i] = thatBucket; - } - } - this.size = that.size; - } - - void fillMapLocal(Map map) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - map.put(thisEntry.tag, thisEntry.objectValue()); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.fillMapFromChain(map); - } - } - } - - void fillStringMapLocal(Map stringMap) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - stringMap.put(thisEntry.tag, thisEntry.stringValue()); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.fillStringMapFromChain(stringMap); - } - } - } - - /** Visits the local entries, then the visible read-through entries. */ + /** Visits the local entries, then the visible inherited ones. */ void forEach(Consumer consumer) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(consumer); - } - } - - // read-through: parent entries not shadowed locally or tombstoned. Kept out of line so the - // common parent == null path stays small / inlinable. + HashSupport.forEach(this.buckets, consumer); if (this.parent != null) { - this.forEachParent(consumer); - } - } - - private void forEachParent(Consumer consumer) { - // Walk the ancestor chain, nearest first. Each entry is emitted once, by the nearest level that - // defines its key, when not shadowed by a nearer level and not tombstoned. - for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) - consumer.accept(parentEntry); - } - } - } - } + this.forEachParent(consumer, null, (c, unused, entry) -> c.accept(entry)); } } void forEach(T thisObj, BiConsumer consumer) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisObj, thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(thisObj, consumer); - } - } - - // read-through: parent entries not shadowed locally or tombstoned (kept out of line). + HashSupport.forEach(this.buckets, thisObj, consumer); if (this.parent != null) { - this.forEachParent(thisObj, consumer); - } - } - - private void forEachParent(T thisObj, BiConsumer consumer) { - for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(thisObj, parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { - consumer.accept(thisObj, parentEntry); - } - } - } - } - } + this.forEachParent(thisObj, consumer, (t, c, entry) -> c.accept(t, entry)); } } void forEach(T thisObj, U otherObj, TriConsumer consumer) { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisObj, otherObj, thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(thisObj, otherObj, consumer); - } - } - - // read-through: parent entries not shadowed locally or tombstoned (kept out of line). + HashSupport.forEach(this.buckets, thisObj, otherObj, consumer); if (this.parent != null) { this.forEachParent(thisObj, otherObj, consumer); } @@ -813,19 +250,18 @@ private void forEachParent( for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) - consumer.accept(thisObj, otherObj, parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { - consumer.accept(thisObj, otherObj, parentEntry); + Entry entry = group._entryAt(j); + if (entry != null && this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); } } } @@ -835,120 +271,55 @@ private void forEachParent( } /** - * Removes every entry, including any inherited through read-through. Detaching the parent (rather - * than tombstoning every inherited key) is simpler and cheaper, and leaves an empty, parent-less - * store. Detach is one-way -- the parent is never re-pointed. + * An independently mutable copy: the same (frozen, shared) parent, this store's own entries + * (shared Entry objects) and its tombstones. */ - void clear() { - Arrays.fill(this.buckets, null); - this.size = 0; - this.parent = null; - this.removedFromParent = null; - } - - void checkIntegrity() { - // Decided to use if ( cond ) throw new IllegalStateException rather than assert - // That was done to avoid the extra static initialization needed for an assertion - // While that's probably an unnecessary optimization, this method is only called in tests - - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - int thisHash = thisEntry.hash(); - - int expectedBucket = thisHash & (thisBuckets.length - 1); - if (expectedBucket != i) { - throw new IllegalStateException("incorrect bucket"); - } - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry thisEntry = curGroup._entryAt(i); - if (thisEntry == null) continue; - - int thisHash = thisEntry.hash(); - assert curGroup._hashAt(i) == thisHash; - - int expectedBucket = thisHash & (thisBuckets.length - 1); - if (expectedBucket != i) { - throw new IllegalStateException("incorrect bucket"); - } - } - } - } + HashStore copy() { + HashStore copy = new HashStore(this.parent); + HashSupport.copyInto(copy.buckets, this.buckets); + copy.count = this.count; + if (this.removedFromParent != null) { + copy.removedFromParent = new HashSet<>(this.removedFromParent); } + return copy; + } - if (this.size != this.computeSize()) { - throw new IllegalStateException("incorrect size"); + /** Copies the entries of the parentless store {@code that}, sharing Entry objects. */ + void putAllLocal(HashStore that) { + if (that.count == 0) { + return; } - // Local-structure invariant: the size counter's emptiness must match the local buckets. - if ((this.size == 0) != this.checkIfEmpty()) { - throw new IllegalStateException("incorrect empty status"); + if (this.count == 0) { + HashSupport.copyInto(this.buckets, that.buckets); + this.count = that.count; + } else { + this.count += HashSupport.merge(this.buckets, that.buckets); } } - int computeSize() { - Object[] thisBuckets = this.buckets; - - int size = 0; - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; - - if (curBucket instanceof Entry) { - size += 1; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - size += curGroup.sizeInChain(); - } - } - return size; + void fillMapLocal(Map map) { + HashSupport.fillMap(this.buckets, map); } - boolean checkIfEmpty() { - Object[] thisBuckets = this.buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; + void fillStringMapLocal(Map map) { + HashSupport.fillStringMap(this.buckets, map); + } - if (curBucket instanceof Entry) { - return false; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - if (!curGroup.isEmptyChain()) return false; - } + /** Tests only: throws if the store's structure is inconsistent. */ + void checkIntegrity() { + HashSupport.checkBuckets(this.buckets); + if (this.count != this.computeSize()) { + throw new IllegalStateException("incorrect size"); } + } - return true; + /** Tests only: the local count, recomputed from the structure. */ + int computeSize() { + return HashSupport.count(this.buckets); } - /** The bucket layout, for deep debugging. */ + /** For deep debugging. */ String toInternalString() { - Object[] thisBuckets = this.buckets; - - StringBuilder ledger = new StringBuilder(128); - for (int i = 0; i < thisBuckets.length; ++i) { - ledger.append('[').append(i).append("] = "); - - Object thisBucket = thisBuckets[i]; - if (thisBucket == null) { - ledger.append("null"); - } else if (thisBucket instanceof Entry) { - ledger.append('{').append(thisBucket).append('}'); - } else if (thisBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) thisBucket; - curGroup != null; - curGroup = curGroup.prev) { - ledger.append(curGroup).append(" -> "); - } - } - ledger.append('\n'); - } - return ledger.toString(); + return HashSupport.toInternalString(this.buckets); } } diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java new file mode 100644 index 00000000000..bce70cd924b --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -0,0 +1,403 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.BucketGroup; +import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * The raw bucket mechanics both tag stores share, over one fixed-size {@code Object[]} of buckets + * of shareable, immutable {@link Entry} objects. A bucket holds a lone Entry directly; because + * Entries are shared between maps, they cannot form a linked list, so colliding entries share a + * fixed-size {@link BucketGroup}, chained when one fills. Removals are assumed rare, so groups are + * never consolidated, but an emptied group is unlinked from its chain. + * + *

Stateless: each operation reports what it did through its return value -- put returns the + * entry it replaced, remove the entry it removed -- and the calling store keeps its own count. + * Copies share Entries and clone only BucketGroup chains. Iteration skips tombstones ({@link + * Entry#tombstone}), which only an id store holds. + */ +final class HashSupport { + private HashSupport() {} + + /** A fresh bucket array; its length must be a power of 2 for bucket masking. */ + static Object[] newBuckets() { + return new Object[1 << 4]; + } + + /** The entry for {@code (hash, tag)}, tombstone or not, or null. */ + static Entry find(Object[] buckets, int hash, String tag) { + return findInBucket(buckets[hash & (buckets.length - 1)], hash, tag); + } + + /** + * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). + */ + static Entry findInBucket(Object bucket, int hash, String tag) { + if (bucket instanceof Entry) { + Entry tagEntry = (Entry) bucket; + return tagEntry.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; + } else if (bucket instanceof BucketGroup) { + return ((BucketGroup) bucket).findInChain(hash, tag); + } + return null; + } + + /** Inserts or replaces {@code newEntry}, returning the entry it replaced, or null if it added. */ + static Entry put(Object[] buckets, Entry newEntry) { + Object[] thisBuckets = buckets; + + int newHash = newEntry.hash(); + int bucketIndex = newHash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + if (bucket == null) { + thisBuckets[bucketIndex] = newEntry; + + return null; + } else if (bucket instanceof Entry) { + Entry existingEntry = (Entry) bucket; + if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { + thisBuckets[bucketIndex] = newEntry; + + // replaced existing entry - no size change + return existingEntry; + } else { + thisBuckets[bucketIndex] = + new BucketGroup(existingEntry.hash(), existingEntry, newHash, newEntry); + + return null; + } + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + BucketGroup containingGroup = lastGroup.findContainingGroupInChain(newHash, newEntry.tag); + if (containingGroup != null) { + // replaced existing entry - no size change + return containingGroup._replace(newHash, newEntry); + } + + if (!lastGroup.insertInChain(newHash, newEntry)) { + thisBuckets[bucketIndex] = new BucketGroup(newHash, newEntry, lastGroup); + } + return null; + } + + // unreachable + return null; + } + + /** Removes the entry for {@code (hash, tag)}, returning it, or null if there was none. */ + static Entry remove(Object[] buckets, int hash, String tag) { + Object[] thisBuckets = buckets; + int bucketIndex = hash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + // null bucket case - do nothing + if (bucket instanceof Entry) { + Entry existingEntry = (Entry) bucket; + if (existingEntry.hash() == hash && existingEntry.matches(tag)) { + thisBuckets[bucketIndex] = null; + + return existingEntry; + } else { + return null; + } + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + BucketGroup containingGroup = lastGroup.findContainingGroupInChain(hash, tag); + if (containingGroup == null) { + return null; + } + + Entry existingEntry = containingGroup._remove(hash, tag); + if (containingGroup._isEmpty()) { + thisBuckets[bucketIndex] = lastGroup.removeGroupInChain(containingGroup); + } + + return existingEntry; + } + return null; + } + + /** + * Copies {@code thatBuckets} into the empty {@code thisBuckets}, sharing Entry objects and + * cloning only BucketGroup chains. + */ + static void copyInto(Object[] thisBuckets, Object[] thatBuckets) { + + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check + // elimination + for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { + Object thatBucket = thatBuckets[i]; + + // faster to explicitly null check first, then do instanceof + if (thatBucket == null) { + // do nothing + } else if (thatBucket instanceof BucketGroup) { + // if it is a BucketGroup, then need to clone + BucketGroup thatGroup = (BucketGroup) thatBucket; + + thisBuckets[i] = thatGroup.cloneChain(); + } else { // if ( thatBucket instanceof Entry ) + thisBuckets[i] = thatBucket; + } + } + } + + /** + * Merges {@code thatBuckets} into {@code thisBuckets}, sharing Entry objects and cloning only + * BucketGroup chains; incoming entries replace existing ones. Returns how many it added. Both + * arrays must be the same length (stores don't expand, so buckets are perfectly aligned). + */ + static int merge(Object[] thisBuckets, Object[] thatBuckets) { + int added = 0; + + // Since TagMap-s don't support expansion, buckets are perfectly aligned + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check + // elimination + for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { + Object thatBucket = thatBuckets[i]; + + // if nothing incoming, nothing to do + if (thatBucket == null) continue; + + Object thisBucket = thisBuckets[i]; + if (thisBucket == null) { + // This bucket is null, easy case + // Either copy over the sole entry or clone the BucketGroup chain + + if (thatBucket instanceof Entry) { + thisBuckets[i] = thatBucket; + added += 1; + } else if (thatBucket instanceof BucketGroup) { + BucketGroup thatGroup = (BucketGroup) thatBucket; + + BucketGroup thisNewGroup = thatGroup.cloneChain(); + thisBuckets[i] = thisNewGroup; + added += thisNewGroup.sizeInChain(); + } + } else if (thisBucket instanceof Entry) { + // This bucket is a single entry, medium complexity case + // If other side is an Entry - just merge the entries into a bucket + // If other side is a BucketGroup - then clone the group and insert the entry normally into + // the cloned group + + Entry thisEntry = (Entry) thisBucket; + int thisHash = thisEntry.hash(); + + if (thatBucket instanceof Entry) { + Entry thatEntry = (Entry) thatBucket; + int thatHash = thatEntry.hash(); + + if (thisHash == thatHash && thisEntry.matches(thatEntry.tag())) { + thisBuckets[i] = thatEntry; + // replacing entry, no size change + } else { + thisBuckets[i] = + new BucketGroup( + thisHash, thisEntry, + thatHash, thatEntry); + added += 1; + } + } else if (thatBucket instanceof BucketGroup) { + BucketGroup thatGroup = (BucketGroup) thatBucket; + + // Clone the other group, then place this entry into that group + BucketGroup thisNewGroup = thatGroup.cloneChain(); + int thisNewGroupSize = thisNewGroup.sizeInChain(); + + Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); + if (incomingEntry != null) { + // there's already an entry w/ the same tag from the incoming TagMap + // incoming entry clobbers the existing try, so we're done + thisBuckets[i] = thisNewGroup; + + // overlapping group - subtract one for clobbered existing entry + added += thisNewGroupSize - 1; + } else if (thisNewGroup.insertInChain(thisHash, thisEntry)) { + // able to add thisEntry into the existing groups + thisBuckets[i] = thisNewGroup; + + // non overlapping group - existing entry already counted + added += thisNewGroupSize; + } else { + // unable to add into the existing groups + thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup); + + // non overlapping group - existing entry already counted + added += thisNewGroupSize; + } + } + } else if (thisBucket instanceof BucketGroup) { + // This bucket is a BucketGroup, medium to hard case + // If the other side is an entry, just normal insertion procedure - no cloning required + BucketGroup thisGroup = (BucketGroup) thisBucket; + + if (thatBucket instanceof Entry) { + Entry thatEntry = (Entry) thatBucket; + int thatHash = thatEntry.hash(); + + if (thisGroup.replaceInChain(thatHash, thatEntry) != null) { + // replaced existing entry no size change + } else if (thisGroup.insertInChain(thatHash, thatEntry)) { + added += 1; + } else { + thisBuckets[i] = new BucketGroup(thatHash, thatEntry, thisGroup); + added += 1; + } + } else if (thatBucket instanceof BucketGroup) { + // Most complicated case - need to walk that bucket group chain and update this chain + BucketGroup thatGroup = (BucketGroup) thatBucket; + + // Taking the easy / expensive way out for updating size + int thisPrevGroupSize = thisGroup.sizeInChain(); + + BucketGroup thisNewGroup = thisGroup.replaceOrInsertAllInChain(thatGroup); + int thisNewGroupSize = thisNewGroup.sizeInChain(); + + thisBuckets[i] = thisNewGroup; + added += (thisNewGroupSize - thisPrevGroupSize); + } + } + } + return added; + } + + /** Visits every entry, skipping tombstones. */ + static void forEach(Object[] buckets, Consumer consumer) { + for (int i = 0; i < buckets.length; ++i) { + Object bucket = buckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone) consumer.accept(entry); + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone) consumer.accept(entry); + } + } + } + } + } + + static void forEach( + Object[] buckets, T thisObj, BiConsumer consumer) { + for (int i = 0; i < buckets.length; ++i) { + Object bucket = buckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone) consumer.accept(thisObj, entry); + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone) consumer.accept(thisObj, entry); + } + } + } + } + } + + static void forEach( + Object[] buckets, T thisObj, U otherObj, TriConsumer consumer) { + for (int i = 0; i < buckets.length; ++i) { + Object bucket = buckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone) consumer.accept(thisObj, otherObj, entry); + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone) consumer.accept(thisObj, otherObj, entry); + } + } + } + } + } + + static void fillMap(Object[] buckets, Map map) { + forEach(buckets, map, (m, entry) -> m.put(entry.tag(), entry.objectValue())); + } + + static void fillStringMap(Object[] buckets, Map map) { + forEach(buckets, map, (m, entry) -> m.put(entry.tag(), entry.stringValue())); + } + + /** The number of live entries, not counting tombstones. Tests only. */ + static int count(Object[] buckets) { + int[] count = new int[1]; + forEach(buckets, count, (c, entry) -> c[0]++); + return count[0]; + } + + /** Throws if an entry sits in the wrong bucket. Tests only. */ + static void checkBuckets(Object[] buckets) { + // Decided to use if ( cond ) throw new IllegalStateException rather than assert + // That was done to avoid the extra static initialization needed for an assertion + // While that's probably an unnecessary optimization, this method is only called in tests + + Object[] thisBuckets = buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + int thisHash = thisEntry.hash(); + + int expectedBucket = thisHash & (thisBuckets.length - 1); + if (expectedBucket != i) { + throw new IllegalStateException("incorrect bucket"); + } + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry thisEntry = curGroup._entryAt(i); + if (thisEntry == null) continue; + + int thisHash = thisEntry.hash(); + assert curGroup._hashAt(i) == thisHash; + + int expectedBucket = thisHash & (thisBuckets.length - 1); + if (expectedBucket != i) { + throw new IllegalStateException("incorrect bucket"); + } + } + } + } + } + } + + static String toInternalString(Object[] buckets) { + Object[] thisBuckets = buckets; + + StringBuilder ledger = new StringBuilder(128); + for (int i = 0; i < thisBuckets.length; ++i) { + ledger.append('[').append(i).append("] = "); + + Object thisBucket = thisBuckets[i]; + if (thisBucket == null) { + ledger.append("null"); + } else if (thisBucket instanceof Entry) { + ledger.append('{').append(thisBucket).append('}'); + } else if (thisBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) thisBucket; + curGroup != null; + curGroup = curGroup.prev) { + ledger.append(curGroup).append(" -> "); + } + } + ledger.append('\n'); + } + return ledger.toString(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index ad65da8380c..eb12e4c3af9 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -1,58 +1,106 @@ package datadog.trace.api; -import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; /** * Known tags keyed by their {@link KnownTags} id. Every method takes an id, or an Entry already * built from one -- never a name, so nothing here can be handed a name that a caller has not yet * resolved. * - *

Each implementation is its own class: today the adapter {@link HashStore#idStore()} returns - * over a hash store's known tags; a dense store later. In hash mode ({@link TagRouting#HASH_MODE}) - * callers reach the hash store directly instead. No locking: guarded by the owner's lock. + *

Each implementation is its own class: {@link HashIdStore} today, a dense store later. Nothing + * that is always loaded may extend this class, so that a call through it stays monomorphic. No + * locking: guarded by the owner's lock. + * + *

The base keeps the slot masks an implementation shares: a tag's slot ({@link + * KnownTagCodec#slotOf}) maps it to one bit of a {@code long}. A bit is a may-be-present hint -- + * tags that don't usually co-occur share a slot -- so a clear bit is a definite miss and a set bit + * means "probe". The masks are never cleared by a removal, only widened. */ -@Restricted(allowedIn = {"datadog.trace.core"}) -public abstract class IdStore { +abstract class IdStore { + /** Slots that may hold a live local entry. */ + long occupancy; + + /** Slots that may hold a local tombstone, hiding an inherited entry. */ + long tombstoneMask; + + /** + * Slots an ancestor may hold, fixed at construction: ancestors are frozen. A clear bit lets a + * local miss skip read-through. + */ + final long inheritedMask; + + IdStore(IdStore parent) { + this.inheritedMask = parent == null ? 0L : parent.occupancy | parent.inheritedMask; + } + + /** {@code tagId}'s slot bit. */ + static long slotBit(long tagId) { + return 1L << KnownTagCodec.slotOf(tagId); + } + + /** Whether the slot {@code bit} may hold a local entry or tombstone. */ + final boolean mayHaveLocal(long bit) { + return ((occupancy | tombstoneMask) & bit) != 0L; + } + + /** Whether an ancestor may hold a tag in the slot {@code bit}. */ + final boolean mayInherit(long bit) { + return (inheritedMask & bit) != 0L; + } + + /** Throws for an id that names no known tag. */ + static IllegalArgumentException notKnown(long tagId) { + return new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); + } + + /* + * Point operations. + */ + /** * The entry for the known tag {@code tagId}, reading through to the parent chain, or null. * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - public abstract Entry get(long tagId); + abstract Entry get(long tagId); /** * {@code get(tagId)}'s value, boxed if necessary, or null -- for a caller that needs only the * value, so a store that keeps known tags without an Entry per tag can answer without one. */ - public abstract Object getKnownValue(long tagId); + abstract Object getKnownValue(long tagId); /* * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. */ - public abstract void set(long tagId, Object value); + abstract void set(long tagId, Object value); - public abstract void set(long tagId, CharSequence value); + abstract void set(long tagId, CharSequence value); - public abstract void set(long tagId, boolean value); + abstract void set(long tagId, boolean value); - public abstract void set(long tagId, int value); + abstract void set(long tagId, int value); - public abstract void set(long tagId, long value); + abstract void set(long tagId, long value); - public abstract void set(long tagId, float value); + abstract void set(long tagId, float value); - public abstract void set(long tagId, double value); + abstract void set(long tagId, double value); /** * Inserts or replaces a known tag's entry, returning the local entry it replaced (or null). Does * not consult the read-through parent. */ - public abstract Entry put(Entry knownEntry); + abstract Entry put(Entry knownEntry); /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - public abstract Entry getAndPut(Entry knownEntry); + abstract Entry getAndPut(Entry knownEntry); /** * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is @@ -60,5 +108,51 @@ public abstract class IdStore { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - public abstract Entry remove(long tagId); + abstract Entry remove(long tagId); + + /* + * Whole-store operations, for the owning TagMap. + */ + + /** Exact count, including the visible part of the read-through chain. */ + abstract int size(); + + abstract boolean isEmpty(); + + /** Cheap: no level of the chain holds a local entry (ignores shadowing and tombstones). */ + abstract boolean isDefinitelyEmpty(); + + /** Upper bound: the local counts of every level. */ + abstract int estimateSize(); + + /** Whether this store reads through to a parent. */ + abstract boolean hasParent(); + + /** Visits the local entries, then the visible inherited ones. */ + abstract void forEach(Consumer consumer); + + abstract void forEach(T thisObj, BiConsumer consumer); + + abstract void forEach( + T thisObj, U otherObj, TriConsumer consumer); + + /** An independently mutable copy with the same parent, sharing Entry objects. */ + abstract IdStore copy(); + + /** Copies the entries of the parentless store {@code that}, sharing Entry objects. */ + abstract void putAllLocal(IdStore that); + + /** Puts the local entries into {@code map}, for a store with no parent. */ + abstract void fillMapLocal(Map map); + + abstract void fillStringMapLocal(Map map); + + /** Tests only: throws if the store's structure is inconsistent. */ + abstract void checkIntegrity(); + + /** Tests only: the local count, recomputed from the structure. */ + abstract int computeSize(); + + /** For deep debugging. */ + abstract String toInternalString(); } 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 6a46bb27238..cc54e3bbdab 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -108,7 +108,7 @@ public static final TagMap createFromParent(TagMap parent) { parent = null; } } - return new TagMap(parent); + return new TagMap(parent, null); } /** Creates a new TagMap.Ledger */ @@ -469,6 +469,40 @@ static Entry newDoubleEntry(String tag, Double box) { volatile String strCache = null; + /** + * True for a known tag's tombstone: the marker an id store puts in its own buckets to hide an + * inherited entry it has removed. Never visible outside the store; see {@link #tombstoneOf}. + */ + final boolean tombstone; + + /** The shared tombstone per known tag, by serial, created on first use. */ + private static final Entry[] TOMBSTONES = new Entry[KnownTagCodec.serialLimit()]; + + /** + * The tombstone for the known tag {@code tagId}: one shared, immutable entry per tag, so + * tombstoning a known tag allocates nothing after the first time. Racing first uses may each + * build one; either serves, since a tombstone is told apart by {@link #tombstone}, not + * identity. + */ + static Entry tombstoneOf(long tagId) { + int serial = KnownTagCodec.serialNum(tagId); + Entry tombstone = TOMBSTONES[serial]; + if (tombstone == null || tombstone.tagHash != tagId) { + tombstone = new Entry(tagId); + TOMBSTONES[serial] = tombstone; + } + return tombstone; + } + + private Entry(long tagId) { + super(requireKnownName(tagId)); + this.tagHash = tagId; + this.tombstone = true; + this.rawType = OBJECT; + this.rawPrim = 0L; + this.rawObj = 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 @@ -483,6 +517,7 @@ private Entry(String tag, 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.tombstone = false; this.rawType = type; this.rawPrim = prim; @@ -493,6 +528,7 @@ private Entry(String tag, 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); + this.tombstone = false; this.rawType = type; this.rawPrim = prim; @@ -1195,26 +1231,30 @@ public EntryChange next() { } /* - * A TagMap is a view over a pair of stores: an IdStore holding the known tags by id, and a name - * store holding the custom tags by name. Their key spaces are disjoint, so each store reads - * through to its own parent chain and keeps its own tombstones, and precedence never crosses - * stores. + * A TagMap holds its tags in two stores, each with its own bucket array: an IdStore with the + * known tags by id, and a HashStore with the custom tags by name. Their key spaces are disjoint, + * so each store reads through to its own parent chain and keeps its own tombstones, and + * precedence never crosses stores. * - * In hash mode -- the only mode so far -- one HashStore serves as both (ids == names), so a map - * is the view plus one store, and no tag costs more than before. A whole-map operation (size, - * iteration, copy, putAll, clear) runs on that one store and so covers both halves; a separate - * IdStore implementation (a dense store) would have to visit ids as well, and would be chosen, - * by a global mode, in the constructor that pairs the stores. + * The custom store is created on the first custom write. Until then, a map with a parent borrows + * the parent's custom store, read-only (namesBorrowed): the parent is frozen, so its store is + * stable, and with no local custom state the borrowed store reads exactly as an own one would. */ - /** The known tags, by id; null in hash mode, where {@link #names} holds them. */ - final IdStore ids; + /** The known tags, by id. Not final only so {@link #clear()} can replace it. */ + IdStore ids; + + /** + * The custom tags, by name: null until the first custom write, or the parent's custom store, + * borrowed read-only while {@link #namesBorrowed}. + */ + HashStore names; - /** The custom tags, by name; in hash mode it holds the known tags too. */ - final HashStore names; + /** True while {@link #names} is the parent's custom store, borrowed until the first write. */ + boolean namesBorrowed; /** - * For a span's view ({@link #viewOf}), the span, which says what direction-scoped names read as; + * For a span's tags ({@link #forSpan}), the span, which says what direction-scoped names read as; * null for a TagMap of its own, which reads every name as written. */ final DirectionSource direction; @@ -1223,15 +1263,15 @@ public EntryChange next() { /** * Optional frozen parent for read-through. Reads that miss locally fall through to the parent - * chain (each store reads through to the parent's matching store); see {@link HashStore#parent}. - * Must be frozen when attached, so it is safely shareable. Not final only so {@link #clear()} can - * detach it (to null); it is otherwise fixed at construction and never re-pointed. - * Package-visible so same-package tests can assert attach/detach directly. + * chain (each store reads through to the parent's matching store). Must be frozen when attached, + * so it is safely shareable. Not final only so {@link #clear()} can detach it (to null); it is + * otherwise fixed at construction and never re-pointed. Package-visible so same-package tests can + * assert attach/detach directly. */ @VisibleForTesting TagMap parent; public TagMap() { - this((TagMap) null); + this(null, null); } /** @@ -1239,28 +1279,33 @@ public TagMap() { * construction and never re-pointed (only detached to null by {@link #clear()}), so read-through * optimizations can treat it as fixed. */ - private TagMap(TagMap parent) { - this(parent, new HashStore(parent == null ? null : parent.names)); - } - - /** - * A mutable map over {@code store}, which reads through to {@code parent}'s store. Where the - * store mode is chosen: hash mode uses the one store for both halves. - */ - private TagMap(TagMap parent, HashStore store) { - this.ids = TagRouting.idStoreFor(store); - this.names = store; - this.direction = null; + private TagMap(TagMap parent, DirectionSource direction) { + this.ids = new HashIdStore(parent == null ? null : parent.ids); + HashStore parentNames = parent == null ? null : parent.names; + this.names = parentNames; + this.namesBorrowed = parentNames != null; + this.direction = direction; this.frozen = false; this.parent = parent; } - /** A live view over stores the caller owns; see {@link #viewOf}. */ - private TagMap(IdStore ids, HashStore names, DirectionSource direction) { + /** A copy over {@code ids} and {@code names}; see {@link #copy()}. */ + private TagMap(TagMap parent, IdStore ids, HashStore names, boolean namesBorrowed) { this.ids = ids; this.names = names; - this.direction = direction; + this.namesBorrowed = namesBorrowed; + this.direction = null; this.frozen = false; + this.parent = parent; + } + + /** Used for inexpensive immutable */ + private TagMap(Object[] buckets, int size) { + this.ids = new HashIdStore(buckets, size); + this.names = null; + this.namesBorrowed = false; + this.direction = null; + this.frozen = true; this.parent = null; } @@ -1276,76 +1321,83 @@ public interface DirectionSource { } /** - * A live, mutable TagMap over a span's stores, for the span's internal readers (tag - * post-processors, serializers). It owns nothing: reads and writes go straight to the stores, and - * name-keyed reads honor the span's direction. Not thread-safe; use it under the lock that guards - * the stores, and only as long as the caller holds it. Small, so a local view is scalar-replaced. - */ - @Restricted(allowedIn = {"datadog.trace.core"}) - public static TagMap viewOf(IdStore ids, HashStore names, DirectionSource direction) { - return new TagMap(ids, names, direction); - } - - /** - * The store a span's HashStore reads through to for the tags in {@code parent}, or null when - * there is nothing to read through to; the stores-level {@link #createFromParent}. + * A span's tags: a mutable TagMap reading through to {@code parent}, as {@link #createFromParent} + * builds, whose name-keyed reads honor the span's direction. The span routes to it with {@link + * TagRouting} under its own lock, and hands it to its internal readers (tag post-processors, + * serializers); it is not thread-safe. * * @throws IllegalStateException if {@code parent} is not frozen */ @Restricted(allowedIn = {"datadog.trace.core"}) - public static HashStore readThroughStore(TagMap parent) { - if (parent == null) { - return null; - } - if (!parent.frozen) { - throw new IllegalStateException("read-through parent must be frozen"); + public static TagMap forSpan(TagMap parent, DirectionSource direction) { + if (parent != null) { + if (!parent.frozen) { + throw new IllegalStateException("read-through parent must be frozen"); + } + if (parent.isEmpty()) { + parent = null; + } } - // An empty parent is dropped, as createFromParent does. - return parent.isEmpty() ? null : parent.names; + return new TagMap(parent, direction); } - /** This map's HashStore. */ - @Restricted(allowedIn = {"datadog.trace.core"}) - public HashStore hashStore() { - return this.names; + /** The custom store to write to: created on the first write, ending any borrowing. */ + HashStore namesForWrite() { + HashStore names = this.names; + if (names == null || this.namesBorrowed) { + // A borrowed store is the nearest ancestor's custom store: read through to it. + names = new HashStore(names); + this.names = names; + this.namesBorrowed = false; + } + return names; } - /** This map's IdStore, null in hash mode; see {@link TagRouting#idStoreFor}. */ - @Restricted(allowedIn = {"datadog.trace.core"}) - public IdStore idStore() { - return this.ids; + /** Removes the custom tag {@code tag}, returning the prior visible entry. */ + Entry removeCustom(String tag) { + HashStore names = this.names; + if (names == null) { + return null; + } + if (this.namesBorrowed) { + if (names.get(tag) == null) { + return null; // nothing to hide: keep borrowing + } + names = this.namesForWrite(); // hiding an inherited tag needs a tombstone of our own + } + return names.remove(tag); } - /** Used for inexpensive immutable */ - private TagMap(Object[] buckets, int size) { - HashStore store = new HashStore(buckets, size); - this.ids = TagRouting.idStoreFor(store); - this.names = store; - this.direction = null; - this.frozen = true; - this.parent = null; + /** Whether either store reads through to a parent. */ + private boolean readsThrough() { + HashStore names = this.names; + return this.ids.hasParent() || this.namesBorrowed || (names != null && names.parent != null); } @Override public int size() { // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. - return this.names.size(); + HashStore names = this.names; + return this.ids.size() + (names == null ? 0 : names.size()); } @Override public boolean isEmpty() { // Exact (Map contract). Under read-through resolves the parent; prefer isDefinitelyEmpty(). - return this.names.isEmpty(); + HashStore names = this.names; + return this.ids.isEmpty() && (names == null || names.isEmpty()); } public boolean isDefinitelyEmpty() { // Cheap: empty iff no level in the chain holds a local entry (ignores shadowing/tombstones). - return this.names.isDefinitelyEmpty(); + HashStore names = this.names; + return this.ids.isDefinitelyEmpty() && (names == null || names.isDefinitelyEmpty()); } public int estimateSize() { // Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. - return this.names.estimateSize(); + HashStore names = this.names; + return this.ids.estimateSize() + (names == null ? 0 : names.estimateSize()); } @Deprecated @@ -1458,15 +1510,15 @@ public Entry getEntry(String tag) { long key = TagRouting.keyOf(tag); DirectionSource direction = this.direction; if (direction == null || key == 0) { - return TagRouting.get(this.ids, this.names, key, tag); + return TagRouting.get(this, key, tag); } return TagRouting.getEntry( - this.ids, this.names, key, tag, direction.tagDirection(), direction.hasPendingAliases()); + this, key, tag, direction.tagDirection(), direction.hasPendingAliases()); } /** The entry for a known tag id. */ public Entry getEntry(long tagId) { - return TagRouting.getById(this.ids, this.names, tagId); + return this.ids.get(tagId); } @Deprecated @@ -1666,14 +1718,14 @@ public void setResolved(long tagId, @Nonnull String tag, double value) { /** The read counterpart of {@link #setResolved(long, String, Object)}. */ public Entry getEntryResolved(long tagId, String tag) { - return TagRouting.get(this.ids, this.names, tagId, tag); + return TagRouting.get(this, tagId, tag); } /** The removal counterpart of {@link #setResolved(long, String, Object)}. */ public Entry removeResolved(long tagId, String tag) { this.checkWriteAccess(); - return TagRouting.remove(this.ids, this.names, tagId, tag); + return TagRouting.remove(this, tagId, tag); } /* @@ -1684,7 +1736,7 @@ public Entry removeResolved(long tagId, String tag) { /** {@code getEntry(tagId)}'s value, or null, for a caller that needs only the value. */ Object getKnownValue(long tagId) { - return TagRouting.getKnownValue(this.ids, this.names, tagId); + return this.ids.getKnownValue(tagId); } public void set(long tagId, @Nonnull Object value) { @@ -1727,7 +1779,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { } this.checkWriteAccess(); - return TagRouting.getAndPut(this.ids, this.names, newEntry); + return TagRouting.getAndPut(this, newEntry); } /** @@ -1739,21 +1791,21 @@ public Entry getAndSet(@Nullable Entry newEntry) { Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); - return TagRouting.put(this.ids, this.names, newEntry); + return TagRouting.put(this, newEntry); } /** {@link #putEntry} for an entry known to be a known tag's. */ void putKnown(Entry knownEntry) { this.checkWriteAccess(); - TagRouting.put(this.ids, this.names, knownEntry); + this.ids.put(knownEntry); } /** {@link #putEntry} for an entry known to be a custom tag's. */ void putCustom(Entry customEntry) { this.checkWriteAccess(); - this.names.put(customEntry); + this.namesForWrite().put(customEntry); } public Entry getAndSet(@Nonnull String tag, Object value) { @@ -1815,35 +1867,45 @@ public void putAll(TagMap that) { } private void putAllOptimizedMap(TagMap that) { - if (that.names.parent != null) { + if (that.readsThrough()) { // read-through source: the bucket-copy paths only see that's local entries, so they would // drop entries visible only through that's ancestor chain (and ignore its tombstones). // Union-copy the full visible set instead -- still shares the source Entry objects. that.forEach(this, (self, entry) -> self.set(entry)); return; } - this.names.putAllLocal(that.names); + this.ids.putAllLocal(that.ids); + HashStore thatNames = that.names; + if (thatNames != null && thatNames.count != 0) { + this.namesForWrite().putAllLocal(thatNames); + } } public void fillMap(Map map) { - if (this.names.parent != null) { + if (this.readsThrough()) { // read-through source: the bucket-only loop below sees just local entries, so it would drop // tags visible only through the parent chain (and ignore shadowing/tombstones). Walk the full // visible union instead -- mirrors putAllOptimizedMap. this.forEach(map, (m, entry) -> m.put(entry.tag(), entry.objectValue())); return; } - this.names.fillMapLocal(map); + this.ids.fillMapLocal(map); + if (this.names != null) { + this.names.fillMapLocal(map); + } } public void fillStringMap(Map stringMap) { - if (this.names.parent != null) { + if (this.readsThrough()) { // read-through source: walk the visible union so parent-only tags aren't dropped -- see // fillMap. this.forEach(stringMap, (m, entry) -> m.put(entry.tag(), entry.stringValue())); return; } - this.names.fillStringMapLocal(stringMap); + this.ids.fillStringMapLocal(stringMap); + if (this.names != null) { + this.names.fillStringMapLocal(stringMap); + } } @Override @@ -1863,21 +1925,24 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. - return TagRouting.remove(this.ids, this.names, TagRouting.keyOf(tag), tag); + return TagRouting.remove(this, TagRouting.keyOf(tag), tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ public Entry getAndRemove(long tagId) { this.checkWriteAccess(); - return TagRouting.removeById(this.ids, this.names, tagId); + return this.ids.remove(tagId); } public TagMap copy() { // The same (frozen, shared) parent, fixed at construction, over a copy of this map's own // entries and tombstones. The copy stays independently mutable (writes land on its local // store, never the shared parent). - return new TagMap(this.parent, this.names.copy()); + HashStore names = this.names; + boolean borrowed = this.namesBorrowed; + return new TagMap( + this.parent, this.ids.copy(), names == null || borrowed ? names : names.copy(), borrowed); } public TagMap immutableCopy() { @@ -1899,24 +1964,39 @@ public Stream stream() { @Override public void forEach(Consumer consumer) { - this.names.forEach(consumer); + this.ids.forEach(consumer); + HashStore names = this.names; + if (names != null) { + names.forEach(consumer); + } } public void forEach(T thisObj, BiConsumer consumer) { - this.names.forEach(thisObj, consumer); + this.ids.forEach(thisObj, consumer); + HashStore names = this.names; + if (names != null) { + names.forEach(thisObj, consumer); + } } public void forEach( T thisObj, U otherObj, TriConsumer consumer) { - this.names.forEach(thisObj, otherObj, consumer); + this.ids.forEach(thisObj, otherObj, consumer); + HashStore names = this.names; + if (names != null) { + names.forEach(thisObj, otherObj, consumer); + } } public void clear() { this.checkWriteAccess(); - // clear() removes ALL mappings, including any inherited through read-through: the store - // detaches its parent, and so does the view. - this.names.clear(); + // clear() removes ALL mappings, including any inherited through read-through. Fresh parentless + // stores (rather than tombstoning every inherited key) are simpler and cheaper, and leave an + // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. + this.ids = new HashIdStore(null); + this.names = null; + this.namesBorrowed = false; this.parent = null; } @@ -1935,15 +2015,20 @@ public void checkWriteAccess() { } void checkIntegrity() { - this.names.checkIntegrity(); + this.ids.checkIntegrity(); + if (this.names != null && !this.namesBorrowed) { + this.names.checkIntegrity(); + } } + /** The local entries, recomputed from both stores' structure. */ int computeSize() { - return this.names.computeSize(); + HashStore names = this.names; + return this.ids.computeSize() + (names == null || this.namesBorrowed ? 0 : names.computeSize()); } boolean checkIfEmpty() { - return this.names.checkIfEmpty(); + return this.computeSize() == 0; } @Override @@ -2001,15 +2086,21 @@ String toPrettyString() { * debugging */ String toInternalString() { - return this.names.toInternalString(); + HashStore names = this.names; + return "ids:\n" + + this.ids.toInternalString() + + "names:\n" + + (names == null ? "none\n" : names.toInternalString()); } abstract static class IteratorBase { - private final HashStore store; - - // the level whose buckets are currently being walked: the leaf (store) first, then each - // ancestor in turn (read-through union). store == level means we're on the leaf's own entries. - private HashStore level; + // Two phases: the id store's chain, then the custom store's. In each, the level whose buckets + // are being walked is the leaf first, then each ancestor in turn (read-through union). + // (A dense id store will need its own walk.) + private final HashIdStore idLeaf; + private final HashStore nameLeaf; + private HashIdStore idLevel; + private HashStore nameLevel; // non-null once in the custom phase private Object[] buckets; private Entry nextEntry; @@ -2020,9 +2111,10 @@ abstract static class IteratorBase { private int groupIndex = 0; IteratorBase(TagMap map) { - this.store = map.names; - this.level = this.store; - this.buckets = this.store.buckets; + this.idLeaf = (HashIdStore) map.ids; + this.nameLeaf = map.names; + this.idLevel = this.idLeaf; + this.buckets = this.idLeaf.buckets; } public final boolean hasNext() { @@ -2062,23 +2154,55 @@ private final Entry advance() { if (tagEntry != null) { // leaf entries emit as-is; ancestor entries only if visible from the leaf -- not shadowed // by a nearer level and not tombstoned. (parentEntryVisible walks the nearer levels.) - if (this.level == this.store || this.store.parentEntryVisible(tagEntry, this.level)) { + if (this.isVisible(tagEntry)) { return tagEntry; } - continue; // ancestor entry shadowed/tombstoned -> skip + continue; // tombstone, or ancestor entry shadowed/tombstoned -> skip } - // current level exhausted; advance to the next ancestor's buckets (read-through union) - if (this.level.parent != null) { - this.level = this.level.parent; - this.buckets = this.level.buckets; - this.bucketIndex = -1; - this.group = null; - this.groupIndex = 0; - continue; + // current level exhausted; advance to the next ancestor's buckets (read-through union), + // then from the id chain to the custom chain + Object[] next = this.nextLevel(); + if (next == null) { + return null; + } + this.buckets = next; + this.bucketIndex = -1; + this.group = null; + this.groupIndex = 0; + } + } + + private boolean isVisible(Entry entry) { + HashStore nameLevel = this.nameLevel; + if (nameLevel == null) { + return !entry.tombstone + && (this.idLevel == this.idLeaf || this.idLeaf.parentEntryVisible(entry, this.idLevel)); + } + return nameLevel == this.nameLeaf || this.nameLeaf.parentEntryVisible(entry, nameLevel); + } + + /** Moves to the next level's buckets, or returns null when both chains are done. */ + private Object[] nextLevel() { + HashStore nameLevel = this.nameLevel; + if (nameLevel == null) { + HashIdStore idParent = this.idLevel.parent; + if (idParent != null) { + this.idLevel = idParent; + return idParent.buckets; + } + if (this.nameLeaf == null) { + return null; } + this.nameLevel = this.nameLeaf; + return this.nameLeaf.buckets; + } + HashStore nameParent = nameLevel.parent; + if (nameParent == null) { return null; } + this.nameLevel = nameParent; + return nameParent.buckets; } /** Next raw entry in the current bucket array, ignoring shadowing/tombstones. */ @@ -2475,86 +2599,6 @@ Entry _remove(int hash, String tag) { return existingEntry; } - void forEachInChain(Consumer consumer) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._forEach(consumer); - } - } - - void _forEach(Consumer consumer) { - if (this.entry0 != null) consumer.accept(this.entry0); - if (this.entry1 != null) consumer.accept(this.entry1); - if (this.entry2 != null) consumer.accept(this.entry2); - if (this.entry3 != null) consumer.accept(this.entry3); - } - - void forEachInChain(T thisObj, BiConsumer consumer) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._forEach(thisObj, consumer); - } - } - - void _forEach(T thisObj, BiConsumer consumer) { - if (this.entry0 != null) consumer.accept(thisObj, this.entry0); - if (this.entry1 != null) consumer.accept(thisObj, this.entry1); - if (this.entry2 != null) consumer.accept(thisObj, this.entry2); - if (this.entry3 != null) consumer.accept(thisObj, this.entry3); - } - - void forEachInChain( - T thisObj, U otherObj, TriConsumer consumer) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._forEach(thisObj, otherObj, consumer); - } - } - - void _forEach(T thisObj, U otherObj, TriConsumer consumer) { - if (this.entry0 != null) consumer.accept(thisObj, otherObj, this.entry0); - if (this.entry1 != null) consumer.accept(thisObj, otherObj, this.entry1); - if (this.entry2 != null) consumer.accept(thisObj, otherObj, this.entry2); - if (this.entry3 != null) consumer.accept(thisObj, otherObj, this.entry3); - } - - void fillMapFromChain(Map map) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._fillMap(map); - } - } - - void _fillMap(Map map) { - Entry entry0 = this.entry0; - if (entry0 != null) map.put(entry0.tag, entry0.objectValue()); - - Entry entry1 = this.entry1; - if (entry1 != null) map.put(entry1.tag, entry1.objectValue()); - - Entry entry2 = this.entry2; - if (entry2 != null) map.put(entry2.tag, entry2.objectValue()); - - Entry entry3 = this.entry3; - if (entry3 != null) map.put(entry3.tag, entry3.objectValue()); - } - - void fillStringMapFromChain(Map map) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._fillStringMap(map); - } - } - - void _fillStringMap(Map map) { - Entry entry0 = this.entry0; - if (entry0 != null) map.put(entry0.tag, entry0.stringValue()); - - Entry entry1 = this.entry1; - if (entry1 != null) map.put(entry1.tag, entry1.stringValue()); - - Entry entry2 = this.entry2; - if (entry2 != null) map.put(entry2.tag, entry2.stringValue()); - - Entry entry3 = this.entry3; - if (entry3 != null) map.put(entry3.tag, entry3.stringValue()); - } - BucketGroup cloneChain() { BucketGroup thisClone = this._cloneEntries(); diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index b0ff5a74578..76c83b306d0 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -6,37 +6,24 @@ import datadog.trace.api.TagMap.Entry; /** - * Where a tag name is resolved and an operation is routed to the store that holds it: a known tag - * to the {@link IdStore} by its id, a custom tag to the {@link HashStore} by its name. The one - * place a name becomes an id, so the stores themselves never see a name that might be a known - * tag's. Shared by {@link TagMap} and by the tracer core's spans, which own their stores directly. + * Where a tag name is resolved and an operation is routed to the store of a {@link TagMap} that + * holds it: a known tag to its {@link IdStore} by id, a custom tag to its {@link HashStore} by name + * (created on the first custom write). The one place a name becomes an id, so the stores themselves + * never see a name that might be a known tag's. Shared by TagMap and by the tracer core's spans, + * which hold their tags in a TagMap and route to it directly, skipping the checks TagMap's public + * methods make. * *

A name resolves once, by {@link #keyOf}, to a key: a known tag's id ({@code > 0}), 0 for a * custom tag, or a negative sentinel for a direction-scoped OpenTelemetry alias ({@code * server.address}), whose tag depends on the span's direction. An alias is the caller's to decide: * the key-taking methods treat any key that is not an id as custom, which is what a TagMap does - * with an alias, having no direction. {@link #getEntry(IdStore, HashStore, long, String, int, - * boolean)} reads an alias by a span's direction. - * - *

In {@link #HASH_MODE} known tags live in the HashStore too, and the id-keyed methods go to it - * directly: the flag is a constant, so the IdStore argument folds away and may be null. + * with an alias, having no direction. {@link #getEntry(TagMap, long, String, int, boolean)} reads + * an alias by a span's direction. */ @Restricted(allowedIn = {"datadog.trace.core"}) public final class TagRouting { - /** - * True while known tags are kept in the HashStore: id-keyed operations call it directly rather - * than through an {@link IdStore}, so an owner needs no IdStore object. False routes them through - * the IdStore -- the shape a dense store needs. - */ - public static final boolean HASH_MODE = true; - private TagRouting() {} - /** The IdStore an owner of {@code hash} pairs with it: none in hash mode. */ - public static IdStore idStoreFor(HashStore hash) { - return HASH_MODE ? null : hash.idStore(); - } - /** {@code tag}'s key: its id when known, 0 when custom, negative for a direction-scoped alias. */ public static long keyOf(String tag) { return KnownTagCodec.keyOrAliasOf(tag); @@ -52,78 +39,70 @@ public static boolean isKnown(long key) { */ /** The known tag {@code tagId}'s entry; throws for an id that names no known tag. */ - public static Entry getById(IdStore ids, HashStore hash, long tagId) { - return HASH_MODE ? hash.getById(tagId) : ids.get(tagId); + public static Entry getById(TagMap tags, long tagId) { + return tags.ids.get(tagId); } /** {@link #getById}'s value, or null. */ - public static Object getKnownValue(IdStore ids, HashStore hash, long tagId) { - return HASH_MODE ? hash.getKnownValue(tagId) : ids.getKnownValue(tagId); + public static Object getKnownValue(TagMap tags, long tagId) { + return tags.ids.getKnownValue(tagId); } /** Removes the known tag {@code tagId}; throws for an id that names no known tag. */ - public static Entry removeById(IdStore ids, HashStore hash, long tagId) { - return HASH_MODE ? hash.removeById(tagId) : ids.remove(tagId); + public static Entry removeById(TagMap tags, long tagId) { + return tags.ids.remove(tagId); } /* * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. */ - public static void setKnown(IdStore ids, HashStore hash, long tagId, Object value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, Object value) { + tags.ids.set(tagId, value); } - public static void setKnown(IdStore ids, HashStore hash, long tagId, CharSequence value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, CharSequence value) { + tags.ids.set(tagId, value); } - public static void setKnown(IdStore ids, HashStore hash, long tagId, boolean value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, boolean value) { + tags.ids.set(tagId, value); } - public static void setKnown(IdStore ids, HashStore hash, long tagId, int value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, int value) { + tags.ids.set(tagId, value); } - public static void setKnown(IdStore ids, HashStore hash, long tagId, long value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, long value) { + tags.ids.set(tagId, value); } - public static void setKnown(IdStore ids, HashStore hash, long tagId, float value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, float value) { + tags.ids.set(tagId, value); } - public static void setKnown(IdStore ids, HashStore hash, long tagId, double value) { - if (HASH_MODE) { - hash.setKnown(tagId, value); - } else { - ids.set(tagId, value); - } + public static void setKnown(TagMap tags, long tagId, double value) { + tags.ids.set(tagId, value); + } + + /* + * Custom tags, by name: never a known tag's name. + */ + + /** The custom tag {@code tag}'s entry, reading through to parents, or null. */ + public static Entry getCustom(TagMap tags, String tag) { + HashStore names = tags.names; + return names == null ? null : names.get(tag); + } + + /** Removes the custom tag {@code tag}, returning the prior visible entry. */ + public static Entry removeCustom(TagMap tags, String tag) { + return tags.removeCustom(tag); + } + + /** Sets the custom tag {@code tag}. */ + public static void setCustom(TagMap tags, String tag, Object value) { + tags.namesForWrite().set(tag, value); } /* @@ -131,13 +110,13 @@ public static void setKnown(IdStore ids, HashStore hash, long tagId, double valu */ /** Puts {@code entry} into the store for its kind, returning the local entry it replaced. */ - public static Entry put(IdStore ids, HashStore hash, Entry entry) { - return HASH_MODE || entry.tagId() == 0 ? hash.put(entry) : ids.put(entry); + public static Entry put(TagMap tags, Entry entry) { + return entry.tagId() != 0 ? tags.ids.put(entry) : tags.namesForWrite().put(entry); } /** {@link #put}, returning the prior visible entry. */ - public static Entry getAndPut(IdStore ids, HashStore hash, Entry entry) { - return HASH_MODE || entry.tagId() == 0 ? hash.getAndPut(entry) : ids.getAndPut(entry); + public static Entry getAndPut(TagMap tags, Entry entry) { + return entry.tagId() != 0 ? tags.ids.getAndPut(entry) : tags.namesForWrite().getAndPut(entry); } /* @@ -145,91 +124,76 @@ public static Entry getAndPut(IdStore ids, HashStore hash, Entry entry) { */ /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ - public static Entry get(IdStore ids, HashStore hash, long key, String tag) { - return isKnown(key) ? getById(ids, hash, key) : hash.get(tag); + public static Entry get(TagMap tags, long key, String tag) { + return isKnown(key) ? tags.ids.get(key) : getCustom(tags, tag); } /** Removes {@code tag}, already resolved to {@code key}, returning the prior visible entry. */ - public static Entry remove(IdStore ids, HashStore hash, long key, String tag) { - return isKnown(key) ? removeById(ids, hash, key) : hash.remove(tag); + public static Entry remove(TagMap tags, long key, String tag) { + return isKnown(key) ? tags.ids.remove(key) : tags.removeCustom(tag); } - public static void set(IdStore ids, HashStore hash, long key, String tag, Object value) { + public static void set(TagMap tags, long key, String tag, Object value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); + tags.ids.set(key, value); } else { - hash.set(tag, value); + tags.namesForWrite().set(tag, value); } } - public static void set(IdStore ids, HashStore hash, long key, String tag, CharSequence value) { + public static void set(TagMap tags, long key, String tag, CharSequence value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); + tags.ids.set(key, value); } else { - hash.set(tag, value); + tags.namesForWrite().set(tag, value); } } - public static void set(IdStore ids, HashStore hash, long key, String tag, boolean value) { + public static void set(TagMap tags, long key, String tag, boolean value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); + tags.ids.set(key, value); } else { - hash.set(tag, value); + tags.namesForWrite().set(tag, value); } } - public static void set(IdStore ids, HashStore hash, long key, String tag, int value) { + public static void set(TagMap tags, long key, String tag, int value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); + tags.ids.set(key, value); } else { - hash.set(tag, value); + tags.namesForWrite().set(tag, value); } } - public static void set(IdStore ids, HashStore hash, long key, String tag, long value) { + public static void set(TagMap tags, long key, String tag, long value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); + tags.ids.set(key, value); } else { - hash.set(tag, value); + tags.namesForWrite().set(tag, value); } } - public static void set(IdStore ids, HashStore hash, long key, String tag, float value) { + public static void set(TagMap tags, long key, String tag, float value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); + tags.ids.set(key, value); } else { - hash.set(tag, value); + tags.namesForWrite().set(tag, value); } } - public static void set(IdStore ids, HashStore hash, long key, String tag, double value) { + public static void set(TagMap tags, long key, String tag, double value) { if (isKnown(key)) { - setKnown(ids, hash, key, value); - } else { - hash.set(tag, value); - } - } - - /** - * Copies every entry visible in {@code source} into the stores, sharing the Entry objects; see - * {@link TagMap#putAll(TagMap)}. - */ - public static void putAll(IdStore ids, HashStore hash, TagMap source) { - if (!HASH_MODE) { - TagMap.viewOf(ids, hash, null).putAll(source); - } else if (source.names.parent != null) { - // read-through source: copy the full visible set, as TagMap.putAll does - source.forEach(hash, (target, entry) -> target.put(entry.entry())); + tags.ids.set(key, value); } else { - // No view here: putAll is too big to inline, so a view would escape and be allocated. - hash.putAllLocal(source.names); + tags.namesForWrite().set(tag, value); } } /* * By name, on a span of a direction. A direction-scoped alias reads the tag it denotes, and a tag - * reads a pending alias (one stored as written, before the span had a direction), which, being - * the newer write, wins. + * reads a pending alias stored as written in the custom store, which, being the newer write, + * wins. (A span keeps its pending aliases apart, and copies them into the custom store before + * handing its TagMap to a reader that uses this.) */ /** @@ -238,35 +202,30 @@ public static void putAll(IdStore ids, HashStore hash, TagMap source) { * single lookup like any other tag. */ public static Entry getEntry( - IdStore ids, - HashStore hash, - long key, - String name, - int direction, - boolean mayHavePendingAliases) { + TagMap tags, long key, String name, int direction, boolean mayHavePendingAliases) { if (isKnown(key)) { - return getById(ids, hash, key, direction, mayHavePendingAliases); + return getById(tags, key, direction, mayHavePendingAliases); } - Entry asWritten = key == 0 || mayHavePendingAliases ? hash.get(name) : null; + Entry asWritten = key == 0 || mayHavePendingAliases ? getCustom(tags, name) : null; if (key == 0 || asWritten != null) { return asWritten; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return isKnown(tagId) ? getById(ids, hash, tagId) : null; + return isKnown(tagId) ? tags.ids.get(tagId) : null; } /** The entry the known tag {@code tagId} reads on a span of {@code direction}; see above. */ public static Entry getById( - IdStore ids, HashStore hash, long tagId, int direction, boolean mayHavePendingAliases) { + TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { if (mayHavePendingAliases && KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - Entry pending = alias == null ? null : hash.get(alias); + Entry pending = alias == null ? null : getCustom(tags, alias); if (pending != null) { return pending; } } - return getById(ids, hash, tagId); + return tags.ids.get(tagId); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 3020a630502..8374e98bd6d 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -5,199 +5,291 @@ 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; /** - * The tag stores a span owns directly ({@link HashStore}, its {@link IdStore} adapter), the {@link - * TagRouting} over them, and the {@link TagMap} view a span hands its internal readers. + * A TagMap's two stores -- {@link HashIdStore} for known tags, {@link HashStore} for custom tags, + * created lazily -- the {@link TagRouting} over them, and the span's TagMap ({@link + * TagMap#forSpan}). */ class TagStoresTest { + private static TagMap frozenParent() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + parent.set("inherited.custom", "c"); + return parent.freeze(); + } + @Test - void trustedKnownSetsStoreWhatTheCheckedSetsStore() { - Object component = new StringBuilder("jdbc"); // not a String: exercises the Object overload - 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); - checked.set(KnownTags.COMPONENT_ID, component); - checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); - checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); + void aMapWithNoCustomTagsHasNoCustomStore() { + TagMap tags = TagMap.create(); + tags.set(KnownTags.HTTP_ROUTE_ID, "/a"); + tags.set(KnownTags.COMPONENT_NAME, "jdbc"); + assertNull(tags.names); + + tags.set("my.tag", "x"); + assertNotNull(tags.names); + assertEquals(3, tags.size()); + } + + @Test + void aChildBorrowsItsParentsCustomStoreUntilItWritesOne() { + TagMap parent = frozenParent(); + TagMap child = TagMap.createFromParent(parent); + assertSame(parent.names, child.names); + assertTrue(child.namesBorrowed); + assertEquals("c", child.getString("inherited.custom")); + + child.set(KnownTags.HTTP_METHOD_ID, "GET"); // a known write keeps borrowing + assertTrue(child.namesBorrowed); + assertFalse(child.remove("not.there")); // nothing to hide keeps borrowing + assertTrue(child.namesBorrowed); + + child.set("own.custom", "o"); + assertFalse(child.namesBorrowed); + assertNotSame(parent.names, child.names); + assertEquals("c", child.getString("inherited.custom")); // still reads through + assertEquals(5, child.size()); + assertEquals("c", parent.getString("inherited.custom")); + assertNull(parent.getString("own.custom")); + } + + @Test + void removingABorrowedCustomTagHidesItLocally() { + TagMap parent = frozenParent(); + TagMap child = TagMap.createFromParent(parent); + + assertEquals("c", child.getAndRemove("inherited.custom").stringValue()); + + assertFalse(child.namesBorrowed); + assertNull(child.getString("inherited.custom")); + assertEquals(2, child.size()); + assertEquals("c", parent.getString("inherited.custom")); + } + + @Test + void removingAnInheritedKnownTagTombstonesItWithoutCountingIt() { + TagMap parent = frozenParent(); + TagMap child = TagMap.createFromParent(parent); + + assertEquals("/inherited", child.getAndRemove(KnownTags.HTTP_ROUTE_ID).stringValue()); + assertNull(child.getAndRemove(KnownTags.HTTP_ROUTE_ID)); // already removed + + assertNull(child.getEntry(KnownTags.HTTP_ROUTE_ID)); + assertEquals(2, child.size()); + assertFalse(child.containsKey(KnownTags.HTTP_ROUTE_NAME)); + for (TagMap.EntryReader entry : child) { + assertNotEquals(KnownTags.HTTP_ROUTE_ID, entry.tagId()); + } + HashIdStore ids = (HashIdStore) child.ids; + assertEquals(0, ids.count); + assertNotEquals(0L, ids.tombstoneMask); + child.checkIntegrity(); + + // a re-set undoes the removal: no prior visible value + assertNull(child.getAndSet(KnownTags.HTTP_ROUTE_NAME, "/again")); + assertEquals("/again", child.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(3, child.size()); + } + + @Test + void tombstonesAreSharedPerTag() { + TagMap.Entry first = TagMap.Entry.tombstoneOf(KnownTags.HTTP_ROUTE_ID); + assertSame(first, TagMap.Entry.tombstoneOf(KnownTags.HTTP_ROUTE_ID)); + assertTrue(first.tombstone); + assertEquals(KnownTags.HTTP_ROUTE_NAME, first.tag()); + } + + @Test + void aClearSlotBitIsADefiniteMiss() { + HashIdStore ids = new HashIdStore(null); + assertEquals(0L, ids.occupancy); + assertNull(ids.get(KnownTags.HTTP_ROUTE_ID)); + + ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); + assertNotEquals(0L, ids.occupancy & IdStore.slotBit(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/a", ids.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + + // a child inherits its parent's slots, so a read-through miss can stop early + HashIdStore child = new HashIdStore(ids); + assertEquals(ids.occupancy, child.inheritedMask); + assertEquals("/a", child.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + } - HashStore hash = new HashStore(); - hash.setKnown(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); - hash.setKnown(KnownTags.HTTP_STATUS_CODE_ID, 200); - hash.setKnown(KnownTags.HTTP_RESEND_COUNT_ID, 2L); - hash.setKnown(KnownTags.DD_PROFILING_ENABLED_ID, true); - hash.setKnown(KnownTags.COMPONENT_ID, component); - hash.setKnown(KnownTags.DB_POOL_NAME_ID, 1.5f); - hash.setKnown(KnownTags.DB_INSTANCE_ID, 2.25d); - - assertEquals(new HashMap<>(checked), new HashMap<>(TagMap.viewOf(null, hash, null))); - assertEquals(KnownTags.HTTP_ROUTE_ID, hash.getById(KnownTags.HTTP_ROUTE_ID).tagId()); - assertEquals(1.5f, hash.getById(KnownTags.DB_POOL_NAME_ID).floatValue()); + @Test + void anUnknownIdIsRejected() { + HashIdStore ids = new HashIdStore(null); + assertThrows(IllegalArgumentException.class, () -> ids.get(0L)); + assertThrows(IllegalArgumentException.class, () -> ids.remove(0L)); } @Test - void theIdStoreAdapterReadsAndWritesTheHashStoresKnownTags() { - HashStore hash = new HashStore(); - IdStore ids = hash.idStore(); - ids.set(KnownTags.HTTP_ROUTE_ID, (Object) "/a"); - ids.set(KnownTags.COMPONENT_ID, "jdbc"); + void theIdStoreSetsEveryValueShape() { + HashIdStore ids = new HashIdStore(null); + ids.set(KnownTags.COMPONENT_ID, (Object) new StringBuilder("jdbc")); + ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); ids.set(KnownTags.DD_PROFILING_ENABLED_ID, true); ids.set(KnownTags.HTTP_STATUS_CODE_ID, 200); ids.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); ids.set(KnownTags.DB_POOL_NAME_ID, 1.5f); ids.set(KnownTags.DB_INSTANCE_ID, 2.25d); - assertEquals("/a", hash.getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals("jdbc", ids.get(KnownTags.COMPONENT_ID).stringValue()); - assertEquals(true, ids.getKnownValue(KnownTags.DD_PROFILING_ENABLED_ID)); - assertEquals(200, ids.getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); - assertEquals(2L, ids.getKnownValue(KnownTags.HTTP_RESEND_COUNT_ID)); - assertEquals(1.5f, ids.getKnownValue(KnownTags.DB_POOL_NAME_ID)); - assertEquals(2.25d, ids.getKnownValue(KnownTags.DB_INSTANCE_ID)); + TagMap checked = TagMap.create(); + checked.set(KnownTags.COMPONENT_ID, ids.get(KnownTags.COMPONENT_ID).objectValue()); + checked.set(KnownTags.HTTP_ROUTE_ID, "/a"); + checked.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + checked.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + checked.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); - TagMap.Entry route = TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/b"); - assertEquals("/a", ids.put(route).stringValue()); - assertEquals( - "/b", ids.getAndPut(TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/c")).stringValue()); - assertEquals("/c", ids.remove(KnownTags.HTTP_ROUTE_ID).stringValue()); - assertNull(hash.getById(KnownTags.HTTP_ROUTE_ID)); - assertThrows(IllegalArgumentException.class, () -> ids.get(0L)); + Map actual = new HashMap<>(); + ids.fillMapLocal(actual); + assertEquals(new HashMap<>(checked), actual); + assertEquals(7, ids.size()); + assertEquals(7, ids.computeSize()); + assertTrue(ids.toInternalString().contains(KnownTags.HTTP_ROUTE_NAME)); } @Test - void customTagsAreKeptByName() { - HashStore hash = new HashStore(); - hash.set("a", (Object) "x"); - hash.set("b", "y"); - hash.set("c", true); - hash.set("d", 1); - hash.set("e", 2L); - hash.set("f", 3f); - hash.set("g", 4d); - - assertEquals("x", hash.get("a").objectValue()); - assertEquals("y", hash.get("b").stringValue()); - assertTrue(hash.get("c").booleanValue()); - assertEquals(1, hash.get("d").intValue()); - assertEquals(2L, hash.get("e").longValue()); - assertEquals(3f, hash.get("f").floatValue()); - assertEquals(4d, hash.get("g").doubleValue()); - assertEquals("x", hash.remove("a").objectValue()); - assertNull(hash.get("a")); + void putAllIntoAStoreWithTombstonesPutsOneAtATime() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.remove(KnownTags.HTTP_ROUTE_NAME); // a tombstone + + TagMap source = TagMap.create(); + source.set(KnownTags.HTTP_ROUTE_ID, "/from-source"); + source.set(KnownTags.HTTP_METHOD_ID, "GET"); + child.putAll(source); + + assertEquals("/from-source", child.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(4, child.size()); // route, method, status code, inherited custom + child.checkIntegrity(); } @Test - void aNameStoreRejectsAKnownTagsName() { - HashStore hash = new HashStore(); - assertThrows(AssertionError.class, () -> hash.get(KnownTags.HTTP_ROUTE_NAME)); - assertThrows(AssertionError.class, () -> hash.remove(KnownTags.HTTP_ROUTE_NAME)); + void aCopyKeepsTombstonesAndBorrowing() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.remove(KnownTags.HTTP_ROUTE_NAME); + + TagMap copy = child.copy(); + assertNull(copy.getString(KnownTags.HTTP_ROUTE_NAME)); + assertTrue(copy.namesBorrowed); + assertEquals(child.size(), copy.size()); + + copy.set(KnownTags.HTTP_ROUTE_NAME, "/copy"); + assertNull(child.getString(KnownTags.HTTP_ROUTE_NAME)); // independent } @Test - void routingSendsKnownTagsByIdAndEverythingElseByName() { - HashStore hash = new HashStore(); - IdStore ids = TagRouting.idStoreFor(hash); + void anIteratorWalksBothStoresAndTheirParents() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(KnownTags.HTTP_METHOD_ID, "GET"); + child.set("own.custom", "o"); + child.remove("inherited.custom"); + + Map seen = new HashMap<>(); + for (TagMap.EntryReader entry : child) { + seen.put(entry.tag(), entry.objectValue()); + } + Map expected = new HashMap<>(); + expected.put(KnownTags.HTTP_ROUTE_NAME, "/inherited"); + expected.put(KnownTags.HTTP_STATUS_CODE_NAME, 200); + expected.put(KnownTags.HTTP_METHOD_NAME, "GET"); + expected.put("own.custom", "o"); + assertEquals(expected, seen); + assertEquals(expected, new HashMap<>(child)); + } + @Test + void routingSendsKnownTagsByIdAndEverythingElseByName() { + TagMap tags = TagMap.create(); long route = TagRouting.keyOf(KnownTags.HTTP_ROUTE_NAME); - long custom = TagRouting.keyOf("my.tag"); long alias = TagRouting.keyOf("server.address"); assertEquals(KnownTags.HTTP_ROUTE_ID, route); - assertEquals(0L, custom); + assertEquals(0L, TagRouting.keyOf("my.tag")); assertTrue(alias < 0); - TagRouting.set(ids, hash, route, KnownTags.HTTP_ROUTE_NAME, (Object) "/a"); - TagRouting.set(ids, hash, custom, "my.tag", (Object) "custom"); - TagRouting.set(ids, hash, alias, "server.address", (Object) "as-written"); + TagRouting.set(tags, route, KnownTags.HTTP_ROUTE_NAME, (Object) "/a"); + TagRouting.set(tags, 0L, "my.tag", (Object) "custom"); + TagRouting.set(tags, alias, "server.address", (Object) "as-written"); - assertEquals("/a", TagRouting.get(ids, hash, route, KnownTags.HTTP_ROUTE_NAME).stringValue()); - assertEquals("/a", TagRouting.getKnownValue(ids, hash, KnownTags.HTTP_ROUTE_ID)); - assertEquals("custom", hash.get("my.tag").stringValue()); - assertEquals("as-written", hash.get("server.address").stringValue()); // an alias is custom + assertEquals("/a", TagRouting.get(tags, route, KnownTags.HTTP_ROUTE_NAME).stringValue()); + assertEquals("/a", TagRouting.getKnownValue(tags, KnownTags.HTTP_ROUTE_ID)); + assertEquals("custom", TagRouting.getCustom(tags, "my.tag").stringValue()); + assertEquals("as-written", TagRouting.getCustom(tags, "server.address").stringValue()); - assertNotNull(TagRouting.remove(ids, hash, route, KnownTags.HTTP_ROUTE_NAME)); - assertNotNull(TagRouting.remove(ids, hash, custom, "my.tag")); - assertNull(TagRouting.getById(ids, hash, KnownTags.HTTP_ROUTE_ID)); - assertNull(hash.get("my.tag")); + assertNotNull(TagRouting.remove(tags, route, KnownTags.HTTP_ROUTE_NAME)); + assertNotNull(TagRouting.removeCustom(tags, "my.tag")); + assertNull(TagRouting.getById(tags, KnownTags.HTTP_ROUTE_ID)); + assertNull(TagRouting.removeById(tags, KnownTags.HTTP_ROUTE_ID)); + assertNull(TagRouting.remove(tags, 0L, "my.tag")); } @Test - void routingSetsEveryValueShapeByKey() { - HashStore hash = new HashStore(); - IdStore ids = TagRouting.idStoreFor(hash); - TagRouting.set(ids, hash, KnownTags.COMPONENT_ID, "component", "jdbc"); - TagRouting.set(ids, hash, KnownTags.DD_PROFILING_ENABLED_ID, "x", true); - TagRouting.set(ids, hash, KnownTags.HTTP_STATUS_CODE_ID, "x", 200); - TagRouting.set(ids, hash, KnownTags.HTTP_RESEND_COUNT_ID, "x", 2L); - TagRouting.set(ids, hash, KnownTags.DB_POOL_NAME_ID, "x", 1.5f); - TagRouting.set(ids, hash, KnownTags.DB_INSTANCE_ID, "x", 2.25d); - TagRouting.set(ids, hash, 0L, "s", "v"); - TagRouting.set(ids, hash, 0L, "b", true); - TagRouting.set(ids, hash, 0L, "i", 1); - TagRouting.set(ids, hash, 0L, "l", 2L); - TagRouting.set(ids, hash, 0L, "f", 3f); - TagRouting.set(ids, hash, 0L, "d", 4d); - - TagMap expected = TagMap.create(); - expected.set(KnownTags.COMPONENT_ID, "jdbc"); - expected.set(KnownTags.DD_PROFILING_ENABLED_ID, true); - expected.set(KnownTags.HTTP_STATUS_CODE_ID, 200); - expected.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); - expected.set(KnownTags.DB_POOL_NAME_ID, 1.5f); - expected.set(KnownTags.DB_INSTANCE_ID, 2.25d); - expected.set("s", "v"); - expected.set("b", true); - expected.set("i", 1); - expected.set("l", 2L); - expected.set("f", 3f); - expected.set("d", 4d); - assertEquals(new HashMap<>(expected), new HashMap<>(TagMap.viewOf(ids, hash, null))); + void routingSetsEveryValueShape() { + TagMap tags = TagMap.create(); + TagRouting.setKnown(tags, KnownTags.COMPONENT_ID, (Object) "jdbc"); + TagRouting.setKnown(tags, KnownTags.HTTP_ROUTE_ID, "/a"); + TagRouting.setKnown(tags, KnownTags.DD_PROFILING_ENABLED_ID, true); + TagRouting.setKnown(tags, KnownTags.HTTP_STATUS_CODE_ID, 200); + TagRouting.setKnown(tags, KnownTags.HTTP_RESEND_COUNT_ID, 2L); + TagRouting.setKnown(tags, KnownTags.DB_POOL_NAME_ID, 1.5f); + TagRouting.setKnown(tags, KnownTags.DB_INSTANCE_ID, 2.25d); + TagRouting.set(tags, KnownTags.HTTP_METHOD_ID, "x", "GET"); + TagRouting.set(tags, KnownTags.DD_PROFILING_ENABLED_ID, "x", false); + TagRouting.set(tags, KnownTags.HTTP_STATUS_CODE_ID, "x", 201); + TagRouting.set(tags, KnownTags.HTTP_RESEND_COUNT_ID, "x", 3L); + TagRouting.set(tags, KnownTags.DB_POOL_NAME_ID, "x", 2.5f); + TagRouting.set(tags, KnownTags.DB_INSTANCE_ID, "x", 3.25d); + TagRouting.set(tags, 0L, "s", "v"); + TagRouting.set(tags, 0L, "b", true); + TagRouting.set(tags, 0L, "i", 1); + TagRouting.set(tags, 0L, "l", 2L); + TagRouting.set(tags, 0L, "f", 3f); + TagRouting.set(tags, 0L, "d", 4d); + TagRouting.setCustom(tags, "o", "obj"); + + assertEquals(201, tags.getObject(KnownTags.HTTP_STATUS_CODE_NAME)); + assertEquals(false, tags.getObject(KnownTags.DD_PROFILING_ENABLED_NAME)); + assertEquals(3.25d, tags.getObject(KnownTags.DB_INSTANCE_NAME)); + assertEquals(4d, tags.getObject("d")); + assertEquals("obj", tags.getObject("o")); + assertEquals(15, tags.size()); } @Test void routingPutsEntriesByTheirKind() { - HashStore hash = new HashStore(); - IdStore ids = TagRouting.idStoreFor(hash); - assertNull(TagRouting.put(ids, hash, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/a"))); - assertNull(TagRouting.getAndPut(ids, hash, TagMap.Entry.create("my.tag", "x"))); + TagMap tags = TagMap.create(); + assertNull(TagRouting.put(tags, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/a"))); + assertNull(TagRouting.getAndPut(tags, TagMap.Entry.create("my.tag", "x"))); assertEquals( "/a", - TagRouting.getAndPut(ids, hash, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/b")) + TagRouting.getAndPut(tags, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/b")) .stringValue()); - assertEquals("x", TagRouting.put(ids, hash, TagMap.Entry.create("my.tag", "y")).stringValue()); - } - - @Test - void putAllCopiesTheVisibleTags() { - TagMap parent = TagMap.create(); - parent.set("inherited", "p"); - TagMap source = TagMap.createFromParent(parent.freeze()); - source.set(KnownTags.HTTP_ROUTE_ID, "/a"); - - HashStore hash = new HashStore(); - TagRouting.putAll(TagRouting.idStoreFor(hash), hash, source); - assertEquals("p", hash.get("inherited").stringValue()); - assertEquals("/a", hash.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("x", TagRouting.put(tags, TagMap.Entry.create("my.tag", "y")).stringValue()); } @Test - void aViewReadsAnAliasByTheSpansDirection() { - HashStore hash = new HashStore(); - hash.setKnown(KnownTags.HTTP_HOSTNAME_ID, "inbound.example"); - hash.setKnown(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); - - TagMap inbound = TagMap.viewOf(null, hash, direction(DIRECTION_INBOUND, false)); - TagMap outbound = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, false)); - TagMap plain = TagMap.viewOf(null, hash, null); + void aSpansTagsReadAnAliasByItsDirection() { + TagMap inbound = TagMap.forSpan(null, direction(DIRECTION_INBOUND, false)); + TagMap outbound = TagMap.forSpan(null, direction(DIRECTION_OUTBOUND, false)); + for (TagMap tags : new TagMap[] {inbound, outbound}) { + tags.set(KnownTags.HTTP_HOSTNAME_ID, "inbound.example"); + tags.set(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); + } + TagMap plain = TagMap.create(); + plain.set(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); assertEquals("inbound.example", inbound.getString("server.address")); assertEquals("outbound.example", outbound.getString("server.address")); @@ -206,14 +298,14 @@ void aViewReadsAnAliasByTheSpansDirection() { } @Test - void aViewReadsAPendingAliasOverItsTag() { - HashStore hash = new HashStore(); - hash.setKnown(KnownTags.PEER_HOSTNAME_ID, "older"); - hash.set("server.address", "pending"); - - TagMap pending = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, true)); - TagMap noPending = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, false)); - TagMap unknown = TagMap.viewOf(null, hash, direction(DIRECTION_UNKNOWN, true)); + void aSpansTagsReadAnEmittedPendingAliasOverItsTag() { + TagMap pending = TagMap.forSpan(null, direction(DIRECTION_OUTBOUND, true)); + TagMap noPending = TagMap.forSpan(null, direction(DIRECTION_OUTBOUND, false)); + TagMap unknown = TagMap.forSpan(null, direction(DIRECTION_UNKNOWN, true)); + for (TagMap tags : new TagMap[] {pending, noPending, unknown}) { + tags.set(KnownTags.PEER_HOSTNAME_ID, "older"); + TagRouting.setCustom(tags, "server.address", "pending"); + } assertEquals("pending", pending.getString(KnownTags.PEER_HOSTNAME_NAME)); assertEquals("pending", pending.getString("server.address")); @@ -224,38 +316,175 @@ void aViewReadsAPendingAliasOverItsTag() { } @Test - void aViewIsLiveOverTheStores() { - HashStore hash = new HashStore(); - TagMap view = TagMap.viewOf(null, hash, direction(DIRECTION_OUTBOUND, false)); - view.set(KnownTags.HTTP_ROUTE_NAME, "/a"); - view.set("my.tag", "x"); - assertEquals("/a", hash.getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals("x", hash.get("my.tag").stringValue()); - - TagMap copy = view.copy(); - view.remove("my.tag"); - assertEquals("x", copy.getString("my.tag")); // a copy is a TagMap of its own - assertFalse(view.containsKey("my.tag")); - assertSame(hash, view.hashStore()); + void forSpanMirrorsCreateFromParent() { + assertThrows( + IllegalStateException.class, + () -> TagMap.forSpan(TagMap.create(), direction(DIRECTION_UNKNOWN, false))); + assertNull(TagMap.forSpan(TagMap.create().freeze(), null).parent); // empty parent dropped + + TagMap span = TagMap.forSpan(frozenParent(), direction(DIRECTION_UNKNOWN, false)); + assertEquals("/inherited", span.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(3, span.size()); } @Test - void readThroughStoreMirrorsCreateFromParent() { - assertNull(TagMap.readThroughStore(null)); - assertThrows(IllegalStateException.class, () -> TagMap.readThroughStore(TagMap.create())); - assertNull(TagMap.readThroughStore(TagMap.create().freeze())); + void clearDetachesBothStores() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set("own", "o"); + child.clear(); + assertTrue(child.isEmpty()); + assertNull(child.names); + assertNull(child.getString("inherited.custom")); + assertTrue(child.toInternalString().contains("names:")); + } - TagMap parent = TagMap.create(); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + /** Every known tag's real id, from the registry. */ + private static java.util.List knownIds() { + java.util.List ids = new java.util.ArrayList<>(); + for (int serial = 1; serial < KnownTagCodec.serialLimit(); serial++) { + long id = KnownTagCodec.keyOf(KnownTagCodec.nameOf(KnownTagCodec.makeTagId(serial))); + if (id > 0) { + ids.add(id); + } + } + return ids; + } + + /** + * A three-level chain with enough tags in each store to fill bucket groups, checked against a + * HashMap model through every read path: lookups, size, emptiness, iteration and forEach. + */ + @Test + void aDeepChainReadsLikeItsModel() { + java.util.List ids = knownIds(); + Map model = new HashMap<>(); + + TagMap grandparent = TagMap.create(); + for (int i = 0; i < ids.size(); i += 2) { + grandparent.set(ids.get(i), "gp" + i); + model.put(KnownTagCodec.nameOf(ids.get(i)), "gp" + i); + } + for (int i = 0; i < 40; i++) { + grandparent.set("custom." + i, "gp" + i); + model.put("custom." + i, "gp" + i); + } + grandparent.freeze(); + + TagMap parent = TagMap.createFromParent(grandparent); + for (int i = 1; i < ids.size(); i += 4) { + parent.set(ids.get(i), "p" + i); + model.put(KnownTagCodec.nameOf(ids.get(i)), "p" + i); + } + for (int i = 0; i < 40; i += 3) { + parent.set("custom." + i, "p" + i); + model.put("custom." + i, "p" + i); + } + parent.remove(KnownTagCodec.nameOf(ids.get(0))); // tombstone a grandparent known tag + model.remove(KnownTagCodec.nameOf(ids.get(0))); + parent.remove("custom.1"); + model.remove("custom.1"); parent.freeze(); - HashStore parentStore = TagMap.readThroughStore(parent); - assertSame(parent.hashStore(), parentStore); - - HashStore child = new HashStore(parentStore); - assertEquals("/inherited", child.getKnownValue(KnownTags.HTTP_ROUTE_ID)); - child.removeById(KnownTags.HTTP_ROUTE_ID); // tombstones the inherited tag - assertNull(child.getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals("/inherited", parent.getString(KnownTags.HTTP_ROUTE_NAME)); + + TagMap child = TagMap.createFromParent(parent); + for (int i = 0; i < ids.size(); i += 3) { + child.set(ids.get(i), "c" + i); + model.put(KnownTagCodec.nameOf(ids.get(i)), "c" + i); + } + for (int i = 2; i < ids.size(); i += 5) { + String name = KnownTagCodec.nameOf(ids.get(i)); + child.remove(name); + model.remove(name); + } + for (int i = 0; i < 40; i += 4) { + child.set("custom." + i, "c" + i); + model.put("custom." + i, "c" + i); + } + child.remove("custom.2"); + model.remove("custom.2"); + + assertEquals(model.size(), child.size()); + assertFalse(child.isEmpty()); + assertFalse(child.isDefinitelyEmpty()); + assertTrue(child.estimateSize() >= model.size()); + assertEquals(model, new HashMap<>(child)); + for (Map.Entry entry : model.entrySet()) { + assertEquals(entry.getValue(), child.getObject(entry.getKey()), entry.getKey()); + } + + Map viaConsumer = new HashMap<>(); + child.forEach(entry -> viaConsumer.put(entry.tag(), entry.objectValue())); + assertEquals(model, viaConsumer); + Map viaBi = new HashMap<>(); + child.forEach(viaBi, (m, entry) -> m.put(entry.tag(), entry.objectValue())); + assertEquals(model, viaBi); + Map viaTri = new HashMap<>(); + child.forEach(viaTri, "", (m, unused, entry) -> m.put(entry.tag(), entry.objectValue())); + assertEquals(model, viaTri); + Map filled = new HashMap<>(); + child.fillMap(filled); + assertEquals(model, filled); + + TagMap flat = TagMap.create(); + flat.set("custom.0", "flat"); + flat.set(ids.get(0), "flat"); + flat.putAll(child); // a read-through source: copied as its visible union + assertEquals(model, new HashMap<>(flat)); + flat.checkIntegrity(); + } + + @Test + void aChildThatHidesEverythingInheritedIsEmpty() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.set("c", "x"); + TagMap child = TagMap.createFromParent(parent.freeze()); + assertFalse(child.isEmpty()); + + child.remove(KnownTags.HTTP_ROUTE_NAME); + assertFalse(child.isEmpty()); + child.remove("c"); + assertTrue(child.isEmpty()); + assertEquals(0, child.size()); + assertTrue(child.isDefinitelyEmpty() == false); // the parent still holds local entries + } + + @Test + void removingALocalOverrideOfAnInheritedTagReturnsTheLocalValue() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(KnownTags.HTTP_ROUTE_ID, "/local"); + child.set("inherited.custom", "local"); + + assertEquals("/local", child.getAndRemove(KnownTags.HTTP_ROUTE_ID).stringValue()); + assertEquals("local", child.getAndRemove("inherited.custom").stringValue()); + assertNull(child.getString(KnownTags.HTTP_ROUTE_NAME)); + assertNull(child.getString("inherited.custom")); + } + + @Test + void putAllMergesIntoNonEmptyStores() { + TagMap dest = TagMap.create(); + dest.set(KnownTags.HTTP_ROUTE_ID, "/dest"); + dest.set("d", "dest"); + TagMap source = TagMap.create(); + source.set(KnownTags.HTTP_METHOD_ID, "GET"); + source.set(KnownTags.HTTP_ROUTE_ID, "/source"); + source.set("s", "source"); + + dest.putAll(source); + + assertEquals("/source", dest.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals("GET", dest.getString(KnownTags.HTTP_METHOD_NAME)); + assertEquals("source", dest.getString("s")); + assertEquals("dest", dest.getString("d")); + assertEquals(4, dest.size()); + dest.checkIntegrity(); + } + + @Test + void aCustomStoreRejectsAKnownTagsName() { + HashStore names = new HashStore(null); + assertThrows(AssertionError.class, () -> names.get(KnownTags.HTTP_ROUTE_NAME)); + assertThrows(AssertionError.class, () -> names.remove(KnownTags.HTTP_ROUTE_NAME)); } private static TagMap.DirectionSource direction(int direction, boolean pendingAliases) { From d147b3d55a621ce1220451a53fcec5c8483b1ad2 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 14:13:51 -0400 Subject: [PATCH 63/99] Measure a read of a known tag the span doesn't hold Co-Authored-By: Claude Opus 5.5 --- .../jmh/java/datadog/trace/core/GetTagBenchmark.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java index d0e1f4ef64a..4da4fde8618 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java @@ -67,6 +67,17 @@ public Object missingTag_getTag() { return span.getTag("not.there"); } + /** A known tag the span doesn't hold: a clear slot bit can answer without a probe. */ + @Benchmark + public Object missingKnownTag_getTag() { + return span.getTag(KnownTags.HTTP_METHOD_NAME); + } + + @Benchmark + public Object missingKnownTag_unsafeGetTag() { + return span.unsafeGetTag(KnownTags.HTTP_METHOD_NAME); + } + @Benchmark public Object knownTag_unsafeGetTag() { return span.unsafeGetTag(KnownTags.HTTP_ROUTE_NAME); From 023313b9dec989edfb3b2b5dbab15f93e73f1bca Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 15:18:11 -0400 Subject: [PATCH 64/99] Let the span use its tag stores directly again DDSpanContext no longer routes its reads and writes through a TagMap. It owns its id store, created with the span, and its custom store, created on the first custom write and until then null or borrowed from the read-through parent. Every span read and write goes span to store, and the span locks on its id store, which always exists. The TagMap is again only a cached view for the span's tag post-processors, the serializer and the getTags() snapshot. Before each use the span points it at its current custom store; afterwards it adopts any custom store a reader created through the view. DirectionalTags works on the span's stores through a small TagStores interface the span implements; its test supplies one over a TagMap. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 347 +++++++++++++----- .../datadog/trace/core/DirectionalTags.java | 76 ++-- .../trace/core/DirectionalTagsTest.java | 142 ++++--- .../forbiddenApiFilters/instrumentation.txt | 15 +- .../java/datadog/trace/api/HashIdStore.java | 24 +- .../java/datadog/trace/api/HashStore.java | 31 +- .../main/java/datadog/trace/api/IdStore.java | 31 +- .../main/java/datadog/trace/api/TagMap.java | 79 ++-- .../java/datadog/trace/api/TagRouting.java | 206 +++-------- .../java/datadog/trace/api/TagStoresTest.java | 162 ++++---- 10 files changed, 647 insertions(+), 466 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 78ab335de29..110c45a4c77 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 @@ -15,6 +15,8 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; +import datadog.trace.api.HashStore; +import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; @@ -45,7 +47,6 @@ 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; @@ -69,17 +70,13 @@ * across Span boundaries and (2) any Datadog fields that are needed to identify or contextualize * the associated Span instance */ -@SuppressFBWarnings( - value = "USO_UNSAFE_ACCESSIBLE_OBJECT_SYNCHRONIZATION", - justification = - "The tags lock is the span's TagMap, handed only to the tracer's internal readers, which" - + " run under it; a separate lock object would cost an allocation per span") public class DDSpanContext implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext, - TagMap.DirectionSource { + TagMap.DirectionSource, + DirectionalTags.TagStores { private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class); public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -185,13 +182,24 @@ private static int[] tagDirectionOfSpanKind() { * rather read and accessed in a serial fashion on thread after thread. The synchronization can * then be wrapped around bulk operations to minimize the costly atomic operations. * - *

The span's tags live in a {@link TagMap} it creates with the span ({@link TagMap#forSpan}), - * which holds an id store for the known tags and, from the first custom write, a custom store. - * The span routes every access to it through {@link TagRouting}, under one lock that guards both - * stores: {@code tags} itself. The same TagMap, whose name-keyed reads honor the span's - * direction, is what the span's internal readers (post-processors, the serializer) get. + *

The span owns its tag stores directly: {@code tagIds}, the known tags by id, created with + * the span; and {@code customTags}, the custom tags by name, created on the first custom write -- + * until then null, or the read-through parent's custom store, borrowed read-only while {@code + * customTagsBorrowed}. Every read and write goes span to store, routed by {@link TagRouting}, + * under one lock that guards both: {@code tagIds}, which always exists. Internal readers get a + * cached {@link #tagView() view}. */ - private final TagMap tags; + private final IdStore tagIds; + + private HashStore customTags; + + private boolean customTagsBorrowed; + + /** + * This span's view of its tag stores for its internal readers, created on first use under the + * {@code tagIds} lock; see {@link #tagView()}. + */ + private TagMap tagView; /** The service name is required, otherwise the span are dropped by the agent */ private volatile String serviceName; @@ -457,7 +465,10 @@ public DDSpanContext( this.pathwayContext = pathwayContext; // Reads through to the frozen tracer-level tags, if any. - this.tags = TagMap.forSpan(readThroughParent, this); + TagMap parentTags = TagRouting.readThroughParent(readThroughParent); + this.tagIds = TagRouting.newIdStore(parentTags); + this.customTags = TagRouting.inheritedCustomStore(parentTags); + this.customTagsBorrowed = this.customTags != null; // must set this before setting the service and resource names below this.profilingContextIntegration = profilingContextIntegration; @@ -706,7 +717,7 @@ private void seedAbsentTags(final TagMap map) { if (map == null) { return; } - synchronized (tags) { + synchronized (tagIds) { map.forEach( this, (ctx, tagEntry) -> { @@ -839,7 +850,7 @@ public int getSamplingPriority() { } public void setSpanSamplingPriority(double rate, int limit) { - synchronized (tags) { + synchronized (tagIds) { unsafeSetTag(SPAN_SAMPLING_MECHANISM_TAG, SamplingMechanism.SPAN_SAMPLING_RATE); unsafeSetTag(SPAN_SAMPLING_RULE_RATE_TAG, rate); if (limit != Integer.MAX_VALUE) { @@ -968,28 +979,178 @@ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); // Checked under the lock alias registration takes: a writer that read no direction registers // its pending alias before this sees the flag, or reads the new kind and resolves at write. - synchronized (tags) { + synchronized (tagIds) { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { // The span now has a direction: fold pending aliases into the tags they denote, so every // later reader (peer service naming, stats, sampling rules, serialization) sees them // resolved. Once none is left, the span is back on the fast path. - pendingAliases = DirectionalTags.resolvePending(tags, pending, tagDirection()); + pendingAliases = DirectionalTags.resolvePending(this, pending, tagDirection()); } } } /** - * This span's tags, for emitting them: first copies any pending aliases into the HashStore under - * their literal names, so a span that never learns its kind still emits them. The holder stays - * the span's record of them. Call under the {@code tags} lock. + * The view for emitting the span's tags: first copies any pending aliases into the custom store + * under their literal names, so a span that never learns its kind still emits them. The holder + * stays the span's record of them. Call under the {@code tagIds} lock, and call {@link + * #adoptViewStores} once done with the view. */ private TagMap emittableTags() { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { - DirectionalTags.copyPending(pending, tags); + DirectionalTags.copyPending(pending, this); + } + return tagView(); + } + + /** + * This span's cached view of its tag stores, for its internal readers (post-processors, the + * serializer, the snapshot copy) -- never its own set/get paths. Before each use it is pointed at + * the current custom store, which the span creates lazily. Call under the {@code tagIds} lock. + */ + private TagMap tagView() { + TagMap view = this.tagView; + if (view == null) { + // a view handed to the post-processor chain escapes, so allocate it once per span + this.tagView = view = TagMap.viewOf(tagIds, customTags, customTagsBorrowed, this); + } else { + view.attachCustomStore(customTags, customTagsBorrowed); + } + return view; + } + + /** + * Adopts the custom store a reader may have created through the view (by writing a custom tag), + * so the span's own paths see it. Call under the {@code tagIds} lock, after using the view. + */ + private void adoptViewStores(TagMap view) { + this.customTags = view.customStore(); + this.customTagsBorrowed = view.isCustomStoreBorrowed(); + } + + /** The custom store to write to: created on the first custom write, ending any borrowing. */ + private HashStore customTagsForWrite() { + HashStore custom = this.customTags; + if (custom == null || this.customTagsBorrowed) { + custom = TagRouting.newCustomStore(custom); // reads through to the store borrowed till now + this.customTags = custom; + this.customTagsBorrowed = false; + } + return custom; + } + + /* + * DirectionalTags.TagStores: the span's stores, for DirectionalTags. Call under the tagIds lock. + */ + + @Override + public Object knownValue(long tagId) { + return tagIds.getKnownValue(tagId); + } + + @Override + public void setKnown(long tagId, Object value) { + tagIds.set(tagId, value); + } + + @Override + public void removeKnown(long tagId) { + tagIds.remove(tagId); + } + + @Override + public Object customValue(String name) { + HashStore custom = this.customTags; + TagMap.Entry entry = custom == null ? null : custom.get(name); + return entry == null ? null : entry.objectValue(); + } + + @Override + public void setCustom(String name, Object value) { + customTagsForWrite().set(name, value); + } + + @Override + public void removeCustom(String name) { + HashStore custom = this.customTags; + if (custom == null) { + return; + } + if (this.customTagsBorrowed) { + if (custom.get(name) == null) { + return; // nothing to hide: keep borrowing + } + custom = customTagsForWrite(); // hiding an inherited tag needs a tombstone of our own + } + custom.remove(name); + } + + /** Copies {@code map}'s tags into the stores, sharing its Entry objects. */ + private void putAllTags(TagMap map) { + TagMap view = tagView(); + view.putAll(map); + adoptViewStores(view); + } + + /* + * The resolved writes: a known tag by id, else a custom tag (or an alias stored as written). + */ + + private void setResolved(long key, String tag, Object value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); + } + } + + private void setResolved(long key, String tag, CharSequence value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); + } + } + + private void setResolved(long key, String tag, boolean value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); + } + } + + private void setResolved(long key, String tag, int value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); + } + } + + private void setResolved(long key, String tag, long value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); + } + } + + private void setResolved(long key, String tag, float value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); + } + } + + private void setResolved(long key, String tag, double value) { + if (TagRouting.isKnown(key)) { + tagIds.set(key, value); + } else { + customTagsForWrite().set(tag, value); } - return tags; } /** @@ -1031,8 +1192,8 @@ public String getSpanKindString() { return SPAN_KIND_VALUES[ordinal]; } // UNSET or CUSTOM -- fall through to tag map - synchronized (tags) { - TagMap.Entry kind = TagRouting.getById(tags, KnownTags.SPAN_KIND_ID); + synchronized (tagIds) { + TagMap.Entry kind = tagIds.get(KnownTags.SPAN_KIND_ID); return kind == null ? null : kind.stringValue(); } } @@ -1044,31 +1205,31 @@ public void setOrigin(final CharSequence origin) { } public void setMetric(final CharSequence key, final Number value) { - synchronized (tags) { + synchronized (tagIds) { unsafeSetTag(key.toString(), value); } } public void setMetric(final CharSequence key, final int value) { - synchronized (tags) { + synchronized (tagIds) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { - synchronized (tags) { + synchronized (tagIds) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { - synchronized (tags) { + synchronized (tagIds) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { - synchronized (tags) { + synchronized (tagIds) { storeTag(key.toString(), value); } } @@ -1078,7 +1239,7 @@ public void setMetric(final TagMap.EntryReader entry) { return; } - synchronized (tags) { + synchronized (tagIds) { storeEntry(entry); } } @@ -1102,7 +1263,7 @@ private long writeKeyOf(String tag) { if (key == 0) { return 0L; } - long aliasedId = DirectionalTags.prepareAliasWrite(tags, pendingAliases, tag, tagDirection()); + long aliasedId = DirectionalTags.prepareAliasWrite(this, pendingAliases, tag, tagDirection()); if (aliasedId > 0) { dropPendingAliasesIfEmpty(); return aliasedId; @@ -1148,7 +1309,7 @@ private void storeTag(String tag, Object value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1157,7 +1318,7 @@ private void storeTag(String tag, CharSequence value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1166,7 +1327,7 @@ private void storeTag(String tag, boolean value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1175,7 +1336,7 @@ private void storeTag(String tag, int value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1184,7 +1345,7 @@ private void storeTag(String tag, long value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1193,7 +1354,7 @@ private void storeTag(String tag, float value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1202,7 +1363,7 @@ private void storeTag(String tag, double value) { if (key == PENDING_ALIAS) { storePendingAlias(tag, value); } else { - TagRouting.set(tags, key, tag, value); + setResolved(key, tag, value); } } @@ -1214,10 +1375,10 @@ private void storeEntry(TagMap.EntryReader entry) { putEntry(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { long aliasedId = - DirectionalTags.prepareAliasWrite(tags, pendingAliases, entry.tag(), tagDirection()); + DirectionalTags.prepareAliasWrite(this, pendingAliases, entry.tag(), tagDirection()); if (aliasedId > 0) { dropPendingAliasesIfEmpty(); - TagRouting.setKnown(tags, aliasedId, entry.objectValue()); + tagIds.set(aliasedId, entry.objectValue()); } else if (aliasedId < 0) { storePendingAlias(entry.tag(), entry.objectValue()); // a real alias, stored as written } else { @@ -1232,7 +1393,11 @@ private void storeEntry(TagMap.EntryReader entry) { private void putEntry(TagMap.EntryReader entry) { TagMap.Entry e = entry.entry(); if (e != null) { - TagRouting.put(tags, e); + if (e.tagId() != 0) { + tagIds.put(e); + } else { + customTagsForWrite().put(e); + } } } @@ -1241,9 +1406,9 @@ public void removeTag(String tag) { // Clear the cached ordinal; the tag store still needs to be updated below. spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (tags) { + synchronized (tagIds) { // A direction-dependent name and the tag it denotes here are removed together. - DirectionalTags.remove(tags, pendingAliases, tag, tagDirection()); + DirectionalTags.remove(this, pendingAliases, tag, tagDirection()); dropPendingAliasesIfEmpty(); } } @@ -1261,8 +1426,8 @@ public void removeTag(long tagId) { if (tagId == KnownTags.SPAN_KIND_ID) { spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (tags) { - DirectionalTags.remove(tags, pendingAliases, tagId, tagDirection()); + synchronized (tagIds) { + DirectionalTags.remove(this, pendingAliases, tagId, tagDirection()); dropPendingAliasesIfEmpty(); } } @@ -1329,10 +1494,10 @@ private boolean interceptEntry(TagMap.EntryReader entry) { */ private boolean isPresent(TagMap.EntryReader entry) { if (KnownTagCodec.isDirectionDependent(entry)) { - return DirectionalTags.get(tags, pendingAliases, entry.tag(), tagDirection()) != null; + return DirectionalTags.get(this, pendingAliases, entry.tag(), tagDirection()) != null; } String tag = entry.tag(); - return TagRouting.get(tags, TagRouting.keyOf(tag), tag) != null; + return TagRouting.get(tagIds, customTags, TagRouting.keyOf(tag), tag) != null; } public void setTag(final String tag, final Object value) { @@ -1345,7 +1510,7 @@ public void setTag(final String tag, final Object value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1361,7 +1526,7 @@ public void setTag(final String tag, final String value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1379,7 +1544,7 @@ public void supersedeAliases(long tagId) { if (!hasPendingAliases()) { return; // entry paths route on the constant mask; nothing to supersede } - synchronized (tags) { + synchronized (tagIds) { supersedePendingAliases(tagId); } } @@ -1391,7 +1556,7 @@ public void supersedeAliases(long tagId) { private void supersedePendingAliases(long tagId) { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null && KnownTagCodec.hasScopedAlias(tagId)) { - DirectionalTags.prepareWrite(tags, pending, tagId, tagDirection()); + DirectionalTags.prepareWrite(this, pending, tagId, tagDirection()); dropPendingAliasesIfEmpty(); } } @@ -1409,8 +1574,8 @@ public void setTag(final long tagId, final Object value) { removeTag(tagId); } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1423,8 +1588,8 @@ public void setTag(final long tagId, final CharSequence value) { removeTag(tagId); } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1436,8 +1601,8 @@ public void setTag(final long tagId, final boolean value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1449,8 +1614,8 @@ public void setTag(final long tagId, final int value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1462,8 +1627,8 @@ public void setTag(final long tagId, final long value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1475,8 +1640,8 @@ public void setTag(final long tagId, final float value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1488,8 +1653,8 @@ public void setTag(final long tagId, final double value) { if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { this.setBox(tagId, value); } else { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, value); + synchronized (tagIds) { + tagIds.set(tagId, value); } } } @@ -1501,7 +1666,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!interceptEntry(entry)) { - synchronized (tags) { + synchronized (tagIds) { storeEntry(entry); } } @@ -1521,8 +1686,8 @@ public void setTag(TagMap.EntryReader entry) { */ private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (tags) { - TagRouting.setKnown(tags, tagId, box); + synchronized (tagIds) { + tagIds.set(tagId, box); } } } @@ -1536,7 +1701,7 @@ public void setTag(final String tag, final boolean value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1551,7 +1716,7 @@ public void setTag(final String tag, final int value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1566,7 +1731,7 @@ public void setTag(final String tag, final long value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1581,7 +1746,7 @@ public void setTag(final String tag, final float value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1596,7 +1761,7 @@ public void setTag(final String tag, final double value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tags) { + synchronized (tagIds) { storeTag(tag, value); } } @@ -1611,7 +1776,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { return; } - synchronized (tags) { + synchronized (tagIds) { if (needsIntercept) { // forEach out-performs the iterator of TagMap // Taking advantage of ability to pass through other context arguments @@ -1625,7 +1790,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { }); } else { // needsIntercept(TagMap) is also true for a map with a direction-dependent name. - tags.putAll(map); + putAllTags(map); } } } @@ -1635,7 +1800,7 @@ void setAllTags(final TagMap.Ledger ledger) { return; } - synchronized (tags) { + synchronized (tagIds) { for (final TagMap.EntryChange entryChange : ledger) { String tag = entryChange.tag(); if (entryChange.isRemoval()) { @@ -1644,7 +1809,7 @@ void setAllTags(final TagMap.Ledger ledger) { spanKindOrdinal = SPAN_KIND_UNSET; } // as removeTag(String): a direction-dependent name and the tag it denotes go together - DirectionalTags.remove(tags, pendingAliases, tag, tagDirection()); + DirectionalTags.remove(this, pendingAliases, tag, tagDirection()); dropPendingAliasesIfEmpty(); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; @@ -1663,7 +1828,7 @@ void setAllTags(final Map map) { } else if (map instanceof TagMap) { setAllTags((TagMap) map); } else if (!map.isEmpty()) { - synchronized (tags) { + synchronized (tagIds) { for (final Map.Entry tag : map.entrySet()) { long tagId = idForWrite(tag.getKey()); boolean intercepted = @@ -1708,7 +1873,7 @@ Object getTag(final String key) { return getSpanKindString(); default: Object value; - synchronized (tags) { + synchronized (tagIds) { value = unsafeGetTag(key); } // maintain previously observable type of http url :| @@ -1724,16 +1889,19 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return DirectionalTags.get(tags, pendingAliases, tag, tagDirection()); + return DirectionalTags.get(this, pendingAliases, tag, tagDirection()); } @Deprecated public TagMap getTags() { - synchronized (tags) { - TagMap tags = this.tags.copy(); + synchronized (tagIds) { + TagMap tags = tagView().copy(); DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { - DirectionalTags.copyPending(pending, tags); // under their literal names + // under their literal names; the copy is a TagMap of its own + for (int i = 0; i < pending.size; i++) { + tags.set(pending.names[i], pending.values[i]); + } } tags.put(DDTags.THREAD_ID, threadId); @@ -1786,8 +1954,10 @@ public void setMetaStruct(final String field, final T value) { } void earlyProcessTags(AppendableSpanLinks links) { - synchronized (tags) { - TagsPostProcessorFactory.eagerProcessor().processTags(emittableTags(), this, links); + synchronized (tagIds) { + TagMap view = emittableTags(); + TagsPostProcessorFactory.eagerProcessor().processTags(view, this, links); + adoptViewStores(view); } } @@ -1816,7 +1986,7 @@ void processTagsAndBaggage( // This is a compromise to avoid... // - 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 (tags) { + synchronized (tagIds) { // One live view of the tag stores, for the post-processors and the serializer TagMap tags = emittableTags(); @@ -1874,6 +2044,7 @@ void processTagsAndBaggage( Config.get().isOtlpTracesExportEnabled() ? OTLP_EXPORT_TRUE : OTLP_EXPORT_FALSE, restrictedSpan.getLinks(), events)); + adoptViewStores(tags); } } @@ -2024,7 +2195,7 @@ public String toString() { s.append(" *measured*"); } - synchronized (tags) { + synchronized (tagIds) { s.append(" tags=").append(new TreeMap<>(getTags())); } return s.toString(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index ccd26ebf449..c3dcda14913 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -48,9 +48,8 @@ *

A span keeps its pending aliases -- the aliases it stored as written -- in a {@link * PendingAliases} holder, not in its stores: null until the first such write, so a span that never * writes one pays nothing, and dropped once they are all resolved. The holder is copied into the - * span's HashStore when its tags are emitted, so an alias that never resolves is emitted under its - * literal name. The store-only methods below, which take no holder, read and resolve aliases stored - * as written in the HashStore itself, as a TagMap holds them. + * span's custom store when its tags are emitted, so an alias that never resolves is emitted under + * its literal name. * *

One more: an alias written before the span has a direction is resolved when the kind arrives, * without passing through {@code TagInterceptor} again, so split-by-tags on its tag doesn't see it. @@ -58,6 +57,25 @@ */ final class DirectionalTags { + /** + * The tag stores DirectionalTags applies a span's direction to: the span itself, which owns them + * (or, in tests, a TagMap). Known tags by id; custom tags -- and aliases stored as written -- by + * name. + */ + interface TagStores { + Object knownValue(long tagId); + + void setKnown(long tagId, Object value); + + void removeKnown(long tagId); + + Object customValue(String name); + + void setCustom(String name, Object value); + + void removeCustom(String name); + } + /** * A span's pending aliases: the direction-scoped aliases it stored as written, with their values, * in write order. Usually one or two. Guarded by the span's tag lock. @@ -129,7 +147,8 @@ static PendingAliases addPending(PendingAliases pending, String alias, Object va * alias only for the sentinel, so a name that merely passed {@link * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. */ - static long prepareAliasWrite(TagMap tags, PendingAliases pending, String alias, int direction) { + static long prepareAliasWrite( + TagStores tags, PendingAliases pending, String alias, int direction) { long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { // a value pending under the alias is older than this write @@ -143,7 +162,7 @@ static long prepareAliasWrite(TagMap tags, PendingAliases pending, String alias, * 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. */ - static void prepareWrite(TagMap tags, PendingAliases pending, long tagId, int direction) { + static void prepareWrite(TagStores tags, PendingAliases pending, long tagId, int direction) { if (KnownTagCodec.hasScopedAlias(tagId)) { removePendingAliases(tags, pending, tagId, direction); } @@ -154,7 +173,7 @@ static void prepareWrite(TagMap tags, PendingAliases pending, long tagId, int di * or, with no direction yet, any it could have. */ private static void removePendingAliases( - TagMap tags, PendingAliases pending, long tagId, int direction) { + TagStores tags, PendingAliases pending, long tagId, int direction) { if (direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); if (alias != null) { @@ -172,10 +191,10 @@ private static void removePendingAliases( /** * Removes {@code alias} as written: from the holder, and from the custom store, which holds it - * once emitted, or when inherited or written through the span's TagMap. + * once emitted, or when inherited or written through the span's view. */ - private static void removeAsWritten(TagMap tags, PendingAliases pending, String alias) { - TagRouting.removeCustom(tags, alias); + private static void removeAsWritten(TagStores tags, PendingAliases pending, String alias) { + tags.removeCustom(alias); if (pending != null) { pending.remove(alias); } @@ -187,28 +206,27 @@ private static void removeAsWritten(TagMap tags, PendingAliases pending, String * write, wins. With no holder, a tag with an alias skips probing for one, so it costs a single * lookup like any other tag. */ - static Object get(TagMap tags, PendingAliases pending, String name, int direction) { + static Object get(TagStores tags, PendingAliases pending, String name, int direction) { long key = TagRouting.keyOf(name); if (key > 0) { return get(tags, pending, key, direction); } if (key == 0) { - TagMap.Entry entry = TagRouting.getCustom(tags, name); - return entry == null ? null : entry.objectValue(); + return tags.customValue(name); } Object written = pending == null ? null : pending.get(name); if (written != null) { return written; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return tagId > 0 ? TagRouting.getKnownValue(tags, tagId) : null; + return tagId > 0 ? tags.knownValue(tagId) : null; } /** * The value of the known tag {@code tagId} on a span of {@code direction}; see {@link - * #get(TagMap, PendingAliases, String, int)}. + * #get(TagStores, PendingAliases, String, int)}. */ - static Object get(TagMap tags, PendingAliases pending, long tagId, int direction) { + static Object get(TagStores tags, PendingAliases pending, long tagId, int direction) { if (pending != null && KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); Object written = alias == null ? null : pending.get(alias); @@ -216,11 +234,11 @@ static Object get(TagMap tags, PendingAliases pending, long tagId, int direction return written; } } - return TagRouting.getKnownValue(tags, tagId); + return tags.knownValue(tagId); } /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ - static void remove(TagMap tags, PendingAliases pending, String name, int direction) { + static void remove(TagStores tags, PendingAliases pending, String name, int direction) { // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { @@ -228,7 +246,7 @@ static void remove(TagMap tags, PendingAliases pending, String name, int directi return; } if (key == 0) { - TagRouting.removeCustom(tags, name); // a custom tag + tags.removeCustom(name); // a custom tag return; } removeAsWritten(tags, pending, name); @@ -238,13 +256,13 @@ static void remove(TagMap tags, PendingAliases pending, String name, int directi } long tagId = KnownTagCodec.directionalKeyOf(name, direction); if (tagId > 0) { - TagRouting.removeById(tags, tagId); + tags.removeKnown(tagId); } } /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ - static void remove(TagMap tags, PendingAliases pending, long tagId, int direction) { - TagRouting.removeById(tags, tagId); + static void remove(TagStores tags, PendingAliases pending, long tagId, int direction) { + tags.removeKnown(tagId); if (KnownTagCodec.hasScopedAlias(tagId)) { removePendingAliases(tags, pending, tagId, direction); } @@ -254,14 +272,14 @@ static void remove(TagMap tags, PendingAliases pending, long tagId, int directio * Removes every tag {@code alias} could denote, for a removal of the alias before the span has a * direction. A removal counts as a write, so no older value can resurface when the kind arrives; * like the write rule, this can drop the other direction's tag. (Removing a tag before then - * likewise removes the pending aliases that could denote it; see {@link #remove(TagMap, + * likewise removes the pending aliases that could denote it; see {@link #remove(TagStores, * PendingAliases, long, int)}.) */ - private static void removeEveryTagOfAlias(TagMap tags, String alias) { + private static void removeEveryTagOfAlias(TagStores tags, String alias) { for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { long aliasedId = KnownTagCodec.directionalKeyOf(alias, direction); if (aliasedId > 0) { - TagRouting.removeById(tags, aliasedId); + tags.removeKnown(aliasedId); } } } @@ -272,7 +290,7 @@ private static void removeEveryTagOfAlias(TagMap tags, String alias) { * an alias with no tag in {@code direction}, or any while the direction is unknown or none -- or * null when none are. */ - static PendingAliases resolvePending(TagMap tags, PendingAliases pending, int direction) { + static PendingAliases resolvePending(TagStores tags, PendingAliases pending, int direction) { if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { return pending; // no alias names a tag on a span with no direction, or an internal one } @@ -281,9 +299,9 @@ static PendingAliases resolvePending(TagMap tags, PendingAliases pending, int di long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { Object value = pending.values[i]; - TagRouting.removeCustom(tags, alias); // a copy emitted earlier + tags.removeCustom(alias); // a copy emitted earlier pending.remove(alias); - TagRouting.setKnown(tags, tagId, value); + tags.setKnown(tagId, value); } else { i++; } @@ -295,9 +313,9 @@ static PendingAliases resolvePending(TagMap tags, PendingAliases pending, int di * Copies the pending aliases into {@code tags} as custom tags under their literal names, so that * emitting the span's tags includes them. The holder stays the span's record of them. */ - static void copyPending(PendingAliases pending, TagMap tags) { + static void copyPending(PendingAliases pending, TagStores tags) { for (int i = 0; i < pending.size; i++) { - TagRouting.setCustom(tags, pending.names[i], pending.values[i]); + tags.setCustom(pending.names[i], pending.values[i]); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java index 6c58fdfce3e..255690833f1 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java @@ -23,6 +23,41 @@ * span tests. */ class DirectionalTagsTest { + /** The stores DirectionalTags works on, over a plain TagMap in place of a span. */ + private static DirectionalTags.TagStores stores(TagMap tags) { + return new DirectionalTags.TagStores() { + @Override + public Object knownValue(long tagId) { + return tags.getObject(KnownTagCodec.nameOf(tagId)); + } + + @Override + public void setKnown(long tagId, Object value) { + tags.set(tagId, value); + } + + @Override + public void removeKnown(long tagId) { + tags.getAndRemove(tagId); + } + + @Override + public Object customValue(String name) { + return tags.getObject(name); + } + + @Override + public void setCustom(String name, Object value) { + tags.set(name, value); + } + + @Override + public void removeCustom(String name) { + tags.remove(name); + } + }; + } + private static PendingAliases pending(String alias, Object value) { return DirectionalTags.addPending(null, alias, value); } @@ -39,16 +74,17 @@ void anAliasAndItsTagReadAsOne(int direction, String alias, String tag) { TagMap tags = TagMap.create(); tags.set(tag, "stored"); - assertEquals("stored", DirectionalTags.get(tags, null, alias, direction)); - assertEquals("stored", DirectionalTags.get(tags, null, tag, direction)); - assertEquals("stored", DirectionalTags.get(tags, null, KnownTagCodec.keyOf(tag), direction)); + assertEquals("stored", DirectionalTags.get(stores(tags), null, alias, direction)); + assertEquals("stored", DirectionalTags.get(stores(tags), null, tag, direction)); + assertEquals( + "stored", DirectionalTags.get(stores(tags), null, KnownTagCodec.keyOf(tag), direction)); // A pending alias is the newer write, so it wins on read, by name or by id. PendingAliases pending = pending(alias, "pending"); - assertEquals("pending", DirectionalTags.get(tags, pending, alias, direction)); - assertEquals("pending", DirectionalTags.get(tags, pending, tag, direction)); + assertEquals("pending", DirectionalTags.get(stores(tags), pending, alias, direction)); + assertEquals("pending", DirectionalTags.get(stores(tags), pending, tag, direction)); assertEquals( - "pending", DirectionalTags.get(tags, pending, KnownTagCodec.keyOf(tag), direction)); + "pending", DirectionalTags.get(stores(tags), pending, KnownTagCodec.keyOf(tag), direction)); } @Test @@ -56,29 +92,34 @@ void withNoDirectionAnAliasReadsOnlyAsWritten() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "stored"); - assertNull(DirectionalTags.get(tags, null, "server.address", DIRECTION_UNKNOWN)); - assertEquals("stored", DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertNull(DirectionalTags.get(stores(tags), null, "server.address", DIRECTION_UNKNOWN)); assertEquals( - "stored", DirectionalTags.get(tags, null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); + "stored", DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals( + "stored", + DirectionalTags.get(stores(tags), null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); PendingAliases pending = pending("server.address", "pending"); assertEquals( - "pending", DirectionalTags.get(tags, pending, "server.address", DIRECTION_UNKNOWN)); + "pending", DirectionalTags.get(stores(tags), pending, "server.address", DIRECTION_UNKNOWN)); assertEquals( - "stored", DirectionalTags.get(tags, pending, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + "stored", + DirectionalTags.get(stores(tags), pending, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); } @Test void aMissReadsAsNull() { TagMap tags = TagMap.create(); - assertNull(DirectionalTags.get(tags, null, "my.tag", DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, null, "server.address", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(stores(tags), null, "my.tag", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get(stores(tags), null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get(stores(tags), null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(stores(tags), null, "server.address", DIRECTION_OUTBOUND)); // client.port names no tag on a client span - assertNull(DirectionalTags.get(tags, null, "client.port", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(stores(tags), null, "client.port", DIRECTION_OUTBOUND)); } @Test @@ -87,10 +128,12 @@ void otherNamesReadAsWritten() { tags.set("my.tag", "custom"); tags.set(Tags.HTTP_METHOD, "GET"); - assertEquals("custom", DirectionalTags.get(tags, null, "my.tag", DIRECTION_OUTBOUND)); - assertEquals("GET", DirectionalTags.get(tags, null, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); + assertEquals("custom", DirectionalTags.get(stores(tags), null, "my.tag", DIRECTION_OUTBOUND)); + assertEquals( + "GET", DirectionalTags.get(stores(tags), null, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); assertEquals( - "GET", DirectionalTags.get(tags, null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + "GET", + DirectionalTags.get(stores(tags), null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); } @Test @@ -99,7 +142,8 @@ void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { PendingAliases pending = pending("server.address", "older"); long tagId = - DirectionalTags.prepareAliasWrite(tags, pending, "server.address", DIRECTION_OUTBOUND); + DirectionalTags.prepareAliasWrite( + stores(tags), pending, "server.address", DIRECTION_OUTBOUND); assertEquals(KnownTags.PEER_HOSTNAME_ID, tagId); assertTrue(pending.isEmpty()); // the older pending value is dropped @@ -117,7 +161,7 @@ void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, PendingAliases pending = pending(name, "pending"); // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all - assertEquals(result, DirectionalTags.prepareAliasWrite(tags, pending, name, direction)); + assertEquals(result, DirectionalTags.prepareAliasWrite(stores(tags), pending, name, direction)); assertEquals("pending", pending.get(name)); } @@ -127,7 +171,7 @@ void aTagWriteWithNoDirectionDropsEveryAliasThatCouldDenoteIt() { PendingAliases pending = pending("client.port", 1); pending.put("server.port", 2); - DirectionalTags.prepareWrite(tags, pending, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite(stores(tags), pending, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); assertTrue(pending.isEmpty()); } @@ -138,7 +182,7 @@ void aTagWriteWithADirectionDropsOnlyItsAliasThere() { PendingAliases pending = pending("client.port", 1); // no tag on a client span: kept pending.put("server.port", 2); - DirectionalTags.prepareWrite(tags, pending, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); + DirectionalTags.prepareWrite(stores(tags), pending, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); assertEquals(1, pending.get("client.port")); assertNull(pending.get("server.port")); @@ -149,8 +193,9 @@ void aTagWithoutAnAliasIsLeftAlone() { TagMap tags = TagMap.create(); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.prepareWrite(tags, pending, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); - DirectionalTags.prepareWrite(tags, pending, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); + DirectionalTags.prepareWrite( + stores(tags), pending, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); + DirectionalTags.prepareWrite(stores(tags), pending, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); assertEquals("pending", pending.get("server.address")); } @@ -161,7 +206,7 @@ void removingAnAliasRemovesItsTag() { tags.set(Tags.PEER_HOSTNAME, "stored"); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.remove(tags, pending, "server.address", DIRECTION_OUTBOUND); + DirectionalTags.remove(stores(tags), pending, "server.address", DIRECTION_OUTBOUND); assertNull(tags.getObject(Tags.PEER_HOSTNAME)); assertTrue(pending.isEmpty()); @@ -172,14 +217,15 @@ void removingATagRemovesItsAlias() { TagMap byName = TagMap.create(); byName.set(Tags.HTTP_HOSTNAME, "stored"); PendingAliases pendingByName = pending("server.address", "pending"); - DirectionalTags.remove(byName, pendingByName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); + DirectionalTags.remove(stores(byName), pendingByName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); assertNull(byName.getObject(Tags.HTTP_HOSTNAME)); assertTrue(pendingByName.isEmpty()); TagMap byId = TagMap.create(); byId.set(Tags.HTTP_HOSTNAME, "stored"); PendingAliases pendingById = pending("server.address", "pending"); - DirectionalTags.remove(byId, pendingById, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); + DirectionalTags.remove( + stores(byId), pendingById, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); assertTrue(pendingById.isEmpty()); } @@ -188,10 +234,10 @@ void removingATagRemovesItsAlias() { void removingAnAliasAlsoRemovesACopyEmittedEarlier() { TagMap tags = TagMap.create(); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.copyPending(pending, tags); + DirectionalTags.copyPending(pending, stores(tags)); assertEquals("pending", tags.getObject("server.address")); - DirectionalTags.remove(tags, pending, "server.address", DIRECTION_UNKNOWN); + DirectionalTags.remove(stores(tags), pending, "server.address", DIRECTION_UNKNOWN); assertNull(tags.getObject("server.address")); assertTrue(pending.isEmpty()); @@ -203,8 +249,8 @@ void removingATagWithNoAliasLeavesUnrelatedTags() { tags.set(Tags.PEER_HOSTNAME, "stored"); tags.set("my.tag", "custom"); - DirectionalTags.remove(tags, null, "my.tag", DIRECTION_OUTBOUND); - DirectionalTags.remove(tags, null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); + DirectionalTags.remove(stores(tags), null, "my.tag", DIRECTION_OUTBOUND); + DirectionalTags.remove(stores(tags), null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("my.tag")); @@ -222,7 +268,7 @@ void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String t tags.set(tag, "older"); tags.set("my.tag", "custom"); - assertNull(DirectionalTags.resolvePending(tags, pending(alias, "newer"), direction)); + assertNull(DirectionalTags.resolvePending(stores(tags), pending(alias, "newer"), direction)); assertEquals("newer", tags.getObject(tag)); assertNull(tags.getObject(alias)); @@ -235,13 +281,13 @@ void resolvingLeavesAnAliasWithNoTagPending() { PendingAliases pending = pending("client.port", 1234); pending.put("server.address", "pending"); - assertSame(pending, DirectionalTags.resolvePending(tags, pending, DIRECTION_OUTBOUND)); + assertSame(pending, DirectionalTags.resolvePending(stores(tags), pending, DIRECTION_OUTBOUND)); assertEquals(1234, pending.get("client.port")); assertNull(pending.get("server.address")); assertEquals("pending", tags.getObject(Tags.PEER_HOSTNAME)); PendingAliases unknown = pending("server.address", "pending"); - assertSame(unknown, DirectionalTags.resolvePending(tags, unknown, DIRECTION_UNKNOWN)); + assertSame(unknown, DirectionalTags.resolvePending(stores(tags), unknown, DIRECTION_UNKNOWN)); assertEquals("pending", unknown.get("server.address")); } @@ -250,7 +296,7 @@ void anAliasThatNeverResolvesIsEmittedUnderItsLiteralName() { TagMap tags = TagMap.create(); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.copyPending(pending, tags); + DirectionalTags.copyPending(pending, stores(tags)); assertEquals("pending", tags.getObject("server.address")); assertEquals(1, tags.size()); @@ -261,27 +307,30 @@ void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { TagMap tags = TagMap.create(); tags.set(Tags.PEER_HOSTNAME, "older"); - DirectionalTags.remove(tags, null, "server.address", DIRECTION_UNKNOWN); + DirectionalTags.remove(stores(tags), null, "server.address", DIRECTION_UNKNOWN); // once the span is a client, server.address names peer.hostname: the removal must stick - assertNull(DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, null, "server.address", DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(DirectionalTags.get(stores(tags), null, "server.address", DIRECTION_OUTBOUND)); } @Test void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { TagMap byName = TagMap.create(); PendingAliases pendingByName = pending("server.address", "pending"); - DirectionalTags.remove(byName, pendingByName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); + DirectionalTags.remove(stores(byName), pendingByName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); assertTrue(pendingByName.isEmpty()); - assertNull(DirectionalTags.get(byName, pendingByName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull( + DirectionalTags.get(stores(byName), pendingByName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); TagMap byId = TagMap.create(); PendingAliases pendingById = pending("server.address", "pending"); - DirectionalTags.remove(byId, pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + DirectionalTags.remove( + stores(byId), pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); assertTrue(pendingById.isEmpty()); assertNull( - DirectionalTags.get(byId, pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + DirectionalTags.get( + stores(byId), pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); } @Test @@ -290,10 +339,11 @@ void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { tags.set(Tags.PEER_HOSTNAME, "kept"); tags.set("my.tag", "gone"); - DirectionalTags.remove(tags, null, "my.tag", DIRECTION_UNKNOWN); + DirectionalTags.remove(stores(tags), null, "my.tag", DIRECTION_UNKNOWN); assertNull(tags.getObject("my.tag")); - assertEquals("kept", DirectionalTags.get(tags, null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertEquals( + "kept", DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); } @Test diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 55187bf9e9e..192a5db5e9c 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -3,10 +3,15 @@ @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) -# A span's tags, and the routing over them, skip checks the tracer core has already made; they are -# @Restricted to the core (see datadog.apicontract.Restricted). Instrumentation uses TagMap's -# checked methods. -@defaultMessage Tag routing is restricted to the tracer core; use TagMap's checked methods instead. +# A span's tag stores, the routing over them and the span's view of them skip checks the tracer +# core has already made; they are @Restricted to the core (see datadog.apicontract.Restricted). +# Instrumentation uses TagMap's checked methods. +@defaultMessage Tag stores are restricted to the tracer core; use TagMap's checked methods instead. +datadog.trace.api.HashStore +datadog.trace.api.IdStore datadog.trace.api.TagRouting datadog.trace.api.TagMap$DirectionSource -datadog.trace.api.TagMap#forSpan(datadog.trace.api.TagMap,datadog.trace.api.TagMap$DirectionSource) +datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,boolean,datadog.trace.api.TagMap$DirectionSource) +datadog.trace.api.TagMap#attachCustomStore(datadog.trace.api.HashStore,boolean) +datadog.trace.api.TagMap#customStore() +datadog.trace.api.TagMap#isCustomStoreBorrowed() diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index cb6a2c984ba..26427ae560f 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -42,7 +42,7 @@ final class HashIdStore extends IdStore { } @Override - Entry get(long tagId) { + public Entry get(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } @@ -62,50 +62,50 @@ private Entry getVisible(long tagId, long bit) { } @Override - Object getKnownValue(long tagId) { + public Object getKnownValue(long tagId) { Entry entry = this.get(tagId); return entry == null ? null : entry.objectValue(); } @Override - void set(long tagId, Object value) { + public void set(long tagId, Object value) { this.put(new Entry(tagId, null, Entry.ANY, 0L, value)); } @Override - void set(long tagId, CharSequence value) { + public void set(long tagId, CharSequence value) { this.put(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); } @Override - void set(long tagId, boolean value) { + public void set(long tagId, boolean value) { this.put( new Entry( tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); } @Override - void set(long tagId, int value) { + public void set(long tagId, int value) { this.put(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); } @Override - void set(long tagId, long value) { + public void set(long tagId, long value) { this.put(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); } @Override - void set(long tagId, float value) { + public void set(long tagId, float value) { this.put(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); } @Override - void set(long tagId, double value) { + public void set(long tagId, double value) { this.put(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } @Override - Entry put(Entry knownEntry) { + public Entry put(Entry knownEntry) { Entry replaced = HashSupport.put(this.buckets, knownEntry); this.occupancy |= slotBit(knownEntry.tagHash); if (replaced == null || replaced.tombstone) { @@ -116,7 +116,7 @@ Entry put(Entry knownEntry) { } @Override - Entry getAndPut(Entry knownEntry) { + public Entry getAndPut(Entry knownEntry) { long tagId = knownEntry.tagHash; long bit = slotBit(tagId); Entry replaced = HashSupport.put(this.buckets, knownEntry); @@ -132,7 +132,7 @@ Entry getAndPut(Entry knownEntry) { } @Override - Entry remove(long tagId) { + public Entry remove(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 208a54731ec..718917a4536 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -1,5 +1,6 @@ package datadog.trace.api; +import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.BucketGroup; import datadog.trace.api.TagMap.Entry; import datadog.trace.api.TagMap.EntryReader; @@ -16,10 +17,12 @@ * custom tag names (asserted): a known tag is resolved to its id first, and goes to the {@link * IdStore}. * - *

Created lazily by its owner on the first custom write; until then the owner borrows its - * parent's custom store, if any, read-only. No locking: guarded by the owner's lock. + *

Created lazily by its owner -- a {@link TagMap}, or a span in the tracer core -- on the first + * custom write ({@link TagRouting#newCustomStore}); until then the owner borrows its parent's + * custom store, if any, read-only. No locking: guarded by the owner's lock. */ -final class HashStore { +@Restricted(allowedIn = {"datadog.trace.core"}) +public final class HashStore { final Object[] buckets; /** Local entries. */ @@ -87,7 +90,7 @@ int estimateSize() { } /** The custom tag {@code tag}'s entry, reading through to the parent chain. */ - Entry get(String tag) { + public Entry get(String tag) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; look it up by its id"; return this.get(tag, Entry.bucketHash(Entry.customHash(tag))); } @@ -111,31 +114,31 @@ private Entry get(String tag, int hash) { * Sets the custom tag {@code tag}, which must not be a known tag's name. */ - void set(String tag, Object value) { + public void set(String tag, Object value) { this.put(Entry.newAnyEntry(tag, value)); } - void set(String tag, CharSequence value) { + public void set(String tag, CharSequence value) { this.put(Entry.newObjectEntry(tag, value)); } - void set(String tag, boolean value) { + public void set(String tag, boolean value) { this.put(Entry.newBooleanEntry(tag, value)); } - void set(String tag, int value) { + public void set(String tag, int value) { this.put(Entry.newIntEntry(tag, value)); } - void set(String tag, long value) { + public void set(String tag, long value) { this.put(Entry.newLongEntry(tag, value)); } - void set(String tag, float value) { + public void set(String tag, float value) { this.put(Entry.newFloatEntry(tag, value)); } - void set(String tag, double value) { + public void set(String tag, double value) { this.put(Entry.newDoubleEntry(tag, value)); } @@ -143,7 +146,7 @@ void set(String tag, double value) { * Inserts or replaces a custom tag's entry, returning the local entry it replaced (or null). Does * not consult the read-through parent. */ - Entry put(Entry newEntry) { + public Entry put(Entry newEntry) { // 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) { @@ -157,7 +160,7 @@ Entry put(Entry newEntry) { } /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - Entry getAndPut(Entry newEntry) { + public Entry getAndPut(Entry newEntry) { // Capture whether the key was tombstoned BEFORE put clears it: a tombstoned key had no visible // prior value (it was removed), so getAndPut must report null rather than the parent's. boolean wasTombstoned = @@ -177,7 +180,7 @@ Entry getAndPut(Entry newEntry) { * Removes the custom tag {@code tag}, returning the prior visible entry. Removing a key the * parent chain still exposes also installs a tombstone, so it stops reading through. */ - Entry remove(String tag) { + public Entry remove(String tag) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; remove it by its id"; int hash = Entry.bucketHash(Entry.customHash(tag)); Entry localRemoved = HashSupport.remove(this.buckets, hash, tag); diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index eb12e4c3af9..3d28b1920ba 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -1,5 +1,6 @@ package datadog.trace.api; +import datadog.apicontract.Restricted; import datadog.trace.api.TagMap.Entry; import datadog.trace.api.TagMap.EntryReader; import datadog.trace.api.function.TriConsumer; @@ -12,6 +13,9 @@ * built from one -- never a name, so nothing here can be handed a name that a caller has not yet * resolved. * + *

A span in the tracer core owns its id store directly ({@link TagRouting#newIdStore}); a {@link + * TagMap} owns one too. + * *

Each implementation is its own class: {@link HashIdStore} today, a dense store later. Nothing * that is always loaded may extend this class, so that a call through it stays monomorphic. No * locking: guarded by the owner's lock. @@ -21,7 +25,8 @@ * tags that don't usually co-occur share a slot -- so a clear bit is a definite miss and a set bit * means "probe". The masks are never cleared by a removal, only widened. */ -abstract class IdStore { +@Restricted(allowedIn = {"datadog.trace.core"}) +public abstract class IdStore { /** Slots that may hold a live local entry. */ long occupancy; @@ -67,40 +72,40 @@ static IllegalArgumentException notKnown(long tagId) { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - abstract Entry get(long tagId); + public abstract Entry get(long tagId); /** * {@code get(tagId)}'s value, boxed if necessary, or null -- for a caller that needs only the * value, so a store that keeps known tags without an Entry per tag can answer without one. */ - abstract Object getKnownValue(long tagId); + public abstract Object getKnownValue(long tagId); /* * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. */ - abstract void set(long tagId, Object value); + public abstract void set(long tagId, Object value); - abstract void set(long tagId, CharSequence value); + public abstract void set(long tagId, CharSequence value); - abstract void set(long tagId, boolean value); + public abstract void set(long tagId, boolean value); - abstract void set(long tagId, int value); + public abstract void set(long tagId, int value); - abstract void set(long tagId, long value); + public abstract void set(long tagId, long value); - abstract void set(long tagId, float value); + public abstract void set(long tagId, float value); - abstract void set(long tagId, double value); + public abstract void set(long tagId, double value); /** * Inserts or replaces a known tag's entry, returning the local entry it replaced (or null). Does * not consult the read-through parent. */ - abstract Entry put(Entry knownEntry); + public abstract Entry put(Entry knownEntry); /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - abstract Entry getAndPut(Entry knownEntry); + public abstract Entry getAndPut(Entry knownEntry); /** * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is @@ -108,7 +113,7 @@ static IllegalArgumentException notKnown(long tagId) { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - abstract Entry remove(long tagId); + public abstract Entry remove(long tagId); /* * Whole-store operations, for the owning TagMap. 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 cc54e3bbdab..d80b7201923 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1254,7 +1254,7 @@ public EntryChange next() { boolean namesBorrowed; /** - * For a span's tags ({@link #forSpan}), the span, which says what direction-scoped names read as; + * For a span's view ({@link #viewOf}), the span, which says what direction-scoped names read as; * null for a TagMap of its own, which reads every name as written. */ final DirectionSource direction; @@ -1289,12 +1289,17 @@ private TagMap(TagMap parent, DirectionSource direction) { this.parent = parent; } - /** A copy over {@code ids} and {@code names}; see {@link #copy()}. */ - private TagMap(TagMap parent, IdStore ids, HashStore names, boolean namesBorrowed) { + /** A map over {@code ids} and {@code names}: a copy ({@link #copy()}) or a view. */ + private TagMap( + TagMap parent, + IdStore ids, + HashStore names, + boolean namesBorrowed, + DirectionSource direction) { this.ids = ids; this.names = names; this.namesBorrowed = namesBorrowed; - this.direction = null; + this.direction = direction; this.frozen = false; this.parent = parent; } @@ -1321,24 +1326,37 @@ public interface DirectionSource { } /** - * A span's tags: a mutable TagMap reading through to {@code parent}, as {@link #createFromParent} - * builds, whose name-keyed reads honor the span's direction. The span routes to it with {@link - * TagRouting} under its own lock, and hands it to its internal readers (tag post-processors, - * serializers); it is not thread-safe. + * A live view over a span's stores, for the span's internal readers (tag post-processors, + * serializers) and its snapshot copy; never on the span's own set/get paths. Name-keyed reads + * honor the span's direction. Not thread-safe: use it under the span's tag lock. * - * @throws IllegalStateException if {@code parent} is not frozen + *

The span owns the stores. Before each use it hands the view its current custom store ({@link + * #attachCustomStore}), which it creates lazily; after, it adopts any the view created ({@link + * #customStore}), since a reader may write a custom tag. */ @Restricted(allowedIn = {"datadog.trace.core"}) - public static TagMap forSpan(TagMap parent, DirectionSource direction) { - if (parent != null) { - if (!parent.frozen) { - throw new IllegalStateException("read-through parent must be frozen"); - } - if (parent.isEmpty()) { - parent = null; - } - } - return new TagMap(parent, direction); + public static TagMap viewOf( + IdStore ids, HashStore names, boolean namesBorrowed, DirectionSource direction) { + return new TagMap(null, ids, names, namesBorrowed, direction); + } + + /** Points this view at its span's current custom store; see {@link #viewOf}. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public void attachCustomStore(HashStore names, boolean namesBorrowed) { + this.names = names; + this.namesBorrowed = namesBorrowed; + } + + /** This map's custom store: null, borrowed ({@link #isCustomStoreBorrowed}), or its own. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public HashStore customStore() { + return this.names; + } + + /** Whether {@link #customStore} is the parent's, borrowed read-only. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public boolean isCustomStoreBorrowed() { + return this.namesBorrowed; } /** The custom store to write to: created on the first write, ending any borrowing. */ @@ -1510,10 +1528,10 @@ public Entry getEntry(String tag) { long key = TagRouting.keyOf(tag); DirectionSource direction = this.direction; if (direction == null || key == 0) { - return TagRouting.get(this, key, tag); + return TagRouting.get(this.ids, this.names, key, tag); } return TagRouting.getEntry( - this, key, tag, direction.tagDirection(), direction.hasPendingAliases()); + this.ids, this.names, key, tag, direction.tagDirection(), direction.hasPendingAliases()); } /** The entry for a known tag id. */ @@ -1718,14 +1736,14 @@ public void setResolved(long tagId, @Nonnull String tag, double value) { /** The read counterpart of {@link #setResolved(long, String, Object)}. */ public Entry getEntryResolved(long tagId, String tag) { - return TagRouting.get(this, tagId, tag); + return TagRouting.get(this.ids, this.names, tagId, tag); } /** The removal counterpart of {@link #setResolved(long, String, Object)}. */ public Entry removeResolved(long tagId, String tag) { this.checkWriteAccess(); - return TagRouting.remove(this, tagId, tag); + return TagRouting.isKnown(tagId) ? this.ids.remove(tagId) : this.removeCustom(tag); } /* @@ -1779,7 +1797,9 @@ public Entry getAndSet(@Nullable Entry newEntry) { } this.checkWriteAccess(); - return TagRouting.getAndPut(this, newEntry); + return newEntry.tagId() != 0 + ? this.ids.getAndPut(newEntry) + : this.namesForWrite().getAndPut(newEntry); } /** @@ -1791,7 +1811,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); - return TagRouting.put(this, newEntry); + return newEntry.tagId() != 0 ? this.ids.put(newEntry) : this.namesForWrite().put(newEntry); } /** {@link #putEntry} for an entry known to be a known tag's. */ @@ -1925,7 +1945,8 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. - return TagRouting.remove(this, TagRouting.keyOf(tag), tag); + long key = TagRouting.keyOf(tag); + return TagRouting.isKnown(key) ? this.ids.remove(key) : this.removeCustom(tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ @@ -1942,7 +1963,11 @@ public TagMap copy() { HashStore names = this.names; boolean borrowed = this.namesBorrowed; return new TagMap( - this.parent, this.ids.copy(), names == null || borrowed ? names : names.copy(), borrowed); + this.parent, + this.ids.copy(), + names == null || borrowed ? names : names.copy(), + borrowed, + null); } public TagMap immutableCopy() { diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index 76c83b306d0..340f6adda0d 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -6,19 +6,20 @@ import datadog.trace.api.TagMap.Entry; /** - * Where a tag name is resolved and an operation is routed to the store of a {@link TagMap} that - * holds it: a known tag to its {@link IdStore} by id, a custom tag to its {@link HashStore} by name - * (created on the first custom write). The one place a name becomes an id, so the stores themselves - * never see a name that might be a known tag's. Shared by TagMap and by the tracer core's spans, - * which hold their tags in a TagMap and route to it directly, skipping the checks TagMap's public - * methods make. + * Where a tag name is resolved and a read is routed to the store that holds it: a known tag to an + * {@link IdStore} by id, a custom tag to a {@link HashStore} by name. The one place a name becomes + * an id, so the stores themselves never see a name that might be a known tag's. Shared by {@link + * TagMap} and by the tracer core's spans, which own their stores directly. + * + *

A custom store may be null: its owner creates it on the first custom write ({@link + * #newCustomStore}), and until then may borrow its parent's. Writes and removals of custom tags are + * the owner's, which alone can create its store. * *

A name resolves once, by {@link #keyOf}, to a key: a known tag's id ({@code > 0}), 0 for a * custom tag, or a negative sentinel for a direction-scoped OpenTelemetry alias ({@code * server.address}), whose tag depends on the span's direction. An alias is the caller's to decide: * the key-taking methods treat any key that is not an id as custom, which is what a TagMap does - * with an alias, having no direction. {@link #getEntry(TagMap, long, String, int, boolean)} reads - * an alias by a span's direction. + * with an alias, having no direction. */ @Restricted(allowedIn = {"datadog.trace.core"}) public final class TagRouting { @@ -35,165 +36,60 @@ public static boolean isKnown(long key) { } /* - * By id. + * Stores for an owner reading through to a frozen parent TagMap. */ - /** The known tag {@code tagId}'s entry; throws for an id that names no known tag. */ - public static Entry getById(TagMap tags, long tagId) { - return tags.ids.get(tagId); - } - - /** {@link #getById}'s value, or null. */ - public static Object getKnownValue(TagMap tags, long tagId) { - return tags.ids.getKnownValue(tagId); - } - - /** Removes the known tag {@code tagId}; throws for an id that names no known tag. */ - public static Entry removeById(TagMap tags, long tagId) { - return tags.ids.remove(tagId); - } - - /* - * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. + /** + * The parent an owner reads through to: {@code parent}, or null when it is null or empty, as + * {@link TagMap#createFromParent} decides. + * + * @throws IllegalStateException if {@code parent} is not frozen */ - - public static void setKnown(TagMap tags, long tagId, Object value) { - tags.ids.set(tagId, value); - } - - public static void setKnown(TagMap tags, long tagId, CharSequence value) { - tags.ids.set(tagId, value); - } - - public static void setKnown(TagMap tags, long tagId, boolean value) { - tags.ids.set(tagId, value); - } - - public static void setKnown(TagMap tags, long tagId, int value) { - tags.ids.set(tagId, value); - } - - public static void setKnown(TagMap tags, long tagId, long value) { - tags.ids.set(tagId, value); - } - - public static void setKnown(TagMap tags, long tagId, float value) { - tags.ids.set(tagId, value); + public static TagMap readThroughParent(TagMap parent) { + if (parent == null) { + return null; + } + if (!parent.isFrozen()) { + throw new IllegalStateException("read-through parent must be frozen"); + } + return parent.isEmpty() ? null : parent; } - public static void setKnown(TagMap tags, long tagId, double value) { - tags.ids.set(tagId, value); + /** A fresh id store reading through to {@code parent}'s known tags (parent may be null). */ + public static IdStore newIdStore(TagMap parent) { + return new HashIdStore(parent == null ? null : parent.ids); } - /* - * Custom tags, by name: never a known tag's name. + /** + * {@code parent}'s custom store, for an owner to borrow read-only until its first custom write, + * or null. */ - - /** The custom tag {@code tag}'s entry, reading through to parents, or null. */ - public static Entry getCustom(TagMap tags, String tag) { - HashStore names = tags.names; - return names == null ? null : names.get(tag); - } - - /** Removes the custom tag {@code tag}, returning the prior visible entry. */ - public static Entry removeCustom(TagMap tags, String tag) { - return tags.removeCustom(tag); - } - - /** Sets the custom tag {@code tag}. */ - public static void setCustom(TagMap tags, String tag, Object value) { - tags.namesForWrite().set(tag, value); + public static HashStore inheritedCustomStore(TagMap parent) { + return parent == null ? null : parent.names; } - /* - * By entry, which already carries its id or its name. + /** + * A fresh custom store for an owner's first custom write, reading through to {@code borrowed}, + * the store it borrowed until now (may be null). */ - - /** Puts {@code entry} into the store for its kind, returning the local entry it replaced. */ - public static Entry put(TagMap tags, Entry entry) { - return entry.tagId() != 0 ? tags.ids.put(entry) : tags.namesForWrite().put(entry); - } - - /** {@link #put}, returning the prior visible entry. */ - public static Entry getAndPut(TagMap tags, Entry entry) { - return entry.tagId() != 0 ? tags.ids.getAndPut(entry) : tags.namesForWrite().getAndPut(entry); + public static HashStore newCustomStore(HashStore borrowed) { + return new HashStore(borrowed); } /* - * By name, already resolved to its key by keyOf. A key that is not an id is a custom tag. + * Reads, by name already resolved to its key by keyOf. A key that is not an id is a custom tag. */ /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ - public static Entry get(TagMap tags, long key, String tag) { - return isKnown(key) ? tags.ids.get(key) : getCustom(tags, tag); - } - - /** Removes {@code tag}, already resolved to {@code key}, returning the prior visible entry. */ - public static Entry remove(TagMap tags, long key, String tag) { - return isKnown(key) ? tags.ids.remove(key) : tags.removeCustom(tag); - } - - public static void set(TagMap tags, long key, String tag, Object value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } - } - - public static void set(TagMap tags, long key, String tag, CharSequence value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } - } - - public static void set(TagMap tags, long key, String tag, boolean value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } - } - - public static void set(TagMap tags, long key, String tag, int value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } - } - - public static void set(TagMap tags, long key, String tag, long value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } - } - - public static void set(TagMap tags, long key, String tag, float value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } - } - - public static void set(TagMap tags, long key, String tag, double value) { - if (isKnown(key)) { - tags.ids.set(key, value); - } else { - tags.namesForWrite().set(tag, value); - } + public static Entry get(IdStore ids, HashStore names, long key, String tag) { + return isKnown(key) ? ids.get(key) : names == null ? null : names.get(tag); } /* * By name, on a span of a direction. A direction-scoped alias reads the tag it denotes, and a tag * reads a pending alias stored as written in the custom store, which, being the newer write, - * wins. (A span keeps its pending aliases apart, and copies them into the custom store before - * handing its TagMap to a reader that uses this.) + * wins. (A span keeps its pending aliases apart, and copies them into its custom store before + * handing its tags to a reader that uses this.) */ /** @@ -202,30 +98,36 @@ public static void set(TagMap tags, long key, String tag, double value) { * single lookup like any other tag. */ public static Entry getEntry( - TagMap tags, long key, String name, int direction, boolean mayHavePendingAliases) { + IdStore ids, + HashStore names, + long key, + String name, + int direction, + boolean mayHavePendingAliases) { if (isKnown(key)) { - return getById(tags, key, direction, mayHavePendingAliases); + return getById(ids, names, key, direction, mayHavePendingAliases); } - Entry asWritten = key == 0 || mayHavePendingAliases ? getCustom(tags, name) : null; + Entry asWritten = (key == 0 || mayHavePendingAliases) && names != null ? names.get(name) : null; if (key == 0 || asWritten != null) { return asWritten; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return isKnown(tagId) ? tags.ids.get(tagId) : null; + return isKnown(tagId) ? ids.get(tagId) : null; } /** The entry the known tag {@code tagId} reads on a span of {@code direction}; see above. */ public static Entry getById( - TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { + IdStore ids, HashStore names, long tagId, int direction, boolean mayHavePendingAliases) { if (mayHavePendingAliases + && names != null && KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - Entry pending = alias == null ? null : getCustom(tags, alias); + Entry pending = alias == null ? null : names.get(alias); if (pending != null) { return pending; } } - return tags.ids.get(tagId); + return ids.get(tagId); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 8374e98bd6d..5d00876d43a 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -212,100 +212,88 @@ void anIteratorWalksBothStoresAndTheirParents() { } @Test - void routingSendsKnownTagsByIdAndEverythingElseByName() { - TagMap tags = TagMap.create(); + void routingReadsKnownTagsByIdAndEverythingElseByName() { + IdStore ids = TagRouting.newIdStore(null); + HashStore names = TagRouting.newCustomStore(null); long route = TagRouting.keyOf(KnownTags.HTTP_ROUTE_NAME); long alias = TagRouting.keyOf("server.address"); assertEquals(KnownTags.HTTP_ROUTE_ID, route); assertEquals(0L, TagRouting.keyOf("my.tag")); assertTrue(alias < 0); + assertTrue(TagRouting.isKnown(route)); + assertFalse(TagRouting.isKnown(alias)); - TagRouting.set(tags, route, KnownTags.HTTP_ROUTE_NAME, (Object) "/a"); - TagRouting.set(tags, 0L, "my.tag", (Object) "custom"); - TagRouting.set(tags, alias, "server.address", (Object) "as-written"); - - assertEquals("/a", TagRouting.get(tags, route, KnownTags.HTTP_ROUTE_NAME).stringValue()); - assertEquals("/a", TagRouting.getKnownValue(tags, KnownTags.HTTP_ROUTE_ID)); - assertEquals("custom", TagRouting.getCustom(tags, "my.tag").stringValue()); - assertEquals("as-written", TagRouting.getCustom(tags, "server.address").stringValue()); + ids.set(route, "/a"); + names.set("my.tag", "custom"); + names.set("server.address", "as-written"); - assertNotNull(TagRouting.remove(tags, route, KnownTags.HTTP_ROUTE_NAME)); - assertNotNull(TagRouting.removeCustom(tags, "my.tag")); - assertNull(TagRouting.getById(tags, KnownTags.HTTP_ROUTE_ID)); - assertNull(TagRouting.removeById(tags, KnownTags.HTTP_ROUTE_ID)); - assertNull(TagRouting.remove(tags, 0L, "my.tag")); + assertEquals("/a", TagRouting.get(ids, names, route, KnownTags.HTTP_ROUTE_NAME).stringValue()); + assertEquals("custom", TagRouting.get(ids, names, 0L, "my.tag").stringValue()); + assertEquals("as-written", TagRouting.get(ids, names, alias, "server.address").stringValue()); + assertNull(TagRouting.get(ids, null, 0L, "my.tag")); // no custom store yet } @Test - void routingSetsEveryValueShape() { - TagMap tags = TagMap.create(); - TagRouting.setKnown(tags, KnownTags.COMPONENT_ID, (Object) "jdbc"); - TagRouting.setKnown(tags, KnownTags.HTTP_ROUTE_ID, "/a"); - TagRouting.setKnown(tags, KnownTags.DD_PROFILING_ENABLED_ID, true); - TagRouting.setKnown(tags, KnownTags.HTTP_STATUS_CODE_ID, 200); - TagRouting.setKnown(tags, KnownTags.HTTP_RESEND_COUNT_ID, 2L); - TagRouting.setKnown(tags, KnownTags.DB_POOL_NAME_ID, 1.5f); - TagRouting.setKnown(tags, KnownTags.DB_INSTANCE_ID, 2.25d); - TagRouting.set(tags, KnownTags.HTTP_METHOD_ID, "x", "GET"); - TagRouting.set(tags, KnownTags.DD_PROFILING_ENABLED_ID, "x", false); - TagRouting.set(tags, KnownTags.HTTP_STATUS_CODE_ID, "x", 201); - TagRouting.set(tags, KnownTags.HTTP_RESEND_COUNT_ID, "x", 3L); - TagRouting.set(tags, KnownTags.DB_POOL_NAME_ID, "x", 2.5f); - TagRouting.set(tags, KnownTags.DB_INSTANCE_ID, "x", 3.25d); - TagRouting.set(tags, 0L, "s", "v"); - TagRouting.set(tags, 0L, "b", true); - TagRouting.set(tags, 0L, "i", 1); - TagRouting.set(tags, 0L, "l", 2L); - TagRouting.set(tags, 0L, "f", 3f); - TagRouting.set(tags, 0L, "d", 4d); - TagRouting.setCustom(tags, "o", "obj"); - - assertEquals(201, tags.getObject(KnownTags.HTTP_STATUS_CODE_NAME)); - assertEquals(false, tags.getObject(KnownTags.DD_PROFILING_ENABLED_NAME)); - assertEquals(3.25d, tags.getObject(KnownTags.DB_INSTANCE_NAME)); - assertEquals(4d, tags.getObject("d")); - assertEquals("obj", tags.getObject("o")); - assertEquals(15, tags.size()); + void anOwnerReadsThroughItsParentsStores() { + assertNull(TagRouting.readThroughParent(null)); + assertThrows(IllegalStateException.class, () -> TagRouting.readThroughParent(TagMap.create())); + assertNull(TagRouting.readThroughParent(TagMap.create().freeze())); // empty parent dropped + + TagMap parent = TagRouting.readThroughParent(frozenParent()); + IdStore ids = TagRouting.newIdStore(parent); + HashStore borrowed = TagRouting.inheritedCustomStore(parent); + assertSame(parent.names, borrowed); + assertEquals("/inherited", ids.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("c", borrowed.get("inherited.custom").stringValue()); + + HashStore own = TagRouting.newCustomStore(borrowed); + own.set("own", "o"); + assertEquals("c", own.get("inherited.custom").stringValue()); + assertNull(borrowed.get("own")); } @Test - void routingPutsEntriesByTheirKind() { - TagMap tags = TagMap.create(); - assertNull(TagRouting.put(tags, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/a"))); - assertNull(TagRouting.getAndPut(tags, TagMap.Entry.create("my.tag", "x"))); - assertEquals( - "/a", - TagRouting.getAndPut(tags, TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, "/b")) - .stringValue()); - assertEquals("x", TagRouting.put(tags, TagMap.Entry.create("my.tag", "y")).stringValue()); + void theCustomStoreSetsEveryValueShape() { + HashStore names = TagRouting.newCustomStore(null); + names.set("o", (Object) "obj"); + names.set("s", "v"); + names.set("b", true); + names.set("i", 1); + names.set("l", 2L); + names.set("f", 3f); + names.set("d", 4d); + assertNull(names.getAndPut(TagMap.Entry.create("p", "x"))); + assertEquals("x", names.put(TagMap.Entry.create("p", "y")).stringValue()); + + assertEquals(true, names.get("b").objectValue()); + assertEquals(4d, names.get("d").objectValue()); + assertEquals(8, names.size()); } @Test - void aSpansTagsReadAnAliasByItsDirection() { - TagMap inbound = TagMap.forSpan(null, direction(DIRECTION_INBOUND, false)); - TagMap outbound = TagMap.forSpan(null, direction(DIRECTION_OUTBOUND, false)); - for (TagMap tags : new TagMap[] {inbound, outbound}) { - tags.set(KnownTags.HTTP_HOSTNAME_ID, "inbound.example"); - tags.set(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); - } - TagMap plain = TagMap.create(); - plain.set(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); + void aViewReadsAnAliasByTheSpansDirection() { + IdStore ids = TagRouting.newIdStore(null); + ids.set(KnownTags.HTTP_HOSTNAME_ID, "inbound.example"); + ids.set(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); + TagMap inbound = TagMap.viewOf(ids, null, false, direction(DIRECTION_INBOUND, false)); + TagMap outbound = TagMap.viewOf(ids, null, false, direction(DIRECTION_OUTBOUND, false)); + TagMap plain = TagMap.viewOf(ids, null, false, null); assertEquals("inbound.example", inbound.getString("server.address")); assertEquals("outbound.example", outbound.getString("server.address")); - assertNull(plain.getString("server.address")); // a TagMap of its own reads names as written + assertNull(plain.getString("server.address")); // with no direction, names read as written assertEquals("inbound.example", outbound.getString(KnownTags.HTTP_HOSTNAME_NAME)); } @Test - void aSpansTagsReadAnEmittedPendingAliasOverItsTag() { - TagMap pending = TagMap.forSpan(null, direction(DIRECTION_OUTBOUND, true)); - TagMap noPending = TagMap.forSpan(null, direction(DIRECTION_OUTBOUND, false)); - TagMap unknown = TagMap.forSpan(null, direction(DIRECTION_UNKNOWN, true)); - for (TagMap tags : new TagMap[] {pending, noPending, unknown}) { - tags.set(KnownTags.PEER_HOSTNAME_ID, "older"); - TagRouting.setCustom(tags, "server.address", "pending"); - } + void aViewReadsAnEmittedPendingAliasOverItsTag() { + IdStore ids = TagRouting.newIdStore(null); + ids.set(KnownTags.PEER_HOSTNAME_ID, "older"); + HashStore names = TagRouting.newCustomStore(null); + names.set("server.address", "pending"); + TagMap pending = TagMap.viewOf(ids, names, false, direction(DIRECTION_OUTBOUND, true)); + TagMap noPending = TagMap.viewOf(ids, names, false, direction(DIRECTION_OUTBOUND, false)); + TagMap unknown = TagMap.viewOf(ids, names, false, direction(DIRECTION_UNKNOWN, true)); assertEquals("pending", pending.getString(KnownTags.PEER_HOSTNAME_NAME)); assertEquals("pending", pending.getString("server.address")); @@ -313,18 +301,32 @@ void aSpansTagsReadAnEmittedPendingAliasOverItsTag() { assertEquals("older", unknown.getString(KnownTags.PEER_HOSTNAME_NAME)); assertEquals("pending", unknown.getString("server.address")); assertNull(noPending.getString("client.port")); // no tag in this direction + // with no custom store, the alias reads the tag it denotes + assertEquals( + "older", + TagMap.viewOf(ids, null, false, direction(DIRECTION_OUTBOUND, true)) + .getString("server.address")); } @Test - void forSpanMirrorsCreateFromParent() { - assertThrows( - IllegalStateException.class, - () -> TagMap.forSpan(TagMap.create(), direction(DIRECTION_UNKNOWN, false))); - assertNull(TagMap.forSpan(TagMap.create().freeze(), null).parent); // empty parent dropped - - TagMap span = TagMap.forSpan(frozenParent(), direction(DIRECTION_UNKNOWN, false)); - assertEquals("/inherited", span.getString(KnownTags.HTTP_ROUTE_NAME)); - assertEquals(3, span.size()); + void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { + TagMap parent = TagRouting.readThroughParent(frozenParent()); + IdStore ids = TagRouting.newIdStore(parent); + HashStore borrowed = TagRouting.inheritedCustomStore(parent); + TagMap view = TagMap.viewOf(ids, borrowed, true, direction(DIRECTION_UNKNOWN, false)); + assertSame(borrowed, view.customStore()); + assertTrue(view.isCustomStoreBorrowed()); + + view.set(KnownTags.HTTP_METHOD_NAME, "GET"); + view.set("own", "o"); + assertEquals("GET", ids.getKnownValue(KnownTags.HTTP_METHOD_ID)); + assertFalse(view.isCustomStoreBorrowed()); + assertNotSame(borrowed, view.customStore()); + assertEquals("o", view.customStore().get("own").stringValue()); + + view.attachCustomStore(null, false); + assertNull(view.getString("own")); + assertNull(view.customStore()); } @Test From cd501d56e964a2381fd824d45bba884d7119e2aa Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 15:18:51 -0400 Subject: [PATCH 65/99] Note why the span's custom-store flag needs no volatile Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/core/DDSpanContext.java | 4 ++++ 1 file changed, 4 insertions(+) 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 110c45a4c77..34d0c5d3aed 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 @@ -47,6 +47,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; @@ -1030,6 +1031,9 @@ private void adoptViewStores(TagMap view) { } /** The custom store to write to: created on the first custom write, ending any borrowing. */ + @SuppressFBWarnings( + value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", + justification = "Written and read only under the tagIds lock") private HashStore customTagsForWrite() { HashStore custom = this.customTags; if (custom == null || this.customTagsBorrowed) { From 08d769f36af7d32965711454047212565cf505e1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 15:26:15 -0400 Subject: [PATCH 66/99] Register the tracer's root-span tags as known tags process_id, _dd.trace_span_attribute_schema and _dd.agent_psr are now declared at the trace level, beside language and runtime-id, with the types the tracer writes them with (long, int and double). Every local root span carries them, so as custom tags they made each root span create a custom tag store; as known tags they go to its id store. Co-Authored-By: Claude Opus 5.5 --- tag-conventions.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tag-conventions.yaml b/tag-conventions.yaml index b54462a6a62..3a37a230339 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -67,6 +67,11 @@ trace_level: - { dd-name: env, type: string, required: recommended } - { dd-name: language, type: string, required: required } - { dd-name: runtime-id, type: string, required: required } + # tracer-internal, set on the local root span only: the process id, the span attribute schema + # version, and the agent-provided sampling rate the sampler applied + - { dd-name: process_id, type: long, required: recommended } + - { dd-name: _dd.trace_span_attribute_schema, type: int, required: recommended } + - { dd-name: _dd.agent_psr, type: double, required: optional } - { dd-name: _dd.tracer_host, type: string, required: recommended } - { dd-name: _dd.git.commit.sha, type: string, required: recommended } - { dd-name: _dd.git.repository_url, type: string, required: recommended } From aa324bebb7201aa7a42e23bfdbc0c71663f06a78 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 16:03:27 -0400 Subject: [PATCH 67/99] Pass DirectionalTags the span's stores instead of the span DDSpanContext no longer implements a store interface for DirectionalTags or exposes store operations publicly. DirectionalTags now takes the span's id store and its custom store, which may be null or borrowed from the read-through parent. It never creates the custom store: a removal that would have to hide a tag in a borrowed store leaves it untouched and returns true, and the span creates its own store and repeats the removal, which is idempotent. An alias write that resolves to its tag now leaves removing the alias from the custom store to the span. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 97 +++---- .../datadog/trace/core/DirectionalTags.java | 179 +++++++----- .../trace/core/DirectionalTagsTest.java | 266 ++++++++++-------- 3 files changed, 302 insertions(+), 240 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 34d0c5d3aed..b472ebd8012 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 @@ -76,8 +76,7 @@ public class DDSpanContext RequestContext, TraceSegment, ProfilerContext, - TagMap.DirectionSource, - DirectionalTags.TagStores { + TagMap.DirectionSource { private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class); public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -986,7 +985,9 @@ public void setSpanKindOrdinal(String kind) { // The span now has a direction: fold pending aliases into the tags they denote, so every // later reader (peer service naming, stats, sampling rules, serialization) sees them // resolved. Once none is left, the span is back on the fast path. - pendingAliases = DirectionalTags.resolvePending(this, pending, tagDirection()); + pendingAliases = + DirectionalTags.resolvePending( + tagIds, customTagsBorrowed ? null : customTags, pending, tagDirection()); } } } @@ -1000,7 +1001,7 @@ public void setSpanKindOrdinal(String kind) { private TagMap emittableTags() { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null) { - DirectionalTags.copyPending(pending, this); + DirectionalTags.copyPending(pending, customTagsForWrite()); } return tagView(); } @@ -1044,39 +1045,10 @@ private HashStore customTagsForWrite() { return custom; } - /* - * DirectionalTags.TagStores: the span's stores, for DirectionalTags. Call under the tagIds lock. + /** + * Removes the custom tag {@code name}: hiding an inherited one needs a store of the span's own. */ - - @Override - public Object knownValue(long tagId) { - return tagIds.getKnownValue(tagId); - } - - @Override - public void setKnown(long tagId, Object value) { - tagIds.set(tagId, value); - } - - @Override - public void removeKnown(long tagId) { - tagIds.remove(tagId); - } - - @Override - public Object customValue(String name) { - HashStore custom = this.customTags; - TagMap.Entry entry = custom == null ? null : custom.get(name); - return entry == null ? null : entry.objectValue(); - } - - @Override - public void setCustom(String name, Object value) { - customTagsForWrite().set(name, value); - } - - @Override - public void removeCustom(String name) { + private void removeCustomTag(String name) { HashStore custom = this.customTags; if (custom == null) { return; @@ -1085,11 +1057,36 @@ public void removeCustom(String name) { if (custom.get(name) == null) { return; // nothing to hide: keep borrowing } - custom = customTagsForWrite(); // hiding an inherited tag needs a tombstone of our own + custom = customTagsForWrite(); } custom.remove(name); } + /** + * {@link DirectionalTags#remove(IdStore, HashStore, boolean, DirectionalTags.PendingAliases, + * String, int)} on this span's stores. Call under the {@code tagIds} lock. + */ + private void removeDirectional(String tag) { + int direction = tagDirection(); + if (DirectionalTags.remove( + tagIds, customTags, customTagsBorrowed, pendingAliases, tag, direction)) { + customTagsForWrite(); // a borrowed tag to hide: repeat on a store of the span's own + DirectionalTags.remove(tagIds, customTags, false, pendingAliases, tag, direction); + } + dropPendingAliasesIfEmpty(); + } + + /** {@link #removeDirectional(String)} for a known tag id. */ + private void removeDirectional(long tagId) { + int direction = tagDirection(); + if (DirectionalTags.remove( + tagIds, customTags, customTagsBorrowed, pendingAliases, tagId, direction)) { + customTagsForWrite(); + DirectionalTags.remove(tagIds, customTags, false, pendingAliases, tagId, direction); + } + dropPendingAliasesIfEmpty(); + } + /** Copies {@code map}'s tags into the stores, sharing its Entry objects. */ private void putAllTags(TagMap map) { TagMap view = tagView(); @@ -1267,8 +1264,9 @@ private long writeKeyOf(String tag) { if (key == 0) { return 0L; } - long aliasedId = DirectionalTags.prepareAliasWrite(this, pendingAliases, tag, tagDirection()); + long aliasedId = DirectionalTags.prepareAliasWrite(pendingAliases, tag, tagDirection()); if (aliasedId > 0) { + removeCustomTag(tag); // a value pending under the alias is older than this write dropPendingAliasesIfEmpty(); return aliasedId; } @@ -1379,8 +1377,9 @@ private void storeEntry(TagMap.EntryReader entry) { putEntry(entry); } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { long aliasedId = - DirectionalTags.prepareAliasWrite(this, pendingAliases, entry.tag(), tagDirection()); + DirectionalTags.prepareAliasWrite(pendingAliases, entry.tag(), tagDirection()); if (aliasedId > 0) { + removeCustomTag(entry.tag()); // a value pending under the alias is older than this write dropPendingAliasesIfEmpty(); tagIds.set(aliasedId, entry.objectValue()); } else if (aliasedId < 0) { @@ -1412,8 +1411,7 @@ public void removeTag(String tag) { } synchronized (tagIds) { // A direction-dependent name and the tag it denotes here are removed together. - DirectionalTags.remove(this, pendingAliases, tag, tagDirection()); - dropPendingAliasesIfEmpty(); + removeDirectional(tag); } } @@ -1431,8 +1429,7 @@ public void removeTag(long tagId) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (tagIds) { - DirectionalTags.remove(this, pendingAliases, tagId, tagDirection()); - dropPendingAliasesIfEmpty(); + removeDirectional(tagId); } } @@ -1498,7 +1495,8 @@ private boolean interceptEntry(TagMap.EntryReader entry) { */ private boolean isPresent(TagMap.EntryReader entry) { if (KnownTagCodec.isDirectionDependent(entry)) { - return DirectionalTags.get(this, pendingAliases, entry.tag(), tagDirection()) != null; + return DirectionalTags.get(tagIds, customTags, pendingAliases, entry.tag(), tagDirection()) + != null; } String tag = entry.tag(); return TagRouting.get(tagIds, customTags, TagRouting.keyOf(tag), tag) != null; @@ -1560,7 +1558,11 @@ public void supersedeAliases(long tagId) { private void supersedePendingAliases(long tagId) { DirectionalTags.PendingAliases pending = pendingAliases; if (pending != null && KnownTagCodec.hasScopedAlias(tagId)) { - DirectionalTags.prepareWrite(this, pending, tagId, tagDirection()); + int direction = tagDirection(); + if (DirectionalTags.prepareWrite(customTags, customTagsBorrowed, pending, tagId, direction)) { + customTagsForWrite(); // a borrowed alias to hide: repeat on a store of the span's own + DirectionalTags.prepareWrite(customTags, false, pending, tagId, direction); + } dropPendingAliasesIfEmpty(); } } @@ -1813,8 +1815,7 @@ void setAllTags(final TagMap.Ledger ledger) { spanKindOrdinal = SPAN_KIND_UNSET; } // as removeTag(String): a direction-dependent name and the tag it denotes go together - DirectionalTags.remove(this, pendingAliases, tag, tagDirection()); - dropPendingAliasesIfEmpty(); + removeDirectional(tag); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; @@ -1893,7 +1894,7 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return DirectionalTags.get(this, pendingAliases, tag, tagDirection()); + return DirectionalTags.get(tagIds, customTags, pendingAliases, tag, tagDirection()); } @Deprecated diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index c3dcda14913..dfa0480a48a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -4,6 +4,8 @@ import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; +import datadog.trace.api.HashStore; +import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.TagMap; import datadog.trace.api.TagRouting; @@ -51,31 +53,18 @@ * span's custom store when its tags are emitted, so an alias that never resolves is emitted under * its literal name. * + *

The methods work on a span's stores: its {@link IdStore}, and its custom {@link HashStore}, + * which may be null, or borrowed read-only from its read-through parent ({@code borrowed}). Only + * the span creates its custom store: a removal that would have to hide a tag in a borrowed store + * changes nothing there and returns true, and the span creates its own store and repeats the + * removal, which is idempotent. + * *

One more: an alias written before the span has a direction is resolved when the kind arrives, * without passing through {@code TagInterceptor} again, so split-by-tags on its tag doesn't see it. * An alias written once the direction is known is intercepted as its tag. */ final class DirectionalTags { - /** - * The tag stores DirectionalTags applies a span's direction to: the span itself, which owns them - * (or, in tests, a TagMap). Known tags by id; custom tags -- and aliases stored as written -- by - * name. - */ - interface TagStores { - Object knownValue(long tagId); - - void setKnown(long tagId, Object value); - - void removeKnown(long tagId); - - Object customValue(String name); - - void setCustom(String name, Object value); - - void removeCustom(String name); - } - /** * A span's pending aliases: the direction-scoped aliases it stored as written, with their values, * in write order. Usually one or two. Guarded by the span's tag lock. @@ -146,13 +135,14 @@ static PendingAliases addPending(PendingAliases pending, String alias, Object va * when it is no alias at all, so its value is an ordinary custom tag. A span keeps a pending * alias only for the sentinel, so a name that merely passed {@link * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. + * + *

For an id, a value pending under the alias is older than the write: this drops it from + * {@code pending}, and the caller removes the alias from its custom store. */ - static long prepareAliasWrite( - TagStores tags, PendingAliases pending, String alias, int direction) { + static long prepareAliasWrite(PendingAliases pending, String alias, int direction) { long tagId = KnownTagCodec.directionalKeyOf(alias, direction); - if (tagId > 0) { - // a value pending under the alias is older than this write - removeAsWritten(tags, pending, alias); + if (tagId > 0 && pending != null) { + pending.remove(alias); } return tagId; } @@ -160,44 +150,60 @@ static long prepareAliasWrite( /** * 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. + * supersedes. A caller with a constant id that lacks the bit can skip the call. Returns true when + * the span must create its own custom store and repeat the call (see above). */ - static void prepareWrite(TagStores tags, PendingAliases pending, long tagId, int direction) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, pending, tagId, direction); - } + static boolean prepareWrite( + HashStore custom, boolean borrowed, PendingAliases pending, long tagId, int direction) { + return KnownTagCodec.hasScopedAlias(tagId) + && removePendingAliases(custom, borrowed, pending, tagId, 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. */ - private static void removePendingAliases( - TagStores tags, PendingAliases pending, long tagId, int direction) { + private static boolean removePendingAliases( + HashStore custom, boolean borrowed, PendingAliases pending, long tagId, int direction) { if (direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - if (alias != null) { - removeAsWritten(tags, pending, alias); - } - return; + return alias != null && removeAsWritten(custom, borrowed, pending, alias); } + boolean blocked = false; for (int d = DIRECTION_INBOUND; d <= DIRECTION_NONE; d++) { String alias = KnownTagCodec.directionScopedNameOf(tagId, d); if (alias != null) { - removeAsWritten(tags, pending, alias); + blocked |= removeAsWritten(custom, borrowed, pending, alias); } } + return blocked; } /** * Removes {@code alias} as written: from the holder, and from the custom store, which holds it * once emitted, or when inherited or written through the span's view. */ - private static void removeAsWritten(TagStores tags, PendingAliases pending, String alias) { - tags.removeCustom(alias); + private static boolean removeAsWritten( + HashStore custom, boolean borrowed, PendingAliases pending, String alias) { if (pending != null) { pending.remove(alias); } + return removeCustom(custom, borrowed, alias); + } + + /** + * Removes the custom tag {@code name} from {@code custom}, unless it is borrowed: then returns + * whether it holds {@code name}, which the span must hide in a store of its own. + */ + private static boolean removeCustom(HashStore custom, boolean borrowed, String name) { + if (custom == null) { + return false; + } + if (borrowed) { + return custom.get(name) != null; + } + custom.remove(name); + return false; } /** @@ -206,27 +212,29 @@ private static void removeAsWritten(TagStores tags, PendingAliases pending, Stri * write, wins. With no holder, a tag with an alias skips probing for one, so it costs a single * lookup like any other tag. */ - static Object get(TagStores tags, PendingAliases pending, String name, int direction) { + static Object get( + IdStore ids, HashStore custom, PendingAliases pending, String name, int direction) { long key = TagRouting.keyOf(name); if (key > 0) { - return get(tags, pending, key, direction); + return get(ids, pending, key, direction); } if (key == 0) { - return tags.customValue(name); + TagMap.Entry entry = custom == null ? null : custom.get(name); + return entry == null ? null : entry.objectValue(); } Object written = pending == null ? null : pending.get(name); if (written != null) { return written; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return tagId > 0 ? tags.knownValue(tagId) : null; + return tagId > 0 ? ids.getKnownValue(tagId) : null; } /** * The value of the known tag {@code tagId} on a span of {@code direction}; see {@link - * #get(TagStores, PendingAliases, String, int)}. + * #get(IdStore, HashStore, PendingAliases, String, int)}. */ - static Object get(TagStores tags, PendingAliases pending, long tagId, int direction) { + static Object get(IdStore ids, PendingAliases pending, long tagId, int direction) { if (pending != null && KnownTagCodec.hasScopedAlias(tagId) && direction != DIRECTION_UNKNOWN) { String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); Object written = alias == null ? null : pending.get(alias); @@ -234,63 +242,81 @@ static Object get(TagStores tags, PendingAliases pending, long tagId, int direct return written; } } - return tags.knownValue(tagId); + return ids.getKnownValue(tagId); } - /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ - static void remove(TagStores tags, PendingAliases pending, String name, int direction) { + /** + * Removes {@code name} from a span of {@code direction}: the tag and its alias together. Returns + * true when the span must create its own custom store and repeat the call. + */ + static boolean remove( + IdStore ids, + HashStore custom, + boolean borrowed, + PendingAliases pending, + String name, + int direction) { // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. long key = KnownTagCodec.keyOrAliasOf(name); if (key > 0) { - remove(tags, pending, key, direction); - return; + return remove(ids, custom, borrowed, pending, key, direction); } if (key == 0) { - tags.removeCustom(name); // a custom tag - return; + return removeCustom(custom, borrowed, name); // a custom tag } - removeAsWritten(tags, pending, name); + boolean blocked = removeAsWritten(custom, borrowed, pending, name); if (direction == DIRECTION_UNKNOWN) { - removeEveryTagOfAlias(tags, name); - return; + removeEveryTagOfAlias(ids, name); + return blocked; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); if (tagId > 0) { - tags.removeKnown(tagId); + ids.remove(tagId); } + return blocked; } - /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ - static void remove(TagStores tags, PendingAliases pending, long tagId, int direction) { - tags.removeKnown(tagId); - if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, pending, tagId, direction); - } + /** + * Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. Returns + * true when the span must create its own custom store and repeat the call. + */ + static boolean remove( + IdStore ids, + HashStore custom, + boolean borrowed, + PendingAliases pending, + long tagId, + int direction) { + ids.remove(tagId); + return KnownTagCodec.hasScopedAlias(tagId) + && removePendingAliases(custom, borrowed, pending, tagId, direction); } /** * Removes every tag {@code alias} could denote, for a removal of the alias before the span has a * direction. A removal counts as a write, so no older value can resurface when the kind arrives; * like the write rule, this can drop the other direction's tag. (Removing a tag before then - * likewise removes the pending aliases that could denote it; see {@link #remove(TagStores, - * PendingAliases, long, int)}.) + * likewise removes the pending aliases that could denote it; see {@link #remove(IdStore, + * HashStore, boolean, PendingAliases, long, int)}.) */ - private static void removeEveryTagOfAlias(TagStores tags, String alias) { + private static void removeEveryTagOfAlias(IdStore ids, String alias) { for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { long aliasedId = KnownTagCodec.directionalKeyOf(alias, direction); if (aliasedId > 0) { - tags.removeKnown(aliasedId); + ids.remove(aliasedId); } } } /** * Resolves a span's pending aliases, once it has a direction, into the tags they denote: the - * alias, being the newer write, overwrites its tag. Returns the holder of those left pending -- - * an alias with no tag in {@code direction}, or any while the direction is unknown or none -- or - * null when none are. + * alias, being the newer write, overwrites its tag, and a copy of it emitted earlier into the + * span's own custom store ({@code ownCustom}, null if the span has none) is dropped. Returns the + * holder of those left pending -- an alias with no tag in {@code direction}, or any while the + * direction is unknown or none -- or null when none are. */ - static PendingAliases resolvePending(TagStores tags, PendingAliases pending, int direction) { + static PendingAliases resolvePending( + IdStore ids, HashStore ownCustom, PendingAliases pending, int direction) { if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { return pending; // no alias names a tag on a span with no direction, or an internal one } @@ -299,9 +325,11 @@ static PendingAliases resolvePending(TagStores tags, PendingAliases pending, int long tagId = KnownTagCodec.directionalKeyOf(alias, direction); if (tagId > 0) { Object value = pending.values[i]; - tags.removeCustom(alias); // a copy emitted earlier + if (ownCustom != null) { + ownCustom.remove(alias); + } pending.remove(alias); - tags.setKnown(tagId, value); + ids.set(tagId, value); } else { i++; } @@ -310,12 +338,13 @@ static PendingAliases resolvePending(TagStores tags, PendingAliases pending, int } /** - * Copies the pending aliases into {@code tags} as custom tags under their literal names, so that - * emitting the span's tags includes them. The holder stays the span's record of them. + * Copies the pending aliases into {@code custom}, the span's own custom store, under their + * literal names, so that emitting the span's tags includes them. The holder stays the span's + * record of them. */ - static void copyPending(PendingAliases pending, TagStores tags) { + static void copyPending(PendingAliases pending, HashStore custom) { for (int i = 0; i < pending.size; i++) { - tags.setCustom(pending.names[i], pending.values[i]); + custom.set(pending.names[i], pending.values[i]); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java index 255690833f1..9f5f6280032 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java @@ -5,13 +5,17 @@ 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.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import datadog.trace.api.HashStore; +import datadog.trace.api.IdStore; import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.TagMap; +import datadog.trace.api.TagRouting; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.core.DirectionalTags.PendingAliases; import org.junit.jupiter.api.Test; @@ -23,39 +27,67 @@ * span tests. */ class DirectionalTagsTest { - /** The stores DirectionalTags works on, over a plain TagMap in place of a span. */ - private static DirectionalTags.TagStores stores(TagMap tags) { - return new DirectionalTags.TagStores() { - @Override - public Object knownValue(long tagId) { - return tags.getObject(KnownTagCodec.nameOf(tagId)); + /** A span's stores, with no span: an id store and an own custom store. */ + static final class Stores { + final IdStore ids; + final HashStore custom; + final boolean borrowed; + + Stores() { + this(TagRouting.newIdStore(null), TagRouting.newCustomStore(null), false); + } + + Stores(IdStore ids, HashStore custom, boolean borrowed) { + this.ids = ids; + this.custom = custom; + this.borrowed = borrowed; + } + + void set(String name, Object value) { + long key = TagRouting.keyOf(name); + if (TagRouting.isKnown(key)) { + ids.set(key, value); + } else { + custom.set(name, value); } + } - @Override - public void setKnown(long tagId, Object value) { - tags.set(tagId, value); - } + Object getObject(String name) { + TagMap.Entry entry = TagRouting.get(ids, custom, TagRouting.keyOf(name), name); + return entry == null ? null : entry.objectValue(); + } - @Override - public void removeKnown(long tagId) { - tags.getAndRemove(tagId); - } + int size() { + return TagMap.viewOf(ids, custom, borrowed, null).size(); + } - @Override - public Object customValue(String name) { - return tags.getObject(name); - } + Object get(PendingAliases pending, String name, int direction) { + return DirectionalTags.get(ids, custom, pending, name, direction); + } - @Override - public void setCustom(String name, Object value) { - tags.set(name, value); - } + Object get(PendingAliases pending, long tagId, int direction) { + return DirectionalTags.get(ids, pending, tagId, direction); + } - @Override - public void removeCustom(String name) { - tags.remove(name); - } - }; + boolean remove(PendingAliases pending, String name, int direction) { + return DirectionalTags.remove(ids, custom, borrowed, pending, name, direction); + } + + boolean remove(PendingAliases pending, long tagId, int direction) { + return DirectionalTags.remove(ids, custom, borrowed, pending, tagId, direction); + } + + long prepareAliasWrite(PendingAliases pending, String alias, int direction) { + return DirectionalTags.prepareAliasWrite(pending, alias, direction); + } + + boolean prepareWrite(PendingAliases pending, long tagId, int direction) { + return DirectionalTags.prepareWrite(custom, borrowed, pending, tagId, direction); + } + + PendingAliases resolvePending(PendingAliases pending, int direction) { + return DirectionalTags.resolvePending(ids, borrowed ? null : custom, pending, direction); + } } private static PendingAliases pending(String alias, Object value) { @@ -71,79 +103,64 @@ private static PendingAliases pending(String alias, Object value) { "server: port alias reads its tag | 0 | client.port | peer.port " }) void anAliasAndItsTagReadAsOne(int direction, String alias, String tag) { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(tag, "stored"); - assertEquals("stored", DirectionalTags.get(stores(tags), null, alias, direction)); - assertEquals("stored", DirectionalTags.get(stores(tags), null, tag, direction)); - assertEquals( - "stored", DirectionalTags.get(stores(tags), null, KnownTagCodec.keyOf(tag), direction)); + assertEquals("stored", tags.get(null, alias, direction)); + assertEquals("stored", tags.get(null, tag, direction)); + assertEquals("stored", tags.get(null, KnownTagCodec.keyOf(tag), direction)); // A pending alias is the newer write, so it wins on read, by name or by id. PendingAliases pending = pending(alias, "pending"); - assertEquals("pending", DirectionalTags.get(stores(tags), pending, alias, direction)); - assertEquals("pending", DirectionalTags.get(stores(tags), pending, tag, direction)); - assertEquals( - "pending", DirectionalTags.get(stores(tags), pending, KnownTagCodec.keyOf(tag), direction)); + assertEquals("pending", tags.get(pending, alias, direction)); + assertEquals("pending", tags.get(pending, tag, direction)); + assertEquals("pending", tags.get(pending, KnownTagCodec.keyOf(tag), direction)); } @Test void withNoDirectionAnAliasReadsOnlyAsWritten() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(Tags.PEER_HOSTNAME, "stored"); - assertNull(DirectionalTags.get(stores(tags), null, "server.address", DIRECTION_UNKNOWN)); - assertEquals( - "stored", DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); - assertEquals( - "stored", - DirectionalTags.get(stores(tags), null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); + assertNull(tags.get(null, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", tags.get(null, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals("stored", tags.get(null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); PendingAliases pending = pending("server.address", "pending"); - assertEquals( - "pending", DirectionalTags.get(stores(tags), pending, "server.address", DIRECTION_UNKNOWN)); - assertEquals( - "stored", - DirectionalTags.get(stores(tags), pending, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); + assertEquals("pending", tags.get(pending, "server.address", DIRECTION_UNKNOWN)); + assertEquals("stored", tags.get(pending, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); } @Test void aMissReadsAsNull() { - TagMap tags = TagMap.create(); - - assertNull(DirectionalTags.get(stores(tags), null, "my.tag", DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get(stores(tags), null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertNull( - DirectionalTags.get(stores(tags), null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(stores(tags), null, "server.address", DIRECTION_OUTBOUND)); + Stores tags = new Stores(); + + assertNull(tags.get(null, "my.tag", DIRECTION_OUTBOUND)); + assertNull(tags.get(null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(tags.get(null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull(tags.get(null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertNull(tags.get(null, "server.address", DIRECTION_OUTBOUND)); // client.port names no tag on a client span - assertNull(DirectionalTags.get(stores(tags), null, "client.port", DIRECTION_OUTBOUND)); + assertNull(tags.get(null, "client.port", DIRECTION_OUTBOUND)); } @Test void otherNamesReadAsWritten() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set("my.tag", "custom"); tags.set(Tags.HTTP_METHOD, "GET"); - assertEquals("custom", DirectionalTags.get(stores(tags), null, "my.tag", DIRECTION_OUTBOUND)); - assertEquals( - "GET", DirectionalTags.get(stores(tags), null, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); - assertEquals( - "GET", - DirectionalTags.get(stores(tags), null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); + assertEquals("custom", tags.get(null, "my.tag", DIRECTION_OUTBOUND)); + assertEquals("GET", tags.get(null, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); + assertEquals("GET", tags.get(null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); } @Test void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("server.address", "older"); - long tagId = - DirectionalTags.prepareAliasWrite( - stores(tags), pending, "server.address", DIRECTION_OUTBOUND); + long tagId = tags.prepareAliasWrite(pending, "server.address", DIRECTION_OUTBOUND); assertEquals(KnownTags.PEER_HOSTNAME_ID, tagId); assertTrue(pending.isEmpty()); // the older pending value is dropped @@ -157,32 +174,32 @@ void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { "not an alias | my.tag | 1 | 0 " }) void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, long result) { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending(name, "pending"); // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all - assertEquals(result, DirectionalTags.prepareAliasWrite(stores(tags), pending, name, direction)); + assertEquals(result, tags.prepareAliasWrite(pending, name, direction)); assertEquals("pending", pending.get(name)); } @Test void aTagWriteWithNoDirectionDropsEveryAliasThatCouldDenoteIt() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("client.port", 1); pending.put("server.port", 2); - DirectionalTags.prepareWrite(stores(tags), pending, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); + tags.prepareWrite(pending, KnownTags.PEER_PORT_ID, DIRECTION_UNKNOWN); assertTrue(pending.isEmpty()); } @Test void aTagWriteWithADirectionDropsOnlyItsAliasThere() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("client.port", 1); // no tag on a client span: kept pending.put("server.port", 2); - DirectionalTags.prepareWrite(stores(tags), pending, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); + tags.prepareWrite(pending, KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND); assertEquals(1, pending.get("client.port")); assertNull(pending.get("server.port")); @@ -190,23 +207,22 @@ void aTagWriteWithADirectionDropsOnlyItsAliasThere() { @Test void aTagWithoutAnAliasIsLeftAlone() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.prepareWrite( - stores(tags), pending, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); - DirectionalTags.prepareWrite(stores(tags), pending, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); + tags.prepareWrite(pending, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); + tags.prepareWrite(pending, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); assertEquals("pending", pending.get("server.address")); } @Test void removingAnAliasRemovesItsTag() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(Tags.PEER_HOSTNAME, "stored"); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.remove(stores(tags), pending, "server.address", DIRECTION_OUTBOUND); + tags.remove(pending, "server.address", DIRECTION_OUTBOUND); assertNull(tags.getObject(Tags.PEER_HOSTNAME)); assertTrue(pending.isEmpty()); @@ -214,30 +230,29 @@ void removingAnAliasRemovesItsTag() { @Test void removingATagRemovesItsAlias() { - TagMap byName = TagMap.create(); + Stores byName = new Stores(); byName.set(Tags.HTTP_HOSTNAME, "stored"); PendingAliases pendingByName = pending("server.address", "pending"); - DirectionalTags.remove(stores(byName), pendingByName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); + byName.remove(pendingByName, Tags.HTTP_HOSTNAME, DIRECTION_INBOUND); assertNull(byName.getObject(Tags.HTTP_HOSTNAME)); assertTrue(pendingByName.isEmpty()); - TagMap byId = TagMap.create(); + Stores byId = new Stores(); byId.set(Tags.HTTP_HOSTNAME, "stored"); PendingAliases pendingById = pending("server.address", "pending"); - DirectionalTags.remove( - stores(byId), pendingById, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); + byId.remove(pendingById, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); assertTrue(pendingById.isEmpty()); } @Test void removingAnAliasAlsoRemovesACopyEmittedEarlier() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.copyPending(pending, stores(tags)); + DirectionalTags.copyPending(pending, tags.custom); assertEquals("pending", tags.getObject("server.address")); - DirectionalTags.remove(stores(tags), pending, "server.address", DIRECTION_UNKNOWN); + tags.remove(pending, "server.address", DIRECTION_UNKNOWN); assertNull(tags.getObject("server.address")); assertTrue(pending.isEmpty()); @@ -245,12 +260,12 @@ void removingAnAliasAlsoRemovesACopyEmittedEarlier() { @Test void removingATagWithNoAliasLeavesUnrelatedTags() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(Tags.PEER_HOSTNAME, "stored"); tags.set("my.tag", "custom"); - DirectionalTags.remove(stores(tags), null, "my.tag", DIRECTION_OUTBOUND); - DirectionalTags.remove(stores(tags), null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); + tags.remove(null, "my.tag", DIRECTION_OUTBOUND); + tags.remove(null, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); assertNull(tags.getObject("my.tag")); @@ -264,11 +279,11 @@ void removingATagWithNoAliasLeavesUnrelatedTags() { "client port | 1 | server.port | peer.port " }) void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String tag) { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(tag, "older"); tags.set("my.tag", "custom"); - assertNull(DirectionalTags.resolvePending(stores(tags), pending(alias, "newer"), direction)); + assertNull(tags.resolvePending(pending(alias, "newer"), direction)); assertEquals("newer", tags.getObject(tag)); assertNull(tags.getObject(alias)); @@ -277,26 +292,26 @@ void resolvingFoldsAPendingAliasIntoItsTag(int direction, String alias, String t @Test void resolvingLeavesAnAliasWithNoTagPending() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("client.port", 1234); pending.put("server.address", "pending"); - assertSame(pending, DirectionalTags.resolvePending(stores(tags), pending, DIRECTION_OUTBOUND)); + assertSame(pending, tags.resolvePending(pending, DIRECTION_OUTBOUND)); assertEquals(1234, pending.get("client.port")); assertNull(pending.get("server.address")); assertEquals("pending", tags.getObject(Tags.PEER_HOSTNAME)); PendingAliases unknown = pending("server.address", "pending"); - assertSame(unknown, DirectionalTags.resolvePending(stores(tags), unknown, DIRECTION_UNKNOWN)); + assertSame(unknown, tags.resolvePending(unknown, DIRECTION_UNKNOWN)); assertEquals("pending", unknown.get("server.address")); } @Test void anAliasThatNeverResolvesIsEmittedUnderItsLiteralName() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); PendingAliases pending = pending("server.address", "pending"); - DirectionalTags.copyPending(pending, stores(tags)); + DirectionalTags.copyPending(pending, tags.custom); assertEquals("pending", tags.getObject("server.address")); assertEquals(1, tags.size()); @@ -304,46 +319,41 @@ void anAliasThatNeverResolvesIsEmittedUnderItsLiteralName() { @Test void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(Tags.PEER_HOSTNAME, "older"); - DirectionalTags.remove(stores(tags), null, "server.address", DIRECTION_UNKNOWN); + tags.remove(null, "server.address", DIRECTION_UNKNOWN); // once the span is a client, server.address names peer.hostname: the removal must stick - assertNull(DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(stores(tags), null, "server.address", DIRECTION_OUTBOUND)); + assertNull(tags.get(null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(tags.get(null, "server.address", DIRECTION_OUTBOUND)); } @Test void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { - TagMap byName = TagMap.create(); + Stores byName = new Stores(); PendingAliases pendingByName = pending("server.address", "pending"); - DirectionalTags.remove(stores(byName), pendingByName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); + byName.remove(pendingByName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); assertTrue(pendingByName.isEmpty()); - assertNull( - DirectionalTags.get(stores(byName), pendingByName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertNull(byName.get(pendingByName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - TagMap byId = TagMap.create(); + Stores byId = new Stores(); PendingAliases pendingById = pending("server.address", "pending"); - DirectionalTags.remove( - stores(byId), pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); + byId.remove(pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); assertTrue(pendingById.isEmpty()); - assertNull( - DirectionalTags.get( - stores(byId), pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertNull(byId.get(pendingById, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); } @Test void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { - TagMap tags = TagMap.create(); + Stores tags = new Stores(); tags.set(Tags.PEER_HOSTNAME, "kept"); tags.set("my.tag", "gone"); - DirectionalTags.remove(stores(tags), null, "my.tag", DIRECTION_UNKNOWN); + tags.remove(null, "my.tag", DIRECTION_UNKNOWN); assertNull(tags.getObject("my.tag")); - assertEquals( - "kept", DirectionalTags.get(stores(tags), null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); + assertEquals("kept", tags.get(null, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); } @Test @@ -359,4 +369,26 @@ void thePendingHolderGrowsAndKeepsWriteOrder() { assertNull(pending.get("b")); assertEquals(3, pending.get("c")); } + + @Test + void removingATagInABorrowedStoreAsksForAStoreOfTheSpansOwn() { + TagMap parent = TagMap.create(); + parent.set("inherited", "p"); + parent.set("server.address", "inherited-alias"); + TagMap frozen = TagRouting.readThroughParent(parent.freeze()); + HashStore borrowed = TagRouting.inheritedCustomStore(frozen); + Stores tags = new Stores(TagRouting.newIdStore(frozen), borrowed, true); + + assertFalse(tags.remove(null, "not.there", DIRECTION_OUTBOUND)); // nothing to hide + assertTrue(tags.remove(null, "inherited", DIRECTION_OUTBOUND)); + assertEquals("p", borrowed.get("inherited").objectValue()); // the borrowed store is untouched + assertTrue(tags.prepareWrite(null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertTrue(tags.remove(null, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); + assertTrue(tags.remove(null, "server.address", DIRECTION_UNKNOWN)); + + // repeated on a store of the span's own, the removal hides the inherited tag + Stores own = new Stores(tags.ids, TagRouting.newCustomStore(borrowed), false); + assertFalse(own.remove(null, "inherited", DIRECTION_OUTBOUND)); + assertNull(own.getObject("inherited")); + } } From dabddda0185ea7a3afac557c5354459e96c0f11d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 16:07:33 -0400 Subject: [PATCH 68/99] Let a Datadog tag name double as another tag's direction-scoped alias The registry rejected any OpenTelemetry name that matched a Datadog name, even for direction-scoped renames. That guarded only against misuse (a tag landing on a span of the wrong direction and colliding at export), and it forced invented names such as http.port for the inbound server port. A scoped rename may now be another tag's Datadog name. The owning tag keeps the name in every direction no alias claims: server.port is peer.port on outbound spans and its own tag (the server's port) otherwise. The name goes through the existing direction-scoped alias handling, so writes resolve by the span's kind. The one remaining collision, the tag written by id on a span where its name belongs to another tag, is handled at OTLP export: the alias's tag wins, so each attribute name is emitted once. Direction-free renames still may not match a Datadog name, since without a direction such a name would resolve to two tags. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 19 +++-- .../buildlogic/tagRegistry/TagRegistry.kt | 37 +++++++-- .../tagRegistry/TagRegistryGeneratorTest.kt | 40 ++++++++++ .../datadog/trace/core/DirectionalTags.java | 13 ++- .../trace/core/otlp/trace/OtlpTraceJson.java | 16 +++- .../trace/core/otlp/trace/OtlpTraceProto.java | 16 +++- .../trace/core/DDSpanDirectionalTagsTest.java | 47 +++++++++++ .../trace/OtlpTraceJsonCollectorTest.java | 45 +++++++++++ .../java/datadog/trace/api/KnownTagCodec.java | 79 ++++++++++++++++++- .../java/datadog/trace/api/KnownTagsTest.java | 27 ++++++- tag-conventions.yaml | 3 + 11 files changed, 322 insertions(+), 20 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 c88b895281b..24178116aa9 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 @@ -61,15 +61,16 @@ object KnownTagsEmitter { 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.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. + // Names that denote a different tag depending on the span's direction. keyOf(name) resolves + // none of them; directionalKeyOf(name, direction) switches on each one's tag per direction. Most + // are OpenTelemetry names only; a few are also a tag's Datadog name (server.port). val directional = sortedMapOf>() for (t in reg.tags) { for ((direction, otel) in t.scopedOtelNames) { directional.getOrPut(otel) { mutableMapOf() }[direction] = t.ddName } } - check(directional.keys.none { name -> name in nameOfConst || name in otelName.values }) { + check(directional.keys.none { name -> name in otelName.values }) { "a direction-dependent name is also direction-free: ${directional.keys}" } return buildString { @@ -135,19 +136,25 @@ object KnownTagsEmitter { otelByCanonical.forEach { (otel, _) -> appendLine(" \"${escape(otel)}\",") } - directional.keys.forEach { appendLine(" \"${escape(it)}\",") } + directional.keys.filter { it !in nameOfConst }.forEach { appendLine(" \"${escape(it)}\",") } appendLine( """ }; private static final long[] KEYOF_VALUES = { """.trimIndent() ) - keyOfOrder.forEach { appendLine(" ${idC(it)},") } + for (name in keyOfOrder) { + if (name in directional) { + appendLine(" KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // $name") + } else { + appendLine(" ${idC(name)},") + } + } otelByCanonical.forEach { (_, canonical) -> appendLine(" ${idC(canonical)},") } // A direction-dependent name's value is not an id but a marker saying it depends on direction. - for (name in directional.keys) { + for (name in directional.keys.filter { it !in nameOfConst }) { appendLine(" KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // $name") } // Resolver. KnownTagCodec.Installed links to this field directly, so merely resolving a tag 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 1a46bea5a4d..f56db4ad763 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 @@ -97,7 +97,32 @@ class TagRegistry private constructor(val tags: List) { } validateOtelNames(tags) - return TagRegistry(tags) + return TagRegistry(claimSharedNames(tags)) + } + + /** + * A tag whose Datadog name is also another tag's direction-scoped OpenTelemetry name keeps that + * name in every direction the other tag doesn't claim it. `server.port` is the inbound server + * port's own name, and `peer.port` on outbound spans: a write of `server.port` on a client span + * is `peer.port`, and on a server or internal span the tag itself. So the name depends on the + * span's direction like any other scoped alias, and the tag carries [HAS_SCOPED_ALIAS]. + */ + private fun claimSharedNames(tags: List): List { + val claimedBy = HashMap>() + for (t in tags) { + for ((direction, otel) in t.scopedOtelNames) { + claimedBy.getOrPut(otel) { mutableSetOf() }.add(direction) + } + } + return tags.map { t -> + val claimed = claimedBy[t.ddName] ?: return@map t + require(t.otelName == null && t.scopedOtelNames.isEmpty()) { + "'${t.name}' is another tag's OpenTelemetry name on ${claimed.joinToString { it.yamlKey }} " + + "spans, so it cannot be renamed itself" + } + val own = TagConventions.Direction.entries.filter { it !in claimed }.associateWith { t.ddName } + t.copy(id = t.id or HAS_SCOPED_ALIAS, scopedOtelNames = own.toSortedMap()) + } } /** @@ -133,10 +158,10 @@ class TagRegistry private constructor(val tags: List) { 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 - * in every direction. Scoped renames may share a name across directions: `server.address` - * maps to `http.hostname` inbound and `peer.hostname` outbound. + * Within each direction, a rename must belong to a single tag. A direction-free rename can't + * be a Datadog name too, since without a direction its name would resolve to two tags. A scoped + * rename may share a name across directions -- `server.address` is `http.hostname` inbound and + * `peer.hostname` outbound -- and may be another tag's Datadog name (see [claimSharedNames]). */ private fun validateOtelNames(tags: List) { val canonical = tags.map { it.ddName }.toSet() @@ -145,7 +170,7 @@ class TagRegistry private constructor(val tags: List) { val byDirection = t.otelName?.let { otel -> TagConventions.Direction.entries.associateWith { otel } } ?: t.scopedOtelNames for ((direction, otel) in byDirection) { - require(otel !in canonical) { + require(otel != t.ddName && (t.otelName == null || otel !in canonical)) { "OpenTelemetry name '$otel' (of '${t.name}') collides with canonical tag name '$otel'" } val prev = owner.put(direction to otel, t.name) 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 2169afd1908..85bb630c723 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 @@ -345,6 +345,44 @@ class TagRegistryGeneratorTest { assertThat(conventions.resolve("http.server").map { it.name }).contains("peer.port@inbound") } + @Test + fun `a Datadog name can be another tag's OpenTelemetry name in some directions`() { + val domain = + """ + span_types: + http.server: + span-kind: server + tags: [{dd-name: server.port, type: int}] + http.client: {span-kind: client, include: [outbound_peer]} + mixins: + outbound_peer: {span-kind: client, tags: [{dd-name: peer.port, type: int, otel-name: server.port}]} + """ + val output = File(directory, "generated") + + TagRegistryGenerator.generate(directory.conventionsFile(domain), output) + + val tags = TagRegistry.build(tagConventions(domain)).tags.associateBy { it.name } + assertThat(tags.getValue("server.port").scopedOtelNames) + .isEqualTo( + mapOf(TagConventions.Direction.INBOUND to "server.port", TagConventions.Direction.NONE to "server.port") + ) + assertThat(tags.getValue("server.port").id and TagRegistry.HAS_SCOPED_ALIAS).isNotZero() + val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + assertThat(source.substringAfter("case \"server.port\":").substringBefore("default:")) + .contains( + "case KnownTagCodec.DIRECTION_INBOUND:", + "return SERVER_PORT_ID;", + "case KnownTagCodec.DIRECTION_OUTBOUND:", + "return PEER_PORT_ID;", + "case KnownTagCodec.DIRECTION_NONE:", + ) + val keyOf = source.substringAfter("KEYOF_NAMES = {").substringBefore("private static final int[]") + assertThat(Regex("\"server\\.port\"|SERVER_PORT_NAME").findAll(keyOf).count()).isEqualTo(1) + assertThat(source.substringAfter("KEYOF_VALUES = {").substringBefore("};")) + .contains("KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // server.port") + .doesNotContain("SERVER_PORT_ID,") + } + @Test fun `a Datadog name declared per direction is one tag`() { val yaml = directory.conventionsFile( @@ -440,6 +478,8 @@ class TagRegistryGeneratorTest { @TableTest( """ scenario | domain | message + scoped rename to own name | span_types: {a: {span-kind: client, tags: [{dd-name: x, otel-name: x}]}} | collides with canonical + shared name renamed itself | span_types: {a: {span-kind: server, tags: [{dd-name: x, otel-name: o}]}, b: {span-kind: client, tags: [{dd-name: y, otel-name: x}]}} | so it cannot be renamed itself same name in one direction | span_types: {a: {span-kind: server, tags: [{dd-name: x, otel-name: o}]}, b: {span-kind: consumer, tags: [{dd-name: y, otel-name: o}]}} | claimed by both 'x' and 'y' on inbound spans neutral contradicts scoped | span_types: {a: {span-kind: server, tags: [{dd-name: x, otel-name: o, span-kind-neutral: true}]}, b: {span-kind: client, tags: [{dd-name: y, otel-name: o}]}} | on outbound spans map-form otel-name | span_types: {a: {span-kind: client, tags: [{dd-name: x, otel-name: {outbound: o}}]}} | invalid otel-name diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java index df26487061a..6c70e99d387 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java @@ -20,6 +20,10 @@ * inbound and {@code peer.hostname} outbound -- denotes its tag by the rule {@code (alias, * direction) -> tag}. * + *

An alias can also be another tag's Datadog name: {@code server.port} is {@code peer.port} on + * outbound spans, and its own tag on every other kind. It is handled like any other alias; only a + * read before the span has a direction falls back to the tag it names itself. + * *

Between a tag and its alias, the last write wins, with no ordering state (a TagMap has no * insertion order): * @@ -120,7 +124,10 @@ static Object get(TagMap tags, String name, int direction, boolean mayHavePendin if (key == 0 || asWritten != null) { return asWritten == null ? null : asWritten.objectValue(); } - long tagId = KnownTagCodec.directionalKeyOf(name, direction); + long tagId = + direction == DIRECTION_UNKNOWN + ? KnownTagCodec.datadogKeyOf(name) // server.port, written by id, is still server.port + : KnownTagCodec.directionalKeyOf(name, direction); return tagId > 0 ? tags.internals().getKnownValue(tagId) : null; } @@ -198,8 +205,8 @@ private static void removeEveryTagOfAlias(TagMap tags, String alias) { * -- or any, while the direction is unknown -- stays as written, a custom tag. */ static void resolveScopedNames(TagMap tags, int direction) { - if (direction == DIRECTION_UNKNOWN || direction == DIRECTION_NONE) { - return; // no alias names a tag on a span with no direction, or an internal one + if (direction == DIRECTION_UNKNOWN) { + return; } List pending = null; for (TagMap.EntryReader entry : tags) { 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 1575b26d228..8115dd41c7d 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 @@ -255,7 +255,21 @@ public void accept(Metadata metadata) { // 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); + if (Config.get().isTraceOtelSemanticsEnabled() + && KnownTagCodec.hasYieldingName(tags, direction)) { + // Rare: a tag named for another tag's alias here (server.port beside peer.port on a client + // span). Emit the alias's tag only, so each attribute name appears once. + tags.forEach( + writer, + direction, + (out, dir, entry) -> { + if (!KnownTagCodec.yieldsName(tags, entry, dir)) { + writeSpanTag(out, dir, entry); + } + }); + } else { + 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 ab64bde46cd..dec21fd167e 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 @@ -332,7 +332,21 @@ public void accept(Metadata metadata) { // 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); + if (Config.get().isTraceOtelSemanticsEnabled() + && KnownTagCodec.hasYieldingName(tags, direction)) { + // Rare: a tag named for another tag's alias here (server.port beside peer.port on a client + // span). Emit the alias's tag only, so each attribute name appears once. + tags.forEach( + buf, + direction, + (out, dir, entry) -> { + if (!KnownTagCodec.yieldsName(tags, entry, dir)) { + writeSpanTag(out, dir, entry); + } + }); + } else { + tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag); + } // reset for next span includeProcessTags = false; 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 5cfd93aa836..5e3fec23557 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 @@ -142,6 +142,53 @@ void anInternalSpanStoresPeerPortAsItsOneTag() { assertEquals(5432, span.getTag(Tags.PEER_PORT)); } + @Test + void aDatadogNameThatIsAlsoAnAliasIsItsOwnTagOnAServerSpan() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + span.setTag("server.port", 8080); + + assertEquals(8080, storedValue(KnownTags.SERVER_PORT_ID)); + assertEquals(8080, span.getTag("server.port")); + assertNull(storedValue(KnownTags.PEER_PORT_ID)); + } + + @Test + void aDatadogNameThatIsAlsoAnAliasIsTheAliasedTagOnAClientSpan() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + span.setTag("server.port", 443); + + assertEquals(443, storedValue(KnownTags.PEER_PORT_ID)); + assertEquals(443, span.getTag("server.port")); + assertNull(storedValue(KnownTags.SERVER_PORT_ID)); + } + + @Test + void aDatadogNameThatIsAlsoAnAliasIsItsOwnTagOnAnInternalSpan() { + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_INTERNAL); + span.setTag("server.port", 8080); + + assertEquals(8080, storedValue(KnownTags.SERVER_PORT_ID)); + assertNull(storedValue(KnownTags.PEER_PORT_ID)); + } + + @Test + void aDatadogNameThatIsAlsoAnAliasResolvesOnceTheSpanHasAKind() { + span.setTag("server.port", 443); + assertEquals(443, span.getTag("server.port")); + + span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + + assertEquals(443, storedValue(KnownTags.PEER_PORT_ID)); + assertNull(storedValue(KnownTags.SERVER_PORT_ID)); + } + + @Test + void aTagWrittenByIdIsReadByItsDatadogNameBeforeTheSpanHasAKind() { + span.setTag(KnownTags.SERVER_PORT_ID, 8080); + + assertEquals(8080, span.getTag("server.port")); + } + private Object storedValue(long tagId) { TagMap.Entry entry = span.getTags().getEntry(tagId); return entry == null ? null : entry.objectValue(); 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 811e3cdf32b..04ffa136f04 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 @@ -19,6 +19,7 @@ import datadog.json.JsonMapper; import datadog.trace.api.Config; import datadog.trace.api.DDTraceId; +import datadog.trace.api.KnownTags; import datadog.trace.api.TracePropagationStyle; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.sampling.SamplingMechanism; @@ -148,6 +149,38 @@ void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( } } + @TableTest({ + "scenario | kind | expectedPort", + "server's own | server | 8080 ", + "client's alias | client | 443 ", + "internal's own | internal | 8080 " + }) + void serverPortIsEmittedOnceInEveryDirection(String kind, long expectedPort) 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); + + // Misuse on the client span: the server's own port written by id beside peer.port, which + // is server.port there too. peer.port wins. + AgentSpan agentSpan = TRACER.startSpan("test", "op.serverPort"); + agentSpan.setTag(SPAN_KIND, kind); + agentSpan.setTag(KnownTags.SERVER_PORT_ID, 8080); + if ("client".equals(kind)) { + agentSpan.setTag(KnownTags.PEER_PORT_ID, 443); + } + agentSpan.finish(); + + OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); + collector.addTrace(asList((CoreSpan) agentSpan)); + List ports = attributeValues(onlySpan(collector.collectTraces()), "server.port"); + + assertEquals(1, ports.size(), "server.port must be emitted once; got " + ports); + assertEquals(String.valueOf(expectedPort), String.valueOf(ports.get(0))); + } + } + private static Set collectTagsAttributeKeys(boolean otelSemanticsEnabled) throws IOException { Config realConfig = Config.get(); @@ -500,6 +533,18 @@ private static Map onlyLink(Map span) { return (Map) links.get(0); } + @SuppressWarnings("unchecked") + private static List attributeValues(Map span, String key) { + List values = new ArrayList<>(); + for (Object attribute : (List) span.get("attributes")) { + Map entry = (Map) attribute; + if (key.equals(entry.get("key"))) { + values.add(((Map) entry.get("value")).values().iterator().next()); + } + } + return values; + } + @SuppressWarnings("unchecked") private static Set attributeKeys(Map span) { List attributes = (List) span.get("attributes"); 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 1e28d4841d2..a509fa9ac30 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -1,6 +1,7 @@ package datadog.trace.api; import datadog.trace.bootstrap.instrumentation.api.Tags; +import java.util.Arrays; /** * Resolves generated tag IDs and names. {@link KnownTags} owns the lookup tables; this class owns @@ -322,6 +323,75 @@ public static long keyOrAliasOf(String name) { return Installed.RESOLVER.lookup(name); } + /** + * The tag whose Datadog name is {@code name}, even when the name depends on the span's direction + * ({@code server.port} is also {@code peer.port}'s outbound alias, so {@link #keyOf} returns 0 + * for it); 0 when no tag has that Datadog name. For a span that has no direction yet. + */ + public static long datadogKeyOf(String name) { + long key = keyOrAliasOf(name); + if (key != DIRECTION_SCOPED_OTEL_NAME_SENTINEL) { + return key < 0 ? 0L : key; + } + for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { + long tagId = directionalKeyOf(name, direction); + if (tagId > 0 && name.equals(nameOf(tagId))) { + return tagId; + } + } + return 0L; + } + + /** + * True if {@code entry} must yield its emitted name on a span of {@code direction}: it is a tag + * named for another tag's alias there ({@code server.port}, on a client span that also holds + * {@code peer.port}). The alias's tag wins, so the span emits each attribute name once. + */ + public static boolean yieldsName(TagMap tags, TagMap.EntryReader entry, int direction) { + long tagId = entry.tagId(); + if (tagId == 0 || !hasScopedAlias(tagId) || direction == DIRECTION_UNKNOWN) { + return false; + } + long owner = directionalKeyOf(openTelemetryTagOf(tagId, direction), direction); + return owner > 0 && owner != tagId && tags.getKnownValue(owner) != null; + } + + /** + * True if any entry of {@code tags} {@link #yieldsName yields its name} on spans of {@code + * direction}. + */ + public static boolean hasYieldingName(TagMap tags, int direction) { + if (direction == DIRECTION_UNKNOWN) { + return false; + } + for (long tagId : SharedNames.TAG_IDS) { + if (tags.getKnownValue(tagId) != null) { + long owner = directionalKeyOf(nameOf(tagId), direction); + if (owner > 0 && owner != tagId && tags.getKnownValue(owner) != null) { + return true; + } + } + } + return false; + } + + /** The tags whose Datadog name is also another tag's alias, built once from the registry. */ + private static final class SharedNames { + static final long[] TAG_IDS = sharedNameTagIds(); + + private static long[] sharedNameTagIds() { + long[] ids = new long[0]; + for (int serial = 1; serial < serialLimit(); serial++) { + String name = nameOf(makeTagId(serial)); + if (keyOrAliasOf(name) == DIRECTION_SCOPED_OTEL_NAME_SENTINEL) { + ids = Arrays.copyOf(ids, ids.length + 1); + ids[ids.length - 1] = datadogKeyOf(name); + } + } + return ids; + } + } + /** * 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 @@ -338,7 +408,14 @@ private static final class AliasFilter { private static long aliasMask() { long mask = 0L; for (int serial = 1; serial < serialLimit(); serial++) { - long tagId = keyOf(nameOf(makeTagId(serial))); + String name = nameOf(makeTagId(serial)); + long tagId = keyOrAliasOf(name); + if (tagId == DIRECTION_SCOPED_OTEL_NAME_SENTINEL) { + // A Datadog name that is also an alias (server.port): the tag's only scoped name is its + // own, which another tag's alias already put in the filter. + mask |= 1L << name.hashCode(); + continue; + } for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { String alias = directionScopedNameOf(tagId, direction); if (alias != null) { 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 fcae594acbb..916d816256c 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -245,7 +245,8 @@ void theLookupCarriesTheDirectionRole() { KnownTags.PEER_HOSTNAME_ID, KnownTags.HTTP_HOSTNAME_ID, KnownTags.NETWORK_CLIENT_IP_ID, - KnownTags.PEER_PORT_ID + KnownTags.PEER_PORT_ID, + KnownTags.SERVER_PORT_ID }) { assertTrue(KnownTagCodec.hasScopedAlias(tagId), KnownTagCodec.nameOf(tagId)); } @@ -266,13 +267,35 @@ void aNameDeclaredPerDirectionIsOneTag() { } } + /** + * server.port is a tag's own Datadog name and peer.port's outbound alias, so like any alias it + * names no single tag without a direction; datadogKeyOf still finds the tag it names itself. + */ + @Test + void aDatadogNameThatIsAlsoAnAliasDependsOnDirection() { + assertEquals("server.port", KnownTagCodec.nameOf(KnownTags.SERVER_PORT_ID)); + assertEquals(0L, KnownTagCodec.keyOf("server.port")); + assertEquals(KnownTags.SERVER_PORT_ID, KnownTagCodec.datadogKeyOf("server.port")); + assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.datadogKeyOf(Tags.PEER_PORT)); + assertEquals(0L, KnownTagCodec.datadogKeyOf("server.address")); + assertEquals(0L, KnownTagCodec.datadogKeyOf("my.custom.tag")); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.SERVER_PORT_ID, DIRECTION_INBOUND)); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND)); + } + static Stream directionalNames() { return Stream.of( 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_ID), - Arguments.of("server.port", DIRECTION_INBOUND, 0L), + Arguments.of("server.port", DIRECTION_INBOUND, KnownTags.SERVER_PORT_ID), + Arguments.of("server.port", DIRECTION_NONE, KnownTags.SERVER_PORT_ID), + Arguments.of("server.port", DIRECTION_UNKNOWN, 0L), Arguments.of("client.port", DIRECTION_INBOUND, KnownTags.PEER_PORT_ID), Arguments.of("client.port", DIRECTION_OUTBOUND, 0L), Arguments.of("client.port", DIRECTION_NONE, 0L), diff --git a/tag-conventions.yaml b/tag-conventions.yaml index b54462a6a62..4381c80e016 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -45,6 +45,8 @@ # 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. +# A directional rename may also be another tag's Datadog name: server.port is peer.port on outbound +# spans, and its own tag (the server's port) in every other direction. # 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. @@ -110,6 +112,7 @@ span_types: tags: - { dd-name: http.route, type: string, required: conditional } # passes through: dd-name already is the OTel name - { dd-name: http.hostname, type: string, required: required, otel-name: server.address } # inbound only; on outbound spans server.address is peer.hostname + - { dd-name: server.port, type: int, required: conditional } # the server's own port; on outbound spans server.port is peer.port - { dd-name: http.useragent, type: string, required: recommended, otel-name: user_agent.original, span-kind-neutral: true } - { dd-name: http.query.string, type: string, required: recommended } # not a rename: url.query is also carried inside url.full, while http.url excludes the query (QueryObfuscator re-appends it) - { dd-name: servlet.path, type: string, required: optional } From b1107a32fe20e2b9dfdc234a8b738e0e886464e3 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 16:13:09 -0400 Subject: [PATCH 69/99] Drop the OTLP export check for a tag written by id on the wrong side Only a by-id write can put a shared-name tag on a span where its name belongs to another tag (server.port's own tag beside peer.port on a client span); name writes always resolve by the span's kind. Ids are trusted, so that case is exported as written rather than reconciled on every 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 | 49 ++++++++++++------ .../java/datadog/trace/api/KnownTagCodec.java | 51 ------------------- 4 files changed, 36 insertions(+), 96 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 8115dd41c7d..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 @@ -255,21 +255,7 @@ public void accept(Metadata metadata) { // The direction is computed once per span; boxing -1..2 hits the Integer cache. TagMap tags = metadata.getTags(); Integer direction = KnownTagCodec.directionOf(tags); - if (Config.get().isTraceOtelSemanticsEnabled() - && KnownTagCodec.hasYieldingName(tags, direction)) { - // Rare: a tag named for another tag's alias here (server.port beside peer.port on a client - // span). Emit the alias's tag only, so each attribute name appears once. - tags.forEach( - writer, - direction, - (out, dir, entry) -> { - if (!KnownTagCodec.yieldsName(tags, entry, dir)) { - writeSpanTag(out, dir, entry); - } - }); - } else { - tags.forEach(writer, direction, OtlpTraceJson::writeSpanTag); - } + 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 dec21fd167e..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 @@ -332,21 +332,7 @@ public void accept(Metadata metadata) { // The direction is computed once per span; boxing -1..2 hits the Integer cache. TagMap tags = metadata.getTags(); Integer direction = KnownTagCodec.directionOf(tags); - if (Config.get().isTraceOtelSemanticsEnabled() - && KnownTagCodec.hasYieldingName(tags, direction)) { - // Rare: a tag named for another tag's alias here (server.port beside peer.port on a client - // span). Emit the alias's tag only, so each attribute name appears once. - tags.forEach( - buf, - direction, - (out, dir, entry) -> { - if (!KnownTagCodec.yieldsName(tags, entry, dir)) { - writeSpanTag(out, dir, entry); - } - }); - } else { - tags.forEach(buf, direction, OtlpTraceProto::writeSpanTag); - } + 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 04ffa136f04..384f903efd0 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 @@ -40,6 +40,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.mockito.AdditionalAnswers; import org.mockito.MockedStatic; @@ -150,34 +151,52 @@ void aDirectionScopedTagIsEmittedUnderTheNameForTheSpansKind( } @TableTest({ - "scenario | kind | expectedPort", - "server's own | server | 8080 ", - "client's alias | client | 443 ", - "internal's own | internal | 8080 " + "scenario | kind | tag | expectedPort", + "server tag | server | server.port | 8080 ", + "client tag | client | peer.port | 8080 ", + "internal | internal | server.port | 8080 " }) - void serverPortIsEmittedOnceInEveryDirection(String kind, long expectedPort) throws IOException { + void serverPortIsEmittedOnceInEveryDirection(String kind, String tag, long expectedPort) + throws IOException { + List ports = exportedServerPorts(kind, span -> span.setTag(tag, 8080)); + + assertEquals(1, ports.size(), "server.port must be emitted once; got " + ports); + assertEquals(String.valueOf(expectedPort), String.valueOf(ports.get(0))); + } + + /** + * Ids are trusted: the server's own port written by id on a client span, beside peer.port (which + * is server.port there too), is exported twice rather than reconciled. + */ + @Test + void aTagWrittenByIdOnTheWrongSideIsExportedAsWritten() throws IOException { + List ports = + exportedServerPorts( + "client", + span -> { + span.setTag(KnownTags.SERVER_PORT_ID, 8080); + span.setTag(KnownTags.PEER_PORT_ID, 443); + }); + + assertEquals(2, ports.size(), "got " + ports); + } + + private static List exportedServerPorts(String kind, Consumer tagger) + 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); - // Misuse on the client span: the server's own port written by id beside peer.port, which - // is server.port there too. peer.port wins. AgentSpan agentSpan = TRACER.startSpan("test", "op.serverPort"); agentSpan.setTag(SPAN_KIND, kind); - agentSpan.setTag(KnownTags.SERVER_PORT_ID, 8080); - if ("client".equals(kind)) { - agentSpan.setTag(KnownTags.PEER_PORT_ID, 443); - } + tagger.accept(agentSpan); agentSpan.finish(); OtlpTraceJsonCollector collector = new OtlpTraceJsonCollector(); collector.addTrace(asList((CoreSpan) agentSpan)); - List ports = attributeValues(onlySpan(collector.collectTraces()), "server.port"); - - assertEquals(1, ports.size(), "server.port must be emitted once; got " + ports); - assertEquals(String.valueOf(expectedPort), String.valueOf(ports.get(0))); + return attributeValues(onlySpan(collector.collectTraces()), "server.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 a509fa9ac30..5a5b8e8536b 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -1,7 +1,6 @@ package datadog.trace.api; import datadog.trace.bootstrap.instrumentation.api.Tags; -import java.util.Arrays; /** * Resolves generated tag IDs and names. {@link KnownTags} owns the lookup tables; this class owns @@ -342,56 +341,6 @@ public static long datadogKeyOf(String name) { return 0L; } - /** - * True if {@code entry} must yield its emitted name on a span of {@code direction}: it is a tag - * named for another tag's alias there ({@code server.port}, on a client span that also holds - * {@code peer.port}). The alias's tag wins, so the span emits each attribute name once. - */ - public static boolean yieldsName(TagMap tags, TagMap.EntryReader entry, int direction) { - long tagId = entry.tagId(); - if (tagId == 0 || !hasScopedAlias(tagId) || direction == DIRECTION_UNKNOWN) { - return false; - } - long owner = directionalKeyOf(openTelemetryTagOf(tagId, direction), direction); - return owner > 0 && owner != tagId && tags.getKnownValue(owner) != null; - } - - /** - * True if any entry of {@code tags} {@link #yieldsName yields its name} on spans of {@code - * direction}. - */ - public static boolean hasYieldingName(TagMap tags, int direction) { - if (direction == DIRECTION_UNKNOWN) { - return false; - } - for (long tagId : SharedNames.TAG_IDS) { - if (tags.getKnownValue(tagId) != null) { - long owner = directionalKeyOf(nameOf(tagId), direction); - if (owner > 0 && owner != tagId && tags.getKnownValue(owner) != null) { - return true; - } - } - } - return false; - } - - /** The tags whose Datadog name is also another tag's alias, built once from the registry. */ - private static final class SharedNames { - static final long[] TAG_IDS = sharedNameTagIds(); - - private static long[] sharedNameTagIds() { - long[] ids = new long[0]; - for (int serial = 1; serial < serialLimit(); serial++) { - String name = nameOf(makeTagId(serial)); - if (keyOrAliasOf(name) == DIRECTION_SCOPED_OTEL_NAME_SENTINEL) { - ids = Arrays.copyOf(ids, ids.length + 1); - ids[ids.length - 1] = datadogKeyOf(name); - } - } - return ids; - } - } - /** * 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 From 1bf3e28d17283418023c565b1e78073493595756 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 16:19:29 -0400 Subject: [PATCH 70/99] Keep pending aliases out of TagInterceptor The id setters now supersede a pending alias themselves, before routing, instead of routing every write of an aliased tag through TagInterceptor while the span held one. TagInterceptor routes on INTERCEPTED alone again and no longer knows aliases exist; DDSpanContext.supersedeAliases is private. The check comes first as hasScopedAlias(id), so for a constant id without an alias it folds away entirely. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 75 +++++++++++-------- .../core/taginterceptor/TagInterceptor.java | 28 ++----- .../taginterceptor/TagInterceptorTest.java | 10 +-- 3 files changed, 49 insertions(+), 64 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 b472ebd8012..21905a61fda 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 @@ -163,10 +163,10 @@ private static int[] tagDirectionOfSpanKind() { * While null -- almost always -- a write of a tag with an alias takes the plain store path: no * second lock, no boxing. * - *

Written under the {@code tags} lock. {@link #routedTagFlags()} reads it without the lock, + *

Written under the {@code tagIds} lock. The id setters read it without the lock, * deliberately: a setter on another thread that has not yet seen the holder 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. + * always sees its own update, so no value is lost relative to superseding on every such write. */ private DirectionalTags.PendingAliases pendingAliases; @@ -1289,17 +1289,6 @@ private void dropPendingAliasesIfEmpty() { } } - /** - * The tag id flags the id setters route through {@link TagInterceptor}: {@link - * KnownTagCodec#INTERCEPTED}, plus {@link KnownTagCodec#HAS_SCOPED_ALIAS} while the span holds a - * pending alias, so a write of a tag with an alias is routed to supersede it. - */ - private int routedTagFlags() { - return pendingAliases == null - ? (int) KnownTagCodec.INTERCEPTED - : (int) (KnownTagCodec.INTERCEPTED | KnownTagCodec.HAS_SCOPED_ALIAS); - } - /** True while the span holds an alias stored as written; see {@link #pendingAliases}. */ @Override public boolean hasPendingAliases() { @@ -1482,7 +1471,7 @@ private boolean interceptEntry(TagMap.EntryReader entry) { && !splitsOnAliasName(entry.tag())) { long aliasedId = KnownTagCodec.directionalKeyOf(entry.tag(), tagDirection()); if (aliasedId > 0) { - return tagInterceptor.needsIntercept(aliasedId, (int) KnownTagCodec.INTERCEPTED) + return tagInterceptor.needsIntercept(aliasedId) && tagInterceptor.interceptTag(this, aliasedId, entry.objectValue()); } } @@ -1535,17 +1524,14 @@ 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 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. + * Removes the pending aliases a write of the id-keyed {@code tagId} supersedes, before the write + * is routed and stored. The id setters call it only for a tag with {@link + * KnownTagCodec#HAS_SCOPED_ALIAS} while the span holds a pending alias, a test that folds away + * for a constant id without an alias, 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 - } + private void supersedeAliases(long tagId) { synchronized (tagIds) { supersedePendingAliases(tagId); } @@ -1578,8 +1564,12 @@ public void setTag(final long tagId, final Object value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) - || !tagInterceptor.interceptTag(this, tagId, value)) { + return; + } + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (tagIds) { tagIds.set(tagId, value); } @@ -1592,8 +1582,12 @@ public void setTag(final long tagId, final CharSequence value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags()) - || !tagInterceptor.interceptTag(this, tagId, value)) { + return; + } + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (tagIds) { tagIds.set(tagId, value); } @@ -1604,7 +1598,10 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (tagIds) { @@ -1617,7 +1614,10 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (tagIds) { @@ -1630,7 +1630,10 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (tagIds) { @@ -1643,7 +1646,10 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (tagIds) { @@ -1656,7 +1662,10 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags())) { + if (KnownTagCodec.hasScopedAlias(tagId) && pendingAliases != null) { + supersedeAliases(tagId); + } + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (tagIds) { 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 e54e8faf099..11cdb8e5566 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 @@ -169,23 +169,12 @@ public boolean needsIntercept(Map map) { } /** - * 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. + * 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 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); + return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); } public boolean needsIntercept(String tag) { @@ -207,10 +196,6 @@ 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); @@ -289,10 +274,7 @@ public boolean interceptTag(DDSpanContext span, long tagId, Object value) { public boolean interceptTag(DDSpanContext span, TagMap.EntryReader entry) { long tagId = entry.tagId(); if (tagId != 0) { - // Only real routes: an entry that isn't routed is stored through DDSpanContext.storeEntry, - // which already supersedes any pending alias, so HAS_SCOPED_ALIAS needs no detour here. - return needsIntercept(tagId, (int) KnownTagCodec.INTERCEPTED) - && interceptTag(span, tagId, entry.objectValue()); + return needsIntercept(tagId) && interceptTag(span, tagId, entry.objectValue()); } return isSplitServiceTag(entry.tag()) && splitService(span, entry.objectValue()); } 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 a8211f74459..3f2d997f468 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 @@ -1106,16 +1106,10 @@ void exactlyTheTagsWithTheInterceptedBitHaveACase() throws IllegalAccessExceptio consumed |= interceptor.interceptTag(span, tagId, value); } - // 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")); + boolean hasCase = consumed || !mockingDetails(span).getInvocations().isEmpty(); assertEquals(KnownTagCodec.isIntercepted(tagId), hasCase, field.getName()); assertEquals( - KnownTagCodec.isIntercepted(tagId) || KnownTagCodec.hasScopedAlias(tagId), - interceptor.needsIntercept(tagId), - field.getName()); + KnownTagCodec.isIntercepted(tagId), interceptor.needsIntercept(tagId), field.getName()); } } From 3b9cc8d726c7b438be9837742ff69d31b56fd2cb Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 16:23:24 -0400 Subject: [PATCH 71/99] Hand the span's view its direction instead of the span TagMap.DirectionSource is gone. The span's cached view now gets the span's direction and whether it may hold pending aliases as two plain fields, set by attach() alongside its current custom store before each use, and a TagMap of its own reads every name as written (direction unknown). DDSpanContext no longer implements an interface for its view, so tagDirection() and hasPendingAliases() are no longer public; its public surface matches master. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 17 ++--- .../trace/core/DirectionalTagsTest.java | 2 +- .../forbiddenApiFilters/instrumentation.txt | 5 +- .../main/java/datadog/trace/api/TagMap.java | 74 ++++++++----------- .../java/datadog/trace/api/TagStoresTest.java | 38 ++++------ 5 files changed, 54 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 21905a61fda..125c80330a9 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 @@ -72,11 +72,7 @@ * the associated Span instance */ public class DDSpanContext - implements AgentSpanContext, - RequestContext, - TraceSegment, - ProfilerContext, - TagMap.DirectionSource { + implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext { private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class); public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -1015,10 +1011,9 @@ private TagMap tagView() { TagMap view = this.tagView; if (view == null) { // a view handed to the post-processor chain escapes, so allocate it once per span - this.tagView = view = TagMap.viewOf(tagIds, customTags, customTagsBorrowed, this); - } else { - view.attachCustomStore(customTags, customTagsBorrowed); + this.tagView = view = TagMap.viewOf(tagIds, customTags, customTagsBorrowed); } + view.attach(customTags, customTagsBorrowed, tagDirection(), pendingAliases != null); return view; } @@ -1157,8 +1152,7 @@ private void setResolved(long key, String tag, double value) { /** * The direction this span reads direction-dependent tag names in; see {@link DirectionalTags}. */ - @Override - public int tagDirection() { + private int tagDirection() { return TAG_DIRECTION_OF_SPAN_KIND[spanKindOrdinal]; } @@ -1290,8 +1284,7 @@ private void dropPendingAliasesIfEmpty() { } /** True while the span holds an alias stored as written; see {@link #pendingAliases}. */ - @Override - public boolean hasPendingAliases() { + boolean hasPendingAliases() { return pendingAliases != null; } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java index 9f5f6280032..aa64f32368e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java @@ -58,7 +58,7 @@ Object getObject(String name) { } int size() { - return TagMap.viewOf(ids, custom, borrowed, null).size(); + return TagMap.viewOf(ids, custom, borrowed).size(); } Object get(PendingAliases pending, String name, int direction) { diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 192a5db5e9c..4e7972c10f8 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -10,8 +10,7 @@ datadog.trace.bootstrap.instrumentation.api.AgentSpan#setServiceName(java.lang.S datadog.trace.api.HashStore datadog.trace.api.IdStore datadog.trace.api.TagRouting -datadog.trace.api.TagMap$DirectionSource -datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,boolean,datadog.trace.api.TagMap$DirectionSource) -datadog.trace.api.TagMap#attachCustomStore(datadog.trace.api.HashStore,boolean) +datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,boolean) +datadog.trace.api.TagMap#attach(datadog.trace.api.HashStore,boolean,int,boolean) datadog.trace.api.TagMap#customStore() datadog.trace.api.TagMap#isCustomStoreBorrowed() 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 d80b7201923..770c5eed524 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -108,7 +108,7 @@ public static final TagMap createFromParent(TagMap parent) { parent = null; } } - return new TagMap(parent, null); + return new TagMap(parent); } /** Creates a new TagMap.Ledger */ @@ -1254,10 +1254,16 @@ public EntryChange next() { boolean namesBorrowed; /** - * For a span's view ({@link #viewOf}), the span, which says what direction-scoped names read as; - * null for a TagMap of its own, which reads every name as written. + * For a span's view ({@link #viewOf}), the span's direction (a {@code KnownTagCodec.DIRECTION_*} + * constant), set by {@link #attach} before each use; {@code DIRECTION_UNKNOWN} for a TagMap of + * its own, which reads every name as written. */ - final DirectionSource direction; + private int direction = KnownTagCodec.DIRECTION_UNKNOWN; + + /** + * For a span's view, whether the span may hold an alias stored as written; see {@link #attach}. + */ + private boolean pendingAliases; private boolean frozen; @@ -1271,7 +1277,7 @@ public EntryChange next() { @VisibleForTesting TagMap parent; public TagMap() { - this(null, null); + this((TagMap) null); } /** @@ -1279,27 +1285,20 @@ public TagMap() { * construction and never re-pointed (only detached to null by {@link #clear()}), so read-through * optimizations can treat it as fixed. */ - private TagMap(TagMap parent, DirectionSource direction) { + private TagMap(TagMap parent) { this.ids = new HashIdStore(parent == null ? null : parent.ids); HashStore parentNames = parent == null ? null : parent.names; this.names = parentNames; this.namesBorrowed = parentNames != null; - this.direction = direction; this.frozen = false; this.parent = parent; } /** A map over {@code ids} and {@code names}: a copy ({@link #copy()}) or a view. */ - private TagMap( - TagMap parent, - IdStore ids, - HashStore names, - boolean namesBorrowed, - DirectionSource direction) { + private TagMap(TagMap parent, IdStore ids, HashStore names, boolean namesBorrowed) { this.ids = ids; this.names = names; this.namesBorrowed = namesBorrowed; - this.direction = direction; this.frozen = false; this.parent = parent; } @@ -1309,42 +1308,36 @@ private TagMap(Object[] buckets, int size) { this.ids = new HashIdStore(buckets, size); this.names = null; this.namesBorrowed = false; - this.direction = null; this.frozen = true; this.parent = null; } - /** - * What direction-scoped names read as on a span: its direction (a {@code - * KnownTagCodec.DIRECTION_*} constant), and whether it may hold an alias stored as written. - */ - @Restricted(allowedIn = {"datadog.trace.core"}) - public interface DirectionSource { - int tagDirection(); - - boolean hasPendingAliases(); - } - /** * A live view over a span's stores, for the span's internal readers (tag post-processors, * serializers) and its snapshot copy; never on the span's own set/get paths. Name-keyed reads * honor the span's direction. Not thread-safe: use it under the span's tag lock. * - *

The span owns the stores. Before each use it hands the view its current custom store ({@link - * #attachCustomStore}), which it creates lazily; after, it adopts any the view created ({@link - * #customStore}), since a reader may write a custom tag. + *

The span owns the stores. Before each use it {@link #attach attaches} the view to its + * current custom store, which it creates lazily, and its current direction; after, it adopts any + * custom store the view created ({@link #customStore}), since a reader may write a custom tag. */ @Restricted(allowedIn = {"datadog.trace.core"}) - public static TagMap viewOf( - IdStore ids, HashStore names, boolean namesBorrowed, DirectionSource direction) { - return new TagMap(null, ids, names, namesBorrowed, direction); + public static TagMap viewOf(IdStore ids, HashStore names, boolean namesBorrowed) { + return new TagMap(null, ids, names, namesBorrowed); } - /** Points this view at its span's current custom store; see {@link #viewOf}. */ + /** + * Points this view at its span's current custom store and direction; see {@link #viewOf}. {@code + * pendingAliases} says whether the span may hold an alias stored as written, which a read of the + * tag it denotes then prefers. + */ @Restricted(allowedIn = {"datadog.trace.core"}) - public void attachCustomStore(HashStore names, boolean namesBorrowed) { + public void attach( + HashStore names, boolean namesBorrowed, int direction, boolean pendingAliases) { this.names = names; this.namesBorrowed = namesBorrowed; + this.direction = direction; + this.pendingAliases = pendingAliases; } /** This map's custom store: null, borrowed ({@link #isCustomStoreBorrowed}), or its own. */ @@ -1526,12 +1519,11 @@ public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. long key = TagRouting.keyOf(tag); - DirectionSource direction = this.direction; - if (direction == null || key == 0) { + int direction = this.direction; + if (direction == KnownTagCodec.DIRECTION_UNKNOWN || key == 0) { return TagRouting.get(this.ids, this.names, key, tag); } - return TagRouting.getEntry( - this.ids, this.names, key, tag, direction.tagDirection(), direction.hasPendingAliases()); + return TagRouting.getEntry(this.ids, this.names, key, tag, direction, this.pendingAliases); } /** The entry for a known tag id. */ @@ -1963,11 +1955,7 @@ public TagMap copy() { HashStore names = this.names; boolean borrowed = this.namesBorrowed; return new TagMap( - this.parent, - this.ids.copy(), - names == null || borrowed ? names : names.copy(), - borrowed, - null); + this.parent, this.ids.copy(), names == null || borrowed ? names : names.copy(), borrowed); } public TagMap immutableCopy() { diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 5d00876d43a..09bf240ca66 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -275,9 +275,9 @@ void aViewReadsAnAliasByTheSpansDirection() { IdStore ids = TagRouting.newIdStore(null); ids.set(KnownTags.HTTP_HOSTNAME_ID, "inbound.example"); ids.set(KnownTags.PEER_HOSTNAME_ID, "outbound.example"); - TagMap inbound = TagMap.viewOf(ids, null, false, direction(DIRECTION_INBOUND, false)); - TagMap outbound = TagMap.viewOf(ids, null, false, direction(DIRECTION_OUTBOUND, false)); - TagMap plain = TagMap.viewOf(ids, null, false, null); + TagMap inbound = view(ids, null, false, DIRECTION_INBOUND, false); + TagMap outbound = view(ids, null, false, DIRECTION_OUTBOUND, false); + TagMap plain = TagMap.viewOf(ids, null, false); assertEquals("inbound.example", inbound.getString("server.address")); assertEquals("outbound.example", outbound.getString("server.address")); @@ -291,9 +291,9 @@ void aViewReadsAnEmittedPendingAliasOverItsTag() { ids.set(KnownTags.PEER_HOSTNAME_ID, "older"); HashStore names = TagRouting.newCustomStore(null); names.set("server.address", "pending"); - TagMap pending = TagMap.viewOf(ids, names, false, direction(DIRECTION_OUTBOUND, true)); - TagMap noPending = TagMap.viewOf(ids, names, false, direction(DIRECTION_OUTBOUND, false)); - TagMap unknown = TagMap.viewOf(ids, names, false, direction(DIRECTION_UNKNOWN, true)); + TagMap pending = view(ids, names, false, DIRECTION_OUTBOUND, true); + TagMap noPending = view(ids, names, false, DIRECTION_OUTBOUND, false); + TagMap unknown = view(ids, names, false, DIRECTION_UNKNOWN, true); assertEquals("pending", pending.getString(KnownTags.PEER_HOSTNAME_NAME)); assertEquals("pending", pending.getString("server.address")); @@ -303,9 +303,7 @@ void aViewReadsAnEmittedPendingAliasOverItsTag() { assertNull(noPending.getString("client.port")); // no tag in this direction // with no custom store, the alias reads the tag it denotes assertEquals( - "older", - TagMap.viewOf(ids, null, false, direction(DIRECTION_OUTBOUND, true)) - .getString("server.address")); + "older", view(ids, null, false, DIRECTION_OUTBOUND, true).getString("server.address")); } @Test @@ -313,7 +311,7 @@ void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { TagMap parent = TagRouting.readThroughParent(frozenParent()); IdStore ids = TagRouting.newIdStore(parent); HashStore borrowed = TagRouting.inheritedCustomStore(parent); - TagMap view = TagMap.viewOf(ids, borrowed, true, direction(DIRECTION_UNKNOWN, false)); + TagMap view = view(ids, borrowed, true, DIRECTION_UNKNOWN, false); assertSame(borrowed, view.customStore()); assertTrue(view.isCustomStoreBorrowed()); @@ -324,7 +322,7 @@ void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { assertNotSame(borrowed, view.customStore()); assertEquals("o", view.customStore().get("own").stringValue()); - view.attachCustomStore(null, false); + view.attach(null, false, DIRECTION_UNKNOWN, false); assertNull(view.getString("own")); assertNull(view.customStore()); } @@ -489,17 +487,11 @@ void aCustomStoreRejectsAKnownTagsName() { assertThrows(AssertionError.class, () -> names.remove(KnownTags.HTTP_ROUTE_NAME)); } - private static TagMap.DirectionSource direction(int direction, boolean pendingAliases) { - return new TagMap.DirectionSource() { - @Override - public int tagDirection() { - return direction; - } - - @Override - public boolean hasPendingAliases() { - return pendingAliases; - } - }; + /** A span-style view over {@code ids} and {@code names}, attached to a direction. */ + private static TagMap view( + IdStore ids, HashStore names, boolean namesBorrowed, int direction, boolean pendingAliases) { + TagMap view = TagMap.viewOf(ids, names, namesBorrowed); + view.attach(names, namesBorrowed, direction, pendingAliases); + return view; } } From d7f0ffc844bde75ec61b2564624d055b1a7f8f23 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 16:52:28 -0400 Subject: [PATCH 72/99] Resolve direction-scoped names at the OpenTelemetry boundary, not in the core Replaces the span-level direction handling (DirectionalTags, pending aliases folded in when the kind arrives, last-write-wins between a tag and its alias, the HAS_SCOPED_ALIAS id bit and the routing it drove) with a smaller rule: in Datadog terms a tag's name never depends on the span's direction, so the core resolves names without one, as on master. Only some OpenTelemetry names depend on direction, and direction is applied where the OpenTelemetry side knows it: - Export already names each tag by the span's direction. - The generator emits KnownTags.Inbound, Outbound and Internal: each OpenTelemetry name that names a tag in that direction, as the tag's id (Outbound.SERVER_ADDRESS_ID is PEER_HOSTNAME_ID). Instrumentation that knows its direction writes in OpenTelemetry terms by id. - KnownTagCodec.directionalKeyOf resolves such a name for a direction. - The OpenTelemetry shim stores a direction-scoped attribute as the tag it denotes for its span's kind; the builder holds one set before the kind until the span starts. Nothing in the tracer writes these names by string, so the pending-alias machinery only served user code writing an alias before setting the kind; such a value now stays as written. server.port stays a Datadog name too: the server's own port, and peer.port on outbound spans. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 71 ++-- .../buildlogic/tagRegistry/TagRegistry.kt | 29 +- .../TagRegistryGeneratorPluginTest.kt | 11 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 90 +++-- .../shim/trace/OtelConventions.java | 13 + .../opentelemetry/shim/trace/OtelSpan.java | 11 +- .../shim/trace/OtelSpanBuilder.java | 77 ++++- ...nTelemetry14DirectionalAttributesTest.java | 83 +++++ .../datadog/trace/core/SetTagBenchmark.java | 25 -- .../datadog/trace/core/DDSpanContext.java | 321 +++--------------- .../datadog/trace/core/DirectionalTags.java | 230 ------------- .../core/taginterceptor/TagInterceptor.java | 35 +- .../DDSpanDirectionalTagConsistencyTest.java | 289 ---------------- .../trace/core/DDSpanDirectionalTagsTest.java | 196 ----------- .../trace/core/DDSpanSetTagByIdTest.java | 4 + .../trace/core/DirectionalTagsTest.java | 271 --------------- .../taginterceptor/TagInterceptorTest.java | 69 +--- .../java/datadog/trace/api/KnownTagCodec.java | 251 ++++---------- .../main/java/datadog/trace/api/TagMap.java | 159 ++------- .../trace/api/KnownTagCodecDirectionTest.java | 71 ---- .../java/datadog/trace/api/KnownTagsTest.java | 314 ++++++++--------- .../trace/api/TagMapDirectionalNameTest.java | 87 ----- .../api/TagMapEntryCreateByNameTest.java | 52 --- .../datadog/trace/api/TagMapSetByIdTest.java | 33 -- .../datadog/trace/api/TagMapTagHashTest.java | 9 +- tag-conventions.yaml | 8 +- 26 files changed, 578 insertions(+), 2231 deletions(-) create mode 100644 dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14DirectionalAttributesTest.java delete mode 100644 dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java delete mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java delete mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java delete mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java delete mode 100644 internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.java delete mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java delete 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 24178116aa9..910f0627c39 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 @@ -59,20 +59,15 @@ object KnownTagsEmitter { fun otelNameC(ddName: String) = otelNameOfConst.getValue(ddName) 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.ddName to it } }.toMap() - // Names that denote a different tag depending on the span's direction. keyOf(name) resolves - // none of them; directionalKeyOf(name, direction) switches on each one's tag per direction. Most - // are OpenTelemetry names only; a few are also a tag's Datadog name (server.port). + // OpenTelemetry names that denote a different tag depending on the span's direction. keyOf + // ignores direction (a shared Datadog name such as server.port resolves to its own tag); + // directionalKeyOf(name, direction) switches on each one's tag per direction. val directional = sortedMapOf>() for (t in reg.tags) { for ((direction, otel) in t.scopedOtelNames) { directional.getOrPut(otel) { mutableMapOf() }[direction] = t.ddName } } - check(directional.keys.none { 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. @@ -99,7 +94,6 @@ 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)}>") @@ -108,6 +102,34 @@ object KnownTagsEmitter { // 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. + // Per-direction aliases: each OpenTelemetry name that names a tag on spans of one direction, as + // that tag's id. Code that knows its direction writes in OpenTelemetry terms by id + // (Outbound.SERVER_ADDRESS_ID is PEER_HOSTNAME_ID) without knowing the Datadog name, and + // without a lookup. Direction-free renames are repeated in each, so one class covers a span. + for (direction in TagConventions.Direction.entries) { + val aliases = sortedMapOf() + for (t in reg.tags) { + val otel = t.otelName ?: t.scopedOtelNames[direction] ?: continue + aliases[otel] = t.ddName + } + if (aliases.isEmpty()) continue + val holder = DIRECTION_CLASS.getValue(direction) + appendLine(" /** OpenTelemetry names on ${DIRECTION_SPANS.getValue(direction)} spans, as the ids of the tags they name. */") + appendLine(" public static final class $holder {") + val local = HashSet() + for ((otel, ddName) in aliases) { + var constant = withSuffix(sanitize(otel), "_ID") + var n = 2 + while (!local.add(constant)) constant = "${withSuffix(sanitize(otel), "_ID")}_${n++}" + appendLine(" /** {@code ${escape(otel)}}: {@code ${escape(ddName)}}. */") + appendLine(" public static final long $constant = $className.${idC(ddName)};") + } + appendLine() + appendLine(" private $holder() {}") + appendLine(" }") + appendLine() + } + appendLine(" // ---- serial numbers ----") for (t in reg.tags) { appendLine(" public static final int ${serialC(t.ddName)} = ${t.serial};") @@ -136,27 +158,16 @@ object KnownTagsEmitter { otelByCanonical.forEach { (otel, _) -> appendLine(" \"${escape(otel)}\",") } - directional.keys.filter { it !in nameOfConst }.forEach { appendLine(" \"${escape(it)}\",") } appendLine( """ }; private static final long[] KEYOF_VALUES = { """.trimIndent() ) - for (name in keyOfOrder) { - if (name in directional) { - appendLine(" KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // $name") - } else { - appendLine(" ${idC(name)},") - } - } + keyOfOrder.forEach { appendLine(" ${idC(it)},") } otelByCanonical.forEach { (_, canonical) -> appendLine(" ${idC(canonical)},") } - // A direction-dependent name's value is not an id but a marker saying it depends on direction. - for (name in directional.keys.filter { it !in nameOfConst }) { - 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. appendLine( @@ -238,7 +249,7 @@ object KnownTagsEmitter { } @Override - public long lookup(String name) { + public long keyOf(String name) { int slot = StringIndex.EmbeddingSupport.indexOf(KEYOF_HASHES, KEYOF_KEYS, name); return slot < 0 ? 0L : KEYOF_IDS[slot]; } @@ -257,7 +268,7 @@ object KnownTagsEmitter { appendLine(" return ${idC(tag)};") } appendLine(" default:") - appendLine(" return KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL;") + appendLine(" return 0L;") appendLine(" }") } appendLine( @@ -275,6 +286,20 @@ object KnownTagsEmitter { } } + private val DIRECTION_CLASS = + mapOf( + TagConventions.Direction.INBOUND to "Inbound", + TagConventions.Direction.OUTBOUND to "Outbound", + TagConventions.Direction.NONE to "Internal", + ) + + private val DIRECTION_SPANS = + mapOf( + TagConventions.Direction.INBOUND to "server and consumer", + TagConventions.Direction.OUTBOUND to "client and producer", + TagConventions.Direction.NONE to "internal", + ) + private fun hex(id: Long): String = "0x%016XL".format(Locale.ROOT, id) private fun escape(value: String): String = String(JsonStringEncoder.getInstance().quoteAsString(value)) 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 f56db4ad763..b8a01f7615b 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 @@ -39,31 +39,18 @@ class TagRegistry private constructor(val tags: List) { companion object { const val FIRST_SERIAL = 1 - const val HAS_SCOPED_ALIAS = 1L shl 0 // low-32 carve bit 0; mirrors KnownTagCodec.HAS_SCOPED_ALIAS const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE - /** - * 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) + intercepted() + traceLevel() -- must stay in sync. - * HAS_SCOPED_ALIAS at bit 0 (the tag has an OpenTelemetry name in some directions only), * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, 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. + * zero. */ - 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" - } + 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 (hasScopedAlias) id = id or HAS_SCOPED_ALIAS return id } @@ -90,7 +77,7 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted, scoped.isNotEmpty()), + id = encode(serial, isTraceLevel, isIntercepted), otelName = otelName, scopedOtelNames = scoped, ) @@ -102,10 +89,10 @@ class TagRegistry private constructor(val tags: List) { /** * A tag whose Datadog name is also another tag's direction-scoped OpenTelemetry name keeps that - * name in every direction the other tag doesn't claim it. `server.port` is the inbound server - * port's own name, and `peer.port` on outbound spans: a write of `server.port` on a client span - * is `peer.port`, and on a server or internal span the tag itself. So the name depends on the - * span's direction like any other scoped alias, and the tag carries [HAS_SCOPED_ALIAS]. + * name in every direction the other tag doesn't claim it: `server.port` is its own tag (the + * server's port), except on outbound spans, where it is `peer.port`. The Datadog name still + * resolves to the tag without a direction; only OpenTelemetry-side resolution by direction maps + * it to the other tag. */ private fun claimSharedNames(tags: List): List { val claimedBy = HashMap>() @@ -121,7 +108,7 @@ class TagRegistry private constructor(val tags: List) { "spans, so it cannot be renamed itself" } val own = TagConventions.Direction.entries.filter { it !in claimed }.associateWith { t.ddName } - t.copy(id = t.id or HAS_SCOPED_ALIAS, scopedOtelNames = own.toSortedMap()) + t.copy(scopedOtelNames = own.toSortedMap()) } } 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 442cf2b9bc6..5874dcfb70d 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,17 +245,16 @@ 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 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, int direction); - long lookup(String name); + long keyOf(String name); long directionalKeyOf(String name, int direction); } + 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 85bb630c723..b4533731e2a 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 @@ -295,10 +295,7 @@ class TagRegistryGeneratorTest { val generated = contents(output) 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.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") @@ -339,8 +336,6 @@ 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") } @@ -366,7 +361,6 @@ class TagRegistryGeneratorTest { .isEqualTo( mapOf(TagConventions.Direction.INBOUND to "server.port", TagConventions.Direction.NONE to "server.port") ) - assertThat(tags.getValue("server.port").id and TagRegistry.HAS_SCOPED_ALIAS).isNotZero() val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") assertThat(source.substringAfter("case \"server.port\":").substringBefore("default:")) .contains( @@ -376,13 +370,57 @@ class TagRegistryGeneratorTest { "return PEER_PORT_ID;", "case KnownTagCodec.DIRECTION_NONE:", ) - val keyOf = source.substringAfter("KEYOF_NAMES = {").substringBefore("private static final int[]") - assertThat(Regex("\"server\\.port\"|SERVER_PORT_NAME").findAll(keyOf).count()).isEqualTo(1) + // Without a direction the Datadog name is its own tag; only directionalKeyOf maps it elsewhere. assertThat(source.substringAfter("KEYOF_VALUES = {").substringBefore("};")) - .contains("KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, // server.port") - .doesNotContain("SERVER_PORT_ID,") + .contains("SERVER_PORT_ID,") + .doesNotContain("SENTINEL") + } + + @Test + fun `each direction gets its OpenTelemetry names as the ids of the tags they name`() { + val yaml = directory.conventionsFile( + """ + span_types: + base: {abstract: true, tags: [{dd-name: http.method, otel-name: http.request.method}]} + http.server: + span-kind: server + tags: + - {dd-name: http.hostname, otel-name: server.address} + - {dd-name: server.port, type: int} + http.client: {span-kind: client, include: [outbound_peer]} + mixins: + outbound_peer: + span-kind: client + tags: + - {dd-name: peer.hostname, otel-name: server.address} + - {dd-name: peer.port, type: int, otel-name: server.port} + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + fun holder(name: String) = + source.substringAfter("public static final class $name {").substringBefore("private $name() {}") + assertThat(holder("Outbound")) + .contains( + "SERVER_ADDRESS_ID = KnownTags.PEER_HOSTNAME_ID;", + "SERVER_PORT_ID = KnownTags.PEER_PORT_ID;", + "HTTP_REQUEST_METHOD_ID = KnownTags.HTTP_METHOD_ID;", + ) + assertThat(holder("Inbound")) + .contains( + "SERVER_ADDRESS_ID = KnownTags.HTTP_HOSTNAME_ID;", + "SERVER_PORT_ID = KnownTags.SERVER_PORT_ID;", + "HTTP_REQUEST_METHOD_ID = KnownTags.HTTP_METHOD_ID;", + ) + assertThat(holder("Internal")) + .contains("SERVER_PORT_ID = KnownTags.SERVER_PORT_ID;") + .doesNotContain("SERVER_ADDRESS_ID") } + @Test fun `a Datadog name declared per direction is one tag`() { val yaml = directory.conventionsFile( @@ -403,10 +441,9 @@ class TagRegistryGeneratorTest { val source = generated.getValue("java/datadog/trace/api/KnownTags.java") assertThat(source) .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("};")) - .contains("PEER_PORT_ID,") - .doesNotContain("SENTINEL") + .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) } @@ -437,9 +474,9 @@ class TagRegistryGeneratorTest { "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") + // Direction-scoped names are not resolved by name yet. + assertThat(source.substringAfter("KEYOF_NAMES = {").substringBefore("};")) + .doesNotContain("client.port", "server.port", "server.address") } @Test @@ -501,23 +538,6 @@ 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-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java index 38d8dc701e9..117c7ab77d8 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java @@ -18,6 +18,7 @@ import static java.util.Collections.emptyMap; import static java.util.Locale.ROOT; +import datadog.trace.api.KnownTagCodec; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -48,6 +49,18 @@ public final class OtelConventions { private OtelConventions() {} + /** + * The tag to store an attribute under on spans of {@code direction}. A direction-scoped + * OpenTelemetry name is stored as the Datadog tag it denotes there: {@code server.address} is + * {@code peer.hostname} on a client span and {@code http.hostname} on a server span. Every other + * name, and a direction-scoped one that denotes no tag in {@code direction}, is stored as + * written. + */ + static String attributeTag(String key, int direction) { + long tagId = KnownTagCodec.directionalKeyOf(key, direction); + return tagId == 0 ? key : KnownTagCodec.nameOf(tagId); + } + /** * Convert OpenTelemetry {@link SpanKind} to {@link Tags#SPAN_KIND} value. * diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java index d9ef3e004d6..cfc14687790 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java @@ -4,6 +4,7 @@ import static datadog.opentelemetry.shim.trace.OtelConventions.applyNamingConvention; import static datadog.opentelemetry.shim.trace.OtelConventions.applyReservedAttribute; import static datadog.opentelemetry.shim.trace.OtelConventions.applySpanEventExceptionAttributes; +import static datadog.opentelemetry.shim.trace.OtelConventions.attributeTag; import static datadog.opentelemetry.shim.trace.OtelConventions.convertEventAttributes; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; import static io.opentelemetry.api.trace.StatusCode.ERROR; @@ -11,12 +12,14 @@ import static io.opentelemetry.api.trace.StatusCode.UNSET; import datadog.context.ContextScope; +import datadog.trace.api.KnownTagCodec; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.bootstrap.instrumentation.api.SpanWrapper; +import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.WithAgentSpan; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; @@ -33,11 +36,17 @@ @ParametersAreNonnullByDefault public class OtelSpan implements Span, WithAgentSpan, SpanWrapper { private final AgentSpan delegate; + + /** The direction of the span's kind, fixed at start; it decides direction-scoped names. */ + private final int direction; + private StatusCode statusCode = UNSET; private volatile boolean recording = true; public OtelSpan(AgentSpan delegate) { this.delegate = delegate; + Object kind = delegate.getTag(Tags.SPAN_KIND); + this.direction = KnownTagCodec.directionOfKind(kind == null ? null : kind.toString()); if (delegate instanceof AttachableWrapper) { ((AttachableWrapper) delegate).attachWrapper(this); } @@ -69,7 +78,7 @@ public Span setAttribute(AttributeKey key, T value) { } break; default: - this.delegate.setTag(key.getKey(), value); + this.delegate.setTag(attributeTag(key.getKey(), this.direction), value); break; } } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java index 63b8f8a6ddf..f5b422519ff 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanBuilder.java @@ -3,14 +3,17 @@ import static datadog.opentelemetry.shim.trace.OtelConventions.ANALYTICS_EVENT_SPECIFIC_ATTRIBUTES; import static datadog.opentelemetry.shim.trace.OtelConventions.HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE; import static datadog.opentelemetry.shim.trace.OtelConventions.OPERATION_NAME_SPECIFIC_ATTRIBUTE; +import static datadog.opentelemetry.shim.trace.OtelConventions.attributeTag; import static datadog.opentelemetry.shim.trace.OtelConventions.toSpanKindTagValue; import static datadog.opentelemetry.shim.trace.OtelExtractedContext.extract; import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; +import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static java.lang.Boolean.parseBoolean; import static java.util.Locale.ROOT; +import datadog.trace.api.KnownTagCodec; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; @@ -21,6 +24,7 @@ import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.annotation.ParametersAreNonnullByDefault; @@ -29,6 +33,16 @@ public class OtelSpanBuilder implements SpanBuilder { private final AgentTracer.SpanBuilder delegate; private boolean spanKindSet; + + /** The direction of the span's kind once set; it decides direction-scoped attribute names. */ + private int direction = DIRECTION_UNKNOWN; + + /** + * Direction-scoped attributes ({@code server.address}) set before the span kind, as name/value + * pairs, stored once the kind is known ({@code null} if none). + */ + private List heldUntilKind; + private boolean ignoreActiveSpan; /** @@ -103,7 +117,7 @@ public SpanBuilder setAttribute(String key, String value) { return this; } // Store as object to prevent delegate to remove tag when value is empty - this.delegate.withTag(key, (Object) value); + withAttribute(key, (Object) value); return this; } @@ -114,13 +128,23 @@ public SpanBuilder setAttribute(String key, long value) { this.overriddenHttpStatusCode = (int) value; return this; } - this.delegate.withTag(key, value); + String tag = tagOf(key); + if (tag == null) { + holdUntilKind(key, value); + } else { + this.delegate.withTag(tag, value); + } return this; } @Override public SpanBuilder setAttribute(String key, double value) { - this.delegate.withTag(key, value); + String tag = tagOf(key); + if (tag == null) { + holdUntilKind(key, value); + } else { + this.delegate.withTag(tag, value); + } return this; } @@ -131,7 +155,12 @@ public SpanBuilder setAttribute(String key, boolean value) { this.overriddenAnalyticsSampleRate = value ? 1 : 0; return this; } - this.delegate.withTag(key, value); + String tag = tagOf(key); + if (tag == null) { + holdUntilKind(key, value); + } else { + this.delegate.withTag(tag, value); + } return this; } @@ -176,17 +205,47 @@ public SpanBuilder setAttribute(AttributeKey key, T value) { } break; default: - this.delegate.withTag(name, value); + withAttribute(name, value); break; } return this; } + private void withAttribute(String key, Object value) { + String tag = tagOf(key); + if (tag == null) { + holdUntilKind(key, value); + } else { + this.delegate.withTag(tag, value); + } + } + + /** + * The tag to store attribute {@code key} under, or {@code null} when it depends on the span's + * direction and the kind is not set yet. + */ + private String tagOf(String key) { + if (this.direction == DIRECTION_UNKNOWN && KnownTagCodec.isDirectionScoped(key)) { + return null; + } + return attributeTag(key, this.direction); + } + + private void holdUntilKind(String key, Object value) { + if (this.heldUntilKind == null) { + this.heldUntilKind = new ArrayList<>(2); + } + this.heldUntilKind.add(key); + this.heldUntilKind.add(value); + } + @Override public SpanBuilder setSpanKind(SpanKind spanKind) { if (spanKind != null) { - this.delegate.withTag(SPAN_KIND, toSpanKindTagValue(spanKind)); + String kind = toSpanKindTagValue(spanKind); + this.delegate.withTag(SPAN_KIND, kind); this.spanKindSet = true; + this.direction = KnownTagCodec.directionOfKind(kind); } return this; } @@ -203,6 +262,12 @@ public Span startSpan() { if (!this.spanKindSet) { setSpanKind(INTERNAL); } + if (this.heldUntilKind != null) { + for (int i = 0; i < this.heldUntilKind.size(); i += 2) { + String key = (String) this.heldUntilKind.get(i); + this.delegate.withTag(attributeTag(key, this.direction), this.heldUntilKind.get(i + 1)); + } + } if (!this.ignoreActiveSpan) { setParent(Context.current()); } diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14DirectionalAttributesTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14DirectionalAttributesTest.java new file mode 100644 index 00000000000..64fce0a8d57 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14DirectionalAttributesTest.java @@ -0,0 +1,83 @@ +package opentelemetry14; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TagsMatcher.defaultTags; +import static datadog.trace.agent.test.assertions.TagsMatcher.tag; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; +import static datadog.trace.test.junit.utils.assertions.Matchers.is; + +import datadog.opentelemetry.shim.trace.OtelConventions; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import org.tabletest.junit.TableTest; + +/** + * A few OpenTelemetry attribute names denote a different Datadog tag depending on the span's kind: + * {@code server.address} is {@code http.hostname} on a server span and {@code peer.hostname} on a + * client span. The shim resolves them by the kind, so the tracer core only sees Datadog names. + */ +public class OpenTelemetry14DirectionalAttributesTest extends AbstractOpenTelemetry14Test { + + @TableTest({ + "scenario | kind | when | attribute | tag ", + "client address, kind first | CLIENT | after kind | server.address | peer.hostname ", + "client address, attribute first | CLIENT | before kind | server.address | peer.hostname ", + "client address, after start | CLIENT | after start | server.address | peer.hostname ", + "server address, kind first | SERVER | after kind | server.address | http.hostname ", + "server address, attribute first | SERVER | before kind | server.address | http.hostname ", + "server address, after start | SERVER | after start | server.address | http.hostname ", + "producer address | PRODUCER | after kind | server.address | peer.hostname ", + "internal address stays as is | INTERNAL | after kind | server.address | server.address" + }) + void aDirectionScopedAttributeIsStoredAsTheTagItDenotes( + String kind, String when, String attribute, String tag) { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + if ("before kind".equals(when)) { + builder.setAttribute(attribute, "example.com"); + } + builder.setSpanKind(SpanKind.valueOf(kind)); + if ("after kind".equals(when)) { + builder.setAttribute(attribute, "example.com"); + } + Span span = builder.startSpan(); + if ("after start".equals(when)) { + span.setAttribute(attribute, "example.com"); + } + span.end(); + + assertTraces( + trace( + span() + .root() + .tags( + defaultTags(), + tag(SPAN_KIND, is(OtelConventions.toSpanKindTagValue(SpanKind.valueOf(kind)))), + tag(tag, is("example.com"))))); + } + + @TableTest({ + "scenario | kind | tag ", + "client server.port | CLIENT | peer.port ", + "server server.port | SERVER | server.port" + }) + void serverPortIsThePeerPortOnClientSpansOnly(String kind, String tag) { + Span span = + this.otelTracer + .spanBuilder("some-name") + .setAttribute("server.port", 8443L) + .setSpanKind(SpanKind.valueOf(kind)) + .startSpan(); + span.end(); + + assertTraces( + trace( + span() + .root() + .tags( + defaultTags(), + tag(SPAN_KIND, is(OtelConventions.toSpanKindTagValue(SpanKind.valueOf(kind)))), + tag(tag, is(8443L))))); + } +} 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 dfcbd2266d0..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 @@ -40,15 +40,12 @@ 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; } @@ -56,7 +53,6 @@ public void setup() { @TearDown public void tearDown() { span.finish(); - spanWithPendingAlias.finish(); } @Benchmark @@ -94,27 +90,6 @@ public Object knownTag_byName() { return span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}"); } - /** 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 c7d8b91429f..9281c377afb 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 @@ -44,7 +44,6 @@ 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; @@ -133,37 +132,8 @@ public class DDSpanContext null // CUSTOM }; - /** - * The direction each span kind ordinal reads direction-dependent tag names in ({@code - * server.address} is {@code peer.hostname} on a client span), derived from {@link - * DirectionalTags#directionOfKind} so the two can't drift. Broker and custom kinds have none. - */ - private static final int[] TAG_DIRECTION_OF_SPAN_KIND = tagDirectionOfSpanKind(); - - private static int[] tagDirectionOfSpanKind() { - int[] directions = new int[SPAN_KIND_VALUES.length]; - for (int i = 0; i < directions.length; i++) { - directions[i] = KnownTagCodec.directionOfKind(SPAN_KIND_VALUES[i]); - } - return directions; - } - 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; @@ -700,11 +670,12 @@ private void seedAbsentTags(final TagMap map) { map.forEach( this, (ctx, tagEntry) -> { - if (ctx.isPresent(tagEntry)) { + final String tag = tagEntry.tag(); + if (ctx.unsafeTags.containsKey(tag)) { return; } - if (!ctx.interceptEntry(tagEntry)) { - ctx.storeEntry(tagEntry); + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { + ctx.unsafeTags.set(tagEntry); } }); } @@ -956,23 +927,6 @@ static boolean tagEquals(String tagValue, String tagLiteral) { */ public void setSpanKindOrdinal(String kind) { spanKindOrdinal = spanKindOrdinalOf(kind); - // Checked under the lock alias registration takes: a writer that read no direction registers - // its pending alias before this sees the flag, or reads the new kind and resolves at write. - synchronized (unsafeTags) { - if (hasPendingAliases()) { - // The span now has a direction: fold pending aliases into the tags they denote, so every - // later reader (peer service naming, stats, sampling rules, serialization) sees them - // resolved. - DirectionalTags.resolveScopedNames(unsafeTags, tagDirection()); - } - } - } - - /** - * The direction this span reads direction-dependent tag names in; see {@link DirectionalTags}. - */ - private int tagDirection() { - return TAG_DIRECTION_OF_SPAN_KIND[spanKindOrdinal]; } static byte spanKindOrdinalOf(String kind) { @@ -1025,25 +979,25 @@ public void setMetric(final CharSequence key, final Number value) { public void setMetric(final CharSequence key, final int value) { synchronized (unsafeTags) { - storeTag(key.toString(), value); + unsafeTags.set(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { synchronized (unsafeTags) { - storeTag(key.toString(), value); + unsafeTags.set(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { synchronized (unsafeTags) { - storeTag(key.toString(), value); + unsafeTags.set(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { synchronized (unsafeTags) { - storeTag(key.toString(), value); + unsafeTags.set(key.toString(), value); } } @@ -1053,98 +1007,6 @@ public void setMetric(final TagMap.EntryReader entry) { } synchronized (unsafeTags) { - storeEntry(entry); - } - } - - /* - * 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 DirectionalTags, 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 DirectionalTags). Call while holding the - * unsafeTags lock. - */ - private long writeKeyOf(String tag) { - long key = KnownTagCodec.keyOrAliasOf(tag); - if (key > 0) { - supersedePendingAliases(key); - return key; - } - if (key == 0) { - return 0L; - } - long aliasedId = DirectionalTags.prepareAliasWrite(unsafeTags, tag, tagDirection()); - if (aliasedId > 0) { - return aliasedId; - } - notePendingAlias(); // the registry said alias, so this is the sentinel: stored as written - return 0L; - } - - /** - * 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}. */ - boolean hasPendingAliases() { - return (routedTagFlags & KnownTagCodec.HAS_SCOPED_ALIAS) != 0; - } - - private void storeTag(String tag, Object value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - private void storeTag(String tag, CharSequence value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - private void storeTag(String tag, boolean value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - private void storeTag(String tag, int value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - private void storeTag(String tag, long value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - private void storeTag(String tag, float value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - private void storeTag(String tag, double value) { - unsafeTags.setResolved(writeKeyOf(tag), tag, value); - } - - /** {@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) { - supersedePendingAliases(tagId); - unsafeTags.set(entry); - } else if (KnownTagCodec.mayBeScopedAlias(entry.tag())) { - long aliasedId = DirectionalTags.prepareAliasWrite(unsafeTags, entry.tag(), tagDirection()); - if (aliasedId > 0) { - unsafeTags.set(aliasedId, entry.objectValue()); - } else { - if (aliasedId < 0) { - notePendingAlias(); // a real alias, stored as written; 0 is a filter false positive - } - unsafeTags.set(entry); - } - } else { unsafeTags.set(entry); } } @@ -1155,14 +1017,13 @@ public void removeTag(String tag) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (unsafeTags) { - // A direction-dependent name and the tag it denotes here are removed together. - DirectionalTags.remove(unsafeTags, tag, tagDirection()); + unsafeTags.remove(tag); } } /** - * 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. + * 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 */ @@ -1174,7 +1035,7 @@ public void removeTag(long tagId) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (unsafeTags) { - DirectionalTags.remove(unsafeTags, tagId, tagDirection()); + unsafeTags.getAndRemove(tagId); } } @@ -1190,74 +1051,18 @@ public void removeTag(long tagId) { * @param tag The tag name. * @param value The nullable tag value. */ - /** - * The id a name-keyed write of {@code tag} goes through: the tag's own id; for a direction-scoped - * alias ({@code server.address}) once the span has a direction, the tag it denotes; else 0. So an - * alias takes the id path, including interception (split-by-tags on {@code peer.hostname} sees a - * {@code server.address} write on a client span). - */ - private long idForWrite(String tag) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId == 0 && KnownTagCodec.mayBeScopedAlias(tag) && !splitsOnAliasName(tag)) { - long aliasedId = KnownTagCodec.directionalKeyOf(tag, tagDirection()); - if (aliasedId > 0) { - return aliasedId; - } - } - return tagId; - } - - /** - * Whether split-by-tags names {@code alias} itself ({@code - * dd.trace.split-by-tags=server.address}). Such a write stays on the custom path, where the - * interceptor matches the configured name, instead of being routed as the tag the alias denotes. - */ - private boolean splitsOnAliasName(String alias) { - return tagInterceptor.needsInterceptCustomTag(alias); - } - - /** - * {@link TagInterceptor#interceptTag(DDSpanContext, TagMap.EntryReader)} for an entry about to be - * stored through {@link #storeEntry}, which resolves a direction-scoped alias to its tag: such an - * entry is intercepted as that tag, so routing sees the tag it is stored as. - */ - private boolean interceptEntry(TagMap.EntryReader entry) { - if (entry.tagId() == 0 - && KnownTagCodec.mayBeScopedAlias(entry.tag()) - && !splitsOnAliasName(entry.tag())) { - long aliasedId = KnownTagCodec.directionalKeyOf(entry.tag(), tagDirection()); - if (aliasedId > 0) { - return tagInterceptor.needsIntercept(aliasedId, (int) KnownTagCodec.INTERCEPTED) - && tagInterceptor.interceptTag(this, aliasedId, entry.objectValue()); - } - } - return tagInterceptor.interceptTag(this, entry); - } - - /** - * Whether {@code entry}'s tag is already on this span, by the tag it denotes here: a - * direction-scoped alias is present if its tag is, and a tag if a pending alias for it is. - */ - private boolean isPresent(TagMap.EntryReader entry) { - if (KnownTagCodec.isDirectionDependent(entry)) { - return DirectionalTags.get(unsafeTags, entry.tag(), tagDirection(), hasPendingAliases()) - != null; - } - return unsafeTags.containsKey(entry.tag()); - } - public void setTag(final String tag, final Object value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, value); } } } @@ -1266,45 +1071,18 @@ public void setTag(final String tag, final String value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, 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 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) { - supersedePendingAliases(tagId); - } - } - - /** - * Removes the pending aliases a write of {@code tagId} supersedes, if it has an alias and the - * span holds any. Callers hold the {@code unsafeTags} lock. - */ - private void supersedePendingAliases(long tagId) { - if (KnownTagCodec.hasScopedAlias(tagId) && hasPendingAliases()) { - DirectionalTags.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 @@ -1316,7 +1094,7 @@ public void setTag(final long tagId, final Object value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) + } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); @@ -1330,7 +1108,7 @@ public void setTag(final long tagId, final CharSequence value) { } if (null == value) { removeTag(tagId); - } else if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) + } else if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); @@ -1342,7 +1120,7 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1355,7 +1133,7 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1368,7 +1146,7 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1381,7 +1159,7 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1394,7 +1172,7 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { synchronized (unsafeTags) { @@ -1409,9 +1187,9 @@ public void setTag(TagMap.EntryReader entry) { } // the interceptor prechecks the entry, to avoid boxing - if (!interceptEntry(entry)) { + if (!tagInterceptor.interceptTag(this, entry)) { synchronized (unsafeTags) { - storeEntry(entry); + unsafeTags.set(entry); } } } @@ -1440,13 +1218,13 @@ public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, value); } } } @@ -1455,13 +1233,13 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, value); } } } @@ -1470,13 +1248,13 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, value); } } } @@ -1485,13 +1263,13 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, value); } } } @@ -1500,13 +1278,13 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - final long tagId = idForWrite(tag); + final long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (unsafeTags) { - storeTag(tag, value); + unsafeTags.set(tag, value); } } } @@ -1528,12 +1306,11 @@ void setAllTags(final TagMap map, boolean needsIntercept) { map.forEach( this, (ctx, tagEntry) -> { - if (!ctx.interceptEntry(tagEntry)) { - ctx.storeEntry(tagEntry); + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { + ctx.unsafeTags.set(tagEntry); } }); } else { - // needsIntercept(TagMap) is also true for a map with a direction-dependent name. unsafeTags.putAll(map); } } @@ -1552,13 +1329,12 @@ void setAllTags(final TagMap.Ledger ledger) { // mirror removeTag(String): keep the cached ordinal in sync with unsafeTags spanKindOrdinal = SPAN_KIND_UNSET; } - // as removeTag(String): a direction-dependent name and the tag it denotes go together - DirectionalTags.remove(unsafeTags, tag, tagDirection()); + unsafeTags.remove(tag); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; - if (!interceptEntry(entry)) { - storeEntry(entry); + if (!tagInterceptor.interceptTag(this, entry)) { + unsafeTags.set(entry); } } } @@ -1573,12 +1349,7 @@ void setAllTags(final Map map) { } else if (!map.isEmpty()) { synchronized (unsafeTags) { for (final Map.Entry tag : map.entrySet()) { - long tagId = idForWrite(tag.getKey()); - boolean intercepted = - tagId != 0 - ? tagInterceptor.interceptTag(this, tagId, tag.getValue()) - : tagInterceptor.interceptCustomTag(this, tag.getKey(), tag.getValue()); - if (!intercepted) { + if (!tagInterceptor.interceptTag(this, tag.getKey(), tag.getValue())) { unsafeSetTag(tag.getKey(), tag.getValue()); } } @@ -1587,19 +1358,19 @@ void setAllTags(final Map map) { } void unsafeSetTag(final String tag, final Object value) { - storeTag(tag, value); + unsafeTags.set(tag, value); } void unsafeSetTag(final String tag, final CharSequence value) { - storeTag(tag, value); + unsafeTags.set(tag, value); } void unsafeSetTag(final String tag, final byte value) { - storeTag(tag, value); + unsafeTags.set(tag, value); } void unsafeSetTag(final String tag, final double value) { - storeTag(tag, value); + unsafeTags.set(tag, value); } Object getTag(final String key) { @@ -1632,7 +1403,7 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return DirectionalTags.get(unsafeTags, tag, tagDirection(), hasPendingAliases()); + return unsafeTags.getObject(tag); } @Deprecated diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java b/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java deleted file mode 100644 index 6c70e99d387..00000000000 --- a/dd-trace-core/src/main/java/datadog/trace/core/DirectionalTags.java +++ /dev/null @@ -1,230 +0,0 @@ -package datadog.trace.core; - -import static datadog.trace.api.KnownTagCodec.DIRECTION_INBOUND; -import static datadog.trace.api.KnownTagCodec.DIRECTION_NONE; -import static datadog.trace.api.KnownTagCodec.DIRECTION_UNKNOWN; - -import datadog.trace.api.KnownTagCodec; -import datadog.trace.api.TagMap; -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 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 -- 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}. - * - *

An alias can also be another tag's Datadog name: {@code server.port} is {@code peer.port} on - * outbound spans, and its own tag on every other kind. It is handled like any other alias; only a - * read before the span has a direction falls back to the tag it names itself. - * - *

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

A removal counts as a write. Before the span has a direction, removing a tag also removes the - * pending aliases that could denote it, and removing an alias also removes every tag it could - * denote, so a removed value can't resurface once the span learns its kind. - * - *

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

One more: an alias written before the span has a direction is resolved when the kind arrives, - * without passing through {@code TagInterceptor} again, so split-by-tags on its tag doesn't see it. - * An alias written once the direction is known is intercepted as its tag. - */ -final class DirectionalTags { - - /** - * Prepares a write of {@code alias} to a span of {@code direction}. Returns the id of the tag to - * store the value under; {@link KnownTagCodec#NO_TAG_IN_DIRECTION_SENTINEL} when {@code alias} is - * a direction-scoped alias that can't resolve yet, so its value is stored as written, pending; or - * 0 when it is no alias at all, so its value is an ordinary custom tag. Callers that keep a - * pending-alias flag set it only for the sentinel, so a name that merely passed {@link - * KnownTagCodec#mayBeScopedAlias}'s filter doesn't slow the span down. - */ - static long prepareAliasWrite(TagMap tags, String alias, int direction) { - long tagId = KnownTagCodec.directionalKeyOf(alias, direction); - if (tagId > 0) { - tags.remove(alias); // a value pending under the alias is older than this write - } - return 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. - */ - static void prepareWrite(TagMap tags, long tagId, int direction) { - if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, tagId, 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. - */ - 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); - } - } - } - - /** - * 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 the same - * single lookup as {@link TagMap#getObject}. - */ - static Object get(TagMap tags, String name, int direction) { - return get(tags, name, direction, true); - } - - /** - * {@link #get(TagMap, String, int)} for a caller that tracks whether {@code tags} can hold a - * pending alias at all. With {@code mayHavePendingAliases} false, a tag with an alias skips - * probing for one, so it costs a single lookup like any other tag. - */ - static Object get(TagMap tags, String name, int direction, boolean mayHavePendingAliases) { - long key = KnownTagCodec.keyOrAliasOf(name); - if (key > 0) { - return get(tags, key, direction, mayHavePendingAliases); - } - TagMap.Entry asWritten = - key == 0 || mayHavePendingAliases ? tags.getEntryResolved(0L, name) : null; - if (key == 0 || asWritten != null) { - return asWritten == null ? null : asWritten.objectValue(); - } - long tagId = - direction == DIRECTION_UNKNOWN - ? KnownTagCodec.datadogKeyOf(name) // server.port, written by id, is still server.port - : KnownTagCodec.directionalKeyOf(name, direction); - return tagId > 0 ? tags.internals().getKnownValue(tagId) : null; - } - - /** - * The value of the known tag {@code tagId} on a span of {@code direction}; see {@link - * #get(TagMap, String, int)}. - */ - static Object get(TagMap tags, long tagId, int direction) { - return get(tags, tagId, direction, true); - } - - /** {@link #get(TagMap, long, int)}; see {@link #get(TagMap, String, int, boolean)}. */ - static Object get(TagMap tags, long tagId, int direction, boolean mayHavePendingAliases) { - if (mayHavePendingAliases - && KnownTagCodec.hasScopedAlias(tagId) - && direction != DIRECTION_UNKNOWN) { - String alias = KnownTagCodec.directionScopedNameOf(tagId, direction); - Object pending = alias == null ? null : tags.getObject(alias); - if (pending != null) { - return pending; - } - } - return tags.internals().getKnownValue(tagId); - } - - /** Removes {@code name} from a span of {@code direction}: the tag and its alias together. */ - static void remove(TagMap tags, String name, int direction) { - // One lookup decides everything: a known tag, a custom tag, or a direction-scoped alias. - long key = KnownTagCodec.keyOrAliasOf(name); - if (key > 0) { - remove(tags, key, direction); - return; - } - tags.removeResolved(0L, name); // a custom tag, or an alias stored as written - if (key == 0) { - return; - } - if (direction == DIRECTION_UNKNOWN) { - removeEveryTagOfAlias(tags, name); - return; - } - long tagId = KnownTagCodec.directionalKeyOf(name, direction); - if (tagId > 0) { - tags.getAndRemove(tagId); - } - } - - /** Removes the known tag {@code tagId}, and its alias, from a span of {@code direction}. */ - static void remove(TagMap tags, long tagId, int direction) { - tags.getAndRemove(tagId); - if (KnownTagCodec.hasScopedAlias(tagId)) { - removePendingAliases(tags, tagId, direction); - } - } - - /** - * Removes every tag {@code alias} could denote, for a removal of the alias before the span has a - * direction. A removal counts as a write, so no older value can resurface when the kind arrives; - * like the write rule, this can drop the other direction's tag. (Removing a tag before then - * likewise removes the pending aliases that could denote it; see {@link #remove(TagMap, long, - * int)}.) - */ - private static void removeEveryTagOfAlias(TagMap tags, String alias) { - for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { - long aliasedId = KnownTagCodec.directionalKeyOf(alias, direction); - if (aliasedId > 0) { - tags.getAndRemove(aliasedId); - } - } - } - - /** - * 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. - */ - static void resolveScopedNames(TagMap tags, int direction) { - if (direction == DIRECTION_UNKNOWN) { - return; - } - List pending = null; - for (TagMap.EntryReader entry : tags) { - if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { - if (pending == null) { - pending = new ArrayList<>(2); - } - pending.add(entry.entry()); - } - } - if (pending == null) { - return; - } - for (TagMap.Entry entry : pending) { - tags.remove(entry.tag()); - tags.set(KnownTagCodec.directionalKeyOf(entry.tag(), direction), entry.objectValue()); - } - } - - private DirectionalTags() {} -} 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 e54e8faf099..b487d4665b7 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 @@ -139,14 +139,9 @@ 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 {@code DirectionalTags}. - * 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) || KnownTagCodec.isDirectionDependent(entry)) return true; + if (needsIntercept(entry)) return true; } return false; } @@ -169,23 +164,12 @@ public boolean needsIntercept(Map map) { } /** - * 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. + * 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 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); + return KnownTagCodec.isIntercepted(tagId) || isSplitServiceTag(tagId); } public boolean needsIntercept(String tag) { @@ -207,10 +191,6 @@ 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); @@ -289,10 +269,7 @@ public boolean interceptTag(DDSpanContext span, long tagId, Object value) { public boolean interceptTag(DDSpanContext span, TagMap.EntryReader entry) { long tagId = entry.tagId(); if (tagId != 0) { - // Only real routes: an entry that isn't routed is stored through DDSpanContext.storeEntry, - // which already supersedes any pending alias, so HAS_SCOPED_ALIAS needs no detour here. - return needsIntercept(tagId, (int) KnownTagCodec.INTERCEPTED) - && interceptTag(span, tagId, entry.objectValue()); + return needsIntercept(tagId) && interceptTag(span, tagId, entry.objectValue()); } return isSplitServiceTag(entry.tag()) && splitService(span, entry.objectValue()); } 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 deleted file mode 100644 index 1fbb290d8b8..00000000000 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagConsistencyTest.java +++ /dev/null @@ -1,289 +0,0 @@ -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.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; - -import datadog.trace.api.KnownTagCodec; -import datadog.trace.api.TagMap; -import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; -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; -import org.tabletest.junit.TableTest; - -/** - * 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)); - } - - /** - * The alias filter passes some custom names that aren't aliases. Seeding one must not mark the - * span as holding a pending alias, which would keep its aliased-tag writes and its serialization - * on the slow path for the span's whole life. - */ - @Test - void aCustomTagThatOnlyPassesTheAliasFilterLeavesTheSpanOnTheFastPath() { - String lookalike = "custom.tag18"; - assertTrue(KnownTagCodec.mayBeScopedAlias(lookalike), "precondition: a filter false positive"); - - DDSpan built = - (DDSpan) - tracer - .buildSpan("datadog", "built") - .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) - .withTag(lookalike, "value") - .start(); - try { - assertFalse(built.spanContext().hasPendingAliases()); - assertEquals("value", built.getTag(lookalike)); - } finally { - built.finish(); - } - } - - @Test - void aRealAliasWrittenBeforeTheKindIsKnownMarksTheSpan() { - DDSpan built = - (DDSpan) tracer.buildSpan("datadog", "built").withTag("server.address", "host").start(); - try { - assertTrue(built.spanContext().hasPendingAliases()); - } finally { - built.finish(); - } - } - - /** A removal before the span has a kind must not let an older value resurface once it does. */ - @Test - void removingAnAliasBeforeTheKindIsKnownSticksOnceTheKindArrives() { - span.setTag(Tags.PEER_HOSTNAME, "older"); - span.setTag("server.address", (Object) null); - - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - - assertNull(span.getTag(Tags.PEER_HOSTNAME)); - assertNull(span.getTag("server.address")); - } - - @Test - void removingATagBeforeTheKindIsKnownDropsAPendingAliasForIt() { - span.setTag("server.address", "pending"); - span.setTag(Tags.PEER_HOSTNAME, (Object) null); - - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - - assertNull(span.getTag(Tags.PEER_HOSTNAME)); - } - - /** - * A pending alias is folded into its tag as soon as the span learns its kind, not at - * serialization, so readers that run before then (peer service naming, stats) see the tag. - */ - @Test - void aPendingAliasIsResolvedAsSoonAsTheKindIsKnown() { - span.setTag("server.address", "db.example.com"); - - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - - TagMap stored = span.getTags(); - assertEquals("db.example.com", stored.getObject(Tags.PEER_HOSTNAME)); - assertNull(stored.getObject("server.address")); - } - - /** A prototype's alias default must not overwrite an explicit value of the tag it denotes. */ - @Test - void aPrototypeAliasDefaultDoesNotClobberAnExplicitTag() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - span.setTag(Tags.PEER_HOSTNAME, "explicit"); - - span.apply(SpanPrototype.builder().initTag("server.address", "from-prototype").build()); - - assertEquals("explicit", span.getTag(Tags.PEER_HOSTNAME)); - } - - /** A builder removal of an alias also removes the tag a direction-aware write stored it as. */ - @Test - void aBuilderRemovalOfAnAliasRemovesTheTagItWasStoredAs() { - DDSpan built = - (DDSpan) - tracer - .buildSpan("datadog", "built") - .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) - .withTag("server.address", "host") - .withTag("server.address", (Object) null) - .start(); - try { - assertNull(built.getTag(Tags.PEER_HOSTNAME)); - assertNull(built.getTags().getObject(Tags.PEER_HOSTNAME)); - } finally { - built.finish(); - } - } - - @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)); - } - - @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(); - DirectionalTags.resolveScopedNames(emitted, direction); - assertEquals("second.example", emitted.getObject(tag)); - assertNull(emitted.getObject("server.address")); - } - - /** - * The accepted corner (see DirectionalTags): 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); - 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 deleted file mode 100644 index 5e3fec23557..00000000000 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java +++ /dev/null @@ -1,196 +0,0 @@ -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 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; - 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 aNameDeclaredPerDirectionIsOneTagInEveryDirection() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - span.setTag(Tags.PEER_PORT, 5432); - - assertEquals(5432, storedValue(KnownTags.PEER_PORT_ID)); - assertEquals(5432, span.getTag(Tags.PEER_PORT)); - } - - @Test - void aNameDeclaredPerDirectionIsStoredBeforeTheSpanHasAKind() { - span.setTag(Tags.PEER_PORT, "1234"); - - assertEquals("1234", storedValue(KnownTags.PEER_PORT_ID)); - assertEquals("1234", span.getTag(Tags.PEER_PORT)); - } - - @Test - void aDirectionScopedOpenTelemetryNameReadsAsTheTagItDenotes() { - 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 aNameSetBeforeTheSpanHasADirectionReadsAsTheTagOnceItHasOne() { - span.setTag("server.address", "outbound.example"); - assertNull(span.getTag(Tags.PEER_HOSTNAME)); - - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - - assertEquals("outbound.example", span.getTag(Tags.PEER_HOSTNAME)); - assertEquals("outbound.example", span.getTag("server.address")); - } - - @Test - 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("scoped.example", span.getTag(Tags.PEER_HOSTNAME)); - assertEquals("scoped.example", span.getTag("server.address")); - assertEquals("scoped.example", storedValue(KnownTags.PEER_HOSTNAME_ID)); - } - - @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 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_ID)); - assertNull(span.getTag(Tags.PEER_PORT)); - } - - @Test - void anInternalSpanStoresPeerPortAsItsOneTag() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_INTERNAL); - span.setTag(Tags.PEER_PORT, 5432); - - assertEquals(5432, storedValue(KnownTags.PEER_PORT_ID)); - assertEquals(5432, span.getTag(Tags.PEER_PORT)); - } - - @Test - void aDatadogNameThatIsAlsoAnAliasIsItsOwnTagOnAServerSpan() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); - span.setTag("server.port", 8080); - - assertEquals(8080, storedValue(KnownTags.SERVER_PORT_ID)); - assertEquals(8080, span.getTag("server.port")); - assertNull(storedValue(KnownTags.PEER_PORT_ID)); - } - - @Test - void aDatadogNameThatIsAlsoAnAliasIsTheAliasedTagOnAClientSpan() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - span.setTag("server.port", 443); - - assertEquals(443, storedValue(KnownTags.PEER_PORT_ID)); - assertEquals(443, span.getTag("server.port")); - assertNull(storedValue(KnownTags.SERVER_PORT_ID)); - } - - @Test - void aDatadogNameThatIsAlsoAnAliasIsItsOwnTagOnAnInternalSpan() { - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_INTERNAL); - span.setTag("server.port", 8080); - - assertEquals(8080, storedValue(KnownTags.SERVER_PORT_ID)); - assertNull(storedValue(KnownTags.PEER_PORT_ID)); - } - - @Test - void aDatadogNameThatIsAlsoAnAliasResolvesOnceTheSpanHasAKind() { - span.setTag("server.port", 443); - assertEquals(443, span.getTag("server.port")); - - span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - - assertEquals(443, storedValue(KnownTags.PEER_PORT_ID)); - assertNull(storedValue(KnownTags.SERVER_PORT_ID)); - } - - @Test - void aTagWrittenByIdIsReadByItsDatadogNameBeforeTheSpanHasAKind() { - span.setTag(KnownTags.SERVER_PORT_ID, 8080); - - assertEquals(8080, span.getTag("server.port")); - } - - private Object storedValue(long tagId) { - TagMap.Entry entry = span.getTags().getEntry(tagId); - return entry == null ? null : entry.objectValue(); - } -} 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 247d9f3958f..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 @@ -68,6 +68,10 @@ void anIdReplacesAValueSetUnderTheName() { span.setTag(KnownTags.PEER_PORT_ID, 443); 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/DirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java deleted file mode 100644 index 4bdb3c2d004..00000000000 --- a/dd-trace-core/src/test/java/datadog/trace/core/DirectionalTagsTest.java +++ /dev/null @@ -1,271 +0,0 @@ -package datadog.trace.core; - -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.assertNull; - -import datadog.trace.api.KnownTagCodec; -import datadog.trace.api.KnownTags; -import datadog.trace.api.TagMap; -import datadog.trace.bootstrap.instrumentation.api.Tags; -import org.junit.jupiter.api.Test; -import org.tabletest.junit.TableTest; - -/** - * {@link DirectionalTags}: a span's reading of direction-dependent names over a plain {@link - * TagMap}. The span-level behavior, with real spans, is in the directional span tests. - */ -class DirectionalTagsTest { - @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", DirectionalTags.get(tags, alias, direction)); - assertEquals("stored", DirectionalTags.get(tags, tag, direction)); - assertEquals("stored", DirectionalTags.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", DirectionalTags.get(tags, alias, direction)); - assertEquals("pending", DirectionalTags.get(tags, tag, direction)); - assertEquals("pending", DirectionalTags.get(tags, KnownTagCodec.keyOf(tag), direction)); - } - - @Test - void withNoDirectionAnAliasReadsOnlyAsWritten() { - TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "stored"); - - assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_UNKNOWN)); - assertEquals("stored", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); - assertEquals( - "stored", DirectionalTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN)); - - tags.set("server.address", "pending"); - assertEquals("pending", DirectionalTags.get(tags, "server.address", DIRECTION_UNKNOWN)); - assertEquals("stored", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN)); - } - - @Test - void aMissReadsAsNull() { - TagMap tags = TagMap.create(); - - assertNull(DirectionalTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_OUTBOUND)); - // client.port names no tag on a client span - assertNull(DirectionalTags.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", DirectionalTags.get(tags, "my.tag", DIRECTION_OUTBOUND)); - assertEquals("GET", DirectionalTags.get(tags, Tags.HTTP_METHOD, DIRECTION_OUTBOUND)); - assertEquals("GET", DirectionalTags.get(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND)); - } - - @Test - void anAliasWriteOnceTheDirectionIsKnownBecomesItsTag() { - TagMap tags = TagMap.create(); - tags.set("server.address", "older"); - - long tagId = DirectionalTags.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 | result", - "no direction yet | server.address | -1 | -3 ", - "no tag in the direction | client.port | 1 | -3 ", - "internal span | server.address | 2 | -3 ", - "not an alias | my.tag | 1 | 0 " - }) - void anAliasWriteThatCannotResolveIsStoredAsWritten(String name, int direction, long result) { - TagMap tags = TagMap.create(); - tags.set(name, "pending"); - - // the sentinel (-3) marks a real alias left pending; 0 marks a name that is no alias at all - assertEquals(result, DirectionalTags.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); - - DirectionalTags.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); - - DirectionalTags.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"); - - DirectionalTags.prepareWrite(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN); - DirectionalTags.prepareWrite(tags, KnownTags.PEER_HOSTNAME_ID, DIRECTION_NONE); - - assertEquals("pending", tags.getObject("server.address")); - } - - @Test - void removingAnAliasRemovesItsTag() { - TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "stored"); - tags.set("server.address", "pending"); - - DirectionalTags.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"); - DirectionalTags.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"); - DirectionalTags.remove(byId, KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND); - assertNull(byId.getObject(Tags.HTTP_HOSTNAME)); - assertNull(byId.getObject("server.address")); - } - - @Test - void removingATagWithNoAliasLeavesUnrelatedTags() { - TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "stored"); - tags.set("my.tag", "custom"); - - DirectionalTags.remove(tags, "my.tag", DIRECTION_OUTBOUND); - DirectionalTags.remove(tags, KnownTags.HTTP_METHOD_ID, DIRECTION_OUTBOUND); - - assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); - 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"); - - DirectionalTags.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"); - - DirectionalTags.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"); - DirectionalTags.resolveScopedNames(unknown, DIRECTION_UNKNOWN); - assertEquals("pending", unknown.getObject("server.address")); - } - - @Test - void resolvingWithNoAliasChangesNothing() { - TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "stored"); - - DirectionalTags.resolveScopedNames(tags, DIRECTION_OUTBOUND); - - assertEquals("stored", tags.getObject(Tags.PEER_HOSTNAME)); - assertEquals(1, tags.size()); - } - - @Test - void removingAnAliasBeforeTheDirectionIsKnownRemovesEveryTagItCouldDenote() { - TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "older"); - - DirectionalTags.remove(tags, "server.address", DIRECTION_UNKNOWN); - - // once the span is a client, server.address names peer.hostname: the removal must stick - assertNull(DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - assertNull(DirectionalTags.get(tags, "server.address", DIRECTION_OUTBOUND)); - } - - @Test - void removingATagBeforeTheDirectionIsKnownRemovesThePendingAliasesThatCouldDenoteIt() { - TagMap byName = TagMap.create(); - byName.set("server.address", "pending"); - DirectionalTags.remove(byName, Tags.PEER_HOSTNAME, DIRECTION_UNKNOWN); - assertNull(DirectionalTags.get(byName, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - - TagMap byId = TagMap.create(); - byId.set("server.address", "pending"); - DirectionalTags.remove(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_UNKNOWN); - assertNull(DirectionalTags.get(byId, KnownTags.PEER_HOSTNAME_ID, DIRECTION_OUTBOUND)); - } - - @Test - void removingACustomTagBeforeTheDirectionIsKnownLeavesOtherTags() { - TagMap tags = TagMap.create(); - tags.set(Tags.PEER_HOSTNAME, "kept"); - tags.set("my.tag", "gone"); - - DirectionalTags.remove(tags, "my.tag", DIRECTION_UNKNOWN); - - assertNull(tags.getObject("my.tag")); - assertEquals("kept", DirectionalTags.get(tags, Tags.PEER_HOSTNAME, DIRECTION_OUTBOUND)); - } -} 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 a8211f74459..6cb01e76f08 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 @@ -421,45 +421,6 @@ void splitByTagsSeparateWritesAreLastWriterWins() { assertEquals(DDTags.LANGUAGE_TAG_VALUE, span.getServiceName()); } - /** Split-by-tags on a tag also splits on its direction-scoped alias, by name or in bulk. */ - @Test - void splitByTagsSeesAnAliasAsTheTagItDenotes() { - CoreTracer tracer = createSplittingTracer(Tags.PEER_HOSTNAME); - - AgentSpan byName = tracer.buildSpan("datadog", "by name").start(); - byName.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - byName.setTag("server.address", "by-name-host"); - byName.finish(); - assertEquals("by-name-host", byName.getServiceName()); - - AgentSpan inBulk = tracer.buildSpan("datadog", "in bulk").start(); - inBulk.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - inBulk.setAllTags(Collections.singletonMap("server.address", "bulk-host")); - inBulk.finish(); - assertEquals("bulk-host", inBulk.getServiceName()); - } - - /** Split-by-tags that names the alias itself still splits on it, by name and from the builder. */ - @Test - void splitByTagsOnAnAliasNameStillSplitsOnIt() { - CoreTracer tracer = createSplittingTracer("server.address"); - - AgentSpan byName = tracer.buildSpan("datadog", "by name").start(); - byName.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); - byName.setTag("server.address", "by-name-host"); - byName.finish(); - assertEquals("by-name-host", byName.getServiceName()); - - AgentSpan built = - tracer - .buildSpan("datadog", "built") - .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) - .withTag("server.address", "built-host") - .start(); - built.finish(); - assertEquals("built-host", built.getServiceName()); - } - @Test void setResourceName() throws Exception { ListWriter writer = new ListWriter(); @@ -1106,16 +1067,9 @@ void exactlyTheTagsWithTheInterceptedBitHaveACase() throws IllegalAccessExceptio consumed |= interceptor.interceptTag(span, tagId, value); } - // 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")); + boolean hasCase = consumed || !mockingDetails(span).getInvocations().isEmpty(); assertEquals(KnownTagCodec.isIntercepted(tagId), hasCase, field.getName()); - assertEquals( - KnownTagCodec.isIntercepted(tagId) || KnownTagCodec.hasScopedAlias(tagId), - interceptor.needsIntercept(tagId), - field.getName()); + assertEquals(KnownTagCodec.isIntercepted(tagId), interceptor.needsIntercept(tagId)); } } @@ -1154,30 +1108,13 @@ void splitByTagsMatchesACustomTagByName() { } @Test - void splitByTagsOnANameDeclaredPerDirectionMatchesItsTag() { + void splitByTagsOnANameDeclaredPerDirectionMatchesItsOneTag() { TagInterceptor interceptor = splittingInterceptor(Tags.PEER_PORT); assertTrue(interceptor.needsIntercept(KnownTags.PEER_PORT_ID)); 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 5a5b8e8536b..790493626bd 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -13,15 +13,17 @@ */ public final class KnownTagCodec { /* - * 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 + * tagId bit layout: [63-48 serialNum (16 bits)] [47-32 reserved, zero] [31-0 flags]. 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 * 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), and bit 1 is the INTERCEPTED 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 @@ -63,58 +65,6 @@ 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; - - /** The direction of a span with these tags, from its {@code span.kind}. */ - public static int directionOf(TagMap tags) { - Object kind = tags.getKnownValue(KnownTags.SPAN_KIND_ID); - return kind == null ? DIRECTION_UNKNOWN : directionOfKind(kind.toString()); - } - - /** 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; - } - } - - /** - * True if a span must apply its direction to {@code entry}: a known tag with {@link - * #HAS_SCOPED_ALIAS}, or a direction-scoped alias stored as written. A mask test for a known tag; - * a filter test, then one lookup, for a custom one. - */ - public static boolean isDirectionDependent(TagMap.EntryReader entry) { - long tagId = entry.tagId(); - if (tagId != 0) { - return hasScopedAlias(tagId); - } - String tag = entry.tag(); - return mayBeScopedAlias(tag) && keyOrAliasOf(tag) == DIRECTION_SCOPED_OTEL_NAME_SENTINEL; - } - - /** 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; @@ -134,10 +84,10 @@ public static long makeTagId(int serialNum) { * 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). + * openTelemetryTagOf(long, int). */ - /** Direction not known, for example a span with no kind: only names that need none resolve. */ + /** 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. */ @@ -149,24 +99,30 @@ public static long makeTagId(int serialNum) { /** 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 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. {@link #keyOrAliasOf} returns it too. - */ - public static final long DIRECTION_SCOPED_OTEL_NAME_SENTINEL = -2L; + /** 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()); + } - /** - * 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 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 { @@ -180,11 +136,8 @@ public interface Resolver { */ String openTelemetryNameOf(long tagId, int direction); - /** - * 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); + /** 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); @@ -239,9 +192,9 @@ public static String openTelemetryTagOf(long tagId) { } /** - * {@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. + * {@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; @@ -266,114 +219,9 @@ 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. A name whose tag - * depends on the span's direction also resolves to 0; see {@link #directionalKeyOf}. - */ + /** The id for {@code name} in any namespace, or 0 when it is not a known tag. */ public static long keyOf(String name) { - long key = Installed.RESOLVER.lookup(name); - return key < 0 ? 0L : key; - } - - /** - * 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. - */ - public static long directionalKeyOf(String name, int direction) { - return Installed.RESOLVER.directionalKeyOf(name, 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. - */ - public static String directionScopedNameOf(long tagId, int direction) { - if (direction == DIRECTION_UNKNOWN || !hasScopedAlias(tagId)) { - return null; - } - Resolver resolver = Installed.RESOLVER; - String name = resolver.openTelemetryNameOf(tagId, direction); - return name != null && resolver.openTelemetryNameOf(tagId, DIRECTION_UNKNOWN) == null - ? name - : null; - } - - /** {@link #directionScopedNameOf(long, int)} by Datadog name. */ - public static String directionScopedNameOf(String ddName, int direction) { - if (direction == DIRECTION_UNKNOWN) { - return null; - } - long tagId = keyOf(ddName); - return tagId != 0 ? directionScopedNameOf(tagId, direction) : null; - } - - /** - * {@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}). - */ - public static long keyOrAliasOf(String name) { - return Installed.RESOLVER.lookup(name); - } - - /** - * The tag whose Datadog name is {@code name}, even when the name depends on the span's direction - * ({@code server.port} is also {@code peer.port}'s outbound alias, so {@link #keyOf} returns 0 - * for it); 0 when no tag has that Datadog name. For a span that has no direction yet. - */ - public static long datadogKeyOf(String name) { - long key = keyOrAliasOf(name); - if (key != DIRECTION_SCOPED_OTEL_NAME_SENTINEL) { - return key < 0 ? 0L : key; - } - for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { - long tagId = directionalKeyOf(name, direction); - if (tagId > 0 && name.equals(nameOf(tagId))) { - return tagId; - } - } - return 0L; - } - - /** - * 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. - */ - 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 long aliasMask() { - long mask = 0L; - for (int serial = 1; serial < serialLimit(); serial++) { - String name = nameOf(makeTagId(serial)); - long tagId = keyOrAliasOf(name); - if (tagId == DIRECTION_SCOPED_OTEL_NAME_SENTINEL) { - // A Datadog name that is also an alias (server.port): the tag's only scoped name is its - // own, which another tag's alias already put in the filter. - mask |= 1L << name.hashCode(); - continue; - } - for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { - String alias = directionScopedNameOf(tagId, direction); - if (alias != null) { - mask |= 1L << alias.hashCode(); - } - } - } - return mask; - } + return Installed.RESOLVER.keyOf(name); } /** @@ -392,4 +240,25 @@ public static String canonicalTagName(String name) { } private KnownTagCodec() {} + + /** + * The tag an OpenTelemetry name denotes on spans of {@code direction}, for a name whose tag + * depends on direction ({@code server.address} is {@code http.hostname} inbound and {@code + * peer.hostname} outbound); 0 for every other name, and for such a name on spans of a direction + * where it names no tag. The core stores and reads Datadog names only; this is for the + * OpenTelemetry side, which knows its span's direction. + */ + public static long directionalKeyOf(String name, int direction) { + return Installed.RESOLVER.directionalKeyOf(name, direction); + } + + /** True if {@code name} denotes a different tag depending on the span's direction. */ + public static boolean isDirectionScoped(String name) { + for (int direction = DIRECTION_INBOUND; direction <= DIRECTION_NONE; direction++) { + if (directionalKeyOf(name, direction) != 0) { + return true; + } + } + return false; + } } 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 0f1f71fc8bd..be07fadbeeb 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -255,65 +255,41 @@ 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 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. - */ - /** * 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 : new Entry(directionFreeKeyOf(tag), tag, ANY, 0L, 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 : new Entry(directionFreeKeyOf(tag), tag, OBJECT, 0, value); + return isEmptyValue(value) ? null : TagMap.objectEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, boolean value) { - return new Entry( - directionFreeKeyOf(tag), tag, BOOLEAN, boolean2Prim(value), Boolean.valueOf(value)); + return TagMap.booleanEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, int value) { - return new Entry(directionFreeKeyOf(tag), tag, INT, int2Prim(value), null); + return TagMap.intEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, long value) { - return new Entry(directionFreeKeyOf(tag), tag, LONG, long2Prim(value), null); + return TagMap.longEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, float value) { - return new Entry(directionFreeKeyOf(tag), tag, FLOAT, float2Prim(value), null); + return TagMap.floatEntryFor(tag, value); } public static final Entry create(@Nonnull String tag, double 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.keyOrAliasOf(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, or set the name on the span"); - } - return key; + return TagMap.doubleEntryFor(tag, value); } /* @@ -948,10 +924,7 @@ public boolean equals(Object obj) { if (!(obj instanceof TagMap.Entry)) return false; TagMap.Entry that = (TagMap.Entry) obj; - // 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()); + return this.tag.equals(that.tag) && this.objectValue().equals(that.objectValue()); } static long boolean2Prim(boolean value) { @@ -1448,13 +1421,12 @@ 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.keyOf(tag); - return tagId == 0 - ? this.getEntry(tag, Entry.customHash(tag)) - : this.getEntry(KnownTagCodec.nameOf(tagId), tagId); + long tagHash = Entry.tagHashOf(tag); + String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; + return this.getEntry(canonicalTag, tagHash); } - /** The entry for a known tag id. */ + /** 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); } @@ -1491,7 +1463,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.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; + return tagEntry.matches(tag) ? tagEntry : null; } else if (bucket instanceof BucketGroup) { return ((BucketGroup) bucket).findInChain(hash, tag); } @@ -1547,11 +1519,8 @@ 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.) A - * direction-scoped OpenTelemetry name ({@code server.address}) resolves to no id, so it is stored - * as written, a custom tag, and resolved by whoever knows the direction. + * 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); @@ -1650,80 +1619,6 @@ 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) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(Entry.newAnyEntry(tag, value)); - } - } - - public void setResolved(long tagId, @Nonnull String tag, @Nonnull CharSequence value) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(Entry.newObjectEntry(tag, value)); - } - } - - public void setResolved(long tagId, @Nonnull String tag, boolean value) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(Entry.newBooleanEntry(tag, value)); - } - } - - public void setResolved(long tagId, @Nonnull String tag, int value) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(Entry.newIntEntry(tag, value)); - } - } - - public void setResolved(long tagId, @Nonnull String tag, long value) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(Entry.newLongEntry(tag, value)); - } - } - - public void setResolved(long tagId, @Nonnull String tag, float value) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(Entry.newFloatEntry(tag, value)); - } - } - - public void setResolved(long tagId, @Nonnull String tag, double value) { - if (tagId > 0) { - this.set(tagId, value); - } else { - this.putEntry(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)); - } - - /** The removal counterpart of {@link #setResolved(long, String, Object)}. */ - public Entry removeResolved(long tagId, String tag) { - this.checkWriteAccess(); - - return tagId > 0 ? this.getAndRemove(tagId) : this.getAndRemove(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; @@ -1783,21 +1678,6 @@ public void setKnown(long tagId, float value) { public void setKnown(long tagId, double value) { this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } - - /** - * The value of the known tag {@code tagId}, boxed if necessary, or null: {@code - * getEntry(tagId)}'s value, including read-through, for a caller that needs only the value. A - * store that keeps known tags without an Entry per tag can answer without building one. - */ - public Object getKnownValue(long tagId) { - return this.map.getKnownValue(tagId); - } - } - - /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ - Object getKnownValue(long tagId) { - Entry entry = this.getEntry(tagId); - return entry == null ? null : entry.objectValue(); } public void set(long tagId, @Nonnull Object value) { @@ -1884,7 +1764,7 @@ Entry putEntry(@Nonnull Entry newEntry) { return null; } else if (bucket instanceof Entry) { Entry existingEntry = (Entry) bucket; - if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { + if (existingEntry.matches(newEntry.tag)) { thisBuckets[bucketIndex] = newEntry; // replaced existing entry - no size change @@ -2198,10 +2078,9 @@ 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.keyOf(tag); - return tagId == 0 - ? this.getAndRemove(tag, Entry.customHash(tag)) - : this.getAndRemove(KnownTagCodec.nameOf(tagId), tagId); + 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)}. */ @@ -2249,7 +2128,7 @@ private Entry removeLocal(String tag, long tagHash) { // null bucket case - do nothing if (bucket instanceof Entry) { Entry existingEntry = (Entry) bucket; - if (existingEntry.hash() == hash && existingEntry.matches(tag)) { + if (existingEntry.matches(tag)) { thisBuckets[bucketIndex] = null; this.size -= 1; diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.java deleted file mode 100644 index 0c8e0a6791c..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecDirectionTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package datadog.trace.api; - -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.assertTrue; - -import datadog.trace.bootstrap.instrumentation.api.Tags; -import org.junit.jupiter.api.Test; -import org.tabletest.junit.TableTest; - -/** - * {@link KnownTagCodec}'s direction helpers: a span's direction, and the direction-dependent names. - */ -class KnownTagCodecDirectionTest { - @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, KnownTagCodec.directionOfKind(kind)); - - TagMap tags = TagMap.create(); - tags.set(Tags.SPAN_KIND, kind); - assertEquals(direction, KnownTagCodec.directionOf(tags)); - } - - @Test - void noKindIsNoDirection() { - assertEquals(DIRECTION_UNKNOWN, KnownTagCodec.directionOfKind(null)); - assertEquals(DIRECTION_UNKNOWN, KnownTagCodec.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, tags.getEntry(tag).openTelemetryTag(direction)); - } - - @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(KnownTagCodec.isDirectionDependent(tags.getEntry("server.address"))); - assertTrue(KnownTagCodec.isDirectionDependent(tags.getEntry(Tags.PEER_HOSTNAME))); - assertFalse(KnownTagCodec.isDirectionDependent(tags.getEntry(Tags.HTTP_METHOD))); - assertFalse(KnownTagCodec.isDirectionDependent(tags.getEntry("my.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 916d816256c..29c7fa1ac17 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -1,18 +1,18 @@ 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 java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -164,192 +164,39 @@ void unknownNamesResolveToZero() { } /** - * {@code peer.port} means the server's port on outbound spans and the client's on inbound ones, - * but it is one tag either way: one id, one stored value. Only its OpenTelemetry name depends on - * 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 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() { - 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, - KnownTags.SERVER_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)); assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.keyOf(Tags.PEER_PORT)); - for (int direction = DIRECTION_UNKNOWN; direction <= DIRECTION_NONE; direction++) { - // peer.port is not an alias: no direction changes which tag it names - assertEquals(0L, KnownTagCodec.directionalKeyOf(Tags.PEER_PORT, direction)); - } - } - - /** - * server.port is a tag's own Datadog name and peer.port's outbound alias, so like any alias it - * names no single tag without a direction; datadogKeyOf still finds the tag it names itself. - */ - @Test - void aDatadogNameThatIsAlsoAnAliasDependsOnDirection() { - assertEquals("server.port", KnownTagCodec.nameOf(KnownTags.SERVER_PORT_ID)); - assertEquals(0L, KnownTagCodec.keyOf("server.port")); - assertEquals(KnownTags.SERVER_PORT_ID, KnownTagCodec.datadogKeyOf("server.port")); - assertEquals(KnownTags.PEER_PORT_ID, KnownTagCodec.datadogKeyOf(Tags.PEER_PORT)); - assertEquals(0L, KnownTagCodec.datadogKeyOf("server.address")); - assertEquals(0L, KnownTagCodec.datadogKeyOf("my.custom.tag")); + 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( - "server.port", - KnownTagCodec.openTelemetryTagOf(KnownTags.SERVER_PORT_ID, DIRECTION_INBOUND)); + "client.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_INBOUND)); assertEquals( "server.port", - KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND)); - } - - static Stream directionalNames() { - return Stream.of( - 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_ID), - Arguments.of("server.port", DIRECTION_INBOUND, KnownTags.SERVER_PORT_ID), - Arguments.of("server.port", DIRECTION_NONE, KnownTags.SERVER_PORT_ID), - Arguments.of("server.port", DIRECTION_UNKNOWN, 0L), - 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)); - } - - /** 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) { - long tagId = KnownTagCodec.directionalKeyOf(name, direction); - assertEquals(expected, tagId > 0 ? tagId : 0L); - assertEquals(0L, KnownTagCodec.keyOf(name)); - assertEquals( - KnownTagCodec.DIRECTION_SCOPED_OTEL_NAME_SENTINEL, KnownTagCodec.keyOrAliasOf(name)); - } - - @Test - void aDirectionFreeNameResolvesTheSameInEveryDirection() { - assertEquals(KnownTags.HTTP_METHOD_ID, KnownTagCodec.keyOf("http.request.method")); - assertEquals(KnownTags.PEER_HOSTNAME_ID, KnownTagCodec.keyOf(Tags.PEER_HOSTNAME)); - for (int direction = DIRECTION_UNKNOWN; direction <= DIRECTION_NONE; direction++) { - for (String name : - new String[] {"http.request.method", Tags.PEER_HOSTNAME, "my.custom.tag"}) { - assertEquals(0L, KnownTagCodec.directionalKeyOf(name, direction), name); - } - } - } - - @Test - void aDirectionScopedRenameIsEmittedOnlyInItsDirection() { + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND)); assertEquals( - "server.address", - KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID, DIRECTION_INBOUND)); - assertEquals( - Tags.HTTP_HOSTNAME, - KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID, DIRECTION_OUTBOUND)); - assertEquals( - "http.request.method", - KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_METHOD_ID, DIRECTION_UNKNOWN)); - assertEquals(Tags.HTTP_HOSTNAME, KnownTagCodec.openTelemetryTagOf(KnownTags.HTTP_HOSTNAME_ID)); + Tags.PEER_PORT, + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_NONE)); } - /** One tag, named for the span's direction; with no direction it keeps its Datadog name. */ - @Test - void aTagDeclaredPerDirectionIsEmittedUnderTheNameForTheDirection() { - assertEquals( - "server.port", - 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)); + @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 @@ -437,4 +284,115 @@ void canonicalTagNameMapsAnOpenTelemetryRenameToItsDatadogName() { assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(Tags.HTTP_METHOD)); assertEquals("my.custom.tag", KnownTagCodec.canonicalTagName("my.custom.tag")); } + + @TableTest({ + "scenario | kind | direction", + "no kind | | -1 ", + "server | 'server' | 0 ", + "consumer | 'consumer' | 0 ", + "client | 'client' | 1 ", + "producer | 'producer' | 1 ", + "internal | 'internal' | 2 ", + "broker | 'broker' | -1 " + }) + void directionOfKind(String kind, int direction) { + assertEquals(direction, KnownTagCodec.directionOfKind(kind)); + } + + @Test + void directionOfReadsTheMapsSpanKind() { + TagMap tags = TagMap.create(); + assertEquals(KnownTagCodec.DIRECTION_UNKNOWN, KnownTagCodec.directionOf(tags)); + + tags.set(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT); + assertEquals(KnownTagCodec.DIRECTION_OUTBOUND, KnownTagCodec.directionOf(tags)); + } + + @Test + void anEntrysDirectionalOpenTelemetryTagFallsBackToItsNameForACustomTag() { + TagMap.Entry custom = TagMap.Entry.newAnyEntry("my.custom.tag", "value"); + assertEquals("my.custom.tag", custom.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); + + TagMap.Entry peerPort = TagMap.Entry.newAnyEntry(KnownTags.PEER_PORT_ID, 443); + assertEquals( + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, KnownTagCodec.DIRECTION_OUTBOUND), + peerPort.openTelemetryTag(KnownTagCodec.DIRECTION_OUTBOUND)); + } + + /** OpenTelemetry names whose tag depends on the span's direction, resolved by that direction. */ + @TableTest({ + "scenario | name | direction | expected ", + "server.address inbound | server.address | 0 | http.hostname ", + "server.address outbound | server.address | 1 | peer.hostname ", + "server.address internal | server.address | 2 | ", + "server.address no direction | server.address | -1 | ", + "server.port outbound | server.port | 1 | peer.port ", + "server.port inbound | server.port | 0 | server.port ", + "server.port internal | server.port | 2 | server.port ", + "client.port inbound | client.port | 0 | peer.port ", + "client.port outbound | client.port | 1 | ", + "client address inbound | network.peer.address | 0 | network.client.ip", + "direction-free name | http.request.method | 1 | ", + "Datadog name | peer.hostname | 1 | " + }) + void aDirectionScopedNameResolvesByDirection(String name, int direction, String expected) { + long tagId = KnownTagCodec.directionalKeyOf(name, direction); + assertEquals(expected, tagId == 0 ? null : KnownTagCodec.nameOf(tagId)); + } + + @Test + void onlyDirectionScopedNamesAreReportedAsSuch() { + assertTrue(KnownTagCodec.isDirectionScoped("server.address")); + assertTrue(KnownTagCodec.isDirectionScoped("server.port")); + assertTrue(KnownTagCodec.isDirectionScoped("client.port")); + assertFalse(KnownTagCodec.isDirectionScoped("http.request.method")); + assertFalse(KnownTagCodec.isDirectionScoped(Tags.PEER_HOSTNAME)); + assertFalse(KnownTagCodec.isDirectionScoped("my.custom.tag")); + } + + /** + * server.port is a Datadog name too: without a direction it is its own tag, the server's port. + * Only OpenTelemetry-side resolution maps it to peer.port on outbound spans. + */ + @Test + void aSharedNameIsItsOwnTagWithoutADirection() { + assertEquals(KnownTags.SERVER_PORT_ID, KnownTagCodec.keyOf("server.port")); + assertEquals("server.port", KnownTagCodec.nameOf(KnownTags.SERVER_PORT_ID)); + assertEquals(0L, KnownTagCodec.keyOf("server.address")); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.PEER_PORT_ID, DIRECTION_OUTBOUND)); + assertEquals( + "server.port", + KnownTagCodec.openTelemetryTagOf(KnownTags.SERVER_PORT_ID, DIRECTION_INBOUND)); + } + + @Test + void eachDirectionNamesItsTagsInOpenTelemetryTerms() { + assertEquals(KnownTags.PEER_HOSTNAME_ID, KnownTags.Outbound.SERVER_ADDRESS_ID); + assertEquals(KnownTags.PEER_PORT_ID, KnownTags.Outbound.SERVER_PORT_ID); + assertEquals(KnownTags.HTTP_HOSTNAME_ID, KnownTags.Inbound.SERVER_ADDRESS_ID); + assertEquals(KnownTags.SERVER_PORT_ID, KnownTags.Inbound.SERVER_PORT_ID); + assertEquals(KnownTags.PEER_PORT_ID, KnownTags.Inbound.CLIENT_PORT_ID); + assertEquals(KnownTags.HTTP_URL_ID, KnownTags.Outbound.URL_FULL_ID); + } + + /** Every per-direction alias names, in its direction, a tag emitted under that alias. */ + @TableTest({ + "scenario | holder | direction", + "inbound | Inbound | 0 ", + "outbound | Outbound | 1 ", + "internal | Internal | 2 " + }) + void perDirectionAliasesAgreeWithTheDirectionalName(String holder, int direction) + throws Exception { + Field[] fields = Class.forName(KnownTags.class.getName() + "$" + holder).getFields(); + assertTrue(fields.length > 0, holder); + for (Field field : fields) { + long tagId = field.getLong(null); + String otelName = KnownTagCodec.openTelemetryTagOf(tagId, direction); + String expected = otelName.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]+", "_") + "_ID"; + assertEquals(expected, field.getName(), holder + "." + field.getName()); + } + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java deleted file mode 100644 index 4991c1ee294..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDirectionalNameTest.java +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 05663dea4e4..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/TagMapEntryCreateByNameTest.java +++ /dev/null @@ -1,52 +0,0 @@ -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 ", - "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()); - } - - /** {@code peer.port} is one tag in every direction; only its OpenTelemetry name varies. */ - @Test - void aDatadogNameDeclaredPerDirectionCreatesItsOneTag() { - TagMap.Entry entry = TagMap.Entry.create("peer.port", 5432); - - 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 e5463372094..4634a02da58 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -73,39 +73,6 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } - @Test - void theCoresKnownValueReadMatchesTheEntrysValue() { - TagMap parent = TagMap.create(); - parent.set(KnownTags.HTTP_STATUS_CODE_ID, 200); - TagMap tags = TagMap.createFromParent(parent.freeze()); - tags.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); - - assertEquals("/users/{id}", tags.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals( - 200, tags.internals().getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); // read-through - assertNull(tags.internals().getKnownValue(KnownTags.HTTP_METHOD_ID)); - } - - @Test - void aResolvedWriteOfAKnownTagStoresWhatAnIdWriteStores() { - TagMap byId = TagMap.create(); - byId.set(KnownTags.HTTP_ROUTE_ID, "/a"); - byId.set(KnownTags.HTTP_STATUS_CODE_ID, 200); - byId.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); - byId.set(KnownTags.DD_PROFILING_ENABLED_ID, true); - - TagMap resolved = TagMap.create(); - resolved.setResolved(KnownTags.HTTP_ROUTE_ID, KnownTags.HTTP_ROUTE_NAME, "/a"); - resolved.setResolved(KnownTags.HTTP_STATUS_CODE_ID, "ignored", 200); - resolved.setResolved(KnownTags.HTTP_RESEND_COUNT_ID, "ignored", 2L); - resolved.setResolved(KnownTags.DD_PROFILING_ENABLED_ID, "ignored", true); - resolved.setResolved(0L, "my.custom.tag", "custom"); - - assertEquals("custom", resolved.getObject("my.custom.tag")); - resolved.remove("my.custom.tag"); - assertEquals(new HashMap<>(byId), new HashMap<>(resolved)); - } - @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 522a2a0058c..3a9c6a585df 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -82,8 +82,8 @@ void everyKnownTagSurvivesBucketCollisionsAndCopies() throws IllegalAccessExcept assertEquals(tagIds.size(), copy.size()); for (long tagId : tagIds) { String name = KnownTagCodec.nameOf(tagId); - assertEquals("value", map.getEntry(tagId).objectValue(), name); - assertEquals("value", copy.getEntry(tagId).objectValue(), name); + assertEquals("value", map.getObject(name), name); + assertEquals("value", copy.getObject(name), name); } } @@ -91,7 +91,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.isKnown(tagId)) { + tagIds.add(tagId); + } } } return tagIds; diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 4381c80e016..2333cf1c5da 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -47,9 +47,11 @@ # direction. Any other repeated declaration is still an error. # A directional rename may also be another tag's Datadog name: server.port is peer.port on outbound # spans, and its own tag (the server's port) in every other direction. -# 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. +# The tracer core resolves names without a direction, so only renames that apply in every direction +# resolve a name to a tag there. A directional rename, including one under `span-kind: internal`, is +# applied when a span's tags are emitted under OpenTelemetry names, and by the OpenTelemetry API, +# which knows its span's kind. Instrumentation that knows its direction writes by id, through +# KnownTags.Inbound / Outbound / Internal. # `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 From f746fbee769b0a157ccab9650afa6fde6751e54a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 17:31:54 -0400 Subject: [PATCH 73/99] Look up known tags by id, not by name, in the id store HashIdStore's lookups (reads, the find before a removal, and the read-through visibility check) now match an entry on its 64-bit id through HashSupport.findById, so they no longer resolve the tag's name with nameOf or compare names. Every id-store entry is keyed by id, and the stored bucket hash still filters before the id compare. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/HashIdStore.java | 14 +++++------ .../java/datadog/trace/api/HashSupport.java | 16 ++++++++++++ .../main/java/datadog/trace/api/TagMap.java | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 26427ae560f..1293af7d33c 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -52,8 +52,7 @@ public Entry get(long tagId) { /** {@link #get} for a validated id and its slot bit. */ private Entry getVisible(long tagId, long bit) { if (this.mayHaveLocal(bit)) { - Entry local = - HashSupport.find(this.buckets, Entry.bucketHash(tagId), KnownTagCodec.nameOf(tagId)); + Entry local = HashSupport.findById(this.buckets, Entry.bucketHash(tagId), tagId); if (local != null) { return local.tombstone ? null : local; } @@ -138,14 +137,13 @@ public Entry remove(long tagId) { } long bit = slotBit(tagId); int hash = Entry.bucketHash(tagId); - String name = KnownTagCodec.nameOf(tagId); - Entry local = this.mayHaveLocal(bit) ? HashSupport.find(this.buckets, hash, name) : null; + Entry local = this.mayHaveLocal(bit) ? HashSupport.findById(this.buckets, hash, tagId) : null; if (local != null) { if (local.tombstone) { return null; // already removed } - HashSupport.remove(this.buckets, hash, name); + HashSupport.remove(this.buckets, hash, local.tag); this.count--; } if (this.mayInherit(bit)) { @@ -218,11 +216,11 @@ boolean hasParent() { * rule the slot out is skipped without a probe. */ boolean parentEntryVisible(Entry parentEntry, HashIdStore fromAncestor) { - long bit = slotBit(parentEntry.tagHash); + long tagId = parentEntry.tagHash; + long bit = slotBit(tagId); int hash = parentEntry.hash(); - String tag = parentEntry.tag; for (HashIdStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - if (nearer.mayHaveLocal(bit) && HashSupport.find(nearer.buckets, hash, tag) != null) { + if (nearer.mayHaveLocal(bit) && HashSupport.findById(nearer.buckets, hash, tagId) != null) { return false; } } diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java index bce70cd924b..01a498258cb 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashSupport.java +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -36,6 +36,22 @@ static Entry find(Object[] buckets, int hash, String tag) { /** * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). */ + /** + * {@link #find} for a known tag, matching on its id rather than its name: an id store's entries + * are all keyed by id, so it never needs a name to look one up. {@code hash} is {@code + * Entry.bucketHash(tagId)}. + */ + static Entry findById(Object[] buckets, int hash, long tagId) { + Object bucket = buckets[hash & (buckets.length - 1)]; + if (bucket instanceof Entry) { + Entry tagEntry = (Entry) bucket; + return tagEntry.tagHash == tagId ? tagEntry : null; + } else if (bucket instanceof BucketGroup) { + return ((BucketGroup) bucket).findInChainById(hash, tagId); + } + return null; + } + static Entry findInBucket(Object bucket, int hash, String tag) { if (bucket instanceof Entry) { Entry tagEntry = (Entry) bucket; 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 770c5eed524..0d312abbbf4 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -2423,6 +2423,31 @@ Entry findInChain(int hash, String tag) { return null; } + /** + * {@link #findInChain} for a known tag: matches on the entry's id, so it needs no name. Only an + * entry stored under {@code tagId} has that id, and the hash check filters first. + */ + Entry findInChainById(int hash, long tagId) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + Entry curEntry = curGroup._findById(hash, tagId); + if (curEntry != null) return curEntry; + } + return null; + } + + Entry _findById(int hash, long tagId) { + if (this.hash0 == hash && this.entry0.tagHash == tagId) { + return this.entry0; + } else if (this.hash1 == hash && this.entry1.tagHash == tagId) { + return this.entry1; + } else if (this.hash2 == hash && this.entry2.tagHash == tagId) { + return this.entry2; + } else if (this.hash3 == hash && this.entry3.tagHash == tagId) { + return this.entry3; + } + return null; + } + Entry _find(int hash, String tag) { // if ( this._mayContain(hash) ) return null; From 2d56d2966ae580059786c0ff68d1efc42ba90b7b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 19:16:04 -0400 Subject: [PATCH 74/99] Average known-tag reads over the span's tags in GetTagBenchmark A single fixed tag measures whether that one tag happens to collide in its bucket, which shifts whenever tag ids do. The new *_cycle arms read each known tag the span holds in turn. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/GetTagBenchmark.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java index 4da4fde8618..eac33cbb0bd 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java @@ -31,6 +31,20 @@ public class GetTagBenchmark { private DDSpan span; + /** + * The known tags the span holds, read in turn by the {@code *_cycle} arms so a read's cost is + * averaged over the span's tags rather than decided by whether one particular tag collides in + * its bucket. A power-of-two length lets the index wrap with a mask. + */ + private static final String[] KNOWN_HELD = { + "span.kind", + KnownTags.HTTP_ROUTE_NAME, + KnownTags.PEER_HOSTNAME_NAME, + KnownTags.HTTP_STATUS_CODE_NAME + }; + + private int next; + @Setup public void setup() { span = (DDSpan) TRACER.startSpan("benchmark", "get.tag"); @@ -83,6 +97,17 @@ public Object knownTag_unsafeGetTag() { return span.unsafeGetTag(KnownTags.HTTP_ROUTE_NAME); } + /** Every known tag the span holds, in turn; see {@link #KNOWN_HELD}. */ + @Benchmark + public Object knownTags_cycle_unsafeGetTag() { + return span.unsafeGetTag(KNOWN_HELD[next++ & (KNOWN_HELD.length - 1)]); + } + + @Benchmark + public Object knownTags_cycle_getTag() { + return span.getTag(KNOWN_HELD[next++ & (KNOWN_HELD.length - 1)]); + } + @Benchmark public Object customTag_unsafeGetTag() { return span.unsafeGetTag("my.custom.tag"); From 7547659dbf3a0fc51d983cf27ac018da0caa1c5c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 19:18:24 -0400 Subject: [PATCH 75/99] Return EntryReader from IdStore's point operations get, put, getAndPut and remove now return an EntryReader that is valid until the store's next operation, so a store that keeps known tags without an Entry per tag can answer with a reused flyweight. HashIdStore keeps returning its Entry (covariantly), so nothing changes in hash mode. TagMap's public API still returns Entry: TagRouting.entryOf converts at that boundary, which is free for a hash store's reader. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/GetTagBenchmark.java | 4 ++-- .../java/datadog/trace/core/DDSpanContext.java | 2 +- .../main/java/datadog/trace/api/IdStore.java | 13 ++++++++----- .../main/java/datadog/trace/api/TagMap.java | 18 ++++++++++++------ .../java/datadog/trace/api/TagRouting.java | 15 ++++++++++++--- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java index eac33cbb0bd..20f2286eff0 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java @@ -33,8 +33,8 @@ public class GetTagBenchmark { /** * The known tags the span holds, read in turn by the {@code *_cycle} arms so a read's cost is - * averaged over the span's tags rather than decided by whether one particular tag collides in - * its bucket. A power-of-two length lets the index wrap with a mask. + * averaged over the span's tags rather than decided by whether one particular tag collides in its + * bucket. A power-of-two length lets the index wrap with a mask. */ private static final String[] KNOWN_HELD = { "span.kind", 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 125c80330a9..7813f4ef4d8 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 @@ -1188,7 +1188,7 @@ public String getSpanKindString() { } // UNSET or CUSTOM -- fall through to tag map synchronized (tagIds) { - TagMap.Entry kind = tagIds.get(KnownTags.SPAN_KIND_ID); + TagMap.EntryReader kind = tagIds.get(KnownTags.SPAN_KIND_ID); return kind == null ? null : kind.stringValue(); } } diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 3d28b1920ba..2f84ab52a79 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -64,7 +64,10 @@ static IllegalArgumentException notKnown(long tagId) { } /* - * Point operations. + * Point operations. A returned EntryReader is valid only until this store's next operation: a + * store that keeps known tags without an Entry per tag answers with a reused flyweight. Call + * EntryReader.entry() to keep one. Stores are inside the tracer's trust boundary, so callers are + * expected to follow this rather than have it enforced. */ /** @@ -72,7 +75,7 @@ static IllegalArgumentException notKnown(long tagId) { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - public abstract Entry get(long tagId); + public abstract EntryReader get(long tagId); /** * {@code get(tagId)}'s value, boxed if necessary, or null -- for a caller that needs only the @@ -102,10 +105,10 @@ static IllegalArgumentException notKnown(long tagId) { * Inserts or replaces a known tag's entry, returning the local entry it replaced (or null). Does * not consult the read-through parent. */ - public abstract Entry put(Entry knownEntry); + public abstract EntryReader put(Entry knownEntry); /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - public abstract Entry getAndPut(Entry knownEntry); + public abstract EntryReader getAndPut(Entry knownEntry); /** * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is @@ -113,7 +116,7 @@ static IllegalArgumentException notKnown(long tagId) { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - public abstract Entry remove(long tagId); + public abstract EntryReader remove(long tagId); /* * Whole-store operations, for the owning TagMap. 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 0d312abbbf4..d8f57ed275e 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1528,7 +1528,7 @@ public Entry getEntry(String tag) { /** The entry for a known tag id. */ public Entry getEntry(long tagId) { - return this.ids.get(tagId); + return TagRouting.entryOf(this.ids.get(tagId)); } @Deprecated @@ -1735,7 +1735,9 @@ public Entry getEntryResolved(long tagId, String tag) { public Entry removeResolved(long tagId, String tag) { this.checkWriteAccess(); - return TagRouting.isKnown(tagId) ? this.ids.remove(tagId) : this.removeCustom(tag); + return TagRouting.isKnown(tagId) + ? TagRouting.entryOf(this.ids.remove(tagId)) + : this.removeCustom(tag); } /* @@ -1790,7 +1792,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { this.checkWriteAccess(); return newEntry.tagId() != 0 - ? this.ids.getAndPut(newEntry) + ? TagRouting.entryOf(this.ids.getAndPut(newEntry)) : this.namesForWrite().getAndPut(newEntry); } @@ -1803,7 +1805,9 @@ public Entry getAndSet(@Nullable Entry newEntry) { Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); - return newEntry.tagId() != 0 ? this.ids.put(newEntry) : this.namesForWrite().put(newEntry); + return newEntry.tagId() != 0 + ? TagRouting.entryOf(this.ids.put(newEntry)) + : this.namesForWrite().put(newEntry); } /** {@link #putEntry} for an entry known to be a known tag's. */ @@ -1938,14 +1942,16 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) ? this.ids.remove(key) : this.removeCustom(tag); + return TagRouting.isKnown(key) + ? TagRouting.entryOf(this.ids.remove(key)) + : this.removeCustom(tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ public Entry getAndRemove(long tagId) { this.checkWriteAccess(); - return this.ids.remove(tagId); + return TagRouting.entryOf(this.ids.remove(tagId)); } public TagMap copy() { diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index 340f6adda0d..e8a124ac4f0 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -82,7 +82,7 @@ public static HashStore newCustomStore(HashStore borrowed) { /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ public static Entry get(IdStore ids, HashStore names, long key, String tag) { - return isKnown(key) ? ids.get(key) : names == null ? null : names.get(tag); + return isKnown(key) ? entryOf(ids.get(key)) : names == null ? null : names.get(tag); } /* @@ -112,7 +112,7 @@ public static Entry getEntry( return asWritten; } long tagId = KnownTagCodec.directionalKeyOf(name, direction); - return isKnown(tagId) ? ids.get(tagId) : null; + return isKnown(tagId) ? entryOf(ids.get(tagId)) : null; } /** The entry the known tag {@code tagId} reads on a span of {@code direction}; see above. */ @@ -128,6 +128,15 @@ public static Entry getById( return pending; } } - return ids.get(tagId); + return entryOf(ids.get(tagId)); + } + + /** + * The Entry an id store's {@code reader} denotes, for a caller that hands it out of the trust + * boundary or keeps it: free for a hash store's reader, which is its Entry; a store reading + * through a reused flyweight builds one here. + */ + public static Entry entryOf(TagMap.EntryReader reader) { + return reader == null ? null : reader.entry(); } } From df0903993b54c7240b90f0b583e09183415d4f10 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 19:21:34 -0400 Subject: [PATCH 76/99] Generate a serial-to-id table so a store can keep only the serial The generator now emits IDS_BY_SERIAL beside NAMES_BY_SERIAL, exposed as KnownTagCodec.idOf(serial). A dense id store can then keep each tag's 16-bit serial instead of its full 64-bit id and still recover the id, flag and slot bits included, when it hands an entry out. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 20 +++++++++++++++++++ .../TagRegistryGeneratorPluginTest.kt | 1 + .../java/datadog/trace/api/KnownTagCodec.java | 12 +++++++++++ .../java/datadog/trace/api/KnownTagsTest.java | 15 ++++++++++++++ 4 files changed, 48 insertions(+) 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 2085b062049..2b2050d1b14 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 @@ -190,6 +190,21 @@ object KnownTagsEmitter { """ }; + /** + * Each tag's id, indexed by serial; slot 0 (no tag) is 0. Lets a store that keeps only the + * 16-bit serial recover the full id, flag and slot bits included. + */ + static final long[] IDS_BY_SERIAL = { + 0L, + """.trimIndent() + ) + for (t in reg.tags.sortedBy { it.serial }) { + appendLine(" ${idC(t.ddName)},") + } + 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 @@ -202,6 +217,11 @@ object KnownTagsEmitter { int serial = KnownTagCodec.serialNum(tagId); return serial < NAMES_BY_SERIAL.length ? NAMES_BY_SERIAL[serial] : null; } + + @Override + public long idOf(int serial) { + return IDS_BY_SERIAL[serial]; + } // 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 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 442cf2b9bc6..0ee6b843970 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 @@ -252,6 +252,7 @@ class TagRegistryGeneratorPluginTest { public static final long NO_TAG_IN_DIRECTION_SENTINEL = -3L; public interface Resolver { String nameOf(long id); + long idOf(int serial); String openTelemetryNameOf(long id, int direction); long lookup(String name); long directionalKeyOf(String name, int direction); 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 6528a67c7ce..50abb4e5e8b 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -203,6 +203,9 @@ public interface Resolver { /** The tag's Datadog-namespace (canonical) name. */ String nameOf(long tagId); + /** The full id of the known tag with {@code serial}, which must be below the serial limit. */ + long idOf(int serial); + /** * 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 @@ -229,6 +232,15 @@ private static final class Installed { static final Resolver RESOLVER = KnownTags.RESOLVER; } + /** + * The full id of the known tag with {@code serial} (flag and slot bits included), for a store + * that keeps only the 16-bit serial. {@code serial} must be a known tag's: {@code 0 < serial < + * serialLimit()}. + */ + public static long idOf(int serial) { + return Installed.RESOLVER.idOf(serial); + } + /** The tag's canonical (Datadog-namespace) name, or {@code null} when the id is not known. */ public static String nameOf(long tagId) { return Installed.RESOLVER.nameOf(tagId); 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 fcae594acbb..e04a80c1bdb 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -414,4 +414,19 @@ void canonicalTagNameMapsAnOpenTelemetryRenameToItsDatadogName() { assertEquals(Tags.HTTP_METHOD, KnownTagCodec.canonicalTagName(Tags.HTTP_METHOD)); assertEquals("my.custom.tag", KnownTagCodec.canonicalTagName("my.custom.tag")); } + + @Test + void idOfRecoversEveryKnownTagsFullIdFromItsSerial() throws IllegalAccessException { + int checked = 0; + for (java.lang.reflect.Field field : KnownTags.class.getFields()) { + if (field.getType() == long.class && field.getName().endsWith("_ID")) { + long tagId = field.getLong(null); + if (KnownTagCodec.isKnown(tagId)) { + assertEquals(tagId, KnownTagCodec.idOf(KnownTagCodec.serialNum(tagId)), field.getName()); + checked++; + } + } + } + assertTrue(checked > 0); + } } From a31a1de58541623463fe55263daff5a5f7f8cdaf Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 19:33:27 -0400 Subject: [PATCH 77/99] Add a dense id store behind an experimental flag DenseIdStore keeps known tags as parallel char[] serials + Object[] values, with no Entry per tag; point reads answer with a reused flyweight, and getKnownValue builds nothing. Inherited removals leave an inline tombstone. IdStoreFactory picks the one IdStore subclass for the JVM from trace.experimental.dense.tags.enabled, so only that class is loaded and CHA keeps IdStore calls monomorphic. TagMap's iterator now walks the id store through IdStore.Cursor instead of reaching into HashIdStore's buckets, and the Map entry view builds an Entry from a flyweight reader. Co-Authored-By: Claude Opus 5.5 --- .../trace/api/config/TracerConfig.java | 6 + .../main/java/datadog/trace/api/Config.java | 7 + .../java/datadog/trace/api/DenseIdStore.java | 538 ++++++++++++++++++ .../java/datadog/trace/api/HashIdStore.java | 48 +- .../java/datadog/trace/api/HashSupport.java | 50 ++ .../main/java/datadog/trace/api/IdStore.java | 18 +- .../datadog/trace/api/IdStoreFactory.java | 62 ++ .../main/java/datadog/trace/api/TagMap.java | 189 +++--- .../java/datadog/trace/api/TagRouting.java | 2 +- .../trace/api/DenseIdStoreForkedTest.java | 50 ++ .../java/datadog/trace/api/TagStoresTest.java | 4 +- metadata/supported-configurations.json | 8 + 12 files changed, 856 insertions(+), 126 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/DenseIdStore.java create mode 100644 internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java create mode 100644 internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java index d31137c3aef..2fa5365f935 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java @@ -190,5 +190,11 @@ public final class TracerConfig { public static final String TRACE_ORG_GUARD_STRICT = "trace.org.guard.strict"; public static final String TRACE_ORG_GUARD_TRUSTED_OPMS = "trace.org.guard.trusted.opms"; + /** + * Stores known tags that have a dense-store slot in TagMap's dense store (no per-tag entry) + * instead of its hash buckets. Experimental, off by default. + */ + public static final String TRACE_DENSE_TAGS_ENABLED = "trace.experimental.dense.tags.enabled"; + private TracerConfig() {} } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 3471750c347..bd1329223a9 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -703,6 +703,7 @@ import static datadog.trace.api.config.TracerConfig.TRACE_CLOUD_PAYLOAD_TAGGING_SERVICES; import static datadog.trace.api.config.TracerConfig.TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING; import static datadog.trace.api.config.TracerConfig.TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING; +import static datadog.trace.api.config.TracerConfig.TRACE_DENSE_TAGS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_EXPERIMENTAL_FEATURES_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_GIT_METADATA_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES; @@ -1462,6 +1463,7 @@ public static String getHostName() { private final boolean jdkSocketEnabled; private final boolean spanBuilderReuseEnabled; + private final boolean traceDenseTagsEnabled; private final int tagNameUtf8CacheSize; private final int tagValueUtf8CacheSize; private final int stackTraceLengthLimit; @@ -3453,6 +3455,7 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) this.spanBuilderReuseEnabled = configProvider.getBoolean(GeneralConfig.SPAN_BUILDER_REUSE_ENABLED, true); + this.traceDenseTagsEnabled = configProvider.getBoolean(TRACE_DENSE_TAGS_ENABLED, false); this.tagNameUtf8CacheSize = Math.max(configProvider.getInteger(GeneralConfig.TAG_NAME_UTF8_CACHE_SIZE, 128), 0); this.tagValueUtf8CacheSize = @@ -5430,6 +5433,10 @@ public boolean isJdkSocketEnabled() { return jdkSocketEnabled; } + public boolean isTraceDenseTagsEnabled() { + return traceDenseTagsEnabled; + } + public boolean isSpanBuilderReuseEnabled() { return spanBuilderReuseEnabled; } diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java new file mode 100644 index 00000000000..fc204647a4d --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -0,0 +1,538 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * The dense {@link IdStore}: known tags in two parallel arrays -- each tag's 16-bit serial and its + * value -- with no Entry per tag, reading through to the frozen parent's store. + * + *

A lookup is a linear scan of the serials, skipped when the slot masks rule the tag out. The + * full id is rebuilt from the serial ({@link KnownTagCodec#idOf}) only where a reader asks for it. + * Values are boxed: a primitive set by id is stored as its wrapper. + * + *

Point reads that need only the value ({@link #getKnownValue}) build nothing. Those that return + * an {@link EntryReader} answer with this store's one reused flyweight, valid until the store's + * next operation (see {@link IdStore}). + * + *

Removing a local tag swaps the last tag into its place. Removing an inherited tag leaves the + * shared {@link #TOMBSTONE} as its value, in place, and sets its slot in the tombstone mask; a + * tombstone is never counted, iterated or returned. + */ +final class DenseIdStore extends IdStore { + /** A value hiding an inherited tag. Never escapes the store. */ + private static final Object TOMBSTONE = new Object(); + + /** First capacity, on a store's first tag. */ + static final int INIT_CAP = 12; + + private static final char[] NO_SERIALS = new char[0]; + private static final Object[] NO_VALUES = new Object[0]; + + /** Each local tag's serial, then {@link #used} slots in; parallel to {@link #values}. */ + private char[] serials = NO_SERIALS; + + private Object[] values = NO_VALUES; + + /** Slots in use, tombstones included. */ + private int used; + + /** Live local tags, not counting tombstones. */ + private int count; + + /** The frozen parent's id store, or null. */ + final DenseIdStore parent; + + /** The reused flyweight point reads answer with; built on first use. */ + private EntryReadingHelper reader; + + DenseIdStore(IdStore parent) { + super(parent); + this.parent = (DenseIdStore) parent; + } + + @Override + public EntryReader get(long tagId) { + return this.readerOf(tagId, this.getKnownValue(tagId)); + } + + @Override + public Object getKnownValue(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw notKnown(tagId); + } + return this.getVisible((char) KnownTagCodec.serialNum(tagId), slotBit(tagId)); + } + + /** The visible value of the tag {@code serial} in the slot {@code bit}, or null. */ + private Object getVisible(char serial, long bit) { + if (this.mayHaveLocal(bit)) { + int i = this.indexOf(serial); + if (i >= 0) { + Object value = this.values[i]; + return value == TOMBSTONE ? null : value; + } + } + return this.mayInherit(bit) ? this.parent.getVisible(serial, bit) : null; + } + + @Override + public void set(long tagId, Object value) { + this.store(tagId, value); + } + + @Override + public void set(long tagId, CharSequence value) { + this.store(tagId, value); + } + + @Override + public void set(long tagId, boolean value) { + this.store(tagId, Boolean.valueOf(value)); + } + + @Override + public void set(long tagId, int value) { + this.store(tagId, Integer.valueOf(value)); + } + + @Override + public void set(long tagId, long value) { + this.store(tagId, Long.valueOf(value)); + } + + @Override + public void set(long tagId, float value) { + this.store(tagId, Float.valueOf(value)); + } + + @Override + public void set(long tagId, double value) { + this.store(tagId, Double.valueOf(value)); + } + + @Override + public EntryReader put(Entry knownEntry) { + long tagId = knownEntry.tagHash; + return this.readerOf(tagId, this.store(tagId, knownEntry.objectValue())); + } + + @Override + public EntryReader getAndPut(Entry knownEntry) { + long tagId = knownEntry.tagHash; + char serial = (char) KnownTagCodec.serialNum(tagId); + long bit = slotBit(tagId); + int i = this.mayHaveLocal(bit) ? this.indexOf(serial) : -1; + Object prior; + if (i >= 0) { + prior = this.values[i]; + if (prior == TOMBSTONE) { + prior = null; // tombstoned, so not visible + this.occupancy |= bit; + this.count++; + } + this.values[i] = knownEntry.objectValue(); + } else { + prior = this.mayInherit(bit) ? this.parent.getVisible(serial, bit) : null; + this.append(serial, bit, knownEntry.objectValue()); + } + return this.readerOf(tagId, prior); + } + + @Override + public EntryReader remove(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw notKnown(tagId); + } + char serial = (char) KnownTagCodec.serialNum(tagId); + long bit = slotBit(tagId); + + int i = this.mayHaveLocal(bit) ? this.indexOf(serial) : -1; + Object local = null; + if (i >= 0) { + local = this.values[i]; + if (local == TOMBSTONE) { + return null; // already removed + } + } + if (this.mayInherit(bit)) { + // Read-through: hide an inherited value with a tombstone. The prior *visible* value is the + // local one if there was one, otherwise the inherited one. + Object inherited = this.parent.getVisible(serial, bit); + if (inherited != null) { + if (i >= 0) { + this.values[i] = TOMBSTONE; + this.count--; + } else { + this.appendSlot(serial, TOMBSTONE); + } + this.tombstoneMask |= bit; + return this.readerOf(tagId, local != null ? local : inherited); + } + } + if (i >= 0) { + this.removeAt(i); + this.count--; + } + return this.readerOf(tagId, local); + } + + /** Sets a known tag's value, returning the prior local value, or null. Builds no Entry. */ + private Object store(long tagId, Object value) { + char serial = (char) KnownTagCodec.serialNum(tagId); + long bit = slotBit(tagId); + int i = this.mayHaveLocal(bit) ? this.indexOf(serial) : -1; + if (i >= 0) { + Object prior = this.values[i]; + this.values[i] = value; + if (prior == TOMBSTONE) { + this.occupancy |= bit; + this.count++; // a tombstone replaced is a removal undone: no prior local value + return null; + } + return prior; + } + this.append(serial, bit, value); + return null; + } + + /** Appends a live local tag. */ + private void append(char serial, long bit, Object value) { + this.appendSlot(serial, value); + this.occupancy |= bit; + this.count++; + } + + private void appendSlot(char serial, Object value) { + int i = this.used; + if (i == this.serials.length) { + this.grow(); + } + this.serials[i] = serial; + this.values[i] = value; + this.used = i + 1; + } + + /** Removes slot {@code i}, swapping the last slot into its place. The masks stay as they were. */ + private void removeAt(int i) { + int last = --this.used; + this.serials[i] = this.serials[last]; + this.values[i] = this.values[last]; + this.serials[last] = 0; + this.values[last] = null; + } + + /** Index of {@code serial} among the used slots, or -1. */ + private int indexOf(char serial) { + char[] serials = this.serials; + for (int i = 0; i < this.used; ++i) { + if (serials[i] == serial) return i; + } + return -1; + } + + /** + * Makes room for one more slot: the first from the shared empty arrays at {@link #INIT_CAP}, then + * doubling, bounded by {@link KnownTagCodec#serialLimit()} -- a store holds each known tag at + * most once. Out of line, as it is rare after the first. + */ + private void grow() { + int length = this.serials.length; + if (length == 0) { + this.serials = new char[INIT_CAP]; + this.values = new Object[INIT_CAP]; + return; + } + int newCap = Math.max(Math.min(length << 1, KnownTagCodec.serialLimit()), length + 1); + this.serials = Arrays.copyOf(this.serials, newCap); + this.values = Arrays.copyOf(this.values, newCap); + } + + /** The flyweight over {@code value}, or null when there is none. */ + private EntryReader readerOf(long tagId, Object value) { + if (value == null) { + return null; + } + EntryReadingHelper reader = this.reader; + if (reader == null) { + reader = new EntryReadingHelper(); + this.reader = reader; + } + reader.set(tagId, value); + return reader; + } + + @Override + int size() { + return this.parent == null ? this.count : this.count + this.visibleParentCount(); + } + + /** Ancestor tags not shadowed or tombstoned by a nearer level, nearest-level-wins. */ + private int visibleParentCount() { + int count = 0; + for (DenseIdStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + for (int i = 0; i < ancestor.used; ++i) { + if (ancestor.values[i] != TOMBSTONE && this.visibleFrom(ancestor, i)) { + count++; + } + } + } + return count; + } + + @Override + boolean isEmpty() { + if (this.count != 0) { + return false; + } + DenseIdStore parent = this.parent; + if (parent == null) { + return true; + } + if (this.tombstoneMask == 0L) { + return parent.isEmpty(); // nothing hidden: the parent's tags are all visible + } + return this.visibleParentCount() == 0; + } + + @Override + boolean isDefinitelyEmpty() { + for (DenseIdStore level = this; level != null; level = level.parent) { + if (level.count != 0) { + return false; + } + } + return true; + } + + @Override + int estimateSize() { + int total = 0; + for (DenseIdStore level = this; level != null; level = level.parent) { + total += level.count; + } + return total; + } + + @Override + boolean hasParent() { + return this.parent != null; + } + + /** + * Whether slot {@code i} of the ancestor level {@code fromAncestor} is visible through this leaf: + * no nearer level holds a value or a tombstone for its tag. A nearer level whose masks rule the + * slot out is skipped without a scan. + */ + private boolean visibleFrom(DenseIdStore fromAncestor, int i) { + char serial = fromAncestor.serials[i]; + long bit = slotBit(KnownTagCodec.idOf(serial)); + for (DenseIdStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { + if (nearer.mayHaveLocal(bit) && nearer.indexOf(serial) >= 0) { + return false; + } + } + return true; + } + + /** Points {@code reader} at slot {@code i} of {@code level}, if it holds a visible tag. */ + private boolean readIfVisible(DenseIdStore level, int i, EntryReadingHelper reader) { + Object value = level.values[i]; + if (value == TOMBSTONE || (level != this && !this.visibleFrom(level, i))) { + return false; + } + reader.set(KnownTagCodec.idOf(level.serials[i]), value); + return true; + } + + @Override + void forEach(Consumer consumer) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (DenseIdStore level = this; level != null; level = level.parent) { + for (int i = 0; i < level.used; ++i) { + if (this.readIfVisible(level, i, reader)) { + consumer.accept(reader); + } + } + } + } + + @Override + void forEach(T thisObj, BiConsumer consumer) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (DenseIdStore level = this; level != null; level = level.parent) { + for (int i = 0; i < level.used; ++i) { + if (this.readIfVisible(level, i, reader)) { + consumer.accept(thisObj, reader); + } + } + } + } + + @Override + void forEach(T thisObj, U otherObj, TriConsumer consumer) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (DenseIdStore level = this; level != null; level = level.parent) { + for (int i = 0; i < level.used; ++i) { + if (this.readIfVisible(level, i, reader)) { + consumer.accept(thisObj, otherObj, reader); + } + } + } + } + + @Override + Cursor cursor() { + return new DenseCursor(this); + } + + /** Walks the leaf's slots, then each ancestor's, skipping tombstones and shadowed tags. */ + private static final class DenseCursor extends Cursor { + private final DenseIdStore leaf; + private DenseIdStore level; + private int index = -1; + private final EntryReadingHelper reader = new EntryReadingHelper(); + + DenseCursor(DenseIdStore leaf) { + this.leaf = leaf; + this.level = leaf; + } + + @Override + EntryReader next() { + DenseIdStore level = this.level; + while (level != null) { + for (++this.index; this.index < level.used; ++this.index) { + if (this.leaf.readIfVisible(level, this.index, this.reader)) { + return this.reader; + } + } + level = level.parent; + this.level = level; + this.index = -1; + } + return null; + } + } + + @Override + IdStore copy() { + DenseIdStore copy = new DenseIdStore(this.parent); + if (this.used != 0) { + copy.serials = Arrays.copyOf(this.serials, this.serials.length); + copy.values = Arrays.copyOf(this.values, this.values.length); + } + copy.used = this.used; + copy.count = this.count; + copy.occupancy = this.occupancy; + copy.tombstoneMask = this.tombstoneMask; + return copy; + } + + @Override + void putAllLocal(IdStore other) { + DenseIdStore that = (DenseIdStore) other; + if (that.count == 0) { + return; + } + if (this.used == 0 && that.used == that.count) { + // nothing here and no tombstones there: take copies of its arrays + this.serials = Arrays.copyOf(that.serials, that.serials.length); + this.values = Arrays.copyOf(that.values, that.values.length); + this.used = that.used; + this.count = that.count; + this.occupancy |= that.occupancy; + return; + } + for (int i = 0; i < that.used; ++i) { + Object value = that.values[i]; + if (value != TOMBSTONE) { + this.store(KnownTagCodec.idOf(that.serials[i]), value); + } + } + } + + @Override + void fillMapLocal(Map map) { + for (int i = 0; i < this.used; ++i) { + Object value = this.values[i]; + if (value != TOMBSTONE) { + map.put(nameAt(i), value); + } + } + } + + @Override + void fillStringMapLocal(Map map) { + for (int i = 0; i < this.used; ++i) { + Object value = this.values[i]; + if (value != TOMBSTONE) { + map.put(nameAt(i), TagValueConversions.toString(value)); + } + } + } + + private String nameAt(int i) { + return KnownTagCodec.nameOf(KnownTagCodec.idOf(this.serials[i])); + } + + @Override + void checkIntegrity() { + if (this.used > this.serials.length || this.serials.length != this.values.length) { + throw new IllegalStateException("incorrect capacity"); + } + for (int i = 0; i < this.used; ++i) { + char serial = this.serials[i]; + if (serial == 0 || serial >= KnownTagCodec.serialLimit()) { + throw new IllegalStateException("not a known serial: " + (int) serial); + } + long bit = slotBit(KnownTagCodec.idOf(serial)); + long mask = this.values[i] == TOMBSTONE ? this.tombstoneMask : this.occupancy; + if ((mask & bit) == 0L) { + throw new IllegalStateException("slot bit not set: " + nameAt(i)); + } + if (this.values[i] == null) { + throw new IllegalStateException("null value: " + nameAt(i)); + } + for (int j = i + 1; j < this.used; ++j) { + if (this.serials[j] == serial) { + throw new IllegalStateException("duplicate tag: " + nameAt(i)); + } + } + } + if (this.count != this.computeSize()) { + throw new IllegalStateException("incorrect size"); + } + } + + @Override + int computeSize() { + int live = 0; + for (int i = 0; i < this.used; ++i) { + if (this.values[i] != TOMBSTONE) live++; + } + return live; + } + + @Override + String toInternalString() { + StringBuilder ledger = new StringBuilder(); + for (int i = 0; i < this.used; ++i) { + Object value = this.values[i]; + ledger + .append('[') + .append(i) + .append("]: ") + .append(nameAt(i)) + .append('=') + .append(value == TOMBSTONE ? "" : value) + .append('\n'); + } + return ledger.toString(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 1293af7d33c..7f2c4c5df9c 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -33,11 +33,14 @@ final class HashIdStore extends IdStore { this.parent = (HashIdStore) parent; } - /** A parentless store over {@code buckets}; reads no statics, so TagMap.EMPTY can use it. */ - HashIdStore(Object[] buckets, int count) { + /** An empty parentless store over one bucket, for {@link TagMap#EMPTY}, which is frozen. */ + static HashIdStore newEmpty() { + return new HashIdStore(new Object[1]); + } + + private HashIdStore(Object[] buckets) { super(null); this.buckets = buckets; - this.count = count; this.parent = null; } @@ -276,6 +279,45 @@ private void forEachParent( } } + @Override + Cursor cursor() { + return new HashCursor(this); + } + + /** Walks the leaf's buckets, then each ancestor's, skipping tombstones and shadowed entries. */ + private static final class HashCursor extends Cursor { + private final HashIdStore leaf; + private HashIdStore level; + private final HashSupport.BucketWalker walker = new HashSupport.BucketWalker(); + + HashCursor(HashIdStore leaf) { + this.leaf = leaf; + this.level = leaf; + this.walker.reset(leaf.buckets); + } + + @Override + EntryReader next() { + HashIdStore level = this.level; + while (level != null) { + Entry entry = this.walker.next(); + if (entry != null) { + if (!entry.tombstone + && (level == this.leaf || this.leaf.parentEntryVisible(entry, level))) { + return entry; + } + continue; + } + level = level.parent; + this.level = level; + if (level != null) { + this.walker.reset(level.buckets); + } + } + return null; + } + } + @Override IdStore copy() { HashIdStore copy = new HashIdStore(this.parent); diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java index 01a498258cb..b9e28a8391b 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashSupport.java +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -416,4 +416,54 @@ static String toInternalString(Object[] buckets) { } return ledger.toString(); } + + /** Walks a bucket array's raw entries, tombstones included, for an iterator. */ + static final class BucketWalker { + private Object[] buckets; + private int bucketIndex; + private BucketGroup group; + private int groupIndex; + + /** Starts over on {@code buckets}. */ + void reset(Object[] buckets) { + this.buckets = buckets; + this.bucketIndex = -1; + this.group = null; + this.groupIndex = 0; + } + + /** The next raw entry, or null when the array is done. */ + Entry next() { + while (this.bucketIndex < this.buckets.length) { + if (this.group != null) { + for (++this.groupIndex; this.groupIndex < BucketGroup.LEN; ++this.groupIndex) { + Entry tagEntry = this.group._entryAt(this.groupIndex); + if (tagEntry != null) return tagEntry; + } + + // done processing - that group, go to next group + this.group = this.group.prev; + this.groupIndex = -1; + } + + // if the group is null, then we've finished the current bucket - so advance the bucket + if (this.group == null) { + for (++this.bucketIndex; this.bucketIndex < this.buckets.length; ++this.bucketIndex) { + Object bucket = this.buckets[this.bucketIndex]; + + if (bucket instanceof Entry) { + return (Entry) bucket; + } else if (bucket instanceof BucketGroup) { + this.group = (BucketGroup) bucket; + this.groupIndex = -1; + + break; + } + } + } + } + + return null; + } + } } diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 2f84ab52a79..47af85a9d07 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -16,9 +16,9 @@ *

A span in the tracer core owns its id store directly ({@link TagRouting#newIdStore}); a {@link * TagMap} owns one too. * - *

Each implementation is its own class: {@link HashIdStore} today, a dense store later. Nothing - * that is always loaded may extend this class, so that a call through it stays monomorphic. No - * locking: guarded by the owner's lock. + *

Each implementation is its own class -- {@link HashIdStore} or {@link DenseIdStore}, one per + * JVM, chosen by {@link IdStoreFactory} -- and nothing that is always loaded may extend this class, + * so a call through it stays monomorphic. No locking: guarded by the owner's lock. * *

The base keeps the slot masks an implementation shares: a tag's slot ({@link * KnownTagCodec#slotOf}) maps it to one bit of a {@code long}. A bit is a may-be-present hint -- @@ -144,6 +144,18 @@ static IllegalArgumentException notKnown(long tagId) { abstract void forEach( T thisObj, U otherObj, TriConsumer consumer); + /** + * A cursor over the visible entries, local then inherited, for an iterator. Each entry it returns + * is valid until its next call, as with a point read. + */ + abstract Cursor cursor(); + + /** Visits a store's visible entries one at a time. */ + abstract static class Cursor { + /** The next visible entry, or null when there are none left. */ + abstract EntryReader next(); + } + /** An independently mutable copy with the same parent, sharing Entry objects. */ abstract IdStore copy(); diff --git a/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java b/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java new file mode 100644 index 00000000000..906ee7ddd69 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java @@ -0,0 +1,62 @@ +package datadog.trace.api; + +/** + * Builds the {@link IdStore}s of the one implementation this JVM uses: {@link DenseIdStore} when + * the dense store is on, {@link HashIdStore} otherwise. Rollout scaffolding: the hash store stays + * one flag away until the dense store is proven. + * + *

Every store is built through {@link Default#INSTANCE}, never with {@code new}. The factory not + * chosen is loaded (to check the field's type) but never linked, so its store class is never + * loaded; with one {@code IdStore} subclass loaded, class hierarchy analysis devirtualizes every + * call on an {@code IdStore} without a guard. A store builds its own copies. + * + *

No static state here, and no {@link TagMap}: {@link TagMap#EMPTY} is built with {@link + * #newEmpty}, so class initialization works whichever of the two starts first. + */ +abstract class IdStoreFactory { + /** The JVM's store, chosen once by the dense-store flag. */ + static final class Default { + static final IdStoreFactory INSTANCE = + Config.get().isTraceDenseTagsEnabled() + ? DenseIdStoreFactory.INSTANCE + : HashIdStoreFactory.INSTANCE; + } + + /** A fresh store reading through to {@code parent} (may be null). */ + abstract IdStore create(IdStore parent); + + /** A new empty parentless store, for a frozen map. */ + abstract IdStore newEmpty(); +} + +final class HashIdStoreFactory extends IdStoreFactory { + static final HashIdStoreFactory INSTANCE = new HashIdStoreFactory(); + + private HashIdStoreFactory() {} + + @Override + IdStore create(IdStore parent) { + return new HashIdStore(parent); + } + + @Override + IdStore newEmpty() { + return HashIdStore.newEmpty(); + } +} + +final class DenseIdStoreFactory extends IdStoreFactory { + static final DenseIdStoreFactory INSTANCE = new DenseIdStoreFactory(); + + private DenseIdStoreFactory() {} + + @Override + IdStore create(IdStore parent) { + return new DenseIdStore(parent); + } + + @Override + IdStore newEmpty() { + return new DenseIdStore(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 d8f57ed275e..371f0545256 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -51,7 +51,7 @@ public final class TagMap implements Map, Iterable. - public static final TagMap EMPTY = new TagMap(new Object[1], 0); + public static final TagMap EMPTY = new TagMap(IdStoreFactory.Default.INSTANCE.newEmpty()); // Sentinel for an ID not yet resolved by the reused EntryReadingHelper. Zero denotes an unknown // tag, so it cannot represent "not computed". @@ -1286,7 +1286,7 @@ public TagMap() { * optimizations can treat it as fixed. */ private TagMap(TagMap parent) { - this.ids = new HashIdStore(parent == null ? null : parent.ids); + this.ids = IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.ids); HashStore parentNames = parent == null ? null : parent.names; this.names = parentNames; this.namesBorrowed = parentNames != null; @@ -1303,9 +1303,9 @@ private TagMap(TagMap parent, IdStore ids, HashStore names, boolean namesBorrowe this.parent = parent; } - /** Used for inexpensive immutable */ - private TagMap(Object[] buckets, int size) { - this.ids = new HashIdStore(buckets, size); + /** A frozen map over the empty parentless store {@code ids}: {@link #EMPTY}. */ + private TagMap(IdStore ids) { + this.ids = ids; this.names = null; this.namesBorrowed = false; this.frozen = true; @@ -2013,7 +2013,7 @@ public void clear() { // clear() removes ALL mappings, including any inherited through read-through. Fresh parentless // stores (rather than tombstoning every inherited key) are simpler and cheaper, and leave an // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. - this.ids = new HashIdStore(null); + this.ids = IdStoreFactory.Default.INSTANCE.create(null); this.names = null; this.namesBorrowed = false; this.parent = null; @@ -2113,27 +2113,18 @@ String toInternalString() { } abstract static class IteratorBase { - // Two phases: the id store's chain, then the custom store's. In each, the level whose buckets - // are being walked is the leaf first, then each ancestor in turn (read-through union). - // (A dense id store will need its own walk.) - private final HashIdStore idLeaf; + // Two phases: the id store's visible entries, through its own cursor, then the custom store's + // chain -- the leaf's buckets first, then each ancestor's in turn (read-through union). + private IdStore.Cursor ids; // null once the id phase is done private final HashStore nameLeaf; - private HashIdStore idLevel; - private HashStore nameLevel; // non-null once in the custom phase - private Object[] buckets; - - private Entry nextEntry; - - private int bucketIndex = -1; + private HashStore nameLevel; // the custom level being walked; null before and after + private HashSupport.BucketWalker walker; - private BucketGroup group = null; - private int groupIndex = 0; + private EntryReader nextEntry; IteratorBase(TagMap map) { - this.idLeaf = (HashIdStore) map.ids; + this.ids = map.ids.cursor(); this.nameLeaf = map.names; - this.idLevel = this.idLeaf; - this.buckets = this.idLeaf.buckets; } public final boolean hasNext() { @@ -2143,9 +2134,9 @@ public final boolean hasNext() { return this.nextEntry != null; } - final Entry nextEntryOrThrowNoSuchElement() { + final EntryReader nextEntryOrThrowNoSuchElement() { if (this.nextEntry != null) { - Entry nextEntry = this.nextEntry; + EntryReader nextEntry = this.nextEntry; this.nextEntry = null; return nextEntry; } @@ -2157,104 +2148,38 @@ final Entry nextEntryOrThrowNoSuchElement() { } } - final Entry nextEntryOrNull() { - if (this.nextEntry != null) { - Entry nextEntry = this.nextEntry; - this.nextEntry = null; - return nextEntry; - } - - return this.hasNext() ? this.nextEntry : null; - } - - private final Entry advance() { - while (true) { - Entry tagEntry = this.rawAdvance(); - if (tagEntry != null) { - // leaf entries emit as-is; ancestor entries only if visible from the leaf -- not shadowed - // by a nearer level and not tombstoned. (parentEntryVisible walks the nearer levels.) - if (this.isVisible(tagEntry)) { - return tagEntry; - } - continue; // tombstone, or ancestor entry shadowed/tombstoned -> skip - } - - // current level exhausted; advance to the next ancestor's buckets (read-through union), - // then from the id chain to the custom chain - Object[] next = this.nextLevel(); - if (next == null) { - return null; - } - this.buckets = next; - this.bucketIndex = -1; - this.group = null; - this.groupIndex = 0; - } - } - - private boolean isVisible(Entry entry) { - HashStore nameLevel = this.nameLevel; - if (nameLevel == null) { - return !entry.tombstone - && (this.idLevel == this.idLeaf || this.idLeaf.parentEntryVisible(entry, this.idLevel)); - } - return nameLevel == this.nameLeaf || this.nameLeaf.parentEntryVisible(entry, nameLevel); - } - - /** Moves to the next level's buckets, or returns null when both chains are done. */ - private Object[] nextLevel() { - HashStore nameLevel = this.nameLevel; - if (nameLevel == null) { - HashIdStore idParent = this.idLevel.parent; - if (idParent != null) { - this.idLevel = idParent; - return idParent.buckets; + private EntryReader advance() { + IdStore.Cursor ids = this.ids; + if (ids != null) { + EntryReader reader = ids.next(); + if (reader != null) { + return reader; } + this.ids = null; if (this.nameLeaf == null) { return null; } this.nameLevel = this.nameLeaf; - return this.nameLeaf.buckets; + this.walker = new HashSupport.BucketWalker(); + this.walker.reset(this.nameLeaf.buckets); } - HashStore nameParent = nameLevel.parent; - if (nameParent == null) { - return null; - } - this.nameLevel = nameParent; - return nameParent.buckets; - } - /** Next raw entry in the current bucket array, ignoring shadowing/tombstones. */ - private final Entry rawAdvance() { - while (this.bucketIndex < this.buckets.length) { - if (this.group != null) { - for (++this.groupIndex; this.groupIndex < BucketGroup.LEN; ++this.groupIndex) { - Entry tagEntry = this.group._entryAt(this.groupIndex); - if (tagEntry != null) return tagEntry; + HashStore level = this.nameLevel; + while (level != null) { + Entry entry = this.walker.next(); + if (entry != null) { + // leaf entries emit as-is; ancestor entries only if not shadowed by a nearer level + if (level == this.nameLeaf || this.nameLeaf.parentEntryVisible(entry, level)) { + return entry; } - - // done processing - that group, go to next group - this.group = this.group.prev; - this.groupIndex = -1; + continue; } - - // if the group is null, then we've finished the current bucket - so advance the bucket - if (this.group == null) { - for (++this.bucketIndex; this.bucketIndex < this.buckets.length; ++this.bucketIndex) { - Object bucket = this.buckets[this.bucketIndex]; - - if (bucket instanceof Entry) { - return (Entry) bucket; - } else if (bucket instanceof BucketGroup) { - this.group = (BucketGroup) bucket; - this.groupIndex = -1; - - break; - } - } + level = level.parent; + this.nameLevel = level; + if (level != null) { + this.walker.reset(level.buckets); } } - return null; } } @@ -2698,9 +2623,23 @@ public boolean isEmpty() { @Override public Iterator> iterator() { - @SuppressWarnings({"rawtypes", "unchecked"}) - Iterator> iter = (Iterator) this.map.iterator(); - return iter; + return new EntriesIterator(this.map); + } + } + + /** + * Map entries for the {@link Map} view: an Entry is its own, while a flyweight reader (a dense + * store's) builds one, since a caller may keep it. + */ + static final class EntriesIterator extends IteratorBase + implements Iterator> { + EntriesIterator(TagMap map) { + super(map); + } + + @Override + public Map.Entry next() { + return this.nextEntryOrThrowNoSuchElement().mapEntry(); } } @@ -2785,10 +2724,18 @@ public Object next() { final class EntryReadingHelper implements TagMap.EntryReader { private Map.Entry mapEntry; - private String tag; + private String tag; // null when set by id: resolved lazily via nameOf on first tag() access private Object value; private long tagId; + /** A known tag's value by id, as a dense id store reads it: no name, no Entry. */ + void set(long tagId, Object value) { + this.mapEntry = null; + this.tag = null; + this.value = value; + this.tagId = tagId; + } + void set(String tag, Object value) { this.mapEntry = null; this.tag = tag; @@ -2805,7 +2752,12 @@ void set(Map.Entry mapEntry) { @Override public String tag() { - return this.tag; + String tag = this.tag; + if (tag != null) return tag; + + tag = KnownTagCodec.nameOf(this.tagId); + this.tag = tag; + return tag; } @Override @@ -2880,7 +2832,10 @@ public Object objectValue() { @Override public TagMap.Entry entry() { - return TagMap.anyEntryFor(this.tag, this.value); + long id = this.tagId; + return id != TagMap.TAG_ID_NOT_COMPUTED && KnownTagCodec.isKnown(id) + ? TagMap.Entry.newAnyEntry(id, this.value) + : TagMap.anyEntryFor(this.tag, this.value); } @Override diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index e8a124ac4f0..35d32c9dbbc 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -57,7 +57,7 @@ public static TagMap readThroughParent(TagMap parent) { /** A fresh id store reading through to {@code parent}'s known tags (parent may be null). */ public static IdStore newIdStore(TagMap parent) { - return new HashIdStore(parent == null ? null : parent.ids); + return IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.ids); } /** diff --git a/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java b/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java new file mode 100644 index 00000000000..536ed80f00d --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java @@ -0,0 +1,50 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +/** + * The dense-store flag on: only {@link DenseIdStore} is loaded, so calls on an {@link IdStore} stay + * monomorphic. The flag is read once into a static final, so this runs in its own JVM ({@code + * ForkedTest}) with the flag set before anything reads it. The shared suites cover the dense + * store's behavior when run with {@code DD_TRACE_EXPERIMENTAL_DENSE_TAGS_ENABLED=true}. + */ +class DenseIdStoreForkedTest { + static { + System.setProperty("dd.trace.experimental.dense.tags.enabled", "true"); + } + + @Test + void onlyTheDenseStoreIsLoaded() { + TagMap map = TagMap.create(); + map.set(KnownTags.COMPONENT_ID, "okhttp"); + map.set("custom.tag", "custom"); + + assertSame(DenseIdStore.class, map.ids.getClass()); + assertSame(DenseIdStore.class, TagMap.EMPTY.ids.getClass()); + assertEquals("okhttp", map.getObject(KnownTags.COMPONENT_NAME)); + assertFalse(isLoaded("datadog.trace.api.HashIdStore")); + } + + /** Whether {@code className} has been loaded by TagMap's class loader, without loading it. */ + private static boolean isLoaded(String className) { + Method findLoadedClass; + try { + findLoadedClass = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + findLoadedClass.setAccessible(true); + } catch (RuntimeException | NoSuchMethodException e) { + assumeTrue(false, "cannot inspect loaded classes on this JVM: " + e); + return false; + } + try { + return findLoadedClass.invoke(TagMap.class.getClassLoader(), className) != null; + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 09bf240ca66..607fa60c9be 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -92,8 +92,8 @@ void removingAnInheritedKnownTagTombstonesItWithoutCountingIt() { for (TagMap.EntryReader entry : child) { assertNotEquals(KnownTags.HTTP_ROUTE_ID, entry.tagId()); } - HashIdStore ids = (HashIdStore) child.ids; - assertEquals(0, ids.count); + IdStore ids = child.ids; + assertEquals(0, ids.computeSize()); assertNotEquals(0L, ids.tombstoneMask); child.checkIntegrity(); diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 45e12e44c85..9b28bf1b01a 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -5844,6 +5844,14 @@ "aliases": [] } ], + "DD_TRACE_EXPERIMENTAL_DENSE_TAGS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED": [ { "version": "A", From 462f75330ae695cf13712c999996786393de5f82 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 21:43:44 -0400 Subject: [PATCH 78/99] Read a root span's trace-level tags through one shared parent With builder tags taking precedence, a root span now reads its root-span tags through a frozen parent holding them over the merged tracer tags, built once per config snapshot, instead of copying them in. The span's own id store then keeps only span-level tags, which the slot coloring assumes: a span-level and a trace-level tag may share a slot bit, so a root tag held locally made a missing span-level read scan. Tags needing interception are still copied in, and the default order (flag off) is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/CoreTracer.java | 61 ++++++++++++- .../core/RootSpanTagsReadThroughTest.java | 90 +++++++++++++++++++ 2 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 9ed3f34aceb..cdd3165d221 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -209,10 +209,43 @@ private static final class LocalRootSpanTags { final TagMap tags; final boolean needsIntercept; + /** The last {@link #traceLevelParent}, with the merged tracer tags it was built over. */ + private volatile TraceLevelTags traceLevel; + LocalRootSpanTags(final TagMap tags, final boolean needsIntercept) { this.tags = tags; this.needsIntercept = needsIntercept; } + + /** + * All of a root span's trace-level tags in one frozen map -- these tags over {@code + * mergedTracerTags} -- for the root span to read through instead of copying them in. Keeps the + * span's own store to span-level tags, which the tag registry's slot coloring assumes: it lets + * a span-level and a trace-level tag share a slot bit. Built once per config snapshot (a racy + * rebuild is harmless: the result is the same). + */ + TagMap traceLevelParent(final TagMap mergedTracerTags) { + TraceLevelTags cached = this.traceLevel; + if (cached != null && cached.mergedTracerTags == mergedTracerTags) { + return cached.parent; + } + TagMap parent = TagMap.create(); + parent.putAll(mergedTracerTags); + parent.putAll(this.tags); + parent.freeze(); + this.traceLevel = new TraceLevelTags(mergedTracerTags, parent); + return parent; + } + } + + private static final class TraceLevelTags { + final TagMap mergedTracerTags; + final TagMap parent; + + TraceLevelTags(final TagMap mergedTracerTags, final TagMap parent) { + this.mergedTracerTags = mergedTracerTags; + this.parent = parent; + } } private volatile LocalRootSpanTags localRootSpanTags; @@ -2139,6 +2172,7 @@ protected static final DDSpanContext buildSpanContext( final CharSequence origin; final TagMap coreTags; final boolean coreTagsNeedsIntercept; + final LocalRootSpanTags localRootSpanTags; final TagMap rootSpanTags; final boolean rootSpanTagsNeedsIntercept; final DDSpanContext context; @@ -2168,6 +2202,7 @@ protected static final DDSpanContext buildSpanContext( origin = null; coreTags = null; coreTagsNeedsIntercept = false; + localRootSpanTags = null; rootSpanTags = null; rootSpanTagsNeedsIntercept = false; parentServiceName = ddsc.getServiceName(); @@ -2242,7 +2277,7 @@ protected static final DDSpanContext buildSpanContext( ciVisibilityContextData = null; } - LocalRootSpanTags localRootSpanTags = tracer.localRootSpanTags; + localRootSpanTags = tracer.localRootSpanTags; rootSpanTags = localRootSpanTags.tags; rootSpanTagsNeedsIntercept = localRootSpanTags.needsIntercept; @@ -2321,11 +2356,27 @@ protected static final DDSpanContext buildSpanContext( final TagMap mergedTracerTags = traceConfig.mergedTracerTags; boolean mergedTracerTagsNeedsIntercept = traceConfig.mergedTracerTagsNeedsIntercept; + // With builder tags taking precedence, a root span reads its trace-level tags (root-span tags + // over the merged tracer tags) through one shared frozen parent rather than copying the + // root-span tags in. They become the precedence floor -- the order already puts them below + // the builder and contextual tags. Tags needing interception are still copied in. + final boolean rootSpanTagsReadThrough = + localRootSpanTags != null + && tracer.builderTagsPrecedence + && !rootSpanTagsNeedsIntercept + && !mergedTracerTagsNeedsIntercept; + final TagMap tagParent; + if (rootSpanTagsReadThrough) { + tagParent = localRootSpanTags.traceLevelParent(mergedTracerTags); + } else { + tagParent = mergedTracerTagsNeedsIntercept ? null : mergedTracerTags; + } + final int tagsSize = mergedTracerTags.size() + (null == tagLedger ? 0 : tagLedger.estimateSize()) + (null == coreTags ? 0 : coreTags.size()) - + (null == rootSpanTags ? 0 : rootSpanTags.size()) + + (null == rootSpanTags || rootSpanTagsReadThrough ? 0 : rootSpanTags.size()) + (null == contextualTags ? 0 : contextualTags.size()); if (builderRequestContextDataAppSec != null) { @@ -2366,7 +2417,7 @@ protected static final DDSpanContext buildSpanContext( tracer.profilingContextIntegration, tracer.injectBaggageAsTags, tracer.injectLinksAsTags, - mergedTracerTagsNeedsIntercept ? null : mergedTracerTags); + tagParent); // By setting the tags on the context we apply decorators to any tags that have been set via // the builder. The `mergedTracerTags` are always applied first (the precedence floor: @@ -2391,7 +2442,9 @@ protected static final DDSpanContext buildSpanContext( } if (tracer.builderTagsPrecedence) { context.setAllTags(coreTags, coreTagsNeedsIntercept); - context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept); + if (!rootSpanTagsReadThrough) { + context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept); + } context.setAllTags(contextualTags); context.setAllTags(tagLedger); } else { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java b/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java new file mode 100644 index 00000000000..2219ad4c2cf --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java @@ -0,0 +1,90 @@ +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.config.TracerConfig; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * With builder tags taking precedence, a root span reads its root-span tags through the shared + * trace-level parent instead of holding copies: the same values, none of them stored locally, so + * the span's own store keeps only span-level tags (the slot coloring lets a span-level and a + * trace-level tag share a slot bit). + */ +class RootSpanTagsReadThroughTest extends DDCoreJavaSpecification { + private static final String CUSTOM_ROOT_TAG = "only_root"; + + private CoreTracer tracer; + + @AfterEach + void cleanup() { + if (tracer != null) { + tracer.close(); + } + } + + private CoreTracer tracer(boolean builderTagsPrecedence) { + Map rootTags = new LinkedHashMap<>(); + rootTags.put(KnownTags.LANGUAGE_NAME, "jvm"); + rootTags.put(KnownTags.PROCESS_ID_NAME, 42L); + rootTags.put(CUSTOM_ROOT_TAG, "value"); + Properties properties = new Properties(); + properties.setProperty( + TracerConfig.TRACE_BUILDER_TAGS_PRECEDENCE_ENABLED, String.valueOf(builderTagsPrecedence)); + return tracerBuilder().withProperties(properties).localRootSpanTags(rootTags).build(); + } + + @Test + void aRootSpanReadsItsRootTagsThroughTheTraceLevelParent() throws Exception { + tracer = tracer(false); + int copiedLocally = localKnownTagCount(rootSpanOf(tracer)); + tracer.close(); + + tracer = tracer(true); + DDSpan root = rootSpanOf(tracer); + assertEquals("jvm", root.getTag(KnownTags.LANGUAGE_NAME)); + assertEquals(42L, root.getTag(KnownTags.PROCESS_ID_NAME)); + assertEquals("value", root.getTag(CUSTOM_ROOT_TAG)); + assertEquals(copiedLocally - 2, localKnownTagCount(root)); + } + + @Test + void aChildSpanStillGetsNoRootTags() { + tracer = tracer(true); + AgentSpan root = tracer.buildSpan("test", "root").start(); + AgentSpan child = tracer.buildSpan("test", "child").asChildOf(root.spanContext()).start(); + try { + assertNull(child.getTag(KnownTags.LANGUAGE_NAME)); + assertNull(child.getTag(CUSTOM_ROOT_TAG)); + } finally { + child.finish(); + root.finish(); + } + } + + private static DDSpan rootSpanOf(CoreTracer tracer) { + AgentSpan span = tracer.buildSpan("test", "root").start(); + span.finish(); + return (DDSpan) span; + } + + /** The known tags the span's own id store holds, not counting what it reads through. */ + private static int localKnownTagCount(DDSpan span) throws Exception { + Field tagIds = DDSpanContext.class.getDeclaredField("tagIds"); + tagIds.setAccessible(true); + Object ids = tagIds.get(span.spanContext()); + Method computeSize = + Class.forName("datadog.trace.api.IdStore").getDeclaredMethod("computeSize"); + computeSize.setAccessible(true); + return (Integer) computeSize.invoke(ids); + } +} From 634e880974ba3de47e2d0b72312301b3d4f6d6a1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 16 Oct 2026 22:16:56 -0400 Subject: [PATCH 79/99] Keep span-level and trace-level slot bits apart with a level set The slot coloring gives trace-level tags their own clique that reuses the span-level slots, since they're meant to live in a separate store. A span's inheritedMask merged the trace-level parent's bits into its word, so a missing span-level tag that shared a slot with a parent's trace-level tag (http.method / runtime-id) still read through and scanned. Each IdStore now records the levels it holds, local and inherited, from the id's level bit, and consults its slot bits only for a level that is present. In a store holding one level the word stays exact; a mixed store still works, with a cross-level collision costing a probe. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseIdStore.java | 56 +++++++++++-------- .../java/datadog/trace/api/HashIdStore.java | 35 +++++++----- .../main/java/datadog/trace/api/IdStore.java | 52 ++++++++++++++--- .../java/datadog/trace/api/TagStoresTest.java | 20 +++++++ 4 files changed, 120 insertions(+), 43 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index fc204647a4d..e3a472d940c 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -66,19 +66,21 @@ public Object getKnownValue(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } - return this.getVisible((char) KnownTagCodec.serialNum(tagId), slotBit(tagId)); + return this.getVisible((char) KnownTagCodec.serialNum(tagId), slotBit(tagId), levelOf(tagId)); } - /** The visible value of the tag {@code serial} in the slot {@code bit}, or null. */ - private Object getVisible(char serial, long bit) { - if (this.mayHaveLocal(bit)) { + /** + * The visible value of the tag {@code serial} in the slot {@code bit} of {@code level}, or null. + */ + private Object getVisible(char serial, long bit, int level) { + if (this.mayHaveLocal(bit, level)) { int i = this.indexOf(serial); if (i >= 0) { Object value = this.values[i]; return value == TOMBSTONE ? null : value; } } - return this.mayInherit(bit) ? this.parent.getVisible(serial, bit) : null; + return this.mayInherit(bit, level) ? this.parent.getVisible(serial, bit, level) : null; } @Override @@ -127,19 +129,20 @@ public EntryReader getAndPut(Entry knownEntry) { long tagId = knownEntry.tagHash; char serial = (char) KnownTagCodec.serialNum(tagId); long bit = slotBit(tagId); - int i = this.mayHaveLocal(bit) ? this.indexOf(serial) : -1; + int level = levelOf(tagId); + int i = this.mayHaveLocal(bit, level) ? this.indexOf(serial) : -1; Object prior; if (i >= 0) { prior = this.values[i]; if (prior == TOMBSTONE) { prior = null; // tombstoned, so not visible - this.occupancy |= bit; + this.markLocal(bit, level); this.count++; } this.values[i] = knownEntry.objectValue(); } else { - prior = this.mayInherit(bit) ? this.parent.getVisible(serial, bit) : null; - this.append(serial, bit, knownEntry.objectValue()); + prior = this.mayInherit(bit, level) ? this.parent.getVisible(serial, bit, level) : null; + this.append(serial, bit, level, knownEntry.objectValue()); } return this.readerOf(tagId, prior); } @@ -151,8 +154,9 @@ public EntryReader remove(long tagId) { } char serial = (char) KnownTagCodec.serialNum(tagId); long bit = slotBit(tagId); + int level = levelOf(tagId); - int i = this.mayHaveLocal(bit) ? this.indexOf(serial) : -1; + int i = this.mayHaveLocal(bit, level) ? this.indexOf(serial) : -1; Object local = null; if (i >= 0) { local = this.values[i]; @@ -160,10 +164,10 @@ public EntryReader remove(long tagId) { return null; // already removed } } - if (this.mayInherit(bit)) { + if (this.mayInherit(bit, level)) { // Read-through: hide an inherited value with a tombstone. The prior *visible* value is the // local one if there was one, otherwise the inherited one. - Object inherited = this.parent.getVisible(serial, bit); + Object inherited = this.parent.getVisible(serial, bit, level); if (inherited != null) { if (i >= 0) { this.values[i] = TOMBSTONE; @@ -171,7 +175,7 @@ public EntryReader remove(long tagId) { } else { this.appendSlot(serial, TOMBSTONE); } - this.tombstoneMask |= bit; + this.markTombstone(bit, level); return this.readerOf(tagId, local != null ? local : inherited); } } @@ -186,25 +190,26 @@ public EntryReader remove(long tagId) { private Object store(long tagId, Object value) { char serial = (char) KnownTagCodec.serialNum(tagId); long bit = slotBit(tagId); - int i = this.mayHaveLocal(bit) ? this.indexOf(serial) : -1; + int level = levelOf(tagId); + int i = this.mayHaveLocal(bit, level) ? this.indexOf(serial) : -1; if (i >= 0) { Object prior = this.values[i]; this.values[i] = value; if (prior == TOMBSTONE) { - this.occupancy |= bit; + this.markLocal(bit, level); this.count++; // a tombstone replaced is a removal undone: no prior local value return null; } return prior; } - this.append(serial, bit, value); + this.append(serial, bit, level, value); return null; } /** Appends a live local tag. */ - private void append(char serial, long bit, Object value) { + private void append(char serial, long bit, int level, Object value) { this.appendSlot(serial, value); - this.occupancy |= bit; + this.markLocal(bit, level); this.count++; } @@ -331,9 +336,11 @@ boolean hasParent() { */ private boolean visibleFrom(DenseIdStore fromAncestor, int i) { char serial = fromAncestor.serials[i]; - long bit = slotBit(KnownTagCodec.idOf(serial)); + long tagId = KnownTagCodec.idOf(serial); + long bit = slotBit(tagId); + int level = levelOf(tagId); for (DenseIdStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - if (nearer.mayHaveLocal(bit) && nearer.indexOf(serial) >= 0) { + if (nearer.mayHaveLocal(bit, level) && nearer.indexOf(serial) >= 0) { return false; } } @@ -431,6 +438,7 @@ IdStore copy() { copy.count = this.count; copy.occupancy = this.occupancy; copy.tombstoneMask = this.tombstoneMask; + copy.levels = this.levels; return copy; } @@ -447,6 +455,7 @@ void putAllLocal(IdStore other) { this.used = that.used; this.count = that.count; this.occupancy |= that.occupancy; + this.levels |= that.levels; return; } for (int i = 0; i < that.used; ++i) { @@ -491,11 +500,14 @@ void checkIntegrity() { if (serial == 0 || serial >= KnownTagCodec.serialLimit()) { throw new IllegalStateException("not a known serial: " + (int) serial); } - long bit = slotBit(KnownTagCodec.idOf(serial)); + long tagId = KnownTagCodec.idOf(serial); long mask = this.values[i] == TOMBSTONE ? this.tombstoneMask : this.occupancy; - if ((mask & bit) == 0L) { + if ((mask & slotBit(tagId)) == 0L) { throw new IllegalStateException("slot bit not set: " + nameAt(i)); } + if ((this.levels & levelOf(tagId)) == 0) { + throw new IllegalStateException("level not set: " + nameAt(i)); + } if (this.values[i] == null) { throw new IllegalStateException("null value: " + nameAt(i)); } diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 7f2c4c5df9c..d1a592bceef 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -49,18 +49,18 @@ public Entry get(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } - return this.getVisible(tagId, slotBit(tagId)); + return this.getVisible(tagId, slotBit(tagId), levelOf(tagId)); } - /** {@link #get} for a validated id and its slot bit. */ - private Entry getVisible(long tagId, long bit) { - if (this.mayHaveLocal(bit)) { + /** {@link #get} for a validated id and its slot bit and level. */ + private Entry getVisible(long tagId, long bit, int level) { + if (this.mayHaveLocal(bit, level)) { Entry local = HashSupport.findById(this.buckets, Entry.bucketHash(tagId), tagId); if (local != null) { return local.tombstone ? null : local; } } - return this.mayInherit(bit) ? this.parent.getVisible(tagId, bit) : null; + return this.mayInherit(bit, level) ? this.parent.getVisible(tagId, bit, level) : null; } @Override @@ -109,7 +109,7 @@ public void set(long tagId, double value) { @Override public Entry put(Entry knownEntry) { Entry replaced = HashSupport.put(this.buckets, knownEntry); - this.occupancy |= slotBit(knownEntry.tagHash); + this.markLocal(slotBit(knownEntry.tagHash), levelOf(knownEntry.tagHash)); if (replaced == null || replaced.tombstone) { this.count++; // a tombstone replaced is a removal undone: no prior local entry return null; @@ -121,16 +121,17 @@ public Entry put(Entry knownEntry) { public Entry getAndPut(Entry knownEntry) { long tagId = knownEntry.tagHash; long bit = slotBit(tagId); + int level = levelOf(tagId); Entry replaced = HashSupport.put(this.buckets, knownEntry); - this.occupancy |= bit; + this.markLocal(bit, level); if (replaced != null && !replaced.tombstone) { return replaced; // replaced a local entry -> that is the prior value } this.count++; - if (replaced != null || !this.mayInherit(bit)) { + if (replaced != null || !this.mayInherit(bit, level)) { return null; // it was tombstoned, so not visible, or nothing to inherit } - return this.parent.getVisible(tagId, bit); + return this.parent.getVisible(tagId, bit, level); } @Override @@ -139,9 +140,11 @@ public Entry remove(long tagId) { throw notKnown(tagId); } long bit = slotBit(tagId); + int level = levelOf(tagId); int hash = Entry.bucketHash(tagId); - Entry local = this.mayHaveLocal(bit) ? HashSupport.findById(this.buckets, hash, tagId) : null; + Entry local = + this.mayHaveLocal(bit, level) ? HashSupport.findById(this.buckets, hash, tagId) : null; if (local != null) { if (local.tombstone) { return null; // already removed @@ -149,13 +152,13 @@ public Entry remove(long tagId) { HashSupport.remove(this.buckets, hash, local.tag); this.count--; } - if (this.mayInherit(bit)) { + if (this.mayInherit(bit, level)) { // Read-through: hide an inherited value with a tombstone. The prior *visible* value is the // local entry if there was one, otherwise the inherited one. - Entry inherited = this.parent.getVisible(tagId, bit); + Entry inherited = this.parent.getVisible(tagId, bit, level); if (inherited != null) { HashSupport.put(this.buckets, Entry.tombstoneOf(tagId)); - this.tombstoneMask |= bit; + this.markTombstone(bit, level); return local != null ? local : inherited; } } @@ -221,9 +224,11 @@ boolean hasParent() { boolean parentEntryVisible(Entry parentEntry, HashIdStore fromAncestor) { long tagId = parentEntry.tagHash; long bit = slotBit(tagId); + int level = levelOf(tagId); int hash = parentEntry.hash(); for (HashIdStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - if (nearer.mayHaveLocal(bit) && HashSupport.findById(nearer.buckets, hash, tagId) != null) { + if (nearer.mayHaveLocal(bit, level) + && HashSupport.findById(nearer.buckets, hash, tagId) != null) { return false; } } @@ -325,6 +330,7 @@ IdStore copy() { copy.count = this.count; copy.occupancy = this.occupancy; copy.tombstoneMask = this.tombstoneMask; + copy.levels = this.levels; return copy; } @@ -346,6 +352,7 @@ void putAllLocal(IdStore other) { this.count += HashSupport.merge(this.buckets, that.buckets); } this.occupancy |= that.occupancy; + this.levels |= that.levels; } @Override diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 47af85a9d07..c922d217c87 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -24,6 +24,12 @@ * KnownTagCodec#slotOf}) maps it to one bit of a {@code long}. A bit is a may-be-present hint -- * tags that don't usually co-occur share a slot -- so a clear bit is a definite miss and a set bit * means "probe". The masks are never cleared by a removal, only widened. + * + *

Span-level and trace-level tags are colored separately and reuse the same slots, since they + * are meant to live in separate stores (a span's trace-level tags in the frozen parent it reads + * through). So each store also records which levels it holds, local and inherited, and a lookup + * consults the slot bits only for a level that is present: in a store holding one level the word is + * exact, and a store holding both still works, with a cross-level collision costing a probe. */ @Restricted(allowedIn = {"datadog.trace.core"}) public abstract class IdStore { @@ -39,8 +45,23 @@ public abstract class IdStore { */ final long inheritedMask; + /** The levels ({@link #SPAN_LEVEL}, {@link #TRACE_LEVEL}) of the local entries and tombstones. */ + byte levels; + + /** The levels an ancestor may hold, fixed at construction like {@link #inheritedMask}. */ + final byte inheritedLevels; + + static final int SPAN_LEVEL = 1; + static final int TRACE_LEVEL = 2; + IdStore(IdStore parent) { - this.inheritedMask = parent == null ? 0L : parent.occupancy | parent.inheritedMask; + if (parent == null) { + this.inheritedMask = 0L; + this.inheritedLevels = 0; + } else { + this.inheritedMask = parent.occupancy | parent.inheritedMask; + this.inheritedLevels = (byte) (parent.levels | parent.inheritedLevels); + } } /** {@code tagId}'s slot bit. */ @@ -48,14 +69,31 @@ static long slotBit(long tagId) { return 1L << KnownTagCodec.slotOf(tagId); } - /** Whether the slot {@code bit} may hold a local entry or tombstone. */ - final boolean mayHaveLocal(long bit) { - return ((occupancy | tombstoneMask) & bit) != 0L; + /** {@code tagId}'s level: {@link #SPAN_LEVEL} or {@link #TRACE_LEVEL}, without a branch. */ + static int levelOf(long tagId) { + return 1 << (int) ((tagId & KnownTagCodec.LEVEL_TRACE) >>> 2); + } + + /** Whether the slot {@code bit} may hold a local entry or tombstone of {@code level}. */ + final boolean mayHaveLocal(long bit, int level) { + return (levels & level) != 0 && ((occupancy | tombstoneMask) & bit) != 0L; + } + + /** Whether an ancestor may hold a tag of {@code level} in the slot {@code bit}. */ + final boolean mayInherit(long bit, int level) { + return (inheritedLevels & level) != 0 && (inheritedMask & bit) != 0L; + } + + /** Records a live local entry in the slot {@code bit} of {@code level}. */ + final void markLocal(long bit, int level) { + this.occupancy |= bit; + this.levels |= (byte) level; } - /** Whether an ancestor may hold a tag in the slot {@code bit}. */ - final boolean mayInherit(long bit) { - return (inheritedMask & bit) != 0L; + /** Records a local tombstone in the slot {@code bit} of {@code level}. */ + final void markTombstone(long bit, int level) { + this.tombstoneMask |= bit; + this.levels |= (byte) level; } /** Throws for an id that names no known tag. */ diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 607fa60c9be..c6a38a8e44b 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -111,6 +111,26 @@ void tombstonesAreSharedPerTag() { assertEquals(KnownTags.HTTP_ROUTE_NAME, first.tag()); } + @Test + void aSpanLevelReadSkipsAParentHoldingOnlyTraceLevelTagsInItsSlot() { + // runtime-id (trace level) and http.method (span level) are colored into the same slot + assertEquals(IdStore.slotBit(KnownTags.RUNTIME_ID), IdStore.slotBit(KnownTags.HTTP_METHOD_ID)); + assertEquals(IdStore.TRACE_LEVEL, IdStore.levelOf(KnownTags.RUNTIME_ID)); + assertEquals(IdStore.SPAN_LEVEL, IdStore.levelOf(KnownTags.HTTP_METHOD_ID)); + + HashIdStore traceLevel = new HashIdStore(null); + traceLevel.set(KnownTags.RUNTIME_ID, "abc"); + HashIdStore span = new HashIdStore(traceLevel); + span.set(KnownTags.HTTP_ROUTE_ID, "/a"); + + long bit = IdStore.slotBit(KnownTags.HTTP_METHOD_ID); + assertFalse(span.mayInherit(bit, IdStore.levelOf(KnownTags.HTTP_METHOD_ID))); + assertTrue(span.mayInherit(bit, IdStore.levelOf(KnownTags.RUNTIME_ID))); + assertFalse(traceLevel.mayHaveLocal(bit, IdStore.SPAN_LEVEL)); + assertNull(span.get(KnownTags.HTTP_METHOD_ID)); + assertEquals("abc", span.getKnownValue(KnownTags.RUNTIME_ID)); + } + @Test void aClearSlotBitIsADefiniteMiss() { HashIdStore ids = new HashIdStore(null); From 030fc85ef26b1c9a0f43cdd44ad6cc7393ffde38 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 10:13:29 -0400 Subject: [PATCH 80/99] Address review: IdStore naming, EntryReader puts, member order - IdStore.get(long) -> getEntryReader, getKnownValue -> getValue. - put/getAndPut take an EntryReader: the hash store keeps its Entry (free when it is one), the dense store copies the value, so a span storing a flyweight read from a dense map builds no Entry. - HashIdStore's typed setters use the Entry factories. - Static members first: the Cursor type in IdStore, newEmpty and the cursors in the stores. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 17 ++- .../java/datadog/trace/api/DenseIdStore.java | 85 +++++++------- .../java/datadog/trace/api/HashIdStore.java | 110 +++++++++--------- .../main/java/datadog/trace/api/IdStore.java | 32 ++--- .../main/java/datadog/trace/api/TagMap.java | 4 +- .../java/datadog/trace/api/TagRouting.java | 2 +- .../java/datadog/trace/api/TagStoresTest.java | 18 +-- 7 files changed, 132 insertions(+), 136 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 2a463bc3a6e..02d9a60bc46 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 @@ -1111,7 +1111,7 @@ public String getSpanKindString() { } // UNSET or CUSTOM -- fall through to tag map synchronized (tagIds) { - TagMap.EntryReader kind = tagIds.get(KnownTags.SPAN_KIND_ID); + TagMap.EntryReader kind = tagIds.getEntryReader(KnownTags.SPAN_KIND_ID); return kind == null ? null : kind.stringValue(); } } @@ -1195,15 +1195,12 @@ private void storeTag(String tag, double value) { setResolved(TagRouting.keyOf(tag), tag, value); } - /** Stores {@code entry} as is, sharing its Entry; a reader with no entry is a no-op. */ + /** Stores {@code entry}, sharing its Entry where the store keeps Entry objects. */ private void putEntry(TagMap.EntryReader entry) { - TagMap.Entry e = entry.entry(); - if (e != null) { - if (e.tagId() != 0) { - tagIds.put(e); - } else { - customTagsForWrite().put(e); - } + if (entry.tagId() != 0) { + tagIds.put(entry); // no Entry built for a dense store, or for a flyweight from one + } else { + customTagsForWrite().put(entry.entry()); } } @@ -1614,7 +1611,7 @@ Object getTag(final String key) { public Object unsafeGetTag(final String tag) { long key = TagRouting.keyOf(tag); if (TagRouting.isKnown(key)) { - return tagIds.getKnownValue(key); + return tagIds.getValue(key); } HashStore custom = customTags; TagMap.Entry entry = custom == null ? null : custom.get(tag); diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index e3a472d940c..2ad9f17bf0f 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -1,6 +1,5 @@ package datadog.trace.api; -import datadog.trace.api.TagMap.Entry; import datadog.trace.api.TagMap.EntryReader; import datadog.trace.api.function.TriConsumer; import java.util.Arrays; @@ -16,9 +15,9 @@ * full id is rebuilt from the serial ({@link KnownTagCodec#idOf}) only where a reader asks for it. * Values are boxed: a primitive set by id is stored as its wrapper. * - *

Point reads that need only the value ({@link #getKnownValue}) build nothing. Those that return - * an {@link EntryReader} answer with this store's one reused flyweight, valid until the store's - * next operation (see {@link IdStore}). + *

Point reads that need only the value ({@link #getValue}) build nothing. Those that return an + * {@link EntryReader} answer with this store's one reused flyweight, valid until the store's next + * operation (see {@link IdStore}). * *

Removing a local tag swaps the last tag into its place. Removing an inherited tag leaves the * shared {@link #TOMBSTONE} as its value, in place, and sets its slot in the tombstone mask; a @@ -34,6 +33,35 @@ final class DenseIdStore extends IdStore { private static final char[] NO_SERIALS = new char[0]; private static final Object[] NO_VALUES = new Object[0]; + /** Walks the leaf's slots, then each ancestor's, skipping tombstones and shadowed tags. */ + private static final class DenseCursor extends Cursor { + private final DenseIdStore leaf; + private DenseIdStore level; + private int index = -1; + private final EntryReadingHelper reader = new EntryReadingHelper(); + + DenseCursor(DenseIdStore leaf) { + this.leaf = leaf; + this.level = leaf; + } + + @Override + EntryReader next() { + DenseIdStore level = this.level; + while (level != null) { + for (++this.index; this.index < level.used; ++this.index) { + if (this.leaf.readIfVisible(level, this.index, this.reader)) { + return this.reader; + } + } + level = level.parent; + this.level = level; + this.index = -1; + } + return null; + } + } + /** Each local tag's serial, then {@link #used} slots in; parallel to {@link #values}. */ private char[] serials = NO_SERIALS; @@ -57,12 +85,12 @@ final class DenseIdStore extends IdStore { } @Override - public EntryReader get(long tagId) { - return this.readerOf(tagId, this.getKnownValue(tagId)); + public EntryReader getEntryReader(long tagId) { + return this.readerOf(tagId, this.getValue(tagId)); } @Override - public Object getKnownValue(long tagId) { + public Object getValue(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } @@ -119,14 +147,14 @@ public void set(long tagId, double value) { } @Override - public EntryReader put(Entry knownEntry) { - long tagId = knownEntry.tagHash; - return this.readerOf(tagId, this.store(tagId, knownEntry.objectValue())); + public EntryReader put(EntryReader known) { + long tagId = known.tagId(); + return this.readerOf(tagId, this.store(tagId, known.objectValue())); } @Override - public EntryReader getAndPut(Entry knownEntry) { - long tagId = knownEntry.tagHash; + public EntryReader getAndPut(EntryReader known) { + long tagId = known.tagId(); char serial = (char) KnownTagCodec.serialNum(tagId); long bit = slotBit(tagId); int level = levelOf(tagId); @@ -139,10 +167,10 @@ public EntryReader getAndPut(Entry knownEntry) { this.markLocal(bit, level); this.count++; } - this.values[i] = knownEntry.objectValue(); + this.values[i] = known.objectValue(); } else { prior = this.mayInherit(bit, level) ? this.parent.getVisible(serial, bit, level) : null; - this.append(serial, bit, level, knownEntry.objectValue()); + this.append(serial, bit, level, known.objectValue()); } return this.readerOf(tagId, prior); } @@ -398,35 +426,6 @@ Cursor cursor() { return new DenseCursor(this); } - /** Walks the leaf's slots, then each ancestor's, skipping tombstones and shadowed tags. */ - private static final class DenseCursor extends Cursor { - private final DenseIdStore leaf; - private DenseIdStore level; - private int index = -1; - private final EntryReadingHelper reader = new EntryReadingHelper(); - - DenseCursor(DenseIdStore leaf) { - this.leaf = leaf; - this.level = leaf; - } - - @Override - EntryReader next() { - DenseIdStore level = this.level; - while (level != null) { - for (++this.index; this.index < level.used; ++this.index) { - if (this.leaf.readIfVisible(level, this.index, this.reader)) { - return this.reader; - } - } - level = level.parent; - this.level = level; - this.index = -1; - } - return null; - } - } - @Override IdStore copy() { DenseIdStore copy = new DenseIdStore(this.parent); diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index d1a592bceef..1a48d79c02e 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -19,6 +19,45 @@ * counted, iterated or returned. */ final class HashIdStore extends IdStore { + /** An empty parentless store over one bucket, for {@link TagMap#EMPTY}, which is frozen. */ + static HashIdStore newEmpty() { + return new HashIdStore(new Object[1]); + } + + /** Walks the leaf's buckets, then each ancestor's, skipping tombstones and shadowed entries. */ + private static final class HashCursor extends Cursor { + private final HashIdStore leaf; + private HashIdStore level; + private final HashSupport.BucketWalker walker = new HashSupport.BucketWalker(); + + HashCursor(HashIdStore leaf) { + this.leaf = leaf; + this.level = leaf; + this.walker.reset(leaf.buckets); + } + + @Override + EntryReader next() { + HashIdStore level = this.level; + while (level != null) { + Entry entry = this.walker.next(); + if (entry != null) { + if (!entry.tombstone + && (level == this.leaf || this.leaf.parentEntryVisible(entry, level))) { + return entry; + } + continue; + } + level = level.parent; + this.level = level; + if (level != null) { + this.walker.reset(level.buckets); + } + } + return null; + } + } + final Object[] buckets; /** Live local entries, not counting tombstones. */ @@ -33,11 +72,6 @@ final class HashIdStore extends IdStore { this.parent = (HashIdStore) parent; } - /** An empty parentless store over one bucket, for {@link TagMap#EMPTY}, which is frozen. */ - static HashIdStore newEmpty() { - return new HashIdStore(new Object[1]); - } - private HashIdStore(Object[] buckets) { super(null); this.buckets = buckets; @@ -45,14 +79,14 @@ private HashIdStore(Object[] buckets) { } @Override - public Entry get(long tagId) { + public Entry getEntryReader(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } return this.getVisible(tagId, slotBit(tagId), levelOf(tagId)); } - /** {@link #get} for a validated id and its slot bit and level. */ + /** {@link #getEntryReader} for a validated id and its slot bit and level. */ private Entry getVisible(long tagId, long bit, int level) { if (this.mayHaveLocal(bit, level)) { Entry local = HashSupport.findById(this.buckets, Entry.bucketHash(tagId), tagId); @@ -64,50 +98,49 @@ private Entry getVisible(long tagId, long bit, int level) { } @Override - public Object getKnownValue(long tagId) { - Entry entry = this.get(tagId); + public Object getValue(long tagId) { + Entry entry = this.getEntryReader(tagId); return entry == null ? null : entry.objectValue(); } @Override public void set(long tagId, Object value) { - this.put(new Entry(tagId, null, Entry.ANY, 0L, value)); + this.put(Entry.newAnyEntry(tagId, value)); } @Override public void set(long tagId, CharSequence value) { - this.put(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + this.put(Entry.newObjectEntry(tagId, value)); } @Override public void set(long tagId, boolean value) { - this.put( - new Entry( - tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + this.put(Entry.newBooleanEntry(tagId, value)); } @Override public void set(long tagId, int value) { - this.put(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + this.put(Entry.newIntEntry(tagId, value)); } @Override public void set(long tagId, long value) { - this.put(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + this.put(Entry.newLongEntry(tagId, value)); } @Override public void set(long tagId, float value) { - this.put(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + this.put(Entry.newFloatEntry(tagId, value)); } @Override public void set(long tagId, double value) { - this.put(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + this.put(Entry.newDoubleEntry(tagId, value)); } @Override - public Entry put(Entry knownEntry) { + public Entry put(EntryReader known) { + Entry knownEntry = known.entry(); Entry replaced = HashSupport.put(this.buckets, knownEntry); this.markLocal(slotBit(knownEntry.tagHash), levelOf(knownEntry.tagHash)); if (replaced == null || replaced.tombstone) { @@ -118,7 +151,8 @@ public Entry put(Entry knownEntry) { } @Override - public Entry getAndPut(Entry knownEntry) { + public Entry getAndPut(EntryReader known) { + Entry knownEntry = known.entry(); long tagId = knownEntry.tagHash; long bit = slotBit(tagId); int level = levelOf(tagId); @@ -289,40 +323,6 @@ Cursor cursor() { return new HashCursor(this); } - /** Walks the leaf's buckets, then each ancestor's, skipping tombstones and shadowed entries. */ - private static final class HashCursor extends Cursor { - private final HashIdStore leaf; - private HashIdStore level; - private final HashSupport.BucketWalker walker = new HashSupport.BucketWalker(); - - HashCursor(HashIdStore leaf) { - this.leaf = leaf; - this.level = leaf; - this.walker.reset(leaf.buckets); - } - - @Override - EntryReader next() { - HashIdStore level = this.level; - while (level != null) { - Entry entry = this.walker.next(); - if (entry != null) { - if (!entry.tombstone - && (level == this.leaf || this.leaf.parentEntryVisible(entry, level))) { - return entry; - } - continue; - } - level = level.parent; - this.level = level; - if (level != null) { - this.walker.reset(level.buckets); - } - } - return null; - } - } - @Override IdStore copy() { HashIdStore copy = new HashIdStore(this.parent); @@ -342,7 +342,7 @@ void putAllLocal(IdStore other) { } if (this.tombstoneMask != 0L) { // A bucket merge would count a replaced tombstone as a replacement, so put one at a time. - HashSupport.forEach(that.buckets, this, (self, entry) -> self.put(entry.entry())); + HashSupport.forEach(that.buckets, this, (self, entry) -> self.put(entry)); return; } if (this.count == 0) { diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index c922d217c87..a4431c56565 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -1,7 +1,6 @@ package datadog.trace.api; import datadog.apicontract.Restricted; -import datadog.trace.api.TagMap.Entry; import datadog.trace.api.TagMap.EntryReader; import datadog.trace.api.function.TriConsumer; import java.util.Map; @@ -33,6 +32,12 @@ */ @Restricted(allowedIn = {"datadog.trace.core"}) public abstract class IdStore { + /** Visits a store's visible entries one at a time. */ + abstract static class Cursor { + /** The next visible entry, or null when there are none left. */ + abstract EntryReader next(); + } + /** Slots that may hold a live local entry. */ long occupancy; @@ -113,13 +118,13 @@ static IllegalArgumentException notKnown(long tagId) { * * @throws IllegalArgumentException if {@code tagId} names no known tag */ - public abstract EntryReader get(long tagId); + public abstract EntryReader getEntryReader(long tagId); /** - * {@code get(tagId)}'s value, boxed if necessary, or null -- for a caller that needs only the - * value, so a store that keeps known tags without an Entry per tag can answer without one. + * {@link #getEntryReader}'s value, boxed if necessary, or null -- for a caller that needs only + * the value, so a store that keeps known tags without an Entry per tag can answer without one. */ - public abstract Object getKnownValue(long tagId); + public abstract Object getValue(long tagId); /* * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. @@ -140,13 +145,14 @@ static IllegalArgumentException notKnown(long tagId) { public abstract void set(long tagId, double value); /** - * Inserts or replaces a known tag's entry, returning the local entry it replaced (or null). Does - * not consult the read-through parent. + * Inserts or replaces a known tag, returning the local entry it replaced (or null). Does not + * consult the read-through parent. A store that keeps Entry objects keeps {@code known}'s Entry + * (free when it is one); a store that doesn't copies its value. */ - public abstract EntryReader put(Entry knownEntry); + public abstract EntryReader put(EntryReader known); - /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ - public abstract EntryReader getAndPut(Entry knownEntry); + /** {@link #put}, returning the prior visible entry -- local, else read through. */ + public abstract EntryReader getAndPut(EntryReader known); /** * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is @@ -188,12 +194,6 @@ abstract void forEach( */ abstract Cursor cursor(); - /** Visits a store's visible entries one at a time. */ - abstract static class Cursor { - /** The next visible entry, or null when there are none left. */ - abstract EntryReader next(); - } - /** An independently mutable copy with the same parent, sharing Entry objects. */ abstract IdStore copy(); 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 38c5b5271c6..9294b584c11 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1477,7 +1477,7 @@ public Entry getEntry(String tag) { /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ public Entry getEntry(long tagId) { - return TagRouting.entryOf(this.ids.get(tagId)); + return TagRouting.entryOf(this.ids.getEntryReader(tagId)); } @Deprecated @@ -1618,7 +1618,7 @@ public void set(@Nonnull String tag, double value) { /** {@code getEntry(tagId)}'s value, or null, for a caller that needs only the value. */ Object getKnownValue(long tagId) { - return this.ids.getKnownValue(tagId); + return this.ids.getValue(tagId); } public void set(long tagId, @Nonnull Object value) { diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index 77466e96734..ead5156b3a6 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -77,7 +77,7 @@ public static HashStore newCustomStore(HashStore borrowed) { /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ public static Entry get(IdStore ids, HashStore names, long key, String tag) { - return isKnown(key) ? entryOf(ids.get(key)) : names == null ? null : names.get(tag); + return isKnown(key) ? entryOf(ids.getEntryReader(key)) : names == null ? null : names.get(tag); } /** diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 33ae0084d20..9a01b96c6b5 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -124,30 +124,30 @@ void aSpanLevelReadSkipsAParentHoldingOnlyTraceLevelTagsInItsSlot() { assertFalse(span.mayInherit(bit, IdStore.levelOf(KnownTags.HTTP_METHOD_ID))); assertTrue(span.mayInherit(bit, IdStore.levelOf(KnownTags.RUNTIME_ID))); assertFalse(traceLevel.mayHaveLocal(bit, IdStore.SPAN_LEVEL)); - assertNull(span.get(KnownTags.HTTP_METHOD_ID)); - assertEquals("abc", span.getKnownValue(KnownTags.RUNTIME_ID)); + assertNull(span.getEntryReader(KnownTags.HTTP_METHOD_ID)); + assertEquals("abc", span.getValue(KnownTags.RUNTIME_ID)); } @Test void aClearSlotBitIsADefiniteMiss() { HashIdStore ids = new HashIdStore(null); assertEquals(0L, ids.occupancy); - assertNull(ids.get(KnownTags.HTTP_ROUTE_ID)); + assertNull(ids.getEntryReader(KnownTags.HTTP_ROUTE_ID)); ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); assertNotEquals(0L, ids.occupancy & IdStore.slotBit(KnownTags.HTTP_ROUTE_ID)); - assertEquals("/a", ids.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/a", ids.getValue(KnownTags.HTTP_ROUTE_ID)); // a child inherits its parent's slots, so a read-through miss can stop early HashIdStore child = new HashIdStore(ids); assertEquals(ids.occupancy, child.inheritedMask); - assertEquals("/a", child.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/a", child.getValue(KnownTags.HTTP_ROUTE_ID)); } @Test void anUnknownIdIsRejected() { HashIdStore ids = new HashIdStore(null); - assertThrows(IllegalArgumentException.class, () -> ids.get(0L)); + assertThrows(IllegalArgumentException.class, () -> ids.getEntryReader(0L)); assertThrows(IllegalArgumentException.class, () -> ids.remove(0L)); } @@ -163,7 +163,7 @@ void theIdStoreSetsEveryValueShape() { ids.set(KnownTags.DB_INSTANCE_ID, 2.25d); TagMap checked = TagMap.create(); - checked.set(KnownTags.COMPONENT_ID, ids.get(KnownTags.COMPONENT_ID).objectValue()); + checked.set(KnownTags.COMPONENT_ID, ids.getEntryReader(KnownTags.COMPONENT_ID).objectValue()); checked.set(KnownTags.HTTP_ROUTE_ID, "/a"); checked.set(KnownTags.DD_PROFILING_ENABLED_ID, true); checked.set(KnownTags.HTTP_STATUS_CODE_ID, 200); @@ -258,7 +258,7 @@ void anOwnerReadsThroughItsParentsStores() { IdStore ids = TagRouting.newIdStore(parent); HashStore borrowed = TagRouting.inheritedCustomStore(parent); assertSame(parent.names, borrowed); - assertEquals("/inherited", ids.getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/inherited", ids.getValue(KnownTags.HTTP_ROUTE_ID)); assertEquals("c", borrowed.get("inherited.custom").stringValue()); HashStore own = TagRouting.newCustomStore(borrowed); @@ -296,7 +296,7 @@ void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { view.set(KnownTags.HTTP_METHOD_NAME, "GET"); view.set("own", "o"); - assertEquals("GET", ids.getKnownValue(KnownTags.HTTP_METHOD_ID)); + assertEquals("GET", ids.getValue(KnownTags.HTTP_METHOD_ID)); assertFalse(view.isCustomStoreBorrowed()); assertNotSame(borrowed, view.customStore()); assertEquals("o", view.customStore().get("own").stringValue()); From 6bcd11fce073a2a91dd72eb8aaf6984292928fc2 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 10:40:23 -0400 Subject: [PATCH 81/99] Clarify comments touched by the store split Prose only. Brings comments in line with the code after the #12731 merge and the dense port: no direction-scoped aliases or lookup sentinels in the core, put/getAndPut take an EntryReader, only the hash id store keeps Entry tombstones, the dense store keeps no Entry objects, and the root-span tags' floor precedence covers header tags and prototype defaults too. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/GetTagBenchmark.java | 5 +++-- .../java/datadog/trace/core/CoreTracer.java | 15 ++++++++------- .../java/datadog/trace/core/DDSpanContext.java | 4 +++- .../java/datadog/trace/api/DenseIdStore.java | 14 +++++++------- .../java/datadog/trace/api/HashIdStore.java | 10 +++++----- .../main/java/datadog/trace/api/HashStore.java | 8 ++++---- .../java/datadog/trace/api/HashSupport.java | 13 +++++-------- .../main/java/datadog/trace/api/IdStore.java | 17 +++++++++-------- .../java/datadog/trace/api/KnownTagCodec.java | 12 ++++++------ .../src/main/java/datadog/trace/api/TagMap.java | 9 +++++---- .../main/java/datadog/trace/api/TagRouting.java | 6 +++--- .../java/datadog/trace/api/TagStoresTest.java | 6 +++--- 12 files changed, 61 insertions(+), 58 deletions(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java index 20f2286eff0..57ce57b404f 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java @@ -17,8 +17,9 @@ /** * Measures reading tags back from a live span, by how the tag is named: a known tag's name, a - * custom name, a direction-scoped alias, and a miss. {@code getTag} takes the span's tag lock; - * {@code unsafeGetTag} reads without it, as the tracer's internal readers do. + * custom name, an OpenTelemetry name ({@code server.address}, which the core treats as a custom + * name), and misses. {@code getTag} takes the span's tag lock; {@code unsafeGetTag} reads without + * it, as the tracer's internal readers do. */ @State(Scope.Thread) @Warmup(iterations = 3, time = 1) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index cdd3165d221..94baae88d4e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -219,10 +219,10 @@ private static final class LocalRootSpanTags { /** * All of a root span's trace-level tags in one frozen map -- these tags over {@code - * mergedTracerTags} -- for the root span to read through instead of copying them in. Keeps the - * span's own store to span-level tags, which the tag registry's slot coloring assumes: it lets - * a span-level and a trace-level tag share a slot bit. Built once per config snapshot (a racy - * rebuild is harmless: the result is the same). + * mergedTracerTags} -- for the root span to read through instead of copying them in. The tag + * registry colors trace-level tags separately, so they reuse span-level slot bits; keeping them + * out of the span's own store keeps its slot bits exact. Built once per config snapshot; a racy + * rebuild is harmless, since it builds the same map. */ TagMap traceLevelParent(final TagMap mergedTracerTags) { TraceLevelTags cached = this.traceLevel; @@ -2357,9 +2357,10 @@ protected static final DDSpanContext buildSpanContext( boolean mergedTracerTagsNeedsIntercept = traceConfig.mergedTracerTagsNeedsIntercept; // With builder tags taking precedence, a root span reads its trace-level tags (root-span tags - // over the merged tracer tags) through one shared frozen parent rather than copying the - // root-span tags in. They become the precedence floor -- the order already puts them below - // the builder and contextual tags. Tags needing interception are still copied in. + // over the merged tracer tags) through one shared frozen parent instead of copying the + // root-span tags in. They become the precedence floor: the flag's order already puts them + // below builder and contextual tags, and now inbound header tags and prototype defaults also + // win over them. Tags needing interception are still copied in. final boolean rootSpanTagsReadThrough = localRootSpanTags != null && tracer.builderTagsPrecedence 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 02d9a60bc46..662e390f199 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 @@ -1012,7 +1012,9 @@ private void removeStored(String tag) { } } - /** Copies {@code map}'s tags into the stores, sharing its Entry objects. */ + /** + * Copies {@code map}'s tags into the stores, sharing its Entry objects where a store keeps them. + */ private void putAllTags(TagMap map) { TagMap view = tagView(); view.putAll(map); diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index 2ad9f17bf0f..437c198d99f 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -11,17 +11,17 @@ * The dense {@link IdStore}: known tags in two parallel arrays -- each tag's 16-bit serial and its * value -- with no Entry per tag, reading through to the frozen parent's store. * - *

A lookup is a linear scan of the serials, skipped when the slot masks rule the tag out. The - * full id is rebuilt from the serial ({@link KnownTagCodec#idOf}) only where a reader asks for it. - * Values are boxed: a primitive set by id is stored as its wrapper. + *

A lookup is a linear scan of the serials, skipped when the slot and level masks rule the tag + * out. The full id is rebuilt from the serial ({@link KnownTagCodec#idOf}) only where a reader asks + * for it. Values are boxed: a primitive set by id is stored as its wrapper. * *

Point reads that need only the value ({@link #getValue}) build nothing. Those that return an * {@link EntryReader} answer with this store's one reused flyweight, valid until the store's next * operation (see {@link IdStore}). * - *

Removing a local tag swaps the last tag into its place. Removing an inherited tag leaves the - * shared {@link #TOMBSTONE} as its value, in place, and sets its slot in the tombstone mask; a - * tombstone is never counted, iterated or returned. + *

Removing a tag that hides nothing inherited swaps the last slot into its place. Removing a tag + * the parent chain also holds stores the shared {@link #TOMBSTONE} as its value instead, and sets + * its slot in the tombstone mask. A tombstone is never counted, iterated or returned. */ final class DenseIdStore extends IdStore { /** A value hiding an inherited tag. Never escapes the store. */ @@ -62,7 +62,7 @@ EntryReader next() { } } - /** Each local tag's serial, then {@link #used} slots in; parallel to {@link #values}. */ + /** The serial of each local tag or tombstone, in the first {@link #used} slots. */ private char[] serials = NO_SERIALS; private Object[] values = NO_VALUES; diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 1a48d79c02e..aca76245082 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -12,11 +12,11 @@ * The hash {@link IdStore}: known tags in their own bucket array ({@link HashSupport}), keyed by * id, reading through to the frozen parent's id store. * - *

A read checks the slot masks before probing: a clear local bit skips the buckets, and a clear - * inherited bit skips the read-through. Removing an inherited tag puts that tag's shared tombstone - * ({@link Entry#tombstoneOf}) in the local buckets, so it allocates nothing, and sets its slot in - * the tombstone mask; the bucket entry is the exact state, the mask the hint. A tombstone is never - * counted, iterated or returned. + *

A read checks the slot and level masks ({@link IdStore}) before probing: a clear local bit + * skips the buckets, and a clear inherited bit skips the read-through. Removing an inherited tag + * puts that tag's shared tombstone ({@link Entry#tombstoneOf}) in the local buckets, so it + * allocates nothing, and sets its slot in the tombstone mask; the bucket entry is the exact state, + * the mask the hint. A tombstone is never counted, iterated or returned. */ final class HashIdStore extends IdStore { /** An empty parentless store over one bucket, for {@link TagMap#EMPTY}, which is frozen. */ diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 718917a4536..8a6b6ea1e96 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -32,9 +32,9 @@ public final class HashStore { final HashStore parent; /** - * Parent keys removed locally (read-through tombstones). Lazily allocated on the first such - * removal; {@code null} both means "no tombstones" and serves as the gate that keeps the hot - * paths untouched. A tombstone stops read-through for its key. + * Parent keys removed locally (read-through tombstones): a tombstone stops read-through for its + * key. Allocated on the first such removal, so while it is {@code null} the hot paths skip the + * tombstone checks. */ private Set removedFromParent; @@ -159,7 +159,7 @@ public Entry put(Entry newEntry) { return replaced; } - /** {@link #put(Entry)}, returning the prior visible entry -- local, else read through. */ + /** {@link #put}, returning the prior visible entry -- local, else read through. */ public Entry getAndPut(Entry newEntry) { // Capture whether the key was tombstoned BEFORE put clears it: a tombstoned key had no visible // prior value (it was removed), so getAndPut must report null rather than the parent's. diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java index b9e28a8391b..e02359314e9 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashSupport.java +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -34,12 +34,8 @@ static Entry find(Object[] buckets, int hash, String tag) { } /** - * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). - */ - /** - * {@link #find} for a known tag, matching on its id rather than its name: an id store's entries - * are all keyed by id, so it never needs a name to look one up. {@code hash} is {@code - * Entry.bucketHash(tagId)}. + * {@link #find} for a known tag, matching on its id rather than its name, so an id store never + * needs a name. {@code hash} is {@code Entry.bucketHash(tagId)}. */ static Entry findById(Object[] buckets, int hash, long tagId) { Object bucket = buckets[hash & (buckets.length - 1)]; @@ -52,6 +48,7 @@ static Entry findById(Object[] buckets, int hash, long tagId) { return null; } + /** Finds an entry by hash and tag in one bucket: an Entry, a BucketGroup chain, or null. */ static Entry findInBucket(Object bucket, int hash, String tag) { if (bucket instanceof Entry) { Entry tagEntry = (Entry) bucket; @@ -173,7 +170,7 @@ static void copyInto(Object[] thisBuckets, Object[] thatBuckets) { static int merge(Object[] thisBuckets, Object[] thatBuckets) { int added = 0; - // Since TagMap-s don't support expansion, buckets are perfectly aligned + // Stores don't expand, so buckets are perfectly aligned // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check // elimination for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { @@ -230,7 +227,7 @@ static int merge(Object[] thisBuckets, Object[] thatBuckets) { Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); if (incomingEntry != null) { // there's already an entry w/ the same tag from the incoming TagMap - // incoming entry clobbers the existing try, so we're done + // incoming entry clobbers the existing entry, so we're done thisBuckets[i] = thisNewGroup; // overlapping group - subtract one for clobbered existing entry diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index a4431c56565..7a1840763ea 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -8,16 +8,17 @@ import java.util.function.Consumer; /** - * Known tags keyed by their {@link KnownTags} id. Every method takes an id, or an Entry already - * built from one -- never a name, so nothing here can be handed a name that a caller has not yet - * resolved. + * Known tags keyed by their {@link KnownTags} id. Every method takes an id, or an {@link + * EntryReader} for a known tag -- never a name, so nothing here can be handed a name that a caller + * has not yet resolved. * *

A span in the tracer core owns its id store directly ({@link TagRouting#newIdStore}); a {@link * TagMap} owns one too. * - *

Each implementation is its own class -- {@link HashIdStore} or {@link DenseIdStore}, one per - * JVM, chosen by {@link IdStoreFactory} -- and nothing that is always loaded may extend this class, - * so a call through it stays monomorphic. No locking: guarded by the owner's lock. + *

{@link IdStoreFactory} loads one implementation per JVM, {@link HashIdStore} or {@link + * DenseIdStore}. With one subclass loaded, class hierarchy analysis devirtualizes every call on an + * {@code IdStore} without a type guard, so nothing that is always loaded may extend this class. No + * locking: guarded by the owner's lock. * *

The base keeps the slot masks an implementation shares: a tag's slot ({@link * KnownTagCodec#slotOf}) maps it to one bit of a {@code long}. A bit is a may-be-present hint -- @@ -194,10 +195,10 @@ abstract void forEach( */ abstract Cursor cursor(); - /** An independently mutable copy with the same parent, sharing Entry objects. */ + /** An independently mutable copy with the same parent, sharing any Entry objects it holds. */ abstract IdStore copy(); - /** Copies the entries of the parentless store {@code that}, sharing Entry objects. */ + /** Copies the entries of the parentless store {@code that}, sharing any Entry objects. */ abstract void putAllLocal(IdStore that); /** Puts the local entries into {@code map}, for a store with no parent. */ 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 09d3d9aff5b..35551b2bc66 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -14,12 +14,12 @@ public final class KnownTagCodec { /* * tagId bit layout: [63-48 serialNum (16 bits, 15 used: bit 63 is kept clear)] [47-32 slot] - * [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] hold the tag's slot ({@link - * #slotOf}), assigned by graph coloring so tags on one span type never share a slot, or {@link - * #NO_SLOT} for a tag on no modeled span type. Of the low 32 flag bits, bit 2 is the trace/span + * [31-0 flags]. The generator keeps bit 63 clear, so every id is positive and a negative value + * can never be an id. 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] hold the tag's slot ({@link #slotOf}), or {@link #NO_SLOT} for a tag on + * no modeled span type. Graph coloring gives tags that appear together on a span type distinct + * slots; trace-level tags are colored separately and reuse the span-level slots. Of the low 32 flag bits, bit 2 is 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. * 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 9294b584c11..6bb76cf7093 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -446,8 +446,9 @@ static Entry newDoubleEntry(String tag, Double box) { volatile String strCache = null; /** - * True for a known tag's tombstone: the marker an id store puts in its own buckets to hide an - * inherited entry it has removed. Never visible outside the store; see {@link #tombstoneOf}. + * True for a known tag's tombstone: the marker the hash id store puts in its own buckets to + * hide an inherited entry it has removed. Never visible outside the store; see {@link + * #tombstoneOf}. */ final boolean tombstone; @@ -1204,8 +1205,8 @@ public EntryChange next() { } /* - * A TagMap holds its tags in two stores, each with its own bucket array: an IdStore with the - * known tags by id, and a HashStore with the custom tags by name. Their key spaces are disjoint, + * A TagMap holds its tags in two stores: an IdStore with the known tags by id, and a HashStore + * with the custom tags by name. Their key spaces are disjoint, * so each store reads through to its own parent chain and keeps its own tombstones, and * precedence never crosses stores. * diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java index ead5156b3a6..cbed77c9203 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ b/internal-api/src/main/java/datadog/trace/api/TagRouting.java @@ -81,9 +81,9 @@ public static Entry get(IdStore ids, HashStore names, long key, String tag) { } /** - * The Entry an id store's {@code reader} denotes, for a caller that hands it out of the trust - * boundary or keeps it: free for a hash store's reader, which is its Entry; a store reading - * through a reused flyweight builds one here. + * The Entry an id store's {@code reader} denotes, or null, for a caller that keeps it or hands it + * out of the trust boundary. Free for a hash store's reader, which is an Entry; for a dense + * store's reused flyweight, this builds one. */ public static Entry entryOf(TagMap.EntryReader reader) { return reader == null ? null : reader.entry(); diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 9a01b96c6b5..5c05a7a7da0 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -15,9 +15,9 @@ import org.junit.jupiter.api.Test; /** - * A TagMap's two stores -- {@link HashIdStore} for known tags, {@link HashStore} for custom tags, - * created lazily -- the {@link TagRouting} over them, and the span's TagMap ({@link - * TagMap#forSpan}). + * A TagMap's two stores -- an {@link IdStore} for known tags ({@link HashIdStore} unless dense mode + * is on), a {@link HashStore} for custom tags, created lazily -- the {@link TagRouting} over them, + * and the span's view ({@link TagMap#viewOf}). */ class TagStoresTest { private static TagMap frozenParent() { From 4e6ead4f9035070aafe631456c996f0597847fe0 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 10:40:55 -0400 Subject: [PATCH 82/99] Rename newEmpty to createEmpty Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/HashIdStore.java | 2 +- .../main/java/datadog/trace/api/IdStoreFactory.java | 10 +++++----- .../src/main/java/datadog/trace/api/TagMap.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index aca76245082..f8c83b3372d 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -20,7 +20,7 @@ */ final class HashIdStore extends IdStore { /** An empty parentless store over one bucket, for {@link TagMap#EMPTY}, which is frozen. */ - static HashIdStore newEmpty() { + static HashIdStore createEmpty() { return new HashIdStore(new Object[1]); } diff --git a/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java b/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java index 906ee7ddd69..765906c62ec 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java @@ -11,7 +11,7 @@ * call on an {@code IdStore} without a guard. A store builds its own copies. * *

No static state here, and no {@link TagMap}: {@link TagMap#EMPTY} is built with {@link - * #newEmpty}, so class initialization works whichever of the two starts first. + * #createEmpty}, so class initialization works whichever of the two starts first. */ abstract class IdStoreFactory { /** The JVM's store, chosen once by the dense-store flag. */ @@ -26,7 +26,7 @@ static final class Default { abstract IdStore create(IdStore parent); /** A new empty parentless store, for a frozen map. */ - abstract IdStore newEmpty(); + abstract IdStore createEmpty(); } final class HashIdStoreFactory extends IdStoreFactory { @@ -40,8 +40,8 @@ IdStore create(IdStore parent) { } @Override - IdStore newEmpty() { - return HashIdStore.newEmpty(); + IdStore createEmpty() { + return HashIdStore.createEmpty(); } } @@ -56,7 +56,7 @@ IdStore create(IdStore parent) { } @Override - IdStore newEmpty() { + IdStore createEmpty() { return new DenseIdStore(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 6bb76cf7093..8e1e8bdadb6 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -51,7 +51,7 @@ public final class TagMap implements Map, Iterable. - public static final TagMap EMPTY = new TagMap(IdStoreFactory.Default.INSTANCE.newEmpty()); + public static final TagMap EMPTY = new TagMap(IdStoreFactory.Default.INSTANCE.createEmpty()); // Sentinel for an ID not yet resolved by the reused EntryReadingHelper. Zero denotes an unknown // tag, so it cannot represent "not computed". From 4d771178d6a16f3838c48baf382d2e979e7c1cf2 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 10:44:59 -0400 Subject: [PATCH 83/99] Fold TagRouting into its users TagRouting was left holding small helpers once direction handling moved to the OpenTelemetry boundary. The span and the TagMap view each route by name or id themselves (KnownTagCodec.keyOf, then the id store or the custom store), and the pieces core can't build itself, because they reach TagMap's package-private stores, become @Restricted statics on the types they belong to: IdStore.create, HashStore.inheritedFrom / create and TagMap.readThroughParent. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 62 +++++----- .../forbiddenApiFilters/instrumentation.txt | 4 +- .../java/datadog/trace/api/HashStore.java | 20 +++- .../main/java/datadog/trace/api/IdStore.java | 9 +- .../main/java/datadog/trace/api/TagMap.java | 113 ++++++++++-------- .../java/datadog/trace/api/TagRouting.java | 91 -------------- .../java/datadog/trace/api/TagStoresTest.java | 44 ++++--- 7 files changed, 144 insertions(+), 199 deletions(-) delete mode 100644 internal-api/src/main/java/datadog/trace/api/TagRouting.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 662e390f199..ab91016b7ce 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 @@ -21,7 +21,6 @@ import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; import datadog.trace.api.TagMap; -import datadog.trace.api.TagRouting; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; import datadog.trace.api.config.TracerConfig; @@ -47,7 +46,6 @@ 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; @@ -153,9 +151,9 @@ public class DDSpanContext *

The span owns its tag stores directly: {@code tagIds}, the known tags by id, created with * the span; and {@code customTags}, the custom tags by name, created on the first custom write -- * until then null, or the read-through parent's custom store, borrowed read-only while {@code - * customTagsBorrowed}. Every read and write goes span to store, routed by {@link TagRouting}, - * under one lock that guards both: {@code tagIds}, which always exists. Internal readers get a - * cached {@link #tagView() view}. + * customTagsBorrowed}. Every read and write goes span to store, routed by name or id, under one + * lock that guards both: {@code tagIds}, which always exists. Internal readers get a cached + * {@link #tagView() view}. */ private final IdStore tagIds; @@ -433,9 +431,9 @@ public DDSpanContext( this.pathwayContext = pathwayContext; // Reads through to the frozen tracer-level tags, if any. - TagMap parentTags = TagRouting.readThroughParent(readThroughParent); - this.tagIds = TagRouting.newIdStore(parentTags); - this.customTags = TagRouting.inheritedCustomStore(parentTags); + TagMap parentTags = TagMap.readThroughParent(readThroughParent); + this.tagIds = IdStore.create(parentTags); + this.customTags = HashStore.inheritedFrom(parentTags); this.customTagsBorrowed = this.customTags != null; // must set this before setting the service and resource names below @@ -972,13 +970,10 @@ private void adoptViewStores(TagMap view) { } /** The custom store to write to: created on the first custom write, ending any borrowing. */ - @SuppressFBWarnings( - value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", - justification = "Written and read only under the tagIds lock") private HashStore customTagsForWrite() { HashStore custom = this.customTags; if (custom == null || this.customTagsBorrowed) { - custom = TagRouting.newCustomStore(custom); // reads through to the store borrowed till now + custom = HashStore.create(custom); // reads through to the store borrowed till now this.customTags = custom; this.customTagsBorrowed = false; } @@ -1004,8 +999,8 @@ private void removeCustomTag(String name) { /** Removes {@code tag}, known or custom. Call under the {@code tagIds} lock. */ private void removeStored(String tag) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { tagIds.remove(key); } else { removeCustomTag(tag); @@ -1026,7 +1021,7 @@ private void putAllTags(TagMap map) { */ private void setResolved(long key, String tag, Object value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1034,7 +1029,7 @@ private void setResolved(long key, String tag, Object value) { } private void setResolved(long key, String tag, CharSequence value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1042,7 +1037,7 @@ private void setResolved(long key, String tag, CharSequence value) { } private void setResolved(long key, String tag, boolean value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1050,7 +1045,7 @@ private void setResolved(long key, String tag, boolean value) { } private void setResolved(long key, String tag, int value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1058,7 +1053,7 @@ private void setResolved(long key, String tag, int value) { } private void setResolved(long key, String tag, long value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1066,7 +1061,7 @@ private void setResolved(long key, String tag, long value) { } private void setResolved(long key, String tag, float value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1074,7 +1069,7 @@ private void setResolved(long key, String tag, float value) { } private void setResolved(long key, String tag, double value) { - if (TagRouting.isKnown(key)) { + if (key != 0) { tagIds.set(key, value); } else { customTagsForWrite().set(tag, value); @@ -1170,31 +1165,31 @@ public void setMetric(final TagMap.EntryReader entry) { */ private void storeTag(String tag, Object value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } private void storeTag(String tag, CharSequence value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } private void storeTag(String tag, boolean value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } private void storeTag(String tag, int value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } private void storeTag(String tag, long value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } private void storeTag(String tag, float value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } private void storeTag(String tag, double value) { - setResolved(TagRouting.keyOf(tag), tag, value); + setResolved(KnownTagCodec.keyOf(tag), tag, value); } /** Stores {@code entry}, sharing its Entry where the store keeps Entry objects. */ @@ -1249,7 +1244,12 @@ public void removeTag(long tagId) { /** Whether {@code entry}'s tag is already on this span. */ private boolean isPresent(TagMap.EntryReader entry) { String tag = entry.tag(); - return TagRouting.get(tagIds, customTags, TagRouting.keyOf(tag), tag) != null; + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return tagIds.getValue(key) != null; + } + HashStore custom = customTags; + return custom != null && custom.get(tag) != null; } public void setTag(final String tag, final Object value) { @@ -1611,8 +1611,8 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { return tagIds.getValue(key); } HashStore custom = customTags; diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 232fe209d77..9f649083f16 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -3,13 +3,13 @@ @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) -# A span's tag stores, the routing over them and the span's view of them skip checks the tracer +# A span's tag stores, how it gets them from a parent, and the span's view of them skip checks the tracer # core has already made; they are @Restricted to the core (see datadog.apicontract.Restricted). # Instrumentation uses TagMap's checked methods. @defaultMessage Tag stores are restricted to the tracer core; use TagMap's checked methods instead. datadog.trace.api.HashStore datadog.trace.api.IdStore -datadog.trace.api.TagRouting +datadog.trace.api.TagMap#readThroughParent(datadog.trace.api.TagMap) datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,boolean) datadog.trace.api.TagMap#attach(datadog.trace.api.HashStore,boolean) datadog.trace.api.TagMap#customStore() diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 8a6b6ea1e96..6e309efa921 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -18,8 +18,8 @@ * IdStore}. * *

Created lazily by its owner -- a {@link TagMap}, or a span in the tracer core -- on the first - * custom write ({@link TagRouting#newCustomStore}); until then the owner borrows its parent's - * custom store, if any, read-only. No locking: guarded by the owner's lock. + * custom write ({@link #create}); until then the owner borrows its parent's custom store, if any, + * read-only. No locking: guarded by the owner's lock. */ @Restricted(allowedIn = {"datadog.trace.core"}) public final class HashStore { @@ -39,6 +39,22 @@ public final class HashStore { private Set removedFromParent; /** A fresh, empty store reading through to {@code parent} (may be null), which is frozen. */ + /** + * {@code parent}'s custom store, for an owner to borrow read-only until its first custom write, + * or null. + */ + public static HashStore inheritedFrom(TagMap parent) { + return parent == null ? null : parent.names; + } + + /** + * A fresh custom store for an owner's first custom write, reading through to {@code borrowed}, + * the store it borrowed until now (may be null). + */ + public static HashStore create(HashStore borrowed) { + return new HashStore(borrowed); + } + HashStore(HashStore parent) { this.buckets = HashSupport.newBuckets(); this.parent = parent; diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 7a1840763ea..5c1404086dd 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -12,8 +12,8 @@ * EntryReader} for a known tag -- never a name, so nothing here can be handed a name that a caller * has not yet resolved. * - *

A span in the tracer core owns its id store directly ({@link TagRouting#newIdStore}); a {@link - * TagMap} owns one too. + *

A span in the tracer core owns its id store directly ({@link #create}); a {@link TagMap} owns + * one too. * *

{@link IdStoreFactory} loads one implementation per JVM, {@link HashIdStore} or {@link * DenseIdStore}. With one subclass loaded, class hierarchy analysis devirtualizes every call on an @@ -70,6 +70,11 @@ abstract static class Cursor { } } + /** A fresh id store reading through to {@code parent}'s known tags (parent may be null). */ + public static IdStore create(TagMap parent) { + return IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.ids); + } + /** {@code tagId}'s slot bit. */ static long slotBit(long tagId) { return 1L << KnownTagCodec.slotOf(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 8e1e8bdadb6..632c53a8f9e 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1288,6 +1288,32 @@ public static TagMap viewOf(IdStore ids, HashStore names, boolean namesBorrowed) return new TagMap(null, ids, names, namesBorrowed); } + /** + * The parent an owner reads through to: {@code parent}, or null when it is null or empty, as + * {@link #createFromParent} decides. + * + * @throws IllegalStateException if {@code parent} is not frozen + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public static TagMap readThroughParent(TagMap parent) { + if (parent == null) { + return null; + } + if (!parent.isFrozen()) { + throw new IllegalStateException("read-through parent must be frozen"); + } + return parent.isEmpty() ? null : parent; + } + + /** + * The Entry an id store's {@code reader} denotes, or null, for a caller that keeps it or hands it + * out. Free for a hash store's reader, which is an Entry; for a dense store's reused flyweight, + * this builds one. + */ + private static Entry entryOf(EntryReader reader) { + return reader == null ? null : reader.entry(); + } + /** Points this view at its span's current custom store; see {@link #viewOf}. */ @Restricted(allowedIn = {"datadog.trace.core"}) public void attach(HashStore names, boolean namesBorrowed) { @@ -1473,12 +1499,17 @@ public Set> entrySet() { public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. - return TagRouting.get(this.ids, this.names, TagRouting.keyOf(tag), tag); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return entryOf(this.ids.getEntryReader(key)); + } + HashStore names = this.names; + return names == null ? null : names.get(tag); } /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ public Entry getEntry(long tagId) { - return TagRouting.entryOf(this.ids.getEntryReader(tagId)); + return entryOf(this.ids.getEntryReader(tagId)); } @Deprecated @@ -1504,53 +1535,43 @@ 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.) */ static Entry anyEntryFor(String tag, Object value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) ? Entry.newAnyEntry(key, value) : Entry.newAnyEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newAnyEntry(key, value) : Entry.newAnyEntry(tag, value); } static Entry objectEntryFor(String tag, CharSequence value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) - ? Entry.newObjectEntry(key, value) - : Entry.newObjectEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newObjectEntry(key, value) : Entry.newObjectEntry(tag, value); } static Entry booleanEntryFor(String tag, boolean value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) - ? Entry.newBooleanEntry(key, value) - : Entry.newBooleanEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newBooleanEntry(key, value) : Entry.newBooleanEntry(tag, value); } static Entry intEntryFor(String tag, int value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) ? Entry.newIntEntry(key, value) : Entry.newIntEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newIntEntry(key, value) : Entry.newIntEntry(tag, value); } static Entry longEntryFor(String tag, long value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) - ? Entry.newLongEntry(key, value) - : Entry.newLongEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newLongEntry(key, value) : Entry.newLongEntry(tag, value); } static Entry floatEntryFor(String tag, float value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) - ? Entry.newFloatEntry(key, value) - : Entry.newFloatEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newFloatEntry(key, value) : Entry.newFloatEntry(tag, value); } static Entry doubleEntryFor(String tag, double value) { - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) - ? Entry.newDoubleEntry(key, value) - : Entry.newDoubleEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newDoubleEntry(key, value) : Entry.newDoubleEntry(tag, value); } public void set(@Nonnull String tag, @Nonnull Object value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newAnyEntry(key, value)); } else { this.putCustom(Entry.newAnyEntry(tag, value)); @@ -1558,8 +1579,8 @@ public void set(@Nonnull String tag, @Nonnull Object value) { } public void set(@Nonnull String tag, @Nonnull CharSequence value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newObjectEntry(key, value)); } else { this.putCustom(Entry.newObjectEntry(tag, value)); @@ -1567,8 +1588,8 @@ public void set(@Nonnull String tag, @Nonnull CharSequence value) { } public void set(@Nonnull String tag, boolean value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newBooleanEntry(key, value)); } else { this.putCustom(Entry.newBooleanEntry(tag, value)); @@ -1576,8 +1597,8 @@ public void set(@Nonnull String tag, boolean value) { } public void set(@Nonnull String tag, int value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newIntEntry(key, value)); } else { this.putCustom(Entry.newIntEntry(tag, value)); @@ -1585,8 +1606,8 @@ public void set(@Nonnull String tag, int value) { } public void set(@Nonnull String tag, long value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newLongEntry(key, value)); } else { this.putCustom(Entry.newLongEntry(tag, value)); @@ -1594,8 +1615,8 @@ public void set(@Nonnull String tag, long value) { } public void set(@Nonnull String tag, float value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newFloatEntry(key, value)); } else { this.putCustom(Entry.newFloatEntry(tag, value)); @@ -1603,8 +1624,8 @@ public void set(@Nonnull String tag, float value) { } public void set(@Nonnull String tag, double value) { - long key = TagRouting.keyOf(tag); - if (TagRouting.isKnown(key)) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { this.putKnown(Entry.newDoubleEntry(key, value)); } else { this.putCustom(Entry.newDoubleEntry(tag, value)); @@ -1663,7 +1684,7 @@ public Entry getAndSet(@Nullable Entry newEntry) { this.checkWriteAccess(); return newEntry.tagId() != 0 - ? TagRouting.entryOf(this.ids.getAndPut(newEntry)) + ? entryOf(this.ids.getAndPut(newEntry)) : this.namesForWrite().getAndPut(newEntry); } @@ -1677,7 +1698,7 @@ Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); return newEntry.tagId() != 0 - ? TagRouting.entryOf(this.ids.put(newEntry)) + ? entryOf(this.ids.put(newEntry)) : this.namesForWrite().put(newEntry); } @@ -1812,17 +1833,15 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. - long key = TagRouting.keyOf(tag); - return TagRouting.isKnown(key) - ? TagRouting.entryOf(this.ids.remove(key)) - : this.removeCustom(tag); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? entryOf(this.ids.remove(key)) : this.removeCustom(tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ public Entry getAndRemove(long tagId) { this.checkWriteAccess(); - return TagRouting.entryOf(this.ids.remove(tagId)); + return entryOf(this.ids.remove(tagId)); } public TagMap copy() { diff --git a/internal-api/src/main/java/datadog/trace/api/TagRouting.java b/internal-api/src/main/java/datadog/trace/api/TagRouting.java deleted file mode 100644 index cbed77c9203..00000000000 --- a/internal-api/src/main/java/datadog/trace/api/TagRouting.java +++ /dev/null @@ -1,91 +0,0 @@ -package datadog.trace.api; - -import datadog.apicontract.Restricted; -import datadog.trace.api.TagMap.Entry; - -/** - * Where a tag name is resolved and a read is routed to the store that holds it: a known tag to an - * {@link IdStore} by id, a custom tag to a {@link HashStore} by name. The one place a name becomes - * an id, so the stores themselves never see a name that might be a known tag's. Shared by {@link - * TagMap} and by the tracer core's spans, which own their stores directly. - * - *

A custom store may be null: its owner creates it on the first custom write ({@link - * #newCustomStore}), and until then may borrow its parent's. Writes and removals of custom tags are - * the owner's, which alone can create its store. - * - *

A name resolves once, by {@link #keyOf}, to a key: a known tag's id ({@code > 0}), or 0 for a - * custom tag. - */ -@Restricted(allowedIn = {"datadog.trace.core"}) -public final class TagRouting { - private TagRouting() {} - - /** {@code tag}'s key: its id when known, 0 when custom. */ - public static long keyOf(String tag) { - return KnownTagCodec.keyOf(tag); - } - - /** Whether {@code key} is a known tag's id, as opposed to a custom tag. */ - public static boolean isKnown(long key) { - return key > 0; - } - - /* - * Stores for an owner reading through to a frozen parent TagMap. - */ - - /** - * The parent an owner reads through to: {@code parent}, or null when it is null or empty, as - * {@link TagMap#createFromParent} decides. - * - * @throws IllegalStateException if {@code parent} is not frozen - */ - public static TagMap readThroughParent(TagMap parent) { - if (parent == null) { - return null; - } - if (!parent.isFrozen()) { - throw new IllegalStateException("read-through parent must be frozen"); - } - return parent.isEmpty() ? null : parent; - } - - /** A fresh id store reading through to {@code parent}'s known tags (parent may be null). */ - public static IdStore newIdStore(TagMap parent) { - return IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.ids); - } - - /** - * {@code parent}'s custom store, for an owner to borrow read-only until its first custom write, - * or null. - */ - public static HashStore inheritedCustomStore(TagMap parent) { - return parent == null ? null : parent.names; - } - - /** - * A fresh custom store for an owner's first custom write, reading through to {@code borrowed}, - * the store it borrowed until now (may be null). - */ - public static HashStore newCustomStore(HashStore borrowed) { - return new HashStore(borrowed); - } - - /* - * Reads, by name already resolved to its key by keyOf. - */ - - /** The entry for {@code tag}, already resolved to {@code key}, reading through to parents. */ - public static Entry get(IdStore ids, HashStore names, long key, String tag) { - return isKnown(key) ? entryOf(ids.getEntryReader(key)) : names == null ? null : names.get(tag); - } - - /** - * The Entry an id store's {@code reader} denotes, or null, for a caller that keeps it or hands it - * out of the trust boundary. Free for a hash store's reader, which is an Entry; for a dense - * store's reused flyweight, this builds one. - */ - public static Entry entryOf(TagMap.EntryReader reader) { - return reader == null ? null : reader.entry(); - } -} diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 5c05a7a7da0..d90341e5b19 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -16,7 +16,7 @@ /** * A TagMap's two stores -- an {@link IdStore} for known tags ({@link HashIdStore} unless dense mode - * is on), a {@link HashStore} for custom tags, created lazily -- the {@link TagRouting} over them, + * is on), a {@link HashStore} for custom tags, created lazily -- the name-or-id routing over them, * and the span's view ({@link TagMap#viewOf}). */ class TagStoresTest { @@ -230,38 +230,34 @@ void anIteratorWalksBothStoresAndTheirParents() { @Test void routingReadsKnownTagsByIdAndEverythingElseByName() { - IdStore ids = TagRouting.newIdStore(null); - HashStore names = TagRouting.newCustomStore(null); - long route = TagRouting.keyOf(KnownTags.HTTP_ROUTE_NAME); - assertEquals(KnownTags.HTTP_ROUTE_ID, route); - assertEquals(0L, TagRouting.keyOf("my.tag")); + IdStore ids = IdStore.create(null); + HashStore names = HashStore.create(null); // a direction-scoped OpenTelemetry name is resolved at the OpenTelemetry boundary, not here - assertEquals(0L, TagRouting.keyOf("server.address")); - assertTrue(TagRouting.isKnown(route)); - assertFalse(TagRouting.isKnown(0L)); + assertEquals(0L, KnownTagCodec.keyOf("server.address")); - ids.set(route, "/a"); + ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); names.set("my.tag", "custom"); - assertEquals("/a", TagRouting.get(ids, names, route, KnownTags.HTTP_ROUTE_NAME).stringValue()); - assertEquals("custom", TagRouting.get(ids, names, 0L, "my.tag").stringValue()); - assertNull(TagRouting.get(ids, null, 0L, "my.tag")); // no custom store yet + TagMap view = TagMap.viewOf(ids, names, false); + assertEquals("/a", view.getEntry(KnownTags.HTTP_ROUTE_NAME).stringValue()); + assertEquals("custom", view.getEntry("my.tag").stringValue()); + assertNull(TagMap.viewOf(ids, null, false).getEntry("my.tag")); // no custom store yet } @Test void anOwnerReadsThroughItsParentsStores() { - assertNull(TagRouting.readThroughParent(null)); - assertThrows(IllegalStateException.class, () -> TagRouting.readThroughParent(TagMap.create())); - assertNull(TagRouting.readThroughParent(TagMap.create().freeze())); // empty parent dropped + assertNull(TagMap.readThroughParent(null)); + assertThrows(IllegalStateException.class, () -> TagMap.readThroughParent(TagMap.create())); + assertNull(TagMap.readThroughParent(TagMap.create().freeze())); // empty parent dropped - TagMap parent = TagRouting.readThroughParent(frozenParent()); - IdStore ids = TagRouting.newIdStore(parent); - HashStore borrowed = TagRouting.inheritedCustomStore(parent); + TagMap parent = TagMap.readThroughParent(frozenParent()); + IdStore ids = IdStore.create(parent); + HashStore borrowed = HashStore.inheritedFrom(parent); assertSame(parent.names, borrowed); assertEquals("/inherited", ids.getValue(KnownTags.HTTP_ROUTE_ID)); assertEquals("c", borrowed.get("inherited.custom").stringValue()); - HashStore own = TagRouting.newCustomStore(borrowed); + HashStore own = HashStore.create(borrowed); own.set("own", "o"); assertEquals("c", own.get("inherited.custom").stringValue()); assertNull(borrowed.get("own")); @@ -269,7 +265,7 @@ void anOwnerReadsThroughItsParentsStores() { @Test void theCustomStoreSetsEveryValueShape() { - HashStore names = TagRouting.newCustomStore(null); + HashStore names = HashStore.create(null); names.set("o", (Object) "obj"); names.set("s", "v"); names.set("b", true); @@ -287,9 +283,9 @@ void theCustomStoreSetsEveryValueShape() { @Test void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { - TagMap parent = TagRouting.readThroughParent(frozenParent()); - IdStore ids = TagRouting.newIdStore(parent); - HashStore borrowed = TagRouting.inheritedCustomStore(parent); + TagMap parent = TagMap.readThroughParent(frozenParent()); + IdStore ids = IdStore.create(parent); + HashStore borrowed = HashStore.inheritedFrom(parent); TagMap view = TagMap.viewOf(ids, borrowed, true); assertSame(borrowed, view.customStore()); assertTrue(view.isCustomStoreBorrowed()); From f634ec7e5dc98311228e7197780370c2d627b778 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 10:52:45 -0400 Subject: [PATCH 84/99] Address review: knownTags, GuardedBy and the span's locking scheme - DDSpanContext.tagIds -> knownTags. - @GuardedBy("knownTags") on the span's tag-store fields, with Javadoc on knownTags describing the storage and the one-lock scheme (reentrant, frozen parents read lock-free, the unsafe accessors). - EntryReader parameters are named entryReader, so entryReader.entry() no longer reads as entry.entry(). Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 179 ++++++++++-------- .../core/RootSpanTagsReadThroughTest.java | 6 +- .../java/datadog/trace/api/DenseIdStore.java | 14 +- .../java/datadog/trace/api/HashIdStore.java | 18 +- .../main/java/datadog/trace/api/IdStore.java | 8 +- 5 files changed, 121 insertions(+), 104 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 ab91016b7ce..5d974c4b3f2 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 @@ -58,6 +58,7 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.function.Function; import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -140,31 +141,47 @@ public class DDSpanContext private CharSequence serviceNameSource; /** - * Tags are associated to the current span, they will not propagate to the children span. + * The span's known tags, by id. Also the lock for all of the span's tag state. Tags belong to + * this span; they don't propagate to its children. * - *

The underlying assumption for using a normal Map with synchronized access instead of a - * ConcurrentHashMap is that even though the tags can be accessed and modified from multiple - * threads, they will rarely, if ever, be read and modified concurrently by multiple threads but - * rather read and accessed in a serial fashion on thread after thread. The synchronization can - * then be wrapped around bulk operations to minimize the costly atomic operations. + *

Storage. The span owns its tag stores directly. This store holds the known tags and + * is created with the span. {@link #customTags} holds the custom tags by name and is created on + * the first custom write. Until then it is null, or the read-through parent's custom store, + * borrowed read-only while {@link #customTagsBorrowed}. Every read and write is routed by name or + * id straight to one of the stores. Internal bulk readers (post-processors, serializers, {@link + * #getTags()}) go through the cached {@link #tagView()} instead. * - *

The span owns its tag stores directly: {@code tagIds}, the known tags by id, created with - * the span; and {@code customTags}, the custom tags by name, created on the first custom write -- - * until then null, or the read-through parent's custom store, borrowed read-only while {@code - * customTagsBorrowed}. Every read and write goes span to store, routed by name or id, under one - * lock that guards both: {@code tagIds}, which always exists. Internal readers get a cached - * {@link #tagView() view}. + *

Locking. One lock, this store itself, guards both stores, {@link #customTagsBorrowed} + * and {@link #tagView}. It is this store because it always exists, while {@link #customTags} may + * be null or borrowed. + * + *

    + *
  • The stores are unsynchronized; they rely on this lock. + *
  • Read-through parents are frozen, so reading through them needs no lock. + *
  • The lock is reentrant, and that matters: code that runs while it is held may call back + * into the span's setters. The tag interceptor does so from {@code setAllTags}, and + * post-processors do so through the view from {@code processTagsAndBaggage}. + *
  • {@link #unsafeGetTag} and {@code unsafeSetTag} skip the lock. Callers either hold it + * already or guarantee that nothing mutates the span concurrently. + *
+ * + *

A plain lock rather than concurrent structures: tags are rarely read and written by several + * threads at once. They are touched by one thread after another, so a lock around each operation, + * or around a bulk operation, costs less than atomic operations on every access. */ - private final IdStore tagIds; + @GuardedBy("knownTags") + private final IdStore knownTags; + /** Custom tags by name: null, borrowed from the read-through parent, or the span's own. */ + @GuardedBy("knownTags") private HashStore customTags; + /** True while {@link #customTags} is the read-through parent's store, borrowed read-only. */ + @GuardedBy("knownTags") private boolean customTagsBorrowed; - /** - * This span's view of its tag stores for its internal readers, created on first use under the - * {@code tagIds} lock; see {@link #tagView()}. - */ + /** This span's view of its tag stores for its internal readers; see {@link #tagView()}. */ + @GuardedBy("knownTags") private TagMap tagView; /** The service name is required, otherwise the span are dropped by the agent */ @@ -432,7 +449,7 @@ public DDSpanContext( // Reads through to the frozen tracer-level tags, if any. TagMap parentTags = TagMap.readThroughParent(readThroughParent); - this.tagIds = IdStore.create(parentTags); + this.knownTags = IdStore.create(parentTags); this.customTags = HashStore.inheritedFrom(parentTags); this.customTagsBorrowed = this.customTags != null; @@ -683,7 +700,7 @@ private void seedAbsentTags(final TagMap map) { if (map == null) { return; } - synchronized (tagIds) { + synchronized (knownTags) { map.forEach( this, (ctx, tagEntry) -> { @@ -816,7 +833,7 @@ public int getSamplingPriority() { } public void setSpanSamplingPriority(double rate, int limit) { - synchronized (tagIds) { + synchronized (knownTags) { unsafeSetTag(SPAN_SAMPLING_MECHANISM_TAG, SamplingMechanism.SPAN_SAMPLING_RATE); unsafeSetTag(SPAN_SAMPLING_RULE_RATE_TAG, rate); if (limit != Integer.MAX_VALUE) { @@ -948,13 +965,13 @@ public void setSpanKindOrdinal(String kind) { /** * This span's cached view of its tag stores, for its internal readers (post-processors, the * serializer, the snapshot copy) -- never its own set/get paths. Before each use it is pointed at - * the current custom store, which the span creates lazily. Call under the {@code tagIds} lock. + * the current custom store, which the span creates lazily. Call under the {@code knownTags} lock. */ private TagMap tagView() { TagMap view = this.tagView; if (view == null) { // a view handed to the post-processor chain escapes, so allocate it once per span - this.tagView = view = TagMap.viewOf(tagIds, customTags, customTagsBorrowed); + this.tagView = view = TagMap.viewOf(knownTags, customTags, customTagsBorrowed); } view.attach(customTags, customTagsBorrowed); return view; @@ -962,7 +979,7 @@ private TagMap tagView() { /** * Adopts the custom store a reader may have created through the view (by writing a custom tag), - * so the span's own paths see it. Call under the {@code tagIds} lock, after using the view. + * so the span's own paths see it. Call under the {@code knownTags} lock, after using the view. */ private void adoptViewStores(TagMap view) { this.customTags = view.customStore(); @@ -997,11 +1014,11 @@ private void removeCustomTag(String name) { custom.remove(name); } - /** Removes {@code tag}, known or custom. Call under the {@code tagIds} lock. */ + /** Removes {@code tag}, known or custom. Call under the {@code knownTags} lock. */ private void removeStored(String tag) { long key = KnownTagCodec.keyOf(tag); if (key != 0) { - tagIds.remove(key); + knownTags.remove(key); } else { removeCustomTag(tag); } @@ -1022,7 +1039,7 @@ private void putAllTags(TagMap map) { private void setResolved(long key, String tag, Object value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1030,7 +1047,7 @@ private void setResolved(long key, String tag, Object value) { private void setResolved(long key, String tag, CharSequence value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1038,7 +1055,7 @@ private void setResolved(long key, String tag, CharSequence value) { private void setResolved(long key, String tag, boolean value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1046,7 +1063,7 @@ private void setResolved(long key, String tag, boolean value) { private void setResolved(long key, String tag, int value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1054,7 +1071,7 @@ private void setResolved(long key, String tag, int value) { private void setResolved(long key, String tag, long value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1062,7 +1079,7 @@ private void setResolved(long key, String tag, long value) { private void setResolved(long key, String tag, float value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1070,7 +1087,7 @@ private void setResolved(long key, String tag, float value) { private void setResolved(long key, String tag, double value) { if (key != 0) { - tagIds.set(key, value); + knownTags.set(key, value); } else { customTagsForWrite().set(tag, value); } @@ -1107,8 +1124,8 @@ public String getSpanKindString() { return SPAN_KIND_VALUES[ordinal]; } // UNSET or CUSTOM -- fall through to tag map - synchronized (tagIds) { - TagMap.EntryReader kind = tagIds.getEntryReader(KnownTags.SPAN_KIND_ID); + synchronized (knownTags) { + TagMap.EntryReader kind = knownTags.getEntryReader(KnownTags.SPAN_KIND_ID); return kind == null ? null : kind.stringValue(); } } @@ -1120,31 +1137,31 @@ public void setOrigin(final CharSequence origin) { } public void setMetric(final CharSequence key, final Number value) { - synchronized (tagIds) { + synchronized (knownTags) { unsafeSetTag(key.toString(), value); } } public void setMetric(final CharSequence key, final int value) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(key.toString(), value); } } @@ -1154,7 +1171,7 @@ public void setMetric(final TagMap.EntryReader entry) { return; } - synchronized (tagIds) { + synchronized (knownTags) { putEntry(entry); } } @@ -1193,11 +1210,11 @@ private void storeTag(String tag, double value) { } /** Stores {@code entry}, sharing its Entry where the store keeps Entry objects. */ - private void putEntry(TagMap.EntryReader entry) { - if (entry.tagId() != 0) { - tagIds.put(entry); // no Entry built for a dense store, or for a flyweight from one + private void putEntry(TagMap.EntryReader entryReader) { + if (entryReader.tagId() != 0) { + knownTags.put(entryReader); // no Entry built for a dense store, or for a flyweight from one } else { - customTagsForWrite().put(entry.entry()); + customTagsForWrite().put(entryReader.entry()); } } @@ -1206,7 +1223,7 @@ public void removeTag(String tag) { // Clear the cached ordinal; the tag store still needs to be updated below. spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (tagIds) { + synchronized (knownTags) { removeStored(tag); } } @@ -1224,8 +1241,8 @@ public void removeTag(long tagId) { if (tagId == KnownTags.SPAN_KIND_ID) { spanKindOrdinal = SPAN_KIND_UNSET; } - synchronized (tagIds) { - tagIds.remove(tagId); + synchronized (knownTags) { + knownTags.remove(tagId); } } @@ -1246,7 +1263,7 @@ private boolean isPresent(TagMap.EntryReader entry) { String tag = entry.tag(); long key = KnownTagCodec.keyOf(tag); if (key != 0) { - return tagIds.getValue(key) != null; + return knownTags.getValue(key) != null; } HashStore custom = customTags; return custom != null && custom.get(tag) != null; @@ -1262,7 +1279,7 @@ public void setTag(final String tag, final Object value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1278,7 +1295,7 @@ public void setTag(final String tag, final String value) { } else if (null == value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1298,8 +1315,8 @@ public void setTag(final long tagId, final Object value) { return; } if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1313,8 +1330,8 @@ public void setTag(final long tagId, final CharSequence value) { return; } if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, value)) { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1326,8 +1343,8 @@ public void setTag(final long tagId, final boolean value) { if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1339,8 +1356,8 @@ public void setTag(final long tagId, final int value) { if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1352,8 +1369,8 @@ public void setTag(final long tagId, final long value) { if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1365,8 +1382,8 @@ public void setTag(final long tagId, final float value) { if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1378,8 +1395,8 @@ public void setTag(final long tagId, final double value) { if (tagInterceptor.needsIntercept(tagId)) { this.setBox(tagId, value); } else { - synchronized (tagIds) { - tagIds.set(tagId, value); + synchronized (knownTags) { + knownTags.set(tagId, value); } } } @@ -1391,7 +1408,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!tagInterceptor.interceptTag(this, entry)) { - synchronized (tagIds) { + synchronized (knownTags) { putEntry(entry); } } @@ -1411,8 +1428,8 @@ public void setTag(TagMap.EntryReader entry) { */ private void setBox(long tagId, Object box) { if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (tagIds) { - tagIds.set(tagId, box); + synchronized (knownTags) { + knownTags.set(tagId, box); } } } @@ -1426,7 +1443,7 @@ public void setTag(final String tag, final boolean value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1441,7 +1458,7 @@ public void setTag(final String tag, final int value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1456,7 +1473,7 @@ public void setTag(final String tag, final long value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1471,7 +1488,7 @@ public void setTag(final String tag, final float value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1486,7 +1503,7 @@ public void setTag(final String tag, final double value) { setTag(tagId, value); } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { - synchronized (tagIds) { + synchronized (knownTags) { storeTag(tag, value); } } @@ -1501,7 +1518,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { return; } - synchronized (tagIds) { + synchronized (knownTags) { if (needsIntercept) { // forEach out-performs the iterator of TagMap // Taking advantage of ability to pass through other context arguments @@ -1524,7 +1541,7 @@ void setAllTags(final TagMap.Ledger ledger) { return; } - synchronized (tagIds) { + synchronized (knownTags) { for (final TagMap.EntryChange entryChange : ledger) { String tag = entryChange.tag(); if (entryChange.isRemoval()) { @@ -1550,7 +1567,7 @@ void setAllTags(final Map map) { } else if (map instanceof TagMap) { setAllTags((TagMap) map); } else if (!map.isEmpty()) { - synchronized (tagIds) { + synchronized (knownTags) { for (final Map.Entry tag : map.entrySet()) { long tagId = KnownTagCodec.keyOf(tag.getKey()); boolean intercepted = @@ -1595,7 +1612,7 @@ Object getTag(final String key) { return getSpanKindString(); default: Object value; - synchronized (tagIds) { + synchronized (knownTags) { value = unsafeGetTag(key); } // maintain previously observable type of http url :| @@ -1613,7 +1630,7 @@ Object getTag(final String key) { public Object unsafeGetTag(final String tag) { long key = KnownTagCodec.keyOf(tag); if (key != 0) { - return tagIds.getValue(key); + return knownTags.getValue(key); } HashStore custom = customTags; TagMap.Entry entry = custom == null ? null : custom.get(tag); @@ -1622,7 +1639,7 @@ public Object unsafeGetTag(final String tag) { @Deprecated public TagMap getTags() { - synchronized (tagIds) { + synchronized (knownTags) { TagMap tags = tagView().copy(); tags.put(DDTags.THREAD_ID, threadId); @@ -1675,7 +1692,7 @@ public void setMetaStruct(final String field, final T value) { } void earlyProcessTags(AppendableSpanLinks links) { - synchronized (tagIds) { + synchronized (knownTags) { TagMap view = tagView(); TagsPostProcessorFactory.eagerProcessor().processTags(view, this, links); adoptViewStores(view); @@ -1707,7 +1724,7 @@ void processTagsAndBaggage( // This is a compromise to avoid... // - 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 (tagIds) { + synchronized (knownTags) { // One live view of the tag stores, for the post-processors and the serializer TagMap tags = tagView(); @@ -1916,7 +1933,7 @@ public String toString() { s.append(" *measured*"); } - synchronized (tagIds) { + synchronized (knownTags) { s.append(" tags=").append(new TreeMap<>(getTags())); } return s.toString(); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java b/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java index 2219ad4c2cf..ad9c16a0b9b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/RootSpanTagsReadThroughTest.java @@ -79,9 +79,9 @@ private static DDSpan rootSpanOf(CoreTracer tracer) { /** The known tags the span's own id store holds, not counting what it reads through. */ private static int localKnownTagCount(DDSpan span) throws Exception { - Field tagIds = DDSpanContext.class.getDeclaredField("tagIds"); - tagIds.setAccessible(true); - Object ids = tagIds.get(span.spanContext()); + Field knownTags = DDSpanContext.class.getDeclaredField("knownTags"); + knownTags.setAccessible(true); + Object ids = knownTags.get(span.spanContext()); Method computeSize = Class.forName("datadog.trace.api.IdStore").getDeclaredMethod("computeSize"); computeSize.setAccessible(true); diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index 437c198d99f..b846b3ba148 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -147,14 +147,14 @@ public void set(long tagId, double value) { } @Override - public EntryReader put(EntryReader known) { - long tagId = known.tagId(); - return this.readerOf(tagId, this.store(tagId, known.objectValue())); + public EntryReader put(EntryReader entryReader) { + long tagId = entryReader.tagId(); + return this.readerOf(tagId, this.store(tagId, entryReader.objectValue())); } @Override - public EntryReader getAndPut(EntryReader known) { - long tagId = known.tagId(); + public EntryReader getAndPut(EntryReader entryReader) { + long tagId = entryReader.tagId(); char serial = (char) KnownTagCodec.serialNum(tagId); long bit = slotBit(tagId); int level = levelOf(tagId); @@ -167,10 +167,10 @@ public EntryReader getAndPut(EntryReader known) { this.markLocal(bit, level); this.count++; } - this.values[i] = known.objectValue(); + this.values[i] = entryReader.objectValue(); } else { prior = this.mayInherit(bit, level) ? this.parent.getVisible(serial, bit, level) : null; - this.append(serial, bit, level, known.objectValue()); + this.append(serial, bit, level, entryReader.objectValue()); } return this.readerOf(tagId, prior); } diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index f8c83b3372d..d515c560100 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -139,10 +139,10 @@ public void set(long tagId, double value) { } @Override - public Entry put(EntryReader known) { - Entry knownEntry = known.entry(); - Entry replaced = HashSupport.put(this.buckets, knownEntry); - this.markLocal(slotBit(knownEntry.tagHash), levelOf(knownEntry.tagHash)); + public Entry put(EntryReader entryReader) { + Entry entry = entryReader.entry(); + Entry replaced = HashSupport.put(this.buckets, entry); + this.markLocal(slotBit(entry.tagHash), levelOf(entry.tagHash)); if (replaced == null || replaced.tombstone) { this.count++; // a tombstone replaced is a removal undone: no prior local entry return null; @@ -151,12 +151,12 @@ public Entry put(EntryReader known) { } @Override - public Entry getAndPut(EntryReader known) { - Entry knownEntry = known.entry(); - long tagId = knownEntry.tagHash; + public Entry getAndPut(EntryReader entryReader) { + Entry entry = entryReader.entry(); + long tagId = entry.tagHash; long bit = slotBit(tagId); int level = levelOf(tagId); - Entry replaced = HashSupport.put(this.buckets, knownEntry); + Entry replaced = HashSupport.put(this.buckets, entry); this.markLocal(bit, level); if (replaced != null && !replaced.tombstone) { return replaced; // replaced a local entry -> that is the prior value @@ -342,7 +342,7 @@ void putAllLocal(IdStore other) { } if (this.tombstoneMask != 0L) { // A bucket merge would count a replaced tombstone as a replacement, so put one at a time. - HashSupport.forEach(that.buckets, this, (self, entry) -> self.put(entry)); + HashSupport.forEach(that.buckets, this, (self, entryReader) -> self.put(entryReader)); return; } if (this.count == 0) { diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 5c1404086dd..5e7fe54cbae 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -152,13 +152,13 @@ static IllegalArgumentException notKnown(long tagId) { /** * Inserts or replaces a known tag, returning the local entry it replaced (or null). Does not - * consult the read-through parent. A store that keeps Entry objects keeps {@code known}'s Entry - * (free when it is one); a store that doesn't copies its value. + * consult the read-through parent. A store that keeps Entry objects keeps {@code entryReader}'s + * Entry (free when it is one); a store that doesn't copies its value. */ - public abstract EntryReader put(EntryReader known); + public abstract EntryReader put(EntryReader entryReader); /** {@link #put}, returning the prior visible entry -- local, else read through. */ - public abstract EntryReader getAndPut(EntryReader known); + public abstract EntryReader getAndPut(EntryReader entryReader); /** * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is From da19019d1a9284c1415c23ec0a576899c279cc6d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 11:01:04 -0400 Subject: [PATCH 85/99] Drop the span's cached view instead of re-attaching it The span's view now keeps the stores it was created over. When the span creates its own custom store, it drops the view, and the next bulk reader creates a new one. TagMap.attach goes away. The span copies a map's tags straight into its stores rather than through the view, so the view is created only by the bulk readers late in a span's life, and dropping it is rare. Adoption is skipped for a view the span dropped during its use, so a custom store the span created in a callback isn't replaced by the view's older one. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 29 +++++---- .../trace/core/DDSpanContextTagViewTest.java | 60 +++++++++++++++++++ .../forbiddenApiFilters/instrumentation.txt | 1 - .../main/java/datadog/trace/api/TagMap.java | 14 ++--- .../java/datadog/trace/api/TagStoresTest.java | 4 -- 5 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTagViewTest.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 5d974c4b3f2..dd30466ee43 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 @@ -963,9 +963,11 @@ public void setSpanKindOrdinal(String kind) { } /** - * This span's cached view of its tag stores, for its internal readers (post-processors, the - * serializer, the snapshot copy) -- never its own set/get paths. Before each use it is pointed at - * the current custom store, which the span creates lazily. Call under the {@code knownTags} lock. + * This span's cached view of its tag stores, for its internal bulk readers (post-processors, the + * serializer, the snapshot copy) -- never its own set/get paths. The view keeps the stores it was + * created over: {@link #customTagsForWrite} drops it when the span creates its own custom store. + * Bulk readers run late in a span's life, after its custom writes, so that is rare. Call under + * the {@code knownTags} lock. */ private TagMap tagView() { TagMap view = this.tagView; @@ -973,7 +975,6 @@ private TagMap tagView() { // a view handed to the post-processor chain escapes, so allocate it once per span this.tagView = view = TagMap.viewOf(knownTags, customTags, customTagsBorrowed); } - view.attach(customTags, customTagsBorrowed); return view; } @@ -982,17 +983,25 @@ private TagMap tagView() { * so the span's own paths see it. Call under the {@code knownTags} lock, after using the view. */ private void adoptViewStores(TagMap view) { - this.customTags = view.customStore(); - this.customTagsBorrowed = view.isCustomStoreBorrowed(); + // Skipped when the span dropped this view during its use: a callback into a span setter + // created the span's own custom store, which must not be replaced by the view's older one. + if (view == this.tagView) { + this.customTags = view.customStore(); + this.customTagsBorrowed = view.isCustomStoreBorrowed(); + } } - /** The custom store to write to: created on the first custom write, ending any borrowing. */ + /** + * The custom store to write to: created on the first custom write, ending any borrowing. A cached + * view still holds the old store, so it is dropped. + */ private HashStore customTagsForWrite() { HashStore custom = this.customTags; if (custom == null || this.customTagsBorrowed) { custom = HashStore.create(custom); // reads through to the store borrowed till now this.customTags = custom; this.customTagsBorrowed = false; + this.tagView = null; } return custom; } @@ -1028,9 +1037,9 @@ private void removeStored(String tag) { * Copies {@code map}'s tags into the stores, sharing its Entry objects where a store keeps them. */ private void putAllTags(TagMap map) { - TagMap view = tagView(); - view.putAll(map); - adoptViewStores(view); + // straight into the stores: a view created this early would likely be dropped by a later + // custom write + map.forEach(this, (ctx, entryReader) -> ctx.putEntry(entryReader)); } /* diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTagViewTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTagViewTest.java new file mode 100644 index 00000000000..561dfc33bcc --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTagViewTest.java @@ -0,0 +1,60 @@ +package datadog.trace.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * The span's cached view of its tag stores keeps the stores it was created over, so a custom store + * the span creates afterwards must replace the view rather than be missed by it. + */ +class DDSpanContextTagViewTest extends DDCoreJavaSpecification { + private CoreTracer tracer; + + @AfterEach + void cleanup() { + if (tracer != null) { + tracer.close(); + } + } + + @Test + void aCustomTagWrittenAfterTheViewIsCreatedIsSeenThroughANewView() { + tracer = tracerBuilder().build(); + AgentSpan span = tracer.buildSpan("test", "op").start(); + try { + DDSpanContext context = (DDSpanContext) span.spanContext(); + assertEquals(null, context.getTags().get("custom.first")); // creates the view + + span.setTag("custom.first", "a"); // the span's first custom store: the view is dropped + span.setTag("custom.second", "b"); + + TagMap tags = context.getTags(); + assertEquals("a", tags.get("custom.first")); + assertEquals("b", tags.get("custom.second")); + } finally { + span.finish(); + } + } + + @Test + void aCustomTagCopiedInFromAMapIsStoredWithoutAView() { + tracer = tracerBuilder().build(); + AgentSpan span = tracer.buildSpan("test", "op").start(); + try { + DDSpanContext context = (DDSpanContext) span.spanContext(); + context.setAllTags(TagMap.fromMap(Collections.singletonMap("custom.copied", "c"))); + span.setTag("custom.later", "d"); + + TagMap tags = context.getTags(); + assertEquals("c", tags.get("custom.copied")); + assertEquals("d", tags.get("custom.later")); + } finally { + span.finish(); + } + } +} diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 9f649083f16..102a57de753 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -11,6 +11,5 @@ datadog.trace.api.HashStore datadog.trace.api.IdStore datadog.trace.api.TagMap#readThroughParent(datadog.trace.api.TagMap) datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,boolean) -datadog.trace.api.TagMap#attach(datadog.trace.api.HashStore,boolean) datadog.trace.api.TagMap#customStore() datadog.trace.api.TagMap#isCustomStoreBorrowed() 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 632c53a8f9e..f4571c93d23 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1279,9 +1279,10 @@ private TagMap(IdStore ids) { * serializers) and its snapshot copy; never on the span's own set/get paths. Not thread-safe: use * it under the span's tag lock. * - *

The span owns the stores. Before each use it {@link #attach attaches} the view to its - * current custom store, which it creates lazily; after, it adopts any custom store the view - * created ({@link #customStore}), since a reader may write a custom tag. + *

The span owns the stores, and the view keeps the ones it was created over. A span that later + * creates its own custom store drops its view and creates a new one. After using the view, the + * span adopts any custom store the view created ({@link #customStore}), since a reader may write + * a custom tag. */ @Restricted(allowedIn = {"datadog.trace.core"}) public static TagMap viewOf(IdStore ids, HashStore names, boolean namesBorrowed) { @@ -1314,13 +1315,6 @@ private static Entry entryOf(EntryReader reader) { return reader == null ? null : reader.entry(); } - /** Points this view at its span's current custom store; see {@link #viewOf}. */ - @Restricted(allowedIn = {"datadog.trace.core"}) - public void attach(HashStore names, boolean namesBorrowed) { - this.names = names; - this.namesBorrowed = namesBorrowed; - } - /** This map's custom store: null, borrowed ({@link #isCustomStoreBorrowed}), or its own. */ @Restricted(allowedIn = {"datadog.trace.core"}) public HashStore customStore() { diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index d90341e5b19..e8ddb7a569f 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -296,10 +296,6 @@ void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { assertFalse(view.isCustomStoreBorrowed()); assertNotSame(borrowed, view.customStore()); assertEquals("o", view.customStore().get("own").stringValue()); - - view.attach(null, false); - assertNull(view.getString("own")); - assertNull(view.customStore()); } @Test From 9a5583fbadb01e192309ef053be7187ed2890fb1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 11:12:55 -0400 Subject: [PATCH 86/99] Prefix the span's store-routing operations with route The private helpers that send a tag to the known or the custom store are now routeSet, routePut, routePutAll and routeRemoval (from setResolved / storeTag, putEntry, putAllTags and removeStored), so a write that picks its store reads differently from one that already has it. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 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 dd30466ee43..50d986bad84 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 @@ -708,7 +708,7 @@ private void seedAbsentTags(final TagMap map) { return; } if (!ctx.tagInterceptor.interceptTag(this, tagEntry)) { - ctx.putEntry(tagEntry); + ctx.routePut(tagEntry); } }); } @@ -1024,7 +1024,7 @@ private void removeCustomTag(String name) { } /** Removes {@code tag}, known or custom. Call under the {@code knownTags} lock. */ - private void removeStored(String tag) { + private void routeRemoval(String tag) { long key = KnownTagCodec.keyOf(tag); if (key != 0) { knownTags.remove(key); @@ -1036,17 +1036,18 @@ private void removeStored(String tag) { /** * Copies {@code map}'s tags into the stores, sharing its Entry objects where a store keeps them. */ - private void putAllTags(TagMap map) { + private void routePutAll(TagMap map) { // straight into the stores: a view created this early would likely be dropped by a later // custom write - map.forEach(this, (ctx, entryReader) -> ctx.putEntry(entryReader)); + map.forEach(this, (ctx, entryReader) -> ctx.routePut(entryReader)); } /* - * The resolved writes: a known tag by id, else a custom tag. + * The route* operations send a tag to the store that holds it: the known store by id, else the + * custom store by name. Call them while holding the knownTags lock. */ - private void setResolved(long key, String tag, Object value) { + private void routeSet(long key, String tag, Object value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1054,7 +1055,7 @@ private void setResolved(long key, String tag, Object value) { } } - private void setResolved(long key, String tag, CharSequence value) { + private void routeSet(long key, String tag, CharSequence value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1062,7 +1063,7 @@ private void setResolved(long key, String tag, CharSequence value) { } } - private void setResolved(long key, String tag, boolean value) { + private void routeSet(long key, String tag, boolean value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1070,7 +1071,7 @@ private void setResolved(long key, String tag, boolean value) { } } - private void setResolved(long key, String tag, int value) { + private void routeSet(long key, String tag, int value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1078,7 +1079,7 @@ private void setResolved(long key, String tag, int value) { } } - private void setResolved(long key, String tag, long value) { + private void routeSet(long key, String tag, long value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1086,7 +1087,7 @@ private void setResolved(long key, String tag, long value) { } } - private void setResolved(long key, String tag, float value) { + private void routeSet(long key, String tag, float value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1094,7 +1095,7 @@ private void setResolved(long key, String tag, float value) { } } - private void setResolved(long key, String tag, double value) { + private void routeSet(long key, String tag, double value) { if (key != 0) { knownTags.set(key, value); } else { @@ -1153,25 +1154,25 @@ public void setMetric(final CharSequence key, final Number value) { public void setMetric(final CharSequence key, final int value) { synchronized (knownTags) { - storeTag(key.toString(), value); + routeSet(key.toString(), value); } } public void setMetric(final CharSequence key, final long value) { synchronized (knownTags) { - storeTag(key.toString(), value); + routeSet(key.toString(), value); } } public void setMetric(final CharSequence key, final float value) { synchronized (knownTags) { - storeTag(key.toString(), value); + routeSet(key.toString(), value); } } public void setMetric(final CharSequence key, final double value) { synchronized (knownTags) { - storeTag(key.toString(), value); + routeSet(key.toString(), value); } } @@ -1181,45 +1182,44 @@ public void setMetric(final TagMap.EntryReader entry) { } synchronized (knownTags) { - putEntry(entry); + routePut(entry); } } /* - * Store a tag that interception let through, by name: resolved once to the store that holds it. - * Call while holding the tags lock. + * Route a tag that interception let through, by name: resolved once to the store that holds it. */ - private void storeTag(String tag, Object value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, Object value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void storeTag(String tag, CharSequence value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, CharSequence value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void storeTag(String tag, boolean value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, boolean value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void storeTag(String tag, int value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, int value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void storeTag(String tag, long value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, long value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void storeTag(String tag, float value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, float value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void storeTag(String tag, double value) { - setResolved(KnownTagCodec.keyOf(tag), tag, value); + private void routeSet(String tag, double value) { + routeSet(KnownTagCodec.keyOf(tag), tag, value); } /** Stores {@code entry}, sharing its Entry where the store keeps Entry objects. */ - private void putEntry(TagMap.EntryReader entryReader) { + private void routePut(TagMap.EntryReader entryReader) { if (entryReader.tagId() != 0) { knownTags.put(entryReader); // no Entry built for a dense store, or for a flyweight from one } else { @@ -1233,7 +1233,7 @@ public void removeTag(String tag) { spanKindOrdinal = SPAN_KIND_UNSET; } synchronized (knownTags) { - removeStored(tag); + routeRemoval(tag); } } @@ -1289,7 +1289,7 @@ public void setTag(final String tag, final Object value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1305,7 +1305,7 @@ public void setTag(final String tag, final String value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1418,7 +1418,7 @@ public void setTag(TagMap.EntryReader entry) { // the interceptor prechecks the entry, to avoid boxing if (!tagInterceptor.interceptTag(this, entry)) { synchronized (knownTags) { - putEntry(entry); + routePut(entry); } } } @@ -1453,7 +1453,7 @@ public void setTag(final String tag, final boolean value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1468,7 +1468,7 @@ public void setTag(final String tag, final int value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1483,7 +1483,7 @@ public void setTag(final String tag, final long value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1498,7 +1498,7 @@ public void setTag(final String tag, final float value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1513,7 +1513,7 @@ public void setTag(final String tag, final double value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - storeTag(tag, value); + routeSet(tag, value); } } } @@ -1536,11 +1536,11 @@ void setAllTags(final TagMap map, boolean needsIntercept) { this, (ctx, tagEntry) -> { if (!ctx.tagInterceptor.interceptTag(this, tagEntry)) { - ctx.putEntry(tagEntry); + ctx.routePut(tagEntry); } }); } else { - putAllTags(map); + routePutAll(map); } } } @@ -1558,12 +1558,12 @@ void setAllTags(final TagMap.Ledger ledger) { // mirror removeTag(String): keep the cached ordinal in sync with the tags spanKindOrdinal = SPAN_KIND_UNSET; } - removeStored(tag); + routeRemoval(tag); } else { TagMap.Entry entry = (TagMap.Entry) entryChange; if (!tagInterceptor.interceptTag(this, entry)) { - putEntry(entry); + routePut(entry); } } } @@ -1592,19 +1592,19 @@ void setAllTags(final Map map) { } void unsafeSetTag(final String tag, final Object value) { - storeTag(tag, value); + routeSet(tag, value); } void unsafeSetTag(final String tag, final CharSequence value) { - storeTag(tag, value); + routeSet(tag, value); } void unsafeSetTag(final String tag, final byte value) { - storeTag(tag, value); + routeSet(tag, value); } void unsafeSetTag(final String tag, final double value) { - storeTag(tag, value); + routeSet(tag, value); } Object getTag(final String key) { From 2ed6e5f179c7ac64bea516fe4fb37cdd030071e1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 11:41:42 -0400 Subject: [PATCH 87/99] Fold EntryReadingHelper into the dense store's reader DenseIdStore was the helper's only user, so its by-name and Map.Entry modes were dead. It becomes DenseIdStore.DenseReader, an id-only flyweight: tagId() is a field read and entry() needs no branch, with only the name resolved lazily. TAG_ID_NOT_COMPUTED goes with it. IdStore.Cursor becomes an interface, so DenseCursor extends DenseReader and returns itself from next(): one object per iteration instead of a cursor plus a helper. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseIdStore.java | 129 +++++++++++-- .../java/datadog/trace/api/HashIdStore.java | 4 +- .../main/java/datadog/trace/api/IdStore.java | 4 +- .../main/java/datadog/trace/api/TagMap.java | 127 ------------- .../datadog/trace/api/DenseReaderTest.java | 107 +++++++++++ .../trace/api/EntryReadingHelperTest.java | 169 ------------------ 6 files changed, 227 insertions(+), 313 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/DenseReaderTest.java delete mode 100644 internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index b846b3ba148..ed008daa5bd 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -33,12 +33,115 @@ final class DenseIdStore extends IdStore { private static final char[] NO_SERIALS = new char[0]; private static final Object[] NO_VALUES = new Object[0]; - /** Walks the leaf's slots, then each ancestor's, skipping tombstones and shadowed tags. */ - private static final class DenseCursor extends Cursor { + /** + * The reused flyweight a dense store answers with: a known tag's id and value, with no Entry. Its + * name is resolved from the id only when asked for. + */ + static class DenseReader implements EntryReader { + private long tagId; + private Object value; + private String tag; // resolved lazily via nameOf on first tag() access + + void set(long tagId, Object value) { + this.tagId = tagId; + this.value = value; + this.tag = null; + } + + @Override + public final String tag() { + String tag = this.tag; + if (tag == null) { + tag = KnownTagCodec.nameOf(this.tagId); + this.tag = tag; + } + return tag; + } + + @Override + public final long tagId() { + return this.tagId; + } + + @Override + public final byte type() { + return TagValueConversions.typeOf(this.value); + } + + @Override + public final boolean is(byte type) { + return TagValueConversions.isA(this.value, type); + } + + @Override + public final boolean isNumber() { + return TagValueConversions.isNumber(this.value); + } + + @Override + public final boolean isNumericPrimitive() { + return TagValueConversions.isNumericPrimitive(this.value); + } + + @Override + public final boolean isObject() { + return TagValueConversions.isObject(this.value); + } + + @Override + public final boolean booleanValue() { + return TagValueConversions.toBoolean(this.value); + } + + @Override + public final int intValue() { + return TagValueConversions.toInt(this.value); + } + + @Override + public final long longValue() { + return TagValueConversions.toLong(this.value); + } + + @Override + public final float floatValue() { + return TagValueConversions.toFloat(this.value); + } + + @Override + public final double doubleValue() { + return TagValueConversions.toDouble(this.value); + } + + @Override + public final String stringValue() { + return TagValueConversions.toString(this.value); + } + + @Override + public final Object objectValue() { + return this.value; + } + + @Override + public final TagMap.Entry entry() { + return TagMap.Entry.newAnyEntry(this.tagId, this.value); + } + + @Override + public final Map.Entry mapEntry() { + return this.entry(); + } + } + + /** + * Walks the leaf's slots, then each ancestor's, skipping tombstones and shadowed tags. The cursor + * is its own flyweight: {@link #next} points it at the next visible tag and returns it. + */ + private static final class DenseCursor extends DenseReader implements Cursor { private final DenseIdStore leaf; private DenseIdStore level; private int index = -1; - private final EntryReadingHelper reader = new EntryReadingHelper(); DenseCursor(DenseIdStore leaf) { this.leaf = leaf; @@ -46,12 +149,12 @@ private static final class DenseCursor extends Cursor { } @Override - EntryReader next() { + public EntryReader next() { DenseIdStore level = this.level; while (level != null) { for (++this.index; this.index < level.used; ++this.index) { - if (this.leaf.readIfVisible(level, this.index, this.reader)) { - return this.reader; + if (this.leaf.readIfVisible(level, this.index, this)) { + return this; } } level = level.parent; @@ -77,7 +180,7 @@ EntryReader next() { final DenseIdStore parent; /** The reused flyweight point reads answer with; built on first use. */ - private EntryReadingHelper reader; + private DenseReader reader; DenseIdStore(IdStore parent) { super(parent); @@ -291,9 +394,9 @@ private EntryReader readerOf(long tagId, Object value) { if (value == null) { return null; } - EntryReadingHelper reader = this.reader; + DenseReader reader = this.reader; if (reader == null) { - reader = new EntryReadingHelper(); + reader = new DenseReader(); this.reader = reader; } reader.set(tagId, value); @@ -376,7 +479,7 @@ private boolean visibleFrom(DenseIdStore fromAncestor, int i) { } /** Points {@code reader} at slot {@code i} of {@code level}, if it holds a visible tag. */ - private boolean readIfVisible(DenseIdStore level, int i, EntryReadingHelper reader) { + private boolean readIfVisible(DenseIdStore level, int i, DenseReader reader) { Object value = level.values[i]; if (value == TOMBSTONE || (level != this && !this.visibleFrom(level, i))) { return false; @@ -387,7 +490,7 @@ private boolean readIfVisible(DenseIdStore level, int i, EntryReadingHelper read @Override void forEach(Consumer consumer) { - EntryReadingHelper reader = new EntryReadingHelper(); + DenseReader reader = new DenseReader(); for (DenseIdStore level = this; level != null; level = level.parent) { for (int i = 0; i < level.used; ++i) { if (this.readIfVisible(level, i, reader)) { @@ -399,7 +502,7 @@ void forEach(Consumer consumer) { @Override void forEach(T thisObj, BiConsumer consumer) { - EntryReadingHelper reader = new EntryReadingHelper(); + DenseReader reader = new DenseReader(); for (DenseIdStore level = this; level != null; level = level.parent) { for (int i = 0; i < level.used; ++i) { if (this.readIfVisible(level, i, reader)) { @@ -411,7 +514,7 @@ void forEach(T thisObj, BiConsumer consumer) { @Override void forEach(T thisObj, U otherObj, TriConsumer consumer) { - EntryReadingHelper reader = new EntryReadingHelper(); + DenseReader reader = new DenseReader(); for (DenseIdStore level = this; level != null; level = level.parent) { for (int i = 0; i < level.used; ++i) { if (this.readIfVisible(level, i, reader)) { diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index d515c560100..260455490ea 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -25,7 +25,7 @@ static HashIdStore createEmpty() { } /** Walks the leaf's buckets, then each ancestor's, skipping tombstones and shadowed entries. */ - private static final class HashCursor extends Cursor { + private static final class HashCursor implements Cursor { private final HashIdStore leaf; private HashIdStore level; private final HashSupport.BucketWalker walker = new HashSupport.BucketWalker(); @@ -37,7 +37,7 @@ private static final class HashCursor extends Cursor { } @Override - EntryReader next() { + public EntryReader next() { HashIdStore level = this.level; while (level != null) { Entry entry = this.walker.next(); diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 5e7fe54cbae..50b5c3b71d0 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -34,9 +34,9 @@ @Restricted(allowedIn = {"datadog.trace.core"}) public abstract class IdStore { /** Visits a store's visible entries one at a time. */ - abstract static class Cursor { + interface Cursor { /** The next visible entry, or null when there are none left. */ - abstract EntryReader next(); + EntryReader next(); } /** Slots that may hold a live local entry. */ 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 f4571c93d23..1d07132a1be 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -53,10 +53,6 @@ public final class TagMap implements Map, Iterable. public static final TagMap EMPTY = new TagMap(IdStoreFactory.Default.INSTANCE.createEmpty()); - // Sentinel for an ID not yet resolved by the reused EntryReadingHelper. Zero denotes an unknown - // tag, so it cannot represent "not computed". - static final long TAG_ID_NOT_COMPUTED = Long.MIN_VALUE; - /** Creates a new mutable TagMap that contains the contents of map */ public static final TagMap fromMap(@Nonnull Map map) { TagMap tagMap = TagMap.create(map.size()); @@ -2606,129 +2602,6 @@ public Object next() { } } -final class EntryReadingHelper implements TagMap.EntryReader { - private Map.Entry mapEntry; - private String tag; // null when set by id: resolved lazily via nameOf on first tag() access - private Object value; - private long tagId; - - /** A known tag's value by id, as a dense id store reads it: no name, no Entry. */ - void set(long tagId, Object value) { - this.mapEntry = null; - this.tag = null; - this.value = value; - this.tagId = tagId; - } - - void set(String tag, Object value) { - this.mapEntry = null; - this.tag = tag; - this.value = value; - this.tagId = TagMap.TAG_ID_NOT_COMPUTED; // resolve lazily via keyOf on first tagId() access - } - - void set(Map.Entry mapEntry) { - this.mapEntry = mapEntry; - this.tag = mapEntry.getKey(); - this.value = mapEntry.getValue(); - this.tagId = TagMap.TAG_ID_NOT_COMPUTED; // resolve lazily via keyOf on first tagId() access - } - - @Override - public String tag() { - String tag = this.tag; - if (tag != null) return tag; - - tag = KnownTagCodec.nameOf(this.tagId); - this.tag = tag; - return tag; - } - - @Override - public long tagId() { - long id = this.tagId; - if (id != TagMap.TAG_ID_NOT_COMPUTED) return id; - - id = KnownTagCodec.keyOf(this.tag); - this.tagId = id; - return id; - } - - @Override - public byte type() { - return TagValueConversions.typeOf(this.value); - } - - @Override - public boolean is(byte type) { - return TagValueConversions.isA(this.value, type); - } - - @Override - public boolean isNumber() { - return TagValueConversions.isNumber(this.value); - } - - @Override - public boolean isNumericPrimitive() { - return TagValueConversions.isNumericPrimitive(this.value); - } - - @Override - public boolean isObject() { - return TagValueConversions.isObject(this.value); - } - - @Override - public boolean booleanValue() { - return TagValueConversions.toBoolean(this.value); - } - - @Override - public int intValue() { - return TagValueConversions.toInt(this.value); - } - - @Override - public long longValue() { - return TagValueConversions.toLong(this.value); - } - - @Override - public float floatValue() { - return TagValueConversions.toFloat(this.value); - } - - @Override - public double doubleValue() { - return TagValueConversions.toDouble(this.value); - } - - @Override - public String stringValue() { - return TagValueConversions.toString(this.value); - } - - @Override - public Object objectValue() { - return this.value; - } - - @Override - public TagMap.Entry entry() { - long id = this.tagId; - return id != TagMap.TAG_ID_NOT_COMPUTED && KnownTagCodec.isKnown(id) - ? TagMap.Entry.newAnyEntry(id, this.value) - : TagMap.anyEntryFor(this.tag, this.value); - } - - @Override - public Map.Entry mapEntry() { - Map.Entry mapEntry = this.mapEntry; - return (mapEntry != null) ? mapEntry : this.entry(); - } -} - final class TagValueConversions { TagValueConversions() {} diff --git a/internal-api/src/test/java/datadog/trace/api/DenseReaderTest.java b/internal-api/src/test/java/datadog/trace/api/DenseReaderTest.java new file mode 100644 index 00000000000..ae9b0f4b31d --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/DenseReaderTest.java @@ -0,0 +1,107 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.DenseIdStore.DenseReader; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class DenseReaderTest { + + @Test + void exposesAnIntValue() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + + assertEquals(200, reader.intValue()); + assertEquals(200L, reader.longValue()); + assertEquals(TagMap.EntryReader.INT, reader.type()); + assertTrue(reader.isNumber()); + assertTrue(reader.isNumericPrimitive()); + assertFalse(reader.isObject()); + assertTrue(reader.is(TagMap.EntryReader.INT)); + } + + @Test + void exposesALongValue() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_STATUS_CODE_ID, 123456789L); + + assertEquals(123456789L, reader.longValue()); + assertEquals(TagMap.EntryReader.LONG, reader.type()); + assertTrue(reader.isNumericPrimitive()); + } + + @Test + void exposesABooleanValue() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_STATUS_CODE_ID, true); + + assertTrue(reader.booleanValue()); + assertEquals(TagMap.EntryReader.BOOLEAN, reader.type()); + assertFalse(reader.isNumber()); + assertFalse(reader.isObject()); + } + + @Test + void exposesFloatAndDoubleValues() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_STATUS_CODE_ID, 1.5f); + assertEquals(1.5f, reader.floatValue()); + assertEquals(TagMap.EntryReader.FLOAT, reader.type()); + + reader.set(KnownTags.HTTP_STATUS_CODE_ID, 3.14); + assertEquals(3.14, reader.doubleValue()); + assertEquals(TagMap.EntryReader.DOUBLE, reader.type()); + } + + @Test + void exposesAStringAsAnObject() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_METHOD_ID, "GET"); + + assertEquals("GET", reader.stringValue()); + assertEquals("GET", reader.objectValue()); + assertEquals(TagMap.EntryReader.OBJECT, reader.type()); + assertTrue(reader.isObject()); + assertFalse(reader.isNumber()); + } + + @Test + void namesTheTagFromItsId() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_METHOD_ID, "GET"); + + assertEquals(KnownTags.HTTP_METHOD_ID, reader.tagId()); + assertEquals(KnownTags.HTTP_METHOD_NAME, reader.tag()); + assertEquals(KnownTags.HTTP_METHOD_NAME, reader.tag()); // the resolved name is reused + } + + @Test + void aNewTagReplacesTheResolvedName() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_METHOD_ID, "GET"); + assertEquals(KnownTags.HTTP_METHOD_NAME, reader.tag()); + + reader.set(KnownTags.HTTP_ROUTE_ID, "/users"); + assertEquals(KnownTags.HTTP_ROUTE_ID, reader.tagId()); + assertEquals(KnownTags.HTTP_ROUTE_NAME, reader.tag()); + } + + @Test + void buildsAnEntryForTheTag() { + DenseReader reader = new DenseReader(); + reader.set(KnownTags.HTTP_METHOD_ID, "GET"); + + TagMap.Entry entry = reader.entry(); + assertEquals(KnownTags.HTTP_METHOD_ID, entry.tagId()); + assertEquals(KnownTags.HTTP_METHOD_NAME, entry.tag()); + assertEquals("GET", entry.objectValue()); + + Map.Entry mapEntry = reader.mapEntry(); + assertEquals(KnownTags.HTTP_METHOD_NAME, mapEntry.getKey()); + assertEquals("GET", mapEntry.getValue()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java b/internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java deleted file mode 100644 index 5da8564cb87..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package datadog.trace.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.AbstractMap; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class EntryReadingHelperTest { - - @Test - void setTagValueExposesIntValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("my.tag", 42); - - assertEquals("my.tag", helper.tag()); - assertEquals(42, helper.intValue()); - assertEquals(42L, helper.longValue()); - assertEquals(TagMap.EntryReader.INT, helper.type()); - assertTrue(helper.isNumber()); - assertTrue(helper.isNumericPrimitive()); - assertFalse(helper.isObject()); - assertTrue(helper.is(TagMap.EntryReader.INT)); - } - - @Test - void setTagValueExposesLongValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("id", 123456789L); - - assertEquals(123456789L, helper.longValue()); - assertEquals(TagMap.EntryReader.LONG, helper.type()); - assertTrue(helper.isNumber()); - assertTrue(helper.isNumericPrimitive()); - } - - @Test - void setTagValueExposesBooleanValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("flag", true); - - assertTrue(helper.booleanValue()); - assertEquals(TagMap.EntryReader.BOOLEAN, helper.type()); - assertFalse(helper.isNumber()); - assertFalse(helper.isObject()); - } - - @Test - void setTagValueExposesFloatValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("rate", 1.5f); - - assertEquals(1.5f, helper.floatValue()); - assertEquals(TagMap.EntryReader.FLOAT, helper.type()); - assertTrue(helper.isNumericPrimitive()); - } - - @Test - void setTagValueExposesDoubleValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("pi", 3.14); - - assertEquals(3.14, helper.doubleValue()); - assertEquals(TagMap.EntryReader.DOUBLE, helper.type()); - assertTrue(helper.isNumericPrimitive()); - } - - @Test - void setTagValueExposesStringAsObject() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("name", "foo"); - - assertEquals("foo", helper.stringValue()); - assertEquals("foo", helper.objectValue()); - assertEquals(TagMap.EntryReader.OBJECT, helper.type()); - assertTrue(helper.isObject()); - assertFalse(helper.isNumber()); - assertFalse(helper.isNumericPrimitive()); - } - - @Test - void setTagValueCreatesEntry() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("tag", "value"); - - TagMap.Entry entry = helper.entry(); - assertNotNull(entry); - assertEquals("tag", entry.tag()); - assertEquals("value", entry.objectValue()); - } - - @Test - void mapEntryFallsBackToNewEntryWhenSetWithTagValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("tag", "value"); - - Map.Entry mapEntry = helper.mapEntry(); - assertEquals("tag", mapEntry.getKey()); - assertEquals("value", mapEntry.getValue()); - } - - @Test - void setMapEntryExposesReadMethods() { - EntryReadingHelper helper = new EntryReadingHelper(); - Map.Entry original = new AbstractMap.SimpleEntry<>("key", "hello"); - helper.set(original); - - assertEquals("key", helper.tag()); - assertEquals("hello", helper.stringValue()); - assertEquals("hello", helper.objectValue()); - assertEquals(TagMap.EntryReader.OBJECT, helper.type()); - assertTrue(helper.isObject()); - assertFalse(helper.isNumber()); - } - - @Test - void mapEntryReturnsSameInstanceWhenSetWithMapEntry() { - EntryReadingHelper helper = new EntryReadingHelper(); - Map.Entry original = new AbstractMap.SimpleEntry<>("key", "value"); - helper.set(original); - - assertSame(original, helper.mapEntry()); - } - - @Test - void setMapEntryCreatesEntry() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set(new AbstractMap.SimpleEntry<>("k", 99)); - - TagMap.Entry entry = helper.entry(); - assertNotNull(entry); - assertEquals("k", entry.tag()); - assertEquals(99, entry.objectValue()); - } - - @Test - void tagIdResolvesLazilyAndCaches() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("http.method", "GET"); - - long expectedId = KnownTagCodec.keyOf("http.method"); - assertEquals(expectedId, helper.tagId()); - // second call hits the already-resolved cache rather than re-resolving - assertEquals(expectedId, helper.tagId()); - } - - @Test - void tagIdOfUnknownTagResolvesToZeroAndCaches() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("my.custom.tag", "value"); - - assertEquals(0L, helper.tagId()); - assertEquals(0L, helper.tagId()); - } - - @Test - void tagIdIsRecomputedAfterSet() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("http.method", "GET"); - assertEquals(KnownTagCodec.keyOf("http.method"), helper.tagId()); - - helper.set(new AbstractMap.SimpleEntry<>("db.type", "mysql")); - assertEquals(KnownTagCodec.keyOf("db.type"), helper.tagId()); - } -} From a722412e3967c3bcadca0456acc572c08888ac02 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 11:51:57 -0400 Subject: [PATCH 88/99] Rename TagMap's stores to knownStore and nameStore From review: TagMap's ids / names / namesBorrowed are now knownStore / nameStore / nameStoreBorrowed (and namesForWrite is nameStoreForWrite); the iterator's id cursor is knownCursor. toInternalString's local StringBuilders are builder rather than ledger. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseIdStore.java | 6 +- .../java/datadog/trace/api/HashStore.java | 2 +- .../main/java/datadog/trace/api/IdStore.java | 2 +- .../main/java/datadog/trace/api/TagMap.java | 241 +++++++++--------- .../trace/api/DenseIdStoreForkedTest.java | 4 +- .../java/datadog/trace/api/TagStoresTest.java | 28 +- 6 files changed, 146 insertions(+), 137 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index ed008daa5bd..6f8cb6f3609 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -635,10 +635,10 @@ int computeSize() { @Override String toInternalString() { - StringBuilder ledger = new StringBuilder(); + StringBuilder builder = new StringBuilder(); for (int i = 0; i < this.used; ++i) { Object value = this.values[i]; - ledger + builder .append('[') .append(i) .append("]: ") @@ -647,6 +647,6 @@ String toInternalString() { .append(value == TOMBSTONE ? "" : value) .append('\n'); } - return ledger.toString(); + return builder.toString(); } } diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 6e309efa921..d03741eb953 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -44,7 +44,7 @@ public final class HashStore { * or null. */ public static HashStore inheritedFrom(TagMap parent) { - return parent == null ? null : parent.names; + return parent == null ? null : parent.nameStore; } /** diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 50b5c3b71d0..a413eeb930d 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -72,7 +72,7 @@ interface Cursor { /** A fresh id store reading through to {@code parent}'s known tags (parent may be null). */ public static IdStore create(TagMap parent) { - return IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.ids); + return IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.knownStore); } /** {@code tagId}'s slot bit. */ 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 1d07132a1be..133aa16586e 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1207,21 +1207,21 @@ public EntryChange next() { * precedence never crosses stores. * * The custom store is created on the first custom write. Until then, a map with a parent borrows - * the parent's custom store, read-only (namesBorrowed): the parent is frozen, so its store is + * the parent's custom store, read-only (nameStoreBorrowed): the parent is frozen, so its store is * stable, and with no local custom state the borrowed store reads exactly as an own one would. */ /** The known tags, by id. Not final only so {@link #clear()} can replace it. */ - IdStore ids; + IdStore knownStore; /** * The custom tags, by name: null until the first custom write, or the parent's custom store, - * borrowed read-only while {@link #namesBorrowed}. + * borrowed read-only while {@link #nameStoreBorrowed}. */ - HashStore names; + HashStore nameStore; - /** True while {@link #names} is the parent's custom store, borrowed until the first write. */ - boolean namesBorrowed; + /** True while {@link #nameStore} is the parent's custom store, borrowed until the first write. */ + boolean nameStoreBorrowed; private boolean frozen; @@ -1244,28 +1244,30 @@ public TagMap() { * optimizations can treat it as fixed. */ private TagMap(TagMap parent) { - this.ids = IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.ids); - HashStore parentNames = parent == null ? null : parent.names; - this.names = parentNames; - this.namesBorrowed = parentNames != null; + this.knownStore = + IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.knownStore); + HashStore parentNames = parent == null ? null : parent.nameStore; + this.nameStore = parentNames; + this.nameStoreBorrowed = parentNames != null; this.frozen = false; this.parent = parent; } - /** A map over {@code ids} and {@code names}: a copy ({@link #copy()}) or a view. */ - private TagMap(TagMap parent, IdStore ids, HashStore names, boolean namesBorrowed) { - this.ids = ids; - this.names = names; - this.namesBorrowed = namesBorrowed; + /** A map over {@code knownStore} and {@code nameStore}: a copy ({@link #copy()}) or a view. */ + private TagMap( + TagMap parent, IdStore knownStore, HashStore nameStore, boolean nameStoreBorrowed) { + this.knownStore = knownStore; + this.nameStore = nameStore; + this.nameStoreBorrowed = nameStoreBorrowed; this.frozen = false; this.parent = parent; } - /** A frozen map over the empty parentless store {@code ids}: {@link #EMPTY}. */ - private TagMap(IdStore ids) { - this.ids = ids; - this.names = null; - this.namesBorrowed = false; + /** A frozen map over the empty parentless store {@code knownStore}: {@link #EMPTY}. */ + private TagMap(IdStore knownStore) { + this.knownStore = knownStore; + this.nameStore = null; + this.nameStoreBorrowed = false; this.frozen = true; this.parent = null; } @@ -1281,8 +1283,8 @@ private TagMap(IdStore ids) { * a custom tag. */ @Restricted(allowedIn = {"datadog.trace.core"}) - public static TagMap viewOf(IdStore ids, HashStore names, boolean namesBorrowed) { - return new TagMap(null, ids, names, namesBorrowed); + public static TagMap viewOf(IdStore knownStore, HashStore nameStore, boolean nameStoreBorrowed) { + return new TagMap(null, knownStore, nameStore, nameStoreBorrowed); } /** @@ -1314,72 +1316,75 @@ private static Entry entryOf(EntryReader reader) { /** This map's custom store: null, borrowed ({@link #isCustomStoreBorrowed}), or its own. */ @Restricted(allowedIn = {"datadog.trace.core"}) public HashStore customStore() { - return this.names; + return this.nameStore; } /** Whether {@link #customStore} is the parent's, borrowed read-only. */ @Restricted(allowedIn = {"datadog.trace.core"}) public boolean isCustomStoreBorrowed() { - return this.namesBorrowed; + return this.nameStoreBorrowed; } /** The custom store to write to: created on the first write, ending any borrowing. */ - HashStore namesForWrite() { - HashStore names = this.names; - if (names == null || this.namesBorrowed) { + HashStore nameStoreForWrite() { + HashStore nameStore = this.nameStore; + if (nameStore == null || this.nameStoreBorrowed) { // A borrowed store is the nearest ancestor's custom store: read through to it. - names = new HashStore(names); - this.names = names; - this.namesBorrowed = false; + nameStore = new HashStore(nameStore); + this.nameStore = nameStore; + this.nameStoreBorrowed = false; } - return names; + return nameStore; } /** Removes the custom tag {@code tag}, returning the prior visible entry. */ Entry removeCustom(String tag) { - HashStore names = this.names; - if (names == null) { + HashStore nameStore = this.nameStore; + if (nameStore == null) { return null; } - if (this.namesBorrowed) { - if (names.get(tag) == null) { + if (this.nameStoreBorrowed) { + if (nameStore.get(tag) == null) { return null; // nothing to hide: keep borrowing } - names = this.namesForWrite(); // hiding an inherited tag needs a tombstone of our own + nameStore = this.nameStoreForWrite(); // hiding an inherited tag needs a tombstone of our own } - return names.remove(tag); + return nameStore.remove(tag); } /** Whether either store reads through to a parent. */ private boolean readsThrough() { - HashStore names = this.names; - return this.ids.hasParent() || this.namesBorrowed || (names != null && names.parent != null); + HashStore nameStore = this.nameStore; + return this.knownStore.hasParent() + || this.nameStoreBorrowed + || (nameStore != null && nameStore.parent != null); } @Override public int size() { // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. - HashStore names = this.names; - return this.ids.size() + (names == null ? 0 : names.size()); + HashStore nameStore = this.nameStore; + return this.knownStore.size() + (nameStore == null ? 0 : nameStore.size()); } @Override public boolean isEmpty() { // Exact (Map contract). Under read-through resolves the parent; prefer isDefinitelyEmpty(). - HashStore names = this.names; - return this.ids.isEmpty() && (names == null || names.isEmpty()); + HashStore nameStore = this.nameStore; + return this.knownStore.isEmpty() && (nameStore == null || nameStore.isEmpty()); } public boolean isDefinitelyEmpty() { // Cheap: empty iff no level in the chain holds a local entry (ignores shadowing/tombstones). - HashStore names = this.names; - return this.ids.isDefinitelyEmpty() && (names == null || names.isDefinitelyEmpty()); + HashStore nameStore = this.nameStore; + return this.knownStore.isDefinitelyEmpty() + && (nameStore == null || nameStore.isDefinitelyEmpty()); } public int estimateSize() { // Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. - HashStore names = this.names; - return this.ids.estimateSize() + (names == null ? 0 : names.estimateSize()); + HashStore nameStore = this.nameStore; + return this.knownStore.estimateSize() + (nameStore == null ? 0 : nameStore.estimateSize()); } @Deprecated @@ -1491,15 +1496,15 @@ public Entry getEntry(String tag) { // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. long key = KnownTagCodec.keyOf(tag); if (key != 0) { - return entryOf(this.ids.getEntryReader(key)); + return entryOf(this.knownStore.getEntryReader(key)); } - HashStore names = this.names; - return names == null ? null : names.get(tag); + HashStore nameStore = this.nameStore; + return nameStore == null ? null : nameStore.get(tag); } /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ public Entry getEntry(long tagId) { - return entryOf(this.ids.getEntryReader(tagId)); + return entryOf(this.knownStore.getEntryReader(tagId)); } @Deprecated @@ -1630,7 +1635,7 @@ public void set(@Nonnull String tag, double value) { /** {@code getEntry(tagId)}'s value, or null, for a caller that needs only the value. */ Object getKnownValue(long tagId) { - return this.ids.getValue(tagId); + return this.knownStore.getValue(tagId); } public void set(long tagId, @Nonnull Object value) { @@ -1674,8 +1679,8 @@ public Entry getAndSet(@Nullable Entry newEntry) { this.checkWriteAccess(); return newEntry.tagId() != 0 - ? entryOf(this.ids.getAndPut(newEntry)) - : this.namesForWrite().getAndPut(newEntry); + ? entryOf(this.knownStore.getAndPut(newEntry)) + : this.nameStoreForWrite().getAndPut(newEntry); } /** @@ -1688,22 +1693,22 @@ Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); return newEntry.tagId() != 0 - ? entryOf(this.ids.put(newEntry)) - : this.namesForWrite().put(newEntry); + ? entryOf(this.knownStore.put(newEntry)) + : this.nameStoreForWrite().put(newEntry); } /** {@link #putEntry} for an entry known to be a known tag's. */ void putKnown(Entry knownEntry) { this.checkWriteAccess(); - this.ids.put(knownEntry); + this.knownStore.put(knownEntry); } /** {@link #putEntry} for an entry known to be a custom tag's. */ void putCustom(Entry customEntry) { this.checkWriteAccess(); - this.namesForWrite().put(customEntry); + this.nameStoreForWrite().put(customEntry); } public Entry getAndSet(@Nonnull String tag, Object value) { @@ -1772,10 +1777,10 @@ private void putAllOptimizedMap(TagMap that) { that.forEach(this, (self, entry) -> self.set(entry)); return; } - this.ids.putAllLocal(that.ids); - HashStore thatNames = that.names; + this.knownStore.putAllLocal(that.knownStore); + HashStore thatNames = that.nameStore; if (thatNames != null && thatNames.count != 0) { - this.namesForWrite().putAllLocal(thatNames); + this.nameStoreForWrite().putAllLocal(thatNames); } } @@ -1787,9 +1792,9 @@ public void fillMap(Map map) { this.forEach(map, (m, entry) -> m.put(entry.tag(), entry.objectValue())); return; } - this.ids.fillMapLocal(map); - if (this.names != null) { - this.names.fillMapLocal(map); + this.knownStore.fillMapLocal(map); + if (this.nameStore != null) { + this.nameStore.fillMapLocal(map); } } @@ -1800,9 +1805,9 @@ public void fillStringMap(Map stringMap) { this.forEach(stringMap, (m, entry) -> m.put(entry.tag(), entry.stringValue())); return; } - this.ids.fillStringMapLocal(stringMap); - if (this.names != null) { - this.names.fillStringMapLocal(stringMap); + this.knownStore.fillStringMapLocal(stringMap); + if (this.nameStore != null) { + this.nameStore.fillStringMapLocal(stringMap); } } @@ -1824,24 +1829,27 @@ public Entry getAndRemove(String tag) { // See getEntry: entries are stored under their canonical Datadog name, so a removal by an // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. long key = KnownTagCodec.keyOf(tag); - return key != 0 ? entryOf(this.ids.remove(key)) : this.removeCustom(tag); + return key != 0 ? entryOf(this.knownStore.remove(key)) : this.removeCustom(tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ public Entry getAndRemove(long tagId) { this.checkWriteAccess(); - return entryOf(this.ids.remove(tagId)); + return entryOf(this.knownStore.remove(tagId)); } public TagMap copy() { // The same (frozen, shared) parent, fixed at construction, over a copy of this map's own // entries and tombstones. The copy stays independently mutable (writes land on its local // store, never the shared parent). - HashStore names = this.names; - boolean borrowed = this.namesBorrowed; + HashStore nameStore = this.nameStore; + boolean borrowed = this.nameStoreBorrowed; return new TagMap( - this.parent, this.ids.copy(), names == null || borrowed ? names : names.copy(), borrowed); + this.parent, + this.knownStore.copy(), + nameStore == null || borrowed ? nameStore : nameStore.copy(), + borrowed); } public TagMap immutableCopy() { @@ -1863,27 +1871,27 @@ public Stream stream() { @Override public void forEach(Consumer consumer) { - this.ids.forEach(consumer); - HashStore names = this.names; - if (names != null) { - names.forEach(consumer); + this.knownStore.forEach(consumer); + HashStore nameStore = this.nameStore; + if (nameStore != null) { + nameStore.forEach(consumer); } } public void forEach(T thisObj, BiConsumer consumer) { - this.ids.forEach(thisObj, consumer); - HashStore names = this.names; - if (names != null) { - names.forEach(thisObj, consumer); + this.knownStore.forEach(thisObj, consumer); + HashStore nameStore = this.nameStore; + if (nameStore != null) { + nameStore.forEach(thisObj, consumer); } } public void forEach( T thisObj, U otherObj, TriConsumer consumer) { - this.ids.forEach(thisObj, otherObj, consumer); - HashStore names = this.names; - if (names != null) { - names.forEach(thisObj, otherObj, consumer); + this.knownStore.forEach(thisObj, otherObj, consumer); + HashStore nameStore = this.nameStore; + if (nameStore != null) { + nameStore.forEach(thisObj, otherObj, consumer); } } @@ -1893,9 +1901,9 @@ public void clear() { // clear() removes ALL mappings, including any inherited through read-through. Fresh parentless // stores (rather than tombstoning every inherited key) are simpler and cheaper, and leave an // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. - this.ids = IdStoreFactory.Default.INSTANCE.create(null); - this.names = null; - this.namesBorrowed = false; + this.knownStore = IdStoreFactory.Default.INSTANCE.create(null); + this.nameStore = null; + this.nameStoreBorrowed = false; this.parent = null; } @@ -1914,16 +1922,17 @@ public void checkWriteAccess() { } void checkIntegrity() { - this.ids.checkIntegrity(); - if (this.names != null && !this.namesBorrowed) { - this.names.checkIntegrity(); + this.knownStore.checkIntegrity(); + if (this.nameStore != null && !this.nameStoreBorrowed) { + this.nameStore.checkIntegrity(); } } /** The local entries, recomputed from both stores' structure. */ int computeSize() { - HashStore names = this.names; - return this.ids.computeSize() + (names == null || this.namesBorrowed ? 0 : names.computeSize()); + HashStore nameStore = this.nameStore; + return this.knownStore.computeSize() + + (nameStore == null || this.nameStoreBorrowed ? 0 : nameStore.computeSize()); } boolean checkIfEmpty() { @@ -1965,19 +1974,19 @@ public String toString() { String toPrettyString() { boolean first = true; - StringBuilder ledger = new StringBuilder(128); - ledger.append('{'); + StringBuilder builder = new StringBuilder(128); + builder.append('{'); for (EntryReader entry : this) { if (first) { first = false; } else { - ledger.append(", "); + builder.append(", "); } - ledger.append(entry.tag()).append('=').append(entry.stringValue()); + builder.append(entry.tag()).append('=').append(entry.stringValue()); } - ledger.append('}'); - return ledger.toString(); + builder.append('}'); + return builder.toString(); } /** @@ -1985,17 +1994,17 @@ String toPrettyString() { * debugging */ String toInternalString() { - HashStore names = this.names; - return "ids:\n" - + this.ids.toInternalString() - + "names:\n" - + (names == null ? "none\n" : names.toInternalString()); + HashStore nameStore = this.nameStore; + return "knownStore:\n" + + this.knownStore.toInternalString() + + "nameStore:\n" + + (nameStore == null ? "none\n" : nameStore.toInternalString()); } abstract static class IteratorBase { // Two phases: the id store's visible entries, through its own cursor, then the custom store's // chain -- the leaf's buckets first, then each ancestor's in turn (read-through union). - private IdStore.Cursor ids; // null once the id phase is done + private IdStore.Cursor knownCursor; // null once the id phase is done private final HashStore nameLeaf; private HashStore nameLevel; // the custom level being walked; null before and after private HashSupport.BucketWalker walker; @@ -2003,8 +2012,8 @@ abstract static class IteratorBase { private EntryReader nextEntry; IteratorBase(TagMap map) { - this.ids = map.ids.cursor(); - this.nameLeaf = map.names; + this.knownCursor = map.knownStore.cursor(); + this.nameLeaf = map.nameStore; } public final boolean hasNext() { @@ -2029,13 +2038,13 @@ final EntryReader nextEntryOrThrowNoSuchElement() { } private EntryReader advance() { - IdStore.Cursor ids = this.ids; - if (ids != null) { - EntryReader reader = ids.next(); + IdStore.Cursor knownCursor = this.knownCursor; + if (knownCursor != null) { + EntryReader reader = knownCursor.next(); if (reader != null) { return reader; } - this.ids = null; + this.knownCursor = null; if (this.nameLeaf == null) { return null; } @@ -2472,15 +2481,15 @@ BucketGroup _cloneEntries() { @Override public String toString() { - StringBuilder ledger = new StringBuilder(32); - ledger.append('['); + StringBuilder builder = new StringBuilder(32); + builder.append('['); for (int i = 0; i < BucketGroup.LEN; ++i) { - if (i != 0) ledger.append(", "); + if (i != 0) builder.append(", "); - ledger.append(this._entryAt(i)); + builder.append(this._entryAt(i)); } - ledger.append(']'); - return ledger.toString(); + builder.append(']'); + return builder.toString(); } } diff --git a/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java b/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java index 536ed80f00d..a2f0dd37a3f 100644 --- a/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/DenseIdStoreForkedTest.java @@ -25,8 +25,8 @@ void onlyTheDenseStoreIsLoaded() { map.set(KnownTags.COMPONENT_ID, "okhttp"); map.set("custom.tag", "custom"); - assertSame(DenseIdStore.class, map.ids.getClass()); - assertSame(DenseIdStore.class, TagMap.EMPTY.ids.getClass()); + assertSame(DenseIdStore.class, map.knownStore.getClass()); + assertSame(DenseIdStore.class, TagMap.EMPTY.knownStore.getClass()); assertEquals("okhttp", map.getObject(KnownTags.COMPONENT_NAME)); assertFalse(isLoaded("datadog.trace.api.HashIdStore")); } diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index e8ddb7a569f..932ecf51d30 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -33,10 +33,10 @@ void aMapWithNoCustomTagsHasNoCustomStore() { TagMap tags = TagMap.create(); tags.set(KnownTags.HTTP_ROUTE_ID, "/a"); tags.set(KnownTags.COMPONENT_NAME, "jdbc"); - assertNull(tags.names); + assertNull(tags.nameStore); tags.set("my.tag", "x"); - assertNotNull(tags.names); + assertNotNull(tags.nameStore); assertEquals(3, tags.size()); } @@ -44,18 +44,18 @@ void aMapWithNoCustomTagsHasNoCustomStore() { void aChildBorrowsItsParentsCustomStoreUntilItWritesOne() { TagMap parent = frozenParent(); TagMap child = TagMap.createFromParent(parent); - assertSame(parent.names, child.names); - assertTrue(child.namesBorrowed); + assertSame(parent.nameStore, child.nameStore); + assertTrue(child.nameStoreBorrowed); assertEquals("c", child.getString("inherited.custom")); child.set(KnownTags.HTTP_METHOD_ID, "GET"); // a known write keeps borrowing - assertTrue(child.namesBorrowed); + assertTrue(child.nameStoreBorrowed); assertFalse(child.remove("not.there")); // nothing to hide keeps borrowing - assertTrue(child.namesBorrowed); + assertTrue(child.nameStoreBorrowed); child.set("own.custom", "o"); - assertFalse(child.namesBorrowed); - assertNotSame(parent.names, child.names); + assertFalse(child.nameStoreBorrowed); + assertNotSame(parent.nameStore, child.nameStore); assertEquals("c", child.getString("inherited.custom")); // still reads through assertEquals(5, child.size()); assertEquals("c", parent.getString("inherited.custom")); @@ -69,7 +69,7 @@ void removingABorrowedCustomTagHidesItLocally() { assertEquals("c", child.getAndRemove("inherited.custom").stringValue()); - assertFalse(child.namesBorrowed); + assertFalse(child.nameStoreBorrowed); assertNull(child.getString("inherited.custom")); assertEquals(2, child.size()); assertEquals("c", parent.getString("inherited.custom")); @@ -89,7 +89,7 @@ void removingAnInheritedKnownTagTombstonesItWithoutCountingIt() { for (TagMap.EntryReader entry : child) { assertNotEquals(KnownTags.HTTP_ROUTE_ID, entry.tagId()); } - IdStore ids = child.ids; + IdStore ids = child.knownStore; assertEquals(0, ids.computeSize()); assertNotEquals(0L, ids.tombstoneMask); child.checkIntegrity(); @@ -201,7 +201,7 @@ void aCopyKeepsTombstonesAndBorrowing() { TagMap copy = child.copy(); assertNull(copy.getString(KnownTags.HTTP_ROUTE_NAME)); - assertTrue(copy.namesBorrowed); + assertTrue(copy.nameStoreBorrowed); assertEquals(child.size(), copy.size()); copy.set(KnownTags.HTTP_ROUTE_NAME, "/copy"); @@ -253,7 +253,7 @@ void anOwnerReadsThroughItsParentsStores() { TagMap parent = TagMap.readThroughParent(frozenParent()); IdStore ids = IdStore.create(parent); HashStore borrowed = HashStore.inheritedFrom(parent); - assertSame(parent.names, borrowed); + assertSame(parent.nameStore, borrowed); assertEquals("/inherited", ids.getValue(KnownTags.HTTP_ROUTE_ID)); assertEquals("c", borrowed.get("inherited.custom").stringValue()); @@ -304,9 +304,9 @@ void clearDetachesBothStores() { child.set("own", "o"); child.clear(); assertTrue(child.isEmpty()); - assertNull(child.names); + assertNull(child.nameStore); assertNull(child.getString("inherited.custom")); - assertTrue(child.toInternalString().contains("names:")); + assertTrue(child.toInternalString().contains("nameStore:")); } /** Every known tag's real id, from the registry. */ From 14f33ca6a37b0fbd0c9af90bd781e0c849573fe0 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 12:07:22 -0400 Subject: [PATCH 89/99] Drop two per-call allocations from the review The interception lambdas in seedAbsentTags and setAllTags passed `this` where the base passed `ctx`, which made each a capturing lambda allocated per call; ctx is this, so they pass ctx again. HashIdStore's and HashStore's visibleParentCount counted through forEachParent with an int[] counter, allocated on every size() of a store with a parent (per serialized span); they count with a plain loop again, as the base did. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 4 +-- .../java/datadog/trace/api/HashIdStore.java | 27 ++++++++++++++++--- .../java/datadog/trace/api/HashStore.java | 27 ++++++++++++++++--- 3 files changed, 50 insertions(+), 8 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 50d986bad84..f636b09ea9d 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 @@ -707,7 +707,7 @@ private void seedAbsentTags(final TagMap map) { if (ctx.isPresent(tagEntry)) { return; } - if (!ctx.tagInterceptor.interceptTag(this, tagEntry)) { + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { ctx.routePut(tagEntry); } }); @@ -1535,7 +1535,7 @@ void setAllTags(final TagMap map, boolean needsIntercept) { map.forEach( this, (ctx, tagEntry) -> { - if (!ctx.tagInterceptor.interceptTag(this, tagEntry)) { + if (!ctx.tagInterceptor.interceptTag(ctx, tagEntry)) { ctx.routePut(tagEntry); } }); diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 260455490ea..17beea580a0 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -206,9 +206,30 @@ int size() { /** Ancestor entries not shadowed or tombstoned by a nearer level, nearest-level-wins. */ private int visibleParentCount() { - int[] count = new int[1]; - this.forEachParent(count, null, (c, unused, entry) -> c[0]++); - return count[0]; + // a plain loop, not forEachParent: counting through a lambda would allocate a counter + int count = 0; + for (HashIdStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + count++; + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + count++; + } + } + } + } + } + } + return count; } @Override diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index d03741eb953..50d4301cdfd 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -67,9 +67,30 @@ int size() { /** Ancestor entries not shadowed or tombstoned by a nearer level, nearest-level-wins. */ private int visibleParentCount() { - int[] count = new int[1]; - this.forEachParent(count, null, (c, unused, entry) -> c[0]++); - return count[0]; + // a plain loop, not forEachParent: counting through a lambda would allocate a counter + int count = 0; + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (this.parentEntryVisible(entry, ancestor)) { + count++; + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && this.parentEntryVisible(entry, ancestor)) { + count++; + } + } + } + } + } + } + return count; } boolean isEmpty() { From 334e26a1825c41595c4edf9cd1a39caafb6b236d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 12:17:59 -0400 Subject: [PATCH 90/99] Tidy the store split from review - isPresent reads the reader's own tagId instead of resolving the name again. - createFromParent delegates to readThroughParent, which now carries the exact-emptiness comment; TagMap(TagMap) uses IdStore.create and HashStore.inheritedFrom; nameStoreForWrite uses HashStore.create. - HashSupport.checkBuckets indexes a bucket group's entries by j, not the bucket index i, so the integrity check really inspects groups (copied as-is from the old TagMap.checkIntegrity). - DDSpanContext's constructors drop the tagsSize parameter, which nothing used once the span owned its stores, and CoreTracer no longer computes it per span (that included a size() walking the read-through parent). Co-Authored-By: Claude Opus 5.5 --- .../src/test/groovy/TypeConverterTest.groovy | 1 - .../src/test/groovy/TypeConverterTest.groovy | 1 - .../src/test/groovy/TypeConverterTest.groovy | 1 - .../writer/ddagent/TraceMapperBenchmark.java | 1 - .../datadog/trace/core/PendingTraceWrite.java | 2 -- .../datadog/trace/core/TracerMapperMap.java | 1 - .../core/propagation/InjectorBenchmark.java | 1 - .../java/datadog/trace/core/CoreTracer.java | 8 ------ .../datadog/trace/core/DDSpanContext.java | 12 ++------ .../writer/PayloadDispatcherImplTest.java | 1 - .../trace/core/DDCoreJavaSpecification.java | 1 - .../trace/core/DDSpanSerializationTest.java | 17 ++++------- .../java/datadog/trace/core/DDSpanTest.java | 1 - .../core/LongRunningTracesTrackerTest.java | 1 - .../trace/core/PendingTraceBufferTest.java | 2 -- .../datadog/trace/core/PendingTraceTest.java | 1 - .../propagation/AbstractHttpInjectorTest.java | 1 - .../java/datadog/trace/api/HashSupport.java | 4 +-- .../main/java/datadog/trace/api/TagMap.java | 28 ++++++------------- 19 files changed, 18 insertions(+), 67 deletions(-) diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy index 60c0f43d9be..9dcf709d75f 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy @@ -53,7 +53,6 @@ class TypeConverterTest extends InstrumentationSpecification { [:], false, "fakeType", - 0, trace, null, null, diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy index 3a3b5c6efca..db29f8de153 100644 --- a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy +++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy @@ -65,7 +65,6 @@ class TypeConverterTest extends InstrumentationSpecification { [:], false, "fakeType", - 0, trace, null, null, diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy index 9ecfd5221ef..1506f3eb983 100644 --- a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy +++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy @@ -65,7 +65,6 @@ class TypeConverterTest extends InstrumentationSpecification { [:], false, "fakeType", - 0, trace, null, null, diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java index 54192804b05..f7a6fec6e2c 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java @@ -116,7 +116,6 @@ public void init(Blackhole blackhole) { Collections.emptyMap(), false, UTF8BytesString.create("type"), - 0, traceCollector, null, null, diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/PendingTraceWrite.java b/dd-trace-core/src/jmh/java/datadog/trace/core/PendingTraceWrite.java index b0a90c7bffb..dda438dfc16 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/PendingTraceWrite.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/PendingTraceWrite.java @@ -55,7 +55,6 @@ public void init(TraceCounters counters, Blackhole blackhole) { Collections.emptyMap(), false, "type", - 0, traceCollector, null, null, @@ -80,7 +79,6 @@ public void init(TraceCounters counters, Blackhole blackhole) { Collections.emptyMap(), false, "type", - 0, traceCollector, null, null, diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/TracerMapperMap.java b/dd-trace-core/src/jmh/java/datadog/trace/core/TracerMapperMap.java index 4075274cca2..0e9f4c57ac2 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/TracerMapperMap.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/TracerMapperMap.java @@ -135,7 +135,6 @@ private DDSpan createSpanWithOrigin(int iter, final String origin) { Collections.emptyMap(), false, "type", - 0, traceCollector, null, null, diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/InjectorBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/InjectorBenchmark.java index 1c070167377..b06ce7b3558 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/InjectorBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/InjectorBenchmark.java @@ -122,7 +122,6 @@ public void setUp(Blackhole blackhole) { Collections.emptyMap(), false, "type", - 0, tracer.createTraceCollector(traceId), null, null, diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 94baae88d4e..05516c73e6b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -2373,13 +2373,6 @@ protected static final DDSpanContext buildSpanContext( tagParent = mergedTracerTagsNeedsIntercept ? null : mergedTracerTags; } - final int tagsSize = - mergedTracerTags.size() - + (null == tagLedger ? 0 : tagLedger.estimateSize()) - + (null == coreTags ? 0 : coreTags.size()) - + (null == rootSpanTags || rootSpanTagsReadThrough ? 0 : rootSpanTags.size()) - + (null == contextualTags ? 0 : contextualTags.size()); - if (builderRequestContextDataAppSec != null) { requestContextDataAppSec = builderRequestContextDataAppSec; } @@ -2407,7 +2400,6 @@ protected static final DDSpanContext buildSpanContext( w3cBaggage, errorFlag, spanType, - tagsSize, parentTraceCollector, requestContextDataAppSec, requestContextDataIast, 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 f636b09ea9d..889e61c2a3b 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 @@ -252,7 +252,6 @@ public DDSpanContext( final Map baggageItems, final boolean errorFlag, final CharSequence spanType, - final int tagsSize, final TraceCollector traceCollector, final Object requestContextDataAppSec, final Object requestContextDataIast, @@ -274,7 +273,6 @@ public DDSpanContext( null, errorFlag, spanType, - tagsSize, traceCollector, requestContextDataAppSec, requestContextDataIast, @@ -301,7 +299,6 @@ public DDSpanContext( final Map baggageItems, final boolean errorFlag, final CharSequence spanType, - final int tagsSize, final TraceCollector traceCollector, final Object requestContextDataAppSec, final Object requestContextDataIast, @@ -325,7 +322,6 @@ public DDSpanContext( null, errorFlag, spanType, - tagsSize, traceCollector, requestContextDataAppSec, requestContextDataIast, @@ -355,7 +351,6 @@ public DDSpanContext( final Baggage w3cBaggage, final boolean errorFlag, final CharSequence spanType, - final int tagsSize, final TraceCollector traceCollector, final Object requestContextDataAppSec, final Object requestContextDataIast, @@ -381,7 +376,6 @@ public DDSpanContext( w3cBaggage, errorFlag, spanType, - tagsSize, traceCollector, requestContextDataAppSec, requestContextDataIast, @@ -410,7 +404,6 @@ public DDSpanContext( final Baggage w3cBaggage, final boolean errorFlag, final CharSequence spanType, - final int tagsSize, final TraceCollector traceCollector, final Object requestContextDataAppSec, final Object requestContextDataIast, @@ -1269,13 +1262,12 @@ public void removeTag(long tagId) { */ /** Whether {@code entry}'s tag is already on this span. */ private boolean isPresent(TagMap.EntryReader entry) { - String tag = entry.tag(); - long key = KnownTagCodec.keyOf(tag); + long key = entry.tagId(); // the reader already carries its id: no name lookup if (key != 0) { return knownTags.getValue(key) != null; } HashStore custom = customTags; - return custom != null && custom.get(tag) != null; + return custom != null && custom.get(entry.tag()) != null; } public void setTag(final String tag, final Object value) { diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java index cf756003e7c..41b53b7ab5d 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/PayloadDispatcherImplTest.java @@ -217,7 +217,6 @@ DDSpan realSpan() throws Exception { Collections.emptyMap(), false, "", - 0, trace, null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDCoreJavaSpecification.java b/dd-trace-core/src/test/java/datadog/trace/core/DDCoreJavaSpecification.java index d5fead39ea7..b83d9c16bd4 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDCoreJavaSpecification.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDCoreJavaSpecification.java @@ -115,7 +115,6 @@ protected DDSpan buildSpan( null, false, spanType, - 0, tracer.createTraceCollector(DDTraceId.ONE), null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java index b8ca25a6cc5..1ab43205181 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java @@ -217,7 +217,6 @@ void serializeTraceWithBaggageAndTagsCorrectlyV04( baggage, false, null, - tags.size(), tracer.createTraceCollector(DDTraceId.ONE), null, null, @@ -294,7 +293,6 @@ void serializeTraceWithBaggageAndTagsCorrectlyV05( baggage, false, null, - tags.size(), tracer.createTraceCollector(DDTraceId.ONE), null, null, @@ -353,7 +351,7 @@ void serializeTraceWithConfiguredBaggageAndTagsCorrectlyV1(String scenario, bool w3cBaggageItems.put("ignored", "ignored-value"); DDSpanContext context = - createSpanContext(tracer, baggage, Baggage.create(w3cBaggageItems), injectBaggage, true, 1); + createSpanContext(tracer, baggage, Baggage.create(w3cBaggageItems), injectBaggage, true); context.setTag("span-tag", "span-value"); DDSpan span = DDSpan.create("test", 0, context, null); @@ -377,7 +375,7 @@ void serializeTraceWithConfiguredBaggageAndTagsCorrectlyV1(String scenario, bool @Test void serializeTraceWithSpanLinksAsStructuredLinksOnlyV1() throws Exception { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); - DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true, 0); + DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true); DDSpan span = DDSpan.create("test", 0, context, null); Map linkAttributes = new HashMap<>(); @@ -407,7 +405,7 @@ void serializeTraceWithSpanLinksAsStructuredLinksOnlyV1() throws Exception { @Test void serializeTraceWithSpanEventsAsStructuredEventsOnlyV1() throws Exception { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); - DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true, 1); + DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true); DDSpan span = DDSpan.create("test", 0, context, null); span.addEvent("event.one", typedAttributes(), 1234567890L, NANOSECONDS); span.addEvent("event.two", Collections.emptyMap(), 1234567891L, NANOSECONDS); @@ -439,7 +437,7 @@ void serializeTraceWithSpanEventsAsStructuredEventsOnlyV1() throws Exception { }) void serializeTraceWithSpanEventsAsEventsTag(String protocol) throws Exception { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); - DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true, 1); + DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true); DDSpan span = DDSpan.create("test", 0, context, null); span.addEvent("event.one", typedAttributes(), 1234567890L, NANOSECONDS); span.addEvent("event \"two\"", Collections.emptyMap(), 1234567891L, NANOSECONDS); @@ -536,7 +534,6 @@ void serializeTraceWithFlatMapTagV04() throws Exception { null, false, null, - 0, tracer.createTraceCollector(DDTraceId.ONE), null, null, @@ -603,7 +600,6 @@ void serializeTraceWithFlatMapTagV05() throws Exception { null, false, null, - 0, tracer.createTraceCollector(DDTraceId.ONE), null, null, @@ -747,7 +743,6 @@ private DDSpanContext createSpanContext( baggage, false, spanType, - 1, tracer.createTraceCollector(DDTraceId.ONE), null, null, @@ -765,8 +760,7 @@ private DDSpanContext createSpanContext( Map baggage, Baggage w3cBaggage, boolean injectBaggage, - boolean injectLinks, - int tagsSize) { + boolean injectLinks) { return new DDSpanContext( DDTraceId.ONE, 1, @@ -782,7 +776,6 @@ private DDSpanContext createSpanContext( w3cBaggage, false, "fakeType", - tagsSize, tracer.createTraceCollector(DDTraceId.ONE), null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanTest.java index 1d252859739..46659a5e9ad 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanTest.java @@ -423,7 +423,6 @@ void inferTopLevelFromParentServiceName( Collections.emptyMap(), false, "fakeType", - 0, tracer.createTraceCollector(DDTraceId.ONE), null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java index 27c8156e27f..aab36487f1e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/LongRunningTracesTrackerTest.java @@ -228,7 +228,6 @@ private static DDSpan newSpanOf(PendingTrace trace, int samplingPriority, long t Collections.emptyMap(), false, "fakeType", - 0, trace, null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceBufferTest.java b/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceBufferTest.java index e72e5fa3a7c..99bc9481c50 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceBufferTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceBufferTest.java @@ -530,7 +530,6 @@ private static DDSpan newSpanOf(PendingTrace trace, int samplingPriority, long t Collections.emptyMap(), false, "fakeType", - 0, trace, null, null, @@ -556,7 +555,6 @@ private static DDSpan newSpanOf(DDSpan parent) { Collections.emptyMap(), false, "fakeType", - 0, traceCollector, null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceTest.java b/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceTest.java index 7d47a0b17c5..e4b003cb688 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/PendingTraceTest.java @@ -65,7 +65,6 @@ private DDSpan createSimpleSpanWithID(PendingTrace trace, long id) { Collections.emptyMap(), false, "", - 0, trace, null, null, diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/AbstractHttpInjectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/AbstractHttpInjectorTest.java index 3098ab217ec..306984b790f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/AbstractHttpInjectorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/AbstractHttpInjectorTest.java @@ -51,7 +51,6 @@ protected DDSpanContext mockSpanContext( baggage, false, "fakeType", - 0, this.tracer.createTraceCollector(DDTraceId.ONE), null, null, diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java index e02359314e9..ee6749e1e4e 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashSupport.java +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -374,11 +374,11 @@ static void checkBuckets(Object[] buckets) { for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry thisEntry = curGroup._entryAt(i); + Entry thisEntry = curGroup._entryAt(j); if (thisEntry == null) continue; int thisHash = thisEntry.hash(); - assert curGroup._hashAt(i) == thisHash; + assert curGroup._hashAt(j) == thisHash; int expectedBucket = thisHash & (thisBuckets.length - 1); if (expectedBucket != i) { 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 133aa16586e..0d9d60fc644 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -92,19 +92,7 @@ public static final TagMap create(int size) { * local miss/removal for no benefit. */ public static final TagMap createFromParent(TagMap parent) { - if (parent != null) { - if (!parent.frozen) { - throw new IllegalStateException("read-through parent must be frozen"); - } - // Exact, not isDefinitelyEmpty(): a multi-level parent can be observationally empty while a - // local level still holds entries (all of them tombstoned by a nearer level). Dropping such a - // parent keeps isEmpty()'s no-tombstone fast path valid -- an attached parent always - // contributes at least one visible entry. - if (parent.isEmpty()) { - parent = null; - } - } - return new TagMap(parent); + return new TagMap(readThroughParent(parent)); } /** Creates a new TagMap.Ledger */ @@ -1244,11 +1232,9 @@ public TagMap() { * optimizations can treat it as fixed. */ private TagMap(TagMap parent) { - this.knownStore = - IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.knownStore); - HashStore parentNames = parent == null ? null : parent.nameStore; - this.nameStore = parentNames; - this.nameStoreBorrowed = parentNames != null; + this.knownStore = IdStore.create(parent); + this.nameStore = HashStore.inheritedFrom(parent); + this.nameStoreBorrowed = this.nameStore != null; this.frozen = false; this.parent = parent; } @@ -1301,6 +1287,10 @@ public static TagMap readThroughParent(TagMap parent) { if (!parent.isFrozen()) { throw new IllegalStateException("read-through parent must be frozen"); } + // Exact, not isDefinitelyEmpty(): a multi-level parent can be observationally empty while a + // local level still holds entries (all of them tombstoned by a nearer level). Dropping such a + // parent keeps isEmpty()'s no-tombstone fast path valid -- an attached parent always + // contributes at least one visible entry. return parent.isEmpty() ? null : parent; } @@ -1330,7 +1320,7 @@ HashStore nameStoreForWrite() { HashStore nameStore = this.nameStore; if (nameStore == null || this.nameStoreBorrowed) { // A borrowed store is the nearest ancestor's custom store: read through to it. - nameStore = new HashStore(nameStore); + nameStore = HashStore.create(nameStore); this.nameStore = nameStore; this.nameStoreBorrowed = false; } From 3a090005f6db5cd6cb19df6fbe9cf45a7dac7f8c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 12:19:15 -0400 Subject: [PATCH 91/99] Remove TagMap.getKnownValue Only its own test called it; the span reads values from its stores directly. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagMap.java | 5 ----- .../java/datadog/trace/api/TagMapSetByIdTest.java | 12 ------------ 2 files changed, 17 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 0d9d60fc644..9538f48b522 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1623,11 +1623,6 @@ public void set(@Nonnull String tag, double value) { * custom tags have no id and use the String setters. */ - /** {@code getEntry(tagId)}'s value, or null, for a caller that needs only the value. */ - Object getKnownValue(long tagId) { - return this.knownStore.getValue(tagId); - } - public void set(long tagId, @Nonnull Object value) { this.putKnown(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 360cac81410..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,16 +72,4 @@ void anUnknownIdIsRejected() { assertThrows(IllegalArgumentException.class, () -> map.set(0L, "value")); assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } - - @Test - void theCoresKnownValueReadMatchesTheEntrysValue() { - TagMap parent = TagMap.create(); - parent.set(KnownTags.HTTP_STATUS_CODE_ID, 200); - TagMap tags = TagMap.createFromParent(parent.freeze()); - tags.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); - - assertEquals("/users/{id}", tags.getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals(200, tags.getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); // read-through - assertNull(tags.getKnownValue(KnownTags.HTTP_METHOD_ID)); - } } From fff066e4d8ea153f6c93c8b5cc9f0b76c348e959 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 12:23:01 -0400 Subject: [PATCH 92/99] Fix misplaced and stale comments from review - isPresent moves above setTag(String, Object)'s Javadoc, which it had been inserted under, so setTag is documented again. - A leftover constructor doc stacked on HashStore.inheritedFrom's is removed. - TagMap.EMPTY's comment described the old length-1 bucket array; it now points at IdStoreFactory, which explains why building EMPTY is safe during class initialization. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 20 +++++++++---------- .../java/datadog/trace/api/HashStore.java | 1 - .../main/java/datadog/trace/api/TagMap.java | 5 ++--- 3 files changed, 12 insertions(+), 14 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 889e61c2a3b..3f2a420d432 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 @@ -1248,6 +1248,16 @@ public void removeTag(long tagId) { } } + /** Whether {@code entry}'s tag is already on this span. */ + private boolean isPresent(TagMap.EntryReader entry) { + long key = entry.tagId(); // the reader already carries its id: no name lookup + if (key != 0) { + return knownTags.getValue(key) != null; + } + HashStore custom = customTags; + return custom != null && custom.get(entry.tag()) != null; + } + /** * Sets a tag to the span. Tags are not propagated to the children. * @@ -1260,16 +1270,6 @@ public void removeTag(long tagId) { * @param tag The tag name. * @param value The nullable tag value. */ - /** Whether {@code entry}'s tag is already on this span. */ - private boolean isPresent(TagMap.EntryReader entry) { - long key = entry.tagId(); // the reader already carries its id: no name lookup - if (key != 0) { - return knownTags.getValue(key) != null; - } - HashStore custom = customTags; - return custom != null && custom.get(entry.tag()) != null; - } - public void setTag(final String tag, final Object value) { if (null == tag) { return; diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 50d4301cdfd..8b526d385a0 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -38,7 +38,6 @@ public final class HashStore { */ private Set removedFromParent; - /** A fresh, empty store reading through to {@code parent} (may be null), which is frozen. */ /** * {@code parent}'s custom store, for an owner to borrow read-only until its first custom write, * or 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 9538f48b522..1d588cfb98e 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -48,9 +48,8 @@ */ public final class TagMap implements Map, Iterable { /** Immutable empty TagMap - similar to {@link Collections#emptyMap()} */ - // Frozen view over a length-1 array: bucket masking needs a power-of-two array length (size 0 - // would fail with ArrayIndexOutOfBoundsException, size 1 works), and the private constructor - // reads no statics, so this is safe to build directly during TagMap's . + // Built from the JVM's chosen store's createEmpty(); IdStoreFactory explains why that is safe + // during TagMap's , whichever class initializes first. public static final TagMap EMPTY = new TagMap(IdStoreFactory.Default.INSTANCE.createEmpty()); /** Creates a new mutable TagMap that contains the contents of map */ From 0f1e5acd2a40121fc71e0d0a128ca9097ab37967 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 17 Oct 2026 14:29:47 -0400 Subject: [PATCH 93/99] Keep one occupancy word per level in the id store Span-level and trace-level tags are colored independently, so their slot numbers overlap, and a local root span holds both in one store. With one occupancy word, a trace-level tag (runtime-id) made the slot of an unrelated span-level tag (http.method) look occupied, so a miss on the root span fell through to a scan: GetTagBenchmark's missing known tag read was 6.1 ns dense against 3.2 ns hash. IdStore keeps spanOccupancy and traceOccupancy and picks the word by the tag's level (a select that folds for a constant id). Tombstones keep one shared mask with their own level set. Costs 8 bytes per store. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseIdStore.java | 14 +++--- .../java/datadog/trace/api/HashIdStore.java | 9 ++-- .../main/java/datadog/trace/api/IdStore.java | 43 ++++++++++++++----- .../java/datadog/trace/api/TagStoresTest.java | 21 +++++++-- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index 6f8cb6f3609..8c6d0cdb2c6 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -538,9 +538,10 @@ IdStore copy() { } copy.used = this.used; copy.count = this.count; - copy.occupancy = this.occupancy; + copy.spanOccupancy = this.spanOccupancy; + copy.traceOccupancy = this.traceOccupancy; copy.tombstoneMask = this.tombstoneMask; - copy.levels = this.levels; + copy.tombstoneLevels = this.tombstoneLevels; return copy; } @@ -556,8 +557,8 @@ void putAllLocal(IdStore other) { this.values = Arrays.copyOf(that.values, that.values.length); this.used = that.used; this.count = that.count; - this.occupancy |= that.occupancy; - this.levels |= that.levels; + this.spanOccupancy |= that.spanOccupancy; + this.traceOccupancy |= that.traceOccupancy; return; } for (int i = 0; i < that.used; ++i) { @@ -603,11 +604,12 @@ void checkIntegrity() { throw new IllegalStateException("not a known serial: " + (int) serial); } long tagId = KnownTagCodec.idOf(serial); - long mask = this.values[i] == TOMBSTONE ? this.tombstoneMask : this.occupancy; + boolean tombstone = this.values[i] == TOMBSTONE; + long mask = tombstone ? this.tombstoneMask : this.occupancy(levelOf(tagId)); if ((mask & slotBit(tagId)) == 0L) { throw new IllegalStateException("slot bit not set: " + nameAt(i)); } - if ((this.levels & levelOf(tagId)) == 0) { + if (tombstone && (this.tombstoneLevels & levelOf(tagId)) == 0) { throw new IllegalStateException("level not set: " + nameAt(i)); } if (this.values[i] == null) { diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 17beea580a0..7d623249618 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -349,9 +349,10 @@ IdStore copy() { HashIdStore copy = new HashIdStore(this.parent); HashSupport.copyInto(copy.buckets, this.buckets); copy.count = this.count; - copy.occupancy = this.occupancy; + copy.spanOccupancy = this.spanOccupancy; + copy.traceOccupancy = this.traceOccupancy; copy.tombstoneMask = this.tombstoneMask; - copy.levels = this.levels; + copy.tombstoneLevels = this.tombstoneLevels; return copy; } @@ -372,8 +373,8 @@ void putAllLocal(IdStore other) { } else { this.count += HashSupport.merge(this.buckets, that.buckets); } - this.occupancy |= that.occupancy; - this.levels |= that.levels; + this.spanOccupancy |= that.spanOccupancy; + this.traceOccupancy |= that.traceOccupancy; } @Override diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index a413eeb930d..4854cfff427 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -39,8 +39,15 @@ interface Cursor { EntryReader next(); } - /** Slots that may hold a live local entry. */ - long occupancy; + /** + * Slots that may hold a live local span-level entry. Span-level and trace-level tags are colored + * independently, so their slot numbers overlap; a local root span holds both, so each level keeps + * its own word and a span-level miss isn't defeated by a trace-level tag in the same slot. + */ + long spanOccupancy; + + /** Slots that may hold a live local trace-level entry; see {@link #spanOccupancy}. */ + long traceOccupancy; /** Slots that may hold a local tombstone, hiding an inherited entry. */ long tombstoneMask; @@ -51,8 +58,8 @@ interface Cursor { */ final long inheritedMask; - /** The levels ({@link #SPAN_LEVEL}, {@link #TRACE_LEVEL}) of the local entries and tombstones. */ - byte levels; + /** The levels ({@link #SPAN_LEVEL}, {@link #TRACE_LEVEL}) of the local tombstones. */ + byte tombstoneLevels; /** The levels an ancestor may hold, fixed at construction like {@link #inheritedMask}. */ final byte inheritedLevels; @@ -65,8 +72,8 @@ interface Cursor { this.inheritedMask = 0L; this.inheritedLevels = 0; } else { - this.inheritedMask = parent.occupancy | parent.inheritedMask; - this.inheritedLevels = (byte) (parent.levels | parent.inheritedLevels); + this.inheritedMask = parent.spanOccupancy | parent.traceOccupancy | parent.inheritedMask; + this.inheritedLevels = (byte) (parent.localLevels() | parent.inheritedLevels); } } @@ -85,9 +92,22 @@ static int levelOf(long tagId) { return 1 << (int) ((tagId & KnownTagCodec.LEVEL_TRACE) >>> 2); } + /** The occupancy word of {@code level}: a select that folds when the level is constant. */ + final long occupancy(int level) { + return level == TRACE_LEVEL ? this.traceOccupancy : this.spanOccupancy; + } + + /** The levels of the local entries and tombstones. */ + final int localLevels() { + return (this.spanOccupancy != 0L ? SPAN_LEVEL : 0) + | (this.traceOccupancy != 0L ? TRACE_LEVEL : 0) + | this.tombstoneLevels; + } + /** Whether the slot {@code bit} may hold a local entry or tombstone of {@code level}. */ final boolean mayHaveLocal(long bit, int level) { - return (levels & level) != 0 && ((occupancy | tombstoneMask) & bit) != 0L; + return (occupancy(level) & bit) != 0L + || ((tombstoneLevels & level) != 0 && (tombstoneMask & bit) != 0L); } /** Whether an ancestor may hold a tag of {@code level} in the slot {@code bit}. */ @@ -97,14 +117,17 @@ final boolean mayInherit(long bit, int level) { /** Records a live local entry in the slot {@code bit} of {@code level}. */ final void markLocal(long bit, int level) { - this.occupancy |= bit; - this.levels |= (byte) level; + if (level == TRACE_LEVEL) { + this.traceOccupancy |= bit; + } else { + this.spanOccupancy |= bit; + } } /** Records a local tombstone in the slot {@code bit} of {@code level}. */ final void markTombstone(long bit, int level) { this.tombstoneMask |= bit; - this.levels |= (byte) level; + this.tombstoneLevels |= (byte) level; } /** Throws for an id that names no known tag. */ diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java index 932ecf51d30..888a7dde3e8 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -131,19 +131,34 @@ void aSpanLevelReadSkipsAParentHoldingOnlyTraceLevelTagsInItsSlot() { @Test void aClearSlotBitIsADefiniteMiss() { HashIdStore ids = new HashIdStore(null); - assertEquals(0L, ids.occupancy); + assertEquals(0L, ids.spanOccupancy); assertNull(ids.getEntryReader(KnownTags.HTTP_ROUTE_ID)); ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); - assertNotEquals(0L, ids.occupancy & IdStore.slotBit(KnownTags.HTTP_ROUTE_ID)); + assertNotEquals(0L, ids.spanOccupancy & IdStore.slotBit(KnownTags.HTTP_ROUTE_ID)); assertEquals("/a", ids.getValue(KnownTags.HTTP_ROUTE_ID)); // a child inherits its parent's slots, so a read-through miss can stop early HashIdStore child = new HashIdStore(ids); - assertEquals(ids.occupancy, child.inheritedMask); + assertEquals(ids.spanOccupancy, child.inheritedMask); assertEquals("/a", child.getValue(KnownTags.HTTP_ROUTE_ID)); } + @Test + void aTraceLevelTagDoesNotDefeatASpanLevelMissInTheSameSlot() { + // A local root span holds trace-level and span-level tags in one store, and the two levels are + // colored independently, so their slot numbers overlap. + for (IdStore ids : new IdStore[] {new HashIdStore(null), new DenseIdStore(null)}) { + ids.set(KnownTags.RUNTIME_ID, "abc"); + ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); + long runtimeIdBit = IdStore.slotBit(KnownTags.RUNTIME_ID); + assertNotEquals(runtimeIdBit, IdStore.slotBit(KnownTags.HTTP_ROUTE_ID), "test precondition"); + + assertTrue(ids.mayHaveLocal(runtimeIdBit, IdStore.TRACE_LEVEL)); + assertFalse(ids.mayHaveLocal(runtimeIdBit, IdStore.SPAN_LEVEL), ids.getClass().getName()); + } + } + @Test void anUnknownIdIsRejected() { HashIdStore ids = new HashIdStore(null); From b306324eb4ef4c4e571172e62f9f1e66cc8f5bb3 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 19 Oct 2026 15:50:43 -0400 Subject: [PATCH 94/99] Cover both id stores with a shared contract test Patch coverage was 75%, mostly DenseIdStore: the dense store is behind a flag that is off by default, so the shared TagMap suites only reached the hash store and the dense store ran in one small forked test. - IdStoreContractTest runs the IdStore contract against HashIdStore and DenseIdStore in the normal test JVM: typed setters and readers, put and getAndPut priors, removing and reviving inherited tags, size and emptiness through parents, every iteration form, copy, putAllLocal (including colliding buckets), the map fills, growth, multi-level reads and id validation. - DDSpanContextStoresTest covers the span's routing: custom tags by name with each value type, primitive sets by id (intercepted, declined and plain), setMetric with known names, map writes, and the custom store borrowed from the tracer-level tags until the span's first custom write. - The by-name setters' custom branches write straight to the custom store: they already know keyOf(tag) is 0, so going through routeSet repeated the lookup. routeSet's boolean overloads lost their last callers and are removed. Local JaCoCo patch coverage (vs #12731): 75% -> 98%. The rest is mostly checkIntegrity's error branches. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 26 +- .../trace/core/DDSpanContextStoresTest.java | 188 +++++++++ .../trace/api/IdStoreContractTest.java | 378 ++++++++++++++++++ 3 files changed, 573 insertions(+), 19 deletions(-) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.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 3f2a420d432..5f5bace0f7d 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 @@ -1056,14 +1056,6 @@ private void routeSet(long key, String tag, CharSequence value) { } } - private void routeSet(long key, String tag, boolean value) { - if (key != 0) { - knownTags.set(key, value); - } else { - customTagsForWrite().set(tag, value); - } - } - private void routeSet(long key, String tag, int value) { if (key != 0) { knownTags.set(key, value); @@ -1191,10 +1183,6 @@ private void routeSet(String tag, CharSequence value) { routeSet(KnownTagCodec.keyOf(tag), tag, value); } - private void routeSet(String tag, boolean value) { - routeSet(KnownTagCodec.keyOf(tag), tag, value); - } - private void routeSet(String tag, int value) { routeSet(KnownTagCodec.keyOf(tag), tag, value); } @@ -1281,7 +1269,7 @@ public void setTag(final String tag, final Object value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } @@ -1297,7 +1285,7 @@ public void setTag(final String tag, final String value) { removeTag(tag); } else if (!tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } @@ -1445,7 +1433,7 @@ public void setTag(final String tag, final boolean value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } @@ -1460,7 +1448,7 @@ public void setTag(final String tag, final int value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } @@ -1475,7 +1463,7 @@ public void setTag(final String tag, final long value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } @@ -1490,7 +1478,7 @@ public void setTag(final String tag, final float value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } @@ -1505,7 +1493,7 @@ public void setTag(final String tag, final double value) { } else if (!tagInterceptor.needsInterceptCustomTag(tag) || !tagInterceptor.interceptCustomTag(this, tag, value)) { synchronized (knownTags) { - routeSet(tag, value); + customTagsForWrite().set(tag, value); // already known to be custom } } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java new file mode 100644 index 00000000000..a04c3e6fa4f --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java @@ -0,0 +1,188 @@ +package datadog.trace.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.common.writer.ListWriter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * How a span routes tags to its own known and custom stores: custom tags by name with every value + * type, primitive sets of intercepted tags by id, map writes, and the custom store borrowed from + * the tracer-level tags until the span's first custom write. + */ +class DDSpanContextStoresTest extends DDCoreJavaSpecification { + private CoreTracer tracer; + + @BeforeEach + void setup() { + tracer = + tracerBuilder() + .writer(new ListWriter()) + .defaultSpanTags(Collections.singletonMap("global.custom", "from-tracer")) + .build(); + } + + @AfterEach + void tearDown() { + tracer.close(); + } + + private DDSpan start() { + return (DDSpan) tracer.startSpan("test-instr", "stores.op"); + } + + @Test + void customTagsKeepEveryValueType() { + DDSpan span = start(); + try { + span.setTag("custom.chars", (CharSequence) "chars"); + span.setTag("custom.bool", true); + span.setTag("custom.int", 7); + span.setTag("custom.long", 8L); + span.setTag("custom.float", 1.5f); + span.setTag("custom.double", 2.5d); + + assertEquals("chars", span.getTag("custom.chars")); + assertEquals(true, span.getTag("custom.bool")); + assertEquals(7, span.getTag("custom.int")); + assertEquals(8L, span.getTag("custom.long")); + assertEquals(1.5f, span.getTag("custom.float")); + assertEquals(2.5d, span.getTag("custom.double")); + } finally { + span.finish(); + } + } + + @Test + void interceptedTagsSetByIdAsPrimitivesAreRouted() { + DDSpan span = start(); + try { + span.spanContext().setTag(KnownTags.HTTP_STATUS_CODE_ID, 404); + assertEquals(404, span.getHttpStatusCode()); + span.spanContext().setTag(KnownTags.HTTP_STATUS_CODE_ID, 503L); + assertEquals(503, span.getHttpStatusCode()); + + span.spanContext().setTag(KnownTags.DD_MEASURED_ID, true); + assertTrue(span.isMeasured()); + + span.spanContext().setTag(KnownTags.DD1_SR_EAUSR_ID, 0.25d); + span.spanContext().setTag(KnownTags.DD1_SR_EAUSR_ID, 0.5f); + assertEquals(0.5f, ((Number) span.getTag("_dd1.sr.eausr")).floatValue()); + } finally { + span.finish(); + } + } + + @Test + void knownTagsSetByIdAsPrimitivesWithoutInterception() { + DDSpan span = start(); + try { + span.spanContext().setTag(KnownTags.PEER_PORT_ID, 5432L); + span.spanContext().setTag(KnownTags.DB_INSTANCE_ID, 1.5f); + span.spanContext().setTag(KnownTags.DB_POOL_NAME_ID, 2.5d); + assertEquals(5432L, span.getTag("peer.port")); + assertEquals(1.5f, span.getTag("db.instance")); + assertEquals(2.5d, span.getTag("db.pool.name")); + + // an intercepted tag the interceptor declines is still stored: span.kind records its + // ordinal and keeps the value + span.spanContext().setTag(KnownTags.SPAN_KIND_ID, 1); + assertEquals("1", String.valueOf(span.getTag("span.kind"))); + } finally { + span.finish(); + } + } + + @Test + void metricsWithKnownNamesGoToTheKnownStore() { + DDSpan span = start(); + try { + span.setMetric("db.user", 200); + span.setMetric("peer.port", 8080L); + span.setMetric("db.instance", 0.5f); + span.setMetric("db.pool.name", 0.75d); + span.setMetric("custom.metric", 3L); + assertEquals(200, span.getTag("db.user")); + assertEquals(8080L, span.getTag("peer.port")); + assertEquals(0.5f, span.getTag("db.instance")); + assertEquals(0.75d, span.getTag("db.pool.name")); + assertEquals(3L, span.getTag("custom.metric")); + } finally { + span.finish(); + } + } + + @Test + void aMapWriteRoutesKnownAndCustomTags() { + DDSpan span = start(); + try { + Map tags = new LinkedHashMap<>(); + tags.put("http.route", "/users"); + tags.put("custom.from.map", "value"); + tags.put("custom.chars.from.map", new StringBuilder("chars")); + span.setAllTags(tags); + span.spanContext().unsafeSetTag("custom.unsafe", (CharSequence) "unsafe"); + + assertEquals("/users", span.getTag("http.route")); + assertEquals("value", span.getTag("custom.from.map")); + assertEquals("chars", String.valueOf(span.getTag("custom.chars.from.map"))); + assertEquals("unsafe", span.getTag("custom.unsafe")); + } finally { + span.finish(); + } + } + + @Test + void removingACustomTagTheSpanDoesNotHaveKeepsBorrowing() { + DDSpan span = start(); + try { + assertEquals("from-tracer", span.getTag("global.custom")); + span.removeTag("never.set"); // nothing to hide: the borrowed store stays as it is + assertEquals("from-tracer", span.getTag("global.custom")); + } finally { + span.finish(); + } + } + + @Test + void removingABorrowedCustomTagHidesItFromThisSpanOnly() { + DDSpan span = start(); + DDSpan other = start(); + try { + span.removeTag("global.custom"); // copies the borrowed store, then hides the tag + assertNull(span.getTag("global.custom")); + assertEquals("from-tracer", other.getTag("global.custom"), "the tracer-level tag stays"); + } finally { + span.finish(); + other.finish(); + } + } + + @Test + void aPrototypesCustomTagIsSeededOnlyWhereAbsent() { + SpanPrototype prototype = + SpanPrototype.builder() + .initInstrumentationName("test-instr") + .initOperationName("stores.proto") + .initTag("global.custom", "from-prototype") + .initTag("proto.custom", "seeded") + .build(); + DDSpan span = (DDSpan) tracer.startSpan(prototype, null); + try { + // the tracer-level tag is already visible, so the prototype's default doesn't replace it + assertEquals("from-tracer", span.getTag("global.custom")); + assertEquals("seeded", span.getTag("proto.custom")); + } finally { + span.finish(); + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java new file mode 100644 index 00000000000..bd18c6cf5de --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -0,0 +1,378 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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 static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.api.TagMap.EntryReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * The {@link IdStore} contract, run against both stores in the normal test JVM: the dense store is + * behind a flag that is off by default, so the shared {@link TagMap} suites only reach the hash + * store. Each store is built from a parent of its own kind, as the factories do. + */ +class IdStoreContractTest { + + static Stream stores() { + return Stream.of( + arguments(named("HashIdStore", (Function) HashIdStore::new)), + arguments(named("DenseIdStore", (Function) DenseIdStore::new))); + } + + /** Every known tag id, in declaration order. */ + private static List knownIds() throws IllegalAccessException { + List ids = new ArrayList<>(); + for (java.lang.reflect.Field field : KnownTags.class.getFields()) { + if (field.getType() == long.class) { + long id = field.getLong(null); + if (KnownTagCodec.isKnown(id) && !ids.contains(id)) { + ids.add(id); + } + } + } + return ids; + } + + private static Map visible(IdStore store) { + Map seen = new HashMap<>(); + store.forEach(entry -> assertNull(seen.put(entry.tag(), entry.objectValue()), entry.tag())); + return seen; + } + + @ParameterizedTest + @MethodSource("stores") + void typedSettersRoundTripThroughTheReader(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, (Object) "/users"); + store.set(KnownTags.HTTP_METHOD_ID, (CharSequence) "GET"); + store.set(KnownTags.ERROR_ID, true); + store.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + store.set(KnownTags.PEER_PORT_ID, 5432L); + store.set(KnownTags.DD_AGENT_PSR_ID, 0.5f); + store.set(KnownTags.DB_TYPE_ID, (Object) "postgres"); + store.set(KnownTags.COMPONENT_ID, (CharSequence) "jdbc"); + store.set(KnownTags.SPAN_KIND_ID, (Object) "client"); + store.set(KnownTags.DB_POOL_NAME_ID, (Object) "pool"); + store.set(KnownTags.HTTP_URL_ID, (Object) "http://x"); + store.set(KnownTags.DB_INSTANCE_ID, 2.5d); + + assertEquals("/users", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + + EntryReader flag = store.getEntryReader(KnownTags.ERROR_ID); + assertTrue(flag.booleanValue()); + assertEquals(EntryReader.BOOLEAN, flag.type()); + assertFalse(flag.isNumber()); + + EntryReader status = store.getEntryReader(KnownTags.HTTP_STATUS_CODE_ID); + assertEquals(200, status.intValue()); + assertEquals(200L, status.longValue()); + assertTrue(status.is(EntryReader.INT)); + assertTrue(status.isNumericPrimitive()); + assertFalse(status.isObject()); + assertEquals(KnownTags.HTTP_STATUS_CODE_ID, status.tagId()); + assertEquals("http.status_code", status.tag()); + + assertEquals(5432L, store.getEntryReader(KnownTags.PEER_PORT_ID).longValue()); + assertEquals(0.5f, store.getEntryReader(KnownTags.DD_AGENT_PSR_ID).floatValue()); + assertEquals(2.5d, store.getEntryReader(KnownTags.DB_INSTANCE_ID).doubleValue()); + + EntryReader route = store.getEntryReader(KnownTags.HTTP_ROUTE_ID); + assertEquals("/users", route.stringValue()); + assertTrue(route.isObject()); + assertEquals("/users", route.entry().objectValue()); + assertEquals("http.route", route.mapEntry().getKey()); + + assertEquals(12, store.size()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void putAndGetAndPutReturnThePriorValue(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + + assertNull(store.put(TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "GET"))); + EntryReader prior = store.put(TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "POST")); + assertEquals("GET", prior.objectValue()); + + // getAndPut reports the prior visible value, inherited included + EntryReader inherited = + store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/local")); + assertEquals("/inherited", inherited.objectValue()); + assertEquals("/local", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals( + "/local", + store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/again")).objectValue()); + assertNull(store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.COMPONENT_ID, "okhttp"))); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void removingAnInheritedTagHidesItUntilItIsSetAgain(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.set(KnownTags.COMPONENT_ID, "okhttp"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + assertTrue(store.hasParent()); + assertEquals(3, store.size()); + + assertEquals("/inherited", store.remove(KnownTags.HTTP_ROUTE_ID).objectValue()); + assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertNull(store.remove(KnownTags.HTTP_ROUTE_ID), "already hidden"); + assertEquals(2, store.size()); + assertEquals("/inherited", parent.getValue(KnownTags.HTTP_ROUTE_ID), "parent untouched"); + + // a local value that also hides an inherited one is removed with its tombstone in place + store.set(KnownTags.COMPONENT_ID, "local"); + assertEquals("local", store.remove(KnownTags.COMPONENT_ID).objectValue()); + assertNull(store.getValue(KnownTags.COMPONENT_ID)); + + assertEquals("GET", store.remove(KnownTags.HTTP_METHOD_ID).objectValue()); + assertNull(store.remove(KnownTags.HTTP_METHOD_ID), "absent"); + assertTrue(store.isEmpty()); + assertTrue(store.isDefinitelyEmpty() || store.estimateSize() >= 0); + + // setting a hidden tag again revives it + store.set(KnownTags.HTTP_ROUTE_ID, "/revived"); + assertEquals("/revived", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(1, store.size()); + assertFalse(store.isEmpty()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void sizeAndEmptinessCountWhatIsVisible(Function newStore) { + IdStore empty = newStore.apply(null); + assertTrue(empty.isEmpty()); + assertTrue(empty.isDefinitelyEmpty()); + assertEquals(0, empty.estimateSize()); + assertFalse(empty.hasParent()); + + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + IdStore child = newStore.apply(parent); + assertFalse(child.isEmpty(), "inherits visible tags"); + assertFalse(child.isDefinitelyEmpty()); + + child.set(KnownTags.HTTP_METHOD_ID, "POST"); // shadows + child.set(KnownTags.COMPONENT_ID, "okhttp"); + assertEquals(3, child.size()); + assertTrue(child.estimateSize() >= 3); + + child.remove(KnownTags.HTTP_ROUTE_ID); + child.remove(KnownTags.HTTP_METHOD_ID); + child.remove(KnownTags.COMPONENT_ID); + assertTrue(child.isEmpty(), "every inherited tag hidden"); + assertEquals(0, child.size()); + } + + @ParameterizedTest + @MethodSource("stores") + void everyIterationSeesEachVisibleTagOnce(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.set(KnownTags.COMPONENT_ID, "okhttp"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "POST"); // shadows the parent's + store.set(KnownTags.SPAN_KIND_ID, "client"); + store.remove(KnownTags.COMPONENT_ID); // hides the parent's + + Map expected = new HashMap<>(); + expected.put("http.route", "/a"); + expected.put("http.method", "POST"); + expected.put("span.kind", "client"); + assertEquals(expected, visible(store)); + + Map viaContext = new HashMap<>(); + store.forEach(viaContext, (map, entry) -> map.put(entry.tag(), entry.objectValue())); + assertEquals(expected, viaContext); + + Map viaTwoContexts = new HashMap<>(); + store.forEach( + viaTwoContexts, "-", (map, suffix, entry) -> map.put(entry.tag(), entry.objectValue())); + assertEquals(expected, viaTwoContexts); + + Map viaCursor = new HashMap<>(); + IdStore.Cursor cursor = store.cursor(); + for (EntryReader entry = cursor.next(); entry != null; entry = cursor.next()) { + assertNull(viaCursor.put(entry.tag(), entry.objectValue()), entry.tag()); + } + assertEquals(expected, viaCursor); + } + + @ParameterizedTest + @MethodSource("stores") + void aCopyIsIndependent(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.remove(KnownTags.HTTP_ROUTE_ID); + + IdStore copy = store.copy(); + copy.set(KnownTags.HTTP_METHOD_ID, "POST"); + copy.set(KnownTags.HTTP_ROUTE_ID, "/copy"); + + assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID), "the original keeps its tombstone"); + assertEquals("POST", copy.getValue(KnownTags.HTTP_METHOD_ID)); + assertEquals("/copy", copy.getValue(KnownTags.HTTP_ROUTE_ID)); + copy.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void putAllLocalMergesAnotherStoresTags(Function newStore) { + IdStore source = newStore.apply(null); + source.set(KnownTags.HTTP_ROUTE_ID, "/a"); + source.set(KnownTags.HTTP_METHOD_ID, "GET"); + + IdStore empty = newStore.apply(null); + empty.putAllLocal(source); // nothing here yet + assertEquals(visible(source), visible(empty)); + + IdStore merged = newStore.apply(null); + merged.set(KnownTags.HTTP_METHOD_ID, "POST"); + merged.set(KnownTags.COMPONENT_ID, "okhttp"); + merged.putAllLocal(source); + assertEquals("GET", merged.getValue(KnownTags.HTTP_METHOD_ID), "the source wins"); + assertEquals("okhttp", merged.getValue(KnownTags.COMPONENT_ID)); + assertEquals(3, merged.size()); + + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore tombstoned = newStore.apply(parent); + tombstoned.remove(KnownTags.HTTP_ROUTE_ID); + tombstoned.putAllLocal(source); // replaces the tombstone + assertEquals("/a", tombstoned.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(2, tombstoned.size()); + + newStore.apply(null).putAllLocal(newStore.apply(null)); // an empty source is a no-op + merged.checkIntegrity(); + tombstoned.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void fillsCopyTheLocalTags(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + store.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + + Map map = new HashMap<>(); + store.fillMapLocal(map); + assertEquals("/a", map.get("http.route")); + assertEquals(200, map.get("http.status_code")); + + Map strings = new HashMap<>(); + store.fillStringMapLocal(strings); + assertEquals("200", strings.get("http.status_code")); + } + + @ParameterizedTest + @MethodSource("stores") + void growsPastItsFirstCapacity(Function newStore) throws Exception { + IdStore store = newStore.apply(null); + List ids = knownIds(); + int n = Math.min(40, ids.size()); + for (int i = 0; i < n; ++i) { + store.set(ids.get(i), "v" + i); + } + for (int i = 0; i < n; ++i) { + assertEquals("v" + i, store.getValue(ids.get(i))); + } + assertEquals(n, store.size()); + assertTrue(store.computeSize() > 0); + assertNotNull(store.toInternalString()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void putAllLocalMergesOverlappingStoresWithCollidingTags(Function newStore) + throws Exception { + // enough tags that buckets hold groups of colliding entries on both sides of the merge + List ids = knownIds(); + IdStore target = newStore.apply(null); + IdStore source = newStore.apply(null); + for (int i = 0; i < 30; ++i) { + target.set(ids.get(i), "target" + i); + } + for (int i = 20; i < 50; ++i) { + source.set(ids.get(i), "source" + i); + } + target.putAllLocal(source); + assertEquals(50, target.size()); + for (int i = 0; i < 50; ++i) { + assertEquals(i < 20 ? "target" + i : "source" + i, target.getValue(ids.get(i))); + } + target.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void getAndPutRevivesARemovedInheritedTag(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + store.remove(KnownTags.HTTP_ROUTE_ID); + + // hidden, so there is no visible prior value + assertNull(store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/revived"))); + assertEquals("/revived", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(1, store.size()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void readsGoThroughEveryAncestor(Function newStore) { + IdStore grandparent = newStore.apply(null); + grandparent.set(KnownTags.HTTP_ROUTE_ID, "/grandparent"); + grandparent.set(KnownTags.COMPONENT_ID, "okhttp"); + IdStore parent = newStore.apply(grandparent); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.remove(KnownTags.COMPONENT_ID); + IdStore store = newStore.apply(parent); + + assertEquals("/grandparent", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + assertNull(store.getValue(KnownTags.COMPONENT_ID), "hidden by the parent"); + assertEquals(2, store.size()); + Map expected = new HashMap<>(); + expected.put("http.route", "/grandparent"); + expected.put("http.method", "GET"); + assertEquals(expected, visible(store)); + } + + @ParameterizedTest + @MethodSource("stores") + void anUnknownIdIsRejected(Function newStore) { + IdStore store = newStore.apply(null); + long unknown = KnownTagCodec.makeTagId(9999); + assertThrows(IllegalArgumentException.class, () -> store.getValue(unknown)); + assertThrows(IllegalArgumentException.class, () -> store.remove(unknown)); + } +} From ac96cd715574b46bad7b18a22f105802b0d344af Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 12:08:18 -0400 Subject: [PATCH 95/99] Keep the no-entry reader no-op, and make makeTagId the real id - A reader whose entry() is null (only possible from an EntryReader outside the tracer) made the span's direct store routes throw, where TagMap.set treats it as a no-op. HashIdStore.put/getAndPut and the custom branch of routePut now skip it; DenseIdStore, which never builds an Entry, skips a reader with nothing to store. Tested on both stores and through DDSpanContext.setTag / setMetric. - makeTagId(serial) built only serial << 48, without slot and flag bits: for a slotted tag that id passed isKnown but differed from the KnownTags constant, so a hash store filed it under the partial id. It now returns the real id from the generated serial-to-id table; a serial outside the known range still gets a serial-only id that isn't known. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 5 +- .../trace/core/DDSpanContextStoresTest.java | 109 ++++++++++++++++++ .../java/datadog/trace/api/DenseIdStore.java | 9 +- .../java/datadog/trace/api/HashIdStore.java | 6 + .../java/datadog/trace/api/KnownTagCodec.java | 10 +- .../trace/api/IdStoreContractTest.java | 104 +++++++++++++++++ .../trace/api/KnownTagCodecSlotTest.java | 10 ++ 7 files changed, 246 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 5f5bace0f7d..95d8de1eaee 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 @@ -1204,7 +1204,10 @@ private void routePut(TagMap.EntryReader entryReader) { if (entryReader.tagId() != 0) { knownTags.put(entryReader); // no Entry built for a dense store, or for a flyweight from one } else { - customTagsForWrite().put(entryReader.entry()); + TagMap.Entry entry = entryReader.entry(); + if (entry != null) { // a reader with no entry is a no-op, as for TagMap.set + customTagsForWrite().put(entry); + } } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java index a04c3e6fa4f..d098d638517 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextStoresTest.java @@ -40,6 +40,24 @@ private DDSpan start() { return (DDSpan) tracer.startSpan("test-instr", "stores.op"); } + @Test + void aReaderWithNoEntryIsANoOp() { + DDSpan span = start(); + try { + span.setTag("http.route", "/users"); + span.setTag("custom.kept", "kept"); + span.spanContext().setTag(new NoEntryReader("http.route", KnownTags.HTTP_ROUTE_ID)); + span.spanContext().setTag(new NoEntryReader("custom.kept", 0L)); + span.spanContext().setMetric(new NoEntryReader("custom.metric", 0L)); + + assertEquals("/users", span.getTag("http.route")); + assertEquals("kept", span.getTag("custom.kept")); + assertNull(span.getTag("custom.metric")); + } finally { + span.finish(); + } + } + @Test void customTagsKeepEveryValueType() { DDSpan span = start(); @@ -185,4 +203,95 @@ void aPrototypesCustomTagIsSeededOnlyWhereAbsent() { span.finish(); } } + + /** An EntryReader with no entry and no value, as a caller outside the tracer may pass. */ + static final class NoEntryReader implements datadog.trace.api.TagMap.EntryReader { + private final String tag; + private final long tagId; + + NoEntryReader(String tag, long tagId) { + this.tag = tag; + this.tagId = tagId; + } + + @Override + public String tag() { + return tag; + } + + @Override + public long tagId() { + return tagId; + } + + @Override + public byte type() { + return datadog.trace.api.TagMap.EntryReader.OBJECT; + } + + @Override + public boolean is(byte type) { + return false; + } + + @Override + public boolean isNumericPrimitive() { + return false; + } + + @Override + public boolean isNumber() { + return false; + } + + @Override + public boolean isObject() { + return true; + } + + @Override + public Object objectValue() { + return null; + } + + @Override + public String stringValue() { + return null; + } + + @Override + public boolean booleanValue() { + return false; + } + + @Override + public int intValue() { + return 0; + } + + @Override + public long longValue() { + return 0; + } + + @Override + public float floatValue() { + return 0; + } + + @Override + public double doubleValue() { + return 0; + } + + @Override + public java.util.Map.Entry mapEntry() { + return null; + } + + @Override + public datadog.trace.api.TagMap.Entry entry() { + return null; + } + } } diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index 8c6d0cdb2c6..0c39ed6d3d8 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -251,12 +251,19 @@ public void set(long tagId, double value) { @Override public EntryReader put(EntryReader entryReader) { + Object value = entryReader.objectValue(); + if (value == null) { + return null; // a reader with nothing to store is a no-op, as for TagMap.set + } long tagId = entryReader.tagId(); - return this.readerOf(tagId, this.store(tagId, entryReader.objectValue())); + return this.readerOf(tagId, this.store(tagId, value)); } @Override public EntryReader getAndPut(EntryReader entryReader) { + if (entryReader.objectValue() == null) { + return null; // a reader with nothing to store is a no-op + } long tagId = entryReader.tagId(); char serial = (char) KnownTagCodec.serialNum(tagId); long bit = slotBit(tagId); diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 7d623249618..2922e653686 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -141,6 +141,9 @@ public void set(long tagId, double value) { @Override public Entry put(EntryReader entryReader) { Entry entry = entryReader.entry(); + if (entry == null) { + return null; // a reader with no entry is a no-op, as for TagMap.set + } Entry replaced = HashSupport.put(this.buckets, entry); this.markLocal(slotBit(entry.tagHash), levelOf(entry.tagHash)); if (replaced == null || replaced.tombstone) { @@ -153,6 +156,9 @@ public Entry put(EntryReader entryReader) { @Override public Entry getAndPut(EntryReader entryReader) { Entry entry = entryReader.entry(); + if (entry == null) { + return null; // a reader with no entry is a no-op + } long tagId = entry.tagHash; long bit = slotBit(tagId); int level = levelOf(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 acd6dfadd11..3439bc88c48 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -105,13 +105,13 @@ public static long traceLevel(long tagId) { } /** - * Builds an id carrying only {@code serialNum}: slot 0 and no flags, so it equals a tag's real id - * only when that tag has neither. Enough for serial-keyed lookups such as {@link #nameOf}; - * compare against a {@code KnownTags} constant, never against this. Inverse of {@link - * #serialNum}. Intended for tests and serial-keyed lookups. + * The id of the known tag with {@code serialNum}, slot and flag bits included, so it equals the + * tag's {@code KnownTags} constant and stores and looks up exactly like it. A serial outside the + * known range gets an id carrying only the serial, which is not a known tag. Inverse of {@link + * #serialNum}. */ public static long makeTagId(int serialNum) { - return (long) serialNum << 48; + return serialNum > 0 && serialNum < serialLimit() ? idOf(serialNum) : (long) serialNum << 48; } /* diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java index bd18c6cf5de..3f0018c9e04 100644 --- a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -367,6 +367,19 @@ void readsGoThroughEveryAncestor(Function newStore) { assertEquals(expected, visible(store)); } + @ParameterizedTest + @MethodSource("stores") + void aReaderWithNoEntryIsANoOp(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + NoEntryReader empty = new NoEntryReader("http.route", KnownTags.HTTP_ROUTE_ID); + + assertNull(store.put(empty)); + assertNull(store.getAndPut(empty)); + assertEquals("/a", store.getValue(KnownTags.HTTP_ROUTE_ID), "left as it was"); + assertEquals(1, store.size()); + } + @ParameterizedTest @MethodSource("stores") void anUnknownIdIsRejected(Function newStore) { @@ -375,4 +388,95 @@ void anUnknownIdIsRejected(Function newStore) { assertThrows(IllegalArgumentException.class, () -> store.getValue(unknown)); assertThrows(IllegalArgumentException.class, () -> store.remove(unknown)); } + + /** An EntryReader with no entry and no value, as a caller outside the tracer may pass. */ + static final class NoEntryReader implements TagMap.EntryReader { + private final String tag; + private final long tagId; + + NoEntryReader(String tag, long tagId) { + this.tag = tag; + this.tagId = tagId; + } + + @Override + public String tag() { + return tag; + } + + @Override + public long tagId() { + return tagId; + } + + @Override + public byte type() { + return TagMap.EntryReader.OBJECT; + } + + @Override + public boolean is(byte type) { + return false; + } + + @Override + public boolean isNumericPrimitive() { + return false; + } + + @Override + public boolean isNumber() { + return false; + } + + @Override + public boolean isObject() { + return true; + } + + @Override + public Object objectValue() { + return null; + } + + @Override + public String stringValue() { + return null; + } + + @Override + public boolean booleanValue() { + return false; + } + + @Override + public int intValue() { + return 0; + } + + @Override + public long longValue() { + return 0; + } + + @Override + public float floatValue() { + return 0; + } + + @Override + public double doubleValue() { + return 0; + } + + @Override + public java.util.Map.Entry mapEntry() { + return null; + } + + @Override + public TagMap.Entry entry() { + return null; + } + } } diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java index ded02b397e5..34c240757c8 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -81,4 +81,14 @@ void anUnslottedTagSharesTheReservedTopBit() { assertEquals(1L << 63, 1L << KnownTagCodec.slotOf(KnownTags.RESOURCE_NAME_ID)); assertTrue(KnownTagCodec.slotCount() <= 63, "real slots must stay below the shared bit 63"); } + + @Test + void makeTagIdBuildsTheRealIdOfAKnownTag() { + // a slotted, intercepted tag: its constant carries slot and flag bits beyond the serial + long id = KnownTags.HTTP_STATUS_CODE_ID; + assertEquals(id, KnownTagCodec.makeTagId(KnownTagCodec.serialNum(id))); + + long unknown = KnownTagCodec.makeTagId(KnownTagCodec.serialLimit() + 1); + assertFalse(KnownTagCodec.isKnown(unknown)); + } } From eee2c9c36e330aaedb555212078d82e009465e25 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 13:06:33 -0400 Subject: [PATCH 96/99] Give point reads their own Entry instead of the store's shared reader DenseIdStore answered every point read with one reader cached on the store. Frozen stores are shared between threads (a span prototype's tags, the tracer-level tags), so concurrent TagMap.getEntry calls could overwrite each other's reader before copying it: a new test reproduces it, one thread reading _dd.base_service as _dd.git.commit.sha. - IdStore.getEntry(long) returns an Entry the caller may keep and shares no state: HashIdStore returns its stored Entry (getEntryReader now delegates to it), DenseIdStore builds one from the value. TagMap.getEntry uses it. - DenseIdStore.getEntryReader returns a fresh reader per call. - The store's reused reader now only answers writes' prior values: a store being written has a single writer, while a shared frozen store is only read. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseIdStore.java | 34 +++++++-- .../java/datadog/trace/api/HashIdStore.java | 12 ++- .../main/java/datadog/trace/api/IdStore.java | 10 +++ .../main/java/datadog/trace/api/TagMap.java | 4 +- .../api/DenseIdStoreConcurrentReadTest.java | 73 +++++++++++++++++++ .../trace/api/IdStoreContractTest.java | 24 ++++++ 6 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/DenseIdStoreConcurrentReadTest.java diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index 0c39ed6d3d8..ff439d7a502 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -15,9 +15,11 @@ * out. The full id is rebuilt from the serial ({@link KnownTagCodec#idOf}) only where a reader asks * for it. Values are boxed: a primitive set by id is stored as its wrapper. * - *

Point reads that need only the value ({@link #getValue}) build nothing. Those that return an - * {@link EntryReader} answer with this store's one reused flyweight, valid until the store's next - * operation (see {@link IdStore}). + *

Point reads that need only the value ({@link #getValue}) build nothing. Those that return a + * reader or an Entry build their own ({@link #getEntryReader}, {@link #getEntry}), so concurrent + * reads of a frozen store shared between threads never share state. Only writes answer their prior + * value with this store's one reused flyweight, valid until the store's next operation (see {@link + * IdStore}). * *

Removing a tag that hides nothing inherited swaps the last slot into its place. Removing a tag * the parent chain also holds stores the shared {@link #TOMBSTONE} as its value instead, and sets @@ -179,7 +181,11 @@ public EntryReader next() { /** The frozen parent's id store, or null. */ final DenseIdStore parent; - /** The reused flyweight point reads answer with; built on first use. */ + /** + * The reused flyweight that writes ({@code put}, {@code getAndPut}, {@code remove}) answer their + * prior value with; built on first use. Only writes use it: a store being written has a single + * writer, while a frozen store that threads share is only read, and reads build their own. + */ private DenseReader reader; DenseIdStore(IdStore parent) { @@ -187,9 +193,25 @@ public EntryReader next() { this.parent = (DenseIdStore) parent; } + /** + * A fresh reader per call, never the store's reused one: point reads can come from several + * threads at once on a frozen store they share. A short-lived reader is usually scalar-replaced. + */ @Override public EntryReader getEntryReader(long tagId) { - return this.readerOf(tagId, this.getValue(tagId)); + Object value = this.getValue(tagId); + if (value == null) { + return null; + } + DenseReader reader = new DenseReader(); + reader.set(tagId, value); + return reader; + } + + @Override + public TagMap.Entry getEntry(long tagId) { + Object value = this.getValue(tagId); + return value == null ? null : TagMap.Entry.newAnyEntry(tagId, value); } @Override @@ -396,7 +418,7 @@ private void grow() { this.values = Arrays.copyOf(this.values, newCap); } - /** The flyweight over {@code value}, or null when there is none. */ + /** The reused flyweight over a write's prior {@code value}, or null when there is none. */ private EntryReader readerOf(long tagId, Object value) { if (value == null) { return null; diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 2922e653686..879d642aac6 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -79,14 +79,20 @@ private HashIdStore(Object[] buckets) { } @Override - public Entry getEntryReader(long tagId) { + public Entry getEntry(long tagId) { if (!KnownTagCodec.isKnown(tagId)) { throw notKnown(tagId); } return this.getVisible(tagId, slotBit(tagId), levelOf(tagId)); } - /** {@link #getEntryReader} for a validated id and its slot bit and level. */ + /** The stored Entry is itself a stable reader, so this is {@link #getEntry}. */ + @Override + public Entry getEntryReader(long tagId) { + return this.getEntry(tagId); + } + + /** {@link #getEntry} for a validated id and its slot bit and level. */ private Entry getVisible(long tagId, long bit, int level) { if (this.mayHaveLocal(bit, level)) { Entry local = HashSupport.findById(this.buckets, Entry.bucketHash(tagId), tagId); @@ -99,7 +105,7 @@ private Entry getVisible(long tagId, long bit, int level) { @Override public Object getValue(long tagId) { - Entry entry = this.getEntryReader(tagId); + Entry entry = this.getEntry(tagId); return entry == null ? null : entry.objectValue(); } diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 4854cfff427..23fc41367ce 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -149,6 +149,16 @@ static IllegalArgumentException notKnown(long tagId) { */ public abstract EntryReader getEntryReader(long tagId); + /** + * The entry for the known tag {@code tagId}, reading through to the parent chain, or null. Unlike + * {@link #getEntryReader}, the result is an {@link TagMap.Entry} the caller may keep, and it + * shares no state with the store, so it is safe to call concurrently on a frozen store that + * several threads read, such as a span prototype's or the tracer-level tags. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + public abstract TagMap.Entry getEntry(long tagId); + /** * {@link #getEntryReader}'s value, boxed if necessary, or null -- for a caller that needs only * the value, so a store that keeps known tags without an Entry per tag can answer without one. 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 1d588cfb98e..4487122db53 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1485,7 +1485,7 @@ public Entry getEntry(String tag) { // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. long key = KnownTagCodec.keyOf(tag); if (key != 0) { - return entryOf(this.knownStore.getEntryReader(key)); + return this.knownStore.getEntry(key); } HashStore nameStore = this.nameStore; return nameStore == null ? null : nameStore.get(tag); @@ -1493,7 +1493,7 @@ public Entry getEntry(String tag) { /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ public Entry getEntry(long tagId) { - return entryOf(this.knownStore.getEntryReader(tagId)); + return this.knownStore.getEntry(tagId); } @Deprecated diff --git a/internal-api/src/test/java/datadog/trace/api/DenseIdStoreConcurrentReadTest.java b/internal-api/src/test/java/datadog/trace/api/DenseIdStoreConcurrentReadTest.java new file mode 100644 index 00000000000..c076bf34725 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/DenseIdStoreConcurrentReadTest.java @@ -0,0 +1,73 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +/** + * Point reads of a frozen dense store that several threads share -- a span prototype's tags, the + * tracer-level tags -- must not share state: each read builds its own reader or Entry. + */ +class DenseIdStoreConcurrentReadTest { + + @Test + void concurrentPointReadsOfASharedStoreKeepTheirOwnValues() throws Exception { + List ids = new ArrayList<>(); + for (java.lang.reflect.Field field : KnownTags.class.getFields()) { + if (field.getType() == long.class && ids.size() < 12) { + long id = field.getLong(null); + if (KnownTagCodec.isKnown(id) && !ids.contains(id)) { + ids.add(id); + } + } + } + DenseIdStore store = new DenseIdStore(null); + for (long id : ids) { + store.set(id, KnownTagCodec.nameOf(id)); // each value names its own tag + } + TagMap shared = TagMap.viewOf(store, null, false); + + int threads = 8; + CountDownLatch start = new CountDownLatch(1); + AtomicReference mismatch = new AtomicReference<>(); + List readers = new ArrayList<>(); + for (int t = 0; t < threads; ++t) { + final int offset = t; + Thread reader = + new Thread( + () -> { + try { + start.await(); + } catch (InterruptedException e) { + return; + } + for (int i = 0; i < 20_000 && mismatch.get() == null; ++i) { + long id = ids.get((i + offset) % ids.size()); + String name = KnownTagCodec.nameOf(id); + TagMap.Entry entry = shared.getEntry(id); + TagMap.EntryReader viaReader = store.getEntryReader(id); + if (!name.equals(entry.tag()) || !name.equals(entry.objectValue())) { + mismatch.set(name + " read as " + entry.tag() + "=" + entry.objectValue()); + } else if (!name.equals(viaReader.tag()) + || !name.equals(viaReader.objectValue())) { + mismatch.set( + name + " read as " + viaReader.tag() + "=" + viaReader.objectValue()); + } + } + }); + readers.add(reader); + reader.start(); + } + start.countDown(); + for (Thread reader : readers) { + reader.join(30_000); + assertTrue(!reader.isAlive(), "reader finished"); + } + assertEquals(null, mismatch.get()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java index 3f0018c9e04..b7f9dcd6267 100644 --- a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -380,6 +380,30 @@ void aReaderWithNoEntryIsANoOp(Function newStore) { assertEquals(1, store.size()); } + @ParameterizedTest + @MethodSource("stores") + void getEntryReturnsAnEntryTheCallerMayKeep(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + + TagMap.Entry method = store.getEntry(KnownTags.HTTP_METHOD_ID); + TagMap.Entry route = store.getEntry(KnownTags.HTTP_ROUTE_ID); + EntryReader methodReader = store.getEntryReader(KnownTags.HTTP_METHOD_ID); + EntryReader routeReader = store.getEntryReader(KnownTags.HTTP_ROUTE_ID); + + // kept side by side, each still holds its own tag + assertEquals("GET", method.objectValue()); + assertEquals(KnownTags.HTTP_METHOD_ID, method.tagId()); + assertEquals("/inherited", route.objectValue()); + assertEquals("GET", methodReader.objectValue()); + assertEquals("/inherited", routeReader.objectValue()); + assertNull(store.getEntry(KnownTags.COMPONENT_ID)); + assertThrows( + IllegalArgumentException.class, () -> store.getEntry(KnownTagCodec.makeTagId(9999))); + } + @ParameterizedTest @MethodSource("stores") void anUnknownIdIsRejected(Function newStore) { From 018a273ae9b469be88fc06341d6bca78e596cc89 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 13:32:11 -0400 Subject: [PATCH 97/99] Read known tag values straight from the store in getObject and getString TagMap.getObject and getString went through getEntry, so on the dense store every value read, by the serializer or a post-processor, built an Entry only to unwrap it. A known tag is now read with IdStore.getValue / getString, which build nothing on the dense store. IdStore.getString is new so the hash store can keep answering with its Entry's cached string: a numeric tag the serializer reads repeatedly is still formatted once. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseIdStore.java | 6 +++ .../java/datadog/trace/api/HashIdStore.java | 6 +++ .../main/java/datadog/trace/api/IdStore.java | 7 ++++ .../main/java/datadog/trace/api/TagMap.java | 24 +++++++++-- .../trace/api/IdStoreContractTest.java | 40 +++++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java index ff439d7a502..6d2d35f5be6 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseIdStore.java @@ -222,6 +222,12 @@ public Object getValue(long tagId) { return this.getVisible((char) KnownTagCodec.serialNum(tagId), slotBit(tagId), levelOf(tagId)); } + @Override + public String getString(long tagId) { + Object value = this.getValue(tagId); + return value == null ? null : value.toString(); + } + /** * The visible value of the tag {@code serial} in the slot {@code bit} of {@code level}, or null. */ diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 879d642aac6..32c065fa8b2 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -109,6 +109,12 @@ public Object getValue(long tagId) { return entry == null ? null : entry.objectValue(); } + @Override + public String getString(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.stringValue(); + } + @Override public void set(long tagId, Object value) { this.put(Entry.newAnyEntry(tagId, value)); diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index 23fc41367ce..fec64355c51 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -165,6 +165,13 @@ static IllegalArgumentException notKnown(long tagId) { */ public abstract Object getValue(long tagId); + /** + * {@link #getValue} as a String, or null. A store that keeps an Entry per tag answers with the + * Entry's cached string, so a numeric tag read repeatedly, as the serializer does, is formatted + * once. + */ + public abstract String getString(long tagId); + /* * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. */ 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 4487122db53..7d835273267 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1384,15 +1384,31 @@ public Object get(Object tag) { return this.getObject((String) tag); } - /** Provides the corresponding entry value as an Object - boxing if necessary */ + /** + * Provides the corresponding entry value as an Object - boxing if necessary. A known tag is read + * straight from its store, so a store without an Entry per tag builds none. + */ public Object getObject(String tag) { - Entry entry = this.getEntry(tag); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getValue(key); + } + HashStore nameStore = this.nameStore; + Entry entry = nameStore == null ? null : nameStore.get(tag); return entry == null ? null : entry.objectValue(); } - /** Provides the corresponding entry value as a String - calling toString if necessary */ + /** + * Provides the corresponding entry value as a String - calling toString if necessary. Like {@link + * #getObject}, a known tag is read straight from its store. + */ public String getString(String tag) { - Entry entry = this.getEntry(tag); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getString(key); + } + HashStore nameStore = this.nameStore; + Entry entry = nameStore == null ? null : nameStore.get(tag); return entry == null ? null : entry.stringValue(); } diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java index b7f9dcd6267..23ae03d9de8 100644 --- a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -367,6 +367,46 @@ void readsGoThroughEveryAncestor(Function newStore) { assertEquals(expected, visible(store)); } + @ParameterizedTest + @MethodSource("stores") + void getStringFormatsTheVisibleValue(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.set(KnownTags.COMPONENT_ID, "hidden"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.PEER_PORT_ID, 5432); + store.set(KnownTags.DB_INSTANCE_ID, 1.5d); + store.remove(KnownTags.COMPONENT_ID); + + assertEquals("5432", store.getString(KnownTags.PEER_PORT_ID)); + assertEquals("1.5", store.getString(KnownTags.DB_INSTANCE_ID)); + assertEquals("/inherited", store.getString(KnownTags.HTTP_ROUTE_ID)); + assertNull(store.getString(KnownTags.COMPONENT_ID), "hidden by the removal"); + assertNull(store.getString(KnownTags.HTTP_METHOD_ID)); + assertThrows( + IllegalArgumentException.class, () -> store.getString(KnownTagCodec.makeTagId(9999))); + } + + @ParameterizedTest + @MethodSource("stores") + void aViewReadsValuesFromEitherStore(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + store.set(KnownTags.PEER_PORT_ID, 5432); + HashStore names = HashStore.create(null); + names.set("my.tag", 7); + + TagMap view = TagMap.viewOf(store, names, false); + assertEquals("/a", view.getObject(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(5432, view.getObject("peer.port")); + assertEquals("5432", view.getString("peer.port")); + assertEquals(7, view.getObject("my.tag")); + assertEquals("7", view.getString("my.tag")); + assertNull(view.getObject(KnownTags.HTTP_METHOD_NAME)); + assertNull(view.getString("other.tag")); + assertNull(TagMap.viewOf(store, null, false).getString("my.tag"), "no custom store"); + } + @ParameterizedTest @MethodSource("stores") void aReaderWithNoEntryIsANoOp(Function newStore) { From 33badb141c70e6d22dbd13056ef2341ab2bca53f Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 13:49:12 -0400 Subject: [PATCH 98/99] Add TagMap.getEntryReader and route the primitive getters through it getBoolean/getInt/getLong/getFloat/getDouble (and their OrDefault forms) and containsKey went through getEntry, so on the dense store each read built an Entry only to unwrap it. They now read through getEntryReader, whose short-lived reader is usually scalar-replaced. getEntryReader is public and @NoEscape: retaining its reader is wasteful but not wrong, since neither store's reader goes stale (the hash store returns its immutable Entry, the dense store a fresh reader holding the value). Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 45 ++++++++++++++----- .../trace/api/IdStoreContractTest.java | 31 +++++++++++++ 2 files changed, 65 insertions(+), 11 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 7d835273267..8adb259badb 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; @@ -1417,8 +1418,8 @@ public boolean getBoolean(String tag) { } public boolean getBooleanOrDefault(String tag, boolean defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.booleanValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.booleanValue(); } public int getInt(String tag) { @@ -1426,8 +1427,8 @@ public int getInt(String tag) { } public int getIntOrDefault(String tag, int defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.intValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.intValue(); } public long getLong(String tag) { @@ -1435,8 +1436,8 @@ public long getLong(String tag) { } public long getLongOrDefault(String tag, long defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.longValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.longValue(); } public float getFloat(String tag) { @@ -1444,8 +1445,8 @@ public float getFloat(String tag) { } public float getFloatOrDefault(String tag, float defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.floatValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.floatValue(); } public double getDouble(String tag) { @@ -1453,15 +1454,15 @@ public double getDouble(String tag) { } public double getDoubleOrDefault(String tag, double defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.doubleValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.doubleValue(); } @Override public boolean containsKey(Object key) { if (!(key instanceof String)) return false; - return (this.getEntry((String) key) != null); + return (this.getEntryReader((String) key) != null); } @Override @@ -1512,6 +1513,28 @@ public Entry getEntry(long tagId) { return this.knownStore.getEntry(tagId); } + /** + * Like {@link #getEntry(String)}, but answers with a reader to read and drop rather than keep, so + * a store without an Entry per tag builds none: a reader that is not retained is usually + * scalar-replaced. Call {@link EntryReader#entry()} to keep one. Safe on a frozen map that + * several threads read. + */ + @NoEscape + public EntryReader getEntryReader(String tag) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getEntryReader(key); + } + HashStore nameStore = this.nameStore; + return nameStore == null ? null : nameStore.get(tag); + } + + /** {@link #getEntryReader(String)} for a known tag id. */ + @NoEscape + public EntryReader getEntryReader(long tagId) { + return this.knownStore.getEntryReader(tagId); + } + @Deprecated @Override public Object put(@Nonnull String tag, Object value) { diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java index 23ae03d9de8..64c7f606b48 100644 --- a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -407,6 +407,37 @@ void aViewReadsValuesFromEitherStore(Function newStore) { assertNull(TagMap.viewOf(store, null, false).getString("my.tag"), "no custom store"); } + @ParameterizedTest + @MethodSource("stores") + void aViewReadsPrimitivesThroughAReader(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.PEER_PORT_ID, 5432); + store.set(KnownTags.DB_INSTANCE_ID, 1.5d); + store.set(KnownTags.DD_MEASURED_ID, true); + HashStore names = HashStore.create(null); + names.set("my.count", 7L); + + TagMap view = TagMap.viewOf(store, names, false); + assertEquals(5432, view.getInt("peer.port")); + assertEquals(5432L, view.getLong("peer.port")); + assertEquals(1.5d, view.getDouble("db.instance")); + assertEquals(1.5f, view.getFloat("db.instance")); + assertTrue(view.getBoolean(KnownTags.DD_MEASURED_NAME)); + assertEquals(7L, view.getLong("my.count")); + assertEquals(-1, view.getIntOrDefault(KnownTags.HTTP_METHOD_NAME, -1)); + assertEquals(-1L, view.getLongOrDefault("other.tag", -1L)); + assertTrue(view.containsKey("peer.port")); + assertTrue(view.containsKey("my.count")); + assertFalse(view.containsKey("other.tag")); + + EntryReader port = view.getEntryReader(KnownTags.PEER_PORT_ID); + assertEquals(5432, port.intValue()); + assertEquals(KnownTags.PEER_PORT_ID, port.tagId()); + assertEquals(7L, view.getEntryReader("my.count").longValue()); + assertNull(view.getEntryReader(KnownTags.HTTP_METHOD_ID)); + assertNull(TagMap.viewOf(store, null, false).getEntryReader("my.count"), "no custom store"); + } + @ParameterizedTest @MethodSource("stores") void aReaderWithNoEntryIsANoOp(Function newStore) { From f7cdf1cb97a5eda5ea22a1f37665c3a6ddde55bd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 20 Oct 2026 14:01:54 -0400 Subject: [PATCH 99/99] State EntryReader's momentary contract as a MUST, not a NoEscape SHOULD An EntryReader is a momentary view: a store may answer with a flyweight it reuses, so a kept reader can later report another tag. That makes keeping one a correctness bug whatever today's stores happen to return, so the contract goes on the EntryReader type, and TagMap.getEntryReader drops @NoEscape, whose SHOULD would invite justified exceptions. The type is to be marked @ScopeBounded (APMLP-2086). Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagMap.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 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 8adb259badb..37733e11c7e 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1,7 +1,6 @@ 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; @@ -140,6 +139,13 @@ public boolean isRemoval() { } } + /** + * A momentary view of one tag: read it where it was obtained and drop it. A reader MUST NOT be + * kept past the call that produced it -- not in a field, a collection or a later step. A store + * may answer with a flyweight it reuses for the next read or write, so a kept reader can later + * report another tag's value. To keep a tag, call {@link #entry()}: an {@link Entry} is the + * keepable form. (To be marked {@code @ScopeBounded}, APMLP-2086.) + */ public interface EntryReader { public static final byte OBJECT = 1; @@ -1514,12 +1520,10 @@ public Entry getEntry(long tagId) { } /** - * Like {@link #getEntry(String)}, but answers with a reader to read and drop rather than keep, so - * a store without an Entry per tag builds none: a reader that is not retained is usually - * scalar-replaced. Call {@link EntryReader#entry()} to keep one. Safe on a frozen map that - * several threads read. + * Like {@link #getEntry(String)}, but answers with a momentary {@link EntryReader}, so a store + * without an Entry per tag builds none. The reader MUST NOT be kept (see {@link EntryReader}); + * call {@link EntryReader#entry()} to keep one. Safe on a frozen map that several threads read. */ - @NoEscape public EntryReader getEntryReader(String tag) { long key = KnownTagCodec.keyOf(tag); if (key != 0) { @@ -1530,7 +1534,6 @@ public EntryReader getEntryReader(String tag) { } /** {@link #getEntryReader(String)} for a known tag id. */ - @NoEscape public EntryReader getEntryReader(long tagId) { return this.knownStore.getEntryReader(tagId); }