Capturing message properties as OpenTelemetry span tags

Component:
NServiceBus
NuGet Package:
NServiceBus 10.x

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.

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.Message.Instance.

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");

Related Articles

  • OpenTelemetry
    Observability of NServiceBus endpoints with OpenTelemetry.