Logging exceptions to Raygun from your Blazor Server app

Logging exceptions to Raygun from your Blazor Server app

There is no official Blazor support from Raygun but it is actually simple to implement.

How does Blazor Server handle errors?

Blazor logs all unhandled exceptions to ILogger instances. For example, in development, errors are logged to the console with Console Logging Provider. For more detailed information I suggest reading the docs. This is good news as it gives us a nice way to log errors with Raygun using this mechanism and creating a custom RaygunLogger.

Setting up Raygun in your app

The first thing to do is to add Raygun to your Blazor Server app. I followed the official guide, steps 1-2. To summarise:

  1. Add "Mindscape.Raygun4Net.AspNetCore" to the Server project
  2. Add Raygun settings to appsettings.json "RaygunSettings": { "ApiKey": "YOUR_APP_API_KEY" }

With Raygun configured, a custom logger must be created. We will add that to the LoggerFactory so that when Blazor logs an unhandled exception our logger will send it to Raygun.

Creating the RaygunLogger

To manually send exceptions to Raygun use the RaygunClient, creating a new instance and calling Send or SendInBackground. e.g.

try
{
  
}
catch (Exception e)
{
  new RaygunClient("YOUR_APP_API_KEY").SendInBackground(e);
}

This is how we will send the exception in the RaygunLogger. Below is the code for RaygunLogger. It implements Microsoft.Extensions.Logging.ILogger.

public class RaygunLogger : ILogger
{
    private readonly string _name;
    private readonly RaygunConfiguration _config;
    private readonly LogLevel _logLevel;
    
    public RaygunLogger(string name, RaygunConfiguration config)
    {
        _name = name;
        _config = config ?? throw new ArgumentNullException(nameof(config));
        
        if (!Enum.TryParse(_config.LogLevel, out _logLevel))
            _logLevel = LogLevel.Error;
    }
    
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
        Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
            return;
            
        new RaygunClient(_config.ApiKey).SendInBackground(exception, new List<string> { _name });
    }
    
    public bool IsEnabled(LogLevel logLevel)
    {
        return logLevel == _logLevel;
    }
    
    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }
}

In the Log method I only log exceptions of a certain logLevel to Raygun.

if (!IsEnabled(logLevel))
  return;

logLevel is set in appsetting.json. If there is no logLevel setting it is set to LogLevel.Error. I have done this because there is a lot of lower level messages (warning, information etc.) and I want to filter out the noise. By setting the log level in appsettings I can change the logLevel in production to help troubleshoot an issue without the need to redeploy the app. I recommend you do this too.

RaygunLoggerProvider

Next is the RaygunLoggerProvider, used to create an instance of the RaygunLogger.

public class RaygunLoggerProvider : ILoggerProvider
{
		private readonly RaygunConfiguration _config;
		private readonly ConcurrentDictionary<string, RaygunLogger> _loggers = new ConcurrentDictionary<string, RaygunLogger>();

		public RaygunLoggerProvider(RaygunConfiguration config)
		{
				_config = config;
		}

		public void Dispose()
		{
				_loggers.Clear();
		}

		public ILogger CreateLogger(string categoryName)
		{
				return _loggers.GetOrAdd(categoryName, x => new RaygunLogger(categoryName, _config));
		}
}

I pass the categoryName to the RaygunLogger and set it as a tag. The category name is typically the class name the error is logged in so it makes a useful tag to filter against when looking at the error dashboard in Raygun.

The final piece is to add the RaygunLoggerProvider to the LoggerFactory. I created an extension method, which is perhaps a little unnecessary, there's not much to it.

public static class RaygunLoggerFactoryExtensions
{
		public static ILoggerFactory AddRaygunLogger(this ILoggerFactory factory, RaygunConfiguration config)
		{
				factory?.AddProvider(new RaygunLoggerProvider(config));
				return factory;
		}
}

In Startup.cs I get the configuration settings from appsettings and add the logger to the logger factory.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
  var settings = Configuration.GetSection("RaygunSettings");
  var raygunSettings = new RaygunConfiguration();
  settings.Bind(raygunSettings);
  
  loggerFactory.AddRaygunLogger(raygunSettings);
}

appsettings.json

{  
  "RaygunSettings": {
    "ApiKey": "YOUR_APP_API_KEY",
    "LogLevel": "Error"
  }
}

RaygunConfiguration.cs

public class RaygunConfiguration
{
    public string ApiKey { get; set; }
    public string LogLevel { get; set; }
}

All unhandled exceptions will be picked up by our logger and sent to Raygun. Handled exceptions can be logged using ILogger. e.g.

public class HomepageBase : ComponentBase
{
    [Inject] protected ILogger<HomepageBase> Logger { get; set; }
    
    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender) return;
        
        try
        {
            throw new Exception("Code not found");
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, ex.Message);
        }
    }
}

In Raygun we can see the error detail and the tag is set to the name of the class.

Raygun error

What if I have Razor pages too?

To cover server pages such as Razor pages follow the official guide, step 3.

Add app.UseRaygun(); to the Configure method and add services.AddRaygun(Configuration); to the ConfigureServices method in Startup.cs.

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    // Add framework services.
    services.AddRazorPages();
    services.AddRaygun(Configuration);
    services.AddServerSideBlazor();
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
    app.UseExceptionHandler("/Home/Error");
    app.UseHttpsRedirection();
    app.UseRaygun();
    app.UseStaticFiles();
    app.UseRouting();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
  }
}

And JavaScript?

Despite the many "Blazor, the JavaScript killer" posts you are going to have JavaScript in your app. To cover any JavaScript errors Raygun has a JavaScript library. Follow the simple installation guide and your set.

Summary

Blazor provides us with an easily extensible mechanism to customise how errors are logged. This example uses Raygun to create a simple logger, but this could apply to many other tools.