Skip to content

feat: Add OpenTelemetry instrumentation for event handlers - #41

Merged
yordis merged 1 commit into
mainfrom
feat-add-event-handler-otel
Jan 16, 2026
Merged

yordis merged 1 commit into
mainfrom
feat-add-event-handler-otel

Conversation

@yordis

@yordis yordis commented Jan 1, 2026

Copy link
Copy Markdown
Member

Signed-off-by: Yordis Prieto yordis.prieto@gmail.com

@cursor

cursor Bot commented Jan 1, 2026 •

Copy link
Copy Markdown

PR Summary

Introduces first‑class OpenTelemetry tracing for Commanded event processing.

  • New Commanded.OpenTelemetry setup API with NimbleOptions config; creates spans for [:commanded, :event, :handle] and [:commanded, :event, :batch] with OTel SemConv + commanded.* attributes
  • Configurable span relationships for event handlers (:link default, :child, :none); clears stale context to avoid unintended parenting; error/exception details recorded
  • Commanded.Middleware.TraceContextPropagator documents injecting W3C traceparent/tracestate into command metadata for correlation
  • Adds required deps: opentelemetry_api, opentelemetry_telemetry, opentelemetry_semantic_conventions, and nimble_options; updates docs grouping and Dialyzer config
  • New docs: fork differences entry and “Setting up OpenTelemetry tracing” how‑to
  • Extensive tests for spans, links/parenting modes, batch behavior, and edge cases

Written by Cursor Bugbot for commit a3418c6. This will update automatically on new commits. Configure here.

@coderabbitai

coderabbitai Bot commented Jan 1, 2026 •

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR introduces comprehensive OpenTelemetry tracing integration to Commanded, adding span creation and context propagation for event handlers and batch processing. It includes new core modules, middleware updates, configuration changes, documentation guides, and extensive test infrastructure supporting the tracing feature.

Changes

Cohort / File(s) Summary
OpenTelemetry Core Integration
lib/commanded/opentelemetry.ex, lib/commanded/opentelemetry/event_handler.ex, lib/commanded/opentelemetry/commanded_attributes.ex
New modules implementing OpenTelemetry tracing: main setup entry point with NimbleOptions validation, event handler telemetry hooks for single and batch event processing with span lifecycle management, and centralized attribute constants following commanded.* naming convention. EventHandler module contains complex span context logic, error handling, and metadata extraction (307 lines).
Middleware & Trace Context
lib/commanded/middleware/trace_context_propagator.ex
Added docstrings and @impl annotations to document W3C trace context header injection into command pipeline metadata before dispatch.
Dependencies & Configuration
mix.exs
Added NimbleOptions and three required OpenTelemetry dependencies (opentelemetry_api, opentelemetry_telemetry, opentelemetry_semantic_conventions). Changed opentelemetry_api from optional to required. Updated dialyzer configuration and module grouping to expose new OpenTelemetry modules.
Documentation
guides/explanations/built-in-vs-external-projections.md
Removed two migration-related documentation links (no functional changes).
OpenTelemetry Setup Guide
guides/explanations/fork-differences.md, guides/howtos/setting-up-opentelemetry-tracing.md
Added new documentation section describing OpenTelemetry integration, span relationships (:link, :child, :none), middleware configuration, and usage examples. New detailed how-to guide covering setup, trace context propagation, and configuration options.
Test Infrastructure
test/opentelemetry/event_handler_test.exs
Comprehensive test suite (935 lines) validating setup behavior, attribute presence, span relationships, error handling, batch processing, and edge cases. Tests span lifecycle, context management, and trace propagation across single/batch events.
Test Support Modules
test/support/opentelemetry_case.ex, test/support/test_domain.ex, test/support/factory.ex
New ExUnit case template configuring OpenTelemetry environment with automatic telemetry cleanup. Test domain structs for account-related commands/events. Factory module with 30+ builder functions for constructing test fixtures, telemetry events, metadata, and domain scenarios.

