Enriching your exceptions with information from Episerver

Adding custom information along with the usual message and stacktrace in your exceptions may possibly make it easier to find out what’s happening in your Episerver website. Here is a bit of code showing how it may be done in a quite simple way. I created it some time back and it has since then been improved by my collegues Svante Seleborg and Martin Lindström. I’ve altered it a bit to make it easier to follow.

The Enrich extension method below will need to suppress any exceptions being thrown by the Enrich functionality itself as throwing exceptions while handling exceptions will be very bad for the website.

public static class ExceptionExtensions
{
  private static ILogger _logger = LogManager.GetLogger(typeof(ExceptionExtensions));

  public static void Enrich(this Exception e)
  {
    if (e == null) return;
            
    try
    {
      EnrichUnsafe(e);
    }
    catch (Exception ex)
    {
      _logger.Log(Level.Error, "Error enriching exception with additional data.", ex);
    }
  }

Wrapping an unsafe private method in a try-statement logging an error if it occurs will allow for the normal handling of exceptions to continue, even if the enrichment process fails.

Custom data may be stored in the Data property of an Exception, so this is where we will add our custom information.

private static void EnrichUnsafe(Exception e)
{
  e.Data["StartPage_ID"] = ContentReference.StartPage?.ID .ToString(CultureInfo.InvariantCulture) ?? string.Empty;

  PageData page = null;
  try
  {
    page = ServiceLocator.Current .GetInstance<IPageRouteHelper>()?.Page;
  }
  catch (Exception ex)
  {
    e.Data["CurrentPage"] = $"Exception {ex.Message}.";
  }
  if (page != null)
  {
    e.Data["CurrentPage"] = $"{page.PageName} (ID:{page.PageLink})";
  }
  IContent content = null;
  try
  {
    content = ServiceLocator.Current.GetInstance<IContentRouteHelper>()?.Content;
  }
  catch (Exception ex)
  {
    e.Data["CurrentContent"] = $"Exception {ex.Message}.";
  }
  if (content != null)
  {
    e.Data["CurrentContent"] = $"{content.Name} (ID:{content.ContentLink})";
  }

  HttpContext context = HttpContext.Current;
  if (context == null)
  {
    return;
  }

  IIdentity identity = context.User?.Identity;
  if (identity != null)
  {
    e.Data["AuthenticatedUser"] = $"{identity.Name} (AuthenticationType:{identity.AuthenticationType})";
  }

  HttpRequest request = context.Request;
  if (request == null)
  {
    return;
  }
  e.Data["Request_Url"] = request.Url.ToString() ?? string.Empty;
  e.Data["Request_RawUrl"] = request.RawUrl ?? string.Empty;
  e.Data["Destination_Domain"] = request.Url.Host ?? string.Empty;
  e.Data["Source_Ip"] = request.UserHostAddress ?? string.Empty;
}

You can add any information you can get your hands on here, but be careful as not to crash anything. Once we have created the Enrich method adding all the custom information we need in our exception, we can call it from the Application_Error method of Global.asax.cs.

public class EPiServerApplication : EPiServer.Global
{
  protected void Application_Error()
  {
    Exception lastError = this.Server.GetLastError();
    if (lastError == null)
    {
      return;
    }

    lastError.Enrich();

This will enrich the exception being thrown with more information. It is also possible to create a custom HandleErrorAttribute and register it in the Application_Start method of Global.asax.cs.

GlobalFilters.Filters.Add(new MyErrorHandlerAttribute());
public class MyErrorHandlerAttribute : HandleErrorAttribute
{
  public override void OnException(ExceptionContext exceptionContext)
  {
    exceptionContext.Exception.Enrich();

    base.OnException(exceptionContext);
  }
}

Or just call the method in catch clauses before rethrowing if you must.

catch (Exception ex)
{
    ex.Enrich();

You may need to configure your logger to output the extra information before you’ll see it in your logs. For NLog, for instance, this may be done by adding data to your exception:format in the relevant target.

    <target xsi:type="File"
            name="MyFileTarget"
            encoding="utf-8"
            writeBom="true"
            fileName="C:/a/file/path/MySite.${shortdate}.log"
            layout="[${longdate}][${uppercase:${level}}][${logger}] ${message} ${exception:format=ToString,StackTrace,data:exceptionDataSeparator=\r\n}" />

This config will give you something like below at the end of your stacktrace:

   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
Request_Url: http://www.mysite.se/
Request_RawUrl: /
StartPage_ID: 6
CurrentPage: MySite (ID:6)
CurrentContent: MySite (ID:6)
Destination_Domain: www.mysite.se
Source_Ip: [the-ip]