This sample shows how to use pipeline behaviors to capture specific values from message body properties and message headers as tags on OpenTelemetry spans.
Running the project
The code consists of a single endpoint that sends messages to itself. Press S to place an order. As the message is processed, the OrderId, CustomerId, and Priority values appear as tags on the process span in the console output.
Why use behaviors
NServiceBus does not capture message body content as span tags by default. Message payloads can contain sensitive or encrypted data, and adding entire payloads as tags is costly. Pipeline behaviors let the endpoint author explicitly choose which values to expose, at the appropriate pipeline stage.
Always check Activity. for null before adding tags, or use the null-conditional operator (?.). When no trace listeners are registered, Activity. is null.
Code walkthrough
Global configuration
OpenTelemetry is configured to export traces to the console.
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(serviceName: "MyEndpoint", serviceInstanceId: Environment.MachineName);
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource("NServiceBus.*")
.AddConsoleExporter()
.Build();
Registering behaviors
Both behaviors are registered when the endpoint is configured:
endpointConfiguration.Pipeline.Register(
new CaptureOrderTagsBehavior(),
"Captures PlaceOrder body properties as OpenTelemetry tags");
endpointConfiguration.Pipeline.Register(
new CaptureHeaderTagsBehavior(),
"Captures the order priority header as an OpenTelemetry tag");
Capturing message body properties
Message body properties are only available after deserialization. The behavior runs at the IIncomingLogicalMessageContext stage, where the deserialized message instance is accessible via context..
class CaptureOrderTagsBehavior : Behavior<IIncomingLogicalMessageContext>
{
public override Task Invoke(IIncomingLogicalMessageContext context, Func<Task> next)
{
if (Activity.Current is { IsAllDataRequested: true } activity
&& context.Message.Instance is PlaceOrder order)
{
activity.SetTag("sample.order.id", order.OrderId);
activity.SetTag("sample.order.customer-id", order.CustomerId);
}
return next();
}
}
The behavior casts the message instance to the expected type. If the cast succeeds, specific properties are added as tags on the current span. The tags appear on the process span in the trace.
Capturing header values
Headers are available before deserialization at the IIncomingPhysicalMessageContext stage. The behavior reads a specific header by name.
class CaptureHeaderTagsBehavior : Behavior<IIncomingPhysicalMessageContext>
{
public override Task Invoke(IIncomingPhysicalMessageContext context, Func<Task> next)
{
if (Activity.Current is { IsAllDataRequested: true } activity
&& context.MessageHeaders.TryGetValue("sample.order.priority", out var priority))
{
activity.SetTag("sample.order.priority", priority);
}
return next();
}
}
The sender sets the header on the outgoing message:
options.SetHeader("sample.order.priority", "high");