Sequence Diagram

sequenceDiagram
    participant App as Application Startup
    participant OT as Commanded.OpenTelemetry
    participant Handler as Event Handler
    participant Telemetry as Telemetry System
    participant Span as OpenTelemetry Span
    participant Backend as Tracing Backend

    App->>OT: setup(span_relationship: :child)
    OT->>Handler: setup(opts)
    Handler->>Telemetry: attach event listeners
    Telemetry-->>Handler: ready

    Handler->>Telemetry: [:commanded, :event, :start]
    Telemetry->>Span: create span
    Span->>Span: add attributes (handler, event, correlation)
    Span->>Backend: export span context
    
    Handler->>Span: process event
    alt Success
        Span->>Span: set status ok
    else Error
        Span->>Span: set status error with message
    end
    
    Handler->>Telemetry: [:commanded, :event, :stop]
    Telemetry->>Span: end span
    Span->>Backend: record span

    Handler->>Telemetry: [:commanded, :event, :batch_start]
    Telemetry->>Span: create batch span
    Span->>Span: add batch attributes
    Handler->>Span: process batch events
    Handler->>Telemetry: [:commanded, :event, :batch_stop]
    Telemetry->>Span: end batch span
    Span->>Backend: record batch span
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰✨ Spans now dance through our events,
Tracing paths the telemetry lends,
From handler hops to batches bright,
Context flows like morning light! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description contains only a 'Signed-off-by' line with minimal information about the changeset. While it lacks substantive detail about what the changes accomplish, it does not actively mislead; it is simply minimal. Consider expanding the description with a brief summary of the OpenTelemetry integration features being added, such as span creation for event handlers, trace context propagation, and configuration options.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add OpenTelemetry instrumentation for event handlers' accurately describes the main change in the PR: introducing comprehensive OpenTelemetry integration for event handlers.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

Comment thread lib/commanded/opentelemetry.ex
@yordis
yordis force-pushed the feat-add-event-handler-otel branch 2 times, most recently from 2f6c832 to af578fb Compare January 1, 2026 20:47
Comment thread lib/commanded/opentelemetry/helper.ex Outdated
@yordis
yordis force-pushed the feat-add-event-handler-otel branch 4 times, most recently from 3ca5650 to 97028f5 Compare January 1, 2026 21:40
Comment thread lib/commanded/opentelemetry/event_handler.ex Outdated
@yordis
yordis force-pushed the feat-add-event-handler-otel branch from 97028f5 to 8126b0e Compare January 1, 2026 21:55
Comment thread lib/commanded/opentelemetry.ex
Comment thread lib/commanded/opentelemetry/event_handler.ex Outdated
@yordis
yordis force-pushed the feat-add-event-handler-otel branch 2 times, most recently from a62a126 to 07798dd Compare January 2, 2026 03:22
Comment thread lib/commanded/opentelemetry/event_handler.ex Outdated
Comment thread test/support/opentelemetry_case.ex
Comment thread lib/commanded/opentelemetry/helper.ex Outdated
Comment thread lib/commanded/opentelemetry/event_handler.ex
@yordis
yordis force-pushed the feat-add-event-handler-otel branch 7 times, most recently from 8105748 to 0d2427f Compare January 10, 2026 09:06
Comment thread lib/commanded/opentelemetry/event_handler.ex
Comment thread lib/commanded/opentelemetry/event_handler.ex
@yordis
yordis force-pushed the feat-add-event-handler-otel branch 3 times, most recently from a6f562c to e411579 Compare January 16, 2026 18:08
@yordis
yordis force-pushed the feat-add-event-handler-otel branch 9 times, most recently from f80a1dd to 0c177d6 Compare January 16, 2026 18:38
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis
yordis force-pushed the feat-add-event-handler-otel branch from 0c177d6 to a3418c6 Compare January 16, 2026 18:56
@yordis
yordis marked this pull request as ready for review January 16, 2026 18:56

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

