Skip to content

feat(tracing): end-to-end trace propagation across the queue (#110, ADR-039)#406

Merged
hardbyte merged 3 commits into
mainfrom
brian/110-trace-propagation
Jul 8, 2026
Merged

feat(tracing): end-to-end trace propagation across the queue (#110, ADR-039)#406
hardbyte merged 3 commits into
mainfrom
brian/110-trace-propagation

Conversation

@hardbyte

@hardbyte hardbyte commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Closes #110 (Rust path + Python handler-side exposure; see "Remaining" below). Implements roadmap decision D8. Design: ADR-039.

What this does

Capture (enqueue). When a producer enqueues inside an OpenTelemetry span, the W3C traceparent is serialized into the job's metadata under the reserved awa:traceparent key. Capture happens in prepare_raw_job_insert — the single choke point that every Rust producer path funnels through (typed, raw, batch, COPY, transactional, Python bindings). No migration, identical on both engines, and SQL-native producers (#342 / ADR-032) participate by setting the key themselves. A caller-supplied value always wins, which keeps the pre-0.7 opt-in pattern working verbatim.

Restore (execute), per OTel messaging semantics:

  • First attempt → remote parent: enqueue → execute reads as one connected trace (the request-triggered-job headline case).
  • Retries → fresh root trace with a span link back to the enqueue site, per the messaging conventions' guidance for deferred processing: a job retried hours later must not stretch the original trace across its backoff schedule, inherit its stale sampling decision, or nest under the dispatcher's per-poll claim span (retries explicitly detach from that ambient context).

Semantic conventions on both sides: producer spans are SpanKind::Producer named send {queue} (send for batch), execution spans are SpanKind::Consumer with messaging.system="awa", messaging.destination.name, messaging.operation.type, messaging.message.id. The execution span keeps its established job.execute {kind} name — kind is the operationally useful dimension for a job queue; the semconv shape rides on kind + attributes (trade-off recorded in the ADR).

Handler-side forwarding: ctx.traceparent() (Rust) and job.traceparent (Python, typed in the .pyi).

E8 gate (< 2% enqueue overhead → default-on)

The added work lives entirely in prepare_raw_job_insert, so the rerunnable #[ignore] bench measures that path directly (end-to-end DB timing cannot resolve a µs delta — it swung ±20% run-to-run on the same code):

capture on : 200000 prepares in 63.36ms (0.32µs/prepare)
capture off: 200000 prepares in 16.71ms (0.08µs/prepare)
capture cost: 0.233µs/enqueue (+0.023% of a 1ms enqueue roundtrip)

Decisively under the gate → capture defaults on, with AWA_TRACE_CAPTURE=off as a process-wide kill switch.

Tests

  • trace_propagation_test.rs (in-memory span exporter, runs in the rest CI shard):
    • full chain test.enqueue → send {queue} → job.execute: same trace id, correct parent ids, producer/consumer span kinds, semconv attributes, handler-visible metadata carries the key;
    • retry: fresh trace id, root span, link back to the enqueue-site span context;
    • the E8 bench above (--ignored).
  • Unit tests: traceparent parse/format roundtrip, W3C malformed/zero-id/version rejection, future-version tolerance, caller-supplied-key precedence, no-ambient-span no-op, non-object metadata untouched.

TLA+

No lifecycle or protocol change — the traceparent is opaque metadata passthrough; capture/restore don't touch state transitions. No model updates required.

Stability policy

Adds one normative paragraph to docs/stability.md: metadata keys prefixed awa: are a reserved namespace (this is its first citizen). Flagging explicitly for review since the policy is now Accepted.

Remaining (#110 follow-up)

Automatic Python producer capture: the Rust core can't see opentelemetry-python's ambient context across the FFI boundary. The documented pattern (set metadata["awa:traceparent"], snippet in getting-started-python.md) covers it manually; automatic capture belongs in the awa-python binding layer and is noted in the ADR. tracestate (vendor state) is intentionally not carried.

Docs: ADR-039, configuration.md § Distributed tracing, getting-started (Rust + Python), CHANGELOG.

Summary by CodeRabbit

  • New Features
    • Added distributed tracing support across job enqueue and execution.
    • Exposed the stored trace context in Rust and Python APIs.
    • Documented a reserved job metadata namespace for trace propagation.
  • Bug Fixes
    • Retry attempts now keep a trace link back to the original enqueue site.
    • First execution attempts continue the original trace automatically.
  • Documentation
    • Updated setup and configuration guides with tracing examples and the disable switch.

…DR-039)

Capture the ambient OpenTelemetry span context at enqueue as a W3C
traceparent under the reserved awa:traceparent metadata key (single choke
point: prepare_raw_job_insert — typed, raw, batch, COPY, transactional and
Python producers all funnel through it; no migration, both engines, SQL
producers set the key themselves). The worker's job.execute span continues
the trace: first attempts join as a remote child; retries detach into a
fresh root trace with a span link back to the enqueue site, per OTel
messaging semantics for deferred processing. Producer and consumer spans
carry messaging semantic conventions. Default-on (E8: ~0.23µs/enqueue,
+0.023% of a 1ms roundtrip), AWA_TRACE_CAPTURE=off kill switch,
ctx.traceparent() / job.traceparent for handler-side forwarding.
@hardbyte hardbyte added this to the v0.7.0 milestone Jul 8, 2026
@hardbyte hardbyte added the full-ci Run the full CI matrix (Python build+test, E2E) on this PR label Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hardbyte, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96d18c63-924c-4a54-8b71-3573e5b28e7d

📥 Commits

Reviewing files that changed from the base of the PR and between 5c963e4 and c853c5c.

📒 Files selected for processing (1)
  • docs/adr/039-trace-propagation.md
📝 Walkthrough

Walkthrough

This PR implements end-to-end distributed tracing across enqueue and worker execution. It adds a trace module in awa-model for W3C traceparent capture/parsing/injection into reserved job metadata, wires it into insert and executor code paths, exposes traceparent via Rust and Python APIs, adds dependencies, integration tests, ADR-039, and documentation.

Changes

Distributed tracing propagation

Layer / File(s) Summary
Workspace dependency additions
Cargo.toml, awa-model/Cargo.toml, awa-worker/Cargo.toml, awa/Cargo.toml
Adds tracing-opentelemetry and opentelemetry crates to workspace and switches consuming crates to workspace-managed versions.
Trace context module
awa-model/src/lib.rs, awa-model/src/trace.rs
Introduces reserved metadata key/env kill switch constants, current_traceparent, parse_traceparent, remote_context, traceparent_from_metadata, inject_traceparent, and unit tests.
Enqueue-time injection and span instrumentation
awa-model/src/insert.rs
prepare_raw_job_insert injects traceparent into metadata; insert span instrumentation for insert_with, insert_many, insert_many_copy, insert_many_copy_from_pool gains OTel/messaging fields.
Worker execution span parenting/linking
awa-worker/src/executor.rs, awa-worker/src/context.rs
job.execute span continues the enqueue trace as remote parent on first attempt or links back on retries; JobContext::traceparent() exposes stored context.
Python bindings
awa-python/src/job.rs, awa-python/python/awa/_awa.pyi
Adds PyJob.traceparent getter and Job[T].traceparent type stub.
Integration tests
awa/tests/trace_propagation_test.rs
Verifies enqueue→execution trace continuation, retry trace-linking, and measures capture overhead.
ADR, changelog, and docs
docs/adr/039-trace-propagation.md, docs/adr/README.md, CHANGELOG.md, docs/configuration.md, docs/getting-started-python.md, docs/getting-started-rust.md, docs/stability.md
Documents design decisions, reserved metadata namespace, kill switch, and usage guidance.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Producer
  participant InsertRs as insert.rs (prepare_raw_job_insert)
  participant Metadata as job.metadata
  participant Executor as JobExecutor
  participant Span as job.execute span

  Producer->>InsertRs: enqueue job
  InsertRs->>Metadata: inject_traceparent(awa:traceparent)
  Executor->>Metadata: traceparent_from_metadata()
  alt first attempt
    Executor->>Span: set_parent(remote enqueue context)
  else retry
    Executor->>Span: clear parent, add link to enqueue span
  end
  Span-->>Producer: exported spans joined/linked to producer trace
Loading

Possibly related PRs

  • hardbyte/awa#259: Both PRs modify the shared prepare_raw_job_insert API in awa-model/src/insert.rs.
  • hardbyte/awa#395: Both PRs touch OpenTelemetry/tracing dependency versions in the manifests.

Poem

A rabbit hops through spans of light,
Tracing threads from left to right,
traceparent tucked in metadata's nest,
Enqueue to worker, joined and blessed. 🐇
Retries link back, never lost,
All this tracing, minimal cost! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: end-to-end trace propagation across the queue.
Linked Issues check ✅ Passed The PR covers the requested enqueue-to-execute propagation, Python/Rust traceparent access, tests, and docs for #110's tracing scope.
Out of Scope Changes check ✅ Passed The added docs, tests, dependencies, and tracing code all support the tracing feature; no unrelated changes are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

ℹ️ 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 thread awa-worker/src/context.rs Outdated
Comment on lines +102 to +106
/// W3C traceparent captured at the enqueue site, if any. The execution
/// span already joins that trace automatically; use this to hand the
/// context onward (outgoing HTTP headers, child processes).
pub fn traceparent(&self) -> Option<&str> {
awa_model::trace::traceparent_from_metadata(&self.job.metadata)

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 Forward the execution span instead

When a handler follows the new ctx.traceparent() guidance for an outgoing HTTP call, this returns the stored enqueue-site traceparent, not the current job.execute span context. Because execute_task has already made job.execute a child of that enqueue span, downstream spans created from this header become siblings of job.execute rather than children, which breaks the trace topology for the exact forwarding scenario documented here; build the header from the current execution span context or avoid recommending this helper for propagation.

Useful? React with 👍 / 👎.

ctx.traceparent()/job.traceparent return the stored enqueue-site context;
propagating that onward would make downstream spans siblings of
job.execute. Rust handlers forward with trace::current_traceparent()
(handlers run inside the execution span); the Python snippet documents the
enqueue site as the best available parent until binding-layer context
attach lands.

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
awa-model/src/trace.rs (2)

121-191: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a test for the capture_disabled env-var path.

The capture_disabled() function and its interaction with inject_traceparent when AWA_TRACE_CAPTURE=off is set are untested. A test using serial_test or similar (to avoid env-var races in parallel test execution) would verify the kill switch works end-to-end. This is low priority since the logic is straightforward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@awa-model/src/trace.rs` around lines 121 - 191, Add a test covering the
AWA_TRACE_CAPTURE=off path, since `capture_disabled()` and `inject_traceparent`
are not exercised together yet. Create a serial test (using `serial_test` or
equivalent) that sets the env var off, calls `inject_traceparent`, and verifies
no `traceparent` is injected; reference `capture_disabled`,
`inject_traceparent`, and `TRACEPARENT_METADATA_KEY` so the test stays easy to
locate.

93-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider whether remote_context is needed as a public API.

The executor constructs the Context inline from parse_traceparent rather than using this wrapper. If no external consumer currently calls it, consider making it pub(crate) or removing it to reduce the public API surface. If it's intended for future use or external consumers, a doc comment noting the intended use case would help.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@awa-model/src/trace.rs` around lines 93 - 96, The `remote_context` wrapper in
`trace.rs` appears unused by internal code and may not need to be part of the
public API. Either reduce its visibility to `pub(crate)` or remove it if no
external consumers rely on it; if you keep it public, add a doc comment on
`remote_context` explaining its intended use so the API surface is clear.
awa/tests/trace_propagation_test.rs (1)

126-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise ctx.traceparent() in the propagation test.

This validates raw metadata but not the new Rust accessor contract. Send ctx.traceparent() from the handler too, so a wrong reserved key or extraction regression is caught.

Proposed test coverage tweak
-    let (tx, mut rx) = tokio::sync::mpsc::channel::<serde_json::Value>(1);
+    let (tx, mut rx) = tokio::sync::mpsc::channel::<(serde_json::Value, Option<String>)>(1);
@@
             let tx = tx.clone();
             let handler_metadata = ctx.job.metadata.clone();
+            let handler_traceparent = ctx.traceparent().map(str::to_owned);
             async move {
-                let _ = tx.send(handler_metadata).await;
+                let _ = tx.send((handler_metadata, handler_traceparent)).await;
                 Ok(JobResult::Completed)
             }
@@
-    let handler_metadata = tokio::time::timeout(Duration::from_secs(20), rx.recv())
+    let (handler_metadata, handler_traceparent) =
+        tokio::time::timeout(Duration::from_secs(20), rx.recv())
         .await
         .expect("job should execute within 20s")
         .expect("handler should send metadata");
@@
         "handler-visible metadata should carry the captured traceparent"
     );
+    assert_eq!(
+        handler_traceparent.as_deref(),
+        Some(stored.as_str()),
+        "ctx.traceparent() should expose the captured traceparent"
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@awa/tests/trace_propagation_test.rs` around lines 126 - 158, The trace
propagation test currently verifies only raw job metadata, so it can miss
regressions in the new accessor contract. Update the handler registered in
Client::builder(...).register::<TracedJob, _, _>(...) to also read
ctx.traceparent() and send that value through the existing channel alongside
handler_metadata, then extend the assertions to compare the accessor result
against the captured stored traceparent. This ensures the test covers both the
reserved metadata key and the ctx.traceparent() extraction path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/039-trace-propagation.md`:
- Around line 25-28: The enqueue-path scope in the ADR text is too broad because
it implies Python producers are included in ambient trace capture. Update the
wording around prepare_raw_job_insert so it only refers to Rust-backed enqueue
paths (typed, raw, batch, COPY, transactional) or otherwise explicitly qualify
that Python producer-side auto-capture is not included yet.

---

Nitpick comments:
In `@awa-model/src/trace.rs`:
- Around line 121-191: Add a test covering the AWA_TRACE_CAPTURE=off path, since
`capture_disabled()` and `inject_traceparent` are not exercised together yet.
Create a serial test (using `serial_test` or equivalent) that sets the env var
off, calls `inject_traceparent`, and verifies no `traceparent` is injected;
reference `capture_disabled`, `inject_traceparent`, and
`TRACEPARENT_METADATA_KEY` so the test stays easy to locate.
- Around line 93-96: The `remote_context` wrapper in `trace.rs` appears unused
by internal code and may not need to be part of the public API. Either reduce
its visibility to `pub(crate)` or remove it if no external consumers rely on it;
if you keep it public, add a doc comment on `remote_context` explaining its
intended use so the API surface is clear.

In `@awa/tests/trace_propagation_test.rs`:
- Around line 126-158: The trace propagation test currently verifies only raw
job metadata, so it can miss regressions in the new accessor contract. Update
the handler registered in Client::builder(...).register::<TracedJob, _, _>(...)
to also read ctx.traceparent() and send that value through the existing channel
alongside handler_metadata, then extend the assertions to compare the accessor
result against the captured stored traceparent. This ensures the test covers
both the reserved metadata key and the ctx.traceparent() extraction path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc362c12-e943-4f02-b3db-37be800a4397

📥 Commits

Reviewing files that changed from the base of the PR and between 5e7afab and 5c963e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • CHANGELOG.md
  • Cargo.toml
  • awa-model/Cargo.toml
  • awa-model/src/insert.rs
  • awa-model/src/lib.rs
  • awa-model/src/trace.rs
  • awa-python/python/awa/_awa.pyi
  • awa-python/src/job.rs
  • awa-worker/Cargo.toml
  • awa-worker/src/context.rs
  • awa-worker/src/executor.rs
  • awa/Cargo.toml
  • awa/tests/trace_propagation_test.rs
  • docs/adr/039-trace-propagation.md
  • docs/adr/README.md
  • docs/configuration.md
  • docs/getting-started-python.md
  • docs/getting-started-rust.md
  • docs/stability.md

Comment thread docs/adr/039-trace-propagation.md
@hardbyte hardbyte merged commit e3d1d8e into main Jul 8, 2026
18 checks passed
@hardbyte hardbyte deleted the brian/110-trace-propagation branch July 8, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run the full CI matrix (Python build+test, E2E) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracing

1 participant