Table of Contents
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
.
There are two options how to extend ELMAH library to save error to the EventLog
.
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.
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.
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.).
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.
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.
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.
protected override void OnInit(HttpApplication application)
{
if (application == null)
throw new ArgumentNullException("application");
IDictionary config =
(IDictionary)ElmahConfiguration.GetSubsection("errorEventLog");
if (config == null)
return;
eventLogSource = ElmahConfiguration.GetSetting
(config, "eventLogSource", string.Empty);
if (string.IsNullOrEmpty(eventLogSource))
return;
try
{
if (!EventLog.SourceExists(eventLogSource))
EventLog.CreateEventSource(eventLogSource, "Application");
}
catch
{
return;
}
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.
protected override bool SupportDiscoverability
{
get { return true; }
}
protected virtual void OnError(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
OnError(context.Server.GetLastError(), context);
}
protected virtual void OnErrorSignaled(object sender, Elmah.ErrorSignalEventArgs args)
{
OnError(args.Exception, args.Context);
}
protected virtual void OnError(Exception e, HttpContext context)
{
if (e == null)
throw new ArgumentNullException("e");
Elmah.ExceptionFilterEventArgs args = new Elmah.ExceptionFilterEventArgs(e, context);
OnFiltering(args);
if (args.Dismissed)
return;
Elmah.Error error = new Elmah.Error(e, context);
ReportError(error);
}
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.
protected virtual void ReportError(Elmah.Error error)
{
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)
{
messageString = messageString.Substring(0, 32765);
}
try
{
EventLog.WriteEntry(eventLogSource,
messageString, EventLogEntryType.Error, error.StatusCode);
}
catch
{
}
}
Eventually, we can put all this code in one file.
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
{
public class ElmahErrorEventLogSectionHandler : SingleTagSectionHandler { }
public class ElmahErrorEventLogModule : HttpModuleBase, IExceptionFiltering
{
private string eventLogSource;
public event Elmah.ExceptionFilterEventHandler Filtering;
protected override void OnInit(HttpApplication application)
{
if (application == null)
throw new ArgumentNullException("application");
IDictionary config =
(IDictionary)ElmahConfiguration.GetSubsection("errorEventLog");
if (config == null)
return;
eventLogSource =
ElmahConfiguration.GetSetting(config, "eventLogSource", string.Empty);
if (string.IsNullOrEmpty(eventLogSource))
return;
try
{
if (!EventLog.SourceExists(eventLogSource))
EventLog.CreateEventSource(eventLogSource, "Application");
}
catch
{
return;
}
application.Error += new EventHandler(OnError);
Elmah.ErrorSignal.Get(application).Raised +=
new Elmah.ErrorSignalEventHandler(OnErrorSignaled);
}
protected override bool SupportDiscoverability
{
get { return true; }
}
protected virtual void OnError(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
OnError(context.Server.GetLastError(), context);
}
protected virtual void OnErrorSignaled(object sender, Elmah.ErrorSignalEventArgs args)
{
OnError(args.Exception, args.Context);
}
protected virtual void OnError(Exception e, HttpContext context)
{
if (e == null)
throw new ArgumentNullException("e");
Elmah.ExceptionFilterEventArgs args = new Elmah.ExceptionFilterEventArgs(e, context);
OnFiltering(args);
if (args.Dismissed)
return;
Elmah.Error error = new Elmah.Error(e, context);
ReportError(error);
}
protected virtual void OnFiltering(Elmah.ExceptionFilterEventArgs args)
{
Elmah.ExceptionFilterEventHandler handler = Filtering;
if (handler != null)
handler(this, args);
}
protected virtual void ReportError(Elmah.Error error)
{
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)
{
messageString = messageString.Substring(0, 32765);
}
try
{
EventLog.WriteEntry(eventLogSource,
messageString, EventLogEntryType.Error, error.StatusCode);
}
catch
{
}
}
}
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;
}
}
}
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.
="1.0" ="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>
You can download all this code from here.
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.