🤖 Fix all issues with AI agents
In `@test/opentelemetry/event_handler_test.exs`:
- Around line 62-65: Replace the brittle assertion that checks the total number
of handlers (length(handlers) == 1) with an assertion that specifically looks
for your handler id: call :telemetry.list_handlers([:commanded, :event, :handle,
:start]), then filter or find handlers whose id equals the exact id used earlier
in the test (the same pattern used at lines 49-51), e.g. Enum.filter(handlers,
fn h -> handler_id_match?(h, expected_id) end) or Enum.any?(handlers,
&match_expected_id?/1), and assert that the filtered result contains your
handler (length == 1 or assert true for Enum.any?). Ensure you reference the
exact expected handler id constant/name used earlier in the file.
- Around line 921-933: The detach_handlers/0 helper is detaching all telemetry
handlers for the given events; change it to only detach handlers installed by
Commanded.OpenTelemetry.EventHandler by checking each handler.id from
:telemetry.list_handlers(event) before calling :telemetry.detach. Inside
detach_handlers/0 (and the inner loop over for handler <-
:telemetry.list_handlers(event)), only call :telemetry.detach(handler.id) when
handler.id identifies the Commanded.OpenTelemetry.EventHandler (e.g., matches
the module atom or contains Commanded.OpenTelemetry.EventHandler in the id
tuple) and skip detaching any other handlers.
♻️ Duplicate comments (1)
lib/commanded/opentelemetry/event_handler.ex (1)

12-46: Propagate attach_many errors instead of raising.

The current :ok = ... pattern will raise on {:error, :already_exists} instead of returning that tuple. Consider chaining the results so the caller can handle the error cleanly.

✅ Suggested fix
 def setup(opts \\ []) do
   span_relationship = Keyword.get(opts, :span_relationship, :link)
   config = %{span_relationship: span_relationship}

-  :ok = attach_handle_handlers(config)
-  :ok = attach_batch_handlers(config)
-
-  :ok
+  with :ok <- attach_handle_handlers(config),
+       :ok <- attach_batch_handlers(config) do
+    :ok
+  end
 end
🧹 Nitpick comments (1)
test/support/factory.ex (1)

278-318: Allow measurement overrides in build_telemetry_event.

Right now opts only affect metadata; passing them through lets tests override system_time/duration when needed.

♻️ Suggested update
 def build_telemetry_event(:start, opts) do
-    measurements = build_telemetry_start_measurements()
+    measurements = build_telemetry_start_measurements(opts)
     metadata = build_event_handler_metadata(opts)
     event_name = [:commanded, :event, :handle, :start]

     {event_name, measurements, metadata}
 end

 def build_telemetry_event(:stop, opts) do
-    measurements = build_telemetry_stop_measurements()
+    measurements = build_telemetry_stop_measurements(opts)
     metadata = build_event_handler_metadata(opts)
     event_name = [:commanded, :event, :handle, :stop]

     {event_name, measurements, metadata}
 end

 def build_telemetry_event(:exception, opts) do
-    measurements = build_telemetry_exception_measurements()
+    measurements = build_telemetry_exception_measurements(opts)
     metadata = build_exception_metadata(opts)
     event_name = [:commanded, :event, :handle, :exception]

     {event_name, measurements, metadata}
 end

 def build_telemetry_event(:batch_start, opts) do
-    measurements = build_telemetry_start_measurements()
+    measurements = build_telemetry_start_measurements(opts)
     metadata = build_batch_handler_metadata(opts)
     event_name = [:commanded, :event, :batch, :start]

     {event_name, measurements, metadata}
 end

 def build_telemetry_event(:batch_stop, opts) do
-    measurements = build_telemetry_stop_measurements()
+    measurements = build_telemetry_stop_measurements(opts)
     metadata = build_batch_handler_metadata(opts)
     event_name = [:commanded, :event, :batch, :stop]

     {event_name, measurements, metadata}
 end
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f966e94 and a3418c6.

