Skip to content

Introduce SpanPrototype: baked-once constant span-tag descriptor (phase 1b) - #11894

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 12 commits into
masterfrom
dougqh/span-prototype-api
Sep 11, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 12 commits into
masterfrom
dougqh/span-prototype-api

Conversation

@dougqh

@dougqh dougqh commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Introduces SpanPrototype — a baked-once, immutable descriptor of a span's constant identity + tags (builder API: extends_ / init*) — and the construction path that builds a span from one: buildSpan(SpanPrototype, operationName) / startSpan(SpanPrototype, operationName).

A decorator declares its constant span shape once (identity + span.kind, component, …); construction seeds those constants with a fast TagMap-to-TagMap copy that reuses the underlying TagMap.Entry objects, instead of BaseDecorator.afterStart stamping each tag per span.

The side effects afterStart performs alongside tag writes are preserved, not just the tags:

  • span.kind seeds through the TagInterceptor, so the cached span-kind ordinal (which drives isOutbound / isKind) is set — not merely the tag.
  • component via initComponentAndIntegration also records the integration name (setIntegrationName), which the IntegrationAdder post-processor serializes as _dd.integration. Plain initComponent stays tag-only.
  • Null / empty constants are dropped (no tag), matching AgentSpan.setTag.

This replaces the per-tag approach used today in BaseDecorator / TagProcessors, which requires working at the TagMap level rather than the span level and a cached TagMap.Entry field per tag.

Motivation

The constant tags a span carries are applied today via per-afterStart setTag calls. SpanPrototype sets up those constants once so a span can be constructed with them with fast, light copying.

Additional Notes

Part of the #11828 split. The decorator demo that bakes prototypes in real integrations is the stacked follow-up, #11895.

Commits:

  1. Drop null/empty constants; add TagMap.Entry.isEmptyValue
  2. Add the construction path (buildSpan / startSpan(SpanPrototype))
  3. Carry the integration name (component → _dd.integration)
  • SpanPrototype.NONE is intentionally kept: the no-prototype sentinel (CoreTracer default field + != NONE guard), not dead code.

🤖 Generated with Claude Code

@datadog-datadog-prod-us1-2

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.83 s 14.70 s [+0.1%; +1.7%] (maybe worse)
startup:insecure-bank:tracing:Agent 13.67 s 13.63 s [-0.5%; +1.1%] (no difference)
startup:petclinic:appsec:Agent 16.93 s 17.33 s [-6.6%; +2.0%] (no difference)
startup:petclinic:iast:Agent 17.36 s 17.51 s [-1.9%; +0.3%] (no difference)
startup:petclinic:profiling:Agent 17.20 s 17.22 s [-1.1%; +0.8%] (no difference)
startup:petclinic:sca:Agent 17.52 s 16.84 s [-0.2%; +8.3%] (no difference)
startup:petclinic:tracing:Agent 16.59 s 16.19 s [-1.8%; +6.7%] (no difference)

Commit: c1d7fd29 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@dougqh

dougqh commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Design note: SpanPrototype × TagInterceptor (resolve before baking is wired into construction, #11834)

Several tags a prototype naturally bakes as constants are exactly the keys TagInterceptor.intercept() special-cases (dd-trace-core/.../taginterceptor/TagInterceptor.java switch): span.type, span.kind, http.method, http.url, component, plus resource.name / service / peer.service. For these, setTag() today is not a plain map write — it has side effects: span.type populates the DDSpan.type field, URL-as-resource-name, service/peer.service extraction, normalization, etc.

So there's a fork once prototypes write baked tags directly to storage:

  • Bypass intercept() (direct write) → the side effects don't fire — e.g. a baked span.type tag wouldn't populate DDSpan.type, URL-as-resource-name wouldn't run.
  • Route baked tags through intercept() per span → correctness preserved, but we lose the baked-once benefit (re-running the interceptor per span is the cost we're trying to remove).

Proposed resolution (fits "baked once"): the interceptor's inputs are constant for a given prototype, so its outputs are too — bake the post-interception result at prototype-construction time (resolve the effect once: set the type field, store normalized values) rather than the raw tag. That means prototype baking partitions its tags into interceptor-relevant (bake the resolved effect) vs inert (direct write), and we want an explicit list of which constant tags carry interceptor semantics.

Ties into the TagInterceptor-retirement direction (absorb interceptor logic into the map as inlinable data) — baking the resolved effect is a step in that same direction.

@dougqh
dougqh force-pushed the dougqh/span-prototype-api branch from 3af8e13 to 41666f5 Compare July 20, 2026 20:04
@dougqh dougqh changed the title Introduce SpanPrototype: baked-once constant span-tag descriptor Introduce SpanPrototype: baked-once constant span-tag descriptor (phase 1b) Jul 20, 2026
@dougqh
dougqh marked this pull request as ready for review July 21, 2026 16:48
@dougqh
dougqh requested review from a team as code owners July 21, 2026 16:48
@dougqh
dougqh requested review from mcculls and removed request for a team July 21, 2026 16:48

@datadog-datadog-prod-us1-2 datadog-datadog-prod-us1-2 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

More details

SpanPrototype is a well-designed immutable descriptor for constant span tags. The builder correctly handles null values, supports inheritance via extends_(), and freezes tags at construction. No behavioral regressions detected from synthetic scenarios covering identity fields, tag accumulation, inheritance chains, and edge cases.

Was this helpful? React 👍 or 👎

📊 Validated against 8 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 41666f5 · What is Autotest? · Any feedback? Reach out in #autotest

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41666f5648

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@dougqh

dougqh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e1b32a9dd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Outdated
Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Outdated
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

View session in Datadog

Bits Code status: ✅ Done

Comment @DataDog to request changes

@dougqh
dougqh force-pushed the dougqh/span-prototype-api branch from 8e1b32a to 45f121f Compare July 21, 2026 18:56
@dougqh

dougqh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45f121f777

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Outdated
Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
@dougqh
dougqh force-pushed the dougqh/span-prototype-api branch from 45f121f to 2143299 Compare July 21, 2026 19:23
@dougqh

dougqh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

1 similar comment
@dougqh

dougqh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@dougqh
dougqh force-pushed the dougqh/span-prototype-api branch from 2143299 to 4276da0 Compare July 21, 2026 19:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4276da0399

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

dougqh and others added 7 commits July 29, 2026 08:12
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 <noreply@anthropic.com>
…formly 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…tion 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/span-prototype-api branch from eadba23 to c2d5fb9 Compare July 29, 2026 12:17
# Conflicts:
#	dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
@dougqh
dougqh requested a review from a team as a code owner August 29, 2026 01:45
@dougqh
dougqh requested review from jpbempel and removed request for a team August 29, 2026 01:45

@datadog-datadog-prod-us1-2 datadog-datadog-prod-us1-2 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: FAIL

The new prototype start method exists only on TracerAPI. Auto-instrumentation uses AgentTracer.startSpan, so it cannot call this overload through the standard facade.

Open Bits AI session

🤖 Datadog Autotest · Commit 7940f30 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

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 <noreply@anthropic.com>
@dougqh
dougqh requested a review from mcculls September 10, 2026 12:39
Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java

@mcculls mcculls left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concerns have been addressed - but I do want to flag a potential future issue that Claude found where special cased tags could still potentially get clobbered by the tagInterceptor.interceptTag call in seedAbsentTags because the intercepted tag never reaches unsafeTags.

Concretely, walk through ANALYTICS_SAMPLE_RATE:

private boolean interceptAnalyticsSampleRate(DDSpanContext span, Object value) {
  Number analyticsSampleRate = getOrTryParse(value);
  if (null != analyticsSampleRate) {
    span.setMetric(ANALYTICS_SAMPLE_RATE, analyticsSampleRate);   // <-- side effect happens here
  }
  return true;   // <-- return value only controls whether unsafeTags.set() also runs
}

Scenario:

  1. Something earlier explicitly sets ANALYTICS_SAMPLE_RATE = 0.5 via the normal tag path. interceptTag runs, calls setMetric(..., 0.5), returns true → nothing is stored in unsafeTags (by design — this tag never lives there).
  2. apply(prototype) runs later with the prototype baking ANALYTICS_SAMPLE_RATE = 1.0.
  3. In seedAbsentTags: ctx.unsafeTags.containsKey("...analytics...") is false (it was never in unsafeTags — see step 1), so we proceed.
  4. ctx.tagInterceptor.interceptTag(ctx, tag, 1.0) runs → calls setMetric(..., 1.0) right there, clobbering the 0.5 that was explicitly set. It then returns true, so unsafeTags.set(tagEntry) is skipped — but the clobber already happened via the interceptor call itself.

So the if (!interceptTag(...)) check only ever prevents a redundant raw-tag write for tags that the interceptor consumes; it can't undo an interceptor side effect that already ran. The containsKey pre-check is the only guard against re-application, and it's blind to any tag whose canonical storage isn't unsafeTags (analytics rate, service name, resource name, http status, origin, measured, sampling priority). The finding stands — it's the interceptor invocation itself, not just the post-hoc unsafeTags.set, that needs to be skipped for tags already logically present via their side-channel field.

EDIT: also some of the javadoc comments could be made more succinct (for example This is the single seam through which a prototype's constant initial state is applied is not really adding much to the readability)

@dougqh
dougqh requested review from vandonr and removed request for jpbempel September 11, 2026 10:53
@dougqh

dougqh commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

My concerns have been addressed - but I do want to flag a potential future issue that Claude found where special cased tags could still potentially get clobbered by the tagInterceptor.interceptTag call in seedAbsentTags because the intercepted tag never reaches unsafeTags.

@mcculls Thanks for the review. I'll admit that I'm having to make compromises to the SpanPrototype in a way that doesn't require changing everything else all at once. I'm trying to take care not to change existing semantics, but the funky tag precedence and TagInterceptor aren't making it easy. I do intend to address both of those eventually, but it is a slow process unraveling the interconnections.

@dougqh
dougqh added this pull request to the merge queue Sep 11, 2026
@dd-octo-sts

dd-octo-sts Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-09-11 16:12:18 UTC ℹ️ Start processing command /merge


2026-09-11 16:12:23 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-09-11 17:23:39 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 11, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 3110972 into master Sep 11, 2026
603 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the dougqh/span-prototype-api branch September 11, 2026 17:23
@github-actions github-actions Bot added this to the 1.67.0 milestone Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants