Making elmah.axd a log viewer for multiple applications


Elmah is a great exception logger for web applications. Next to the exception data (stacktrace, exception message etc.) it collects detailed request and runtime information. If I also mention that it’s easy to install and highly configurable it comes with no surprise that it became an industrial logging standard for .NET web applications. Elmah gives you a great choice of places where you can send your logs, though a database is the one I consider most useful. What I also value in Elmah is its log viewer (elmah.axd) – it has a nice exception list and a great exception details screen – I wish the ASP.NET Yellow Screen of Death had that look :). The only thing I don’t really like in this viewer is a fact that by default it is bound to the application that has Elmah installed and is accessible only under http://<app-url>/elmah.axd. If you do not secure access to this handler everyone can see detailed information on your code and environment – just have a look how many developers forget about it. In this post I will show you how to remove the elmah.axd handler from your application and how to create a separate ASP.NET application that would be a central Elmah log viewer for your applications.

First part is simple, just remove highlighted lines from your web.config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.web>
        <httpHandlers>
            <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
        </httpHandlers>
    </system.web>
    <system.webServer>
        <handlers>
            <add name="Elmah" path="elmah.axd" verb="POST,GET,HEAD" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
        </handlers>
    </system.webServer>
</configuration>

Now, the second part :). Let’s create an empty ASP.NET web site and install the Elmah Core Library (no config) package from the online Nuget repository. Then let’s add an App_Code folder and create a new file in it (I named it GeneralErrorLogPageFactory.cs):

using System;
using System.Reflection;
using System.Web;
using System.Web.Configuration;
using Elmah;

namespace LowLevelDesign
{
    public class GeneralErrorLogPageFactory : ErrorLogPageFactory
    {
        public override IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
        {
            var appname = context.Request.Params["app"];
            if (String.IsNullOrEmpty(appname))
            {
                // appname was not found - let's check the referrer
                if (context.Request.UrlReferrer != null && !"/stylesheet".Equals(context.Request.PathInfo, StringComparison.OrdinalIgnoreCase))
                {
                    appname = HttpUtility.ParseQueryString(context.Request.UrlReferrer.Query)["app"];
                    context.Response.Redirect(String.Format("{0}{1}app={2}", context.Request.RawUrl,
                                                            context.Request.Url.Query.Length > 0 ? "&" : "?", appname));
                }
            }

            IHttpHandler handler = base.GetHandler(context, requestType, url, pathTranslated);
            var err = new MySqlErrorLog(WebConfigurationManager.ConnectionStrings["MySqlTraceConnString"].ConnectionString);
            err.ApplicationName = appname ?? "Not found";

            // we need to fool elmah completely
            object v = typeof(ErrorLog).GetField("_contextKey", BindingFlags.NonPublic | BindingFlags.Static)
                                            .GetValue(null);
            context.Items[v] = err;

            return handler;
        }
    }
}

As you can see from the above snippet we created a class that inherits from Elmah.ErrorLogPageFactory which implements System.Web.IHttpHandlerFactory. This class will be responsible for creating a handler for each request to the elmah.axd address. In order to filter the logs for a given application I need to get its name from the request parameters. If there comes a request with no app parameter I try to deduce it from the RefererUrl and make a redirect, otherwise I fail and set the application name to Not found. In next lines we replace the ErrorLog that Elmah is using with the one specific to our application. In my case I have all logs in a MySql database so I just create an instance of the MySqlErrorLog class with the application name retrieved from the request parameter. When Elmah tries to get the default ErrorLog to save the exception log (or read it) it first checks the HttpContext.Items collection for an item with a key equal to ErrorLog._contextKey. If it does not find it, it parses the configuration file. That is why we are replacing this item with our own ErrorLog instance. ErrorLog._contextKey is a private static field so the only way to get its value is through reflection. Last step is to add our handler to the web.config file (as well as its connection string):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="MySqlTraceConnString" connectionString="Data Source=localhost;Initial Catalog=DiagnosticsDB;User Id=test;Password=test" />
  </connectionStrings>
  <system.web>
    <compilation debug="false" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <httpHandlers>
      <add verb="POST,GET,HEAD" path="elmah.axd" type="LowLevelDesign.GeneralErrorLogPageFactory" />
    </httpHandlers>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <handlers>
      <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="LowLevelDesign.GeneralErrorLogPageFactory" preCondition="integratedMode" />
    </handlers>
  </system.webServer>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="MySql.Data" publicKeyToken="c5687fc88969c44d" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-6.6.4.0" newVersion="6.6.4.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

You can now view logs from any application by opening http://<your-app-url>/elmah.axd?app=<app-name >. Finally you may create a default page with a list of your applications. The sample log viewer can be downloaded from my .NET Diagnostics Toolkit page.

4 thoughts on “Making elmah.axd a log viewer for multiple applications

  1. I must thank you for the efforts you have put in writing this blog.
    I am hoping to view the same high-grade content from you in the future as well.
    In fact, your creative writing abilities has inspired me to
    get my very own site now 😉

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s