Technology Toolbox

Your technology Sherpa for the Microsoft platform

Jeremy Jameson - Founder and Principal

Search

Search

Logging Exceptions in .NET Applications

Note
This post originally appeared on my MSDN blog:

Since I no longer work for Microsoft, I have copied it here in case that blog ever goes away.

Last summer I wrote a post introducing my simple, but highly effective approach to logging -- including a Logger class that is really just a thin wrapper around the System.Diagnostics.TraceSource class.

A few months ago, I enhanced the Logger class to log exceptions in a consistent fashion.

I used the "Yellow Page of Death" provided by ASP.NET as a reference for logging the details of the exception. Here's a screenshot from a sample that I whipped up this morning:

ASP.NET error page
Figure 1: ASP.NET error page

The corresponding code-behind from the page that I used to generate the above screenshot is listed below:

using System;

using Fabrikam.Demo.CoreServices.Logging;

namespace Fabrikam.Demo.Web.UI.Logging
{
    public partial class UnhandledExceptionPage : System.Web.UI.Page
    {
        protected void Page_Load(
            object sender,
            EventArgs e)
        {
            DoSomethingBad();
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Performance",
            "CA1804:RemoveUnusedLocals",
            MessageId = "divideByZero")]
        private static void DoSomethingBad()
        {
            int numerator = 1;
            int denominator = 0;

            try
            {
                int divideByZero = numerator / denominator;
            }
            catch (DivideByZeroException ex)
            {
                string errorMessage = "Something bad happened.";
                Logger.LogError(errorMessage);

                throw new InvalidOperationException(
                    errorMessage,
                    ex);
            }
        }
    }
}

A couple of interesting notes about the ASP.NET error page:

  • The error message displayed at the top of the page is actually from the innermost exception (i.e. the "base" exception).
  • The stack trace shows the details for the actual exception that occurred as well as any inner exceptions.

This makes perfect sense when you think about it because when you start investigating an error, it's best to begin with the crux of the problem. In other words, for this example, it's much better start off with the "Attempted to divide by zero" message from the DivideByZeroException, rather than the "Something bad happened" message from the InvalidOperationExeption.

In order to log exceptions, I added two new overloads for the LogError method:

        /// <summary>
        /// Logs the specified exception to the trace listeners.
        /// </summary>
        /// <param name="ex">The exception to log.</param>
        public static void LogError(
            Exception ex)
        {
            LogError(ex, null);
        }

        /// <summary>
        /// Logs the specified exception to the trace listeners.
        /// </summary>
        /// <remarks>The details of the exception are logged using a format
        /// similar to the ASP.NET error page. Information about the base
        /// exception is logged first, followed by information for any
        /// "outer" exceptions.</remarks>
        /// <param name="ex">The exception to log.</param>
        /// <param name="requestUrl">The URL of the web request for which the
        /// exception occurred.</param>
        public static void LogError(
            Exception ex,
            Uri requestUrl)
        {
            if (ex == null)
            {
                throw new ArgumentNullException("ex");
            }

            StringBuilder buffer = new StringBuilder();

            if (requestUrl == null)
            {
                buffer.Append(
                    "An error occurred in the application.");
            }
            else
            {
                buffer.Append(
                    "An error occurred during the execution of the"
                    + " web request.");
            }

            buffer.Append(
                " Please review the stack trace for more information about the"
                + " error and where it originated in the code.");

            buffer.Append(Environment.NewLine);
            buffer.Append(Environment.NewLine);

            if (requestUrl != null)
            {
                buffer.Append("Request URL: ");
                buffer.Append(requestUrl.AbsoluteUri);

                buffer.Append(Environment.NewLine);
                buffer.Append(Environment.NewLine);
            }

            Exception baseException = ex.GetBaseException();
            Type baseExceptionType = baseException.GetType();

            buffer.Append("Exception Details: ");
            buffer.Append(baseExceptionType.FullName);
            buffer.Append(": ");
            buffer.Append(baseException.Message);

            buffer.Append(Environment.NewLine);
            buffer.Append(Environment.NewLine);

            buffer.Append("Stack Trace:");
            buffer.Append(Environment.NewLine);
            buffer.Append(Environment.NewLine);

            AppendExceptionDetail(ex, buffer);

            string message = buffer.ToString();
            Log(TraceEventType.Error, message);
        }

Note that the stack trace is generated using the AppendExceptionDetail helper method.

This makes it really easy to log exceptions in ASP.NET Web applications -- including solutions built on Microsoft Office SharePoint Server (MOSS) 2007 and Windows SharePoint Services -- as well as other types of .NET applications (e.g. console applications).

Here is the complete source for the updated Logger class:

