Error Filtering

See also:

Introduction

When an unhandled exception is reported to ELMAH by ASP.NET, an application can decide whether to dismiss the exception or not. There are two ways for an application to do this, either programmatically or declaratively via the configuration file. The simpler of the two is programmatically because you do not need to learn anything new except write an event handler in your favorite language. The downside of the programmatic approach is that you need to write code and modify your web application (requiring possibly a static re-compile). With the configuration-based approach, you can simply apply filtering of exceptions to a running application.

Filtering Programmatically

Let’s assume the following:

  • You have the ErrorLogModule installed and configured to log errors.
  • The ErrorLogModule is named ErrorLog via the name attribute of the entry under the <httpModules> section.
  • You wish to filter exceptions of type System.Web.HttpRequestValidationException.

To add an exception filter programmatically, put the following in your Global.asax file (or in the code-behind file):

// Don't forget to reference the Elmah assembly and import its namespace.

void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs e)
{
    if (e.Exception.GetBaseException() is HttpRequestValidationException)
        e.Dismiss();
}

It’s really simple as that. When the error logging module receives an exception, it raises the Filtering event to see if any listeners wish to dismiss the exception. If an event handler calls the Dismiss method of the ExceptionFilterEventArgs object then the exception is not logged. Note that dismissing an exception does not clear the error so it still continues to surface as an unhandled exception. Dismissing here means that the module will not log it.

If you also have the ErrorMailModule enabled, the filtering procedure is similar. You setup another event handler based on the name of the module and the event:

void ErrorMail_Filtering(object sender, ExceptionFilterEventArgs e)
{
    if (e.Exception.GetBaseException() is HttpRequestValidationException)
        e.Dismiss();
}

Naturally, you can combine the two by delegating to a common method so that you have the set of conditions for filtering in one place:

void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs args)
{
    Filter(args);
}

void ErrorMail_Filtering(object sender, ExceptionFilterEventArgs args)
{
    Filter(args);
}

void Filter(ExceptionFilterEventArgs args)
{
    if (args.Exception.GetBaseException() is HttpRequestValidationException)
        args.Dismiss();
}

Filtering Declaratively via Configuration

Filtering via configuration is a bit more involved than the programmatic approach but the benefit is that you can apply it to any running web application without the need to modify or re-compile the application.

The first step is to configure an additional module called Elmah.ErrorFilterModule. Make sure you add it after any of the logging modules from ELMAH, as shown here with ErrorLogModule:

<httpModules>
    ...
    <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
    <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
    ...
</httpModules>

ErrorFilterModule takes care of subscribing to the Filtering event of the modules that precede it. When the logging and mailing modules fire their Filtering event, ErrorFilterModule runs through one or more assertions to decide whether to dismiss the exception or not. If the assertions test true, the exception is dismissed. The assertions are indicated in a separate configuration section. The configuration section is handled by Elmah.ErrorFilterSectionHandler and must be registered as shown here:

<configSections>
    ...
    <sectionGroup name="elmah">
        ...
        <section name="errorFilter" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
        ...
    </sectionGroup>
    ...
</configSections>

ASP.NET applications running under medium trust should use the following version for the section entry that specifies the additional requirePermission attribute and sets its value to false:

<section name="errorFilter" type="Elmah.ErrorFilterSectionHandler, Elmah" requirePermission="false" />

Now you can add the actual section under the <elmah> group:

<elmah>
    ...
    <errorFilter>
        <test>
            <equal binding="HttpStatusCode" value="404" type="Int32" />
        </test>
    </errorFilter>
</elmah>

