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();
+ }
}
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 c280000f252..55ee9e2372c 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;
@@ -1077,6 +1079,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);
@@ -1182,6 +1205,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);
@@ -1622,6 +1656,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;
@@ -1660,6 +1695,7 @@ protected static final DDSpan buildSpan(
boolean errorFlag,
CharSequence spanType,
TagMap.Ledger tagLedger,
+ SpanPrototype spanPrototype,
List 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);
@@ -1391,6 +1447,7 @@ public void setIntegrationName(CharSequence integrationName) {
this.integrationName = integrationName;
}
+ @Override
public CharSequence getIntegrationName() {
return integrationName;
}
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..7a3722d77cf
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/SpanPrototypeConstructionTest.java
@@ -0,0 +1,159 @@
+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 static org.junit.jupiter.api.Assertions.assertNull;
+
+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()
+ .initInstrumentationName("test-instr")
+ .initOperationName("proto.op")
+ .initSpanType("web")
+ .initKind(SPAN_KIND_SERVER)
+ .initComponentOnly("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();
+ }
+ }
+
+ @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
new file mode 100644
index 00000000000..c75e9eefa94
--- /dev/null
+++ b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java
@@ -0,0 +1,96 @@
+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. 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)
+@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()
+ .initComponentOnly("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/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java
index 39160ae11ff..2f2423d5239 100644
--- a/internal-api/src/main/java/datadog/trace/api/TagMap.java
+++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java
@@ -208,6 +208,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
@@ -215,23 +226,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/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java
index aa270020e78..b5d0429ce2f 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
@@ -219,6 +219,46 @@ 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);
+ }
+ });
+
+ // 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);
+ }
+ }
+ }
+
default AgentSpan asAgentSpan() {
return this;
}
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
*
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 1fce96a2992..a40bf5e7603 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
@@ -69,6 +69,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);
}
@@ -322,6 +330,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}
@@ -425,6 +457,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) {
@@ -501,6 +540,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) {
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..24ccd2b39d8
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java
@@ -0,0 +1,179 @@
+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 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();
+ }
+
+ public String instrumentationName() {
+ return instrumentationName;
+ }
+
+ public CharSequence operationName() {
+ return operationName;
+ }
+
+ 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;
+ }
+
+ 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();
+
+ 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;
+ }
+ if (base.integrationName != null) {
+ this.integrationName = base.integrationName;
+ }
+ this.tags.putAll(base.tags);
+ }
+ return this;
+ }
+
+ public Builder initInstrumentationNames(final String[] instrumentationNames) {
+ return (instrumentationNames == null || instrumentationNames.length == 0)
+ ? this
+ : initInstrumentationName(instrumentationNames[0]);
+ }
+
+ public Builder initInstrumentationName(final String instrumentationName) {
+ this.instrumentationName = instrumentationName;
+ return this;
+ }
+
+ public Builder initOperationName(final CharSequence operationName) {
+ this.operationName = operationName;
+ return this;
+ }
+
+ public Builder initSpanType(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 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);
+ }
+ return this;
+ }
+
+ public Builder initTag(final String key, final Object value) {
+ if (!TagMap.Entry.isEmptyValue(value)) {
+ 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..e01ba8e8591
--- /dev/null
+++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java
@@ -0,0 +1,180 @@
+package datadog.trace.bootstrap.instrumentation.api;
+
+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 =
+ SpanPrototype.builder()
+ .initInstrumentationName("base")
+ .initSpanType("base-type")
+ .initKind("server")
+ .build();
+ final SpanPrototype derived =
+ SpanPrototype.builder()
+ .extends_(base)
+ .initComponentOnly("netty")
+ .initSpanType("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 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
+ // empty tag. A raw tags.set would otherwise bake a tag that per-span stamping never emits.
+ final SpanPrototype proto =
+ SpanPrototype.builder()
+ .initComponentOnly("") // 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
+ }
+
+ @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());
+ }
+}
+ *
+ *
+ *