[reference] R2DBC net-new instrumentation (io.r2dbc:r2dbc-spi 1.0.0, toolkit output) - #12032
[reference] R2DBC net-new instrumentation (io.r2dbc:r2dbc-spi 1.0.0, toolkit output)#12032jordan-wong wants to merge 4 commits into
Conversation
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>
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>
|
🎯 Code Coverage (details) 🔗 Commit SHA: ada5972 | Docs | Datadog PR Page | Give us feedback! |
🟢 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. |
|
@codex review |
There was a problem hiding this comment.
💡 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".
| // Connection may be closed or metadata unavailable | ||
| } | ||
| if (INJECT_COMMENT) { | ||
| sql = R2dbcSQLCommenter.inject(sql, null, dbType, null, null, null); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
No code change — recorded as a documented finding, not fixed on this branch. This is a [reference] not-for-merge research PR, and this is its central instrumentation-logic finding (the missing traceparent + broader connection-metadata gap): it can't be fixed without moving comment injection out of createStatement() to after the span exists, which is the same span-lifecycle restructure as the defer-until-subscribe finding. OpenTelemetry solves it by hooking ConnectionFactories.find() and injecting via a propagator-driven commenter — side-by-side comparison with line refs: https://github.com/DataDog/apm-instrumentation-toolkit/blob/eval/java/docs/eval-research/r2dbc-otel-comparison.md . Captured in the PR body and docs/eval-research/hypotheses/r2dbc.md.
| public void onComplete() { | ||
| try { | ||
| DECORATE.beforeFinish(span); | ||
| span.finish(); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
No code change — documented finding, not fixed. Correct: the span finishes when the outer Publisher<Result> completes, so errors/latency during row/update-count consumption (e.g. flatMap(result -> result.map(...)), getRowsUpdated()) are missed. Fixing means wrapping the emitted Result's downstream consumption publishers — coupled to the same span-lifecycle restructure as the defer-until-subscribe finding above. Recorded as an instrumentation-logic finding in the PR body + hypotheses/r2dbc.md; left as-generated on this reference PR.
|
|
||
| String sql = info != null ? info.getSql() : null; | ||
|
|
||
| AgentSpan span = startSpan("r2dbc", R2DBC_QUERY); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
No code change — documented finding, not fixed. Correct diagnosis: the span is started in execute()'s OnMethodEnter rather than in the returned publisher's subscribe(), so an assembled-but-never-subscribed pipeline leaks the span or records inflated latency. Fixing this = moving span creation/decoration into TracingPublisher.subscribe(), a fundamental restructure of the module's approach; on a reference PR that would destroy the artifact this experiment exists to show. Recorded as an instrumentation-logic finding in the PR body + docs/eval-research/hypotheses/r2dbc.md rather than patched.
Codex review (PR #12032): when a returned Publisher is cancelled before onComplete/onError (take(1), timeout, disconnected request), TracingSubscriber passed the driver's Subscription straight through, so cancellation was never observed and the span created in execute() could stay open indefinitely. Wrap the Subscription (named TracingSubscription helper) so cancel() finishes the span, guarded by an AtomicBoolean shared with onComplete/onError so a cancel racing a terminal signal can't double-finish. Registered the new helper class in R2dbcInstrumenterModule.helperClassNames() (muzzle requires it). Verified: compileJava + muzzle + module test suite all green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| final String originalSql = sql; | ||
| String dbType = null; | ||
| try { | ||
| ConnectionMetadata metadata = connection.getMetadata(); |
There was a problem hiding this comment.
Hook-point difference vs OpenTelemetry (primary). This reads connection identity from Connection.getMetadata() (io.r2dbc.spi.ConnectionMetadata), which by SPI contract exposes only getDatabaseProductName()/getDatabaseVersion() — no host/port/database/user. OTel Java instead hooks io.r2dbc.spi.ConnectionFactories.find(ConnectionFactoryOptions) (R2dbcInstrumentation.java#L25-L34) and captures ConnectionFactoryOptions at factory-creation, then threads it forward per query in DbExecution (DbExecution.java#L84-L97) — which is why OTel can populate db.namespace/server.address/server.port/db.user and this cannot. For a shippable integration, the hook point likely needs to move to the connection factory. Full side-by-side: docs/eval-research/r2dbc-otel-comparison.md (toolkit repo).
| // Connection may be closed or metadata unavailable | ||
| } | ||
| if (INJECT_COMMENT) { | ||
| sql = R2dbcSQLCommenter.inject(sql, null, dbType, null, null, null); |
There was a problem hiding this comment.
DBM injection difference vs OpenTelemetry. Four of six args to R2dbcSQLCommenter.inject(...) are null here — dbService, hostname, dbName, and crucially traceParent (last arg). So even in full DBM mode the SQL comment carries no traceparent, and DB-side query samples cannot be correlated to the APM trace. Root cause is the same as the hook-point comment above: this runs at createStatement() time, before the span exists, so there is no traceparent to pass. OTel avoids this by injecting through a propagator-driven commenter after the span is established (R2dbcSqlCommenterUtil). A shippable version needs the traceparent wired in (which depends on the hook-point/lifecycle change above).
| if (INJECT_COMMENT) { | ||
| sql = R2dbcSQLCommenter.inject(sql, null, dbType, null, null, null); | ||
| } | ||
| return R2dbcConnectionInfo.of(originalSql, dbType, null, null, null); |
There was a problem hiding this comment.
Dead connection-metadata fields (consequence of the hook point). R2dbcConnectionInfo.of(originalSql, dbType, null, null, null) passes null for dbInstance/dbUser/dbHostname — so those fields exist on R2dbcConnectionInfo but are never populated, and the decorator overrides that would consume them (R2dbcDecorator.dbUser/dbInstance/dbHostname, lines 58-69) all return null. Net effect on spans: db.name (REQUIRED), peer.hostname, db.user, network.destination.port are never set. OTel populates all of these from ConnectionFactoryOptions (R2dbcSqlAttributesGetter.java#L50-L116). These three comments are one finding with one fix: move the hook to the connection factory.
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static AgentScope onEnter() { | ||
| AgentSpan span = startSpan("r2dbc", R2DBC_BATCH); | ||
| DECORATE.afterStart(span); |
There was a problem hiding this comment.
Batch spans carry zero connection metadata (secondary). This advice does startSpan + afterStart but never calls onConnection/attaches any R2dbcConnectionInfo, so Batch.execute() spans have no db.*/peer.* tags at all — even less than the statement path. OTel treats batch executions through the same per-execution DbExecution carrying ConnectionFactoryOptions, so batch spans get the same connection attributes as statement spans. Same root fix (connection-factory hook) resolves this too. Lower priority than the statement path, but noting it for completeness of the shippability assessment.
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_workThe toolkit's own review_cycle ran 5 iterations and did not reach
approved(todos_remaining=15after iter5, the apparent iteration cap). Total cost: $65.81, duration: ~2h17m.What went right (structural checks all clean):
io.r2dbc.spi.Statement/Connection/Batch), no concrete-driver couplingsuper("r2dbc"), no spurious version aliasWhat's broken — real semantic-completeness gaps (dominant finding):
db.name(REQUIRED tag) is never set.ConnectionInstrumentationonly extracts fromConnectionMetadata, which doesn't expose database/host/port/user. Root cause (per reviewer): should hookConnectionFactory.create()or theConnectionconstructor instead, which exposesConnectionFactoryOptionswith that data.peer.hostname,db.user,network.destination.port(RECOMMENDED tags) — all missing for the same reason.R2dbcConnectionInfohas fields (dbInstance,dbUser,dbHostname) defined but never populated.BatchInstrumentationspans have zero connection metadata tags.db.typevalue 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=truealready isolates," but see CI Triage Status below: thatassertInverse=truewas 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.ConnectionFactoryOptionsat connection-factory creation — not onConnection/ConnectionMetadata(which exposes onlygetDatabaseProductName()/getDatabaseVersion()). Where you hook decides whether you can populate these tags at all.Connection.createStatement()+connection.getMetadata()(ConnectionInstrumentation.java:30,38,53)ConnectionFactories.find(ConnectionFactoryOptions)(instrumentation/r2dbc-1.0/javaagent/.../R2dbcInstrumentation.java)R2dbcConnectionInfoDbExecutionholding capturedConnectionFactoryOptions(.../library/.../internal/DbExecution.java)db.name/db.namespaceConnectionMetadatalacks itConnectionFactoryOptions.DATABASEpeer.hostname/server.addressConnectionFactoryOptions.HOSTConnectionFactoryOptions.PORTdb.userConnectionFactoryOptions.USERtraceparent(full mode)traceParent=null(ConnectionInstrumentation.java:64)OTel history check: no PR/issue in
opentelemetry-java-instrumentation'sr2dbc-1.0shows 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 captureConnectionFactoryOptionsat factory-create time and thread it forward (as OTel'sDbExecutiondoes), instead of readingConnectionMetadataper statement. Full write-up: toolkit repodocs/eval-research/r2dbc-otel-comparison.md.CI Triage Status
Last updated: 2026-07-22
Confirmed research findings (do not fix — core instrumentation-logic gap):
db.name(REQUIRED tag),peer.hostname/db.user/network.destination.port(RECOMMENDED tags) never populated.ConnectionInstrumentationonly extracts fromConnectionMetadata(product name/version only) instead of hookingConnectionFactory.create()/theConnectionconstructor, which exposesConnectionFactoryOptionswith 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'sdocs/eval-research/BACKLOG.md.BatchInstrumentationspans carry zero connection-metadata tags — same root cause as above, compounded (batch path never wires through what statement path partially has).R2dbcConnectionInfofields (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.RELEASEand-0.9.1.RELEASEFAILED: "Instrumentation unexpectedly passed Muzzle validation". The generatedbuild.gradledeclaredversions = "[1.0.0.RELEASE,)"withassertInverse = true, asserting pre-1.0 releases must fail — butio.r2dbc.spi.Statement/Connection/Batchare structurally unchanged across 0.8.x/0.9.x/1.0.x, so they pass anyway. Fixed in commit521b0d52a1: droppedassertInverse = true. Verified locally AND confirmed green in CI (allmuzzle: [*/8]shards SUCCESS post-push). Does not touch the hook point, tags, or integration name — the dominant finding above (missingdb.name/peer.hostname/db.user/etc.) is unrelated and unaddressed here on purpose.Fixed (config/mechanical):
dd-gitlab/build—verifyAgentJarIntegrationsfailed: "Integration list differs frommetadata/agent-jar-checks.properties" —super("r2dbc")registered but the golden file was never updated. Fixed in commit368d2094ce: addedr2dbc,\alphabetically betweenquartzandratpack. Verified locally AND confirmed green in CI post-push.Classified as flake / unrelated:
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 forDD_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 add-trace-javacommit.🤖 Generated by apm-instrumentation-toolkit. Not a merge candidate — reference/research artifact only.