# Customizing OpenTelemetry tracing 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 walkthrough ### Global configuration OpenTelemetry is configured to export all traces to the command line. It includes the `NServiceBus.*` sources built into NServiceBus and a custom activity source defined in the sample (see below). ```cs var resourceBuilder = ResourceBuilder.CreateDefault() .AddService(serviceName: "MyEndpoint", serviceInstanceId: Environment.MachineName); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) .AddSource("NServiceBus.*") .AddSource(CustomActivitySources.Name) .AddConsoleExporter() .Build(); ``` ### Custom activities The sample includes a custom activity source. ```cs static class CustomActivitySources { public const string Name = "Sample.ActivitySource"; public static readonly ActivitySource Main = new(Name); } ``` The handler for `CreateOrder` includes a custom activity that wraps around the billing section. ```cs public async Task Handle(CreateOrder message, IMessageHandlerContext context) { using var activity = CustomActivitySources.Main.StartActivity("Billing Order"); if (message.SimulateFailure) { throw new MyBusinessException{ ReasonCode = 1}; } 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 > [!NOTE] > `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 (`?.`). The handler for `ShipOrder` adds tags to the ambient behavior. ```cs class ShipOrderHandler : IHandleMessages { 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. ```cs class TraceOutgoingMessageSizeBehavior : Behavior { public override Task Invoke(IOutgoingPhysicalMessageContext context, Func next) { Activity.Current?.AddTag("sample.messaging.body.size", context.Body.Length); return next(); } } ``` A behavior in the handler pipeline captures a custom business exception and adds its "reason code" as a tag on the handler span created by NServiceBus. ```cs class TraceCustomExceptionInHandlerBehavior : Behavior { public override async Task Invoke(IInvokeHandlerContext context, Func next) { try { await next(); } catch (MyBusinessException e) { Activity.Current?.AddTag("sample.business-exception.reason", e.ReasonCode); throw; } } } public class MyBusinessException : Exception { public int ReasonCode { get; init; } } ```