Build a Really Useful ASP.NET Exception Engine
By Peter A. Bromberg, Ph.D.
Peter Bromberg

If you have read some of the .NET - related articles on exception handling (including one of mine, "Documenting Exceptional Developers") then you probably have a pretty good idea that the rules of the game in .NET are twofold:

1) Unhandled exceptions (when they are thrown) are expensive and burn up a lot of extra CPU cycles.
2) Do everything you can to prevent exceptions from happening. Avoid using exceptions to handle business logic conditions in your code.

Having said this, the next step is "what to do with exceptions". All developers know that when an application is deployed, it's almost never perfect. Sooner or later, some user is going to find a way to break your app, correct? (After all, isn't that what users are for!) Users create inputs and other business use conditions that we developers could never have anticipated, and. uh -- well, "bad things" happen.

More often that not, users see that nasty looking red and yellow default ASP.NET error page because we extra - cool developers have never even taken the time to create a custom error page! What the heck good is a stack trace to a user? Heck, they can't even use the app correctly - they just caused an exception with it, right?



Of course you can easily set up your web.config (and even IIS) to redirect to a more friendly and Really Useful (a la "Thomas the Tank Engine") error page using the <customErrors> element in web.config. I encourage you to do this, and there is plenty of good documentation on it. However, that is not what this article is about, and so I don't cover the subject here. The problem with exceptions for the developer usually still exists: often the error page doesn't provide enough information for us to thoroughly and easily nail down a problem in a production web application. And so we are forced to spend much more time than we should attempting to figure out "why" it broke in production when it seems to work just fine in debug mode on our local development box. This doesn't have to be.

This article presents some concrete and "really useful" steps you can take (including a Registry trick with the Event Log viewer that you won't find anywhere else) that will enable you to easily zero - in on the exact error along with all the details right down to the page and exact line of code where your "perfect app" well -- BLEW UP! Not only that, but you can easily wire up any of your web apps to use this little framework simply by adding a few items to the web.config and the global.asax and dropping in a little 16K DLL into the /bin folder. The Really Useful Exception Engine works fine with codebehind as well as script-only ASP.NET apps.

First, understand that in ASP.NET, the Global Application_Error event handler receives all unhandled exceptions (including those you choose to throw on your own). And while I'm ranting on the subject, the following is NOT my idea of a legitimately "handled" exception:

Try
' your buggy spaghetti code here
Catch
End Try

-- I really hope, if you have digested the message correctly, that the above VB.NET snippet doesn't look familiar! If it does, go back and read (at the least) my other article linked at the top of this article.

The GetLastError() method of the Server object returns a reference to a generic HttpException wrapping the original exception that was passed from your ASP.NET page to the Application_Error event. Gain access to the original exception by calling its GetBaseException() method. This will provide the original exception instance, regardless of how many layers have been added to the exception stack. Once Application_Error has completed, it automatically performs a redirect to your custom error page that you can set up in web.config. See the MSDN documentation on how to set this up if you want it.

NOTE: If you are re-throwing an exception in your catch block after doing some business logic with it, you don't want to do this:

catch(Exception e){
// your stuff here
throw(e);
}

Instead, you want to do this:

catch(Exception e){
// your stuff here
throw;
}

If you use the first model, you may be throwing away the Stack Trace information. In the first example above, you are not rethrowing e, you are beginning a new exception flow using the same exception object instance. The C# throw syntax is "throw expr", where expr is an expression that evaluates to an exception object. It doesn't need to be a new object, any Exception-derived object can be used, even if it has been thrown a number of times before.

What's more important, and what we'll be dealing with here, is the fact that you can invoke whatever logging and additional exception - handling code you wish from within Application_Error. You don't need to use fancy HttpModules. All you need to do is set a "using" or "Imports" reference to you ExceptionHandler class library in your Global.asax and call your method(s).

In writing this ExceptionLogging class library, my initial objectives were:

1) It should be simple to write and easy to understand.
2) It should be portable (e.g., easy to set up with a minimum of configuration steps)
3) It should be extensible - that is, you can easily improve on or add to it.
4) It's initial scope should be to effectively log exceptions and, notify developers that an exception occurred.
5) It should be very easy to look in the event log and get details (e.g. go to a web page report).
6) It should use a database, not text files. Who wants to look through text files? Jeesh!
7) You should be able to easily deploy it in a production app, and only "turn it on" when you need it.
8) ALL exceptions from ALL apps in the enterprise should be logged to a central database so that admins and other responsible individuals can access this information easily.

As mentioned above, I do NOT like "log files". They are hard to parse, difficult to find things in, and they can get big really fast and start to slow down the whole works. They are also local to a specific machine and thus somewhat counterproductive to a development / production environment where numbers of dev and admin types are required by the overall business model to be able to work well together in a team environment. Databases, on the other hand, provide fast access throughout the enterprise, are easier to control vis-a-vis security, and the RDBMS model makes it much easier to sort, select and report on information. Additionally, scheduled jobs can perform cleanup or needed replication. So, if you want to write exception stuff to log files, you can choose to either modify my code, or roll your own.

