.NET HttpModule to log all unhandled exceptions in IIS 7.5 through Log4NET (globally or per site)

Tags: Development C#

Ever wondered what exceptions are being thrown by code running on an IIS (7+) installation, without having to open the Event Viewer on that machine all the time? Today, I ran into a scenario like this. The bottom line is that I am preparing for the migration of many websites and applications to a new server environment. Although all applications have their own error handling, I really wanted to catch any additional unhandled exceptions and log them to a big screen to make our development teams aware of issues.

The solution

In the past, I've already been using HTTP Modules to do additional processing for all requests to IIS. A HttpModule is an assembly that is injected into IIS' pipeline and is executed for all requests or for .NET requests (depending on the configuration). The nice thing about a HttpModule is that it can do it's processing on any moment during the request, before, during or after. A HttpModule can also catch an OnError event that communicates unhandled exceptions. Why not just use global.asax' OnError event? You could. But a HttpModule can be installed globally, so that it will fire for all websites managed by that IIS installation.

Below, you can find the full source and the compiled assembly. Detailed instructions on how to use the module are included with the source. As the HttpModule uses Log4NET, you can use any of it's appenders to do the actual logging. There's nothing preventing you from configuring the module to e-mail any unhandled exceptions, send them to a UDP address, save them to a database or something else. In our case, I logged the messages to a special console application (C#, very quickly written so not suited for this blogpost right now) that is running in our 'server migration headquarters':

Prerequisites

  • IIS 7+ installation with .NET 3.5 (at least) installed. You can compile the source for .NET 2.0, but you'll have to do that yourself;
  • Some knowledge of Log4NET and using appenders. See here for more information;
  • IIS Express installed on your development machine (not required when you don't want to test the module locally);
  • MVC3 installed if you wish to test the HttpModule locally (the test website is MVC3);

The HttpModule itself

As you can see below, the HttpModule is straightforward. It attaches an event handler to the OnError event in the Init method (that is called by IIS when the HttpModule is instantiated). The OnError event handler, when it is fired, extracts basic information about the user (IP and Url) and details about the last exception that occured on the webserver that was not handled. The exception is logged through Log4NET, based on the configuration in log4net.config.

/// <summary&gt;
    /// The HttpModule catches any unhandled exception by IIS and passes it to Log4NET. 
    /// </summary&gt;
    /// <remarks&gt;
    /// Logging can be disabled by setting 'LogUnhandledExceptions' in app.config or web.config to 'false'. Alternatively, the HttpModule
    /// can simply be removed. It is possible to install the module on IIS as a global managed module, so that all unhandled exceptions
    /// for all methods can be logged. Use the files in the \Install folder to see how.
    /// </remarks&gt;
    public class ErrorHttpModule : IHttpModule
    {
        private bool logUnhandeldExceptions;

        public void Init(HttpApplication context)
        {
            bool success = bool.TryParse(ConfigurationManager.AppSettings["LogUnhandledExceptions"], out logUnhandeldExceptions);
            if (!success) { logUnhandeldExceptions = true; }

            context.Error += new EventHandler(OnError);
        }

        private void OnError(object sender, EventArgs e)
        {
            try
            {
                if (!logUnhandeldExceptions) { return; }

                string userIp;
                string url;
                string exception;

                HttpContext context = HttpContext.Current;

                if (context != null)
                {
                    userIp = context.Request.UserHostAddress;
                    url = context.Request.Url.ToString();

                    // get last exception, but check if it exists
                    Exception lastException = context.Server.GetLastError();

                    if (lastException != null)
                    {
                        exception = lastException.ToString();
                    }
                    else
                    {
                        exception = "no error";
                    }
                }
                else
                {
                    userIp = "no httpcontext";
                    url = "no httpcontext";
                    exception = "no httpcontext";
                }

                Logging.Instance.Error("Unhandled exception occured. UserIp [{0}]. Url [{1}]. Exception [{2}]", userIp, url, exception);
            }
            catch (Exception ex)
            {
                Logging.Instance.Error("Exception occured in OnError: [{0}]", ex.ToString());
            }
        }

        public void Dispose()
        {
        }
    }

For our environment, the configuration for log4NET is as follows:

<?xml version="1.0">
<log4net>
  <appender name="UdpAppender" type="log4net.Appender.UdpAppender">
    <param name="RemoteAddress" value="logging.nowonlineutrecht.nl"/>
    <param name="RemotePort" value="8081"/>
    <layout type="log4net.Layout.PatternLayout" value="NowOnline ErrorHandling - %property{log4net:HostName} - %level - %date{MM/dd HH:mm:ss} - %type - %M - %message"/>
  </appender>
  <root>
    <level value="ALL" />
    <appender-ref ref="UdpAppender"/>
  </root>
</log4net>

Log4NET's configuration is stored in a separate log4net.config file that is compiled as an embedded resource with the HttpModule. Although this might be a bit inflexible, it avoids having to add the XML configuration into the machine.config for IIS. For some measure of flexibility, I configured the RemoteAddress in the UDP Appender to send the packets to a DNS entry for our domain which resolves to the IP address (on the VPN) of our logging display.

Installing it

Before actually deploying the application, you'll have to change the Log4NET configuration to suite your needs and recompile the project to embed the Log4NET configuration into the HttpModule assembly. Also, you should create your own signing key to sign the HttpModule with if you wish the deploy the module globally. In that case, you're going to register the assembly in the Global Assembly Cache (GAC), which requires a strong name (or signed) assembly.

Option 1: Deploy per website

<system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules runAllManagedModulesForAllRequests="true">
       <add name="NowOnlineErrorHandlingModule" type="NowOnline.Services.ErrorHandling.ErrorHttpModule, nowonline.services.errorhandling"/>
   </modules>
</system.webServer>
  • Simply add a reference to the nowonline.services.errorhandling.dll assembly to your website project (MVC, WebForms or other);
  • Add to above XML to your web.config the XML to register the HTTP Module in the IIS pipeline;
  • Publish the application

Option 2: Deploy globally on IIS

  • Upload the Release publishes of nowonline.services.errorhandling.dll and log4net.dll into the Install folder in the project;
  • Copy all the files in that folder to the desired webserver;
  • Run the install.bat as administrator. Log4net.dll and nowonline.services.errorhandling.dll should be registered in the global assembly cache (GAC);
  • In IIS, go to Modules (for the server as a whole, not one website in particular). Choose to add a managed module;
  • Select nowonline.services.errorhandling.dll;
  • The module should now be run for every request, which means that all unhandled exceptions will be logged through the log4net.config settings;

Testing in Visual Studio 

The errorhandling module can be tested in Visual Studio. Just run the website in the solution. Beware that this requires IIS Express to be installed and configured as the main development server. If you wish to use the normal development server for Visual Studio, read here how to install a HttpModule for older IIS. See http://msdn.microsoft.com/en-us/library/ms227673.aspx for more information.

Good to know

  • The firewall on your server, if you wish to use the UDP appender. In the example, port 8081 (outbound) should be allowed.
  • You can disable the HttpModule globally by adding LogUnhandledExceptions to AppSettings (per site or globally, in machine.config) and setting it to 'false';

Source

Download the source here:
https://bitbucket.org/cverwijs/examples.httploggingmodule

Christiaan Verwijs
Christiaan Verwijs

Scrum Master, Trainer, Developer & founder of Agilistic