Logging to Sentry in ASP.NET Core

Martin Cerruti
5 min readMay 26, 2018

--

With the increasingly distributed nature of modern web applications, logging can become problematic. Infrastructure providers such as Docker enable us to linearly scale applications on any number of virtual machines. That’s great, but what about the logs? When using traditional file logging implementations, we’d have to first figure out which container the error occurred on, we’d then have to attach to the container, and retrieve the relevant logs. That takes up a lot more time than it should.

Fortunately, error reporting services such as Sentry come to the rescue. They enable applications to log to a centralized repository, and provide rich tools for analysing any errors or log message that are reported. No longer are we plagued with logging into various containers or machines and retrieving text files — it’s all right there, on a single dashboard.

I couldn’t find many resources on how to implement Sentry in ASP.NET Core in an idiomatic way. This article covers implementing sentry through the use of a middleware component, providing you with a flexible implementation to log your exceptions to Sentry.

Middleware

ASP.NET Core has a concept called middleware. Middleware is user code that runs as part of the request pipeline. It can obtain information about requests, modify the response, and virtually everything in between.

In .NET Core 2.0, a good example of standard middleware is the DeveloperExceptionPageMiddleware component. When in development, if an exception occurs, this middleware component catches it and displays the relevant details to the developer.

That’s fantastic, but we definitely don’t want to be exposing stack traces and other juicy details to end users in a production setting, so we can’t be running that out in the wild.

But then what do we do? As it turns out, ASP.NET Core MVC doesn’t really provide any production-ready solution for logging unhandled exceptions. We’ll have to roll our own.

Logging exceptions

By default, ASP.NET Core comes with a DeveloperExceptionPageMiddleware. This component catches an exception and then renders it in a developer-friendly way. Fortunately for us, this component is open-source:

If we look at the source for DeveloperExceptionPageMiddleware, we can see that it essentially takes the RequestDelegate, wraps it in a try/catch clause, and if an exception occurs, some code is executed to display the exception details to the developer.

Surely we can do something similar to log unhandled exceptions to an error reporting service such as Sentry.

Sentry uses Raven to do its error reporting. For .NET Core, a package named RavenSharp.Core is available on NuGet. It’s worth noting that this is not actually an official package, but instead one written by as community member. At the moment of writing, there is no official .NET Core package available, and the one written by wakawaka54 works just fine.

For short, we’ll need the following for our middleware to work:

  1. Configuration for our Sentry DSN
  2. A class to send our error reports to Sentry
  3. A middleware component to catch any unhandled exceptions

Configuration

Configuration in ASP.NET Core applications is specified in appSettings.json. In our case, we’ll need to be able to configure the Sentry DSN, which is just a string:

 “Sentry”: {
“Dsn”: “{your_DSN_here}”
}

Alright. Next up, we’ll need to hook this up to a type so we can pass it as an IOptions<T> type to our error reporting class.

Something along the lines of:

csharp
public class SentryOptions
{
public string Dsn { get; set; }
}

Should do just fine. Of course, there are more variables you can configure, but I’ll leave those up to you.

Next up, in Startup.cs, we’ll need to grab the options in our app settings and map them to our freshly created SentryOptions type so we can inject IOptions<SentryOptions> further down the road. In your ConfigureServices method, add the following line:

 services.Configure<SentryOptions>(Configuration.GetSection(“Sentry”));

Of course, if you named your configuration section anything other than Sentry, you’ll need to adjust the above call accordingly.

Alright, that should take care of the configuration part. We should be all set to write the actual error reporter now.

The Error Reporting class

Thanks to RavenSharp, writing the error reporting class is a breeze. All we’ll need to do is wrap the CaptureAsync method for at least Exception (though it’s useful to wrap the string overload as well for non-exception logging purposes)

The below code should be fairly straight forward:


public interface IErrorReporter
{
Task CaptureAsync(Exception exception);
Task CaptureAsync(string message);
}

The reason I’m declaring an IErrorReporter interface instead of hooking this stuff up to the Microsoft.Extensions.Loggingabstractions is because logging unhandled exceptions to services like Sentry is a hail-mary, last resort action, whereas general logging should (in my opinion) not end up in services such as Sentry, but in files, log stashes, or similar.

Our Middleware

While we have a perfectly viable way of logging exceptions to Sentry at our disposal now, we’re doing this whole thing so we can handle those pesky unhandled exceptions that would otherwise go unnoticed.

As discussed before, we can achieve this through the use of a middleware component. Since we’re not manipulating the response, we can simplify the developer exception page middleware significantly, down to the point where just need to inject our error reporter class and wrap the request in a try/catch block:

And that’s really all there’s to it.

Wiring it all up

All that’s left is to wire up the dependency injection and adding the middleware to the request pipeline.

First off, in your Startup.cs, add the following to your ConfigureServices method:

 services.AddScoped<IErrorReporter, SentryErrorReporter>();

This will set up the DI container to inject an instance of our SentryErrorReporter into the middleware on a per-request scope.

Then, near the top of Configure, add:

 app.UseMiddleware<SentryMiddleware>();

Keep in mind the ASP.NET Core request pipeline goes in both directions, as illustrated by this image:

Since we’ve written our middleware to not handle the exception but instead to rethrow it, we need to make sure we add our middleware before any other exception-handling middleware on the way back to the response.

By default, ASP.NET Core MVC applications contain an error handler along the lines of app.UseExceptionHandler(“/Error”); which will actually catch the exception and handles it by displaying a page to the user whenever an error occurs. We’ll want to make sure that during response, our middleware gets the chance to catch the exception before such middleware runs.

Ergo, we should add it along the lines of:


if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler(“/Error”);
}

app.UseMiddleware<SentryMiddleware>();

So we get the chance to report the exception to Sentry before any other middleware gets a chance to handle it.

Conclusion

We’ve set up a middleware component that allows all exceptions that aren’t handled by user code to be reported directly to Sentry. This way you’ll always know what exceptions are being thrown in your exception, in nigh-realtime, allowing you to quickly fix any issues instead of relying on your users to report them to you.

Of course you could replace Sentry with any other similar service in a similar fashion as outlined above.

Keep in mind that users generally don’t report errors. They just abandon your product and go search for an alternative that does work. Being aware of issues your users run in as they occur is extremely important, and at least with ASP.NET Core, it’s a breeze too.

--

--

Martin Cerruti
Martin Cerruti

Written by Martin Cerruti

Software Architect and Technology Writer.

Responses (1)