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 hxxp://app-address/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 hxxp://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.

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

  1. Ney Alves Major July 28, 2017 / 09:05

    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 😉

  2. AR December 19, 2017 / 02:19

    This is great, thank you. How do we go about using additional URL query string params for further Elmah UI side filtering on the other columns like Host, Type, Error, User, Date/Time?

    • Sebastian Solnica December 19, 2017 / 10:58

      Thanks, this should be possible to implement. Unfortunately, it has been years since I used Elmah (this post is four years old) and it would take me a moment to catch up. If you happen to implement these queries please share them somewhere and drop a link in the comment 🙂

  3. Rajesh Chilambi January 4, 2019 / 19:06

    This works but does not filter by appname

Leave a reply to Sebastian Solnica Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.