Customizing OpenTelemetry tracing

Component: NServiceBus
NuGet Package: NServiceBus (8-pre)
This page targets a pre-release version. Pre-releases are subject to change and samples are not guaranteed to be fully functional.

This sample shows how to extend the OpenTelemetry activities in different ways.

Running the project

The code consists of a single endpoint project that sends messages to itself.

Press O to send a CreateOrder message with a randomized OrderId. When the message is handled, two more messages are created: BillOrder and ShipOrder.

As the messages are sent and processed, trace data is exported to the console. Some of the trace data originates from NServiceBus and some from custom code in the sample.

Code walk through

Global configuration

NServiceBus OpenTelemetry instrumentation is not enabled by default. It must be enabled on the endpoint configuration.

var endpointConfiguration = new EndpointConfiguration("CustomTelemetry");
endpointConfiguration.EnableOpenTelemetry();

OpenTelemetry is configured to export all traces to the command line. It includes the NServiceBus.Core source which is built into NServiceBus and a custom activity source defined in the sample (see below).

var resourceBuilder = ResourceBuilder.CreateDefault()
    .AddService("CustomTelemetry");

var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
    .SetResourceBuilder(resourceBuilder)
    .AddSource("NServiceBus.Core")
    .AddSource(CustomActivitySources.Name)
    .AddProcessor(new NetHostProcessor())
    .AddConsoleExporter();

A custom processor is registered which adds the machine name as a tag to every activity created by this trace listener.

class NetHostProcessor : BaseProcessor<Activity>
{
    string hostName;

    public NetHostProcessor()
    {
        hostName = Dns.GetHostName();
    }

    public override void OnStart(Activity data)
    {
        data.SetTag("net.host.name", hostName);
    }
}

Custom activities

The sample includes a custom activity source.

static class CustomActivitySources
{
    public const string Name = "Sample.ActivitySource";
    public static ActivitySource Main = new ActivitySource(Name);
}

The handler for CreateOrder includes a custom activity that wraps around the billing section.

public async Task Handle(CreateOrder message, IMessageHandlerContext context)
{
    using(var activity = CustomActivitySources.Main.StartActivity("Billing Order"))
    {
        Console.WriteLine($"Billing order {message.OrderId}");
        activity?.AddTag("sample.billing.system", "paypal");
        // Calculate order cost
        await context.SendLocal(new BillOrder { OrderId = message.OrderId });
    }

    Console.WriteLine($"Shipping order {message.OrderId}");
    await context.SendLocal(new ShipOrder { OrderId = message.OrderId });
}

This will automatically be created as a child activity of the invoke handler activity created by NServiceBus. The NServiceBus send message activity will treat this custom activity as its parent.

Send CreateOrder
  Process CreateOrder
    Invoke CreateOrderHandler
      Billing Order <-- Custom activity
        Send BillOrder
      Send ShipOrder

Adding tags

The handler for ShipOrder adds tags to the ambient behavior.

class ShipOrderHandler : IHandleMessages<ShipOrder>
{
    public Task Handle(ShipOrder message, IMessageHandlerContext context)
    {
        Console.WriteLine($"Order shipped {message.OrderId}");
        // Figure out what state we are shipping to
        Activity.Current?.AddTag("sample.shipping.state", "STATE");
        return Task.CompletedTask;
    }
}

In the sample, these tags will be added to the NServiceBus invoke handler activity.

Send ShipOrder
  Process ShipOrder
    Invoke ShipOrderHandler <-- Custom tag gets added here

A behavior in the outgoing pipeline adds the size of the message as a tag for all outgoing message activities.

class TraceOutgoingMessageSizeBehavior : Behavior<IOutgoingPhysicalMessageContext>
{
    public override Task Invoke(
        IOutgoingPhysicalMessageContext context,
        Func<Task> next
    )
    {
        Activity.Current?.AddTag("sample.messaging.body.size", context.Body.Length);
        return next();
    }
}
Activity.Current may be null if there are no configured trace listeners. Always check if the value is null before calling methods on an Activity instance, or use the null-conditional operator (?.).

Related Articles

  • OpenTelemetry
    Observability of NServiceBus endpoints with OpenTelemetry.

Last modified