Recording Exceptions with 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 record an exception?
In the previous post I showed how you could listen to activities and log them to the console.
But what is an exception occurs? How should you record that detail as part of an activity?
Here is an example of an activity in use:
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);
...
// Do Some Work
...
}
Suppose an exception occurs? The OpenTelemetry protocol recommends setting the status of the activity to ‘Error’, which can be done using the SetStatus
method:
using var activity = Source.StartActivity(DiagnosticsNames.DoWork);
try
{
var workId = Guid.NewGuid().ToString();
activity?.SetTag("work-id", workId);
...
// Do Some Work
...
}
catch (Exception)
{
activity?.SetStatus(ActivityStatusCode.Error);
throw;
}
}
If you want to record the exception details as part of the activity, the OpenTelemetry protocol has a recommendation for that as well:
https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/#attributes
It states that an exception should be recorded as an event with the name exception
. Many of the language-specific APIs include a recordException
method for this purpose. The Diagnostics API in .NET does not include such a method. However, inspecting the OpenTelemetry library for .NET, one can see the shim they use for this missing feature. It adds an event with the name exception
and if provided, includes tags for:
exception.type
exception.stacktrace
exception.message
We can mimic an extension method for this behavior as well:
public static class ActivityExtensions
{
public static void RecordException(this Activity activity, Exception ex, bool hasEscaped = true)
{
var tags = new ActivityTagsCollection
{
{ "exception.type", ex.GetType().FullName },
{ "exception.message", ex.Message },
{ "exception.stacktrace", ex.ToString() },
{ "exception.escaped", hasEscaped }
};
var exceptionEvent = new ActivityEvent("exception", default, tags);
activity.AddEvent(exceptionEvent);
activity.SetStatus(ActivityStatusCode.Error);
}
}
Now we can use it in our catch
block:
using var activity = Source.StartActivity(DiagnosticsNames.DoWork);
try
{
var workId = Guid.NewGuid().ToString();
activity?.SetTag("work-id", workId);
...
// Do Some Work
...
}
catch (Exception)
{
activity?.RecordException(ex, true);
throw;
}
}
And the listener captures the exception details:
Trace:00-aebc8545cc9e56e90a535a36839c7fc8-2de822fca0a2818c-01
Name:Do Work
Source:Tracing.Sample
SpanId:aebc8545cc9e56e90a535a36839c7fc8
SpanId:2de822fca0a2818c
Parent:
Duration:00:00:00.0303756
Status:Error
Attributes:
work-id 793b1373-44e0-46f1-bfb1-4f9ff7c20c8f
Events:
Name:exception
Timestamp:4/7/2024 12:04:58 PM +00:00
Attributes:
exception.type System.Exception
exception.message My Exception
exception.stacktrace System.Exception: My Exception
at Tracing.Worker.DoWorkAsync(CancellationToken stoppingToken) in C:\Users\edgamat\projects\Tracing\Worker.cs:line 37
exception.escaped True
NOTE If you are using the OpenTelemetry NuGet package, then use its RecordException
extension method, no reason to write your own!
Summary
When instrumenting your code with custom activities, make sue you handle exception appropriately. Follow the OpenTelemetry standards, as they make a lot of sense and are easily to use consistently.