#define TRACE

using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;

namespace Fabrikam.Demo.CoreServices.Logging
{
    /// <summary>
    /// Exposes static methods for logging various events (e.g. debug,
    /// informational, warning, etc.). This class cannot be inherited.
    /// </summary>
    /// <remarks>
    /// All methods of the <b>Logger</b> class are static
    /// and can therefore be called without creating an instance of the class.
    /// </remarks>
    public static class Logger
    {
        private static TraceSource defaultTraceSource =
            new TraceSource("defaultTraceSource");

        private static void AppendExceptionDetail(
            Exception ex,
            StringBuilder buffer)
        {
            Debug.Assert(ex != null);
            Debug.Assert(buffer != null);

            if (ex.InnerException != null)
            {
                AppendExceptionDetail(ex.InnerException, buffer);
            }

            Type exceptionType = ex.GetType();

            buffer.Append('[');
            buffer.Append(exceptionType.Name);
            buffer.Append(": ");
            buffer.Append(ex.Message);
            buffer.Append(']');
            buffer.Append(Environment.NewLine);

            // Note: Exception.StackTrace includes both _stackTrace and
            // _remoteStackTrace, so use System.Diagnostics.StackTrace
            // to retrieve only the portion we want to output
            StackTrace trace = new StackTrace(ex);
            buffer.Append(trace.ToString());

            buffer.Append(Environment.NewLine);
        }

        /// <summary>
        /// Flushes all the trace listeners in the trace listener collection.
        /// </summary>
        public static void Flush()
        {
            defaultTraceSource.Flush();
        }

        /// <summary>
        /// Logs an event to the trace listeners using the specified
        /// event type and message.
        /// </summary>
        /// <param name="eventType">One of the System.Diagnostics.TraceEventType
        /// values that specifies the type of event being logged.</param>
        /// <param name="message">The message to log.</param>
        public static void Log(
            TraceEventType eventType,
            string message)
        {
#if DEBUG
            // Some debug listeners (e.g. DbgView.exe) don't buffer output, so
            // Debug.Write() is effectively the same as Debug.WriteLine().
            // For optimal appearance in these listeners, format the output
            // for a single call to Debug.WriteLine().
            StringBuilder sb = new StringBuilder();

            sb.Append(eventType.ToString());
            sb.Append(": ");
            sb.Append(message);

            string formattedMessage = sb.ToString();
            Debug.WriteLine(formattedMessage);
#endif

            defaultTraceSource.TraceEvent(eventType, 0, message);
        }

        /// <summary>
        /// Logs a debug event to the trace listeners using the specified
        /// format string and arguments.
        /// </summary>
        /// <param name="provider">An System.IFormatProvider that supplies
        /// culture-specific formatting information.</param>
        /// <param name="format">A composite format string.</param>
        /// <param name="args">An System.Object array containing zero or more
        /// objects to format.</param>
        public static void LogDebug(
            IFormatProvider provider,
            string format,
            params object[] args)
        {
            string message = string.Format(
                provider, format, args);

            LogDebug(message);
        }

        /// <summary>
        /// Logs a debug event to the trace listeners.
        /// </summary>
        /// <param name="message">The message to log.</param>
        public static void LogDebug(
            string message)
        {
            Log(TraceEventType.Verbose, message);
        }

        /// <summary>
        /// Logs a critical event to the trace listeners using the specified
        /// format string and arguments.
        /// </summary>
        /// <param name="provider">An System.IFormatProvider that supplies
        /// culture-specific formatting information.</param>
        /// <param name="format">A composite format string.</param>
        /// <param name="args">An System.Object array containing zero or more
        /// objects to format.</param>
        public static void LogCritical(
            IFormatProvider provider,
            string format,
            params object[] args)
        {
            string message = string.Format(
                provider, format, args);

            LogCritical(message);
        }

        /// <summary>
        /// Logs a critical event to the trace listeners.
        /// </summary>
        /// <param name="message">The message to log.</param>
        public static void LogCritical(
            string message)
        {
            Log(TraceEventType.Critical, message);
        }

        /// <summary>
        /// Logs an error event to the trace listeners using the specified
        /// format string and arguments.
        /// </summary>
        /// <param name="provider">An System.IFormatProvider that supplies
        /// culture-specific formatting information.</param>
        /// <param name="format">A composite format string.</param>
        /// <param name="args">An System.Object array containing zero or more
        /// objects to format.</param>
        public static void LogError(
            IFormatProvider provider,
            string format,
            params object[] args)
        {
            string message = string.Format(
                provider, format, args);

            LogError(message);
        }

