~/codewithstu

OpenTelemetry: Simple Distributed Tracing in .NET Libraries

Transcript

Hi, my name is Stu and in this video I'm going to show you how to add some basic tracing to your .NET libraries. If you're new here, this channel is all about me breaking down different technologies into easily digestible chunks. In this video, I'm going to take you through how, as a library author, you can add some basic distributed tracing logic to your package.

So to go through the application that we're going to be using today, we have a hosted service called App which has a dependency on a service which is defined in our library project. All the background service does is call this service five times, and Service A has a random little delay on it so that we can see stuff in action. When we add distributed tracing logic, this will be captured by our OpenTelemetry integration which we have in the application project. So just to confirm, the App class doesn't really do anything special. It just calls the service five times. The service is injected in through the constructor, and the service just waits between 500 milliseconds and two seconds and then writes something to the console.

The library project has no dependencies other than the target framework .NET 5. The application project only has dependencies on Microsoft.Extensions.Hosting, which is our host builder and runner, then we've got the OpenTelemetry integration for that and the OpenTelemetry console exporter as well. The library project that we've referenced here could either be used as a project reference, or equally you could package this up and distribute it as a NuGet package. The approach works in the exact same way.

As a library author, there are multiple ways that you could add some basic distributed tracing logic to your libraries that you distribute. One of the ways that you could do this is to add the OpenTelemetry project or a similar project inside of your package and create the dependency between them. Personally, I don't like this kind of coupling between packages as you're very subjected to the API changes that projects like OpenTelemetry are currently going through. The approach that I'm going to show you is by using the new ActivitySource, which is available natively in .NET 5 and via a NuGet package in .NET Core App 3.1.

So now let's take a look at creating our first ActivitySource. For brevity, I'm just going to do this all in the same file, but naturally we would split this out in our real-life applications. I'm going to create a new class called MyActivitySource. The first thing I'm going to do is create a static string called Name. This is what our ActivitySource is going to be named, and for simplicity I'm just going to call it nameof MyActivitySource, just so that we have something there that we can reference both here and in the OpenTelemetry portion which I'll show you later on.

Next, we need to create a property called the ActivitySource. I personally like to do this as a singleton type pattern, but you could in theory just do it in any way that you wanted to. To do this, I'm going to go public static ActivitySource, I'm going to call it Instance, and it's going to be a new ActivitySource and we need to pass in the name of the ActivitySource here as well. If you wanted to version your activity sources with the version of your library, you could also do that by adding in a version here such as 1.0.0, but for this demo we don't need it so I'm not going to add it in.

From here, the next bit that we need to do is actually go into our method and start calling the instance of the ActivitySource to start events. So I'm going to go into the DoSomethingAsync method and I'm going to type using var activity = MyActivitySource.Instance.StartActivity. This takes a couple of parameters. The one that we're interested in for now is the name parameter, and this represents the name of the operation that you're performing. So this could be "my API call" for example.

Depending on whether or not anybody is actually listening to the ActivitySource that you created depends on whether you actually get an activity instance back from the StartActivity method. For example, if nobody decided to listen to your ActivitySource, then you would get a null coming back from the StartActivity method. This is extremely important to know, because once you do have an activity returned from this API call, then you can add things like tags (which may be the status code of a web request) through to events that happen inside of your library. These all require an instance of an activity to work.

So to guard against this, let's say we're going to add a new tag to our activity saying that the result was successful. We would say activity?.AddTag, give it a name "success", and we're just going to pass in the value true. Now this would work if the activity came back with something, but if it's going to be a null then this method will blow up, so we just need to guard it with a null coalescing operator. Just to be a little bit more explicit about stopping the activity, I'm just going to go after the delay and call activity?.Stop in the same manner as if I was going to add a tag.

So now that we've gone through and instrumented our library, let's check out what happens when we run our application with OpenTelemetry but we're not actually listening for our new ActivitySource. As you can see, the application ran to completion completely fine. We've got zero error handling in here. So now let's make OpenTelemetry aware of our ActivitySource so that it can start displaying the activity. To do this, I'm going to go over to the Program.cs where I've registered OpenTelemetry and I'm going to add the source using the AddSource method and I'm going to pass in MyActivitySource.Name. If I hit F5 again now, you'll see that our tracing has started to work.

If you want to target anything less than .NET 5, then you're going to need to add the NuGet package System.Diagnostics.DiagnosticSource. This provides the ActivitySource type which you'll need for earlier versions of the .NET framework.

My recommendation for building activity sources is that you keep one ActivitySource per library project that you create. So for example, if I had two library projects in this one, I would create one for each. This allows me to just switch on and off different sections of the instrumentation as I see fit. I would also probably have this AddSource implementation hidden inside of an extension method so I could just say .AddMyLibrariesInstrumentation.

So that's pretty much it. It's actually really easy and simple to get distributed tracing in your applications.

If you enjoyed this video, consider subscribing to the YouTube channel for more content like this.

// share_this