📒 Files selected for processing (13)
  • guides/explanations/built-in-vs-external-projections.md
  • guides/explanations/fork-differences.md
  • guides/howtos/setting-up-opentelemetry-tracing.md
  • lib/commanded/middleware/trace_context_propagator.ex
  • lib/commanded/opentelemetry.ex
  • lib/commanded/opentelemetry/commanded_attributes.ex
  • lib/commanded/opentelemetry/event_handler.ex
  • mix.exs
  • test/middleware/trace_context_propagator_test.exs
  • test/opentelemetry/event_handler_test.exs
  • test/support/factory.ex
  • test/support/opentelemetry_case.ex
  • test/support/test_domain.ex
💤 Files with no reviewable changes (1)
  • guides/explanations/built-in-vs-external-projections.md
🧰 Additional context used
🧬 Code graph analysis (3)
lib/commanded/opentelemetry.ex (1)
lib/commanded/opentelemetry/event_handler.ex (1)
  • setup (12-20)
lib/commanded/opentelemetry/event_handler.ex (1)
lib/commanded/opentelemetry.ex (1)
  • setup (94-101)
test/support/factory.ex (1)
test/opentelemetry/event_handler_test.exs (1)
  • build_recorded_event (880-887)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Quality Assurance (1.19.x, 27)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (23)
lib/commanded/middleware/trace_context_propagator.ex (2)

44-61: Clear pre-dispatch trace context docs.

The updated docstring clarifies behavior and improves readability without changing logic.


69-75: Marking post-dispatch hooks as internal is fine.

The @doc false + @impl annotations keep the public surface tidy.

test/support/test_domain.ex (1)

1-40: Clean, focused test fixtures.

Structs are simple and appropriately scoped for test support.

mix.exs (3)

186-208: Docs grouping for OpenTelemetry modules is consistent.

Module grouping and nesting aligns with the new API surface.


249-261: Dialyzer PLT additions are appropriate.

Including OTel apps in PLT aligns with new dependencies.


70-79: OpenTelemetry and NimbleOptions versions are compatible with the project's Elixir ~> 1.12 requirement.

All selected versions check out:

  • nimble_options 1.1.1 (latest) has no minimum Elixir constraint
  • opentelemetry_api 1.0+ requires Elixir 1.11+, satisfied by 1.12
  • opentelemetry_semantic_conventions 1.27 requires Elixir ~> 1.12, which matches the project exactly
  • OTP 22+ (required by opentelemetry_api) is supported by Elixir 1.12

No compatibility concerns.

lib/commanded/opentelemetry.ex (1)

1-101: API surface and docs are clear.

Setup flow and span relationship semantics are well documented and easy to follow.

guides/howtos/setting-up-opentelemetry-tracing.md (1)

1-62: Docs are clear and actionable.

The guide gives concise setup steps and configuration examples.

test/support/factory.ex (5)

6-114: LGTM — command/event builder helpers look consistent.


116-152: LGTM — RecordedEvent factory is well-structured.


154-188: LGTM — telemetry measurement builders are straightforward.


190-276: LGTM — metadata builders are cohesive.


320-388: LGTM — scenario builders read well.

lib/commanded/opentelemetry/commanded_attributes.ex (1)

1-144: LGTM — clear, centralized attribute constants.

lib/commanded/opentelemetry/event_handler.ex (7)

48-115: LGTM — handle start span setup looks solid.


117-149: LGTM — stop/exception span handling is clear.


151-195: LGTM — batch start path looks good.


197-229: LGTM — batch stop/exception handling looks good.


231-253: LGTM — error helpers are tidy.


254-299: LGTM — context helpers are concise.


301-307: LGTM — small helpers are fine.

guides/explanations/fork-differences.md (1)

220-245: LGTM — OpenTelemetry feature entry is clear.

test/support/opentelemetry_case.ex (1)

1-54: LGTM — test case template setup/cleanup reads cleanly.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread test/opentelemetry/event_handler_test.exs
Comment thread test/opentelemetry/event_handler_test.exs
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