In the above example, there is a single assertion that will be true when the HttpStatusCode equals the integer value 404. There are a lot of components at work here so let’s dissect this a bit more. ELMAH comes with a number of built-in assertions and you can even add your own. The most interesting of the assertions test their environment or context for a condition. If the condition is met, the assertion outcome is true. The context is usually the exception that is being raised and the HTTP transaction running its course. Both of these are available to the handler of the Filtering event as properties of the ExceptionFilterEventArgs. The ErrorFilterModule takes these two and adds a few more helper properties that are subsequently available as the total context for all assertions. The <equal> assertion above has a binding attribute that is actually a data-binding expression that you already know. It is the very same expression and syntax you specify to the Eval method when data-binding within the server-side markup of ASP.NET pages and user controls. This expression is evaluated against the context so naturally HttpStatusCode is a property of the context. The assertion evaluates the expression and uses the resulting value to compare against the literal given in the value attribute. To compare apples to apples and oranges to oranges, you have to specify the type of the value expected from the binding expression as well as that of the text in the value attribute. That is the purpose of the type attribute, which can be one of the TypeCode enumeration members. Consequently, the current set of comparison assertions are limited to primitive types represented by the TypeCode members (except Empty, DBNull and Object). This may seem limiting at first but it will suffice the majority of filtering you will need. For exotic cases, you can always write your own assertions. If you omit the type attribute then the value type of String is assumed. Finally, if the comparison succeeds, the assertion flags true and the exception is eventually dismissed. With this explanation in mind, here is a quick recap of what happens in the above example. First HttpStatusCode is evaluated against the filter context. Its result is converted to a 32-bit integer and compared against the value 404. If they equal, the assertion tells the the ErrorFilterModule that the exception matches the condition for dismissal.

As mentioned earlier, ELMAH ships with several built-in assertions and you can use them together to create complex conditions. For example, here the <and>, <greater> and <lesser> assertions are used together to filter all exceptions where the HTTP status code for the response falls in the integer range 400 to 499 (inclusive).

<elmah>
    ...
    <errorFilter>
        <test>
            <and>
                <greater binding="HttpStatusCode" value="399" type="Int32" />
                <lesser  binding="HttpStatusCode" value="500" type="Int32" />
            </and>
        </test>
    </errorFilter>
</elmah>

The <and> is a composite assertion that results to true when all its sub-assertions also result to true. Likewise, you also have the logical <or> and <not> composite assertions.

Using JScript

If you are familiar with the JavaScript programming language then you can also use the <jscript> assertion to specify a filtering condition as a simple JavaScript expression rather than using a whole tree of assertions. Below is an example of a complex condition expressed as JavaScript:

<elmah><errorFilter>
        <test>
            <jscript>
                <expression>
                <![CDATA[
                // @assembly mscorlib
                // @assembly System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
                // @import System.IO
                // @import System.Web

                $.HttpStatusCode == 404
                || $.BaseException instanceof FileNotFoundException 
                || $.BaseException instanceof HttpRequestValidationException
                /* Using RegExp below (see http://msdn.microsoft.com/en-us/library/h6e2eb7w.aspx) */
                || $.Context.Request.UserAgent.match(/crawler/i)                      
                || $.Context.Request.ServerVariables['REMOTE_ADDR'] == '127.0.0.1' // IPv4 only
                ]]>
                </expression>
            </jscript>
        </test>
    </errorFilter>
</elmah>

NOTE! For versions earlier than ELMAH 1.2 SP2, replace $ in the example above with the undocumented $context to work around issue #278.

The above causes errors to be filtered when any of the following is true:

Filtering By Source

With version 1.1, ELMAH allows filtering based on the source module, enabling scenarios like logging all errors but only having a subset being e-mailed. The three properties available for binding that enable this are called FilterSource, FilterSourceType and FilterSourceAssemblyName. FilterSource represents the object requesting filtering whereas FilterSourceType and FilterSourceAssemblyName are just helpers that yield the source object’s type and assembly name. Using any of these, you can set up assertions that filter errors based on aspects of the source object. The following example shows how to prevent having 404 HTTP errors being mailed.

<elmah>
    ...
    <errorFilter>
        <test>
            <and>
                <equal binding="HttpStatusCode" value="404" type="Int32" />
                <regex binding="FilterSourceType.Name" pattern="mail" />
            </and>
        </test>
    </errorFilter>
</elmah>

