Skip to main content

OpenTelemetry Instrumentation

Hermodr provides built-in distributed tracing support through the Hermodr.Publisher.OpenTelemetry package. It instruments the publish pipeline with W3C trace context propagation, enabling end-to-end correlation across service boundaries via CloudEvents extension attributes.

The problem

Events cross process boundaries but carry no trace context by default, making it impossible to correlate a published event with the originating request in a distributed trace. Without trace propagation, a span in the producer service is disconnected from any span in the consumer service.

What it provides

CapabilityHow it works
Producer spanCreates an Activity (kind Producer) for every PublishEventAsync call
Trace injectionWrites W3C traceparent and tracestate as CloudEvents extension attributes on the event
Consumer spanExtracts traceparent/tracestate from incoming events and creates an Activity (kind Consumer)
Handler participationSubscription handlers automatically see Activity.Current set to the consumer span
Custom enrichmentOptional EnrichWithEvent callback to add custom span tags

Installation

dotnet add package Hermodr.Publisher.OpenTelemetry

The package depends on OpenTelemetry.Api only — it does not require the full SDK. Configure your OpenTelemetry SDK separately to listen to the Hermodr activity source.

Quick start

Enable instrumentation with a single builder call:

builder.Services
.AddEventPublisher(opts => opts.Source = new Uri("https://myapp.example.com"))
.AddOpenTelemetry()
.AddRabbitMq(opts => { opts.ConnectionString = "amqp://..."; opts.ExchangeName = "events"; })
.AddSubscriptions(subs =>
{
subs.Subscribe("com.example.order.*", HandleOrderAsync);
});

That's it. Every published event carries traceparent and tracestate extensions, and every subscription handler runs inside a consumer span linked to the producer span.

How trace propagation works

Publish side (inject)

Incoming request span ──► OpenTelemetryPublishMiddleware

├─ StartActivity("publish com.example.order.created", Producer)
├─ Inject traceparent + tracestate into CloudEvent extensions
├─ await next(context) ──► channel.PublishAsync(...)
└─ SetStatus(Ok) / SetStatus(Error)

The middleware creates a producer span, injects the W3C trace context into the CloudEvent, and propagates any exceptions to the span status.

Subscription side (extract)

Received CloudEvent ──► OpenTelemetrySubscriptionMiddleware

├─ Extract traceparent + tracestate from CloudEvent extensions
├─ StartActivity("handle com.example.order.created", Consumer, parentContext)
├─ await next(context) ──► EventDispatcher ──► subscription handlers
└─ SetStatus(Ok) / SetStatus(Error)

The middleware extracts the remote parent context from the CloudEvent extensions and creates a consumer span. Subscription handlers see Activity.Current set to this span, so they can create child spans or attach baggage naturally.

End-to-end trace

Service A (producer) Service B (consumer)
────────────────── ──────────────────
[HTTP request span]

└─► [publish com.example.order.created] ──► transport (RabbitMQ, ASB, ...)
traceparent: 00-{traceId}-{spanA}-01 │

[handle com.example.order.created]
traceparent matches → same traceId
parentSpanId = spanA

Configuration

Full instrumentation (default)

builder.Services
.AddEventPublisher()
.AddOpenTelemetry(opts =>
{
opts.ActivitySourceName = "MyService.Hermodr";
opts.RecordException = true;
opts.EnrichWithEvent = (activity, @event) =>
{
activity.SetTag("tenant.id", GetTenantId(@event));
};
});

Publisher-only instrumentation

Use when you only produce events and don't have in-process subscriptions:

builder.Services
.AddEventPublisher()
.AddOpenTelemetryPublisherInstrumentation();

Subscription-only instrumentation

Use when you only consume events (e.g., a worker service that receives events from a broker):

builder.Services
.AddEventPublisher()
.AddOpenTelemetrySubscriptionInstrumentation();

Options reference

OptionTypeDefaultDescription
ActivitySourceNamestring"Hermodr"Name of the ActivitySource emitted by the middleware
InstrumentPublisherbooltrueWhether to register the publish-side middleware
InstrumentSubscriptionbooltrueWhether to register the subscription-side middleware
RecordExceptionbooltrueWhether to attach exception details to spans on error
EnrichWithEventAction<Activity, CloudEvent>?nullCallback invoked after span creation to add custom tags

CloudEvents extension attributes

The middleware uses standard W3C extension attribute names on the CloudEvent:

ExtensionTypeDescription
traceparentstringW3C Trace Context traceparent value (e.g., 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01)
tracestatestringW3C Trace Context tracestate value (only set when present on the activity)

These are standard CloudEvents extension attributes — any CloudEvents-compatible consumer can read them, even if it doesn't use Hermodr.

Span tags

Every span created by the instrumentation includes these tags:

TagValueDescription
event.typeThe CloudEvent typeIdentifies the event being published or handled
event.idThe CloudEvent ID (when set)Unique event identifier
messaging.system"hermodr"Identifies the messaging framework
messaging.operation"publish" or "receive"Distinguishes producer from consumer spans

Custom tags can be added via the EnrichWithEvent callback.

Accessing the activity in subscription handlers

Subscription handlers can access the current consumer span through Activity.Current:

subs.Subscribe("com.example.order.*", async (evt, ct) =>
{
// Activity.Current is the consumer span created by OpenTelemetrySubscriptionMiddleware
var currentActivity = Activity.Current;

// Create a child span for your handler logic
using var handlerSpan = new ActivitySource("MyService")
.StartActivity("process-order", ActivityKind.Internal);

// ... handler logic ...
});

The middleware also stores the consumer Activity reference in EventContext.Items under the key "Hermodr.Activity" for middleware that runs between the subscription middleware and the dispatcher.

OpenTelemetry SDK configuration

The package only emits activities — you need to configure the OpenTelemetry SDK to collect and export them:

builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("Hermodr") // or your custom ActivitySourceName
.AddAspNetCoreInstrumentation()
.AddOtlpExporter());

Middleware ordering

The OpenTelemetry middleware follows the standard registration order:

caller → OpenTelemetryPublishMiddleware
→ OpenTelemetrySubscriptionMiddleware
→ EventDispatcher (if AddSubscriptions was called)
→ [your custom middleware]
→ [terminal: enrich, validate, dispatch to channels]

Place custom middleware that depends on trace context after AddOpenTelemetry() so it runs inside the producer/consumer span:

builder.Services
.AddEventPublisher()
.AddOpenTelemetry()
.Use<AuditMiddleware>() // runs inside the producer span
.AddChannel<RabbitMqPublishChannel>();