Inconsistencies in Health Monitoring Between WebForms and MVC

Posted on April 19, 2010  |  

Posted in Development

6 comments

As I have written and spoken on numerous occasions, Health Monitoring happens to be one of my favorite features in ASP.NET. In WebForms, it's a path well trodden. However, while building Morts & Elvises with MVC2, I ran into a strange inconsistency, which I'd like to describe here.

WebForms

Whenever a WebForm throws an unhandled exception, at the very least the error is written to the system's even log. Suppose we have this silly simple page method:

protected override void OnLoad (EventArgs e)
{
  base.OnLoad (e);
  throw new InvalidOperationException (
      "Health monitoring with WebForms.");
}

Pop the Event Viewer, and you will see it logged, along with a plethora of information. If you deploy pdb files, you will even have the line number!

Important point #1: this happens whether custom error pages are enabled in web.config or not!

MVC

Let's switch gears to MVC. Consider this simple action:

public ActionResult Index()
{
 throw new InvalidOperationException ("Health monitoring with MVC.");
}

Important point #2: the only time I see the exception logged is when custom errors are turned off or visible only locally, i.e. <customErrors mode="Off" /> or <customErrors mode="RemoteOnly" />. With <customErrors mode="On" /> they vanish into thin air without a log entry.

This is silly. Of all things, I wouldn't want exceptions quietly disappear like that in production.

[HandleError] attribute messing up?

I found the following comment in Steven Sanderson's Pro ASP.NET MVC Framework:

Caution: HandleErrorAttribute only takes effect when you've enabled custom errors in your web.config file—for example, by adding <customErrors mode="On" /> inside the <system.web> node. The default custom errors mode is RemoteOnly, which means that during development, HandleErrorAttribute won’t intercept exceptions at all, but when you deploy to a production server and make requests from another computer, HandleErrorAttribute will take effect. This can be confusing! To see what end users are going to see, make sure you’ve set the custom errors mode to On.

Indeed, if you download MVC source code and peek inside HandleErrorAttribute.cs, you will see the following check:

// If custom errors are disabled, we need to let the 
// normal ASP.NET exception handler
// execute so that the user can see useful debugging information.
if (filterContext.ExceptionHandled || 
   !filterContext.HttpContext.IsCustomErrorEnabled) {
  return;
}

In other words, when the HandleError filter kicks in (which is when <customErrors mode="On" />), Health Monitoring doesn't get to log the exception, perhaps because the exception is flagged as handled, and the “normal ASP.NET exception handler” doesn’t run.

Custom exception filter for Health Monitoring

To fill the gap, I've whipped up this custom exception filter:

public class LogExceptionsViaHealthMonitoring : 
  FilterAttribute, IExceptionFilter
 {
 /// <summary>
 /// Called when an exception occurs.
 /// </summary>
 /// <param name="filterContext">The filter context.</param>
 public void OnException (ExceptionContext filterContext)
 {
  if (!filterContext.HttpContext.IsCustomErrorEnabled)
      return;
 
  UnhandledErrorEvent error = new UnhandledErrorEvent (
    "Unhandled exception", 
    this, 
    WebEventCodes.WebExtendedBase + 1, 
    filterContext.Exception);

  error.Raise ();
 }
}

UnhandledErrorEvent derives from WebBaseErrorEvent (see the Event Class Hierarchy) and doesn’t do anything useful. It simply gets our foot in the door:

public class UnhandledErrorEvent : WebErrorEvent
{
 public UnhandledErrorEvent (
   string msg, object eventSource, 
   int eventCode, Exception exception)
   : base (msg, eventSource, eventCode, exception)
  {
  }

  public UnhandledErrorEvent (
    string msg, object eventSource,
    int eventCode, 
    int eventDetailCode, Exception exception)
    : base (msg, eventSource, eventCode, eventDetailCode, exception)
  {
  }
}

The two attributes should now coexist peacefully:

[HandleError]
[LogExceptionsViaHealthMonitoring]
public class HomeController : Controller
{
  // ...
}

If anyone has further insight, please share!

6 comments

Terry
on April 21, 2010

To avoid having to always remember to 'pair' up the HandleError and the 'LogAttribute', I derived from HandleErrorAttribute and did something like the following:

public class MadHatterHandleErrorAttribute : System.Web.Mvc.HandleErrorAttribute
{
public override void OnException( ExceptionContext filterContext )
{
new UnhandledErrorEvent( "An unhandled exception has occurred.", this, WebEventCodes.WebExtendedBase + 1, filterContext.Exception ).Raise();
base.OnException( filterContext );
}
}

public class UnhandledErrorEvent : WebErrorEvent
{
public UnhandledErrorEvent( string msg, object eventSource, int eventCode, Exception exception )
: base( msg, eventSource, eventCode, exception )
{
}

public UnhandledErrorEvent( string msg, object eventSource, int eventCode, int eventDetailCode, Exception exception )
: base( msg, eventSource, eventCode, eventDetailCode, exception )
{
}
}


freelancer
on May 11, 2010

hi,

I'm new to asp.net health monitoring functions.
Where is this feature available in?


Milan Negovan
on May 12, 2010

It's been there since ASP.NET 2.0.


Christopher Roberts
on May 31, 2010

This is some very handy code, I am also relatively new to asp.net, however I believe that when I have learnt some more, this code will be very useful!

Thanks :)


sean hogg
on November 1, 2010

Hi, I like the article... the website http://mortsandelvises.com/ has two spelling mistakes: "indepdendent" and "inpedendent". Thx.


Milan Negovan
on November 1, 2010

Sean,

LOL! It slipped past the spell checker. Thank you for heads-up!