Skip to content

R2DBC net-new instrumentation (io.r2dbc:r2dbc-spi 1.0.0)#12032

Draft
jordan-wong wants to merge 3 commits into
masterfrom
eval/r2dbc-net-new-20260721
Draft

R2DBC net-new instrumentation (io.r2dbc:r2dbc-spi 1.0.0)#12032
jordan-wong wants to merge 3 commits into
masterfrom
eval/r2dbc-net-new-20260721

Conversation

@jordan-wong

@jordan-wong jordan-wong commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Reference PR from the apm-instrumentation-toolkit's net-new generation workflow, targeting io.r2dbc:r2dbc-spi:1.0.0.RELEASE. No dd-trace-java module exists for R2DBC today, so this was not a blind regen — there was nothing to hide/restore. This measures from-scratch generation quality against an SPI-only Maven coordinate (no concrete driver dependency).

Generated module: dd-java-agent/instrumentation/r2dbc/r2dbc-1.0/

Toolkit reviewer verdict: needs_work

The toolkit's own review_cycle ran 5 iterations and did not reach approved (todos_remaining=15 after iter5, the apparent iteration cap). Total cost: $65.81, duration: ~2h17m.

What went right (structural checks all clean):

  • Correct SPI-first targeting throughout (io.r2dbc.spi.Statement/Connection/Batch), no concrete-driver coupling
  • Correct single-name super("r2dbc"), no spurious version alias
  • Correctly classified as Cat A (span-creating); all Cat B checks correctly "not applicable"
  • No scheduler-exclusion, overload-selection, or context-store-hoisting issues

What's broken — real semantic-completeness gaps (dominant finding):

  • db.name (REQUIRED tag) is never set. ConnectionInstrumentation only extracts from ConnectionMetadata, which doesn't expose database/host/port/user. Root cause (per reviewer): should hook ConnectionFactory.create() or the Connection constructor instead, which exposes ConnectionFactoryOptions with that data.
  • peer.hostname, db.user, network.destination.port (RECOMMENDED tags) — all missing for the same reason.
  • Dead code: R2dbcConnectionInfo has fields (dbInstance, dbUser, dbHostname) defined but never populated.
  • BatchInstrumentation spans have zero connection metadata tags.
  • Test coverage gap mirrors the code gap; inconsistent db.type value between batch ("r2dbc") and statement ("testdb") tests.

Minor: 1 inline narrative comment to remove; missing explicit muzzle fail blocks for 0.8.x/0.9.x version families. Originally noted as "not urgent — assertInverse=true already isolates," but see CI Triage Status below: that assertInverse=true was actually asserting a false compatibility boundary and has been removed as part of a CI fix, so there is now no automated check that pre-1.0 releases are excluded — this remains a real (if low-priority) gap.

This PR is left as-is (not iterated further) so the diff reflects exactly what the toolkit produced — do not fix findings on this branch; it's a research artifact, not a merge candidate.

Comparison with OpenTelemetry's R2DBC instrumentation (reference for the correct hook point)

OTel Java solved exactly the gap above by hooking a different SPI entry point. R2DBC exposes connection identity (host/port/db/user) only in io.r2dbc.spi.ConnectionFactoryOptions at connection-factory creation — not on Connection/ConnectionMetadata (which exposes only getDatabaseProductName()/getDatabaseVersion()). Where you hook decides whether you can populate these tags at all.

this PR (dd-trace-java) OpenTelemetry Java
Hook point Connection.createStatement() + connection.getMetadata() (ConnectionInstrumentation.java:30,38,53) ConnectionFactories.find(ConnectionFactoryOptions) (instrumentation/r2dbc-1.0/javaagent/.../R2dbcInstrumentation.java)
Carrier R2dbcConnectionInfo DbExecution holding captured ConnectionFactoryOptions (.../library/.../internal/DbExecution.java)
db.name/db.namespace ❌ can't — ConnectionMetadata lacks it ConnectionFactoryOptions.DATABASE
peer.hostname/server.address ConnectionFactoryOptions.HOST
port ConnectionFactoryOptions.PORT
db.user ConnectionFactoryOptions.USER
DBM traceparent (full mode) ❌ passes traceParent=null (ConnectionInstrumentation.java:64) ✅ propagator-driven sqlcommenter injects real W3C traceparent

OTel history check: no PR/issue in opentelemetry-java-instrumentation's r2dbc-1.0 shows these attributes were ever missing-then-added — the factory-level hook is original design, chosen precisely because it's the one place the full connection params are structured data. So this isn't a shared gap; it's a design divergence.

Takeaway: the toolkit correctly recognized R2DBC as SPI-shaped and hooked io.r2dbc.spi.* interfaces — but hooked the wrong SPI entry point for connection identity. The remediation is to capture ConnectionFactoryOptions at factory-create time and thread it forward (as OTel's DbExecution does), instead of reading ConnectionMetadata per statement. Full write-up: toolkit repo docs/eval-research/r2dbc-otel-comparison.md.

CI Triage Status

Last updated: 2026-07-22

Confirmed research findings (do not fix — core instrumentation-logic gap):

  • [Reviewer-surfaced, not CI-surfaced] db.name (REQUIRED tag), peer.hostname/db.user/network.destination.port (RECOMMENDED tags) never populated. ConnectionInstrumentation only extracts from ConnectionMetadata (product name/version only) instead of hooking ConnectionFactory.create()/the Connection constructor, which exposes ConnectionFactoryOptions with the actual host/port/db/user data. No CI check catches this — there's no assertion anywhere that these tags must be present — so this finding would otherwise only live in prose. See "What's broken" above for full detail; candidate skill fix is N-DBM-2 (silent-narrowing process gap) in the toolkit's docs/eval-research/BACKLOG.md.
  • [Reviewer-surfaced, not CI-surfaced] BatchInstrumentation spans carry zero connection-metadata tags — same root cause as above, compounded (batch path never wires through what statement path partially has).
  • [Reviewer-surfaced, not CI-surfaced] Dead code: R2dbcConnectionInfo fields (dbInstance, dbUser, dbHostname) defined but never populated — same root cause.

Fixed (scaffolding, root cause is a documented finding):

  • dd-gitlab/muzzle: [4/8]muzzle-AssertFail-io.r2dbc-r2dbc-spi-0.8.6.RELEASE and -0.9.1.RELEASE FAILED: "Instrumentation unexpectedly passed Muzzle validation". The generated build.gradle declared versions = "[1.0.0.RELEASE,)" with assertInverse = true, asserting pre-1.0 releases must fail — but io.r2dbc.spi.Statement/Connection/Batch are structurally unchanged across 0.8.x/0.9.x/1.0.x, so they pass anyway. Fixed in commit 521b0d52a1: dropped assertInverse = true. Verified locally AND confirmed green in CI (all muzzle: [*/8] shards SUCCESS post-push). Does not touch the hook point, tags, or integration name — the dominant finding above (missing db.name/peer.hostname/db.user/etc.) is unrelated and unaddressed here on purpose.

Fixed (config/mechanical):

  • dd-gitlab/buildverifyAgentJarIntegrations failed: "Integration list differs from metadata/agent-jar-checks.properties" — super("r2dbc") registered but the golden file was never updated. Fixed in commit 368d2094ce: added r2dbc,\ alphabetically between quartz and ratpack. Verified locally AND confirmed green in CI post-push.

Classified as flake / unrelated:

  • (none yet)

Still unclassified: none.

Process note (not a code finding):

  • dd-gitlab/validate_supported_configurations_v2_local_file — Same pattern as PR [reference] eval: blind regeneration of PostgreSQL JDBC driver (toolkit output) #11997's PostgreSQL finding, investigated the same way: manually checked https://feature-parity.us1.prod.dog/#/configurations for DD_TRACE_R2DBC_ENABLED/DD_TRACE_R2DBC_ANALYTICS_ENABLED/DD_TRACE_R2DBC_ANALYTICS_SAMPLE_RATE — none visible in the registry UI, consistent with these being genuinely new config names with no existing registry entry. Our local entries follow the same convention as master's real integrations. Confirmed process note — remediation requires feature-parity registry write access neither of us has, not a dd-trace-java commit.

🤖 Generated by apm-instrumentation-toolkit. Not a merge candidate — reference/research artifact only.

Reference PR from toolkit net-new generation (no prior dd-trace-java
module exists for R2DBC, so this is not a blind regen). Generated
against io.r2dbc:r2dbc-spi:1.0.0.RELEASE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jordan-wong jordan-wong added tag: ai generated Largely based on code generated by an AI or LLM tag: apm integration toolkit Changes generated by DataDog/apm-instrumentation-toolkit as part of IDM AIT experimentation labels Jul 22, 2026
jordan-wong and others added 2 commits July 22, 2026 17:06
The advice hooks io.r2dbc.spi.Statement/Connection/Batch interfaces,
which are structurally unchanged across the 0.8.x/0.9.x/1.0.x
releases -- assertInverse was asserting that all versions outside
[1.0.0.RELEASE,) must fail muzzle, which is false for the older-but-
compatible SPI releases and caused
muzzle-AssertFail-io.r2dbc-r2dbc-spi-{0.8.6,0.9.1}.RELEASE to fail
CI ("unexpectedly passed Muzzle validation").

This does not touch the hook point -- R2DBC's core finding (missing
db.name/peer.hostname/db.user/network.destination.port from hooking
ConnectionMetadata instead of ConnectionFactoryOptions, see
docs/eval-research/hypotheses/r2dbc.md on the toolkit repo) is
unrelated and unaddressed here on purpose.

Verified locally: `./gradlew :dd-java-agent:instrumentation:r2dbc:r2dbc-1.0:muzzle`
BUILD SUCCESSFUL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Integration list differs from metadata/agent-jar-checks.properties --
super("r2dbc") registered in R2dbcInstrumenterModule.java but the
golden file was never updated. Inserted alphabetically between
quartz and ratpack.

Hand-edited rather than running the Gradle golden-file-update task
directly, but verified via `./gradlew :dd-java-agent:verifyAgentJarIntegrations`
BUILD SUCCESSFUL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Jul 22, 2026

Copy link
Copy Markdown

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 57.26% (-0.13%)

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

@dd-octo-sts

dd-octo-sts Bot commented Jul 22, 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 13.99 s 14.07 s [-1.5%; +0.4%] (no difference)
startup:insecure-bank:tracing:Agent 12.96 s 13.11 s [-1.9%; -0.4%] (maybe better)
startup:petclinic:appsec:Agent 17.02 s 16.65 s [+1.0%; +3.4%] (significantly worse)
startup:petclinic:iast:Agent 16.92 s 16.38 s [-0.9%; +7.6%] (no difference)
startup:petclinic:profiling:Agent 16.86 s 16.92 s [-1.7%; +1.0%] (no difference)
startup:petclinic:sca:Agent 17.02 s 16.73 s [+0.8%; +2.8%] (maybe worse)
startup:petclinic:tracing:Agent 16.14 s 16.10 s [-0.7%; +1.3%] (no difference)

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

@jordan-wong jordan-wong changed the title [reference] R2DBC net-new instrumentation (io.r2dbc:r2dbc-spi 1.0.0) R2DBC net-new instrumentation (io.r2dbc:r2dbc-spi 1.0.0) Jul 23, 2026
@ygree

ygree commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@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: 368d2094ce

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +40 to +41
public void onSubscribe(Subscription s) {
delegate.onSubscribe(s);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Finish spans when R2DBC subscriptions are cancelled

When a returned Publisher is cancelled before onComplete/onError (for example via take(1), a timeout, or a disconnected request), this wrapper passes the driver's original Subscription straight through, so TracingSubscriber never observes the cancellation and the span that was already created in execute() can remain open indefinitely. Wrap the Subscription and finish the span on cancel() (with a guard against later terminal signals) so cancelled R2DBC queries do not leak spans.

AGENTS.md reference: AGENTS.md:L79-L81

Useful? React with 👍 / 👎.

// Connection may be closed or metadata unavailable
}
if (INJECT_COMMENT) {
sql = R2dbcSQLCommenter.inject(sql, null, dbType, null, null, null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass an actual traceparent for DBM full mode

When DD_DBM_PROPAGATION_MODE=full, this is the only point where the SQL text is modified, but the traceParent argument is always null because the R2DBC span is not created until execute(); the later execute advice only sets _dd.dbm_trace_injected and cannot update the SQL. The resulting DBM comment has no traceparent, so full-mode DBM cannot correlate the database query with the APM span despite the span being tagged as injected; delay comment injection until a span exists or avoid setting the injected tag unless a traceparent was included.

Useful? React with 👍 / 👎.

Comment on lines +40 to +41
public void onSubscribe(Subscription s) {
delegate.onSubscribe(s);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Finish spans when R2DBC subscriptions are cancelled

When a returned Publisher is cancelled before onComplete/onError (for example via take(1), a timeout, or a disconnected request), this wrapper passes the driver's original Subscription straight through, so TracingSubscriber never observes the cancellation and the span that was already created in execute() can remain open indefinitely. Wrap the Subscription and finish the span on cancel() (with a guard against later terminal signals) so cancelled R2DBC queries do not leak spans.

Useful? React with 👍 / 👎.

Comment on lines +61 to +64
public void onComplete() {
try {
DECORATE.beforeFinish(span);
span.finish();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep spans open through R2DBC result consumption

For R2DBC callers that consume the returned Result later (for example Mono.from(statement.execute()).flatMap(result -> result.map(...)) or getRowsUpdated()), the outer Publisher<Result> can complete before the row/update-count Publisher is consumed, but this line finishes the database span at that outer completion. Errors and latency that occur while consuming rows or update counts are then missed; wrap the emitted Result consumption Publishers or otherwise defer finishing until the result stream terminates.

Useful? React with 👍 / 👎.


String sql = info != null ? info.getSql() : null;

AgentSpan span = startSpan("r2dbc", R2DBC_QUERY);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer R2DBC spans until subscription

When clients assemble a reactive pipeline without immediately subscribing (common with Spring/Reactor, or if a branch never subscribes), this starts a span during Statement.execute() even though no database work has begun. That span is attached to the assembly-time context and can stay open forever if the publisher is never subscribed, or record inflated latency if subscribed later; create/decorate the span in the returned publisher's subscribe path instead.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tag: ai generated Largely based on code generated by an AI or LLM tag: apm integration toolkit Changes generated by DataDog/apm-instrumentation-toolkit as part of IDM AIT experimentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants