From 83be1ca03e2a61582c850a53cd57ee272abc5ff6 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 1 Jul 2026 09:28:16 -0400 Subject: [PATCH 1/3] Introduce SpanPrototype + BaseDecorator bake (increment 1: abstraction only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpanPrototype is a baked-once, frozen descriptor of a span's constant initial tags — the per-decorator constants afterStart stamps one entry at a time. BaseDecorator.prototype() composes them across the hierarchy via a buildPrototype(TagMap) contribute-chain (mirroring the afterStart super-chain, but run once at bake) and caches lazily (same static-init caution as componentEntry()). Purely additive: afterStart is untouched and prototype() is not yet wired into span creation — this is just the provider surface + the bake. The setAllTags seed hook (and gating afterStart's constants behind prototype != NONE) is the next increment. Rides the existing TagMap API, so it's independent of the deeper TagMap rework. Co-Authored-By: Claude Opus 4.8 --- .../decorator/BaseDecorator.java | 35 +++++++++ .../decorator/ClientDecorator.java | 6 ++ .../decorator/ServerDecorator.java | 7 ++ .../decorator/SpanPrototypeTest.java | 76 +++++++++++++++++++ .../java/datadog/trace/api/SpanPrototype.java | 39 ++++++++++ 5 files changed, 163 insertions(+) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java create mode 100644 internal-api/src/main/java/datadog/trace/api/SpanPrototype.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 2628e9416cf..f48d2835236 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 @@ -7,6 +7,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.Functions; +import datadog.trace.api.SpanPrototype; import datadog.trace.api.TagMap; import datadog.trace.api.cache.QualifiedClassNameCache; import datadog.trace.bootstrap.instrumentation.api.AgentScope; @@ -48,6 +49,10 @@ public String apply(Class clazz) { // Deliberately not volatile, reading null and repeating the calculation is safe private TagMap.Entry cachedComponentEntry = null; + // Deliberately not volatile, same benign-race reasoning as cachedComponentEntry: baking twice is + // safe because the constant tags are identical. + private SpanPrototype cachedPrototype = null; + protected BaseDecorator() { final Config config = Config.get(); final String[] instrumentationNames = instrumentationNames(); @@ -90,6 +95,36 @@ protected boolean traceAnalyticsDefault() { return false; } + /** + * The baked-once {@link SpanPrototype} for this decorator: the constant tags {@link #afterStart} + * would otherwise stamp one at a time. Composed across the hierarchy via {@link + * #buildPrototype(TagMap)} and cached (lazily, to respect the same static-init ordering caution + * as {@link #componentEntry()}). + * + *

Not yet wired into span creation — this is the provider surface; the seed hook comes next. + */ + public final SpanPrototype prototype() { + SpanPrototype prototype = cachedPrototype; + if (prototype == null) { + final TagMap tags = TagMap.create(); + buildPrototype(tags); + cachedPrototype = prototype = SpanPrototype.of(tags); + } + return prototype; + } + + /** + * Contributes this decorator's constant tags to the prototype under construction. Overrides must + * call {@code super.buildPrototype(tags)} first, then add their own — mirroring the {@link + * #afterStart} super-chain, but run once at bake time rather than per span. + */ + protected void buildPrototype(final TagMap tags) { + tags.set(componentEntry()); + if (traceAnalyticsEntry != null) { + tags.set(traceAnalyticsEntry); + } + } + public AgentSpan afterStart(final AgentSpan span) { if (spanType() != null) { span.setSpanType(spanType()); 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 99dec2dbc08..a60f10d2ec5 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 @@ -43,4 +43,10 @@ public AgentSpan afterStart(final AgentSpan span) { span.setMeasured(true); return super.afterStart(span); } + + @Override + protected void buildPrototype(final TagMap tags) { + super.buildPrototype(tags); + tags.set(spanKindEntry()); + } } 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 20b11038ffd..da3df5da3aa 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 @@ -18,4 +18,11 @@ public AgentSpan afterStart(final AgentSpan span) { return super.afterStart(span); } + + @Override + protected void buildPrototype(final TagMap tags) { + super.buildPrototype(tags); + tags.set(SPAN_KIND_ENTRY); + tags.set(LANG_ENTRY); + } } diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java new file mode 100644 index 00000000000..3651ad52adb --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java @@ -0,0 +1,76 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.junit.jupiter.api.Test; + +class SpanPrototypeTest { + + @Test + void serverPrototypeComposesBaseAndServerConstants() { + final TagMap tags = new TestServerDecorator().prototype().tags(); + + // BaseDecorator contribution + assertEquals("test-component", tags.getString(Tags.COMPONENT)); + // ServerDecorator contribution + assertEquals(Tags.SPAN_KIND_SERVER, tags.getString(Tags.SPAN_KIND)); + assertEquals(DDTags.LANGUAGE_TAG_VALUE, tags.getString(DDTags.LANGUAGE_TAG_KEY)); + } + + @Test + void clientPrototypeComposesBaseAndClientConstants() { + final TagMap tags = new TestClientDecorator().prototype().tags(); + + assertEquals("test-component", tags.getString(Tags.COMPONENT)); + assertEquals(Tags.SPAN_KIND_CLIENT, tags.getString(Tags.SPAN_KIND)); + } + + @Test + void prototypeIsBakedOnce() { + final TestServerDecorator decorator = new TestServerDecorator(); + assertSame(decorator.prototype(), decorator.prototype()); + } + + static final class TestServerDecorator extends ServerDecorator { + @Override + protected String[] instrumentationNames() { + return new String[] {"test"}; + } + + @Override + protected CharSequence spanType() { + return "test-type"; + } + + @Override + protected CharSequence component() { + return "test-component"; + } + } + + static final class TestClientDecorator extends ClientDecorator { + @Override + protected String[] instrumentationNames() { + return new String[] {"test"}; + } + + @Override + protected CharSequence spanType() { + return "test-type"; + } + + @Override + protected CharSequence component() { + return "test-component"; + } + + @Override + protected String service() { + return "test-service"; + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/api/SpanPrototype.java new file mode 100644 index 00000000000..03efd68eb0e --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/SpanPrototype.java @@ -0,0 +1,39 @@ +package datadog.trace.api; + +/** + * A baked-once, frozen descriptor of a span's constant initial tags — the per-decorator constants + * that are otherwise stamped one entry at a time in {@code BaseDecorator.afterStart}. + * + *

Provided by {@code BaseDecorator.prototype()} (composed across the decorator hierarchy and + * cached), and applied to a span via {@code setAllTags} — later, seeded into the span's {@link + * TagMap} at construction. Because it rides the existing {@code TagMap} API, it is independent of + * any deeper {@code TagMap} rework: the internal seed can get faster without changing this surface. + * + *

v1 carries only the constant tag set. Constant span fields ({@code spanType}, integration + * name, …) and later facets (derivation, canonicalization, lifecycle hooks) are deliberately out of + * scope — the concept earns its extensibility by being simple and well-placed, not by pre-built + * slots. + */ +public final class SpanPrototype { + /** The empty prototype — for spans created without a decorator-provided prototype. */ + public static final SpanPrototype NONE = new SpanPrototype(TagMap.create(0).immutableCopy()); + + private final TagMap tags; + + private SpanPrototype(final TagMap frozenTags) { + this.tags = frozenTags; + } + + /** + * Bakes a prototype from the given constant tags. The tags are frozen (an immutable copy is + * taken), so the caller may reuse the source map. + */ + public static SpanPrototype of(final TagMap tags) { + return new SpanPrototype(tags.immutableCopy()); + } + + /** The frozen constant tags this prototype stamps onto a span. */ + public TagMap tags() { + return tags; + } +} From a630809348c0cca6d432be5659dc4dbf2e611706 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 1 Jul 2026 15:49:43 -0400 Subject: [PATCH 2/3] Reshape SpanPrototype to a builder API (extends_ / init*, no TagMap exposed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: instrumentation authors should never see TagMap/TagMap.Entry. Moves SpanPrototype to bootstrap.instrumentation.api (next to Tags) and replaces the buildPrototype(TagMap) contribute-chain — which leaked TagMap to decorators — with a builder: SpanPrototype.builder() .extends_(base) // inherit a SpanType base's identity + tags .instrumentationName(...).operationName(...).spanType(...) .initKind(...).initComponent(...).initTag(key, value) .initTag(entry) // advanced/internal escape (cached/metric entries) .build(); SpanPrototype now carries span identity (instrumentation name / operation name / span type) alongside the frozen constant TagMap, matching the "prototype generates the span" model. Concrete class (no abstract/impl split); frozen internal TagMap (no TagMap.Prototype for now). BaseDecorator composes via a prototypeBuilder() super-chain (TagMap-free); extends_ is the primitive for the explicit SpanType-base pattern, not yet used by the class-hierarchy bake. Still additive: afterStart untouched, prototype() dormant. SpanPrototypeTest 4/4. Co-Authored-By: Claude Opus 4.8 --- .../decorator/BaseDecorator.java | 32 ++-- .../decorator/ClientDecorator.java | 6 +- .../decorator/ServerDecorator.java | 9 +- .../decorator/SpanPrototypeTest.java | 24 ++- .../java/datadog/trace/api/SpanPrototype.java | 39 ----- .../instrumentation/api/SpanPrototype.java | 145 ++++++++++++++++++ 6 files changed, 192 insertions(+), 63 deletions(-) delete mode 100644 internal-api/src/main/java/datadog/trace/api/SpanPrototype.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.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 f48d2835236..0ed7b470727 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 @@ -7,12 +7,12 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.Functions; -import datadog.trace.api.SpanPrototype; import datadog.trace.api.TagMap; import datadog.trace.api.cache.QualifiedClassNameCache; 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; @@ -96,33 +96,33 @@ protected boolean traceAnalyticsDefault() { } /** - * The baked-once {@link SpanPrototype} for this decorator: the constant tags {@link #afterStart} - * would otherwise stamp one at a time. Composed across the hierarchy via {@link - * #buildPrototype(TagMap)} and cached (lazily, to respect the same static-init ordering caution - * as {@link #componentEntry()}). + * The baked-once {@link SpanPrototype} for this decorator: the constant identity + tags {@link + * #afterStart} would otherwise stamp one at a time. Composed across the hierarchy via {@link + * #prototypeBuilder()} and cached (lazily, to respect the same static-init ordering caution as + * {@link #componentEntry()}). * *

Not yet wired into span creation — this is the provider surface; the seed hook comes next. */ public final SpanPrototype prototype() { SpanPrototype prototype = cachedPrototype; if (prototype == null) { - final TagMap tags = TagMap.create(); - buildPrototype(tags); - cachedPrototype = prototype = SpanPrototype.of(tags); + cachedPrototype = prototype = prototypeBuilder().build(); } return prototype; } /** - * Contributes this decorator's constant tags to the prototype under construction. Overrides must - * call {@code super.buildPrototype(tags)} first, then add their own — mirroring the {@link - * #afterStart} super-chain, but run once at bake time rather than per span. + * Contributes this decorator's constant identity + tags to the prototype under construction. + * Overrides must call {@code super.prototypeBuilder()} first, then add their own — mirroring the + * {@link #afterStart} super-chain, but run once at bake time rather than per span. Authors + * compose via the typed builder and never see {@link TagMap}. */ - protected void buildPrototype(final TagMap tags) { - tags.set(componentEntry()); - if (traceAnalyticsEntry != null) { - tags.set(traceAnalyticsEntry); - } + protected SpanPrototype.Builder prototypeBuilder() { + return SpanPrototype.builder() + .instrumentationName(instrumentationNames()) + .spanType(spanType()) + .initComponent(component()) + .initTag(traceAnalyticsEntry); } public AgentSpan afterStart(final AgentSpan span) { 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 a60f10d2ec5..bc534e495fd 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 @@ -2,6 +2,7 @@ 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 { @@ -45,8 +46,7 @@ public AgentSpan afterStart(final AgentSpan span) { } @Override - protected void buildPrototype(final TagMap tags) { - super.buildPrototype(tags); - tags.set(spanKindEntry()); + protected SpanPrototype.Builder prototypeBuilder() { + return super.prototypeBuilder().initKind(spanKind()); } } 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 da3df5da3aa..d36dd73b866 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 @@ -3,6 +3,7 @@ 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 { @@ -20,9 +21,9 @@ public AgentSpan afterStart(final AgentSpan span) { } @Override - protected void buildPrototype(final TagMap tags) { - super.buildPrototype(tags); - tags.set(SPAN_KIND_ENTRY); - tags.set(LANG_ENTRY); + protected SpanPrototype.Builder prototypeBuilder() { + return super.prototypeBuilder() + .initKind(Tags.SPAN_KIND_SERVER) + .initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE); } } diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java index 3651ad52adb..6c8b1f388ab 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/SpanPrototypeTest.java @@ -5,6 +5,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; import datadog.trace.bootstrap.instrumentation.api.Tags; import org.junit.jupiter.api.Test; @@ -12,8 +13,12 @@ class SpanPrototypeTest { @Test void serverPrototypeComposesBaseAndServerConstants() { - final TagMap tags = new TestServerDecorator().prototype().tags(); + final SpanPrototype prototype = new TestServerDecorator().prototype(); + final TagMap tags = prototype.tags(); + // Identity (BaseDecorator) + assertEquals("test", prototype.instrumentationName()); + assertEquals("test-type", prototype.spanType()); // BaseDecorator contribution assertEquals("test-component", tags.getString(Tags.COMPONENT)); // ServerDecorator contribution @@ -21,6 +26,23 @@ void serverPrototypeComposesBaseAndServerConstants() { assertEquals(DDTags.LANGUAGE_TAG_VALUE, tags.getString(DDTags.LANGUAGE_TAG_KEY)); } + @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 + } + @Test void clientPrototypeComposesBaseAndClientConstants() { final TagMap tags = new TestClientDecorator().prototype().tags(); diff --git a/internal-api/src/main/java/datadog/trace/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/api/SpanPrototype.java deleted file mode 100644 index 03efd68eb0e..00000000000 --- a/internal-api/src/main/java/datadog/trace/api/SpanPrototype.java +++ /dev/null @@ -1,39 +0,0 @@ -package datadog.trace.api; - -/** - * A baked-once, frozen descriptor of a span's constant initial tags — the per-decorator constants - * that are otherwise stamped one entry at a time in {@code BaseDecorator.afterStart}. - * - *

Provided by {@code BaseDecorator.prototype()} (composed across the decorator hierarchy and - * cached), and applied to a span via {@code setAllTags} — later, seeded into the span's {@link - * TagMap} at construction. Because it rides the existing {@code TagMap} API, it is independent of - * any deeper {@code TagMap} rework: the internal seed can get faster without changing this surface. - * - *

v1 carries only the constant tag set. Constant span fields ({@code spanType}, integration - * name, …) and later facets (derivation, canonicalization, lifecycle hooks) are deliberately out of - * scope — the concept earns its extensibility by being simple and well-placed, not by pre-built - * slots. - */ -public final class SpanPrototype { - /** The empty prototype — for spans created without a decorator-provided prototype. */ - public static final SpanPrototype NONE = new SpanPrototype(TagMap.create(0).immutableCopy()); - - private final TagMap tags; - - private SpanPrototype(final TagMap frozenTags) { - this.tags = frozenTags; - } - - /** - * Bakes a prototype from the given constant tags. The tags are frozen (an immutable copy is - * taken), so the caller may reuse the source map. - */ - public static SpanPrototype of(final TagMap tags) { - return new SpanPrototype(tags.immutableCopy()); - } - - /** The frozen constant tags this prototype stamps onto a span. */ - public TagMap tags() { - return tags; - } -} 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); + } + } +} From 54b438634c71358c6b5888eae8c8a0b8941825b1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 1 Jul 2026 16:28:49 -0400 Subject: [PATCH 3/3] Add SpanPrototypeBenchmark: per-mechanism constant-application benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Isolates the SpanPrototype mechanism's value — the constant-tag application a span pays at start — three ways, holding the tag set identical: - oldPerSpanStamps: fresh map + N set(entry) (what afterStart does per span) - newBulkApply: fresh map + one putAll of the baked prototype - newConstructionSeed: copy() of the frozen prototype (clone-at-birth; previews the construction-seed increment 2 unlocks) The SpanPrototype counterpart to the extractor-side per-mechanism benchmarks (TagProjectionBenchmark, DatabaseClientConnectionBenchmark). @Fork(3) @Threads(8), run with -prof gc. Co-Authored-By: Claude Opus 4.8 --- .../trace/api/SpanPrototypeBenchmark.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.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(); + } +}