ASP.NET Production Exception Logging For Dummies [Updated]

by Peter A. Bromberg, Ph.D.

Peter Bromberg
There was a programmer from Rose
Whose objects just wouldn't dispose.
He'd play with his classes
Till he smoked up his glasses
And now his hard drive light just glows.

Don't be put off by my funky article title - after all, I'm the dummy that wrote this! The idea is to keep it simple, that's why "for Dummies".

Have you ever built your ASP.NET application and it works fine on your development machine, and you are so happy and pleased with yourself. And now, you set the <compilation debug="false" ...> element in your web.config file like a good trooper (You BETTER do that!) and you deploy it into production. . .

BAM! An SBH! (Something Bad Happened). Well, now, we can't tell what it is, can we? Debugging is turned off, we have no IDE, our wonderful application is on it's own now and it is supposed to be able to swim! But - it just drowned, and we don't know why. Sound familiar?



One of the biggest deterrents to rapid application development is the tendency for newer developers (and even some advanced developers) to avoid taking the small amount of extra time needed to wire up their code for exception handling and reporting. I frequent the Microsoft C# and ASP.NET newsgroups, as well as our forums here at eggheadcafe.com, and the pattern is so obvious it is almost ubiquitous -- usually there is a post that says "What am I doing wrong" or expressing some incredulity about what the Framework is doing with their code, or in some cases even the overly-arrogant "There's a bug in XXX!". These problems are universally caused by what I refer to as "Exceptionless Programmer Syndrome" -- the inability or unwillingness to wrap any and all code that could possibly throw an exception in some sort of exception handling and / or logging code. Fortunately, recovery from this affliction is relatively simple:

1) You must renounce your arrogance and accept that no matter how sophisticated a programmer you are, any line of code you write could potentially throw an exception.

2) You must assume that no matter how good your code is, that an end user will somehow cause an exception and so you must provide a framework for the handling and logging of same so that you can easily track it and improve your already perfect code.

Let's look at a very easy way to wire up any ASP.NET application with automatic unhandled exception logging. We are going to log to a database, we are going to have the option to send out a SysLog message, and finally, we are going to send an email to whomever we want with a hyperlink in it that will actually bring up our exception reporting page with the details of the exact exception that our little logging framework just inserted into our SQL Server logging table. The framework can also be used for logging "Handled" exceptions by simply passing an instance of the caught exception, along with any additional "Data" items (new in the exception class in .NET 2.0), into the static HandleException method.

Unhandled exceptions aren't rare; they are actually pretty common, especially when real users (not developers) start using the application. If you can catch most of them during the testing phase, so much the better. This little framework can help a lot.

Easy to Add to any Application

To add this logging framework to any ASP.NET web application you only need to do the following:

1) Add the following to your Global.asax:

protected void Application_Error(Object sender, EventArgs e)
{
PAB.ExceptionHandler.ExceptionLogger.HandleException(Server.GetLastError().GetBaseException());
}

2) Add the following to your web.config:

</system.web>
<appSettings>
<add key="LogExceptions" value="true" />
<add key="sendSysLogMessages" value ="true" />
<add key="exceptionLogConnString" value="server=127.0.0.1;database=WebAppLogs;User id=sa;password=;" />
<add key="emailAddresses" value="you@yourcompany.com;otherdev@yourcompany.com" />
<add key="smtpServer" value="mail01.yourcompany.com" />
<add key="fromEmail" value="thisapp@yourcompany.com" />
<add key="detailURL" value="http://yourwebserver/exceptionlogger/report.aspx" />
<add key="sysLogIp" value="10.10.9.231" />
</appSettings>
</configuration>

3) Drop the "ExceptionHandler.dll" into the /bin folder of your app.

That's it! Once your database is set up and you have your "Report.aspx" page in another app that can be pointed to by any one of your Web Applications, you are pretty much good to go!

How it works:

When there is an unhandled exception, we still have the choice of dealing with it at the Page (Page.Error) or at the Application level. Application is usually easiest, since it provides us with a centralized location and requires less duplication of code. If you look at the Application_Error handler above, we are capturing the Server.GetLastError method's base exception - that's what we want here. Then, we simply pass the exception into the static HandleException method of our little "framework".

If you have "LogExceptions" set to "true", it will go through it's paces, optionally sending a SysLog message if the "sendSysLogMessages" element is also set "true". If you aren't familiar with SysLog, I strongly recommend that you familiarize yourself with it. Kiwi has a very nice Windows Syslog client, and it's free. This can be kept open on a machine, and it's easy to see messages that may have been sent from multiple apps. They can even be color-coded in the display by priority / severity. They are nothing but little UDP Packets. In our enterprise, where we have lots of SysLog messages flying around from many different sources, I built a Windows Service that serves as the endpoint "router" for all SysLog messages. It reads, parses, and does business logic, re-routes the UDP Packet if required, and handles all alarming and notifications - email, MSN Messenger, SMS cellphone text messages, and more. All messages are also stored in a database for reporting and diagnostic purposes. In short, we "live SysLog", 24/7 at my organization.

So let's take a look at what happens when an exception is passed to the HandleException method:

using System;

using System.Web;

using System.Diagnostics;

using System.Data;

using System.Data.SqlClient;

using System.Web.Mail;

using System.Configuration;

namespace PAB.ExceptionHandler

{   

    public class  ExceptionLogger 

    {

        private static bool logExceptions = Convert.ToBoolean(ConfigurationSettings.AppSettings["logExceptions"]);

        private static bool sendSysLogMessages =  Convert.ToBoolean(ConfigurationSettings.AppSettings["sendSysLogMessages"]);

        private static string sysLogIp=ConfigurationSettings.AppSettings["sysLogIp"];

 

        private ExceptionLogger( ) //pvt ctor, all methods static

        {

        }   

 

        public static void HandleException( Exception ex)

        {

            if(!logExceptions) return; // user set web.config setting to false, abort

            HttpContext ctx = HttpContext.Current;   

            string strData=String.Empty;

            Guid eventId =    System.Guid.NewGuid();           

           string dbConnString=

    System.Configuration.ConfigurationSettings.AppSettings["exceptionLogConnString"].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 logDateTime =DateTime.Now.ToString();

        string sQuery =

            (ctx.Request.QueryString !=null)?ctx.Request.QueryString.ToString():String.Empty;

                    strData="\nSOURCE: " + ex.Source +

                    "\nLogDateTime: " +logDateTime +

                    "\nMESSAGE: " +ex.Message +

                    "\nFORM: " + sForm +

                    "\nQUERYSTRING: " + sQuery +

                    "\nTARGETSITE: " + ex.TargetSite +

                    "\nSTACKTRACE: " + ex.StackTrace +

                    "\nREFERER: " +referer;

 

        if(sendSysLogMessages)Sender.Send(Sender.PriorityType.Critical,DateTime.Now,strData);

 

        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("@EventId",eventId ));

                cmd.Parameters.Add(new SqlParameter("@Source", ex.Source));

                cmd.Parameters.Add(new SqlParameter("@LogDateTime", logDateTime));

                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));               

                cmd.ExecuteNonQuery();               

                cmd.Dispose();

                cn.Close();           

                }

                catch (Exception exc)

                {

                // database error, not much you can do here except for debugging

                  System.Diagnostics.Debug.WriteLine(exc.Message);

                }

                finally

                {

                cmd.Dispose();

                cn.Close();

                }              

            }

 

        string strEmails         =System.Configuration.ConfigurationSettings.AppSettings["emailAddresses"].ToString();

  if (strEmails.Length >0)

    {

     string[] emails = strEmails.Split(Convert.ToChar(";"));

     string    fromEmail=

           System.Configuration.ConfigurationSettings.AppSettings["fromEmail"].ToString();   

     string subject = "Web application error on " +System.Environment.MachineName;

     string detailURL=

           System.Configuration.ConfigurationSettings.AppSettings["detailURL"].ToString();

    string fullMessage=strData + detailURL +"?EvtId="+ eventId.ToString();   

   string SmtpServer =

          System.Configuration.ConfigurationSettings.AppSettings["smtpServer"].ToString();

                System.Web.Mail.MailMessage msg = new MailMessage();

                string ccs=String.Join(";",emails,1,emails.Length -1);

                msg.To =emails[0];

                msg.From =fromEmail;

                msg.Cc=ccs;

                msg.Body=fullMessage;

                msg.Subject =subject;

                try

                {                       

                    System.Web.Mail.SmtpMail.Send(msg);                   

                }

                catch (Exception excm )

                {

                    Debug.WriteLine(excm.Message);

                    // nothing worthwhile to do here other than for debugging.

                }

          }

    } // end method HandleException

   }

}

First, notice that everything in the class is static and the constructor is private so developers can't mistakenly try to create an instance of our class. At the begining, at class - level, we read our configuration items and store them so the HandleException method can use these.

Next, we gain access to the current HttpContext, and create a new GUID that will be used as the key in our LogItems SQL Server table. Then we assemble all the Referrer, the Form, datetime, and major properties of the exception object into a message "body".

If SysLog is turned on, we send all this out to the SysLog machine IP as a SysLog message. Next, we build our SqlCommand and insert our exception data.

Finally, we send out an email to all interested parties. It contains a link to our Report.aspx page (Wherever we have set that to be) with the EvtId (the GUID) on the querystring. The Report page simply makes that the "WHERE" clause of its SQL Statment, or it does a global SELECT on everything if there is nothing on the QueryString. This is so we can simply click the link in our email and instantly see the exact exception detail of what was just logged.

This is not sophisticated at all. Its' "Just enough" to get the job done. Hope it's helpful to you!

Here is a sample email:

SOURCE: ExceptionLogger
LogDateTime: 5/31/2006 1:33:37 PM
MESSAGE: Blah, blah, Humbug!
FORM:
QUERYSTRING:
TARGETSITE: Void Page_Load(System.Object, System.EventArgs)
STACKTRACE: at ExceptionLogger.WebForm1.Page_Load(Object sender, EventArgs e) in c:\csharpbin2\exceptionlogger\webform1.aspx.cs:line 21
at System.Web.UI.Control.OnLoad(EventArgs e)
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Page.ProcessRequestMain()
REFERER: http://localhost/exceptionlogger/report.aspx?EvtId=60483d68-56a3-4a9f-b291-9c6770bfb275

and, here is a sample of what you see when you click the hyperlink in the email:

My downloadable solution includes an additional "SearchExceptionLog" stored proc that is driven by a block of filters that looks like this on the page:

This includes a nice "Free" DatePicker control for the BeginDate and EndDate filters.

TIP: Don't "Swallow" Exceptions!

I have seen a number of instances where developers wrap exception - prone code in try catch blocks and do nothing in the catch block at all - essentially sending the exception, which carries useful information, into the "Black Hole". Please do not do this. If you don't know what to do with an exception, then don't catch it - let it go into the log as an unhandled exeception, where you'll have a lot more information about what caused it and where, and what you can do about it. But when you "swallow" exceptions by throwing them away, you have not only missed addressing the actual issue, you've made it doubly hard for others who must use and maintain the code that you wrote.

In almost every case, it is possible to code defensively, checking to see if an item is null before attempting to use it, for example, in order to avoid an exception. The practice of deliberately swallowing exceptions just to "get my code to work" is very bad coding practice, and should be avoided. Remember - we don't develop "in a vacuum"!

The download includes the SQL Script that will create the Table and the stored proc for the insert. The current configuration expects a SQL Server Database name of WebAppLogs; you'll need to create that database before running the SQL Script against it.

UPDATE (9/4/2006): I've updated this with a separate, enhanced Visual Studio 2005 solution download that adds new columns and handles the new Data Collection field in the .NET 2.0 Exception object.

Download the Visual Studio.NET 2003 Solution accompanying this article

Download the updated and enhanced Visual Studio 2005 solution


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.
Article Discussion: