Repository navigation
Introduce SpanPrototype: baked-once constant span-tag descriptor (phase 1b) - #11894
Conversation
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
Design note: SpanPrototype × TagInterceptor (resolve before baking is wired into construction, #11834)Several tags a prototype naturally bakes as constants are exactly the keys So there's a fork once prototypes write baked tags directly to storage:
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 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. |
3af8e13 to
41666f5
Compare
There was a problem hiding this comment.
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.
📊 Validated against 8 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit 41666f5 · What is Autotest? · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 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".
|
@codex review |
There was a problem hiding this comment.
💡 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".
|
Bits Code status: ✅ Done Comment @DataDog to request changes |
8e1b32a to
45f121f
Compare
|
@codex review |
There was a problem hiding this comment.
💡 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".
45f121f to
2143299
Compare
|
@codex review |
1 similar comment
|
@codex review |
2143299 to
4276da0
Compare
There was a problem hiding this comment.
💡 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".
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>
eadba23 to
c2d5fb9
Compare
# Conflicts: # dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
There was a problem hiding this comment.
The new prototype start method exists only on TracerAPI. Auto-instrumentation uses AgentTracer.startSpan, so it cannot call this overload through the standard facade.
🤖 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>
There was a problem hiding this comment.
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:
- 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).
- apply(prototype) runs later with the prototype baking ANALYTICS_SAMPLE_RATE = 1.0.
- In seedAbsentTags: ctx.unsafeTags.containsKey("...analytics...") is false (it was never in unsafeTags — see step 1), so we proceed.
- 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)
@mcculls Thanks for the review. I'll admit that I'm having to make compromises to the |
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|
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 underlyingTagMap.Entryobjects, instead ofBaseDecorator.afterStartstamping each tag per span.The side effects
afterStartperforms alongside tag writes are preserved, not just the tags:span.kindseeds through theTagInterceptor, so the cached span-kind ordinal (which drivesisOutbound/isKind) is set — not merely the tag.componentviainitComponentAndIntegrationalso records the integration name (setIntegrationName), which theIntegrationAdderpost-processor serializes as_dd.integration. PlaininitComponentstays tag-only.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 cachedTagMap.Entryfield per tag.Motivation
The constant tags a span carries are applied today via per-
afterStartsetTagcalls.SpanPrototypesets 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:
TagMap.Entry.isEmptyValuebuildSpan/startSpan(SpanPrototype))component→_dd.integration)SpanPrototype.NONEis intentionally kept: the no-prototype sentinel (CoreTracerdefault field +!= NONEguard), not dead code.🤖 Generated with Claude Code