From eabce403e8e17a8a3ea6022339e57b55a401971b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 09:31:13 -0400 Subject: [PATCH 01/17] Bucket known tags by slot in TagMap TagMap bucketed a known tag by folding its serial, so which tags shared one of the 16 buckets was luck, and each shared bucket costs a BucketGroup (48 B). Slot coloring already gives tags that appear together on a span type distinct slots, so bucketing a known tag by its slot keeps them in distinct buckets more often. Trace-level tags reuse span-level slot numbers, so the level bit moves them to the other half; known tags without a slot and custom tags keep their hashes. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 23 +++++++++++++++---- 1 file changed, 18 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 be07fadbeeb..0b42cb3f9d2 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -497,14 +497,27 @@ static long customHash(String tag) { } /** - * 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. + * Folds a tag hash to the 32-bit bucket hash, whose low bits pick the bucket. * - *

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

A known tag with a slot is bucketed by its slot: slot coloring gives tags that appear + * together on a span type distinct slots, so they mostly land in distinct buckets and need no + * BucketGroup. Trace-level tags reuse the span-level slot numbers, so their level bit moves + * them to the other half of the buckets. The serial sits above the slot, so two known tags + * never share a bucket hash. + * + *

A known tag without a slot folds its serial (bits 63-48) down instead, and a custom tag + * uses its name hash. + * + *

Never zero: BucketGroup treats a zero hash as a vacant slot. A slotted known tag's hash + * carries its nonzero serial; the other folds map zero to the same nonzero sentinel {@link + * #_hash} uses. */ static int bucketHash(long tagHash) { + int slot = (int) (tagHash >>> 32) & 0xFFFF; + if ((tagHash >>> 32) != 0 && slot != KnownTagCodec.NO_SLOT) { + int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: + 8 + return ((int) (tagHash >>> 48) << 16) | (slot ^ level); + } int hash = (int) (tagHash >>> 48) ^ (int) tagHash; return hash == 0 ? 0xDD06 : hash; } From 19b2e886c3774ab874b8b07c7ac45925519ceb86 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 09:43:10 -0400 Subject: [PATCH 02/17] Add whole-span and whole-trace allocation benchmarks SpanScenarioBenchmark builds spans and traces the way today's decorators do (prototype in afterStart, then request or connection tags by name): bare spans, a server span, the server span via the builder, a jdbc span, and a server trace with 1, 5 or 20 jdbc children. TraceShapeBenchmark builds one web-request-shaped trace three ways: every tag by name, the decorators' calls, and the newest span APIs. Both finish what they start, so allocation per op covers whole spans and traces. Single-span results can hide per-child costs that only the trace scenarios show. TraceShapeBenchmark is the same file #12918 adds. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/SpanScenarioBenchmark.java | 219 ++++++++++++++ .../trace/core/TraceShapeBenchmark.java | 276 ++++++++++++++++++ 2 files changed, 495 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java new file mode 100644 index 00000000000..5030e61a15c --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java @@ -0,0 +1,219 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.ContextScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +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.Param; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Span and trace scenarios built the way today's decorators build them: a {@link SpanPrototype} + * applied in {@code afterStart}, then the request or connection tags by name. Each op finishes what + * it starts, so allocation per op covers the whole span or trace. + * + *

+ * + *

Single-span and multi-span results can move in opposite directions: a change can make a + * trace's root cheaper while charging every child, so read the {@code webServerTrace} rows too. + * Values such as the client port and the status fall outside {@code Integer}'s box cache on + * purpose. + * + *

Passing {@code -jvmArgsAppend} on the command line replaces the {@code @Fork} arguments below, + * including {@code -DTEST_LOG_LEVEL=warn}; keep it, or debug logging dominates the allocation. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class SpanScenarioBenchmark { + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); + static final CharSequence WEB = UTF8BytesString.create("web"); + static final String USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; + static final String METHOD = "GET"; + static final String URL = "http://localhost:8080/owners/42"; + static final String HOSTNAME = "localhost"; + static final String CLIENT_IP = "127.0.0.1"; + static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache + static final String ROUTE = "/owners/{ownerId}"; + static final String SERVER_RESOURCE_NAME = "GET /owners/{ownerId}"; + static final CharSequence SERVER_RESOURCE = UTF8BytesString.create(SERVER_RESOURCE_NAME); + static final int STATUS = 200; // outside the box cache + + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); + static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); + static final CharSequence JDBC_PREPARED_STATEMENT = + UTF8BytesString.create("java-jdbc-prepared_statement"); + static final CharSequence SQL = UTF8BytesString.create("sql"); + static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); + + static final String BARE_INSTRUMENTATION = "bench"; + static final CharSequence BARE_OPERATION = UTF8BytesString.create("bare.op"); + + /** The trace size, taken only by {@code webServerTrace} so the single-span scenarios run once. */ + @State(Scope.Benchmark) + public static class Shape { + @Param({"1", "5", "20"}) + int children; + } + + CoreTracer tracer; + SpanPrototype serverProto; + SpanPrototype jdbcProto; + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType(WEB) + .initComponentAndIntegration(SERVER_COMPONENT) + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType(SQL) + .initComponentAndIntegration(JAVA_JDBC) + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + @Benchmark + public void bareStartSpan() { + tracer.startSpan(BARE_INSTRUMENTATION, BARE_OPERATION).finish(); + } + + @Benchmark + public void bareBuildSpan() { + tracer.buildSpan(BARE_INSTRUMENTATION, BARE_OPERATION).start().finish(); + } + + @Benchmark + public void webServerSpan() { + AgentSpan server = startServer(); + server.setHttpStatusCode(STATUS); + server.finish(); + } + + @Benchmark + public void webServerSpanViaBuilder() { + AgentSpan server = + tracer + .buildSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION) + .withSpanType(WEB) + .withTag(Tags.COMPONENT, SERVER_COMPONENT) + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER) + .withTag("language", "jvm") + .withTag(Tags.HTTP_USER_AGENT, USER_AGENT) + .withTag(Tags.HTTP_METHOD, METHOD) + .withTag(Tags.HTTP_URL, URL) + .withTag(Tags.HTTP_HOSTNAME, HOSTNAME) + .withTag(Tags.PEER_HOST_IPV4, CLIENT_IP) + .withTag(Tags.PEER_PORT, CLIENT_PORT) + .withTag(Tags.HTTP_ROUTE, ROUTE) + .withResourceName(SERVER_RESOURCE_NAME) + .start(); + server.setHttpStatusCode(STATUS); + server.finish(); + } + + @Benchmark + public void jdbcClientSpan() { + jdbc().finish(); + } + + @Benchmark + public void webServerTrace(Shape shape) { + AgentSpan server = startServer(); + try (ContextScope serverScope = tracer.activateSpan(server)) { + for (int i = 0; i < shape.children; ++i) { + AgentSpan jdbc = jdbc(); + ContextScope jdbcScope = tracer.activateSpan(jdbc); + jdbcScope.close(); + jdbc.finish(); + } + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + /** HttpServerDecorator: startSpan, afterStart (prototype), onRequest, then the route. */ + AgentSpan startServer() { + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.applyOverwriting(serverProto); + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort + server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves + return server; + } + + /** + * JDBCDecorator over a postgres prepared statement: startSpan, afterStart (prototype), + * onConnection, onPreparedStatement. + */ + AgentSpan jdbc() { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, "postgresql"); // processDatabaseType + jdbc.setServiceName("postgresql", JAVA_JDBC); // postProcessServiceAndOperationName + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, "HikariPool-1"); + jdbc.setTag(Tags.DB_USER, "app"); // DatabaseClientDecorator.onConnection + jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); + jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); + jdbc.setResourceName(STATEMENT); // withQueryInfo + jdbc.setTag(Tags.DB_OPERATION, "SELECT"); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + return jdbc; + } +} diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java new file mode 100644 index 00000000000..5556fa26139 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java @@ -0,0 +1,276 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.ContextScope; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * One whole trace per op, shaped like a web request: a server root, five jdbc prepared-statement + * children and one {@code @Trace} internal span, all finished. Divide by 7 for per-span figures. + * + *

For retrospective comparison across tracer versions, each version keeps its own copy of this + * benchmark, so the arms reflect that version's code: + * + *

+ * + *

Tag values are realistic on purpose: the client port, the database port and the status fall + * outside {@code Integer}'s box cache, so primitive boxing shows up. + * + *

This copy: 1.68 snapshot (decorators apply a prototype in {@code afterStart}, #12037; peer + * tags by id, #12715). + */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class TraceShapeBenchmark { + static final int JDBC_SPANS = 5; + + // server: HttpServerDecorator (tomcat) + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); + static final CharSequence WEB = UTF8BytesString.create("web"); + static final String USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; + static final String METHOD = "GET"; + static final String URL = "http://localhost:8080/owners/42"; + static final String HOSTNAME = "localhost"; + static final String CLIENT_IP = "127.0.0.1"; + static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache + static final String ROUTE = "/owners/{ownerId}"; + static final CharSequence SERVER_RESOURCE = UTF8BytesString.create("GET /owners/{ownerId}"); + static final int STATUS = 200; // outside the box cache + + // jdbc: JDBCDecorator over a postgres prepared statement + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); + static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); + static final CharSequence JDBC_PREPARED_STATEMENT = + UTF8BytesString.create("java-jdbc-prepared_statement"); + static final CharSequence SQL = UTF8BytesString.create("sql"); + static final String DB_TYPE = "postgresql"; + static final String DB_SERVICE = "postgresql"; + static final String DB_POOL = "HikariPool-1"; + static final String DB_USER = "app"; + static final String DB_INSTANCE = "petclinic"; + static final String DB_HOST = "db.internal"; + static final String DB_OPERATION = "SELECT"; + static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); + + // internal: TraceDecorator (@Trace) + static final String TRACE_INSTRUMENTATION = "trace"; + static final CharSequence TRACE_OPERATION = UTF8BytesString.create("trace.annotation"); + static final CharSequence TRACE_COMPONENT = UTF8BytesString.create("trace"); + static final CharSequence TRACE_RESOURCE = UTF8BytesString.create("OwnerService.findOwner"); + + CoreTracer tracer; + + SpanPrototype serverProto; + SpanPrototype jdbcProto; + SpanPrototype traceProto; + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType(WEB) + .initComponentAndIntegration(SERVER_COMPONENT) + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType(SQL) + .initComponentAndIntegration(JAVA_JDBC) + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + this.traceProto = + SpanPrototype.builder() + .initInstrumentationName(TRACE_INSTRUMENTATION) + .initOperationName(TRACE_OPERATION) + .initComponentAndIntegration(TRACE_COMPONENT) + .build(); + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + // ---- legacy: every tag by name ---- + + @Benchmark + public void legacy() { + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.setSpanType(WEB); + server.setTag(Tags.COMPONENT, SERVER_COMPONENT); + server.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + server.setTag("language", "jvm"); + legacyRequest(server); + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.setSpanType(SQL); + jdbc.setTag(Tags.COMPONENT, JAVA_JDBC); + jdbc.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + legacyJdbc(jdbc); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(TRACE_INSTRUMENTATION, TRACE_OPERATION); + internal.setTag(Tags.COMPONENT, TRACE_COMPONENT); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + static void legacyRequest(AgentSpan server) { + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); + server.setTag(Tags.HTTP_ROUTE, ROUTE); + } + + static void legacyJdbc(AgentSpan jdbc) { + jdbc.setTag(Tags.DB_TYPE, DB_TYPE); + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, DB_POOL); + jdbc.setTag(Tags.DB_USER, DB_USER); + jdbc.setTag(Tags.DB_INSTANCE, DB_INSTANCE); + jdbc.setTag(Tags.PEER_HOSTNAME, DB_HOST); + jdbc.setResourceName(STATEMENT); + jdbc.setTag(Tags.DB_OPERATION, DB_OPERATION); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + } + + // ---- normal: what this version's decorators do ---- + + @Benchmark + public void normal() { + // HttpServerDecorator: startSpan, afterStart, onRequest, then onResponseStatus at the end + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.applyOverwriting(serverProto); // BaseDecorator.afterStart + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort + server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < JDBC_SPANS; ++i) { + // AbstractPreparedStatementInstrumentation: startSpan, afterStart, onConnection, + // onPreparedStatement + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, DB_TYPE); // processDatabaseType + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); // postProcessServiceAndOperationName + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, DB_POOL); + jdbc.setTag(Tags.DB_USER, DB_USER); // DatabaseClientDecorator.onConnection + jdbc.setTag(Tags.DB_INSTANCE, DB_INSTANCE); + jdbc.setTag(Tags.PEER_HOSTNAME, DB_HOST); + jdbc.setResourceName(STATEMENT); // withQueryInfo + jdbc.setTag(Tags.DB_OPERATION, DB_OPERATION); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(TRACE_INSTRUMENTATION, TRACE_OPERATION); + internal.applyOverwriting(traceProto); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); // onResponseStatus + server.finish(); + } + + // ---- newApi: construction from a prototype, tags by id ---- + + @Benchmark + public void newApi() { + AgentSpan server = tracer.startSpan(serverProto, SERVER_OPERATION); + server.setTag(KnownTags.HTTP_USERAGENT_ID, USER_AGENT); + server.setTag(KnownTags.HTTP_METHOD_ID, METHOD); + server.setTag(KnownTags.HTTP_URL_ID, URL); + server.setTag(KnownTags.HTTP_HOSTNAME_ID, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); + server.setTag(KnownTags.PEER_IPV4_ID, CLIENT_IP); + server.setTag(KnownTags.PEER_PORT_ID, CLIENT_PORT); + server.setTag(KnownTags.HTTP_ROUTE_ID, ROUTE); + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(jdbcProto, JDBC_OPERATION); + jdbc.setTag(KnownTags.DB_TYPE_ID, DB_TYPE); + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(KnownTags.DB_POOL_NAME_ID, DB_POOL); + jdbc.setTag(KnownTags.DB_USER_ID, DB_USER); + jdbc.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE); + jdbc.setTag(KnownTags.PEER_HOSTNAME_ID, DB_HOST); + jdbc.setResourceName(STATEMENT); + jdbc.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(KnownTags.COMPONENT_ID, JDBC_PREPARED_STATEMENT); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(traceProto, TRACE_OPERATION); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + /** The advice's exit: activate around the call, then close and finish. */ + void finishScoped(AgentSpan span) { + ContextScope scope = tracer.activateSpan(span); + scope.close(); + span.finish(); + } +} From 6e65a3119af1bfc6b0f61355847ee5960499ae29 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 11:40:30 -0400 Subject: [PATCH 03/17] Vary TraceShapeBenchmark's trace size; test bucketHash TraceShapeBenchmark takes the number of jdbc children (1, 5, 20), so each arm shows how a trace's cost grows with its span count: single-span and small-trace results can hide costs that land on every child span. It covers what SpanScenarioBenchmark's trace scenario added, and the rest of that benchmark overlapped SpanCreationBenchmark (same scenario names, different semantics), so SpanScenarioBenchmark goes. TagMapBucketHashTest pins down the bucket hash: known tags get distinct, nonzero hashes, a slotted tag lands in its slot's bucket (trace-level in the other half), and tags without a slot and custom tags keep their hashes. The inline comment now says the level bit flips bit 3. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/SpanScenarioBenchmark.java | 219 ------------------ .../trace/core/TraceShapeBenchmark.java | 20 +- .../main/java/datadog/trace/api/TagMap.java | 2 +- .../trace/api/TagMapBucketHashTest.java | 67 ++++++ 4 files changed, 82 insertions(+), 226 deletions(-) delete mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java deleted file mode 100644 index 5030e61a15c..00000000000 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java +++ /dev/null @@ -1,219 +0,0 @@ -package datadog.trace.core; - -import static java.util.concurrent.TimeUnit.MICROSECONDS; - -import datadog.context.ContextScope; -import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; -import datadog.trace.bootstrap.instrumentation.api.Tags; -import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; -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.Param; -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.Threads; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - -/** - * Span and trace scenarios built the way today's decorators build them: a {@link SpanPrototype} - * applied in {@code afterStart}, then the request or connection tags by name. Each op finishes what - * it starts, so allocation per op covers the whole span or trace. - * - *

- * - *

Single-span and multi-span results can move in opposite directions: a change can make a - * trace's root cheaper while charging every child, so read the {@code webServerTrace} rows too. - * Values such as the client port and the status fall outside {@code Integer}'s box cache on - * purpose. - * - *

Passing {@code -jvmArgsAppend} on the command line replaces the {@code @Fork} arguments below, - * including {@code -DTEST_LOG_LEVEL=warn}; keep it, or debug logging dominates the allocation. - */ -@State(Scope.Benchmark) -@Warmup(iterations = 5) -@Measurement(iterations = 5) -@BenchmarkMode(Mode.Throughput) -@Threads(8) -@OutputTimeUnit(MICROSECONDS) -@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") -public class SpanScenarioBenchmark { - static final String SERVER_INSTRUMENTATION = "tomcat-server"; - static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); - static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); - static final CharSequence WEB = UTF8BytesString.create("web"); - static final String USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; - static final String METHOD = "GET"; - static final String URL = "http://localhost:8080/owners/42"; - static final String HOSTNAME = "localhost"; - static final String CLIENT_IP = "127.0.0.1"; - static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache - static final String ROUTE = "/owners/{ownerId}"; - static final String SERVER_RESOURCE_NAME = "GET /owners/{ownerId}"; - static final CharSequence SERVER_RESOURCE = UTF8BytesString.create(SERVER_RESOURCE_NAME); - static final int STATUS = 200; // outside the box cache - - static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; - static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); - static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); - static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); - static final CharSequence JDBC_PREPARED_STATEMENT = - UTF8BytesString.create("java-jdbc-prepared_statement"); - static final CharSequence SQL = UTF8BytesString.create("sql"); - static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); - - static final String BARE_INSTRUMENTATION = "bench"; - static final CharSequence BARE_OPERATION = UTF8BytesString.create("bare.op"); - - /** The trace size, taken only by {@code webServerTrace} so the single-span scenarios run once. */ - @State(Scope.Benchmark) - public static class Shape { - @Param({"1", "5", "20"}) - int children; - } - - CoreTracer tracer; - SpanPrototype serverProto; - SpanPrototype jdbcProto; - - @Setup - public void setup(Blackhole blackhole) { - this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); - // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) - this.serverProto = - SpanPrototype.builder() - .initInstrumentationName(SERVER_INSTRUMENTATION) - .initOperationName(SERVER_OPERATION) - .initSpanType(WEB) - .initComponentAndIntegration(SERVER_COMPONENT) - .initKind(Tags.SPAN_KIND_SERVER) - .initTag("language", "jvm") - .build(); - this.jdbcProto = - SpanPrototype.builder() - .initInstrumentationName(JDBC_INSTRUMENTATION) - .initOperationName(JDBC_OPERATION) - .initSpanType(SQL) - .initComponentAndIntegration(JAVA_JDBC) - .initKind(Tags.SPAN_KIND_CLIENT) - .build(); - } - - @TearDown - public void tearDown() { - this.tracer.close(); - } - - @Benchmark - public void bareStartSpan() { - tracer.startSpan(BARE_INSTRUMENTATION, BARE_OPERATION).finish(); - } - - @Benchmark - public void bareBuildSpan() { - tracer.buildSpan(BARE_INSTRUMENTATION, BARE_OPERATION).start().finish(); - } - - @Benchmark - public void webServerSpan() { - AgentSpan server = startServer(); - server.setHttpStatusCode(STATUS); - server.finish(); - } - - @Benchmark - public void webServerSpanViaBuilder() { - AgentSpan server = - tracer - .buildSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION) - .withSpanType(WEB) - .withTag(Tags.COMPONENT, SERVER_COMPONENT) - .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER) - .withTag("language", "jvm") - .withTag(Tags.HTTP_USER_AGENT, USER_AGENT) - .withTag(Tags.HTTP_METHOD, METHOD) - .withTag(Tags.HTTP_URL, URL) - .withTag(Tags.HTTP_HOSTNAME, HOSTNAME) - .withTag(Tags.PEER_HOST_IPV4, CLIENT_IP) - .withTag(Tags.PEER_PORT, CLIENT_PORT) - .withTag(Tags.HTTP_ROUTE, ROUTE) - .withResourceName(SERVER_RESOURCE_NAME) - .start(); - server.setHttpStatusCode(STATUS); - server.finish(); - } - - @Benchmark - public void jdbcClientSpan() { - jdbc().finish(); - } - - @Benchmark - public void webServerTrace(Shape shape) { - AgentSpan server = startServer(); - try (ContextScope serverScope = tracer.activateSpan(server)) { - for (int i = 0; i < shape.children; ++i) { - AgentSpan jdbc = jdbc(); - ContextScope jdbcScope = tracer.activateSpan(jdbc); - jdbcScope.close(); - jdbc.finish(); - } - } - server.setHttpStatusCode(STATUS); - server.finish(); - } - - /** HttpServerDecorator: startSpan, afterStart (prototype), onRequest, then the route. */ - AgentSpan startServer() { - AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); - server.applyOverwriting(serverProto); - server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); - server.setTag(Tags.HTTP_METHOD, METHOD); - server.setTag(Tags.HTTP_URL, URL); - server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); - server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath - server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); - server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort - server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves - return server; - } - - /** - * JDBCDecorator over a postgres prepared statement: startSpan, afterStart (prototype), - * onConnection, onPreparedStatement. - */ - AgentSpan jdbc() { - AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); - jdbc.applyOverwriting(jdbcProto); - jdbc.setTag(Tags.DB_TYPE, "postgresql"); // processDatabaseType - jdbc.setServiceName("postgresql", JAVA_JDBC); // postProcessServiceAndOperationName - jdbc.setOperationName(JDBC_RENAMED_OPERATION); - jdbc.setTag(Tags.DB_POOL_NAME, "HikariPool-1"); - jdbc.setTag(Tags.DB_USER, "app"); // DatabaseClientDecorator.onConnection - jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); - jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); - jdbc.setResourceName(STATEMENT); // withQueryInfo - jdbc.setTag(Tags.DB_OPERATION, "SELECT"); - jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); - jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); - return jdbc; - } -} diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java index 5556fa26139..46e2887084e 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java @@ -14,6 +14,7 @@ import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; @@ -23,8 +24,13 @@ import org.openjdk.jmh.infra.Blackhole; /** - * One whole trace per op, shaped like a web request: a server root, five jdbc prepared-statement - * children and one {@code @Trace} internal span, all finished. Divide by 7 for per-span figures. + * One whole trace per op, shaped like a web request: a server root, {@code children} jdbc + * prepared-statement children and one {@code @Trace} internal span, all finished. A trace has + * {@code children + 2} spans. + * + *

{@code children} varies the trace size, because single-span and small-trace results can hide + * costs that land on every child span: a change can make the root cheaper while charging each + * child, which only the larger traces show. * *

For retrospective comparison across tracer versions, each version keeps its own copy of this * benchmark, so the arms reflect that version's code: @@ -54,7 +60,9 @@ @OutputTimeUnit(MICROSECONDS) @Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") public class TraceShapeBenchmark { - static final int JDBC_SPANS = 5; + /** The number of jdbc children, so the trace has {@code children + 2} spans. */ + @Param({"1", "5", "20"}) + int children; // server: HttpServerDecorator (tomcat) static final String SERVER_INSTRUMENTATION = "tomcat-server"; @@ -146,7 +154,7 @@ public void legacy() { server.setTag("language", "jvm"); legacyRequest(server); try (ContextScope scope = tracer.activateSpan(server)) { - for (int i = 0; i < JDBC_SPANS; ++i) { + for (int i = 0; i < children; ++i) { AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); jdbc.setSpanType(SQL); jdbc.setTag(Tags.COMPONENT, JAVA_JDBC); @@ -203,7 +211,7 @@ public void normal() { server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves try (ContextScope scope = tracer.activateSpan(server)) { - for (int i = 0; i < JDBC_SPANS; ++i) { + for (int i = 0; i < children; ++i) { // AbstractPreparedStatementInstrumentation: startSpan, afterStart, onConnection, // onPreparedStatement AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); @@ -244,7 +252,7 @@ public void newApi() { server.setTag(KnownTags.PEER_PORT_ID, CLIENT_PORT); server.setTag(KnownTags.HTTP_ROUTE_ID, ROUTE); try (ContextScope scope = tracer.activateSpan(server)) { - for (int i = 0; i < JDBC_SPANS; ++i) { + for (int i = 0; i < children; ++i) { AgentSpan jdbc = tracer.startSpan(jdbcProto, JDBC_OPERATION); jdbc.setTag(KnownTags.DB_TYPE_ID, DB_TYPE); jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); 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 0b42cb3f9d2..3e9fe756d88 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -515,7 +515,7 @@ static long customHash(String tag) { static int bucketHash(long tagHash) { int slot = (int) (tagHash >>> 32) & 0xFFFF; if ((tagHash >>> 32) != 0 && slot != KnownTagCodec.NO_SLOT) { - int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: + 8 + int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: flip bit 3 return ((int) (tagHash >>> 48) << 16) | (slot ^ level); } int hash = (int) (tagHash >>> 48) ^ (int) tagHash; diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java new file mode 100644 index 00000000000..7a58dbe3534 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java @@ -0,0 +1,67 @@ +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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** How {@link TagMap.Entry#bucketHash} places known and custom tags in the buckets. */ +class TagMapBucketHashTest { + private static final int BUCKET_MASK = 15; // TagMap's 16 buckets + + /** Every known tag id the registry generated. */ + static List knownTagIds() throws IllegalAccessException { + List ids = new ArrayList<>(); + for (Field field : KnownTags.class.getFields()) { + if (Modifier.isStatic(field.getModifiers()) + && field.getType() == long.class + && field.getName().endsWith("_ID")) { + ids.add(field.getLong(null)); + } + } + return ids; + } + + @Test + void knownTagsGetDistinctNonzeroBucketHashes() throws IllegalAccessException { + List ids = knownTagIds(); + assertFalse(ids.isEmpty()); + Set hashes = new HashSet<>(); + for (long id : ids) { + int hash = TagMap.Entry.bucketHash(id); + assertNotEquals(0, hash, "zero marks a vacant BucketGroup slot"); + assertTrue( + hashes.add(hash), "two known tags share a bucket hash: " + KnownTagCodec.nameOf(id)); + } + } + + @Test + void aSlottedKnownTagIsBucketedBySlot() throws IllegalAccessException { + for (long id : knownTagIds()) { + int slot = KnownTagCodec.slotOf(id); + if (slot == KnownTagCodec.NO_SLOT) { + continue; + } + // trace-level tags reuse the span-level slot numbers, so they move to the other half + int expected = (slot ^ (KnownTagCodec.isTraceLevel(id) ? 8 : 0)) & BUCKET_MASK; + assertEquals(expected, TagMap.Entry.bucketHash(id) & BUCKET_MASK, KnownTagCodec.nameOf(id)); + } + } + + @Test + void tagsWithoutASlotAndCustomTagsKeepTheirHashes() { + long noSlot = ((long) 5 << 48) | ((long) KnownTagCodec.NO_SLOT << 32); + assertEquals((int) (noSlot >>> 48) ^ (int) noSlot, TagMap.Entry.bucketHash(noSlot)); + + long custom = TagMap.Entry.customHash("my.custom.tag"); + assertEquals((int) custom == 0 ? 0xDD06 : (int) custom, TagMap.Entry.bucketHash(custom)); + } +} From 2271e4f4fd011b0b56aa5fc70a1c99b673d53fbd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 17:50:26 -0400 Subject: [PATCH 04/17] Add a whole-request benchmark for the header code paths HeaderPathBenchmark runs one request per op through the propagation paths: extract the inbound headers with the default propagator, start the server span from what was extracted, run a few children, inject into an outbound call, finish everything. Inbound headers vary: none, Datadog, W3C (with dd, OpenTelemetry and third-party tracestate members), both, and both plus baggage. The existing propagation benchmarks each measure one step, so they can't see costs the extracted state causes later on every span; this one can. A setup check fails the run if nothing is extracted or injected. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/HeaderPathBenchmark.java | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java new file mode 100644 index 00000000000..d73d130d069 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java @@ -0,0 +1,226 @@ +package datadog.trace.core; + +import static datadog.context.Context.root; +import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext; +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.Context; +import datadog.context.ContextScope; +import datadog.context.propagation.CarrierSetter; +import datadog.context.propagation.CarrierVisitor; +import datadog.context.propagation.Propagators; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +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.Param; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * One whole request per op, through the header code paths: extract the inbound headers with the + * default propagator, start the server span from what was extracted, run a few children, make one + * outbound HTTP call that injects its headers, then finish everything. + * + *

The existing propagation benchmarks each measure one step (extracting into a context, + * injecting from a prebuilt one, parsing one header value). They can't see costs that the extracted + * state causes later, on every span of the trace: for example, a {@code tracestate} that leads each + * span to allocate sampling state. Measuring the whole request catches those. + * + *

{@code headers} selects the inbound headers, under the default propagation styles: + * + *

+ */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class HeaderPathBenchmark { + static final int JDBC_SPANS = 3; + + static final String TRACE_ID_HEX = "66a0f6c800000000a3ce929d0e0e4736"; + static final String TRACE_ID_LOW = "11803532876627986230"; // a3ce929d0e0e4736 + static final String PARENT_ID = "67667974448284343"; // 00f067aa0ba902b7 + static final String PARENT_ID_HEX = "00f067aa0ba902b7"; + + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final String CLIENT_INSTRUMENTATION = "okhttp"; + static final CharSequence CLIENT_OPERATION = UTF8BytesString.create("okhttp.request"); + + static final CarrierVisitor> GETTER = + new CarrierVisitor>() { + @Override + public void forEachKeyValue( + Map carrier, BiConsumer visitor) { + carrier.forEach(visitor); + } + }; + static final CarrierSetter> SETTER = Map::put; + + @Param({"none", "datadog", "tracecontext", "both", "bothBaggage"}) + String headers; + + CoreTracer tracer; + Map inbound; + SpanPrototype serverProto; + SpanPrototype jdbcProto; + SpanPrototype clientProto; + + /** One outbound carrier per thread, reused across requests. */ + @State(Scope.Thread) + public static class Outbound { + final Map headers = new HashMap<>(); + } + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + this.inbound = inboundHeaders(headers); + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType("web") + .initComponentAndIntegration("tomcat-server") + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType("sql") + .initComponentAndIntegration("java-jdbc") + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + this.clientProto = + SpanPrototype.builder() + .initInstrumentationName(CLIENT_INSTRUMENTATION) + .initOperationName(CLIENT_OPERATION) + .initSpanType("http") + .initComponentAndIntegration("okhttp") + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + checkPropagation(); + } + + /** + * Fails fast if the headers aren't actually extracted and injected, which would measure nothing. + */ + void checkPropagation() { + Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); + if (!headers.equals("none") && fromContext(parent) == null) { + throw new IllegalStateException("nothing extracted from " + headers + " headers"); + } + Outbound outbound = new Outbound(); + request(outbound); + if (!outbound.headers.containsKey("x-datadog-trace-id")) { + throw new IllegalStateException("nothing injected: " + outbound.headers); + } + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + static Map inboundHeaders(String style) { + Map h = new HashMap<>(); + h.put("host", "localhost:8080"); + h.put("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0"); + h.put("accept", "application/json"); + if (style.equals("datadog") || style.startsWith("both")) { + h.put("x-datadog-trace-id", TRACE_ID_LOW); + h.put("x-datadog-parent-id", PARENT_ID); + h.put("x-datadog-sampling-priority", "1"); + h.put("x-datadog-tags", "_dd.p.dm=-0,_dd.p.tid=66a0f6c800000000"); + } + if (style.equals("tracecontext") || style.startsWith("both")) { + h.put("traceparent", "00-" + TRACE_ID_HEX + "-" + PARENT_ID_HEX + "-01"); + h.put( + "tracestate", + "dd=s:1;p:" + + PARENT_ID_HEX + + ";t.dm:-0;t.tid:66a0f6c800000000," + + "ot=th:8;rv:9b8233f7e3a151,rojo=00f067aa0ba902b7"); + } + if (style.equals("bothBaggage")) { + h.put("baggage", "userId=alice,serverNode=DF%2028,isProduction=false"); + } + return h; + } + + @Benchmark + public void request(Outbound outbound) { + outbound.headers.clear(); + // HttpServerDecorator.extract / startSpan + Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); + AgentSpan extractedSpan = fromContext(parent); + AgentSpanContext extracted = extractedSpan == null ? null : extractedSpan.spanContext(); + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION, extracted); + server.setMeasured(true); + server.applyOverwriting(serverProto); + server.setTag(Tags.HTTP_METHOD, "GET"); + server.setTag(Tags.HTTP_URL, "http://localhost:8080/owners/42"); + server.setTag(Tags.HTTP_ROUTE, "/owners/{ownerId}"); + server.setTag(Tags.PEER_PORT, 54321); + + try (ContextScope serverScope = parent.with(server).attach()) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, "postgresql"); + jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); + jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); + jdbc.setResourceName("SELECT * FROM owners WHERE id = ?"); + try (ContextScope s = tracer.activateSpan(jdbc)) { + // the statement executes + } + jdbc.finish(); + } + + // the outbound call: start the client span, inject its headers, finish + AgentSpan client = tracer.startSpan(CLIENT_INSTRUMENTATION, CLIENT_OPERATION); + client.applyOverwriting(clientProto); + client.setTag(Tags.HTTP_METHOD, "GET"); + client.setTag(Tags.HTTP_URL, "http://inventory.internal/items/7"); + client.setTag(Tags.PEER_HOSTNAME, "inventory.internal"); + client.setTag(Tags.PEER_PORT, 8080); + Propagators.defaultPropagator() + .inject(Context.current().with(client), outbound.headers, SETTER); + client.setHttpStatusCode(200); + client.finish(); + } + server.setHttpStatusCode(200); + server.finish(); + } +} From fa9b44184088a634d93cddab3f59bab28501f933 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 23:17:19 -0400 Subject: [PATCH 05/17] Make a null tag value remove the tag in TagMap setTag(tag, null) already removes a tag from a span, but TagMap's own setters stored a null-valued entry. Internal writers that go straight to a TagMap could therefore leave null tags on a span, and the serializer sent them as a msgpack nil under meta. TagMap.put / set / getAndSet (by name or id, and putAll / fromMap through set) now remove the tag on a null value and return its prior value. A null Entry or reader is still a no-op. The one writer that relied on null tags was payload tagging: a JSON null in a request or response body produced a tag with a null value. It now produces no tag, so such a value also no longer counts toward the max-tags limit. Co-Authored-By: Claude Opus 5.5 --- .../tagprocessor/PayloadTagsProcessor.java | 2 +- .../PayloadTagsProcessorTest.java | 12 +--- .../main/java/datadog/trace/api/TagMap.java | 36 +++++++++--- .../trace/api/TagMapNullToleranceTest.java | 57 ++++++++++++++++++- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java index 09d382843a3..860cff77d68 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java @@ -245,7 +245,7 @@ public void doubleValue(PathCursor path, double value) { @Override public void nullValue(PathCursor path) { - collectedTags.set(path.toString(tagPrefix), null); + // a JSON null sets no tag: on a span, a null value means remove } @Override diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java index 1405c6329ae..85e3dad0ebc 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java @@ -203,8 +203,6 @@ void expandPreservingTagTypes() { 3.14d, "payload.tag4.Value.0", "string", - "payload.tag5", - null, "payload.tag6", false); assertEquals(expected, unsafeTags); @@ -246,8 +244,6 @@ void expandStringifiedJsonTags() { 2, "p.j3.2", 3.14d, - "p.j3.3", - null, "p.j3.4", true, "p.j4.foo", @@ -410,8 +406,6 @@ void applyRedactionRules() { 2, "p.j3.2", 3.14d, - "p.j3.3", - null, "p.j3.4", true, "p.j4.foo", @@ -444,8 +438,8 @@ void respectMaxTagsLimit() { 2, "p.j3.2", 3.14d, - "p.j3.3", - null, + "p.j3.4", + true, "_dd.payload_tags_incomplete", true), unsafeTags); @@ -474,8 +468,6 @@ void respectMaxDepthLimit() { 2, "p.j3.2", 3.14d, - "p.j3.3", - null, "p.j3.4", true, "p.j3.5.0", 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 be07fadbeeb..0fde50c117f 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1502,7 +1502,9 @@ boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { @Deprecated @Override public Object put(@Nonnull String tag, Object value) { - TagMap.Entry entry = this.getAndSet(anyEntryFor(tag, value)); + // a null value removes the tag, rather than mapping it to null as HashMap would + TagMap.Entry entry = + value == null ? this.getAndRemove(tag) : this.getAndSet(anyEntryFor(tag, value)); return entry == null ? null : entry.objectValue(); } @@ -1556,7 +1558,12 @@ static Entry doubleEntryFor(String tag, double value) { return tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value); } - public void set(@Nonnull String tag, @Nonnull Object value) { + /** A null value removes the tag, as {@code setTag(tag, null)} does on a span. */ + public void set(@Nonnull String tag, @Nullable Object value) { + if (value == null) { + this.getAndRemove(tag); + return; + } long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); @@ -1565,7 +1572,12 @@ public void set(@Nonnull String tag, @Nonnull Object value) { } } - public void set(@Nonnull String tag, @Nonnull CharSequence value) { + /** A null value removes the tag, as {@code setTag(tag, null)} does on a span. */ + public void set(@Nonnull String tag, @Nullable CharSequence value) { + if (value == null) { + this.getAndRemove(tag); + return; + } long tagId = KnownTagCodec.keyOf(tag); if (tagId != 0) { this.set(tagId, value); @@ -1680,11 +1692,21 @@ public void setKnown(long tagId, double value) { } } - public void set(long tagId, @Nonnull Object value) { + /** A null value removes the tag, as {@code setTag(tag, null)} does on a span. */ + public void set(long tagId, @Nullable Object value) { + if (value == null) { + this.getAndRemove(tagId); + return; + } this.putEntry(Entry.newAnyEntry(tagId, value)); } - public void set(long tagId, @Nonnull CharSequence value) { + /** A null value removes the tag, as {@code setTag(tag, null)} does on a span. */ + public void set(long tagId, @Nullable CharSequence value) { + if (value == null) { + this.getAndRemove(tagId); + return; + } this.putEntry(Entry.newObjectEntry(tagId, value)); } @@ -1797,11 +1819,11 @@ Entry putEntry(@Nonnull Entry newEntry) { } public Entry getAndSet(@Nonnull String tag, Object value) { - return this.getAndSet(anyEntryFor(tag, value)); + return value == null ? this.getAndRemove(tag) : this.getAndSet(anyEntryFor(tag, value)); } public Entry getAndSet(@Nonnull String tag, CharSequence value) { - return this.getAndSet(objectEntryFor(tag, value)); + return value == null ? this.getAndRemove(tag) : this.getAndSet(objectEntryFor(tag, value)); } public TagMap.Entry getAndSet(@Nonnull String tag, boolean value) { diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapNullToleranceTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapNullToleranceTest.java index b8764472847..261422c2732 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapNullToleranceTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapNullToleranceTest.java @@ -6,12 +6,16 @@ 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.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; /** * Pins the Entry-pathway null-tolerance contract: {@code create(...)} returns null for a null or * empty value, and the {@code Entry} sinks ({@code set(Entry)} / {@code getAndSet(Entry)}) treat - * null as a no-op -- so a null/empty value flows through as "no tag" without any caller guarding. + * null as a no-op -- so a null/empty value flows through as "no tag" without any caller guarding. A + * null value given to a setter removes the tag, as {@code setTag(tag, null)} does on a span. */ class TagMapNullToleranceTest { @@ -63,4 +67,55 @@ void nullOrEmptyFlowsThroughTheEntryPathwayAsNoTag() { assertEquals(1, map.size()); assertTrue(map.containsKey("present")); } + + @Test + void setNullRemovesAKnownOrCustomTag() { + TagMap map = TagMap.create(); + map.set(Tags.COMPONENT, "okhttp"); + map.set("custom.tag", "v"); + + map.set(Tags.COMPONENT, (Object) null); + map.set("custom.tag", (CharSequence) null); + assertEquals(0, map.size()); + assertFalse(map.containsKey(Tags.COMPONENT)); + assertFalse(map.containsKey("custom.tag")); + } + + @Test + void setNullByIdRemovesTheTag() { + TagMap map = TagMap.create(); + map.set(KnownTags.COMPONENT_ID, "okhttp"); + map.set(KnownTags.COMPONENT_ID, (Object) null); + assertNull(map.getObject(Tags.COMPONENT)); + + map.set(KnownTags.COMPONENT_ID, "okhttp"); + map.set(KnownTags.COMPONENT_ID, (CharSequence) null); + assertEquals(0, map.size()); + } + + @Test + void putAndGetAndSetNullRemoveTheTagAndReturnItsPriorValue() { + TagMap map = TagMap.create(); + map.set("custom.tag", "v"); + assertEquals("v", map.put("custom.tag", null)); + assertFalse(map.containsKey("custom.tag")); + + map.set(Tags.COMPONENT, "okhttp"); + assertEquals("okhttp", map.getAndSet(Tags.COMPONENT, (Object) null).objectValue()); + assertNull(map.getAndSet(Tags.COMPONENT, (CharSequence) null), "already removed"); + assertEquals(0, map.size()); + } + + @Test + void putAllWithANullValueLeavesNoTag() { + Map source = new HashMap<>(); + source.put("custom.tag", null); + source.put(Tags.COMPONENT, null); + source.put("present", "v"); + + TagMap map = TagMap.fromMap(source); + assertEquals(1, map.size()); + assertFalse(map.containsKey("custom.tag")); + assertFalse(map.containsKey(Tags.COMPONENT)); + } } From 8e57e8754adc99ddc8ef22940ea6acda8424e3f0 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 22:26:36 -0400 Subject: [PATCH 06/17] Introduce SpanStorage for eager tag post-processors Eager post-processors run on the application thread as a span finishes. They now take a SpanStorage (a span's tags as stored) instead of a TagMap, and DDSpanContext implements SpanStorage directly, so no TagMap view needs to be built for them once span storage stops being a single TagMap. SpanStorage has its own verbs, load and store, apart from TagMap's get/set and the span's intercepted getTag/setTag, so DDSpanContext can offer the raw and the intercepted operations without a clash. Stores bypass interception; thread safety is left to each implementation (DDSpanContext's are unsynchronized, called under the tag lock). TagMap implements it too, through its own get and set, for standalone maps. This is the first slice: load, loadString and two stores. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 30 +++++++++++++++++-- .../tagprocessor/EagerPostProcessorChain.java | 24 +++++++++++++++ .../tagprocessor/EagerTagsPostProcessor.java | 21 +++++++++++++ .../HttpEndpointPostProcessor.java | 12 ++++---- .../core/tagprocessor/InternalTagsAdder.java | 11 +++---- .../tagprocessor/PeerServiceCalculator.java | 16 +++++----- .../TagsPostProcessorFactory.java | 10 +++---- .../java/datadog/trace/api/SpanStorage.java | 27 +++++++++++++++++ .../main/java/datadog/trace/api/TagMap.java | 25 +++++++++++++++- .../trace/api/endpoint/EndpointResolver.java | 9 +++--- .../trace/api/naming/NamingSchema.java | 4 +-- .../api/naming/v0/PeerServiceNamingV0.java | 4 +-- .../api/naming/v1/PeerServiceNamingV1.java | 21 ++++++------- .../api/endpoint/EndpointResolverTest.java | 23 +++++++------- 14 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java create mode 100644 internal-api/src/main/java/datadog/trace/api/SpanStorage.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 1e236d3a120..8fdf11b8a85 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,6 +18,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; +import datadog.trace.api.SpanStorage; import datadog.trace.api.TagMap; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; @@ -68,7 +69,7 @@ * the associated Span instance */ public class DDSpanContext - implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext { + implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext, SpanStorage { private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class); public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -1395,6 +1396,31 @@ Object getTag(final String key) { } } + /* + * SpanStorage: the span's tags, loaded and stored with no interception. + * Unsynchronized, like unsafeGetTag: callers hold the span's tag lock, as post-processing does. + */ + + @Override + public Object load(String tag) { + return unsafeTags.getObject(tag); + } + + @Override + public String loadString(String tag) { + return unsafeTags.getString(tag); + } + + @Override + public void store(String tag, Object value) { + unsafeTags.set(tag, value); + } + + @Override + public void store(TagMap.EntryReader entry) { + unsafeTags.set(entry); + } + /** * This is not thread-safe and must only be used when it can be guaranteed that the context will * not be mutated. This is internal API and must not be exposed to users. @@ -1500,7 +1526,7 @@ public void setMetaStruct(final String field, final T value) { void earlyProcessTags(AppendableSpanLinks links) { synchronized (unsafeTags) { - TagsPostProcessorFactory.eagerProcessor().processTags(unsafeTags, this, links); + TagsPostProcessorFactory.eagerProcessor().processTags(this, this, links); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java new file mode 100644 index 00000000000..f5ac6765293 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java @@ -0,0 +1,24 @@ +package datadog.trace.core.tagprocessor; + +import datadog.trace.api.SpanStorage; +import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; +import datadog.trace.core.DDSpanContext; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** Runs eager post-processors in order. */ +public final class EagerPostProcessorChain extends EagerTagsPostProcessor { + private final EagerTagsPostProcessor[] chain; + + public EagerPostProcessorChain(@Nonnull final EagerTagsPostProcessor... processors) { + chain = Objects.requireNonNull(processors); + } + + @Override + public void processTags( + SpanStorage storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + for (final EagerTagsPostProcessor processor : chain) { + processor.processTags(storage, spanContext, spanLinks); + } + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java new file mode 100644 index 00000000000..54ace639c80 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java @@ -0,0 +1,21 @@ +package datadog.trace.core.tagprocessor; + +import datadog.trace.api.SpanStorage; +import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; +import datadog.trace.core.DDSpanContext; + +/** + * A tag post-processor run on the application thread as a span finishes. It works on the span's + * {@link SpanStorage} rather than a {@code TagMap}, so the span needn't build one for it. + */ +public abstract class EagerTagsPostProcessor { + /** + * Processes the span's tags. + * + * @param contents the span's contents to read and write; the caller holds the span's tag lock + * @param spanContext the span, for its other properties + * @param spanLinks the span's links + */ + public abstract void processTags( + SpanStorage storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks); +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java index c2e0dd72761..6e6f9cfd829 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java @@ -4,7 +4,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; -import datadog.trace.api.TagMap; +import datadog.trace.api.SpanStorage; import datadog.trace.api.endpoint.EndpointResolver; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -30,7 +30,7 @@ *
  • If {@code alwaysSimplifiedEndpoint=true}, always compute from URL regardless of route * */ -public class HttpEndpointPostProcessor extends TagsPostProcessor { +public class HttpEndpointPostProcessor extends EagerTagsPostProcessor { private static final Logger log = LoggerFactory.getLogger(HttpEndpointPostProcessor.class); private final EndpointResolver endpointResolver; @@ -55,19 +55,19 @@ public HttpEndpointPostProcessor() { @Override public void processTags( - TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { if (!endpointResolver.isEnabled()) { log.debug("EndpointResolver is not enabled, skipping HTTP endpoint post processing"); return; } - if (unsafeTags.getObject(HTTP_METHOD) == null) { + if (unsafeTags.load(HTTP_METHOD) == null) { return; } try { - String httpRoute = unsafeTags.getString(HTTP_ROUTE); - String httpUrl = unsafeTags.getString(HTTP_URL); + String httpRoute = unsafeTags.loadString(HTTP_ROUTE); + String httpUrl = unsafeTags.loadString(HTTP_URL); endpointResolver.resolveEndpoint(unsafeTags, httpRoute, httpUrl); } catch (Throwable t) { log.debug("Error processing HTTP endpoint for span {}", spanContext.getSpanId(), t); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java index 3e6b8b13192..afcf8c30438 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java @@ -3,6 +3,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.VERSION; import datadog.trace.api.DDTags; +import datadog.trace.api.SpanStorage; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -10,7 +11,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -public final class InternalTagsAdder extends TagsPostProcessor { +public final class InternalTagsAdder extends EagerTagsPostProcessor { private final UTF8BytesString ddService; // Prebuilt once to avoid per-span Entry allocation. @@ -28,7 +29,7 @@ public InternalTagsAdder(@Nonnull final String ddService, @Nullable final String @Override public void processTags( - TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { if (spanContext == null) { return; } @@ -36,13 +37,13 @@ public void processTags( if (!ddService.toString().equalsIgnoreCase(spanContext.getServiceName())) { if (baseServiceEntry != null) { // service name != DD_SERVICE - unsafeTags.set(baseServiceEntry); + unsafeTags.store(baseServiceEntry); } } else { // as per config consistency, the version tag is added across tracers only if // the service name is DD_SERVICE and version tag is not manually set - if (versionEntry != null && !unsafeTags.containsKey(VERSION)) { - unsafeTags.set(versionEntry); + if (versionEntry != null && unsafeTags.load(VERSION) == null) { + unsafeTags.store(versionEntry); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java index 198e2c78f1c..8d5d0513962 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java @@ -2,7 +2,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; -import datadog.trace.api.TagMap; +import datadog.trace.api.SpanStorage; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.api.naming.NamingSchema; import datadog.trace.api.naming.SpanNaming; @@ -12,7 +12,7 @@ import java.util.Map; import javax.annotation.Nonnull; -public final class PeerServiceCalculator extends TagsPostProcessor { +public final class PeerServiceCalculator extends EagerTagsPostProcessor { private final NamingSchema.ForPeerService peerServiceNaming; private final Map peerServiceMapping; @@ -34,8 +34,8 @@ public PeerServiceCalculator() { @Override public void processTags( - TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - Object peerService = unsafeTags.getObject(Tags.PEER_SERVICE); + SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + Object peerService = unsafeTags.load(Tags.PEER_SERVICE); // the user set it if (peerService != null) { if (canRemap) { @@ -46,18 +46,18 @@ public void processTags( // calculate the defaults (if any) peerServiceNaming.tags(unsafeTags); // only remap if the mapping is not empty (saves one get) - remapPeerService(unsafeTags, canRemap ? unsafeTags.getObject(Tags.PEER_SERVICE) : null); + remapPeerService(unsafeTags, canRemap ? unsafeTags.load(Tags.PEER_SERVICE) : null); return; } // we have no peer.service and we do not compute defaults. Leave the map untouched } - private void remapPeerService(TagMap unsafeTags, Object value) { + private void remapPeerService(SpanStorage unsafeTags, Object value) { if (value != null) { String mapped = peerServiceMapping.get(value); if (mapped != null) { - unsafeTags.put(Tags.PEER_SERVICE, mapped); - unsafeTags.put(DDTags.PEER_SERVICE_REMAPPED_FROM, value); + unsafeTags.store(Tags.PEER_SERVICE, mapped); + unsafeTags.store(DDTags.PEER_SERVICE_REMAPPED_FROM, value); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java index 08c52a2658a..2e3cd4ca345 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java @@ -9,11 +9,11 @@ public final class TagsPostProcessorFactory { private static boolean addRemoteHostname = true; private static class Lazy { - private static TagsPostProcessor eagerProcessor = createEagerChain(); + private static EagerTagsPostProcessor eagerProcessor = createEagerChain(); private static TagsPostProcessor lazyProcessor = createLazyChain(); - private static TagsPostProcessor createEagerChain() { - final List processors = new ArrayList<>(3); + private static EagerTagsPostProcessor createEagerChain() { + final List processors = new ArrayList<>(3); processors.add(new PeerServiceCalculator()); if (addInternalTags) { processors.add( @@ -24,7 +24,7 @@ private static TagsPostProcessor createEagerChain() { if (Config.get().isTraceResourceRenamingEnabled()) { processors.add(new HttpEndpointPostProcessor()); } - return new PostProcessorChain(processors.toArray(new TagsPostProcessor[0])); + return new EagerPostProcessorChain(processors.toArray(new EagerTagsPostProcessor[0])); } private static TagsPostProcessor createLazyChain() { @@ -54,7 +54,7 @@ private static TagsPostProcessor createLazyChain() { } } - public static TagsPostProcessor eagerProcessor() { + public static EagerTagsPostProcessor eagerProcessor() { return Lazy.eagerProcessor; } diff --git a/internal-api/src/main/java/datadog/trace/api/SpanStorage.java b/internal-api/src/main/java/datadog/trace/api/SpanStorage.java new file mode 100644 index 00000000000..09b2fab946d --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/SpanStorage.java @@ -0,0 +1,27 @@ +package datadog.trace.api; + +/** + * A span's tags as stored, for the tracer core's processing pipeline: loads and stores go straight + * to storage, with no interception. Thread safety is up to the implementation; {@code + * DDSpanContext}'s expects the span's tag lock to be held. + * + *

    Its own verbs, {@code load} and {@code store}, keep it apart from {@link TagMap}'s {@code + * get}/{@code set} and the span's intercepted {@code getTag}/{@code setTag}, so a type can offer + * several of these without a clash. + * + *

    This is the first slice: only what the eager tag post-processors need. Id variants, typed + * loads and primitive stores come as more of the pipeline moves to it. + */ +public interface SpanStorage { + /** The tag's value, or null when it's absent. */ + Object load(String tag); + + /** The tag's value as a string, or null when it's absent. */ + String loadString(String tag); + + /** Stores the tag; a null value removes it. */ + void store(String tag, Object value); + + /** Stores the entry's tag; a null entry is a no-op. */ + void store(TagMap.EntryReader 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 296d52399a6..b4cc9997222 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -48,7 +48,8 @@ *

  • adaptive collision * */ -public final class TagMap implements Map, Iterable { +public final class TagMap + implements Map, Iterable, SpanStorage { /** 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 @@ -1346,6 +1347,28 @@ public String getString(String tag) { return entry == null ? null : entry.stringValue(); } + /* SpanStorage, for standalone maps handed to the pipeline: TagMap's own get and set. */ + + @Override + public Object load(String tag) { + return this.getObject(tag); + } + + @Override + public String loadString(String tag) { + return this.getString(tag); + } + + @Override + public void store(String tag, Object value) { + this.set(tag, value); + } + + @Override + public void store(EntryReader entry) { + this.set(entry); + } + public boolean getBoolean(String tag) { return this.getBooleanOrDefault(tag, false); } diff --git a/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java b/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java index c5e47328734..30114ca2372 100644 --- a/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java +++ b/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java @@ -2,6 +2,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT; +import datadog.trace.api.SpanStorage; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,9 +51,7 @@ public EndpointResolver(boolean enabled, boolean alwaysSimplifiedEndpoint) { */ @Nullable public String resolveEndpoint( - java.util.Map unsafeTags, - @Nullable String httpRoute, - @Nullable String httpUrl) { + SpanStorage unsafeTags, @Nullable String httpRoute, @Nullable String httpUrl) { if (!enabled) { return null; } @@ -61,7 +60,7 @@ public String resolveEndpoint( if (alwaysSimplifiedEndpoint) { String endpoint = computeEndpoint(httpUrl); if (endpoint != null) { - unsafeTags.put(HTTP_ENDPOINT, endpoint); + unsafeTags.store(HTTP_ENDPOINT, endpoint); } return endpoint; } @@ -74,7 +73,7 @@ public String resolveEndpoint( // Compute endpoint from URL and tag the span String endpoint = computeEndpoint(httpUrl); if (endpoint != null) { - unsafeTags.put(HTTP_ENDPOINT, endpoint); + unsafeTags.store(HTTP_ENDPOINT, endpoint); } return endpoint; } diff --git a/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java b/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java index 9ddf3178f3b..b17b52bc58b 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java @@ -1,6 +1,6 @@ package datadog.trace.api.naming; -import datadog.trace.api.TagMap; +import datadog.trace.api.SpanStorage; import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -232,7 +232,7 @@ interface ForPeerService { * @param unsafeTags the span tags. Map to be mutated */ @Nonnull - void tags(@Nonnull TagMap unsafeTags); + void tags(@Nonnull SpanStorage unsafeTags); } interface ForServer { diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java b/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java index 3e76d657069..d802622205a 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java @@ -1,6 +1,6 @@ package datadog.trace.api.naming.v0; -import datadog.trace.api.TagMap; +import datadog.trace.api.SpanStorage; import datadog.trace.api.naming.NamingSchema; import javax.annotation.Nonnull; @@ -12,5 +12,5 @@ public boolean supports() { @Nonnull @Override - public void tags(@Nonnull final TagMap unsafeTags) {} + public void tags(@Nonnull final SpanStorage unsafeTags) {} } diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java index 827a7489e09..021dbda7aee 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java @@ -1,7 +1,7 @@ package datadog.trace.api.naming.v1; import datadog.trace.api.DDTags; -import datadog.trace.api.TagMap; +import datadog.trace.api.SpanStorage; import datadog.trace.api.naming.NamingSchema; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -53,8 +53,8 @@ public boolean supports() { return true; } - private void resolve(@Nonnull final TagMap unsafeTags) { - final Object component = unsafeTags.getObject(Tags.COMPONENT); + private void resolve(@Nonnull final SpanStorage unsafeTags) { + final Object component = unsafeTags.load(Tags.COMPONENT); // avoid issues with UTF8ByteString or others final String componentString = component == null ? null : component.toString(); final String override = overridesByComponent.get(componentString); @@ -71,14 +71,15 @@ private void resolve(@Nonnull final TagMap unsafeTags) { resolveBy(unsafeTags, DEFAULT_PRECURSORS); } - private boolean resolveBy(@Nonnull final TagMap unsafeTags, @Nullable final String[] precursors) { + private boolean resolveBy( + @Nonnull final SpanStorage unsafeTags, @Nullable final String[] precursors) { if (precursors == null) { return false; } Object value = null; String source = null; for (String precursor : precursors) { - value = unsafeTags.getObject(precursor); + value = unsafeTags.load(precursor); if (value != null) { // we have a match. Use the tag name for the source source = precursor; @@ -90,18 +91,18 @@ private boolean resolveBy(@Nonnull final TagMap unsafeTags, @Nullable final Stri return true; } - private void set(@Nonnull final TagMap unsafeTags, Object value, String source) { + private void set(@Nonnull final SpanStorage unsafeTags, Object value, String source) { if (value != null) { - unsafeTags.put(Tags.PEER_SERVICE, value); - unsafeTags.put(DDTags.PEER_SERVICE_SOURCE, source); + unsafeTags.store(Tags.PEER_SERVICE, value); + unsafeTags.store(DDTags.PEER_SERVICE_SOURCE, source); } } @Nonnull @Override - public void tags(@Nonnull final TagMap unsafeTags) { + public void tags(@Nonnull final SpanStorage unsafeTags) { // check span.kind eligibility - final Object kind = unsafeTags.getObject(Tags.SPAN_KIND); + final Object kind = unsafeTags.load(Tags.SPAN_KIND); if (Tags.SPAN_KIND_CLIENT.equals(kind) || Tags.SPAN_KIND_PRODUCER.equals(kind)) { // we can calculate the peer service now resolve(unsafeTags); diff --git a/internal-api/src/test/java/datadog/trace/api/endpoint/EndpointResolverTest.java b/internal-api/src/test/java/datadog/trace/api/endpoint/EndpointResolverTest.java index 10d6ad5b940..eeec260b9eb 100644 --- a/internal-api/src/test/java/datadog/trace/api/endpoint/EndpointResolverTest.java +++ b/internal-api/src/test/java/datadog/trace/api/endpoint/EndpointResolverTest.java @@ -6,8 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.HashMap; -import java.util.Map; +import datadog.trace.api.TagMap; import org.junit.jupiter.api.Test; import org.tabletest.junit.TableTest; @@ -47,7 +46,7 @@ void computeEndpointDelegatesToEndpointSimplifier(String input, String expected) @Test void disabledResolverReturnsNull() { EndpointResolver resolver = new EndpointResolver(false, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); String result = resolver.resolveEndpoint(unsafeTags, "/users", "http://example.com/users/123"); @@ -59,7 +58,7 @@ void disabledResolverReturnsNull() { @Test void alwaysSimplifiedEndpointComputesFromUrlEvenWithValidRoute() { EndpointResolver resolver = new EndpointResolver(true, true); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); String result = resolver.resolveEndpoint(unsafeTags, "/users", "http://example.com/users/123"); @@ -71,7 +70,7 @@ void alwaysSimplifiedEndpointComputesFromUrlEvenWithValidRoute() { @Test void usesEligibleRouteWithoutTagging() { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); String result = resolver.resolveEndpoint(unsafeTags, "/api/v1/users", "http://example.com/users/123"); @@ -91,7 +90,7 @@ void usesEligibleRouteWithoutTagging() { void computesEndpointAndTagsWhenRouteIsIneligible( String ineligibleRoute, String url, String expectedEndpoint) { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); String result = resolver.resolveEndpoint(unsafeTags, ineligibleRoute, url); @@ -115,7 +114,7 @@ void isAlwaysSimplifiedEndpointReturnsConfigurationValue() { @Test void realWorldScenarioServiceWithRoute() { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); // framework provides accurate route String result = @@ -132,7 +131,7 @@ void realWorldScenarioServiceWithRoute() { @Test void realWorldScenarioProxyWithoutRoute() { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); // no route available (tracer in proxy) String result = @@ -147,7 +146,7 @@ void realWorldScenarioProxyWithoutRoute() { @Test void realWorldScenarioBadInstrumentationWithCatchAll() { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); // instrumentation provides unhelpful catch-all route String result = @@ -161,7 +160,7 @@ void realWorldScenarioBadInstrumentationWithCatchAll() { @Test void realWorldScenarioTestingWithAlwaysSimplified() { EndpointResolver resolver = new EndpointResolver(true, true); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); // testing mode to compare with backend inference String result = @@ -176,7 +175,7 @@ void realWorldScenarioTestingWithAlwaysSimplified() { @Test void edgeCaseMalformedUrlReturnsDefaultEndpoint() { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); String result = resolver.resolveEndpoint(unsafeTags, null, "not-a-url"); @@ -189,7 +188,7 @@ void edgeCaseMalformedUrlReturnsDefaultEndpoint() { @Test void edgeCaseBothRouteAndUrlMissing() { EndpointResolver resolver = new EndpointResolver(true, false); - Map unsafeTags = new HashMap<>(); + TagMap unsafeTags = TagMap.create(); String result = resolver.resolveEndpoint(unsafeTags, null, null); From 3c91e7f27b93c1c756e976e58f5693c9ccdd325d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 11:49:11 -0400 Subject: [PATCH 07/17] Attach HeaderPathBenchmark's server span through the local tracer Context.attach() and Context.current() go to the global context manager, which a standalone JMH run never wires to the benchmark's CoreTracer. The JDBC and client spans became separate root traces instead of children of the extracted server span. Use the tracer's attach/currentContext, and check at setup that the outbound call continues the extracted trace. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/HeaderPathBenchmark.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java index d73d130d069..e9e2d73b517 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java @@ -134,7 +134,8 @@ public void setup(Blackhole blackhole) { } /** - * Fails fast if the headers aren't actually extracted and injected, which would measure nothing. + * Fails fast if the headers aren't actually extracted and injected, or if the outbound call + * doesn't continue the extracted trace, either of which would measure something else. */ void checkPropagation() { Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); @@ -143,9 +144,14 @@ void checkPropagation() { } Outbound outbound = new Outbound(); request(outbound); - if (!outbound.headers.containsKey("x-datadog-trace-id")) { + String injectedTraceId = outbound.headers.get("x-datadog-trace-id"); + if (injectedTraceId == null) { throw new IllegalStateException("nothing injected: " + outbound.headers); } + if (!headers.equals("none") && !injectedTraceId.equals(TRACE_ID_LOW)) { + throw new IllegalStateException( + "outbound call didn't continue the extracted trace: " + outbound.headers); + } } @TearDown @@ -194,7 +200,7 @@ public void request(Outbound outbound) { server.setTag(Tags.HTTP_ROUTE, "/owners/{ownerId}"); server.setTag(Tags.PEER_PORT, 54321); - try (ContextScope serverScope = parent.with(server).attach()) { + try (ContextScope serverScope = tracer.attach(parent.with(server))) { for (int i = 0; i < JDBC_SPANS; ++i) { AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); jdbc.applyOverwriting(jdbcProto); @@ -216,7 +222,7 @@ public void request(Outbound outbound) { client.setTag(Tags.PEER_HOSTNAME, "inventory.internal"); client.setTag(Tags.PEER_PORT, 8080); Propagators.defaultPropagator() - .inject(Context.current().with(client), outbound.headers, SETTER); + .inject(tracer.currentContext().with(client), outbound.headers, SETTER); client.setHttpStatusCode(200); client.finish(); } From f1b60e47b3cb64b6f948939c1ba46898a0a83ef2 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 11:56:51 -0400 Subject: [PATCH 08/17] Remove an earlier payload tag when a later value at its path is null With null now meaning remove, a JSON null set no tag at all, so a duplicate key such as {"x":"old","x":null} left the stale x=old tag. Setting null again removes it, as overwriting with null did before. Co-Authored-By: Claude Opus 5.5 --- .../core/tagprocessor/PayloadTagsProcessor.java | 3 ++- .../tagprocessor/PayloadTagsProcessorTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java index 860cff77d68..c927e4f6a54 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java @@ -245,7 +245,8 @@ public void doubleValue(PathCursor path, double value) { @Override public void nullValue(PathCursor path) { - // a JSON null sets no tag: on a span, a null value means remove + // a JSON null sets no tag, and removes one an earlier value set at the same path + collectedTags.set(path.toString(tagPrefix), null); } @Override diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java index 85e3dad0ebc..cb91a78789e 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.java @@ -253,6 +253,23 @@ void expandStringifiedJsonTags() { assertEquals(expected, unsafeTags); } + @Test + void nullRemovesEarlierValueAtSamePath() { + PayloadTagsProcessor ptp = tagsProcessor("p", Collections.emptyList(), 10, 758); + + Map st = + spanTags( + "p", + pv(pc().push("j1"), "{'x': 'old', 'x': null, 'y': 1}"), + pv(pc().push("v"), "old"), + pv(pc().push("v"), null)); + + TagMap unsafeTags = TagMap.fromMap(st); + ptp.processTags(unsafeTags, null, link -> {}); + + assertEquals(mapOf("p.j1.y", 1), unsafeTags); + } + @Test void expandSerializedEscapedInnerJsonWithinInnerJson() throws IOException { Buffer b0 = new Buffer(); From 884c508dacf68901f7f4c3ec7103ee81940de20a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 12:56:12 -0400 Subject: [PATCH 09/17] Load and store known tags by id through SpanStorage The eager post-processors only touch known tags, yet loaded and stored them by name, so every call classified the name first. SpanStorage gains id variants of load, loadString and store, and the processors, the peer service naming and the endpoint resolver use KnownTags ids. A known tag's id and name address the same tag. The peer service precursor lists stay names: they include tags outside the registry. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/DDSpanContext.java | 15 +++++++++++++++ .../tagprocessor/HttpEndpointPostProcessor.java | 12 ++++++------ .../core/tagprocessor/InternalTagsAdder.java | 10 +++++----- .../tagprocessor/PeerServiceCalculator.java | 13 +++++++------ .../java/datadog/trace/api/SpanStorage.java | 16 ++++++++++++++-- .../src/main/java/datadog/trace/api/TagMap.java | 17 +++++++++++++++++ .../trace/api/endpoint/EndpointResolver.java | 6 +++--- .../api/naming/v1/PeerServiceNamingV1.java | 14 +++++++++----- 8 files changed, 76 insertions(+), 27 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 8fdf11b8a85..044314cecf4 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 @@ -1411,11 +1411,26 @@ public String loadString(String tag) { return unsafeTags.getString(tag); } + @Override + public Object load(long tagId) { + return unsafeTags.load(tagId); + } + + @Override + public String loadString(long tagId) { + return unsafeTags.loadString(tagId); + } + @Override public void store(String tag, Object value) { unsafeTags.set(tag, value); } + @Override + public void store(long tagId, Object value) { + unsafeTags.set(tagId, value); + } + @Override public void store(TagMap.EntryReader entry) { unsafeTags.set(entry); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java index 6e6f9cfd829..9f1a9b8a51e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java @@ -1,8 +1,8 @@ package datadog.trace.core.tagprocessor; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL; +import static datadog.trace.api.KnownTags.HTTP_METHOD_ID; +import static datadog.trace.api.KnownTags.HTTP_ROUTE_ID; +import static datadog.trace.api.KnownTags.HTTP_URL_ID; import datadog.trace.api.SpanStorage; import datadog.trace.api.endpoint.EndpointResolver; @@ -61,13 +61,13 @@ public void processTags( return; } - if (unsafeTags.load(HTTP_METHOD) == null) { + if (unsafeTags.load(HTTP_METHOD_ID) == null) { return; } try { - String httpRoute = unsafeTags.loadString(HTTP_ROUTE); - String httpUrl = unsafeTags.loadString(HTTP_URL); + String httpRoute = unsafeTags.loadString(HTTP_ROUTE_ID); + String httpUrl = unsafeTags.loadString(HTTP_URL_ID); endpointResolver.resolveEndpoint(unsafeTags, httpRoute, httpUrl); } catch (Throwable t) { log.debug("Error processing HTTP endpoint for span {}", spanContext.getSpanId(), t); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java index afcf8c30438..8847b833c44 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java @@ -1,8 +1,8 @@ package datadog.trace.core.tagprocessor; -import static datadog.trace.bootstrap.instrumentation.api.Tags.VERSION; +import static datadog.trace.api.KnownTags.DD_BASE_SERVICE_ID; +import static datadog.trace.api.KnownTags.VERSION_ID; -import datadog.trace.api.DDTags; import datadog.trace.api.SpanStorage; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -20,10 +20,10 @@ public final class InternalTagsAdder extends EagerTagsPostProcessor { public InternalTagsAdder(@Nonnull final String ddService, @Nullable final String version) { this.ddService = UTF8BytesString.create(ddService); - this.baseServiceEntry = TagMap.Entry.create(DDTags.BASE_SERVICE, this.ddService); + this.baseServiceEntry = TagMap.Entry.create(DD_BASE_SERVICE_ID, this.ddService); this.versionEntry = version != null && !version.isEmpty() - ? TagMap.Entry.create(VERSION, UTF8BytesString.create(version)) + ? TagMap.Entry.create(VERSION_ID, UTF8BytesString.create(version)) : null; } @@ -42,7 +42,7 @@ public void processTags( } else { // as per config consistency, the version tag is added across tracers only if // the service name is DD_SERVICE and version tag is not manually set - if (versionEntry != null && unsafeTags.load(VERSION) == null) { + if (versionEntry != null && unsafeTags.load(VERSION_ID) == null) { unsafeTags.store(versionEntry); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java index 8d5d0513962..83a39c52002 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java @@ -1,13 +1,14 @@ package datadog.trace.core.tagprocessor; +import static datadog.trace.api.KnownTags.DD_PEER_SERVICE_REMAPPED_FROM_ID; +import static datadog.trace.api.KnownTags.PEER_SERVICE_ID; + import datadog.trace.api.Config; -import datadog.trace.api.DDTags; import datadog.trace.api.SpanStorage; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.api.naming.NamingSchema; import datadog.trace.api.naming.SpanNaming; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; -import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.core.DDSpanContext; import java.util.Map; import javax.annotation.Nonnull; @@ -35,7 +36,7 @@ public PeerServiceCalculator() { @Override public void processTags( SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { - Object peerService = unsafeTags.load(Tags.PEER_SERVICE); + Object peerService = unsafeTags.load(PEER_SERVICE_ID); // the user set it if (peerService != null) { if (canRemap) { @@ -46,7 +47,7 @@ public void processTags( // calculate the defaults (if any) peerServiceNaming.tags(unsafeTags); // only remap if the mapping is not empty (saves one get) - remapPeerService(unsafeTags, canRemap ? unsafeTags.load(Tags.PEER_SERVICE) : null); + remapPeerService(unsafeTags, canRemap ? unsafeTags.load(PEER_SERVICE_ID) : null); return; } // we have no peer.service and we do not compute defaults. Leave the map untouched @@ -56,8 +57,8 @@ private void remapPeerService(SpanStorage unsafeTags, Object value) { if (value != null) { String mapped = peerServiceMapping.get(value); if (mapped != null) { - unsafeTags.store(Tags.PEER_SERVICE, mapped); - unsafeTags.store(DDTags.PEER_SERVICE_REMAPPED_FROM, value); + unsafeTags.store(PEER_SERVICE_ID, mapped); + unsafeTags.store(DD_PEER_SERVICE_REMAPPED_FROM_ID, value); } } } diff --git a/internal-api/src/main/java/datadog/trace/api/SpanStorage.java b/internal-api/src/main/java/datadog/trace/api/SpanStorage.java index 09b2fab946d..8a5f18b1b99 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanStorage.java +++ b/internal-api/src/main/java/datadog/trace/api/SpanStorage.java @@ -9,8 +9,11 @@ * get}/{@code set} and the span's intercepted {@code getTag}/{@code setTag}, so a type can offer * several of these without a clash. * - *

    This is the first slice: only what the eager tag post-processors need. Id variants, typed - * loads and primitive stores come as more of the pipeline moves to it. + *

    A known tag can be addressed by its {@link KnownTags} id or by its name, and both address the + * same tag. Prefer the id: it skips classifying the name on every call. A custom tag has no id. + * + *

    This is the first slice: only what the eager tag post-processors need. Typed loads and + * primitive stores come as more of the pipeline moves to it. */ public interface SpanStorage { /** The tag's value, or null when it's absent. */ @@ -19,9 +22,18 @@ public interface SpanStorage { /** The tag's value as a string, or null when it's absent. */ String loadString(String tag); + /** The known tag's value, or null when it's absent. */ + Object load(long tagId); + + /** The known tag's value as a string, or null when it's absent. */ + String loadString(long tagId); + /** Stores the tag; a null value removes it. */ void store(String tag, Object value); + /** Stores the known tag; a null value removes it. */ + void store(long tagId, Object value); + /** Stores the entry's tag; a null entry is a no-op. */ void store(TagMap.EntryReader 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 b4cc9997222..7de3856b002 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1359,11 +1359,28 @@ public String loadString(String tag) { return this.getString(tag); } + @Override + public Object load(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.objectValue(); + } + + @Override + public String loadString(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.stringValue(); + } + @Override public void store(String tag, Object value) { this.set(tag, value); } + @Override + public void store(long tagId, Object value) { + this.set(tagId, value); + } + @Override public void store(EntryReader entry) { this.set(entry); diff --git a/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java b/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java index 30114ca2372..f9877d708bf 100644 --- a/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java +++ b/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java @@ -1,6 +1,6 @@ package datadog.trace.api.endpoint; -import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ENDPOINT; +import static datadog.trace.api.KnownTags.HTTP_ENDPOINT_ID; import datadog.trace.api.SpanStorage; import javax.annotation.Nullable; @@ -60,7 +60,7 @@ public String resolveEndpoint( if (alwaysSimplifiedEndpoint) { String endpoint = computeEndpoint(httpUrl); if (endpoint != null) { - unsafeTags.store(HTTP_ENDPOINT, endpoint); + unsafeTags.store(HTTP_ENDPOINT_ID, endpoint); } return endpoint; } @@ -73,7 +73,7 @@ public String resolveEndpoint( // Compute endpoint from URL and tag the span String endpoint = computeEndpoint(httpUrl); if (endpoint != null) { - unsafeTags.store(HTTP_ENDPOINT, endpoint); + unsafeTags.store(HTTP_ENDPOINT_ID, endpoint); } return endpoint; } diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java index 021dbda7aee..ae1e7d6ed19 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java @@ -1,6 +1,10 @@ package datadog.trace.api.naming.v1; -import datadog.trace.api.DDTags; +import static datadog.trace.api.KnownTags.COMPONENT_ID; +import static datadog.trace.api.KnownTags.DD_PEER_SERVICE_SOURCE_ID; +import static datadog.trace.api.KnownTags.PEER_SERVICE_ID; +import static datadog.trace.api.KnownTags.SPAN_KIND_ID; + import datadog.trace.api.SpanStorage; import datadog.trace.api.naming.NamingSchema; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; @@ -54,7 +58,7 @@ public boolean supports() { } private void resolve(@Nonnull final SpanStorage unsafeTags) { - final Object component = unsafeTags.load(Tags.COMPONENT); + final Object component = unsafeTags.load(COMPONENT_ID); // avoid issues with UTF8ByteString or others final String componentString = component == null ? null : component.toString(); final String override = overridesByComponent.get(componentString); @@ -93,8 +97,8 @@ private boolean resolveBy( private void set(@Nonnull final SpanStorage unsafeTags, Object value, String source) { if (value != null) { - unsafeTags.store(Tags.PEER_SERVICE, value); - unsafeTags.store(DDTags.PEER_SERVICE_SOURCE, source); + unsafeTags.store(PEER_SERVICE_ID, value); + unsafeTags.store(DD_PEER_SERVICE_SOURCE_ID, source); } } @@ -102,7 +106,7 @@ private void set(@Nonnull final SpanStorage unsafeTags, Object value, String sou @Override public void tags(@Nonnull final SpanStorage unsafeTags) { // check span.kind eligibility - final Object kind = unsafeTags.load(Tags.SPAN_KIND); + final Object kind = unsafeTags.load(SPAN_KIND_ID); if (Tags.SPAN_KIND_CLIENT.equals(kind) || Tags.SPAN_KIND_PRODUCER.equals(kind)) { // we can calculate the peer service now resolve(unsafeTags); From 590f6e7e1bf653b30081edd3e2b74ad997b18257 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 12:56:14 -0400 Subject: [PATCH 10/17] Test that a known tag's id and name address the same tag in SpanStorage Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/SpanStorageTest.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java diff --git a/dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java b/dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java new file mode 100644 index 00000000000..3c6c24277d1 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java @@ -0,0 +1,87 @@ +package datadog.trace.core; + +import static datadog.trace.api.KnownTags.HTTP_ROUTE_ID; +import static datadog.trace.api.KnownTags.HTTP_STATUS_CODE_ID; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_STATUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagMap; +import datadog.trace.common.writer.ListWriter; +import java.util.function.Consumer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** A known tag's id and name address the same tag, in every {@link SpanStorage}. */ +class SpanStorageTest extends DDCoreJavaSpecification { + private CoreTracer tracer; + + @BeforeEach + void setup() { + tracer = tracerBuilder().writer(new ListWriter()).build(); + } + + @AfterEach + void cleanup() { + tracer.close(); + } + + @Test + void storedByIdLoadsByName() { + forEachStorage( + storage -> { + storage.store(HTTP_ROUTE_ID, "/users/{id}"); + + assertEquals("/users/{id}", storage.load(HTTP_ROUTE)); + assertEquals("/users/{id}", storage.load(HTTP_ROUTE_ID)); + }); + } + + @Test + void storedByNameLoadsById() { + forEachStorage( + storage -> { + storage.store(HTTP_ROUTE, "/users/{id}"); + + assertEquals("/users/{id}", storage.load(HTTP_ROUTE_ID)); + }); + } + + @Test + void loadStringByIdConverts() { + forEachStorage( + storage -> { + storage.store(HTTP_STATUS_CODE_ID, 404); + + assertEquals("404", storage.loadString(HTTP_STATUS_CODE_ID)); + assertEquals("404", storage.loadString(HTTP_STATUS)); + }); + } + + @Test + void nullStoredByIdRemoves() { + forEachStorage( + storage -> { + storage.store(HTTP_ROUTE, "/users/{id}"); + + storage.store(HTTP_ROUTE_ID, null); + + assertNull(storage.load(HTTP_ROUTE)); + assertNull(storage.loadString(HTTP_ROUTE_ID)); + }); + } + + private void forEachStorage(Consumer check) { + check.accept(TagMap.create()); + + DDSpan span = (DDSpan) tracer.buildSpan("test", "span").start(); + try { + check.accept(span.spanContext()); + } finally { + span.finish(); + } + } +} From 38dc4b38cbac5ed63943db4ffa97451bbb9e3a40 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 15:42:18 -0400 Subject: [PATCH 11/17] Rename SpanStorage to TagStore Everything on it is a tag, and TagMap implements it too, so TagStore says what it is. Span-named types stay the layer with more to them: SpanData is the intercepted view, with span-level fields as well as tags. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/core/DDSpanContext.java | 6 +++--- .../core/tagprocessor/EagerPostProcessorChain.java | 4 ++-- .../core/tagprocessor/EagerTagsPostProcessor.java | 6 +++--- .../core/tagprocessor/HttpEndpointPostProcessor.java | 4 ++-- .../trace/core/tagprocessor/InternalTagsAdder.java | 4 ++-- .../trace/core/tagprocessor/PeerServiceCalculator.java | 6 +++--- .../core/{SpanStorageTest.java => TagStoreTest.java} | 8 ++++---- .../src/main/java/datadog/trace/api/TagMap.java | 5 ++--- .../trace/api/{SpanStorage.java => TagStore.java} | 2 +- .../datadog/trace/api/endpoint/EndpointResolver.java | 4 ++-- .../java/datadog/trace/api/naming/NamingSchema.java | 4 ++-- .../trace/api/naming/v0/PeerServiceNamingV0.java | 4 ++-- .../trace/api/naming/v1/PeerServiceNamingV1.java | 10 +++++----- 13 files changed, 33 insertions(+), 34 deletions(-) rename dd-trace-core/src/test/java/datadog/trace/core/{SpanStorageTest.java => TagStoreTest.java} (92%) rename internal-api/src/main/java/datadog/trace/api/{SpanStorage.java => TagStore.java} (98%) 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 044314cecf4..8c77f2fbb39 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,8 +18,8 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; -import datadog.trace.api.SpanStorage; import datadog.trace.api.TagMap; +import datadog.trace.api.TagStore; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; import datadog.trace.api.config.TracerConfig; @@ -69,7 +69,7 @@ * the associated Span instance */ public class DDSpanContext - implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext, SpanStorage { + implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext, TagStore { private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class); public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -1397,7 +1397,7 @@ Object getTag(final String key) { } /* - * SpanStorage: the span's tags, loaded and stored with no interception. + * TagStore: the span's tags, loaded and stored with no interception. * Unsynchronized, like unsafeGetTag: callers hold the span's tag lock, as post-processing does. */ diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java index f5ac6765293..21cc4ed9be6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java @@ -1,6 +1,6 @@ package datadog.trace.core.tagprocessor; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; import datadog.trace.core.DDSpanContext; import java.util.Objects; @@ -16,7 +16,7 @@ public EagerPostProcessorChain(@Nonnull final EagerTagsPostProcessor... processo @Override public void processTags( - SpanStorage storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + TagStore storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { for (final EagerTagsPostProcessor processor : chain) { processor.processTags(storage, spanContext, spanLinks); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java index 54ace639c80..503d922e8a9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java @@ -1,12 +1,12 @@ package datadog.trace.core.tagprocessor; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; import datadog.trace.core.DDSpanContext; /** * A tag post-processor run on the application thread as a span finishes. It works on the span's - * {@link SpanStorage} rather than a {@code TagMap}, so the span needn't build one for it. + * {@link TagStore} rather than a {@code TagMap}, so the span needn't build one for it. */ public abstract class EagerTagsPostProcessor { /** @@ -17,5 +17,5 @@ public abstract class EagerTagsPostProcessor { * @param spanLinks the span's links */ public abstract void processTags( - SpanStorage storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks); + TagStore storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java index 9f1a9b8a51e..c447bc32ec6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessor.java @@ -4,7 +4,7 @@ import static datadog.trace.api.KnownTags.HTTP_ROUTE_ID; import static datadog.trace.api.KnownTags.HTTP_URL_ID; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import datadog.trace.api.endpoint.EndpointResolver; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; @@ -55,7 +55,7 @@ public HttpEndpointPostProcessor() { @Override public void processTags( - SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { if (!endpointResolver.isEnabled()) { log.debug("EndpointResolver is not enabled, skipping HTTP endpoint post processing"); return; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java index 8847b833c44..b0f8184e5d9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/InternalTagsAdder.java @@ -3,8 +3,8 @@ import static datadog.trace.api.KnownTags.DD_BASE_SERVICE_ID; import static datadog.trace.api.KnownTags.VERSION_ID; -import datadog.trace.api.SpanStorage; import datadog.trace.api.TagMap; +import datadog.trace.api.TagStore; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; @@ -29,7 +29,7 @@ public InternalTagsAdder(@Nonnull final String ddService, @Nullable final String @Override public void processTags( - SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { if (spanContext == null) { return; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java index 83a39c52002..98fd6888d45 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java @@ -4,7 +4,7 @@ import static datadog.trace.api.KnownTags.PEER_SERVICE_ID; import datadog.trace.api.Config; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.api.naming.NamingSchema; import datadog.trace.api.naming.SpanNaming; @@ -35,7 +35,7 @@ public PeerServiceCalculator() { @Override public void processTags( - SpanStorage unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { Object peerService = unsafeTags.load(PEER_SERVICE_ID); // the user set it if (peerService != null) { @@ -53,7 +53,7 @@ public void processTags( // we have no peer.service and we do not compute defaults. Leave the map untouched } - private void remapPeerService(SpanStorage unsafeTags, Object value) { + private void remapPeerService(TagStore unsafeTags, Object value) { if (value != null) { String mapped = peerServiceMapping.get(value); if (mapped != null) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java b/dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java similarity index 92% rename from dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java rename to dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java index 3c6c24277d1..5f12d64300a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/SpanStorageTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java @@ -7,16 +7,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import datadog.trace.api.SpanStorage; import datadog.trace.api.TagMap; +import datadog.trace.api.TagStore; import datadog.trace.common.writer.ListWriter; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -/** A known tag's id and name address the same tag, in every {@link SpanStorage}. */ -class SpanStorageTest extends DDCoreJavaSpecification { +/** A known tag's id and name address the same tag, in every {@link TagStore}. */ +class TagStoreTest extends DDCoreJavaSpecification { private CoreTracer tracer; @BeforeEach @@ -74,7 +74,7 @@ void nullStoredByIdRemoves() { }); } - private void forEachStorage(Consumer check) { + private void forEachStorage(Consumer check) { check.accept(TagMap.create()); DDSpan span = (DDSpan) tracer.buildSpan("test", "span").start(); 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 7de3856b002..bb18907b390 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -48,8 +48,7 @@ *

  • adaptive collision * */ -public final class TagMap - implements Map, Iterable, SpanStorage { +public final class TagMap implements Map, Iterable, TagStore { /** 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 @@ -1347,7 +1346,7 @@ public String getString(String tag) { return entry == null ? null : entry.stringValue(); } - /* SpanStorage, for standalone maps handed to the pipeline: TagMap's own get and set. */ + /* TagStore, for standalone maps handed to the pipeline: TagMap's own get and set. */ @Override public Object load(String tag) { diff --git a/internal-api/src/main/java/datadog/trace/api/SpanStorage.java b/internal-api/src/main/java/datadog/trace/api/TagStore.java similarity index 98% rename from internal-api/src/main/java/datadog/trace/api/SpanStorage.java rename to internal-api/src/main/java/datadog/trace/api/TagStore.java index 8a5f18b1b99..04e81c43e67 100644 --- a/internal-api/src/main/java/datadog/trace/api/SpanStorage.java +++ b/internal-api/src/main/java/datadog/trace/api/TagStore.java @@ -15,7 +15,7 @@ *

    This is the first slice: only what the eager tag post-processors need. Typed loads and * primitive stores come as more of the pipeline moves to it. */ -public interface SpanStorage { +public interface TagStore { /** The tag's value, or null when it's absent. */ Object load(String tag); diff --git a/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java b/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java index f9877d708bf..29533b14e18 100644 --- a/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java +++ b/internal-api/src/main/java/datadog/trace/api/endpoint/EndpointResolver.java @@ -2,7 +2,7 @@ import static datadog.trace.api.KnownTags.HTTP_ENDPOINT_ID; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +51,7 @@ public EndpointResolver(boolean enabled, boolean alwaysSimplifiedEndpoint) { */ @Nullable public String resolveEndpoint( - SpanStorage unsafeTags, @Nullable String httpRoute, @Nullable String httpUrl) { + TagStore unsafeTags, @Nullable String httpRoute, @Nullable String httpUrl) { if (!enabled) { return null; } diff --git a/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java b/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java index b17b52bc58b..7876901a68d 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java @@ -1,6 +1,6 @@ package datadog.trace.api.naming; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -232,7 +232,7 @@ interface ForPeerService { * @param unsafeTags the span tags. Map to be mutated */ @Nonnull - void tags(@Nonnull SpanStorage unsafeTags); + void tags(@Nonnull TagStore unsafeTags); } interface ForServer { diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java b/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java index d802622205a..004a8e654bd 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java @@ -1,6 +1,6 @@ package datadog.trace.api.naming.v0; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import datadog.trace.api.naming.NamingSchema; import javax.annotation.Nonnull; @@ -12,5 +12,5 @@ public boolean supports() { @Nonnull @Override - public void tags(@Nonnull final SpanStorage unsafeTags) {} + public void tags(@Nonnull final TagStore unsafeTags) {} } diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java index ae1e7d6ed19..4f88c13aa46 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java @@ -5,7 +5,7 @@ import static datadog.trace.api.KnownTags.PEER_SERVICE_ID; import static datadog.trace.api.KnownTags.SPAN_KIND_ID; -import datadog.trace.api.SpanStorage; +import datadog.trace.api.TagStore; import datadog.trace.api.naming.NamingSchema; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -57,7 +57,7 @@ public boolean supports() { return true; } - private void resolve(@Nonnull final SpanStorage unsafeTags) { + private void resolve(@Nonnull final TagStore unsafeTags) { final Object component = unsafeTags.load(COMPONENT_ID); // avoid issues with UTF8ByteString or others final String componentString = component == null ? null : component.toString(); @@ -76,7 +76,7 @@ private void resolve(@Nonnull final SpanStorage unsafeTags) { } private boolean resolveBy( - @Nonnull final SpanStorage unsafeTags, @Nullable final String[] precursors) { + @Nonnull final TagStore unsafeTags, @Nullable final String[] precursors) { if (precursors == null) { return false; } @@ -95,7 +95,7 @@ private boolean resolveBy( return true; } - private void set(@Nonnull final SpanStorage unsafeTags, Object value, String source) { + private void set(@Nonnull final TagStore unsafeTags, Object value, String source) { if (value != null) { unsafeTags.store(PEER_SERVICE_ID, value); unsafeTags.store(DD_PEER_SERVICE_SOURCE_ID, source); @@ -104,7 +104,7 @@ private void set(@Nonnull final SpanStorage unsafeTags, Object value, String sou @Nonnull @Override - public void tags(@Nonnull final SpanStorage unsafeTags) { + public void tags(@Nonnull final TagStore unsafeTags) { // check span.kind eligibility final Object kind = unsafeTags.load(SPAN_KIND_ID); if (Tags.SPAN_KIND_CLIENT.equals(kind) || Tags.SPAN_KIND_PRODUCER.equals(kind)) { From 4540183d9560bd43fb5e14eb636a3c74d23cce7c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 15:43:54 -0400 Subject: [PATCH 12/17] Apply suggestion from @sarahchen6 Co-authored-by: Sarah Chen --- .../datadog/trace/core/tagprocessor/PayloadTagsProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java index c927e4f6a54..e254a756a87 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java @@ -245,7 +245,7 @@ public void doubleValue(PathCursor path, double value) { @Override public void nullValue(PathCursor path) { - // a JSON null sets no tag, and removes one an earlier value set at the same path + // a JSON null sets no tag, and removes an earlier value set at the same path collectedTags.set(path.toString(tagPrefix), null); } From 773906530c7b130b2a012ddb8b6841a47af7e328 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 15:46:10 -0400 Subject: [PATCH 13/17] Say why the span implements TagStore A span isn't conceptually a TagStore: the store is the storage beneath it, as a Map was beneath a TagMap. It implements one only so post-processing can be handed its storage without allocating, since the processor chain's dispatch loop keeps escape analysis from removing a wrapper. Drop the TagStore Javadoc's note about this being a first slice. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/core/DDSpanContext.java | 8 ++++++-- .../trace/core/tagprocessor/EagerTagsPostProcessor.java | 6 ++++-- .../src/main/java/datadog/trace/api/TagStore.java | 7 ++----- 3 files changed, 12 insertions(+), 9 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 8c77f2fbb39..cb6fa415438 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 @@ -1397,8 +1397,12 @@ Object getTag(final String key) { } /* - * TagStore: the span's tags, loaded and stored with no interception. - * Unsynchronized, like unsafeGetTag: callers hold the span's tag lock, as post-processing does. + * TagStore: the span's tags as stored, loaded and stored with no locking, interception or + * interpretation -- the storage beneath the span, as a Map was beneath a TagMap. A span isn't + * conceptually a TagStore. It implements one so post-processing is handed the span's storage + * without allocating: a wrapper would be free only if escape analysis removed it, and the + * post-processor chain's dispatch loop defeats that. Callers hold the span's tag lock, as + * post-processing does. */ @Override diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java index 503d922e8a9..55aa3579216 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java @@ -6,13 +6,15 @@ /** * A tag post-processor run on the application thread as a span finishes. It works on the span's - * {@link TagStore} rather than a {@code TagMap}, so the span needn't build one for it. + * tags as stored, through a {@link TagStore}, rather than a {@code TagMap} the span would have to + * build for it. The span passes itself as that store, which allocates nothing. */ public abstract class EagerTagsPostProcessor { /** * Processes the span's tags. * - * @param contents the span's contents to read and write; the caller holds the span's tag lock + * @param storage the span's tags as stored, to read and write; the caller holds the span's tag + * lock * @param spanContext the span, for its other properties * @param spanLinks the span's links */ diff --git a/internal-api/src/main/java/datadog/trace/api/TagStore.java b/internal-api/src/main/java/datadog/trace/api/TagStore.java index 04e81c43e67..b0e63cbf15f 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagStore.java +++ b/internal-api/src/main/java/datadog/trace/api/TagStore.java @@ -2,8 +2,8 @@ /** * A span's tags as stored, for the tracer core's processing pipeline: loads and stores go straight - * to storage, with no interception. Thread safety is up to the implementation; {@code - * DDSpanContext}'s expects the span's tag lock to be held. + * to storage, with no interception or interpretation. Thread safety is up to the implementation; + * {@code DDSpanContext}'s expects the span's tag lock to be held. * *

    Its own verbs, {@code load} and {@code store}, keep it apart from {@link TagMap}'s {@code * get}/{@code set} and the span's intercepted {@code getTag}/{@code setTag}, so a type can offer @@ -11,9 +11,6 @@ * *

    A known tag can be addressed by its {@link KnownTags} id or by its name, and both address the * same tag. Prefer the id: it skips classifying the name on every call. A custom tag has no id. - * - *

    This is the first slice: only what the eager tag post-processors need. Typed loads and - * primitive stores come as more of the pipeline moves to it. */ public interface TagStore { /** The tag's value, or null when it's absent. */ From 36bbbd276c9e1425f107de30890329b918d04f53 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 15:56:53 -0400 Subject: [PATCH 14/17] Test that urlOrPath answers the same from a TagStore as from a map Co-Authored-By: Claude Opus 5.5 --- .../api/OtelHttpSemanticsTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java index bfbf0a748fe..ea6ec759ffa 100644 --- a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/OtelHttpSemanticsTest.java @@ -13,6 +13,8 @@ import datadog.trace.api.DDTags; import datadog.trace.api.KnownTags; +import datadog.trace.api.TagMap; +import datadog.trace.api.TagStore; import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -79,6 +81,26 @@ void urlOrPathFallsBackOnlyWhenUrlIsAbsent(String httpUrl, String path, String e assertEquals(expected, OtelHttpSemantics.urlOrPath(tags)); } + /** From a span's tags as stored, it answers as it does from the same tags as a map. */ + @TableTest({ + "Scenario | Http URL | URL path ", + "No URL or path | | ", + "URL only | https://example.com/full | ", + "Path only | | /orders ", + "Prefer URL | https://example.com/full | /different", + "Empty URL | '' | /orders ", + "Empty path | | '' " + }) + void urlOrPathFromTagStoreMatchesMap(String httpUrl, String path) { + TagMap tags = TagMap.create(); + tags.store(KnownTags.HTTP_URL_ID, httpUrl); + tags.store(KnownTags.URL_PATH_ID, path); + + assertEquals( + OtelHttpSemantics.urlOrPath((Map) tags), + OtelHttpSemantics.urlOrPath((TagStore) tags)); + } + @Test void urlOrPathDoesNotRenderLazyUrl() { URIUtils.LazyUrl url = URIUtils.lazyValidURL("https", "example.com", 443, "/orders"); From b9dbfa9704b1cea8310965589c20315b6b26828b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 16:43:10 -0400 Subject: [PATCH 15/17] Name the post-processor chain's TagStore parameter unsafeTags EagerTagsPostProcessor and EagerPostProcessorChain still called it storage after the rename to TagStore, while every processor calls it unsafeTags. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/tagprocessor/EagerPostProcessorChain.java | 4 ++-- .../trace/core/tagprocessor/EagerTagsPostProcessor.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java index 21cc4ed9be6..df1ee6ea25d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerPostProcessorChain.java @@ -16,9 +16,9 @@ public EagerPostProcessorChain(@Nonnull final EagerTagsPostProcessor... processo @Override public void processTags( - TagStore storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { + TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) { for (final EagerTagsPostProcessor processor : chain) { - processor.processTags(storage, spanContext, spanLinks); + processor.processTags(unsafeTags, spanContext, spanLinks); } } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java index 55aa3579216..15b1dcf4e1a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/EagerTagsPostProcessor.java @@ -13,11 +13,11 @@ public abstract class EagerTagsPostProcessor { /** * Processes the span's tags. * - * @param storage the span's tags as stored, to read and write; the caller holds the span's tag + * @param unsafeTags the span's tags as stored, to read and write; the caller holds the span's tag * lock * @param spanContext the span, for its other properties * @param spanLinks the span's links */ public abstract void processTags( - TagStore storage, DDSpanContext spanContext, AppendableSpanLinks spanLinks); + TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks); } From 3eed22059a3706f0ce6b1b4b071d45fa5c763b43 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 16:43:12 -0400 Subject: [PATCH 16/17] Rename TagStoreTest's storage helper and locals Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/TagStoreTest.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java b/dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java index 5f12d64300a..fd6e413f4e8 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/TagStoreTest.java @@ -31,50 +31,50 @@ void cleanup() { @Test void storedByIdLoadsByName() { - forEachStorage( - storage -> { - storage.store(HTTP_ROUTE_ID, "/users/{id}"); + forEachTagStore( + tags -> { + tags.store(HTTP_ROUTE_ID, "/users/{id}"); - assertEquals("/users/{id}", storage.load(HTTP_ROUTE)); - assertEquals("/users/{id}", storage.load(HTTP_ROUTE_ID)); + assertEquals("/users/{id}", tags.load(HTTP_ROUTE)); + assertEquals("/users/{id}", tags.load(HTTP_ROUTE_ID)); }); } @Test void storedByNameLoadsById() { - forEachStorage( - storage -> { - storage.store(HTTP_ROUTE, "/users/{id}"); + forEachTagStore( + tags -> { + tags.store(HTTP_ROUTE, "/users/{id}"); - assertEquals("/users/{id}", storage.load(HTTP_ROUTE_ID)); + assertEquals("/users/{id}", tags.load(HTTP_ROUTE_ID)); }); } @Test void loadStringByIdConverts() { - forEachStorage( - storage -> { - storage.store(HTTP_STATUS_CODE_ID, 404); + forEachTagStore( + tags -> { + tags.store(HTTP_STATUS_CODE_ID, 404); - assertEquals("404", storage.loadString(HTTP_STATUS_CODE_ID)); - assertEquals("404", storage.loadString(HTTP_STATUS)); + assertEquals("404", tags.loadString(HTTP_STATUS_CODE_ID)); + assertEquals("404", tags.loadString(HTTP_STATUS)); }); } @Test void nullStoredByIdRemoves() { - forEachStorage( - storage -> { - storage.store(HTTP_ROUTE, "/users/{id}"); + forEachTagStore( + tags -> { + tags.store(HTTP_ROUTE, "/users/{id}"); - storage.store(HTTP_ROUTE_ID, null); + tags.store(HTTP_ROUTE_ID, null); - assertNull(storage.load(HTTP_ROUTE)); - assertNull(storage.loadString(HTTP_ROUTE_ID)); + assertNull(tags.load(HTTP_ROUTE)); + assertNull(tags.loadString(HTTP_ROUTE_ID)); }); } - private void forEachStorage(Consumer check) { + private void forEachTagStore(Consumer check) { check.accept(TagMap.create()); DDSpan span = (DDSpan) tracer.buildSpan("test", "span").start(); From cdfc195b4fb9a6a242978fdd9940440f1231a2cc Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 28 Oct 2026 10:53:58 -0400 Subject: [PATCH 17/17] Annotate TagStore's nullable values and keep implementation details out of its Javadoc Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagStore.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagStore.java b/internal-api/src/main/java/datadog/trace/api/TagStore.java index b0e63cbf15f..6a0abb43830 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagStore.java +++ b/internal-api/src/main/java/datadog/trace/api/TagStore.java @@ -1,9 +1,10 @@ package datadog.trace.api; +import javax.annotation.Nullable; + /** * A span's tags as stored, for the tracer core's processing pipeline: loads and stores go straight - * to storage, with no interception or interpretation. Thread safety is up to the implementation; - * {@code DDSpanContext}'s expects the span's tag lock to be held. + * to storage, with no interception or interpretation. Thread safety is up to the implementation. * *

    Its own verbs, {@code load} and {@code store}, keep it apart from {@link TagMap}'s {@code * get}/{@code set} and the span's intercepted {@code getTag}/{@code setTag}, so a type can offer @@ -14,23 +15,27 @@ */ public interface TagStore { /** The tag's value, or null when it's absent. */ + @Nullable Object load(String tag); /** The tag's value as a string, or null when it's absent. */ + @Nullable String loadString(String tag); /** The known tag's value, or null when it's absent. */ + @Nullable Object load(long tagId); /** The known tag's value as a string, or null when it's absent. */ + @Nullable String loadString(long tagId); /** Stores the tag; a null value removes it. */ - void store(String tag, Object value); + void store(String tag, @Nullable Object value); /** Stores the known tag; a null value removes it. */ - void store(long tagId, Object value); + void store(long tagId, @Nullable Object value); /** Stores the entry's tag; a null entry is a no-op. */ - void store(TagMap.EntryReader entry); + void store(@Nullable TagMap.EntryReader entry); }