Adding a CorrelationId for Exceptions for Application Insights (DRAFT)

Admittedly, this title could be shorter, but it s also a bit of a niche topic.

AppInsights OperationID in REST ASP NET Core API Response bij Exceptions

Use case / why

TODO

Choosing the right approach

When searching for the right approach, I came across a couple of options to solve this ‘aspect’ (cross cutting concern) in an elegant way: middleware, filters or the default exception handler.

Using Middleware

ASP.NET Core Middleware are components that are put into an application ‘pipeline’ to handle requests and responses. Each component in the pipeline processes requests, can pass them to the next component, and can handle responses. They’re executed in the order they are added to the pipeline.

Filters

Filters in ASP.NET Core are components that can be applied before or after certain stages in the request pipeline. Exception filters specifically handle exceptions thrown during the execution of an MVC action.

Exception filters catch exceptions thrown in MVC controllers, providing a mechanism to handle these exceptions within the context of the controller actions.

Default Exception Handler

What is the Default Exception Handler?

ASP.NET Core provides a built-in middleware for handling exceptions. This middleware can be configured to customize the error response and include additional information like a trace ID.

Conclusion

The following table summarizes the pros/cons of these approaches:

ProCon
MiddlewareGlobal Scope: Handles exceptions across the entire application.
Centralized Control: Provides a single place to manage exception handling logic.
Complexity: Can add complexity to the application setup.
Performance: Adds a slight overhead due to the additional processing layer.
FiltersContextual Handling: Suitable for handling exceptions within MVC controllers.
Easy Integration: Simple to implement and integrate with MVC actions.
Limited Scope: Only handles exceptions in MVC actions.
Redundancy: May require multiple error-handling mechanisms if the application includes non-MVC components.
Default Exception HandlerSimplicity: Easy to set up and requires minimal configuration.
Framework Integration: Leverages existing ASP.NET Core infrastructure.
Limited Customization: May not offer the same level of flexibility as custom middleware.
Dependency: Relies on middleware, which might not fit all application types.

Each approach for handling exceptions in ASP.NET Core—Middleware, Filters, and the Default Exception Handler—has its own strengths and limitations. Middleware is ideal for global error handling, ensuring consistent behavior across the application. Filters provide a focused mechanism for handling exceptions within MVC controllers, while the Default Exception Handler offers a straightforward way to leverage built-in framework capabilities with minimal setup. The best choice depends on your application’s architecture and specific error-handling requirements.

Since our application already uses middleware, we’ll be going for that one.

Getting the right ‘Id’

Obviously there are a lot of Ids flying around in your context; the clue is to get the right one. I want the one that is front and center in Application Insights.

Through trial and error, I found that the TraceId of the current System.Diagnostics.Activity is the one that appears as OperationId in Application Insights:

Indeed, from the docs:

[…] every trace is assigned a globally unique 16-byte trace-id (Activity.TraceId), and every Activity within the trace is assigned a unique 8-byte span-id (Activity.SpanId).

Each Activity records the trace-id, its own span-id, and the span-id of its parent (Activity.ParentSpanId)

This blog post does a better (visual) job of explaining/visualizing the distributed tracing concept:

Source: Tsuyoshi Ushio‘s blog

Now that we’ve established that we want to return the TraceId in case of an exception, we have all that we need to write our middleware.

Adding the Middleware

Add the middleware class:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            // Log the Exception
            _logger.LogError(ex, ex.Message);

            // Get the operation ID
            var operationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier;

            // Construct the response
            httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            var response = new
            {
                message = ex.Message,
                operationId = operationId
            };

            await httpContext.Response.WriteAsJsonAsync(response);
        }
    }
}

Register the middleware:

app.UseMiddleware<ExceptionHandlingMiddleware>();

Leave a Reply

Your email address will not be published. Required fields are marked *