Logging Diagnostic Activities in .NET
The Diagnostics API in .NET provides a great way to record activities in your code using the new ActivitySource
and Activity
objects. But how do you listen to these activities (and log them)?
Let’s start with a simple scenario, you want to record an activity within a background service. In this case, the service calls a DoWorkAsync
method, which is the activity we want to record.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
await Task.Delay(1000, stoppingToken);
}
}
We can add an activity to record a span:
public static readonly ActivitySource Source = new("Tracing.Sample", "1.0.0");
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
using var activity = Source.StartActivity(DiagnosticsNames.DoWork);
var workId = Guid.NewGuid().ToString();
activity?.SetTag("work-id", workId);
...
...
The first thing you will notice when stepping through this code using a debugger is the activity object is null
. This is why it is necessary to use the null-conditional operator to call the SetTags
method. In order for the activity
object to not be null
, you need to configure an activity listener.
Activity Listeners
An ActivityListener
is type within the Diagnostics API you can use to listen to the activities being recorded. It gives you a lot of flexibility to control if you want to ignore certain activities or if you want to only record a sample of the activities.
Here is a simple listener that records all activities to the console:
var consoleListener = new ActivityListener();
consoleListener.ShouldListenTo = _ => true;
consoleListener.Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData;
consoleListener.ActivityStopped = LogActivityStopped;
public static void LogActivityStopped(Activity activity)
{
Console.WriteLine($"Trace:{activity.Id}");
Console.WriteLine($"\tName:{activity.DisplayName}");
Console.WriteLine($"\tSource:{activity.Source.Name}");
Console.WriteLine($"\tSpanId:{activity.TraceId}");
Console.WriteLine($"\tSpanId:{activity.SpanId}");
Console.WriteLine($"\tParent:{activity.ParentId}");
Console.WriteLine($"\tDuration:{activity.Duration}");
Console.WriteLine($"\tStatus:{activity.Status}");
Console.WriteLine($"\tAttributes:");
foreach ((string key, string? value) in activity.Tags)
{
Console.WriteLine($"\t\t{key}\t{value}");
}
Console.WriteLine($"\tEvents:");
foreach (var activityEvent in activity.Events)
{
Console.WriteLine($"\t\tName:{activityEvent.Name}");
Console.WriteLine($"\t\tTimestamp:{activityEvent.Timestamp}");
Console.WriteLine($"\t\tAttributes:");
foreach ((string key, object? value) in activityEvent.Tags)
{
Console.WriteLine($"\t\t\t{key}\t{value}");
}
}
}
To enable it, you need to add it to the activity source:
ActivitySource.AddActivityListener(consoleListener);
It will produce logs like this:
Trace:00-74c8a413e656fd3feb7511a5cd0b156c-26eca8f5ee9d7994-00
Name:Do Work
Source:Tracing.Sample
Duration:00:00:00.0000683
And if you debug the application, the activity
in the DoWorkAsync
method is no longer null
.
In the above example, the ShouldListenTo
action always returns true
. This means the listener is going to listen to every activity. If you want to be selective, you can change the action to suit your needs:
consoleListener.ShouldListenTo = source => source.Name == "Tracing.Sample";
The Sample
action always returns AllData
which means the activity will include all attributes, links, events and propagation information. But you could change it to None
if certain criteria are met, meaning these activities will not be logged.
The ActivityStopped
action is a callback that is called when an activity stops. This allows us to see the last state the activity is in, prior to it being disposed. You can record as much or as little of the activity as necessary. Tags, links and events are all available to be logged.
Summary
Instrumenting your code with ActivitySource
and Activity
objects can be a bit confusing at first because without a listener, nothing gets recorded! Thankfully, adding an ActivityListener
is pretty straightforward. The listener gives you a lot of control to record as much or as little as you want!