The overall simple concept of my exception reporting engine is as follows:

1) We definitely want to log unhandled exceptions including basic details to the local event log. If possible, we also want to identify each exception with a custom ID so that we can create an HTTP URL link to a custom report page that will query our detailed exception information out of our database and present it in a nice web - page report, accessible by anyone with the proper credentials, throughout the enterprise.

2) We want optional email notification to a short list of people who should know about the problem and are capable of taking fast action to fix it, preferably (again) with a link to the report page URL.

With this in mind, we need to look at a little and very much undocumented Registry hack. If you've ever noticed, some of your event log entries have an actual clickable hyperlink in them that takes you to the "help and support engine" and if the exception has a description there, you get to read up on it. Big deal, right? I have "Help and support" service turned off on all my boxes, cause all it does is take up extra memory and resources. And besides, since we as professional developers already know everything, why would we need Help and Support?

Now open up REGEDIT and look at:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Event Viewer

You'll see entries for:
MicrosoftRedirectionURL,
MicrosoftRedirectionProgram,and
MicrosoftRedirectionProgramCommandLineParameters

By simply changing some of these values, you can tell the Event Viewer to stop using the custom help and support executable and simply go to a URL of your choosing. It will append the s% variable, which contains a whole bunch of querystring information, onto the end of your custom URL, and if you have set a custom EventId such as the Identity value of a newly inserted database record, it will happily supply this on the querystring to your reporting URL so that it can go find the newly inserted database record! (Don't look for this trick in MSDN, you'll won't find anything beyond a vague description of the Registry entries- I had to figure it out through personal toil and pain). Of course what this means is that your Event Log entries just became about 10 times more useful, because now, if you think you found the right exception, all you need to do is click on the hyperlink and you get the whole report in a nice web page! And, as we'll see shortly, you can also turn on email notification - which will provide much the same information, including the useful hyperlink that zeroes in on the exact exception record from your database!

Oh, and don't worry - I've included a handy registry script with the downloadable zipfile below. All you need to do is double-click on it and the modifications will be made for you automatically.

First, before we delve into some code, lets take a quick review of what it takes to configure an ASP.NET web application to use the engine:

1) web.config needs to have an <appSettings> section with the following entries, which should be pretty much self-explanatory:

</system.web>
<appSettings>
<add key="LogErrors" value="true" />
<add key="dbConnString" value="server=(local);database=WebAppLogs;User id=sa;password=;" />
<add key="emailAddresses" value="you@yourdomain.com|friend@yourdomain.com" />
<add key="smtpServer" value="mail.yourdomain.com" />
<add key="fromEmail" value="yourEmailFrom@yourdomain.com" />
<add key="detailURL" value="http://yourappURL/exceptionlogger/report.aspx" />
</appSettings>

< /configuration>

-- That's it for the web.config! Easy!

Next, we move to your Global.asax:

using ExceptionHandler;
. . .
protected void Application_Error(Object sender, EventArgs e)
{
ExceptionHandler.LogException exc = new ExceptionHandler.LogException();
exc.HandleException(Server.GetLastError().GetBaseException());
}

-- Pretty simple! Aside from the Registry entries and the database setup (a sample SQL script is provided) we are now 100% set up! Note that the "LogErrors" key can be set to "false" -- and it's as if the exception engine didn't even exist.

So now, lets cruise through my spaghetti code and see what this thing really does, under the hood:


using System;
using System.Web;
using System.Diagnostics;
using System.Data;
using System.Data.SqlClient;
using System.Web.Mail;
namespace ExceptionHandler
{  
 public class LogException  
 {
  public LogException( ) //ctor
  { 
  } 
  public void HandleException( Exception ex)
  {
   HttpContext ctx = HttpContext.Current; 
   string strData=String.Empty;
   int evtId = 0;
   bool logIt = 
    Convert.ToBoolean(System.Configuration.ConfigurationSettings.AppSettings["logErrors"]);
   if(logIt)
   {
           string dbConnString=
 System.Configuration.ConfigurationSettings.AppSettings["dbConnString"].ToString();
    
    string referer=String.Empty;
    if(ctx.Request.ServerVariables["HTTP_REFERER"]!=null)
    {
     referer = ctx.Request.ServerVariables["HTTP_REFERER"].ToString();
    }
  string sForm = 
   (ctx.Request.Form !=null)?ctx.Request.Form.ToString():String.Empty;
      
        string sQuery =
   (ctx.Request.QueryString !=null)?ctx.Request.QueryString.ToString():String.Empty;
      strData="\nSOURCE: " + ex.Source +
            "\nMESSAGE: " +ex.Message +
     "\nFORM: " + sForm + 
     "\nQUERYSTRING: " + sQuery +
     "\nTARGETSITE: " + ex.TargetSite +
     "\nSTACKTRACE: " + ex.StackTrace +
     "\nREFERER: " +referer;
    
   

   if(dbConnString.Length >0)
   {                  
    SqlCommand cmd = new SqlCommand();
    cmd.CommandType=CommandType.StoredProcedure;
    cmd.CommandText="usp_WebAppLogsInsert";
    SqlConnection cn = new SqlConnection(dbConnString);
    cmd.Connection=cn;
    cn.Open();       
    try
    { 
    cmd.Parameters.Add(new SqlParameter("@Source", ex.Source));
    cmd.Parameters.Add(new SqlParameter("@Message",ex.Message)); 
cmd.Parameters.Add(new SqlParameter("@Form",sForm)); cmd.Parameters.Add(new SqlParameter("@QueryString",sQuery)); cmd.Parameters.Add(new SqlParameter("@TargetSite",ex.TargetSite.ToString())); cmd.Parameters.Add(new SqlParameter("@StackTrace",ex.StackTrace.ToString())); cmd.Parameters.Add(new SqlParameter("@Referer",referer)); SqlParameter outParm=new SqlParameter("@EventId",SqlDbType.Int); outParm.Direction =ParameterDirection.Output; cmd.Parameters.Add(outParm); cmd.ExecuteNonQuery(); evtId =Convert.ToInt32(cmd.Parameters[7].Value); cmd.Dispose(); cn.Close(); } catch (Exception exc) { EventLog.WriteEntry (ex.Source, "Database Error From Exception Log!",
EventLogEntryType.Error,65535); } finally { cmd.Dispose(); cn.Close(); } try { EventLog.WriteEntry (ex.Source, strData,EventLogEntryType.Error,evtId); } catch(Exception exl) { throw; } } } string strEmails =System.Configuration.ConfigurationSettings.AppSettings["emailAddresses"].ToString(); if (strEmails.Length >0) { string[] emails = strEmails.Split(Convert.ToChar("|")); MailMessage msg = new MailMessage(); msg.BodyFormat = MailFormat.Text; msg.To = emails[0]; for (int i =1;i<emails.Length;i++) msg.Cc=emails[i]; string fromEmail= System.Configuration.ConfigurationSettings.AppSettings["fromEmail"].ToString();
msg.From=fromEmail; msg.Subject = "Web application error!"; string detailURL= System.Configuration.ConfigurationSettings.AppSettings["detailURL"].ToString(); msg.Body = strData + detailURL +"?EvtId="+ evtId.ToString(); SmtpMail.SmtpServer = System.Configuration.ConfigurationSettings.AppSettings["smtpServer"].ToString(); try { SmtpMail.Send(msg); } catch (Exception excm ) { throw; } } else { return; } } } }


if you walk through the above code which is very "linear" in nature, you should have no difficulty understanding what is going on. Now here are the steps to set up the sample app in the download below, so that you can begin to use and / or customize it: The download zipfile contains code that adds an additional "LogDateTime" field in the table to make searching and sorting easier for any reporting pages you may wish to create.

1) Unzip the files into a new folder under your wwwroot called "ExceptionLogger".
2) Go Into IIS Manager and set this to be a new Virtual Directory and make it an IIS Application. (The actual ExceptionHandler class project is in a subfolder).
3) In Windows Explorer double-click the EventLogEntries.reg Registry script to make the Registry modifications described above (You can export the existing keys first if you want so you can restore them). If you want a custom IIS app for your reporting page(s), you'll need to make those changes in the Registry script before you
execute it).
4) Open Up Sql Query Analyzer and connect to your SQL Server database. Load the ExceptionLogger.sql script and execute it.
5) Make any custom modifications to the web.config that pertain to your email and other settings. Make sure that your IIS SMTP Server is running.

NOTE: If your app is running under the default "machine" account in machine.config, that's the weak asp.net account, whch may not have write permissions to the Event log. Either grant it the permission, change the userName in the processModel section of machine.config to "system", or run your code under impersonation of a stronger account with the correct setting in your web.config.

Your Really Useful Exception Engine is now ready. If you request the Webform1.aspx test page, you'll see that it attempts a division by zero and creates an unhandled exception. If you bring up your Event Viewer and look in the Application log, you'll see the event log entry for this. If you scroll to the bottom of the entry, clcking on the help URL will bring up the Report.aspx page and display your custom exception database record information! You should also get an email with a link to the same page and info.

Your goal when you put your app through testing and QA should be to turn on the Really Useful Exception Engine, and over an extended testing period, see NO ENTRIES. Then, turn it off when you deploy your app into production. If there is ever a user report of a "BTH" (Bad thing happened), you can turn it back on temporarily to help diagnose the issue so it can be fixed quickly.

Finally, in the interest of completeness, there is one issue you should be aware of. The eventlog classes wrap the OS API and the eventID is an integer with a maximum value of 65535. Once your table gets over that number of rows, you will need to drop and recreate the LogItems table, or at least delete the records and re-seed the EventId column back to "1". The most elegant way to do this is to test in the stored procedure itself, and issue a TRUNCATE TABLE command which reseeds the Primary Key back to start at "1" I've included code in the sproc that will do this in the download below. Of course, only developers with Obsessive Compulsive Disorder are ever likely to rack up that many exception records in their lifetimes, but - you never know, do ya'?

Download the code accompanying this article

 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.