Click here to Skip to main content
15,881,803 members
Articles / Web Development / ASP.NET

Error Logging to the Windows Event Log using ELMAH

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
5 Mar 2012Apache5 min read 25.4K   552   6  
Error Logging to the Windows Event Log using ELMAH

Table of Contents

Introduction

One of the popular libraries for logging of unhandled errors in ASP.NET applications is ELMAH. It has many advantages, including convenience of using and easiness of integration to a project. And there are many articles with explanation of how to use ELMAH.

But there is one thing which ELMAH cannot do. It’s a saving error to the Windows Event Log. ELMAH can save error to text files, or to database, or store in memory, or sent error by e-mail. And it has a built-in web-interface for errors viewing. But you’ll admit that a saving to the Event Log is a more universal way. Especially as there would be errors related to writing to a database or to a file. And, if you choose memory storage in ELMAH, this storage is temporary and a buffer is limited.

In our project, we want that ELMAH stores errors in both database and EventLog.

Choosing a Solution

There are two options how to extend ELMAH library to save error to the EventLog.

Option 1

You can implement a custom logging provider inheriting from Elmah.ErrorLog class and overriding GetError(), GetErrors(), and Log() methods. In this case, it will work as well as other ELMAH logging providers which save errors to database, file or memory. But there are two problems.

The first is related that ELMAH can run only one logging provider at a time. ELMAH logging provider not only writes errors but also reads them to show via its web-interface. Therefore, you need specify only one source of storing and reading.

The second is related that ELMAH stores error information in its own XML format. It’s needed to storing as much structured information as possible for future showing in the web-interface. Therefore, you also need to store error information to the EventLog in the ELMAH XML format if you will implement your custom logging provider. But it’s not convenient for a human reading. And, in my opinion, you will lose a meaning of your solution because just people (programmers, administrators, etc.) usually view the EventLog.

However, there is such solution. It’s described in the article “EventLog based Error Logging using ELMAH” of Deba Khadanga.

Option 2

Thankfully, there is an ErrorMailModule in the ELMAH library. This module doesn’t write or read errors anywhere, but just sends information about errors by e-mail. And it can work with logging providers simultaneously. So, we will use it as a base for our solution.

Implementation

Source code of the ELMAH library is available on its download page. You can familiarize yourself with the Elmah.ErrorMailModule class. But we will implement our own ElmahErrorEventLogModule class on its base.

To begin with, we will implement some additional classes which we will use for a reading of configuration parameters. There already is an Elmah.Configuration class in the ELMAH library. But it is modified as internal sealed and we cannot use it. Therefore, we will implement our own ElmahConfiguration class which with a copy of Elmah.Configuration. And will add some other methods from the Elmah.ErrorMailModule class related to configuration reading (on example GetSetting(), etc.).

C#
/// <summary>
/// Get the configuration from the "elmah" section of the configuration file.
/// </summary>
public class ElmahConfiguration
{
    internal const string GroupName = "elmah";
    internal const string GroupSlash = GroupName + "/";

    public ElmahConfiguration() { }

    public static NameValueCollection AppSettings
    {
        get
        {
            return ConfigurationManager.AppSettings;
        }
    }

    public static object GetSubsection(string name)
    {
        return GetSection(GroupSlash + name);
    }

    public static object GetSection(string name)
    {
        return ConfigurationManager.GetSection(name);
    }

    public static string GetSetting(IDictionary config, string name)
    {
        return GetSetting(config, name, null);
    }