        /// <summary>
        /// Logs the specified exception to the trace listeners.
        /// </summary>
        /// <param name="ex">The exception to log.</param>
        public static void LogError(
            Exception ex)
        {
            LogError(ex, null);
        }

        /// <summary>
        /// Logs the specified exception to the trace listeners.
        /// </summary>
        /// <remarks>The details of the exception are logged using a format
        /// similar to the ASP.NET error page. Information about the base
        /// exception is logged first, followed by information for any
        /// "outer" exceptions.</remarks>
        /// <param name="ex">The exception to log.</param>
        /// <param name="requestUrl">The URL of the web request for which the
        /// exception occurred.</param>
        public static void LogError(
            Exception ex,
            Uri requestUrl)
        {
            if (ex == null)
            {
                throw new ArgumentNullException("ex");
            }

            StringBuilder buffer = new StringBuilder();

            if (requestUrl == null)
            {
                buffer.Append(
                    "An error occurred in the application.");
            }
            else
            {
                buffer.Append(
                    "An error occurred during the execution of the"
                    + " web request.");
            }

            buffer.Append(
                " Please review the stack trace for more information about the"
                + " error and where it originated in the code.");

            buffer.Append(Environment.NewLine);
            buffer.Append(Environment.NewLine);

            if (requestUrl != null)
            {
                buffer.Append("Request URL: ");
                buffer.Append(requestUrl.AbsoluteUri);

                buffer.Append(Environment.NewLine);
                buffer.Append(Environment.NewLine);
            }

            Exception baseException = ex.GetBaseException();
            Type baseExceptionType = baseException.GetType();

            buffer.Append("Exception Details: ");
            buffer.Append(baseExceptionType.FullName);
            buffer.Append(": ");
            buffer.Append(baseException.Message);

            buffer.Append(Environment.NewLine);
            buffer.Append(Environment.NewLine);

            buffer.Append("Stack Trace:");
            buffer.Append(Environment.NewLine);
            buffer.Append(Environment.NewLine);

            AppendExceptionDetail(ex, buffer);

            string message = buffer.ToString();
            Log(TraceEventType.Error, message);
        }

        /// <summary>
        /// Logs an error event to the trace listeners.
        /// </summary>
        /// <param name="message">The message to log.</param>
        public static void LogError(
            string message)
        {
            Log(TraceEventType.Error, message);
        }

        /// <summary>
        /// Logs an informational event to the trace listeners using the specified
        /// format string and arguments.
        /// </summary>
        /// <param name="provider">An System.IFormatProvider that supplies
        /// culture-specific formatting information.</param>
        /// <param name="format">A composite format string.</param>
        /// <param name="args">An System.Object array containing zero or more
        /// objects to format.</param>
        public static void LogInfo(
            IFormatProvider provider,
            string format,
            params object[] args)
        {
            string message = string.Format(
                provider, format, args);

            LogInfo(message);
        }

        /// <summary>
        /// Logs an informational event to the trace listeners.
        /// </summary>
        /// <param name="message">The message to log.</param>
        public static void LogInfo(
            string message)
        {
            Log(TraceEventType.Information, message);
        }

        /// <summary>
        /// Logs a warning event to the trace listeners using the specified
        /// format string and arguments.
        /// </summary>
        /// <param name="provider">An System.IFormatProvider that supplies
        /// culture-specific formatting information.</param>
        /// <param name="format">A composite format string.</param>
        /// <param name="args">An System.Object array containing zero or more
        /// objects to format.</param>
        public static void LogWarning(
            IFormatProvider provider,
            string format,
            params object[] args)
        {
            string message = string.Format(
                provider, format, args);

            LogWarning(message);
        }

        /// <summary>
        /// Logs a warning event to the trace listeners.
        /// </summary>
        /// <param name="message">The message to log.</param>
        public static void LogWarning(
            string message)
        {
            Log(TraceEventType.Warning, message);
        }
    }
}

In a follow-up post, I'll cover how to handle errors in SharePoint Web applications without necessarily adding a bunch or try/catch blocks, while still avoiding the out-of-the-box SharePoint error page (which doesn't look very good on an Internet-facing site).

Update (2011-01-31)
I've attached a sample Visual Studio solution that demonstrates the Logger class.

Attachment

Demo (LoggingAndDiagnostics).zip (41.3 KB)

Comments

  1. # re: Logging Exceptions in .NET Applications

    March 21, 2010 3:50 AM
    Hatim

    Hey Jeremy,

    Great Post. I believe proper exception handling and logging is the most obscure and unmastered subject for beginners to intermediate developers. At least that's the impression I get from interviews I conduct.

  2. # re: Logging Exceptions in .NET Applications

    March 21, 2010 10:05 AM
    krg

    Why not use the Enterprise Library instead?

  3. # re: Logging Exceptions in .NET Applications

    March 21, 2010 4:52 PM
    SallyT

    Why not just use Elmah or log4net?

  4. # re: Logging Exceptions in .NET Applications

    March 22, 2010 7:52 AM
    Jeremy Jameson
    Gravatar

    I haven't used Enterprise Library in years. You are certainly welcome to use it instead, but I find the System.Diagnostics stuff introduced in .NET 2.0 to be much easier to implement on a project. [My general opinion about the Enterprise Library is certainly much stronger than what I've briefly mentiond here, but I'll leave it at that ;-) ]

    As for log4net -- honestly, I never used this on any projects primarily because of "open source" and supportability issues (at Microsoft Consulting Services, we have to jump through some hoops in order to use any third party code on customer projects that we lead). One of the customers that I worked with years ago actually used log4net prior to engaging with MCS, but having already implemented it in their solution mitigated the supportability and open source issues.

    I looked ever so briefly at ELMAH about a year ago (if memory serves) shortly after it was mentioned on Scott Hanselman's blog, but again, I've never used it "in the real world" on a project that I've been involved with. Has anyone used it in conjunction with SharePoint? That would be my primary concern (i.e. it either doesn't work or isn't supported when used on a SharePoint site). There's also the issue of supportability with ELMAH as well (since I believe it's also an open source project).

  5. # re: Logging Exceptions in .NET Applications

    March 22, 2010 4:46 PM
    Mark

    Jeremy, since log4net is OSS, you can support it and "claim it" as your own.  It is not 3rd party software, which i can definitely understand getting permission for, especially since you don't have the source. It is much better than just rolling your own.

    But rules are rules. Silly and short-sighted as they might be.

  6. # re: Logging Exceptions in .NET Applications

    March 23, 2010 10:52 AM
    Jeremy Jameson
    Gravatar

    Hmm...evidently through this post, I've provided kindling for some religious debate ;-)

    Apparently I should have pointed out that it was never my intent that customers currently using log4net or some other logging infrastructure (e.g. Enterprise Library) should rip and replace it with my, admittedly, very simple Logger class. If you like what you are currently using, then by all means, keep using it.

    In case you haven't seen my original post about logging (http://blogs.msdn.com/jjameson/archive/2009/06/18/a-simple-but-highly-effective-approach-to-logging.aspx), here's a small excerpt:

    "A common question that frequently arises both with customers and fellow consultants is what do I recommend for logging? [...]

    However, with numerous options available -- e.g. log4net, Enterprise Library, Common.Logging, SharePoint ULS, etc. -- it's no wonder this is frequently a "hot" topic.

    Several years ago, I created a simple, but highly effective approach to logging based on the (then) new System.Diagnostics features in the .NET Framework version 2.0."

    ...oh, and yes, our legal department definitely considers log4net to be "3rd party software" ;-)

    However, that doesn't necessarily mean the Microsoft consultants are forbidden from using log4net on a project. Just because I've never recommended it for a project certainly doesn't mean that nobody from MCS has.

  7. # re: Logging Exceptions in .NET Applications

    March 15, 2011 3:52 AM
    Mike

    Hi Jeremy

    I used System.Diagnostics on my dev box amd it worked fine but on live production server it created problems.We need to change some registry keys or play around with the users permissions apparently so i ditched it and used Log4net intead.

  8. # re: Logging Exceptions in .NET Applications

    March 15, 2011 7:10 AM
    Jeremy Jameson
    Gravatar

    Mike,

    Without additional details, it's impossible to say for sure what the issue was that you encountered in Production, but I'm guessing it was caused by trying to log to the Windows event log using a custom "Source" and a low-privilege service account. I've used this configuration in no less than a dozen different customer solutions over the years and it's always worked fine for me. However, you need to be sure that any custom event sources are created as part of the deployment process (which is performed by an administrator).

    There are several different ways to create a custom event source -- it's as easy as running one line of .NET code. Refer to the following post for a sample PowerShell script that creates a custom "Fabrikam Demo Site" event source (Add Event Log Sources.ps1):

    blogs.msdn.com/.../deployment-scripts-for-sharepoint-server-2010.aspx

    Note that the custom event source must be registered on each Web server in the farm (or on whichever servers are attempting to write to the event log with the custom source).

Add Comment

Optional, but recommended (especially if you have a Gravatar). Note that your email address will not appear with your comment.
If URL is specified, it will be included as a link with your name.

To prevent spam from being submitted, please select the following fruit: Pear

Strawberry
Cherries
Apple
Watermelon
Pear
Grapes
 
Please add 1 and 4 and type the answer here: