From 165514874c08ad5fd9e759643db1bb3f6101ad4b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 9 Jul 2026 05:35:18 -0400 Subject: [PATCH 01/10] 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 9dbf799d98f1014c4cd96cc21c6f08eac00703fc Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 14:52:27 -0400 Subject: [PATCH 02/10] 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 723cbf8f71a6f0dfc3b8bd5056fcf292b2f9e6f8 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 15:33:36 -0400 Subject: [PATCH 03/10] 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 289647c5c74..aa57177c25d 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 @@ -76,6 +76,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; @@ -133,6 +134,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; @@ -1049,6 +1051,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); @@ -1154,6 +1177,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); @@ -1603,6 +1637,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; @@ -1641,6 +1676,7 @@ protected static final DDSpan buildSpan( boolean errorFlag, CharSequence spanType, TagMap.Ledger tagLedger, + SpanPrototype spanPrototype, List links, Object builderRequestContextDataAppSec, Object builderRequestContextDataIast, @@ -1660,6 +1696,7 @@ protected static final DDSpan buildSpan( errorFlag, spanType, tagLedger, + spanPrototype, links, builderRequestContextDataAppSec, builderRequestContextDataIast, @@ -1745,6 +1782,7 @@ protected AgentSpan startImpl() { this.errorFlag, this.spanType, this.tagLedger, + this.spanPrototype, this.links, this.builderRequestContextDataAppSec, this.builderRequestContextDataIast, @@ -1771,6 +1809,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 */, @@ -1790,6 +1855,7 @@ protected static final AgentSpan startSpan( boolean errorFlag, CharSequence spanType, TagMap.Ledger tagLedger, + SpanPrototype spanPrototype, List links, Object builderRequestContextDataAppSec, Object builderRequestContextDataIast, @@ -1852,6 +1918,7 @@ protected static final AgentSpan startSpan( errorFlag, spanType, tagLedger, + spanPrototype, links, builderRequestContextDataAppSec, builderRequestContextDataIast, @@ -1990,6 +2057,7 @@ protected static final DDSpanContext buildSpanContext( boolean errorFlag, CharSequence spanType, TagMap.Ledger tagLedger, + SpanPrototype spanPrototype, List links, Object builderRequestContextDataAppSec, Object builderRequestContextDataIast, @@ -2228,6 +2296,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); @@ -2308,6 +2382,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 d3a25d8ecfe..02c7f1cc9f4 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 @@ -338,6 +338,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} @@ -441,6 +465,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 1cb38a13471ac6a53b2eff03508f48c36de07b2e Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 15:34:38 -0400 Subject: [PATCH 04/10] 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 aa57177c25d..f9bd628cbf4 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 @@ -2301,6 +2301,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 8bd84d0ec690cd64ada3ce035d1886b312b7e8ed Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 21 Jul 2026 19:30:24 -0400 Subject: [PATCH 05/10] 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 1e095310ac1dcabc8d018b9cc3a76e1ba16bd1c5 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 22 Jul 2026 17:53:27 -0400 Subject: [PATCH 06/10] 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 f9bd628cbf4..c45694d1642 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 @@ -2297,16 +2297,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 adad872a6c6e58f22ec29f763bc861157141cc26 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 24 Jul 2026 07:55:06 -0400 Subject: [PATCH 07/10] Guard integration name in AgentSpan.apply via AgentSpanContext getter Add getIntegrationName() (default null) to AgentSpanContext, symmetric with the existing no-op setIntegrationName, so the default apply() can honor never-clobber like DDSpanContext.apply instead of unconditionally overwriting. Co-Authored-By: Claude Opus 4.8 --- .../src/main/java/datadog/trace/core/DDSpanContext.java | 1 + .../trace/bootstrap/instrumentation/api/AgentSpan.java | 9 ++++++--- .../bootstrap/instrumentation/api/AgentSpanContext.java | 9 +++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 27df74fb2e0..462c3c430f6 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 @@ -1348,6 +1348,7 @@ public void setIntegrationName(CharSequence integrationName) { this.integrationName = integrationName; } + @Override public CharSequence getIntegrationName() { return integrationName; } 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 1952853dfeb..6df4a3e88c4 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 @@ -249,9 +249,12 @@ default void apply(@Nonnull final SpanPrototype prototype) { } }); - final CharSequence integrationName = prototype.integrationName(); - if (integrationName != null) { - spanContext().setIntegrationName(integrationName); + // Never-clobber: only stamp the integration name when the context reports none. + if (spanContext().getIntegrationName() == null) { + final CharSequence integrationName = prototype.integrationName(); + if (integrationName != null) { + spanContext().setIntegrationName(integrationName); + } } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanContext.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanContext.java index 1dba9438168..6e950925305 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanContext.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanContext.java @@ -55,6 +55,15 @@ default void mergePathwayContext(PathwayContext pathwayContext) {} default void setIntegrationName(CharSequence componentName) {} + /** + * The integration name recorded on this context, or {@code null} if none. Default {@code null} + * mirrors {@link #setIntegrationName}'s no-op default: contexts that do not track an integration + * name report absence, which lets never-clobber callers guard before setting. + */ + default CharSequence getIntegrationName() { + return null; + } + /** * Gets whether the span context used is part of the local trace or from another service * From 703adabf2d05159e18223c80a1ab0302f376934a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 24 Jul 2026 08:22:09 -0400 Subject: [PATCH 08/10] Override NoopTracerAPI.buildSpan(SpanPrototype) to return null Mirrors the existing buildSpan(String,...) noop contract so the prototype builder path matches. startSpan(SpanPrototype) was already noop-safe. A chainable NoopSpanBuilder to fix the null-vs-NoopSpan asymmetry is left to a separate PR. Co-Authored-By: Claude Opus 4.8 --- .../trace/bootstrap/instrumentation/api/AgentTracer.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 02c7f1cc9f4..db63dab9eeb 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 @@ -559,6 +559,15 @@ public SpanBuilder buildSpan(final String instrumentationName, final CharSequenc return null; } + @Override + public SpanBuilder buildSpan( + @Nonnull final SpanPrototype prototype, final CharSequence operationName) { + // Mirrors buildSpan(String,...): the noop tracer returns a null builder. Callers that need a + // noop-safe entry point use startSpan(...), which is overridden above. A chainable + // NoopSpanBuilder would fix the null-vs-NoopSpan asymmetry, but that is a separate PR. + return null; + } + @Override public SpanBuilder singleSpanBuilder( final String instrumentationName, final CharSequence spanName) { From c2d5fb9ac3cd50ad51961bb753516c68495f6213 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 24 Jul 2026 08:31:24 -0400 Subject: [PATCH 09/10] Bulk up SpanPrototype.Builder test coverage Cover every Builder branch and getter so the SpanPrototype.Builder jacoco rule (branch >= 0.7, instr >= 0.8) that failed test_base is satisfied: initInstrumentationNames null/empty/multi, extends_(null) + full copy, initComponentAndIntegration set/empty, initTag(Object)/initTag(EntryReader) null and non-null, and all five getters incl. integrationName(). Co-Authored-By: Claude Opus 4.8 --- .../api/SpanPrototypeTest.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) 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 522a2918036..e01ba8e8591 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 @@ -2,11 +2,93 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import datadog.trace.api.TagMap; import org.junit.jupiter.api.Test; class SpanPrototypeTest { + @Test + void noneHasNoIdentityAndNoTags() { + assertNull(SpanPrototype.NONE.instrumentationName()); + assertNull(SpanPrototype.NONE.operationName()); + assertNull(SpanPrototype.NONE.spanType()); + assertNull(SpanPrototype.NONE.integrationName()); + assertTrue(SpanPrototype.NONE.tags().isEmpty()); + } + + @Test + void gettersReflectBuilderState() { + final SpanPrototype proto = + SpanPrototype.builder() + .initInstrumentationName("instr") + .initOperationName("op") + .initSpanType("web") + .initComponentAndIntegration("netty") // sets integration name + component tag + .build(); + + assertEquals("instr", proto.instrumentationName()); + assertEquals("op", proto.operationName()); + assertEquals("web", proto.spanType()); + assertEquals("netty", proto.integrationName()); + assertEquals("netty", proto.tags().getString(Tags.COMPONENT)); + } + + @Test + void initInstrumentationNamesTakesFirstElement() { + final SpanPrototype proto = + SpanPrototype.builder().initInstrumentationNames(new String[] {"first", "second"}).build(); + + assertEquals("first", proto.instrumentationName()); + } + + @Test + void initInstrumentationNamesNullArrayLeavesNameUnset() { + final SpanPrototype proto = SpanPrototype.builder().initInstrumentationNames(null).build(); + + assertNull(proto.instrumentationName()); + } + + @Test + void initInstrumentationNamesEmptyArrayLeavesNameUnset() { + final SpanPrototype proto = + SpanPrototype.builder().initInstrumentationNames(new String[0]).build(); + + assertNull(proto.instrumentationName()); + } + + @Test + void extendsNullBaseIsNoOp() { + final SpanPrototype proto = SpanPrototype.builder().extends_(null).build(); + + assertNull(proto.instrumentationName()); + assertNull(proto.operationName()); + assertNull(proto.spanType()); + assertNull(proto.integrationName()); + assertTrue(proto.tags().isEmpty()); + } + + @Test + void extendsCopiesAllIdentityAndTags() { + final SpanPrototype base = + SpanPrototype.builder() + .initInstrumentationName("base") + .initOperationName("base.op") + .initSpanType("base-type") + .initComponentAndIntegration("base-comp") // integration name + component tag + .initKind("server") + .build(); + final SpanPrototype derived = SpanPrototype.builder().extends_(base).build(); + + assertEquals("base", derived.instrumentationName()); + assertEquals("base.op", derived.operationName()); + assertEquals("base-type", derived.spanType()); + assertEquals("base-comp", derived.integrationName()); + assertEquals("base-comp", derived.tags().getString(Tags.COMPONENT)); + assertEquals("server", derived.tags().getString(Tags.SPAN_KIND)); + } + @Test void extendsInheritsBaseIdentityAndTagsThenOverrides() { final SpanPrototype base = @@ -28,6 +110,30 @@ void extendsInheritsBaseIdentityAndTagsThenOverrides() { assertEquals("netty", derived.tags().getString(Tags.COMPONENT)); // added tag } + @Test + void initComponentAndIntegrationSetsBothComponentTagAndIntegrationName() { + final SpanPrototype proto = + SpanPrototype.builder().initComponentAndIntegration("netty").build(); + + assertEquals("netty", proto.tags().getString(Tags.COMPONENT)); + assertEquals("netty", proto.integrationName()); + } + + @Test + void initComponentAndIntegrationEmptyIsNoOpForBoth() { + final SpanPrototype proto = SpanPrototype.builder().initComponentAndIntegration("").build(); + + assertNull(proto.tags().getString(Tags.COMPONENT)); + assertNull(proto.integrationName()); + } + + @Test + void initKindSetsSpanKindTag() { + final SpanPrototype proto = SpanPrototype.builder().initKind("client").build(); + + assertEquals("client", proto.tags().getString(Tags.SPAN_KIND)); + } + @Test void emptyOrNullConstantsAreDroppedNotBaked() { // Match AgentSpan.setTag / the cached-Entry path: a null or empty constant is "no tag", not an @@ -49,4 +155,26 @@ void emptyOrNullConstantsAreDroppedNotBaked() { assertNull(proto.tags().getString("null.obj")); assertEquals("v", proto.tags().getString("kept")); // sanity: non-empty still stored } + + @Test + void initTagObjectStoresNonEmptyValue() { + final SpanPrototype proto = SpanPrototype.builder().initTag("count", (Object) 42).build(); + + assertEquals(42, proto.tags().get("count")); + } + + @Test + void initTagEntryReaderStoresEntry() { + final TagMap.EntryReader entry = TagMap.Entry.create("cached", "value"); + final SpanPrototype proto = SpanPrototype.builder().initTag(entry).build(); + + assertEquals("value", proto.tags().getString("cached")); + } + + @Test + void initTagNullEntryReaderIsNoOp() { + final SpanPrototype proto = SpanPrototype.builder().initTag((TagMap.EntryReader) null).build(); + + assertTrue(proto.tags().isEmpty()); + } } From 3f6d43d474c6d947f64c7233c57cfc5e4f278e15 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 1 Sep 2026 17:37:08 -0400 Subject: [PATCH 10/10] Expose SpanPrototype starts through the static AgentTracer facade Auto-instrumentation statically imports AgentTracer, but only the instance-level TracerAPI had a startSpan(SpanPrototype, CharSequence) overload. Add the matching static facade method so callers can use it. Co-Authored-By: Claude Sonnet 5 --- .../trace/bootstrap/instrumentation/api/AgentTracer.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 1e4225c19e6..22aeaf136b0 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 @@ -68,6 +68,14 @@ public static AgentSpan startSpan( return get().startSpan(instrumentationName, spanName, parent, startTimeMicros); } + /** + * @see TracerAPI#startSpan(SpanPrototype, CharSequence) + */ + public static AgentSpan startSpan( + @Nonnull final SpanPrototype prototype, final CharSequence operationName) { + return get().startSpan(prototype, operationName); + } + public static AgentScope activateSpan(final AgentSpan span) { return get().activateSpan(span); }