The regex assertion binds to the type name of the source object and checks if it contains the word “mail”. When the mailing module from ELMAH is filtering, FilterSource will be an instance of Elmah.ErrorMailModule. The assertion will therefore get the type name (irrespective of namespace) and filter the error if it contains “mail”. When the logging module from ELMAH is filtering, FilterSource will be an instance of Elmah.ErrorLogModule and the assertion will let the error pass through and get logged.

Writing Your Own Assertion

Writing your own assertions is a two-step process. First, you create a class that implements the IAssertion interface from the Elmah.Assertions namespace. This interface contains only a single method called Test that receives a context as parameter and which should return a Boolean result depending on whether the condition represented by the assertion is met or not. Next, you create a factory method that will be called to initialize the assertion as the configuration is applied at run-time.

Here is how the IAssertion interface is defined:

public interface IAssertion
{
    bool Test(object context);
}

And here is a very simple implementation of the interface that always returns true no matter what:

namespace MyAssertions
{
    public sealed class TrueAssertion : IAssertion
    {
        public bool Test(object context) 
        {
            return true;
        }
    }
}

If you were to add this assertion as an error filter test then you effectively end up suppressing exceptions from being logged or mailed. Granted it is not very useful in practice, but it should be good enough to see its effect in action.

As a second step, you need to setup a factory class for your assertions. The factory class is used to convert the configuration elements into IAssertion objects at runtime. Here is the assertion factory for creating TrueAssertion objects:

namespace MyAssertions
{
    public sealed class AssertionFactory
    {
        public static bool always_true(XmlElement config) 
        {
            return new TrueAssertion();
        }
    }
}

To keep things simple, the assertion factory class is required to follow a few conventions, which are:

  1. The factory class must be public
  2. The factory class must live in a namespace
  3. The factory class must be called AssertionFactory.

The factory class hosts factory methods, each of which is responsible for creating, configuring and returning an IAssertion object. Each factory method must observe the following rules:

  1. The factory method must be public.
  2. The factory method must be static (not bound to any class instance).
  3. The return value of the factory method must be IAssertion.
  4. The factory method must accept a single parameter of type XmlElement.
  5. The factory method must be named after the XML configuration element. An underscore may be used in the method name where a dash is expected in the XML element name such that not-equal becomes not_equal.

Now on to using the actual assertion in the configuration file:

<errorFilter>
    <test 
        xmlns:my="http://schemas.microsoft.com/clr/nsassem/MyAssertions/MyAssertionsLib">
        <my:always-true />
    </test>
</errorFilter>

Since our method was called always_true, it is a simple matter of naming the XML element correspondingly. In addition, you need to scope your element to an XML-based namespace whose value provides the necessary hints for the namespace and assembly where the factory class is located. In the above example, the my prefix corresponds to the XML namespace http://schemas.microsoft.com/clr/nsassem/MyAssertions/MyAssertionsLib and is used to scope always-true. Here is a deconstruction of how ELMAH converts the always-true element into an IAssertion object:

  1. The element prefix is used to lookup the corresponding XML namespace. If there is no prefix then the assertion is assumed to be one of the built-in ones provided by ELMAH.
  2. If the XML namesapce starts with http://schemas.microsoft.com/clr/nsassem/ then the remaining two path components are extracted as the type namespace followed by the assembly name where the assertion class factory can be found.
  3. The MyAssertionsLib assembly is loaded.
  4. The assembly is probed for the type MyAssertions.AssertionFactory. Here, AssertionFactory is simply dot-appended to the type namespace extracted from the XML namespace.
  5. The XML element name (always-true) is unmangled by replacing dashes with underscores and then used to find a public and static method on MyAssertions.AssertionFactory called always_true.
  6. Finally, the always_true method is invoked (after passing signature compatibility checks) and given the entire XML configuration element as its sole parameter so that it can extract further information needed for the configuration of the actual assertion.

In the sample case of TrueAssertion, there is no configuration to be done so the factory method simply creates a new instance and returns it. In fact, it wouldn’t even have to create a new instance each time since the assertion result is constant.

Assertions Reference

  • equal
  • greater
  • greater-or-equal
  • is-type
  • is-type-compatible
  • lesser
  • lesser-or-equal
  • not
  • not-equal
  • regex
  • jscript

To be completed.