Skip to content

v7: Carry trace context in message headers across processes - #116

Merged
christopherread merged 2 commits into
v7from
v7-trace-propagation
Oct 25, 2026
Merged

christopherread merged 2 commits into
v7from
v7-trace-propagation

Conversation

@christopherread

Copy link
Copy Markdown
Owner

Sixth v7 increment, completing the tracing half of #90 (follows #115). Targets v7.

What changes

  • Traces now connect across processes. A handler's process span becomes a child of the sender's send, publish or request span, in the same trace. Request → handler → reply stay in one trace too.
  • New public ObvsMessageHeaders: Get(message) and Set(message, headers) attach headers to a message instance without changing its type or any transport interface. Apps can use them for their own headers, for example a tenant ID; trace context is added alongside and never replaces them. It also defines the W3C header names, ObvsMessageHeaders.TraceParent and TraceState.
  • Sending adds the producer span's W3C trace context to the outgoing message's headers, using .NET's standard DistributedContextPropagator. If nobody listens to Obvs's own spans, it propagates the ambient span instead, such as an incoming web request's.
  • RabbitMQ sends a message's headers as AMQP headers and sets them on each received message. String headers come back as UTF-8 bytes and are decoded.
  • In-memory transport: it passes the sender's object to the receiver, so headers already travel with it and it needed no change.

Design notes

  • Headers are held per message instance in a weak table, so they're freed with the message, and two equal records don't share headers. The trade-off: one instance sent twice concurrently shares one set of headers, so the later send's trace context wins.
  • The receiving span is a child of the sender's span, not a link. OpenTelemetry's conventions allow both; a child gives one connected trace per message flow.
  • Kafka, NATS, ActiveMQ and NetMQ get header support as part of their own Use…() increments.

Tests (written first)

  • Core (in-memory), 7 tests:
    • the header store: read back, empty when unset, separate for equal records;
    • the exact W3C traceparent of the send span lands in the message's headers;
    • app headers are kept;
    • a handler in another host continues the sender's trace;
    • request, handler and reply stay in one trace.
  • RabbitMQ (Explicit, real broker), 2 tests: a trace continues across two hosts connected only through RabbitMQ, and an app-set header survives the trip. The receiver works on a freshly deserialized object, so these pass only if the context really crossed the wire.
  • Red first: all 9 failed before the implementation. The trace tests failed because the receiving span had no parent or was in another trace; the rest with NotImplementedException.
  • Results: all 9 RabbitMQ broker tests passed 3 runs in a row against a local RabbitMQ 4.4. A clean build of the solution has 0 warnings, and all 205 unit tests pass.

🤖 Generated with Claude Code

…sses

ObvsMessageHeaders.Get/Set attach headers to a message instance without
changing its type or any transport interface; they're held in a weak
table, so they go when the message does. Sending adds the producer span's
W3C trace context (traceparent, tracestate) to the outgoing message's
headers, keeping any the app set, or the ambient span's when nobody
listens to Obvs's spans. A handler's process span becomes a child of the
span in the message's traceparent, so the receiving service continues the
sender's trace.

RabbitMQ sends a message's headers as AMQP headers and sets them on each
message it receives. The in-memory transport passes the sender's object to
the receiver, so headers already travel with it. Kafka, NATS, ActiveMQ and
NetMQ will get header support with their Use<Transport>() increments.

Part of #90.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@christopherread christopherread added this to the v7 milestone Oct 25, 2026
@christopherread christopherread self-assigned this Oct 25, 2026
Microsoft's download origin returns 409 for the new SDK 10.0.403 and
runtime 10.0.14, so setup-dotnet fails to install 10.0.x. Go back to
10.0.x once that release downloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@christopherread
christopherread merged commit 664607b into v7 Oct 25, 2026
1 check passed
@christopherread
christopherread deleted the v7-trace-propagation branch October 25, 2026 19:39
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