Skip to content

feat(telemetry): propagate caller trace context to aggregate load spans - #90

Merged
yordis merged 1 commit into
mainfrom
yordis/feat-aggregate-load-context-propagation
Apr 20, 2026
Merged

yordis merged 1 commit into
mainfrom
yordis/feat-aggregate-load-context-propagation

Conversation

@yordis

@yordis yordis commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Aggregate load and populate spans were orphaned root spans because they fire during handle_continue in a GenServer process that has no OTel context from the caller
  • Thread the dispatch command's metadata (containing traceparent/tracestate) through Supervisor.open_aggregate → Aggregate.start_link → init → handle_continue → AggregateStateBuilder.populate → telemetry events
  • The AggregatePopulate OTel handler now extracts and attaches the propagated trace context before creating the load span, making it a child of the dispatch trace

@cursor

cursor Bot commented Apr 20, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Threads dispatch metadata through aggregate startup and load telemetry, which touches aggregate process initialization and telemetry metadata; mistakes could affect tracing correctness or aggregate startup behavior, but core command execution logic is largely unchanged.

Overview
Ensures aggregate load/populate OpenTelemetry spans are no longer orphaned by propagating the dispatch caller’s W3C trace context (traceparent/tracestate) through Supervisor.open_aggregate → Aggregate.start_link/init → AggregateStateBuilder.populate and into [:commanded, :aggregate, :load] telemetry metadata.

Commanded.OpenTelemetry.AggregatePopulate now extracts/attaches the propagated context before starting the load span, and tests were expanded to assert correct parent/child relationships plus edge cases (missing/invalid/nil metadata). Documentation was updated to record the new fork change (PR #90), and the OTel test harness was adjusted to set the batch processor exporter after startup.

Reviewed by Cursor Bugbot for commit aeb38e0. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@yordis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35b68bf5-ba2f-47cb-b9b9-a036eef18de1

📥 Commits

Reviewing files that changed from the base of the PR and between f734fad and aeb38e0.

📒 Files selected for processing (9)
  • guides/explanations/fork-differences.md
  • lib/commanded/aggregates/aggregate.ex
  • lib/commanded/aggregates/aggregate_state_builder.ex
  • lib/commanded/aggregates/supervisor.ex
  • lib/commanded/commands/dispatcher.ex
  • lib/commanded/opentelemetry/aggregate_populate.ex
  • test/opentelemetry/aggregate_populate_test.exs
  • test/support/factory.ex
  • test/support/opentelemetry_case.ex

Walkthrough

The pull request threads metadata through aggregate startup and dispatcher execution, enabling W3C trace context (traceparent/tracestate) propagation from dispatch to aggregate load/populate spans, allowing those spans to be linked as children of the dispatch trace rather than appearing as orphaned roots.

Changes

Cohort / File(s) Summary
Documentation
guides/explanations/fork-differences.md
Updated OpenTelemetry Integration section to document aggregate load trace context propagation behavior and PR #90 reference.
Aggregate Lifecycle & Metadata Threading
lib/commanded/aggregates/aggregate.ex, lib/commanded/aggregates/aggregate_state_builder.ex, lib/commanded/aggregates/supervisor.ex
Extended aggregate initialization to accept and thread metadata parameter through startup process: open_aggregate/4, init with continue tuple containing metadata, and populate/2 forwarding metadata to telemetry metadata generation.
Dispatcher Integration
lib/commanded/commands/dispatcher.ex
Modified dispatcher to pass context.metadata when opening aggregates, enabling trace context availability downstream.
OpenTelemetry Context Extraction
lib/commanded/opentelemetry/aggregate_populate.ex
Updated load span handler to extract propagated W3C trace context from metadata via Helpers.extract_propagated_ctx/1 and attach context before span creation.
Tests & Support
test/opentelemetry/aggregate_populate_test.exs, test/support/factory.ex
Added comprehensive trace context propagation tests verifying span parent relationships with valid/missing traceparent headers, and extended factory to include optional metadata field.

Sequence Diagram

sequenceDiagram
    participant Dispatcher
    participant Supervisor
    participant AggregateProcess as Aggregate Process
    participant StateBuilder as StateBuilder
    participant OTelHandler as OTel Handler
    participant OTelContext as OTel Context

    Dispatcher->>Supervisor: open_aggregate(app, module, uuid, metadata)
    Note over Dispatcher: metadata contains traceparent/tracestate
    
    Supervisor->>AggregateProcess: start_link with {state, metadata}
    
    AggregateProcess->>AggregateProcess: init({state, metadata})
    AggregateProcess->>AggregateProcess: schedule continue with metadata
    
    AggregateProcess->>StateBuilder: populate(state, metadata)
    
    StateBuilder->>StateBuilder: rebuild_from_events(events, metadata)
    
    StateBuilder->>OTelHandler: telemetry event [:commanded, :aggregate, :load, :start]<br/>(includes metadata)
    
    OTelHandler->>OTelHandler: extract_propagated_ctx(metadata)
    
    alt Traceparent Present
        OTelHandler->>OTelContext: attach(extracted_ctx)
        Note over OTelContext: Span becomes child of dispatch trace
    else Traceparent Missing
        Note over OTelHandler: Context undefined, span is orphaned root
    end
    
    OTelHandler->>OTelHandler: create_span (with propagated parent)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • PR #38: Implements middleware that injects W3C trace context (traceparent/tracestate) into pipeline metadata, providing the source context that this PR propagates downstream to aggregate spans.
  • PR #47: Introduces the original AggregatePopulate OpenTelemetry instrumentation that this PR extends with metadata and trace context propagation capabilities.
  • PR #58: Establishes the foundational load/populate telemetry events in AggregateStateBuilder that this PR now enriches with metadata and context extraction.

Poem

🐰 A rabbit hopped through traces bright,
Threading metadata left and right!
Spans now dance in parent-child delight,
No orphaned roots—just context right.
Traceparent hops through the OpenTelemetry night! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the primary change: propagating caller trace context to aggregate load spans via telemetry.
Description check ✅ Passed The description clearly explains the problem, solution approach, and technical details of how metadata is threaded through the system to fix orphaned spans.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/feat-aggregate-load-context-propagation

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 and usage tips.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/commanded/aggregates/aggregate_state_builder.ex (1)

117-188: ⚠️ Potential issue | 🟡 Minor

Propagate metadata consistently to load stop and populate telemetry.

metadata is included only in the load :start event. The load :stop and populate :start/:stop events still call helpers with the default %{}, so telemetry handlers on those events cannot see the propagated trace metadata.

🔧 Proposed propagation fix
-          Telemetry.stop(
+          Telemetry.stop(
             load_prefix,
             load_start,
-            load_stop_metadata(state, snapshot_used, snapshot_source_version),
+            load_stop_metadata(state, snapshot_used, snapshot_source_version, metadata),
             %{count: 0}
           )
@@
-          {state, count} = rebuild_from_event_stream(event_stream, state)
+          {state, count} = rebuild_from_event_stream(event_stream, state, metadata)
@@
-          Telemetry.stop(
+          Telemetry.stop(
             load_prefix,
             load_start,
-            load_stop_metadata(state, snapshot_used, snapshot_source_version),
+            load_stop_metadata(state, snapshot_used, snapshot_source_version, metadata),
             %{count: count}
           )
@@
-  defp rebuild_from_event_stream(event_stream, %Aggregate{} = state) do
+  defp rebuild_from_event_stream(event_stream, %Aggregate{} = state, metadata \\ %{}) do
     telemetry_prefix = [:commanded, :aggregate, :populate]
-    start_time = Telemetry.start(telemetry_prefix, telemetry_metadata(state))
+    start_time = Telemetry.start(telemetry_prefix, telemetry_metadata(state, metadata))
@@
-    Telemetry.stop(telemetry_prefix, start_time, telemetry_metadata(state), %{count: count})
+    Telemetry.stop(telemetry_prefix, start_time, telemetry_metadata(state, metadata), %{count: count})
@@
-  defp load_stop_metadata(aggregate, snapshot_used, snapshot_source_version) do
-    telemetry_metadata(aggregate)
+  defp load_stop_metadata(aggregate, snapshot_used, snapshot_source_version, metadata \\ %{}) do
+    telemetry_metadata(aggregate, metadata)
     |> Map.merge(%{
       snapshot_used: snapshot_used,
       snapshot_source_version: snapshot_source_version

Also applies to: 195-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/commanded/aggregates/aggregate_state_builder.ex` around lines 117 - 188,
The load :stop and populate :start/:stop telemetry calls don't receive the
incoming trace metadata; update functions to propagate the metadata argument
through: when calling Telemetry.start and Telemetry.stop in the loader, pass
telemetry_metadata(state, metadata) instead of telemetry_metadata(state); change
rebuild_from_event_stream to accept a metadata parameter
(rebuild_from_event_stream(event_stream, state, metadata)) and use
Telemetry.start(telemetry_prefix, telemetry_metadata(state, metadata)) and
Telemetry.stop(..., telemetry_metadata(state, metadata), ...); update its call
site(s) where rebuild_from_event_stream is invoked to forward the metadata; and
change load_stop_metadata/3 to call telemetry_metadata(aggregate, metadata) (and
update its callers) so all load/stop and populate events include the original
metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/commanded/opentelemetry/aggregate_populate.ex`:
- Around line 33-36: The code currently calls :otel_ctx.attach(ctx) when
Helpers.extract_propagated_ctx(meta[:metadata]) returns {_links, ctx} but never
clears or restores context; change the match to call
:otel_ctx.attach(:undefined) in the {_links, :undefined} branch to explicitly
clear context and, when attaching ctx, capture the return value (prev_ctx =
:otel_ctx.attach(ctx)) and after the load span ends call
:otel_ctx.attach(prev_ctx) to restore the previous context so aggregate
processes do not retain the caller’s OTel context; reference
Helpers.extract_propagated_ctx, :otel_ctx.attach and the load span surrounding
this logic when making the change.

---

Outside diff comments:
In `@lib/commanded/aggregates/aggregate_state_builder.ex`:
- Around line 117-188: The load :stop and populate :start/:stop telemetry calls
don't receive the incoming trace metadata; update functions to propagate the
metadata argument through: when calling Telemetry.start and Telemetry.stop in
the loader, pass telemetry_metadata(state, metadata) instead of
telemetry_metadata(state); change rebuild_from_event_stream to accept a metadata
parameter (rebuild_from_event_stream(event_stream, state, metadata)) and use
Telemetry.start(telemetry_prefix, telemetry_metadata(state, metadata)) and
Telemetry.stop(..., telemetry_metadata(state, metadata), ...); update its call
site(s) where rebuild_from_event_stream is invoked to forward the metadata; and
change load_stop_metadata/3 to call telemetry_metadata(aggregate, metadata) (and
update its callers) so all load/stop and populate events include the original
metadata.
🪄 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: 5afddf85-df7e-41dd-a3bf-7d9d1d6f3390

📥 Commits

Reviewing files that changed from the base of the PR and between 6aa37fd and f734fad.

📒 Files selected for processing (8)
  • guides/explanations/fork-differences.md
  • lib/commanded/aggregates/aggregate.ex
  • lib/commanded/aggregates/aggregate_state_builder.ex
  • lib/commanded/aggregates/supervisor.ex
  • lib/commanded/commands/dispatcher.ex
  • lib/commanded/opentelemetry/aggregate_populate.ex
  • test/opentelemetry/aggregate_populate_test.exs
  • test/support/factory.ex

Comment thread lib/commanded/opentelemetry/aggregate_populate.ex
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis
yordis force-pushed the yordis/feat-aggregate-load-context-propagation branch from b540b85 to aeb38e0 Compare April 20, 2026 20:28
@yordis
yordis merged commit 9a6ee07 into main Apr 20, 2026
3 checks passed
@yordis
yordis deleted the yordis/feat-aggregate-load-context-propagation branch April 20, 2026 20:43
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.

1 participant