Skip to content

Add TagContributor / TagExtractor span-tag APIs#11817

Draft
dougqh wants to merge 3 commits into
masterfrom
dougqh/tag-extractor-api
Draft

Add TagContributor / TagExtractor span-tag APIs#11817
dougqh wants to merge 3 commits into
masterfrom
dougqh/tag-extractor-api

Conversation

@dougqh

@dougqh dougqh commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Draft — API contract + justifying benchmark. Consumers stack on top: #11820 (JDBC), #11822 (peer-connection), #11823 (dbUser peel).

Foundation for incrementally dissolving the span Decorators (deep virtual hierarchy: BaseDecorator → {Client,Server}Decorator → {Http,Database,…}* → ~149 concrete) into composable, JIT-friendly tag projection.

What

  • TagContributor (intrinsic) — a typed POJO we own projects its own state onto a span via addTo(AgentSpan). DBInfo / git-meta / ExtractedContext are the precedents.
  • TagExtractor<T> (extrinsic, @FunctionalInterface) — extracts tags from a foreign object we don't own (InetSocketAddress/Request) onto a span. Realized as a static final non-capturing extractor invoked from the integration's own (monomorphic) advice site.
  • AgentSpan.setTags(source, extractor) — the span-first, product-facing form (span.setTags(dbInfo, EXTRACTOR), matching setTag/setAllTags). The extra indirection inlines away at a monomorphic site; it's also the seam an implementer can override to coarse-lock the whole extraction.
  • TagProjectionBenchmark — justifies the direction (decorator vs extractor vs contributor dispatch).

Both interfaces target AgentSpan for now (so they can also drive span-level state — resource name, error, status — during the transition); the sink narrows toward TagMap as those fields migrate into the tag model.

Convention established by the consumers: extractors are promoted to named singleton classes (XExtractor implements TagExtractor<T> + private-ctor INSTANCE) rather than inline lambdas — for a name at the call site, a home for pure statics + caches (class static, not lambda-captured → stays non-capturing), and composability the inheritance chain lacked. The one rubric line: no capturing lambdas (allocates per call + breaks memoization on any lazy variant).

Why (benchmark, @Threads(8) @Fork(3), Throughput + -prof gc)

The template-method decorator makes one dynamic dispatch per hook; an extractor/contributor makes one total.

arm mono mega
decorator 1525M 426M (−72%)
extractor 1256M 914M (−27%)
contributor 1655M 1026M (−38%)
  • mono (one type): all inline → ~tie — the abstraction is free at the decorator's best case.
  • mega (8 equivalent loaded types → megamorphic): decorator collapses; extractor/contributor degrade far less. Zero allocation in all arms (pure dispatch).
  • -XX:+PrintInlining confirms the mechanism: decorator-mega leaves all 4 template calls as virtual call (megamorphic, not inlined); extractor-mega is one virtual call + inlined body.

Two stacked wins: a robust floor (fewer dynamic dispatches, N→1 — holds on the interpreter/C1/cold path, no JIT needed) and a JIT ceiling (devirt + inline → ~zero when monomorphic). By construction the decorator's dispatch is structurally megamorphic; extractors are per-integration monomorphic → ~3× in the real-world comparison (extractor-mono vs decorator-mega).

/techdebt deferred until ready-for-review (draft).

🤖 Generated with Claude Code

dougqh and others added 2 commits June 30, 2026 17:03
Foundational contract for dissolving Decorators (see decorator-dissolution.md).
API types only -- no wiring or consumers yet.

- TagContributor (intrinsic): a typed POJO we own projects its own state onto
  a span via addTo(AgentSpan). DbInfo / git-meta / ExtractedContext are the
  precedents; addTo is a flat setTag sweep that becomes setTag(KnownTagIds.X,
  field) once the id-arm lands.
- TagExtractor<T> (extrinsic, @FunctionalInterface): extracts tags from a
  foreign object we don't own (Connection/Request) onto a span. Intended as a
  static-final non-capturing lambda invoked from the integration's own advice
  (a monomorphic site the JIT devirtualizes + inlines) -- the opposite of the
  decorator's shared megamorphic dispatch. Two modes: place tags directly, or
  build a memoized POJO that is itself a TagContributor.

Both target AgentSpan for now so they can drive span-level state (resource
name, error, status) during the transition; the sink narrows toward TagMap as
those fields migrate into the tag model. Javadoc bakes in the compile-to-~zero
litmus + the monomorphic-site / no-shared-sink discipline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Representative benchmark for the decorator-dissolution thesis: project a
typed source's fields onto a span three ways (decorator template-method
pattern / TagExtractor-shaped lambda / TagContributor-shaped), to show
what the JIT compiles away and what survives. Synthetic structural
equivalents + a light DCE-safe sink isolate *dispatch* cost.

@Param mode {mono, mega}: mono = one type (charitable to the decorator);
mega = 8 equivalent loaded types exercised so the call site / shared-base
template calls go megamorphic. Methodology: @threads(8), @fork(3),
Throughput + -prof gc (dougqh's standard — multi-thread reveals alloc
pressure, 3+ forks expose bimodal JIT/devirt).

Results (ops/s, 3f x 8t): mono ~ties (1.26-1.66B, abstraction free);
mega decorator 426M (-72%) vs extractor 914M (-27%) / contributor 1026M
(-38%). Zero alloc in all arms (pure dispatch). PrintInlining confirms
the mechanism: decorator-mono inlines all 4 template calls; decorator-
mega leaves all 4 as `virtual call` (megamorphic); extractor-mega has ONE
virtual call + inlined body. The template-method pattern pays one
megamorphic dispatch PER template method; extractor/contributor pay one
total -> the decorator's cost scales with field count (worse for the 7+
HttpServer hooks), while extractor/contributor can stay monomorphic
per-site (~3-4x faster in reality).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh dougqh added type: enhancement Enhancements and improvements comp: core Tracer core tag: no release notes Changes to exclude from release notes tag: ai generated Largely based on code generated by an AI or LLM labels Jun 30, 2026
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 0.00%
Overall Coverage: 54.94% (+0.03%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: f2878b2 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Jun 30, 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.79 s 14.64 s [+0.3%; +1.8%] (maybe worse)
startup:insecure-bank:tracing:Agent 13.63 s 13.67 s [-1.1%; +0.5%] (no difference)
startup:petclinic:appsec:Agent 16.71 s 15.99 s [+0.1%; +8.8%] (maybe worse)
startup:petclinic:iast:Agent 16.39 s 16.89 s [-7.2%; +1.2%] (no difference)
startup:petclinic:profiling:Agent 16.81 s 16.72 s [-0.5%; +1.5%] (no difference)
startup:petclinic:sca:Agent 16.37 s 16.58 s [-5.6%; +3.2%] (no difference)
startup:petclinic:tracing:Agent 16.12 s 16.23 s [-1.7%; +0.3%] (no difference)

Commit: f2878b2f · 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.

The span-first, product-dev-facing form of extractor.extract(source, this)
(matches setTag / setAllTags). The indirection inlines away at a monomorphic
call site, and it's the seam where an implementer can coarse-lock the whole
extraction. Part of the TagExtractor API; consumed by the JDBC canary and
subsequent extractor migrations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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: enhancement Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant