From 53dc37104d32d2b86fd22132469d6403c4d688cd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 9 Jul 2026 05:35:18 -0400 Subject: [PATCH 1/8] Introduce SpanPrototype: baked-once constant span-tag descriptor The builder API (extends_/init*) plus its per-mechanism microbenchmark and a pure-API test, split out from the combined span-prototype work so the abstraction lands independently of the decorator demo. Co-Authored-By: Claude Opus 4.8 --- .../trace/api/SpanPrototypeBenchmark.java | 91 +++++++++++ .../instrumentation/api/SpanPrototype.java | 145 ++++++++++++++++++ .../api/SpanPrototypeTest.java | 25 +++ 3 files changed, 261 insertions(+) create mode 100644 internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java create mode 100644 internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java diff --git a/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java new file mode 100644 index 00000000000..2ef2b182dd4 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java @@ -0,0 +1,91 @@ +package datadog.trace.api; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Per-mechanism benchmark for {@link SpanPrototype}: the constant-tag application a span pays at + * start. Compares the three phases of the mechanism, holding the resulting tag set identical: + * + * + * + *

Isolates the constant-application only (not span creation or the {@code afterStart} virtual + * chain), so the delta is purely N-stamps vs. bulk-copy. Run with {@code -prof gc} — the + * interesting axes are ops/s and B/op. + */ +@State(Scope.Thread) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(SECONDS) +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +@Fork(3) +@Threads(8) +public class SpanPrototypeBenchmark { + + // The constant set a typical server span carries, as cached entries (the shared-Entry + // hand-optimization the decorators use today). + private static final TagMap.Entry COMPONENT = TagMap.Entry.create(Tags.COMPONENT, "netty"); + private static final TagMap.Entry KIND = + TagMap.Entry.create(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + private static final TagMap.Entry LANGUAGE = + TagMap.Entry.create(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE); + private static final TagMap.Entry ANALYTICS = + TagMap.Entry.create(DDTags.ANALYTICS_SAMPLE_RATE, 1.0d); + + private SpanPrototype prototype; + + @Setup(Level.Trial) + public void setUp() { + // Baked once — the same constants, composed through the builder. + prototype = + SpanPrototype.builder() + .initComponent("netty") + .initKind(Tags.SPAN_KIND_SERVER) + .initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE) + .initTag(ANALYTICS) + .build(); + } + + @Benchmark + public TagMap oldPerSpanStamps() { + TagMap tags = TagMap.create(); + tags.set(COMPONENT); + tags.set(KIND); + tags.set(LANGUAGE); + tags.set(ANALYTICS); + return tags; + } + + @Benchmark + public TagMap newBulkApply() { + TagMap tags = TagMap.create(); + tags.putAll(prototype.tags()); + return tags; + } + + @Benchmark + public TagMap newConstructionSeed() { + return prototype.tags().copy(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java new file mode 100644 index 00000000000..6a0d9f53e4c --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java @@ -0,0 +1,145 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import datadog.trace.api.TagMap; + +/** + * A baked-once, frozen descriptor of a span's constant initial state — the per-decorator constants + * (instrumentation name, span type, {@code span.kind}, component, …) that {@code + * BaseDecorator.afterStart} otherwise stamps one entry at a time, per span. + * + *

Composed through {@link #builder()}: authors set identity and constant tags via typed methods + * and never touch {@link TagMap} directly. {@link Builder#extends_(SpanPrototype)} inherits a base + * prototype (e.g. a SpanType base like {@code HttpServer}) so an integration adds only what's + * specific to it. Rides the existing {@code TagMap} API, so it's independent of any deeper TagMap + * rework — the internal seed can get faster without changing this surface. + * + *

v1 carries identity + constant tags. Derivation / canonicalization / lifecycle hooks are + * deliberately out — grown when the work that needs each arrives, not pre-slotted. + */ +public final class SpanPrototype { + /** The empty prototype — for spans created without a decorator-provided prototype. */ + public static final SpanPrototype NONE = builder().build(); + + public static Builder builder() { + return new Builder(); + } + + private final String instrumentationName; + private final CharSequence operationName; + private final CharSequence spanType; + private final TagMap tags; // frozen + + private SpanPrototype(final Builder builder) { + this.instrumentationName = builder.instrumentationName; + this.operationName = builder.operationName; + this.spanType = builder.spanType; + this.tags = builder.tags.immutableCopy(); + } + + public String instrumentationName() { + return instrumentationName; + } + + public CharSequence operationName() { + return operationName; + } + + public CharSequence spanType() { + return spanType; + } + + /** The frozen constant tags — the internal seed applied at span construction. */ + public TagMap tags() { + return tags; + } + + public static final class Builder { + private String instrumentationName; + private CharSequence operationName; + private CharSequence spanType; + // Internal accumulator — never exposed; authors compose via the typed methods below. + private final TagMap tags = TagMap.create(); + + private Builder() {} + + /** + * Inherit a base prototype's identity and constant tags (e.g. a SpanType base). Subsequent + * identity / {@code init*} calls on this builder override the inherited values. + */ + public Builder extends_(final SpanPrototype base) { + if (base != null) { + if (base.instrumentationName != null) { + this.instrumentationName = base.instrumentationName; + } + if (base.operationName != null) { + this.operationName = base.operationName; + } + if (base.spanType != null) { + this.spanType = base.spanType; + } + this.tags.putAll(base.tags); + } + return this; + } + + public Builder instrumentationName(final String[] instrumentationNames) { + return (instrumentationNames == null || instrumentationNames.length == 0) + ? this + : instrumentationName(instrumentationNames[0]); + } + + public Builder instrumentationName(final String instrumentationName) { + this.instrumentationName = instrumentationName; + return this; + } + + public Builder operationName(final CharSequence operationName) { + this.operationName = operationName; + return this; + } + + public Builder spanType(final CharSequence spanType) { + this.spanType = spanType; + return this; + } + + /** Sets {@code span.kind}. */ + public Builder initKind(final CharSequence kind) { + return initTag(Tags.SPAN_KIND, kind); + } + + /** Sets {@code component}. */ + public Builder initComponent(final CharSequence component) { + return initTag(Tags.COMPONENT, component); + } + + public Builder initTag(final String key, final CharSequence value) { + if (value != null) { + this.tags.set(key, value); + } + return this; + } + + public Builder initTag(final String key, final Object value) { + if (value != null) { + this.tags.set(key, value); + } + return this; + } + + /** + * Advanced/internal: reuse an already-built entry — a decorator's cached constant or a metric + * entry — rather than re-creating it. Authors should prefer the typed {@code init*} methods. + */ + public Builder initTag(final TagMap.EntryReader entry) { + if (entry != null) { + this.tags.set(entry); + } + return this; + } + + public SpanPrototype build() { + return new SpanPrototype(this); + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java new file mode 100644 index 00000000000..fa5a20c1b1f --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java @@ -0,0 +1,25 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class SpanPrototypeTest { + + @Test + void extendsInheritsBaseIdentityAndTagsThenOverrides() { + final SpanPrototype base = + SpanPrototype.builder() + .instrumentationName("base") + .spanType("base-type") + .initKind("server") + .build(); + final SpanPrototype derived = + SpanPrototype.builder().extends_(base).initComponent("netty").spanType("http").build(); + + assertEquals("base", derived.instrumentationName()); // inherited + assertEquals("http", derived.spanType()); // overridden + assertEquals("server", derived.tags().getString(Tags.SPAN_KIND)); // inherited tag + assertEquals("netty", derived.tags().getString(Tags.COMPONENT)); // added tag + } +} From 041687654c18ed9afb67e94f449248b4ad903b7d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 14:52:27 -0400 Subject: [PATCH 2/8] Drop null/empty SpanPrototype constants; add TagMap.Entry.isEmptyValue A prototype constant that is null or an empty CharSequence should be "no tag" -- matching AgentSpan.setTag and the decorators' cached-Entry path -- not a baked empty tag. Add TagMap.Entry.isEmptyValue as the single definition of an empty value (both Entry.create overloads now delegate to it), and gate SpanPrototype.Builder.initTag on it via the plain set(key, value) path so no Entry is allocated (the wrong path once tags are stored densely). Co-Authored-By: Claude Opus 4.8 --- .../main/java/datadog/trace/api/TagMap.java | 24 ++++++++++--------- .../instrumentation/api/SpanPrototype.java | 4 ++-- .../api/SpanPrototypeTest.java | 23 ++++++++++++++++++ 3 files changed, 38 insertions(+), 13 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 761320a2205..37335e8d2d8 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -177,6 +177,17 @@ public static final class Entry extends EntryChange */ static final byte ANY = 0; + /** + * Whether {@code value} is treated as "no tag" — a null, or an empty {@link CharSequence}. Set + * paths that honor the tag-filtering contract (e.g. {@code AgentSpan.setTag}, {@link + * SpanPrototype}) can gate on this without constructing an Entry — which matters once a dense + * store makes per-tag Entry allocation the wrong path. + */ + public static boolean isEmptyValue(Object value) { + return value == null + || (value instanceof CharSequence && ((CharSequence) value).length() == 0); + } + /** * Entry for {@code (tag, value)}, or null when {@code value} is null or an empty {@code * CharSequence} -- checked by runtime type, so an empty String passed as {@code Object} skips @@ -184,23 +195,14 @@ public static final class Entry extends EntryChange */ @Nullable public static final Entry create(@Nonnull String tag, Object value) { - if (value == null) { - return null; - } - if (value instanceof CharSequence && ((CharSequence) value).length() == 0) { - return null; - } - return TagMap.Entry.newAnyEntry(tag, value); + return isEmptyValue(value) ? null : TagMap.Entry.newAnyEntry(tag, value); } /** If value is non-null, returns a new TagMap.Entry If value is null or empty, returns null */ @Nullable public static final Entry create(@Nonnull String tag, CharSequence value) { // NOTE: From the static typing, we know that value is not a primitive box - - return (value == null || value.length() == 0) - ? null - : TagMap.Entry.newObjectEntry(tag, value); + return isEmptyValue(value) ? null : TagMap.Entry.newObjectEntry(tag, value); } public static final Entry create(@Nonnull String tag, boolean value) { diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java index 6a0d9f53e4c..c474b48dab0 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java @@ -114,14 +114,14 @@ public Builder initComponent(final CharSequence component) { } public Builder initTag(final String key, final CharSequence value) { - if (value != null) { + if (!TagMap.Entry.isEmptyValue(value)) { this.tags.set(key, value); } return this; } public Builder initTag(final String key, final Object value) { - if (value != null) { + if (!TagMap.Entry.isEmptyValue(value)) { this.tags.set(key, value); } return this; diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java index fa5a20c1b1f..bb3004fc0a3 100644 --- a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java @@ -1,6 +1,7 @@ package datadog.trace.bootstrap.instrumentation.api; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -22,4 +23,26 @@ void extendsInheritsBaseIdentityAndTagsThenOverrides() { assertEquals("server", derived.tags().getString(Tags.SPAN_KIND)); // inherited tag assertEquals("netty", derived.tags().getString(Tags.COMPONENT)); // added tag } + + @Test + void emptyOrNullConstantsAreDroppedNotBaked() { + // Match AgentSpan.setTag / the cached-Entry path: a null or empty constant is "no tag", not an + // empty tag. A raw tags.set would otherwise bake a tag that per-span stamping never emits. + final SpanPrototype proto = + SpanPrototype.builder() + .initComponent("") // empty -> dropped + .initKind("") // empty -> dropped + .initTag("empty.cs", "") // empty CharSequence -> dropped + .initTag("null.cs", (CharSequence) null) // null -> dropped + .initTag("null.obj", (Object) null) // null -> dropped + .initTag("kept", "v") // non-empty -> present + .build(); + + assertNull(proto.tags().getString(Tags.COMPONENT)); + assertNull(proto.tags().getString(Tags.SPAN_KIND)); + assertNull(proto.tags().getString("empty.cs")); + assertNull(proto.tags().getString("null.cs")); + assertNull(proto.tags().getString("null.obj")); + assertEquals("v", proto.tags().getString("kept")); // sanity: non-empty still stored + } } From 24fb71fcd93c16d4370af380239076739b5cffe9 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 15:33:36 -0400 Subject: [PATCH 3/8] Add SpanPrototype construction path: buildSpan/startSpan(SpanPrototype) Thread a SpanPrototype through span construction: AgentTracer gains buildSpan/startSpan(SpanPrototype, operationName) (defaults seed identity only, correct for the noop tracer, with an explicit NoopTracerAPI.startSpan override). CoreTracer overrides buildSpan to seed the prototype's frozen constant tags in buildSpanContext at the precedence slot just before the builder's own tags (prototype and builder form one precedence atom; explicit builder tags win), and overrides startSpan to seed builder-free via the static CoreSpanBuilder.startSpan path (no MultiSpanBuilder allocation, mirroring startSpan(String,...)). Explicit operationName wins; null falls back to the prototype's. Intercepted constants (e.g. span.kind) seed through the interceptor so their context side-effects still fire. Prototype params @Nonnull. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/core/CoreTracer.java | 75 +++++++++++++ .../core/SpanPrototypeConstructionTest.java | 100 ++++++++++++++++++ .../instrumentation/api/AgentTracer.java | 31 ++++++ 3 files changed, 206 insertions(+) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index c48d8f94df6..13bf01b6fcb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -75,6 +75,7 @@ import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; import datadog.trace.bootstrap.instrumentation.api.SpanLink; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.TagContext; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.civisibility.interceptor.CiVisibilityApmProtocolInterceptor; @@ -132,6 +133,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.zip.ZipOutputStream; +import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1041,6 +1043,27 @@ public CoreSpanBuilder buildSpan( return createMultiSpanBuilder(instrumentationName, operationName); } + /** + * Seeds identity (instrumentation name, operation, span type) and constant tags from a prototype. + * {@code operationName} overrides the prototype's when non-null — the explicit value wins, the + * prototype is the fallback. The prototype's tags are seeded during {@link CoreSpanBuilder} + * construction just before the builder's own tags, so explicit tags override prototype constants. + */ + @Override + public CoreSpanBuilder buildSpan( + @Nonnull final SpanPrototype prototype, CharSequence operationName) { + if (operationName == null) { + operationName = prototype.operationName(); + } + CoreSpanBuilder builder = + createMultiSpanBuilder(prototype.instrumentationName(), operationName); + builder.spanPrototype = prototype; + if (prototype.spanType() != null) { + builder.spanType = prototype.spanType(); + } + return builder; + } + MultiSpanBuilder createMultiSpanBuilder( final String instrumentationName, final CharSequence operationName) { return new MultiSpanBuilder(this, instrumentationName, operationName); @@ -1146,6 +1169,17 @@ public AgentSpan startSpan( this, instrumentationName, spanName, parent, CoreSpanBuilder.IGNORE_SCOPE, startTimeMicros); } + @Override + public AgentSpan startSpan( + @Nonnull final SpanPrototype prototype, final CharSequence operationName) { + return CoreSpanBuilder.startSpan( + this, + prototype, + operationName != null ? operationName : prototype.operationName(), + CoreSpanBuilder.USE_SCOPE, + CoreSpanBuilder.AUTO_ASSIGN_TIMESTAMP); + } + @Override public AgentScope activateSpan(AgentSpan span) { return scopeManager.activateSpan(span); @@ -1588,6 +1622,7 @@ public abstract static class CoreSpanBuilder implements AgentTracer.SpanBuilder // Builder attributes // Make sure any fields added here are also reset properly in ReusableSingleSpanBuilder.reset protected TagMap.Ledger tagLedger; + protected SpanPrototype spanPrototype = SpanPrototype.NONE; protected long timestampMicro; protected AgentSpanContext parent; protected String serviceName; @@ -1626,6 +1661,7 @@ protected static final DDSpan buildSpan( boolean errorFlag, CharSequence spanType, TagMap.Ledger tagLedger, + SpanPrototype spanPrototype, List links, Object builderRequestContextDataAppSec, Object builderRequestContextDataIast, @@ -1645,6 +1681,7 @@ protected static final DDSpan buildSpan( errorFlag, spanType, tagLedger, + spanPrototype, links, builderRequestContextDataAppSec, builderRequestContextDataIast, @@ -1730,6 +1767,7 @@ protected AgentSpan startImpl() { this.errorFlag, this.spanType, this.tagLedger, + this.spanPrototype, this.links, this.builderRequestContextDataAppSec, this.builderRequestContextDataIast, @@ -1756,6 +1794,33 @@ protected static final AgentSpan startSpan( false /* errorFlag */, null /* spanType */, null /* tagLedger */, + SpanPrototype.NONE /* spanPrototype */, + null /* links */, + null /* appSec */, + null /* iast */, + null /* ciViz */); + } + + protected static final AgentSpan startSpan( + final CoreTracer tracer, + final SpanPrototype prototype, + final CharSequence operationName, + final boolean ignoreScope, + final long timestampMicros) { + return startSpan( + tracer, + AUTO_ASSIGN_SPAN_ID, + prototype.instrumentationName(), + timestampMicros, + null /* serviceName */, + operationName, + null /* resourceName */, + null /* specifiedParentSpanContext */, + ignoreScope, + false /* errorFlag */, + prototype.spanType(), + null /* tagLedger */, + prototype, null /* links */, null /* appSec */, null /* iast */, @@ -1775,6 +1840,7 @@ protected static final AgentSpan startSpan( boolean errorFlag, CharSequence spanType, TagMap.Ledger tagLedger, + SpanPrototype spanPrototype, List links, Object builderRequestContextDataAppSec, Object builderRequestContextDataIast, @@ -1837,6 +1903,7 @@ protected static final AgentSpan startSpan( errorFlag, spanType, tagLedger, + spanPrototype, links, builderRequestContextDataAppSec, builderRequestContextDataIast, @@ -1975,6 +2042,7 @@ protected static final DDSpanContext buildSpanContext( boolean errorFlag, CharSequence spanType, TagMap.Ledger tagLedger, + SpanPrototype spanPrototype, List links, Object builderRequestContextDataAppSec, Object builderRequestContextDataIast, @@ -2213,6 +2281,12 @@ protected static final DDSpanContext buildSpanContext( // the builder. This is the order that the tags were added previously, but maybe the `tags` // set in the builder should come last, so that they override other tags. context.setAllTags(mergedTracerTags, mergedTracerTagsNeedsIntercept); + if (spanPrototype != SpanPrototype.NONE) { + // Seed the frozen constant tags through the interceptor. A cheaper bulk-share path that + // skips interception for non-intercepted tags is deferred to the dense-store / tag-registry + // work, which will expose intercept status at the internal-api level. + context.setAllTags(spanPrototype.tags()); + } context.setAllTags(tagLedger); context.setAllTags(coreTags, coreTagsNeedsIntercept); context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept); @@ -2293,6 +2367,7 @@ final boolean reset(String instrumentationName, CharSequence operationName) { this.operationName = operationName; if (this.tagLedger != null) this.tagLedger.reset(); + this.spanPrototype = SpanPrototype.NONE; this.timestampMicro = 0L; this.parent = null; this.serviceName = null; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java new file mode 100644 index 00000000000..c70820fdd52 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java @@ -0,0 +1,100 @@ +package datadog.trace.core; + +import static datadog.trace.bootstrap.instrumentation.api.Tags.COMPONENT; +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.common.writer.ListWriter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Verifies the SpanPrototype construction path: {@code buildSpan(prototype, operationName)} and + * {@code startSpan(prototype, operationName)} seed the prototype's identity + constant tags, with + * the explicit operationName / builder tags overriding the prototype's (prototype = defaults). + */ +public class SpanPrototypeConstructionTest extends DDCoreJavaSpecification { + + private ListWriter writer; + private CoreTracer tracer; + private SpanPrototype prototype; + + @BeforeEach + void setup() { + writer = new ListWriter(); + tracer = tracerBuilder().writer(writer).build(); + prototype = + SpanPrototype.builder() + .instrumentationName("test-instr") + .operationName("proto.op") + .spanType("web") + .initKind(SPAN_KIND_SERVER) + .initComponent("test-component") + .build(); + } + + @AfterEach + void cleanup() { + tracer.close(); + } + + @Test + void buildSpanSeedsPrototypeAndFallsBackToPrototypeOperationName() { + DDSpan span = (DDSpan) tracer.buildSpan(prototype, null).start(); + try { + assertEquals("proto.op", span.getOperationName().toString()); // null -> prototype's + assertEquals("web", span.getSpanType()); + assertEquals("test-component", span.getTags().get(COMPONENT)); // constant tag seeded + } finally { + span.finish(); + } + } + + @Test + void seedsSpanKindOrdinalAndTag() { + // span.kind is intercepted (its ordinal drives isOutbound). The prototype's tags seed through + // the interceptor, so BOTH the ordinal side-effect and the span.kind tag Entry must land. + DDSpan span = (DDSpan) tracer.buildSpan(prototype, null).start(); + try { + assertEquals(SPAN_KIND_SERVER, span.getSpanKindString()); // ordinal side-effect applied + assertEquals(SPAN_KIND_SERVER, span.getTags().get(SPAN_KIND)); // tag (shared Entry) present + } finally { + span.finish(); + } + } + + @Test + void explicitOperationNameOverridesPrototype() { + DDSpan span = (DDSpan) tracer.buildSpan(prototype, "explicit.op").start(); + try { + assertEquals("explicit.op", span.getOperationName().toString()); // explicit wins + } finally { + span.finish(); + } + } + + @Test + void startSpanSeedsPrototype() { + DDSpan span = (DDSpan) tracer.startSpan(prototype, null); + try { + assertEquals("proto.op", span.getOperationName().toString()); + assertEquals("test-component", span.getTags().get(COMPONENT)); + } finally { + span.finish(); + } + } + + @Test + void explicitBuilderTagOverridesPrototypeConstant() { + // prototype seeds `component` just before the builder's own tags, so the explicit withTag wins + DDSpan span = (DDSpan) tracer.buildSpan(prototype, null).withTag(COMPONENT, "override").start(); + try { + assertEquals("override", span.getTags().get(COMPONENT)); + } finally { + span.finish(); + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java index e10ee9e3fe7..4d10d02cf54 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java @@ -350,6 +350,30 @@ default AgentSpan blackholeSpan() { */ SpanBuilder buildSpan(String instrumentationName, CharSequence spanName); + /** + * Returns a SpanBuilder seeded from a {@link SpanPrototype}: the prototype supplies the + * instrumentation name, span type, and constant tags. {@code operationName} overrides the + * prototype's when non-null — the explicit value wins, the prototype is the fallback. + * + *

This default seeds identity only; a real tracer should override it to also seed the + * prototype's tags (see {@code CoreTracer}). The no-op tracer discards tags, so identity-only + * is correct there. + */ + default SpanBuilder buildSpan(@Nonnull SpanPrototype prototype, CharSequence operationName) { + return buildSpan( + prototype.instrumentationName(), + operationName != null ? operationName : prototype.operationName()); + } + + /** + * Creates and starts a span seeded from a {@link SpanPrototype}. This is the + * auto-instrumentation entry point mirroring {@link #startSpan(String, CharSequence)}; see + * {@link #buildSpan(SpanPrototype, CharSequence)}. + */ + default AgentSpan startSpan(@Nonnull SpanPrototype prototype, CharSequence operationName) { + return buildSpan(prototype, operationName).start(); + } + /** * Returns a SpanBuilder that can be used to produce one and only one span. By imposing the * single span creation limitation, this method is more efficient than {@link #buildSpan} @@ -453,6 +477,13 @@ public AgentSpan startSpan(final String instrumentationName, final CharSequence return NoopSpan.INSTANCE; } + @Override + public AgentSpan startSpan( + @Nonnull final SpanPrototype prototype, final CharSequence operationName) { + // The default routes through buildSpan(String,...), which is null on the noop tracer -> NPE. + return NoopSpan.INSTANCE; + } + @Override public AgentSpan startSpan( final String instrumentationName, final CharSequence spanName, final long startTimeMicros) { From 87189521d643ab57ed530b1c5603ae9ce112a1da Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 15:34:38 -0400 Subject: [PATCH 4/8] Carry integration name in SpanPrototype; make the builder surface uniformly init* BaseDecorator.afterStart sets the integration name as a side effect alongside the component tag (setIntegrationName(component)), which IntegrationAdder later serializes as _dd.integration. A prototype baking only the component tag would drop that. Add initComponentAndIntegration(component): sets the component tag AND records it as the integration name (inherited via extends_), applied via setIntegrationName at construction. Rename the builder setters to a uniform init* surface now that a component sibling exists and to convey "everything here bakes the prototype's initial state": initComponent -> initComponentOnly, instrumentationName -> initInstrumentationName(s), operationName -> initOperationName, spanType -> initSpanType. Accessors are unchanged. Renames are confined to SpanPrototype.Builder and its callers. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/core/CoreTracer.java | 6 ++ .../core/SpanPrototypeConstructionTest.java | 67 +++++++++++++++++-- .../trace/api/SpanPrototypeBenchmark.java | 11 ++- .../instrumentation/api/SpanPrototype.java | 48 +++++++++++-- .../api/SpanPrototypeTest.java | 12 ++-- 5 files changed, 126 insertions(+), 18 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 13bf01b6fcb..abaf9b3fa3e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -2286,6 +2286,12 @@ protected static final DDSpanContext buildSpanContext( // skips interception for non-intercepted tags is deferred to the dense-store / tag-registry // work, which will expose intercept status at the internal-api level. context.setAllTags(spanPrototype.tags()); + // Apply the integration-name side effect BaseDecorator.afterStart performs alongside the + // component tag; IntegrationAdder serializes it as _dd.integration. + final CharSequence integrationName = spanPrototype.integrationName(); + if (integrationName != null) { + context.setIntegrationName(integrationName); + } } context.setAllTags(tagLedger); context.setAllTags(coreTags, coreTagsNeedsIntercept); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java index c70820fdd52..7a3722d77cf 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java @@ -4,6 +4,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.common.writer.ListWriter; @@ -28,11 +29,11 @@ void setup() { tracer = tracerBuilder().writer(writer).build(); prototype = SpanPrototype.builder() - .instrumentationName("test-instr") - .operationName("proto.op") - .spanType("web") + .initInstrumentationName("test-instr") + .initOperationName("proto.op") + .initSpanType("web") .initKind(SPAN_KIND_SERVER) - .initComponent("test-component") + .initComponentOnly("test-component") .build(); } @@ -97,4 +98,62 @@ void explicitBuilderTagOverridesPrototypeConstant() { span.finish(); } } + + @Test + void initComponentAndIntegrationSetsIntegrationName() { + // Mirrors BaseDecorator.afterStart: the component tag is seeded AND the integration name is set + // on the context, which IntegrationAdder serializes as _dd.integration (field -> tag mapping is + // covered by IntegrationAdderTest). + SpanPrototype proto = + SpanPrototype.builder() + .initInstrumentationName("test-instr") + .initComponentAndIntegration("netty") + .build(); + DDSpan span = (DDSpan) tracer.buildSpan(proto, "op").start(); + try { + assertEquals("netty", span.getTags().get(COMPONENT)); // component tag seeded + assertEquals("netty", ((DDSpanContext) span.spanContext()).getIntegrationName()); + } finally { + span.finish(); + } + } + + @Test + void initComponentOnlyDoesNotSetIntegrationName() { + // initComponentOnly is tag-only: no integration-name side effect, so no _dd.integration. + SpanPrototype proto = + SpanPrototype.builder() + .initInstrumentationName("test-instr") + .initComponentOnly("netty") + .build(); + DDSpan span = (DDSpan) tracer.buildSpan(proto, "op").start(); + try { + assertEquals("netty", span.getTags().get(COMPONENT)); // tag present + assertNull(((DDSpanContext) span.spanContext()).getIntegrationName()); // but no integration + } finally { + span.finish(); + } + } + + @Test + void extendsWithComponentOnlyOverrideLeavesInheritedIntegrationName() { + // Documents a known desync: overriding an inherited initComponentAndIntegration component with + // tag-only initComponentOnly does NOT clear the inherited integration name. Use + // initComponentAndIntegration to override both together. + SpanPrototype base = + SpanPrototype.builder() + .initInstrumentationName("test-instr") + .initComponentAndIntegration("netty") + .build(); + SpanPrototype derived = + SpanPrototype.builder().extends_(base).initComponentOnly("other").build(); + DDSpan span = (DDSpan) tracer.buildSpan(derived, "op").start(); + try { + assertEquals("other", span.getTags().get(COMPONENT)); // component overridden + // integration name stays inherited from the base (the documented desync) + assertEquals("netty", ((DDSpanContext) span.spanContext()).getIntegrationName()); + } finally { + span.finish(); + } + } } diff --git a/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java index 2ef2b182dd4..c75e9eefa94 100644 --- a/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java @@ -31,8 +31,13 @@ * * *

Isolates the constant-application only (not span creation or the {@code afterStart} virtual - * chain), so the delta is purely N-stamps vs. bulk-copy. Run with {@code -prof gc} — the - * interesting axes are ops/s and B/op. + * chain), so the delta is purely N-stamps vs. bulk-copy. All arms apply tags at the {@code TagMap} + * level and skip the per-tag {@code TagInterceptor} dispatch that the real construction seed still + * incurs (span.kind, analytics-rate, ... are intercepted). That dispatch is a common cost + * on both the old and new production paths, so it cancels in the delta — but it means the absolute + * ops/s and the new/old ratio here are a TagMap-level upper bound, not the end-to-end win. (The + * interceptor-free bulk-share is what TagInterceptor retirement eventually unlocks; the current + * seed intercepts.) Run with {@code -prof gc} — the interesting axes are ops/s and B/op. */ @State(Scope.Thread) @BenchmarkMode(Mode.Throughput) @@ -60,7 +65,7 @@ public void setUp() { // Baked once — the same constants, composed through the builder. prototype = SpanPrototype.builder() - .initComponent("netty") + .initComponentOnly("netty") .initKind(Tags.SPAN_KIND_SERVER) .initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE) .initTag(ANALYTICS) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java index c474b48dab0..24ccd2b39d8 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java @@ -27,12 +27,14 @@ public static Builder builder() { private final String instrumentationName; private final CharSequence operationName; private final CharSequence spanType; + private final CharSequence integrationName; private final TagMap tags; // frozen private SpanPrototype(final Builder builder) { this.instrumentationName = builder.instrumentationName; this.operationName = builder.operationName; this.spanType = builder.spanType; + this.integrationName = builder.integrationName; this.tags = builder.tags.immutableCopy(); } @@ -48,6 +50,15 @@ public CharSequence spanType() { return spanType; } + /** + * The integration name to record on the span context ({@code setIntegrationName}), which the + * IntegrationAdder serializer step turns into {@code _dd.integration}. Null unless set via {@link + * Builder#initComponentAndIntegration}. Mirrors {@code BaseDecorator.afterStart}'s side effect. + */ + public CharSequence integrationName() { + return integrationName; + } + /** The frozen constant tags — the internal seed applied at span construction. */ public TagMap tags() { return tags; @@ -57,6 +68,7 @@ public static final class Builder { private String instrumentationName; private CharSequence operationName; private CharSequence spanType; + private CharSequence integrationName; // Internal accumulator — never exposed; authors compose via the typed methods below. private final TagMap tags = TagMap.create(); @@ -77,28 +89,31 @@ public Builder extends_(final SpanPrototype base) { if (base.spanType != null) { this.spanType = base.spanType; } + if (base.integrationName != null) { + this.integrationName = base.integrationName; + } this.tags.putAll(base.tags); } return this; } - public Builder instrumentationName(final String[] instrumentationNames) { + public Builder initInstrumentationNames(final String[] instrumentationNames) { return (instrumentationNames == null || instrumentationNames.length == 0) ? this - : instrumentationName(instrumentationNames[0]); + : initInstrumentationName(instrumentationNames[0]); } - public Builder instrumentationName(final String instrumentationName) { + public Builder initInstrumentationName(final String instrumentationName) { this.instrumentationName = instrumentationName; return this; } - public Builder operationName(final CharSequence operationName) { + public Builder initOperationName(final CharSequence operationName) { this.operationName = operationName; return this; } - public Builder spanType(final CharSequence spanType) { + public Builder initSpanType(final CharSequence spanType) { this.spanType = spanType; return this; } @@ -108,11 +123,30 @@ public Builder initKind(final CharSequence kind) { return initTag(Tags.SPAN_KIND, kind); } - /** Sets {@code component}. */ - public Builder initComponent(final CharSequence component) { + /** + * Sets the {@code component} tag only. Does NOT touch the integration name — so overriding a + * component inherited via {@link #initComponentAndIntegration} with this leaves the inherited + * integration name in place (a desync). Use {@link #initComponentAndIntegration} to override + * both together. + */ + public Builder initComponentOnly(final CharSequence component) { return initTag(Tags.COMPONENT, component); } + /** + * Sets the {@code component} tag AND records it as the integration name — the {@code + * BaseDecorator.afterStart} pairing (component tag + {@code setIntegrationName(component)}, + * which the IntegrationAdder serializer turns into {@code _dd.integration}). Null/empty is a + * no-op for both. + */ + public Builder initComponentAndIntegration(final CharSequence component) { + if (!TagMap.Entry.isEmptyValue(component)) { + this.tags.set(Tags.COMPONENT, component); + this.integrationName = component; + } + return this; + } + public Builder initTag(final String key, final CharSequence value) { if (!TagMap.Entry.isEmptyValue(value)) { this.tags.set(key, value); diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java index bb3004fc0a3..522a2918036 100644 --- a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java @@ -11,12 +11,16 @@ class SpanPrototypeTest { void extendsInheritsBaseIdentityAndTagsThenOverrides() { final SpanPrototype base = SpanPrototype.builder() - .instrumentationName("base") - .spanType("base-type") + .initInstrumentationName("base") + .initSpanType("base-type") .initKind("server") .build(); final SpanPrototype derived = - SpanPrototype.builder().extends_(base).initComponent("netty").spanType("http").build(); + SpanPrototype.builder() + .extends_(base) + .initComponentOnly("netty") + .initSpanType("http") + .build(); assertEquals("base", derived.instrumentationName()); // inherited assertEquals("http", derived.spanType()); // overridden @@ -30,7 +34,7 @@ void emptyOrNullConstantsAreDroppedNotBaked() { // empty tag. A raw tags.set would otherwise bake a tag that per-span stamping never emits. final SpanPrototype proto = SpanPrototype.builder() - .initComponent("") // empty -> dropped + .initComponentOnly("") // empty -> dropped .initKind("") // empty -> dropped .initTag("empty.cs", "") // empty CharSequence -> dropped .initTag("null.cs", (CharSequence) null) // null -> dropped From b8aa652551469ea2462b7c6a6f11869065349c7b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 19:30:24 -0400 Subject: [PATCH 5/8] Add span-creation benchmark for the SpanPrototype construction path A dd-trace-core JMH benchmark covering the full create -> (tag) -> finish lifecycle, finished against a no-op DropWriter so -prof gc isolates create/tag/finish allocation from serialization. Pairs baseline shapes (web-server 7 tags, JDBC 9 tags; setTag and builder-withTag) with prototype arms: buildSpan(SpanPrototype).start() and the builder-free startSpan(SpanPrototype). Measured (Threads(8), -f3 -wi5 -i5 -prof gc): prototype construction cuts gc.alloc.rate.norm ~-5% web (-80 B/op) / ~-10% jdbc (-120 B/op) vs baseline -- tracking the number of baked constants (fewer per-span TagMap.Entry allocations). The builder-free startSpan is deterministic (no MultiSpanBuilder); buildSpan's builder is escape-analyzed away in this shallow micro, so startSpan is the EA-independent path for production's deeper/megamorphic call sites. Co-Authored-By: Claude Opus 4.8 --- .../trace/core/SpanCreationBenchmark.java | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationBenchmark.java index d288463faad..c2bcd4a1cfb 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationBenchmark.java @@ -3,6 +3,7 @@ import static java.util.concurrent.TimeUnit.MICROSECONDS; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -42,7 +43,10 @@ * five-method {@code Writer} interface (implemented as a no-op {@link DropWriter}). If you add to * it, keep it inside that stable surface or grafting it onto old tags for the historical curve will * stop compiling. (Source rebuilds only reach ~v1.53 — older tags hit dead build-time dependencies; - * deeper history is a published-jar job.) + * deeper history is a published-jar job.) The {@code *ViaPrototype} / {@code + * *ViaPrototypeStartSpan} arms are the exception: they exercise {@link SpanPrototype} (new in this + * PR) and do not graft onto pre-SpanPrototype tags — the historical table above covers the baseline + * arms only. * *

Spans are finished against {@link DropWriter} so the create/tag/finish allocation is isolated * from serialization and agent I/O — those live on a different lever and would otherwise leak into @@ -132,11 +136,33 @@ public class SpanCreationBenchmark { CoreTracer tracer; + // Baked-once prototypes carrying only the type-constant subset each baseline sets individually + // (component + span.kind; jdbc also db.type). The dynamic tags are set per-span in both arms, so + // the *ViaPrototype vs *Span delta isolates the construction-path seeding of just those + // constants. + SpanPrototype webProto; + SpanPrototype jdbcProto; + @Setup public void setup(Blackhole blackhole) { // DropWriter keeps finish() from pulling in serialization / agent I/O, so -prof gc reflects // span creation + tagging + PendingTrace completion only. this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + this.webProto = + SpanPrototype.builder() + .initInstrumentationName(INSTRUMENTATION_NAME) + .initOperationName(SERVER_OPERATION_NAME) + .initComponentOnly(COMPONENT_VALUE) + .initKind(Tags.SPAN_KIND_SERVER) + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(INSTRUMENTATION_NAME) + .initOperationName(JDBC_OPERATION_NAME) + .initComponentOnly(DB_COMPONENT_VALUE) + .initKind(Tags.SPAN_KIND_CLIENT) + .initTag(Tags.DB_TYPE, DB_TYPE_VALUE) + .build(); } @TearDown @@ -211,4 +237,69 @@ public void jdbcClientSpan() { span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE); span.finish(); } + + /** + * Web-server-shaped span via {@link SpanPrototype}: the type-constants (component, span.kind) + * ride a baked-once prototype seeded at construction; the dynamic http.* / peer.port tags are set + * per-span, as real instrumentation does. Compare against {@link #webServerSpan} (identical tags, + * all set individually) to read the prototype's construction-path win on a full span. + */ + @Benchmark + public void webServerSpanViaPrototype() { + AgentSpan span = tracer.buildSpan(webProto, null).start(); // null -> prototype's operationName + span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE); + span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE); + span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE); + span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE); + span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE); + span.finish(); + } + + /** + * JDBC/DB-client-shaped span via {@link SpanPrototype}: component, span.kind, and db.type ride + * the prototype; the dynamic db.* / peer.* tags are set per-span. Compare against {@link + * #jdbcClientSpan}. + */ + @Benchmark + public void jdbcClientSpanViaPrototype() { + AgentSpan span = tracer.buildSpan(jdbcProto, null).start(); + span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE); + span.setTag(Tags.DB_USER, DB_USER_VALUE); + span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE); + span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE); + span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE); + span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE); + span.finish(); + } + + /** + * Web-server-shaped span via {@code startSpan(SpanPrototype, ...)} — the builder-free + * construction entry (no MultiSpanBuilder allocation), the auto-instrumentation path. Compare + * against {@link #webServerSpanViaPrototype} (same prototype, but {@code buildSpan(...).start()} + * allocates a builder) to read the builder-free saving, and against {@link #webServerSpan} for + * the full win. + */ + @Benchmark + public void webServerSpanViaPrototypeStartSpan() { + AgentSpan span = tracer.startSpan(webProto, null); // null -> prototype's operationName + span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE); + span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE); + span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE); + span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE); + span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE); + span.finish(); + } + + /** JDBC/DB-client-shaped span via the builder-free {@code startSpan(SpanPrototype, ...)}. */ + @Benchmark + public void jdbcClientSpanViaPrototypeStartSpan() { + AgentSpan span = tracer.startSpan(jdbcProto, null); + span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE); + span.setTag(Tags.DB_USER, DB_USER_VALUE); + span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE); + span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE); + span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE); + span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE); + span.finish(); + } } From 36913594e19708f9b8945ddeb63a8b4c3522b44a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 22 Jul 2026 17:53:27 -0400 Subject: [PATCH 6/8] Add AgentSpan/DDSpanContext.apply(SpanPrototype) seam; route construction through it Introduce apply(SpanPrototype) as the single seam for stamping a prototype's constant initial state. It applies span type, constant tags, and integration name as fallback defaults -- only where the span has not already set them -- so it never clobbers explicit values, is order-independent, and self-neutralizes once construction has already seeded the same prototype. DDSpanContext.apply is the authoritative implementation (the context owns the tag map and will host the eventual bulk-share fast path + identity short-circuit); DDSpan.apply routes straight to it. The AgentSpan default is the best-effort fallback for non-core spans. The construction path (CoreSpanBuilder) now calls context.apply(prototype) instead of inlining the tag + integration-name seeding. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/core/CoreTracer.java | 14 ++--- .../main/java/datadog/trace/core/DDSpan.java | 8 +++ .../datadog/trace/core/DDSpanContext.java | 56 +++++++++++++++++++ .../instrumentation/api/AgentSpan.java | 37 ++++++++++++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index abaf9b3fa3e..4ce535fd072 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -2282,16 +2282,10 @@ protected static final DDSpanContext buildSpanContext( // set in the builder should come last, so that they override other tags. context.setAllTags(mergedTracerTags, mergedTracerTagsNeedsIntercept); if (spanPrototype != SpanPrototype.NONE) { - // Seed the frozen constant tags through the interceptor. A cheaper bulk-share path that - // skips interception for non-intercepted tags is deferred to the dense-store / tag-registry - // work, which will expose intercept status at the internal-api level. - context.setAllTags(spanPrototype.tags()); - // Apply the integration-name side effect BaseDecorator.afterStart performs alongside the - // component tag; IntegrationAdder serializes it as _dd.integration. - final CharSequence integrationName = spanPrototype.integrationName(); - if (integrationName != null) { - context.setIntegrationName(integrationName); - } + // Seed the prototype's constant tags + integration name as fallback defaults (span type was + // already seeded onto the builder). apply never clobbers, so tags set below still win, and + // this is the same seam decorator afterStart uses. + context.apply(spanPrototype); } context.setAllTags(tagLedger); context.setAllTags(coreTags, coreTagsNeedsIntercept); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index a288c405e6f..394ce6522d5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -31,6 +31,7 @@ import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.SpanWrapper; import datadog.trace.core.util.StackTraces; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -671,6 +672,13 @@ public final DDSpan setSpanType(final CharSequence type) { return this; } + @Override + public void apply(@Nonnull final SpanPrototype prototype) { + // Route straight to the context (owner of the tag map + future fast path) rather than through + // the interface default's per-setter delegation. + context.apply(prototype); + } + // Getters @Override 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 6120502ec09..27df74fb2e0 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 @@ -30,6 +30,7 @@ import datadog.trace.bootstrap.instrumentation.api.ProfilerContext; import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.propagation.PropagationTags; @@ -539,6 +540,61 @@ public void setSpanType(final CharSequence spanType) { this.spanType = spanType; } + /** + * Applies a {@link SpanPrototype} as fallback defaults: stamps its span type, constant tags, and + * integration name only where the span has not already set them. Prototype values are the lowest + * precedence -- anything explicitly set (a builder {@code withSpanType}, explicit tags, an + * earlier decorator) wins. Because it never clobbers, {@code apply} is order-independent and + * self-neutralizes once construction has already seeded the same prototype. + * + *

This is the shared seam for both the construction path ({@code CoreSpanBuilder}) and + * decorator {@code afterStart} (via {@link DDSpan#apply}). The context owns the tag map, so the + * eventual cheaper bulk-share path (skipping interception for non-intercepted tags) and the + * identity short-circuit will land here -- deferred to the dense-store / tag-registry work, which + * exposes intercept status at the internal-api level. Until then the constant tags route through + * the interceptor, identical to the per-tag calls this replaces. + */ + public void apply(@Nonnull final SpanPrototype prototype) { + if (this.spanType == null) { + final CharSequence spanType = prototype.spanType(); + if (spanType != null) { + setSpanType(spanType); + } + } + seedAbsentTags(prototype.tags()); + if (this.integrationName == null) { + final CharSequence integrationName = prototype.integrationName(); + if (integrationName != null) { + setIntegrationName(integrationName); + } + } + } + + /** + * Seeds tags that are not already present, routed through the interceptor. Mirrors {@link + * #setAllTags(TagMap, boolean)}'s intercepting path but skips any key already set, so explicit + * tags keep precedence over the prototype's constant defaults. + */ + private void seedAbsentTags(final TagMap map) { + if (map == null) { + return; + } + synchronized (unsafeTags) { + map.forEach( + this, + (ctx, tagEntry) -> { + final String tag = tagEntry.tag(); + if (ctx.unsafeTags.containsKey(tag)) { + return; + } + final Object value = tagEntry.objectValue(); + if (!ctx.tagInterceptor.interceptTag(ctx, tag, value)) { + ctx.unsafeTags.set(tagEntry); + } + }); + } + } + /** Forces the local root span sampling decision to keep according manual mechanism. */ public void forceKeep() { forceKeep(SamplingMechanism.MANUAL); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index c1b38140d22..1952853dfeb 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -218,6 +218,43 @@ default boolean isValid() { boolean isOutbound(); + /** + * Applies a {@link SpanPrototype} as fallback defaults: stamps its span type, constant tags, and + * integration name only where this span has not already set them. Prototype values are the lowest + * precedence -- anything explicitly set wins -- so {@code apply} never clobbers, is + * order-independent, and self-neutralizes once construction has already seeded the same + * prototype. + * + *

This is the single seam through which a prototype's constant initial state is applied, + * shared by the construction path (buildSpan/startSpan) and decorator {@code afterStart}. Core + * spans override to route straight to the context, which owns the tag map and will host the + * eventual fast path (bulk share / identity short-circuit); this default is the best-effort + * fallback for other span implementations. + */ + default void apply(@Nonnull final SpanPrototype prototype) { + if (getSpanType() == null) { + final CharSequence spanType = prototype.spanType(); + if (spanType != null) { + setSpanType(spanType); + } + } + + // Prototype tags are fallback defaults: only fill keys this span has not already set. + prototype + .tags() + .forEach( + (tag, value) -> { + if (getTag(tag) == null) { + setTag(tag, value); + } + }); + + final CharSequence integrationName = prototype.integrationName(); + if (integrationName != null) { + spanContext().setIntegrationName(integrationName); + } + } + default AgentSpan asAgentSpan() { return this; } From 911fc5763b68284b6778a438b7d6c308f1022fc5 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 22 Jul 2026 11:42:33 -0400 Subject: [PATCH 7/8] Apply SpanPrototype in decorator afterStart; migrate afterStart tests off mocks Have BaseDecorator/ServerDecorator/ClientDecorator build a lazily-cached SpanPrototype (extension chain mirroring the decorator hierarchy) and apply it in afterStart via span.setSpanType/setAllTags/setIntegrationName, replacing the per-Entry setTag calls. Behavior-identical: setAllTags runs the same constant tags through the same interceptor path the per-tag calls used. Migrate the four afterStart specs from Spock mock-interaction assertions to a state-based harness (RecordingSpan/RecordingSpanContext accumulate applied state; ExpectedSpanState asserts the whole state at once), with three leniency modes matching Spock's polymorphic feature-method inheritance across the decorator hierarchy. Other specs (onPeerConnection/onConnection/onStatement/ beforeFinish) are unchanged. Also drop the born-dead SpanPrototype.Builder.initInstrumentationNames(String[]) overload (no caller); initInstrumentationName covers the single-name case. Co-Authored-By: Claude Opus 4.8 --- .../decorator/BaseDecorator.java | 62 ++-- .../decorator/ClientDecorator.java | 33 +-- .../decorator/ServerDecorator.java | 20 +- .../decorator/BaseDecoratorTest.groovy | 28 +- .../decorator/ClientDecoratorTest.groovy | 32 +-- .../DatabaseClientDecoratorTest.groovy | 24 +- .../decorator/ServerDecoratorTest.groovy | 32 +-- .../decorator/ExpectedSpanState.java | 153 ++++++++++ .../decorator/RecordingSpan.java | 271 ++++++++++++++++++ .../decorator/RecordingSpanContext.java | 62 ++++ .../instrumentation/api/SpanPrototype.java | 6 - 11 files changed, 589 insertions(+), 134 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpan.java create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpanContext.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java index b52de4fa192..14c7776c649 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java @@ -12,6 +12,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; import java.lang.reflect.Method; import java.net.Inet4Address; @@ -45,8 +46,9 @@ public String apply(Class clazz) { private final TagMap.Entry traceAnalyticsEntry; - // Deliberately not volatile, reading null and repeating the calculation is safe - private TagMap.Entry cachedComponentEntry = null; + // Deliberately not volatile: reading a stale null and rebuilding is safe. SpanPrototype is + // frozen, so a benign race produces two equivalent prototypes and either is fine. + private SpanPrototype cachedSpanPrototype = null; protected BaseDecorator() { final Config config = Config.get(); @@ -72,18 +74,38 @@ protected BaseDecorator() { protected abstract CharSequence component(); - /** Caches the component TagMap.Entry, so it isn't recreated for every trace */ - protected final TagMap.Entry componentEntry() { - // DQH = Tried calling component() in the constructor, but that had issues with static - // field ordering. That was caught be an integration test, but I didn't want to risk - // breaking other integrations where the test is not as thorough. - - // This approach while more complicated doesn't have any field initialization ordering issues. - TagMap.Entry componentEntry = cachedComponentEntry; - if (componentEntry == null) { - cachedComponentEntry = componentEntry = TagMap.Entry.create(Tags.COMPONENT, component()); + /** + * The baked-once {@link SpanPrototype} carrying this decorator's constant identity and tags: span + * type, component, integration name, and — via the {@link ServerDecorator} / {@link + * ClientDecorator} extensions — span kind and language. + * + *

Built lazily on first access, not in the constructor: {@link #component()}, {@link + * #spanType()}, and (in {@link ClientDecorator}) {@code spanKind()} are overridable and may + * reference statics that are not yet initialized while the decorator singleton is under + * construction. Deferring the build sidesteps that field-initialization-ordering hazard (the same + * one the old per-{@link TagMap.Entry} caches guarded against) while collapsing those several + * caches into a single object. Not volatile: {@link SpanPrototype} is frozen, so a benign race + * rebuilds an equivalent prototype. + */ + protected final SpanPrototype spanPrototype() { + SpanPrototype prototype = cachedSpanPrototype; + if (prototype == null) { + cachedSpanPrototype = prototype = buildSpanPrototype(); } - return componentEntry; + return prototype; + } + + /** + * Builds this decorator's {@link SpanPrototype}. Subclasses extend the chain with {@link + * SpanPrototype.Builder#extends_} to add their level's constants (see {@link ServerDecorator} / + * {@link ClientDecorator}), mirroring the decorator class hierarchy. Called once per decorator, + * lazily — see {@link #spanPrototype()}. + */ + protected SpanPrototype buildSpanPrototype() { + return SpanPrototype.builder() + .initSpanType(spanType()) + .initComponentAndIntegration(component()) + .build(); } protected boolean traceAnalyticsDefault() { @@ -91,16 +113,10 @@ protected boolean traceAnalyticsDefault() { } public void afterStart(final AgentSpan span) { - if (spanType() != null) { - span.setSpanType(spanType()); - } - - span.setTag(componentEntry()); - - // DQH - Could retrieve the value from componentEntry and cast to avoid the virtual call, - // unclear which option is better here - final CharSequence component = component(); - span.spanContext().setIntegrationName(component); + // Stamps the prototype's constant span type, tags, and integration name as fallback defaults. + // apply is the single seam the construction-seeding path shares; because it never clobbers, it + // self-neutralizes once construction has already seeded the same prototype. + span.apply(spanPrototype()); // null handled by setMetric span.setMetric(traceAnalyticsEntry); diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ClientDecorator.java index 681a0e7a9d8..c47b45e3506 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ClientDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ClientDecorator.java @@ -1,43 +1,34 @@ package datadog.trace.bootstrap.instrumentation.decorator; -import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; public abstract class ClientDecorator extends BaseDecorator { - // Deliberately not volatile, reading a stale null and creating an extra Entry is safe - private TagMap.Entry cachedSpanKindEntry = null; protected abstract String service(); - /** Caches span kind entry to reduce allocation */ - private final TagMap.Entry spanKindEntry() { - // DQH - I considered moving the creation of the TagMap.Entry into a ClientDecorator - // constructor, but that introduces a subtle ordering requirement. - - // If the spanKind method refers to a static that isn't yet initialized, - // then spanKind will return null when the Decorator singleton is being constructed. - - // Such an ordering problem did occur with similar changes in BaseDecorator, so I've - // decided to be cautious here, too. - TagMap.Entry kindEntry = cachedSpanKindEntry; - if (kindEntry == null) { - cachedSpanKindEntry = kindEntry = TagMap.Entry.create(Tags.SPAN_KIND, spanKind()); - } - return kindEntry; - } - protected String spanKind() { return Tags.SPAN_KIND_CLIENT; } + @Override + protected SpanPrototype buildSpanPrototype() { + // Extend the base prototype with the client-level span.kind. spanKind() is overridable and may + // read a not-yet-initialized static during singleton construction -- building lazily (via + // spanPrototype()) preserves the ordering safety the old cached spanKindEntry provided. + return SpanPrototype.builder() + .extends_(super.buildSpanPrototype()) + .initKind(spanKind()) + .build(); + } + @Override public void afterStart(final AgentSpan span) { final String service = service(); if (service != null) { span.setServiceName(service, component()); } - span.setTag(spanKindEntry()); // Generate metrics for all client spans. span.setMeasured(true); diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ServerDecorator.java index cb0fcfe1f64..dd3b1df3e88 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/ServerDecorator.java @@ -1,21 +1,19 @@ package datadog.trace.bootstrap.instrumentation.decorator; import datadog.trace.api.DDTags; -import datadog.trace.api.TagMap; -import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; public abstract class ServerDecorator extends BaseDecorator { - private static final TagMap.Entry SPAN_KIND_ENTRY = - TagMap.Entry.create(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); - private static final TagMap.Entry LANG_ENTRY = - TagMap.Entry.create(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE); @Override - public void afterStart(final AgentSpan span) { - span.setTag(SPAN_KIND_ENTRY); - span.setTag(LANG_ENTRY); - - super.afterStart(span); + protected SpanPrototype buildSpanPrototype() { + // Extend the base prototype with the server-level constants (span.kind=server, language). The + // prototype chain mirrors the decorator class hierarchy; base afterStart applies the whole set. + return SpanPrototype.builder() + .extends_(super.buildSpanPrototype()) + .initKind(Tags.SPAN_KIND_SERVER) + .initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE) + .build(); } } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy index 5b70cba2085..4a71c7aae43 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/BaseDecoratorTest.groovy @@ -1,6 +1,5 @@ package datadog.trace.bootstrap.instrumentation.decorator -import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities @@ -25,25 +24,20 @@ class BaseDecoratorTest extends DDSpecification { def spanContext = Mock(AgentSpanContext) def "test afterStart"() { + setup: + def recordingSpan = new RecordingSpan() + when: - decorator.afterStart(span) + decorator.afterStart(recordingSpan) then: - 1 * span.setSpanType(decorator.spanType()) - 1 * span.setTag(TagMap.Entry.create(Tags.COMPONENT, "test-component")) - 1 * span.spanContext() >> spanContext - 1 * spanContext.setIntegrationName("test-component") - _ * span.setTag(_) - _ * span.setTag(_, _) // Want to allow other calls from child implementations. - _ * span.setTag(_) - _ * span.setMeasured(true) - _ * span.setMetric(_) - _ * span.setMetric(_, _) - _ * span.setMetric(_) - _ * span.setServiceName(_, _) - _ * span.setOperationName(_) - _ * span.setSamplingPriority(_) - 0 * _ + // The base spec runs polymorphically against every subclass decorator, so it only asserts the + // baseline identity every decorator applies, tolerating the tags subclasses layer on. Each + // level's exact tag set is asserted by its own afterStart spec. + ExpectedSpanState.expected() + .spanType(decorator.spanType()) + .component("test-component") + .assertIdentityAppliedTo(recordingSpan) } def "test onPeerConnection"() { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy index fec5748f089..9b3a4ef243e 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ClientDecoratorTest.groovy @@ -1,10 +1,6 @@ package datadog.trace.bootstrap.instrumentation.decorator -import datadog.trace.api.DDTags -import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext -import datadog.trace.bootstrap.instrumentation.api.Tags class ClientDecoratorTest extends BaseDecoratorTest { @@ -13,28 +9,24 @@ class ClientDecoratorTest extends BaseDecoratorTest { def "test afterStart"() { setup: def decorator = newDecorator((String) serviceName) - def spanContext = Mock(AgentSpanContext) + def recordingSpan = new RecordingSpan() when: - decorator.afterStart(span) + decorator.afterStart(recordingSpan) then: + def expected = ExpectedSpanState.expected() + .spanType(decorator.spanType()) + .component("test-component") + .spanKind("client") + .measured(true) + .analyticsSampleRate(1.0d) if (serviceName != null) { - 1 * span.setServiceName(serviceName, "test-component") + expected.serviceName(serviceName, "test-component") } - 1 * span.setMeasured(true) - 1 * span.setTag(TagMap.Entry.create(Tags.COMPONENT, "test-component")) - 1 * span.spanContext() >> spanContext - 1 * spanContext.setIntegrationName("test-component") - 1 * span.setTag(TagMap.Entry.create(Tags.SPAN_KIND, "client")) - 1 * span.setSpanType(decorator.spanType()) - 1 * span.setMetric(TagMap.Entry.create(DDTags.ANALYTICS_SAMPLE_RATE, 1.0)) - _ * span.setTag(_) - _ * span.setTag(_, _) // Want to allow other calls from child implementations. - _ * span.setTag(_) - _ * span.setServiceName(_) - _ * span.setOperationName(_) - 0 * _ + // Polymorphic parent spec: subclass decorators (e.g. DB-type processing) layer on extra tags in + // afterStart, so tolerate additional tags while asserting the client-level scalars exactly. + expected.assertAppliedAllowingExtraTags(recordingSpan) where: serviceName << ["test-service", "other-service", null] diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy index 93852ccc88c..6f2b6e5d036 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy @@ -1,9 +1,6 @@ package datadog.trace.bootstrap.instrumentation.decorator -import datadog.trace.api.DDTags -import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext import datadog.trace.bootstrap.instrumentation.api.Tags import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_HOST @@ -17,23 +14,22 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest { def "test afterStart"() { setup: def decorator = newDecorator((String) serviceName) - def spanContext = Mock(AgentSpanContext) + def recordingSpan = new RecordingSpan() when: - decorator.afterStart(span) + decorator.afterStart(recordingSpan) then: + def expected = ExpectedSpanState.expected() + .spanType("test-type") + .component("test-component") + .spanKind("client") + .measured(true) + .analyticsSampleRate(1.0d) if (serviceName != null) { - 1 * span.setServiceName(serviceName, "test-component") + expected.serviceName(serviceName, "test-component") } - 1 * span.setMeasured(true) - 1 * span.setTag(TagMap.Entry.create(Tags.COMPONENT, "test-component")) - 1 * span.spanContext() >> spanContext - 1 * spanContext.setIntegrationName("test-component") - 1 * span.setTag(TagMap.Entry.create(Tags.SPAN_KIND, "client")) - 1 * span.setSpanType("test-type") - 1 * span.setMetric(TagMap.Entry.create(DDTags.ANALYTICS_SAMPLE_RATE, 1.0)) - 0 * _ + expected.assertAppliedTo(recordingSpan) where: serviceName << ["test-service", "other-service", null] diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy index d60c1534627..45ed7b645ba 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/ServerDecoratorTest.groovy @@ -1,39 +1,27 @@ package datadog.trace.bootstrap.instrumentation.decorator -import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext - -import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE -import static datadog.trace.api.DDTags.LANGUAGE_TAG_KEY -import static datadog.trace.api.DDTags.LANGUAGE_TAG_VALUE -import static datadog.trace.bootstrap.instrumentation.api.Tags.COMPONENT -import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND class ServerDecoratorTest extends BaseDecoratorTest { def span = Mock(AgentSpan) def "test afterStart"() { + setup: def decorator = newDecorator() - def spanContext = Mock(AgentSpanContext) + def recordingSpan = new RecordingSpan() when: - decorator.afterStart(span) + decorator.afterStart(recordingSpan) then: - 1 * span.setTag(TagMap.Entry.create(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE)) - 1 * span.setTag(TagMap.Entry.create(COMPONENT, "test-component")) - 1 * span.spanContext() >> spanContext - 1 * spanContext.setIntegrationName("test-component") - 1 * span.setTag(TagMap.Entry.create(SPAN_KIND, "server")) - 1 * span.setSpanType(decorator.spanType()) - if (decorator.traceAnalyticsEnabled) { - 1 * span.setMetric(TagMap.Entry.create(ANALYTICS_SAMPLE_RATE, 1.0)) - } else { - 1 * span.setMetric(null) - } - 0 * _ + ExpectedSpanState.expected() + .spanType(decorator.spanType()) + .component("test-component") + .spanKind("server") + .language() + .analyticsSampleRate(decorator.traceAnalyticsEnabled ? 1.0d : null) + .assertAppliedTo(recordingSpan) } def "test beforeFinish"() { diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java new file mode 100644 index 00000000000..ea9e194dac3 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java @@ -0,0 +1,153 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; +import static datadog.trace.api.DDTags.LANGUAGE_TAG_KEY; +import static datadog.trace.api.DDTags.LANGUAGE_TAG_VALUE; +import static datadog.trace.bootstrap.instrumentation.api.Tags.COMPONENT; +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The span state a decorator's {@code afterStart} is expected to apply, built up level by level to + * mirror the {@code buildSpanPrototype()} extension chain (base identity, then server/client kind, + * then any specialization). {@link #assertAppliedTo(RecordingSpan)} verifies the whole accumulated + * state at once instead of asserting individual mock interactions. + */ +final class ExpectedSpanState { + private CharSequence spanType; + private final Map tags = new LinkedHashMap<>(); + private CharSequence integrationName; + + private boolean expectService; + private String serviceName; + private CharSequence serviceNameSource; + + private boolean measured; + + // null => expect setMetric(null); non-null => expect the analytics-rate metric entry. + private Double analyticsSampleRate; + + static ExpectedSpanState expected() { + return new ExpectedSpanState(); + } + + ExpectedSpanState spanType(CharSequence type) { + this.spanType = type; + return this; + } + + ExpectedSpanState tag(String key, CharSequence value) { + tags.put(key, String.valueOf(value)); + return this; + } + + /** + * Baked component tag plus the integration name derived from it, as {@code BaseDecorator} does. + */ + ExpectedSpanState component(CharSequence component) { + tags.put(COMPONENT, String.valueOf(component)); + this.integrationName = component; + return this; + } + + ExpectedSpanState spanKind(CharSequence kind) { + tags.put(SPAN_KIND, String.valueOf(kind)); + return this; + } + + ExpectedSpanState language() { + tags.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); + return this; + } + + ExpectedSpanState serviceName(String serviceName, CharSequence source) { + this.expectService = true; + this.serviceName = serviceName; + this.serviceNameSource = source; + return this; + } + + ExpectedSpanState measured(boolean measured) { + this.measured = measured; + return this; + } + + ExpectedSpanState analyticsSampleRate(Double rate) { + this.analyticsSampleRate = rate; + return this; + } + + /** + * Lenient baseline check for the polymorphic base {@code afterStart} spec, which runs against + * every subclass decorator: asserts the identity a decorator must apply (span type, the declared + * tags as a subset, integration name) while tolerating the extra tags/state a subclass layers on. + */ + void assertIdentityAppliedTo(RecordingSpan span) { + assertEquals(str(spanType), str(span.recordedSpanType()), "span type"); + assertEquals(str(integrationName), str(span.recordedIntegrationName()), "integration name"); + for (Map.Entry expectedTag : tags.entrySet()) { + assertEquals( + expectedTag.getValue(), + span.recordedTags().get(expectedTag.getKey()), + "tag " + expectedTag.getKey()); + } + } + + /** Exact check: the recorded state must match exactly, with no additional tags. */ + void assertAppliedTo(RecordingSpan span) { + assertAppliedTo(span, false); + } + + /** + * Scalar-exact check that tolerates additional tags, for a polymorphic parent spec (e.g. {@code + * ClientDecoratorTest}) whose subclass decorators layer on extra tags in {@code afterStart}. Span + * type, integration name, service, measured flag and metric are still asserted exactly. + */ + void assertAppliedAllowingExtraTags(RecordingSpan span) { + assertAppliedTo(span, true); + } + + private void assertAppliedTo(RecordingSpan span, boolean allowExtraTags) { + assertEquals(str(spanType), str(span.recordedSpanType()), "span type"); + if (allowExtraTags) { + for (Map.Entry expectedTag : tags.entrySet()) { + assertEquals( + expectedTag.getValue(), + span.recordedTags().get(expectedTag.getKey()), + "tag " + expectedTag.getKey()); + } + } else { + assertEquals(tags, span.recordedTags(), "applied tags"); + } + assertEquals(str(integrationName), str(span.recordedIntegrationName()), "integration name"); + + if (expectService) { + assertTrue(span.serviceNameSet(), "expected setServiceName to be called"); + assertEquals(serviceName, span.recordedServiceName(), "service name"); + assertEquals(str(serviceNameSource), str(span.recordedServiceNameSource()), "service source"); + } else { + assertFalse(span.serviceNameSet(), "did not expect setServiceName to be called"); + } + + assertEquals(measured, span.recordedMeasured(), "measured"); + + if (analyticsSampleRate == null) { + assertNull(span.recordedMetric(), "expected no analytics metric"); + } else { + assertTrue(span.metricSet(), "expected setMetric to be called"); + assertEquals(ANALYTICS_SAMPLE_RATE, span.recordedMetric().tag(), "analytics metric key"); + assertEquals( + analyticsSampleRate, span.recordedMetric().doubleValue(), "analytics metric value"); + } + } + + private static String str(CharSequence value) { + return value == null ? null : value.toString(); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpan.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpan.java new file mode 100644 index 00000000000..0ba67d2642b --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpan.java @@ -0,0 +1,271 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; +import datadog.trace.api.TraceConfig; +import datadog.trace.api.gateway.Flow.Action.RequestBlockingAction; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.ImmutableSpan; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A recording {@link AgentSpan} test double for decorator {@code afterStart} tests. Rather than + * verifying individual mock interactions, it accumulates the state a decorator applies (span type, + * tags, integration name, service name, measured flag, analytics metric) so a test can assert the + * resulting span state as a whole -- see {@link ExpectedSpanState}. + * + *

Only the mutators {@code afterStart} exercises are recorded; every other {@link AgentSpan} + * method inherits the inert {@link ImmutableSpan} / noop behavior. + */ +final class RecordingSpan extends ImmutableSpan { + private final RecordingSpanContext context = new RecordingSpanContext(); + private final Map tags = new LinkedHashMap<>(); + + private CharSequence spanType; + private String serviceName; + private CharSequence serviceNameSource; + private boolean serviceNameSet; + private boolean measured; + private boolean metricSet; + private TagMap.EntryReader metric; + + // ----- recorded mutators ----- + + @Override + public AgentSpan setSpanType(CharSequence type) { + this.spanType = type; + return this; + } + + @Override + public AgentSpan setAllTags(Map map) { + if (map == null || map.isEmpty()) { + return this; + } + if (map instanceof TagMap) { + ((TagMap) map) + .forEach(reader -> tags.put(reader.tag(), String.valueOf(reader.objectValue()))); + } else { + for (Map.Entry entry : map.entrySet()) { + tags.put(entry.getKey(), String.valueOf(entry.getValue())); + } + } + return this; + } + + @Override + public AgentSpan setTag(TagMap.EntryReader entry) { + if (entry != null) { + tags.put(entry.tag(), String.valueOf(entry.objectValue())); + } + return this; + } + + @Override + public AgentSpan setTag(String key, CharSequence value) { + tags.put(key, String.valueOf(value)); + return this; + } + + @Override + public AgentSpan setTag(String key, String value) { + tags.put(key, value); + return this; + } + + @Override + public AgentSpan setTag(String key, Object value) { + tags.put(key, String.valueOf(value)); + return this; + } + + @Override + public void setServiceName(String serviceName, CharSequence source) { + this.serviceNameSet = true; + this.serviceName = serviceName; + this.serviceNameSource = source; + } + + @Override + public AgentSpan setMeasured(boolean measured) { + this.measured = measured; + return this; + } + + @Override + public AgentSpan setMetric(TagMap.EntryReader metricEntry) { + this.metricSet = true; + this.metric = metricEntry; + return this; + } + + @Override + public AgentSpanContext spanContext() { + return context; + } + + // ----- recorded state accessors ----- + + CharSequence recordedSpanType() { + return spanType; + } + + Map recordedTags() { + return tags; + } + + CharSequence recordedIntegrationName() { + return context.recordedIntegrationName(); + } + + boolean serviceNameSet() { + return serviceNameSet; + } + + String recordedServiceName() { + return serviceName; + } + + CharSequence recordedServiceNameSource() { + return serviceNameSource; + } + + boolean recordedMeasured() { + return measured; + } + + boolean metricSet() { + return metricSet; + } + + TagMap.EntryReader recordedMetric() { + return metric; + } + + // ----- inert reads (mirror NoopSpan) ----- + + @Override + public DDTraceId getTraceId() { + return DDTraceId.ZERO; + } + + @Override + public long getSpanId() { + return 0; + } + + @Override + public RequestBlockingAction getRequestBlockingAction() { + return null; + } + + @Override + public boolean isError() { + return false; + } + + @Override + public Object getTag(String key) { + return tags.get(key); + } + + @Override + public long getStartTime() { + return 0; + } + + @Override + public long getDurationNano() { + return 0; + } + + @Override + public String getOperationName() { + return null; + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public CharSequence getResourceName() { + return null; + } + + @Override + public RequestContext getRequestContext() { + return RequestContext.Noop.INSTANCE; + } + + @Override + public Integer getSamplingPriority() { + return (int) PrioritySampling.UNSET; + } + + @Override + public String getSpanType() { + return spanType == null ? null : spanType.toString(); + } + + @Override + public TagMap getTags() { + return TagMap.EMPTY; + } + + @Override + public AgentSpan getRootSpan() { + return this; + } + + @Override + public short getHttpStatusCode() { + return 0; + } + + @Override + public AgentSpan getLocalRootSpan() { + return this; + } + + @Override + public boolean isSameTrace(AgentSpan otherSpan) { + return otherSpan == this; + } + + @Override + public String getBaggageItem(String key) { + return null; + } + + @Override + public String getSpanName() { + return ""; + } + + @Override + public boolean hasResourceName() { + return false; + } + + @Override + public byte getResourceNamePriority() { + return Byte.MAX_VALUE; + } + + @Override + public TraceConfig traceConfig() { + return AgentTracer.NoopTraceConfig.INSTANCE; + } + + @Override + public boolean isOutbound() { + return false; + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpanContext.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpanContext.java new file mode 100644 index 00000000000..dc3a0565fc9 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/RecordingSpanContext.java @@ -0,0 +1,62 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import datadog.trace.api.DDTraceId; +import datadog.trace.api.datastreams.PathwayContext; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentTraceCollector; +import java.util.Collections; +import java.util.Map; + +/** + * A recording {@link AgentSpanContext} test double for decorator {@code afterStart} tests. Captures + * the integration name applied by {@link datadog.trace.bootstrap.instrumentation.decorator} + * decorators; every other accessor returns an inert default. + */ +final class RecordingSpanContext implements AgentSpanContext { + private CharSequence integrationName; + + @Override + public void setIntegrationName(CharSequence componentName) { + this.integrationName = componentName; + } + + CharSequence recordedIntegrationName() { + return integrationName; + } + + @Override + public DDTraceId getTraceId() { + return DDTraceId.ZERO; + } + + @Override + public long getSpanId() { + return 0; + } + + @Override + public AgentTraceCollector getTraceCollector() { + return null; + } + + @Override + public int getSamplingPriority() { + return PrioritySampling.UNSET; + } + + @Override + public Iterable> baggageItems() { + return Collections.emptyList(); + } + + @Override + public PathwayContext getPathwayContext() { + return null; + } + + @Override + public boolean isRemote() { + return false; + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java index 24ccd2b39d8..bac500c1480 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java @@ -97,12 +97,6 @@ public Builder extends_(final SpanPrototype base) { return this; } - public Builder initInstrumentationNames(final String[] instrumentationNames) { - return (instrumentationNames == null || instrumentationNames.length == 0) - ? this - : initInstrumentationName(instrumentationNames[0]); - } - public Builder initInstrumentationName(final String instrumentationName) { this.instrumentationName = instrumentationName; return this; From 8e3d18fbfe3a0cade927c5dcaab6f96bc5a224c4 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 28 Aug 2026 22:31:44 -0400 Subject: [PATCH 8/8] Remove unused ExpectedSpanState.tag(String, CharSequence) overload No caller uses this generic setter -- tag population goes through the more specific component()/spanKind()/language() builder methods. --- .../instrumentation/decorator/ExpectedSpanState.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java index ea9e194dac3..e9bfd5d3517 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/ExpectedSpanState.java @@ -42,11 +42,6 @@ ExpectedSpanState spanType(CharSequence type) { return this; } - ExpectedSpanState tag(String key, CharSequence value) { - tags.put(key, String.valueOf(value)); - return this; - } - /** * Baked component tag plus the integration name derived from it, as {@code BaseDecorator} does. */