    public static string GetSetting(IDictionary config, string name, string defaultValue)
    {
        string value = NullString((string)config[name]);

        if (value.Length == 0)
        {
            if (defaultValue == null)
            {
                throw new Elmah.ApplicationException(string.Format(
                    "The required configuration setting '{0}'
                    is missing for the error eventlog module.", name));
            }

            value = defaultValue;
        }

        return value;
    }

    public static string NullString(string s)
    {
        return s == null ? string.Empty : s;
    }
}

We also need a SectionHandler class for reading of <elmah> section from a configuration file. Yet again, we will implement our own ElmahErrorEventLogSectionHandler class by analogy with Elmah.ErrorMailSectionHandler. Happily, it’s very small and takes up only one line.

C#
/// <summary>
/// Handler for the "elmah/errorEventLog" section of the configuration file.
/// </summary>
public class ElmahErrorEventLogSectionHandler : SingleTagSectionHandler { }

Now enter upon an implementation of the main ElmahErrorEventLogModule class.

Let’s declare an eventLogSource variable which will store a name of event source in the EventLog. We will read this name from a configuration file. And let’s also declare a delegate of Elmah.ExceptionFilterEventHandler type which can be used for an additional filtration.

C#
private string eventLogSource;
public event Elmah.ExceptionFilterEventHandler Filtering;

Now let’s implement an OnInit() method for the module initialization. It uses the ElmahConfiguration class for configuration reading. Then it checks if the event source is already registered in the EventLog. If not, it tries to register the source. Unfortunately, it requires administration rights for an application. But ASP.NET applications usually don’t have such rights. Therefore, you need to register your event source manually before. You need to execute the following command in Windows with administration rights for it:

eventcreate /ID 1 /L APPLICATION /T INFORMATION 
/SO "your_eventLog_source_name" /D "Registering"

And, after all checks are done, it registers an event handler of the module.

C#
/// <summary>
/// Initializes the module and prepares it to handle requests.
/// </summary>
protected override void OnInit(HttpApplication application)
    {
        if (application == null)
            throw new ArgumentNullException("application");

        // Get the configuration section of this module.
        // If it's not there then there is nothing to initialize or do.
        // In this case, the module is as good as mute.
        IDictionary config =
        (IDictionary)ElmahConfiguration.GetSubsection("errorEventLog");
        if (config == null)
            return;

        // Get settings.
        eventLogSource = ElmahConfiguration.GetSetting
        (config, "eventLogSource", string.Empty);
        if (string.IsNullOrEmpty(eventLogSource))
            return;

        // Register an event source in the Application log.
        try
        {
            if (!EventLog.SourceExists(eventLogSource))
                EventLog.CreateEventSource(eventLogSource, "Application");
        }
        catch
        {
            // Don't register event handlers if it's
            // not possible to register an EventLog source.
            // Most likely an application hasn't rights
            // to register a new source in the EventLog.
            // Administration rights are required for this.
            // Please register a new source manually.
            // Or maybe eventLogSource is not valid.
            return;
        }

        // Hook into the Error event of the application.
        application.Error += new EventHandler(OnError);
        Elmah.ErrorSignal.Get(application).Raised +=
        new Elmah.ErrorSignalEventHandler(OnErrorSignaled);
    }

Further, let’s implement some more methods for handling and filtering of incoming errors. Also, we will override a SupportDiscoverability() method.

C#
/// <summary>
/// Determines whether the module will be registered
/// for discovery in partial trust environments or not.
/// </summary>
protected override bool SupportDiscoverability
{
    get { return true; }
}

/// <summary>
/// The handler called when an unhandled exception bubbles up to the module.
/// </summary>
protected virtual void OnError(object sender, EventArgs e)
{
    HttpContext context = ((HttpApplication)sender).Context;
    OnError(context.Server.GetLastError(), context);
}

/// <summary>
/// The handler called when an exception is explicitly signaled.
/// </summary>
protected virtual void OnErrorSignaled(object sender, Elmah.ErrorSignalEventArgs args)
{
    OnError(args.Exception, args.Context);
}

/// <summary>
/// Reports the exception.
/// </summary>
protected virtual void OnError(Exception e, HttpContext context)
{
    if (e == null)
        throw new ArgumentNullException("e");

    // Fire an event to check if listeners
    // want to filter out reporting of the uncaught exception.
    Elmah.ExceptionFilterEventArgs args = new Elmah.ExceptionFilterEventArgs(e, context);
    OnFiltering(args);

    if (args.Dismissed)
        return;

    // Get the last error and then write it to the EventLog.
    Elmah.Error error = new Elmah.Error(e, context);
    ReportError(error);
}

/// <summary>
/// Raises the event.
/// </summary>
protected virtual void OnFiltering(Elmah.ExceptionFilterEventArgs args)
{
    Elmah.ExceptionFilterEventHandler handler = Filtering;

    if (handler != null)
        handler(this, args);
}

And finally, let’s implement a ReportError() method. It will write error entries to the EventLog. We will compose and format an error message string before writing. In our project, I implemented the composing in such way. But you can reuse this composing or make your own.

C#
/// <summary>
/// Writes the error to the EventLog.
/// </summary>
protected virtual void ReportError(Elmah.Error error)
{
    // Compose an error message.
    StringBuilder sb = new StringBuilder();
    sb.Append(error.Message);
    sb.AppendLine();
    sb.AppendLine();
    sb.Append("Date and Time: " + error.Time.ToString("dd.MM.yyyy HH.mm.ss"));
    sb.AppendLine();
    sb.Append("Host Name: " + error.HostName);
    sb.AppendLine();
    sb.Append("Error Type: " + error.Type);
    sb.AppendLine();
    sb.Append("Error Source: " + error.Source);
    sb.AppendLine();
    sb.Append("Error Status Code: " + error.StatusCode.ToString());
    sb.AppendLine();
    sb.Append("Error Request Url: " + HttpContext.Current.Request.Url.AbsoluteUri);
    sb.AppendLine();
    sb.AppendLine();
    sb.Append("Error Details:");
    sb.AppendLine();
    sb.Append(error.Detail);
    sb.AppendLine();

    string messageString = sb.ToString();
    if (messageString.Length > 32765)
    {
        // Max limit of characters that EventLog allows for an event is 32766.
        messageString = messageString.Substring(0, 32765);
    }

    // Write the error entry to the event log.
    try
    {
        EventLog.WriteEntry(eventLogSource,
        messageString, EventLogEntryType.Error, error.StatusCode);
    }
    catch
    {
        // Nothing to do if it is not possible to write an error message to the EventLog.
        // Most likely an application hasn't rights to write to the EventLog.
        // Or maybe eventLogSource is not valid.
    }
}

Full Source Code

Eventually, we can put all this code in one file.

C#
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.Text;
using System.Web;
using Elmah;

namespace MyNamespace
{
    /// <summary>
    /// Handler for the "elmah/errorEventLog" section of the configuration file.
    /// </summary>
    public class ElmahErrorEventLogSectionHandler : SingleTagSectionHandler { }

    /// <summary>
    /// HTTP module that writes Elmah logged error to the 
    /// Windows Application EventLog whenever an unhandled exception occurs 
    /// in an ASP.NET web application.
    /// </summary>
    public class ElmahErrorEventLogModule : HttpModuleBase, IExceptionFiltering
    {
        private string eventLogSource;

        public event Elmah.ExceptionFilterEventHandler Filtering;

        /// <summary>
        /// Initializes the module and prepares it to handle requests.
        /// </summary>
        protected override void OnInit(HttpApplication application)
        {
            if (application == null)
                throw new ArgumentNullException("application");

            // Get the configuration section of this module.
            // If it's not there then there is nothing to initialize or do.
            // In this case, the module is as good as mute.
            IDictionary config = 
            (IDictionary)ElmahConfiguration.GetSubsection("errorEventLog");
            if (config == null)
                return;

            // Get settings.
            eventLogSource = 
            ElmahConfiguration.GetSetting(config, "eventLogSource", string.Empty);
            if (string.IsNullOrEmpty(eventLogSource))
                return;

            // Register an event source in the Application log.
            try
            {
                if (!EventLog.SourceExists(eventLogSource))
                    EventLog.CreateEventSource(eventLogSource, "Application");
            }
            catch
            {
                // Don't register event handlers 
                // if it's not possible to register an EventLog source.
                // Most likely an application hasn't rights 
                // to register a new source in the EventLog.
                // Administration rights are required for this. 
                // Please register a new source manually.
                // Or maybe eventLogSource is not valid.
                return;
            }

            // Hook into the Error event of the application.
            application.Error += new EventHandler(OnError);
            Elmah.ErrorSignal.Get(application).Raised += 
                new Elmah.ErrorSignalEventHandler(OnErrorSignaled);
        }

        /// <summary>
        /// Determines whether the module will be registered 
        /// for discovery in partial trust environments or not.
        /// </summary>
        protected override bool SupportDiscoverability
        {
            get { return true; }
        }

        /// <summary>
        /// The handler called when an unhandled exception bubbles up to the module.
        /// </summary>
        protected virtual void OnError(object sender, EventArgs e)
        {
            HttpContext context = ((HttpApplication)sender).Context;
            OnError(context.Server.GetLastError(), context);
        }

        /// <summary>
        /// The handler called when an exception is explicitly signaled.
        /// </summary>
        protected virtual void OnErrorSignaled(object sender, Elmah.ErrorSignalEventArgs args)
        {
            OnError(args.Exception, args.Context);
        }

        /// <summary>
        /// Reports the exception.
        /// </summary>
        protected virtual void OnError(Exception e, HttpContext context)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            // Fire an event to check if listeners want to filter out 
            // reporting of the uncaught exception.
            Elmah.ExceptionFilterEventArgs args = new Elmah.ExceptionFilterEventArgs(e, context);
            OnFiltering(args);

            if (args.Dismissed)
                return;

            // Get the last error and then write it to the EventLog.
            Elmah.Error error = new Elmah.Error(e, context);
            ReportError(error);
        }

        /// <summary>
        /// Raises the <see cref="Filtering"/> event.
        /// </summary>
        protected virtual void OnFiltering(Elmah.ExceptionFilterEventArgs args)
        {
            Elmah.ExceptionFilterEventHandler handler = Filtering;

            if (handler != null)
                handler(this, args);
        }

        /// <summary>
        /// Writes the error to the EventLog.
        /// </summary>
        protected virtual void ReportError(Elmah.Error error)
        {
            // Compose an error message.
            StringBuilder sb = new StringBuilder();
            sb.Append(error.Message);
            sb.AppendLine();
            sb.AppendLine();
            sb.Append("Date and Time: " + 
            error.Time.ToString("dd.MM.yyyy HH.mm.ss"));
            sb.AppendLine();
            sb.Append("Host Name: " + error.HostName);
            sb.AppendLine();
            sb.Append("Error Type: " + error.Type);
            sb.AppendLine();
            sb.Append("Error Source: " + error.Source);
            sb.AppendLine();
            sb.Append("Error Status Code: " + error.StatusCode.ToString());
            sb.AppendLine();
            sb.Append("Error Request Url: " + 
            HttpContext.Current.Request.Url.AbsoluteUri);
            sb.AppendLine();
            sb.AppendLine();
            sb.Append("Error Details:");
            sb.AppendLine();
            sb.Append(error.Detail);
            sb.AppendLine();

            string messageString = sb.ToString();
            if (messageString.Length > 32765)
            {
                // Max limit of characters that EventLog allows for an event is 32766.
                messageString = messageString.Substring(0, 32765);
            }

            // Write the error entry to the event log.
            try
            {
                EventLog.WriteEntry(eventLogSource, 
                messageString, EventLogEntryType.Error, error.StatusCode);
            }
            catch
            {
                // Nothing to do if it is not possible to write an error message to the EventLog.
                // Most likely an application hasn't rights to write to the EventLog.
                // Or maybe eventLogSource is not valid.
            }
        }
    }

    /// <summary>
    /// Get the configuration from the "elmah" section of the configuration file.
    /// </summary>
    public class ElmahConfiguration
    {
        internal const string GroupName = "elmah";
        internal const string GroupSlash = GroupName + "/";

        public ElmahConfiguration() { }

        public static NameValueCollection AppSettings
        {
            get
            {
                return ConfigurationManager.AppSettings;
            }
        }

        public static object GetSubsection(string name)
        {
            return GetSection(GroupSlash + name);
        }

        public static object GetSection(string name)
        {
            return ConfigurationManager.GetSection(name);
        }

        public static string GetSetting(IDictionary config, string name)
        {
            return GetSetting(config, name, null);
        }

        public static string GetSetting(IDictionary config, string name, string defaultValue)
        {
            string value = NullString((string)config[name]);

            if (value.Length == 0)
            {
                if (defaultValue == null)
                {
                    throw new Elmah.ApplicationException(string.Format(
                        "The required configuration setting '{0}' 
                        is missing for the error eventlog module.", name));
                }

                value = defaultValue;
            }

            return value;
        }

        public static string NullString(string s)
        {
            return s == null ? string.Empty : s;
        }
    }
}

Configuration File

Now let’s have a look at the configuration file.

Register our SectionHandler in the <configSections><sectionGroup name=”elmah”> section after ELMAH handlers. Certainly, a namespace and a DLL name will be other in your case.

As well, it’s needed to register our module in the <system.web> and <system.webServer> sections after ELMAH modules.

And finally, add a configuration parameter errorEventLog for our module to the <elmah> section.

XML
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="elmah">
      <section name="security" requirePermission="false" 
      type="Elmah.SecuritySectionHandler, Elmah" />
      <section name="errorLog" requirePermission="false" 
      type="Elmah.ErrorLogSectionHandler, Elmah" />
      <section name="errorMail" requirePermission="false" 
      type="Elmah.ErrorMailSectionHandler, Elmah" />
      <section name="errorFilter" requirePermission="false" 
      type="Elmah.ErrorFilterSectionHandler, Elmah" />
      <section name="errorEventLog" requirePermission="false" 
      type="MyNamespace.ElmahErrorEventLogSectionHandler, MyApplicationOrLybraryDllName" />
    </sectionGroup>
  </configSections>
  
  <system.web>
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
      <add name="ErrorFilter" 
      type="Elmah.ErrorFilterModule, Elmah" />
      <add name="ErrorEventLog" 
      type="MyNamespace.ElmahErrorEventLogModule, MyApplicationOrLybraryDllName" />
    </httpModules>
    <httpHandlers>
      <add verb="POST,GET,HEAD" path="elmah.axd" 
      type="Elmah.ErrorLogPageFactory, Elmah" />
    </httpHandlers>
  </system.web>
  
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="ErrorLog" type="Elmah.ErrorLogModule, 
      Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, 
      Elmah" preCondition="managedHandler" />
      <add name="ErrorEventLog" 
      type="MyNamespace.ElmahErrorEventLogModule, 
      MyApplicationOrLybraryDllName" preCondition="managedHandler" />
    </modules>
    <handlers>
      <add name="Elmah" path="elmah.axd" 
      verb="POST,GET,HEAD" type="Elmah.ErrorLogPageFactory, 
      Elmah" preCondition="integratedMode" />
    </handlers>
  </system.webServer>
  
  <elmah>
    <errorLog type="Elmah.MemoryErrorLog, Elmah" size="50" />
    <errorEventLog eventLogSource="MyEventLogSourceName" />
  </elmah>
</configuration>

Download

You can download all this code from here.

Conclusion

I hope this article will help somebody. I haven’t found such a solution when I worked on a project. And this was a reason to write the article.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer (Senior)
Latvia Latvia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --