Skip to content

feat(contrib/riverqueue/river): add River job queue integration - #5816

Open
Mohith26 wants to merge 3 commits into
DataDog:mainfrom
Mohith26:feat/contrib-riverqueue-river
Open

Mohith26 wants to merge 3 commits into
DataDog:mainfrom
Mohith26:feat/contrib-riverqueue-river

Conversation

@Mohith26

@Mohith26 Mohith26 commented Nov 4, 2026 •

Copy link
Copy Markdown

What does this PR do?

Adds contrib/riverqueue/river, a tracing integration for River (github.com/riverqueue/river), the Postgres job queue, with Orchestrion support.

rivertrace.NewMiddleware() is a River middleware (rivertype.JobInsertMiddleware + rivertype.WorkerMiddleware) that creates:

  • river.insert_many (producer) around every insert call (Insert, InsertTx, InsertMany, ...). One span per batch; resource is the job kind, or multiple for mixed batches. Tags: messaging.system=river, messaging.destination.name (queue, when the batch shares one), messaging.batch.message_count, river.insert.unique_skipped_as_duplicate_count.
  • river.work (consumer) around every job attempt. Resource is the job kind. Tags: messaging.destination.name, messaging.message.id (job ID), river.job.kind, river.job.attempt, river.job.max_attempts, river.job.priority.

Propagation. The insert span's context is stored in each job's metadata under _datadog (same key as the SQS/SNS integrations), and the work span is its child. This is the part the OTel route (rivercontrib/otelriver) doesn't give Datadog users, as noted in #3103: the sampling decision of the request that enqueued the job now carries over to every attempt of that job, including retries that run hours later. If the worker's context already holds a span (for example one that was active when client.Start was called), the propagated context still wins. WithDistributedTracing(false) turns propagation off.

Errors. Snoozes (river.JobSnooze) are not errors and get river.job.snoozed. Cancels (river.JobCancel) are errors and get river.job.cancelled. Panics are recorded and re-raised with the original value; the end-to-end test checks that River still records the worker's own stack frame in AttemptError.Trace. WithErrorCheck can ignore errors. Tracing never fails an insert: metadata that isn't a JSON object is left for River to reject as it does today.

Options. WithService (default DD_SERVICE, resolved lazily like the connect integration, since Orchestrion can build clients before tracer.Start), WithCustomTag, WithErrorCheck, WithDistributedTracing.

Orchestrion. river.NewClient is generic and takes its middleware inside *river.Config, so the aspect prepends to NewClient's body: it copies the config and adds the middleware first (outermost) with rivertrace.AddMiddleware. The caller's Config and slices are never modified (the TestCaseExistingMiddleware suite checks spare capacity too), a client that already has the middleware (in Middleware, Plugins or the deprecated fields) isn't traced twice, and configs using the deprecated JobInsertMiddleware/WorkerMiddleware fields get it there instead, since River rejects mixing them with Middleware. The contrib package only imports rivertype, so injecting it into package river has no import cycle.

Performance. Static tags are built once per middleware (NewStartSpanConfig), per-call tags go through one WithTags, ordered as contrib/INTEGRATIONS.md requires. The trace context is encoded once per batch. Empty metadata (River's default) and objects without a trace context get the entry spliced in without decoding the metadata. BenchmarkWithTraceContext, median of 6 runs on Apple Silicon arm64, compared to decoding and re-encoding the object:

metadata before after
small object (4 keys) 26 allocs, 1331 B 1 alloc, 288 B
1 KiB object 20 allocs, 3201 B 1 alloc, 1281 B

ns/op went from about 2.8 us to 1.2 us and 7.2 us to 2.6 us, but that machine was noisy, so I'd treat the alloc numbers as the reliable ones. BenchmarkInsertMany and BenchmarkWork cover the full middleware.

Motivation

Fixes #3103. Revives #3245 (closed in favour of OTel), following the v2 contrib layout and the review feedback there: no .v0 suffix, propagation can be turned off, no swallowed errors in tests, low-cardinality resource names.

Testing

  • go test -race -shuffle=on ./... in contrib/riverqueue/river, 98.4% coverage. Table-driven unit tests for both middleware methods, plus end-to-end tests that run a real River client on River's SQLite driver (riversqlite + modernc.org/sqlite), so they need no database server: trace continuation through the database round trip, sampling priority (manual.keep on the inserting request survives to the work span), batch inserts, user metadata kept, and panics.
  • internal/orchestrion/_integration/riverqueue_river: three TestCases (no middleware, existing application middleware, deprecated WorkerMiddleware field), also on SQLite. Ran locally with go run github.com/DataDog/orchestrion go test ./riverqueue_river/; all pass. After the review fixes I re-checked the woven code with a smaller Orchestrion build (no middleware, deprecated fields, and manual middleware or plugin plus the woven one, each traced once).
  • go test ./scripts/ciselect/, go test ./instrumentation/ ./ddtrace/tracer/, golangci-lint with the repo config on the new packages, the copyright check and configinverter check. Generated files were regenerated with their own generators (gencontribdoc, gencontribs, the orchestrion/all and _integration generators, configinverter generate), and the three touched modules were tidied; I did not run the full make generate / make fix-modules over every module.
  • Not run locally: the Postgres driver (riverpgxv5). River's behaviour is driver independent here and metadata goes through jsonb the same way, but I couldn't run Docker on this machine, so there is no Postgres test in this PR. Happy to add one with the testcontainers helper if you'd like.

Notes for reviewers

  • go.mod requires River v0.51.0 (current). River's own go.mod requires pgx/v5 v5.11.0, so orchestrion/all and _integration move from pgx v5.9.2 to v5.11.0.
  • AddMiddleware is exported for the Orchestrion aspect. I kept it generic over River's three middleware slice types rather than adding three helpers.
  • EnvVarPrefix is RIVER, so TestInstrumentation_AnalyticsRate needs DD_TRACE_RIVER_ANALYTICS_ENABLED in supported_configurations.json (boolean, default false, implementation A). Could a maintainer register it in the configuration registry? The registry validation job will fail until then.
  • AI-assisted (Claude); I reviewed the code and ran everything above locally.

Reviewer's Checklist

  • Changed code has unit tests for its functionality at or near 100% coverage.
  • System-Tests covering this feature have been added and enabled with the va.b.c-dev version tag.
  • There is a benchmark for any new code, or changes to existing code.
  • If this interacts with the agent in a new way, a system test has been added.
  • New code is free of linting errors. You can check this by running make lint locally.
  • New code doesn't break existing tests. You can check this by running make test locally. (Ran the affected packages, see Testing.)
  • Add an appropriate team label so this PR gets put in the right place for the release notes.
  • All generated files are up to date. You can check this by running make generate locally. (Per generator, see Testing.)
  • Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild. Make sure all nested modules are up to date by running make fix-modules locally.

Add a tracing integration for github.com/riverqueue/river. NewMiddleware
returns a River middleware that creates a river.insert_many producer span
per insert batch and a river.work consumer span per job attempt.

The insert span's context, including its sampling decision, is stored in
each job's metadata under "_datadog", so work spans continue the trace that
inserted the job, retries included. WithDistributedTracing(false) turns this
off.

Snoozes are not errors, cancels are, and panics are recorded and re-raised
with their original value. Orchestrion adds the middleware to every
river.Client by prepending to river.NewClient, on a copy of the caller's
config.

Fixes DataDog#3103
@Mohith26
Mohith26 requested review from a team as code owners November 4, 2026 13:10
@Mohith26
Mohith26 requested review from rarguelloF and removed request for a team November 4, 2026 13:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T13:17:36.923831Z fb66637 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@dd-octo-sts dd-octo-sts Bot mentioned this pull request Nov 4, 2026

@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: fb666373d3

ℹ️ 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 contrib/riverqueue/river/river.go Outdated
tags := map[string]any{
ext.ResourceName: job.Kind,
ext.MessagingDestinationName: job.Queue,
ext.MessagingMessageID: job.ID,

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 Encode River job IDs as strings

On every consumer span, this passes River's int64 job ID directly to the tracer, which stores numeric tag values in the span metrics map as float64; messaging.message.id is an identity/string attribute, so BIGINT IDs above 2^53 are rounded and even smaller IDs are emitted in the wrong payload field, preventing reliable correlation with the River job row. Format the ID as a base-10 string before adding the tag.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, fixed in 2cdbd78: the job ID is now formatted as a base-10 string, and the test asserts the string value.

River job IDs are int64. Numeric tags are stored as float64 metrics, which
rounds IDs above 2^53 and puts an identifier in the metrics map.

@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.

Bits Code Review: FAIL

Orchestrion adds duplicate tracing when River middleware is registered through Config.Plugins. This produces duplicate spans and bypasses an explicitly disabled propagation setting.

Open Bits AI session

🤖 Bits Code Review · Commit fb66637

rivertrace: github.com/DataDog/dd-trace-go/contrib/riverqueue/river/v2
template: |-
{{- $cfg := .Function.Argument 1 -}}
if {{ $cfg }} != nil {

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.

P2 Respect tracing middleware registered through Config.Plugins

When an application registers NewMiddleware through Config.Plugins, Orchestrion injects another instance because it checks only the middleware fields. River executes both instances, producing duplicate insert and worker spans. The added default instance also propagates trace context despite WithDistributedTracing(false) on the application's instance. Check Plugins before injecting into either middleware configuration path.

Suggested change
if {{ $cfg }} != nil {
__dd_tracedPlugin := false
if {{ $cfg }} != nil {
for _, __dd_plugin := range {{ $cfg }}.Plugins {
if _, ok := __dd_plugin.(*rivertrace.Middleware); ok {
__dd_tracedPlugin = true
break
}
}
}
if {{ $cfg }} != nil && !__dd_tracedPlugin {

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Right, fixed in 076a5f4: the woven code now also checks Config.Plugins before adding the middleware. Verified with an Orchestrion build where the app registers NewMiddleware as a plugin: one insert span and one work span per job.

…lugin

River runs a middleware registered through Config.Plugins like any other.
Skip clients that already have the tracing middleware there, as the woven
code already did for the deprecated WorkerMiddleware field.
// spliceTraceContext returns the JSON object {"_datadog":traceContext,rest},
// where rest holds the members of another object, without its braces.
func spliceTraceContext(rest, traceContext []byte) []byte {
out := make([]byte, 0, len(metadataKey)+len(traceContext)+len(rest)+6)
@@ -0,0 +1,103 @@
module github.com/DataDog/dd-trace-go/contrib/riverqueue/river/v2
@datadog-datadog-prod-us1-2

Copy link
Copy Markdown
Contributor

Pipelines

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 10 Pipeline jobs failed

Generate | generate — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

Govulncheck | govulncheck-tests — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

Static Checks | check-format — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

View all 10 failed jobs.

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 076a5f4 | Docs | View more details | Give us feedback!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

proposal: support github.com/riverqueue/river

2 participants