This is the third post in a series.
The first post described the problem: ASP.NET wasn't reporting inner exception stack traces.
The second post described my solution.
This post shows the code I used to solve the problem: a custom email provider for the Health Monitoring system in ASP.NET. Enjoy!
Here's the provider. Note that I opted *not* to build a buffering provider to keep things simple:
public class MyMailWebEventProvider : WebEventProvider
{
string to;
string from;
string subjectPrefix;
public override void Initialize(string name,
NameValueCollection config)
{
base.Initialize(name, config);
to = GetAndRemoveStringAttribute(config, "to", true);
from = GetAndRemoveStringAttribute(config, "from", true);
subjectPrefix = GetAndRemoveStringAttribute(config,
"subjectPrefix", false);
}
public override void ProcessEvent(WebBaseEvent raisedEvent)
{
SendMail(raisedEvent);
}
private void SendMail(WebBaseEvent raisedEvent)
{
string subject = ComputeEmailSubject(raisedEvent);
string body = ComputeEmailBody(raisedEvent);
MailMessage msg = new MailMessage(from, to, subject, body);
new SmtpClient().Send(msg);
}
private string ComputeEmailBody(WebBaseEvent raisedEvent)
{
WebRequestErrorEvent errorEvent =
raisedEvent as WebRequestErrorEvent;
if (null != errorEvent)
return ErrorEventFormattingHelper.FormatRequestErrorEvent(errorEvent);
else return raisedEvent.ToString();
}
private string ComputeEmailSubject(WebBaseEvent raisedEvent)
{
StringBuilder subjectBuilder = new StringBuilder();
// surface some details in subject about error events
WebBaseErrorEvent errorEvent = raisedEvent as WebBaseErrorEvent;
if (null != errorEvent)
{
Exception unhandledException = errorEvent.ErrorException;
// drill through reflection exceptions to show the root cause
TargetInvocationException invocationException =
unhandledException as TargetInvocationException;
if (null != invocationException)
{
Exception innerException =
DrillIntoTargetInvocationException(invocationException);
subjectBuilder.AppendFormat("{0}",
(innerException ?? invocationException).GetType().Name);
if (null != innerException)
subjectBuilder.Append(" (via reflection)");
}
else subjectBuilder.Append(unhandledException.GetType().Name);
}
// if we've not got anything better
// just show the event type in the subject
if (0 == subjectBuilder.Length)
subjectBuilder.AppendFormat("Event type: {0}",
raisedEvent.GetType().Name);
if (!string.IsNullOrEmpty(subjectPrefix)) {
subjectBuilder.Insert(0, ' ');
subjectBuilder.Insert(0, subjectPrefix);
}
return subjectBuilder.ToString();
}
/// <summary>
/// Reflection often hides exception details, so we try to drill down
/// through the plumbing exceptions to find a likely cause
/// </summary>
private Exception DrillIntoTargetInvocationException(
TargetInvocationException outerException)
{
Exception innerException = outerException.InnerException;
TargetInvocationException innerInvocationException =
innerException as TargetInvocationException;
if (null != innerInvocationException)
return DrillIntoTargetInvocationException(innerInvocationException);
else if (null != innerException)
return innerException;
else return null;
}
private static string GetAndRemoveStringAttribute(NameValueCollection config,
string attributeName, bool required)
{
string value = config.Get(attributeName);
if (required && string.IsNullOrEmpty(value))
throw new ConfigurationErrorsException(string.Format(
"Expected attribute {0}, which is missing or empty.",
attributeName));
config.Remove(attributeName);
return value;
}
public override void Flush()
{
// nothing to do - this is not a buffering provider
}
public override void Shutdown()
{
// nothing to do here either
}
}
Here's a helper class that formats the error messages the way I want to see them. Note that I've omitted some fields that I personally didn't care about, and I've reordered things a bit, so you might want to tweak this if you're going to use it in your own system.
internal static class ErrorEventFormattingHelper
{
internal static string FormatRequestErrorEvent(
WebRequestErrorEvent errorEvent)
{
CustomEventFormatter formatter =
new CustomEventFormatter();
formatter.AppendLine(string.Format(
"Unhandled Exception in {0}:",
WebBaseEvent.ApplicationInformation
.ApplicationVirtualPath));
formatter.Indent();
EmitExceptionAtAGlance(formatter,
errorEvent.ErrorException);
formatter.RevertIndent();
formatter.AppendLine();
formatter.AppendLine("Exception stack trace(s):");
EmitExceptionStackTrace(formatter,
errorEvent.ErrorException);
formatter.AppendLine();
formatter.AppendLine("Event information:");
formatter.Indent();
EmitEventInfo(formatter, errorEvent);
formatter.RevertIndent();
formatter.AppendLine();
formatter.AppendLine("Application information:");
formatter.Indent();
EmitApplicationInfo(formatter,
WebBaseEvent.ApplicationInformation);
formatter.RevertIndent();
formatter.AppendLine();
formatter.AppendLine("Process/thread information:");
formatter.Indent();
EmitProcessInfo(formatter,
errorEvent.ProcessInformation);
formatter.RevertIndent();
formatter.AppendLine();
formatter.AppendLine("Request information:");
formatter.Indent();
EmitRequestInfo(formatter,
errorEvent.RequestInformation);
formatter.RevertIndent();
return formatter.ToString();
}
private static void EmitEventInfo(
CustomEventFormatter formatter,
WebBaseEvent theEvent)
{
formatter.AppendLine(string.Format(
"Event code: {0}",
theEvent.EventCode.ToString(
CultureInfo.InvariantCulture)));
formatter.AppendLine(string.Format(
"Event message: {0}",
theEvent.Message));
formatter.AppendLine(string.Format(
"Event time: {0}",
theEvent.EventTime.ToString(
CultureInfo.InvariantCulture)));
formatter.AppendLine(string.Format(
"Event ID: {0}",
theEvent.EventID.ToString("N",
CultureInfo.InvariantCulture)));
}
private static void EmitApplicationInfo(
CustomEventFormatter formatter,
WebApplicationInformation appInfo)
{
formatter.AppendLine(string.Format(
"Application domain: {0}",
appInfo.ApplicationDomain));
formatter.AppendLine(string.Format(
"Application Virtual Path: {0}",
appInfo.ApplicationVirtualPath));
formatter.AppendLine(string.Format(
"Application Physical Path: {0}",
appInfo.ApplicationPath));
}
private static void EmitProcessInfo(
CustomEventFormatter formatter,
WebProcessInformation webProcessInfo)
{
formatter.AppendLine(string.Format(
"Process ID: {0}",
webProcessInfo.ProcessID.ToString(
CultureInfo.InvariantCulture)));
formatter.AppendLine(string.Format(
"Process name: {0}",
webProcessInfo.ProcessName));
formatter.AppendLine(string.Format(
"Account name: {0}",
webProcessInfo.AccountName));
}
private static void EmitRequestInfo(
CustomEventFormatter formatter,
WebRequestInformation webRequestInfo)
{
string name = null;
if (webRequestInfo.Principal != null)
name = webRequestInfo.Principal.Identity.Name;
formatter.AppendLine(string.Format(
"Request URL: {0}",
webRequestInfo.RequestUrl));
formatter.AppendLine(string.Format(
"Request path: {0}",
webRequestInfo.RequestPath));
formatter.AppendLine(string.Format(
"User name: {0}",
name ?? "[ANONYMOUS]"));
formatter.AppendLine(string.Format(
"User host address: {0}",
webRequestInfo.UserHostAddress));
}
private static void EmitExceptionAtAGlance(
CustomEventFormatter formatter,
Exception exception)
{
formatter.AppendLine(string.Format(
"Type: {0}",
exception.GetType().Name));
formatter.AppendLine(string.Format(
"Message: {0}",
exception.Message));
if (null != exception.InnerException)
{
formatter.Indent();
formatter.AppendLine("-->Inner Exception");
EmitExceptionAtAGlance(formatter,
exception.InnerException);
formatter.RevertIndent();
}
}
private static void EmitExceptionStackTrace(
CustomEventFormatter formatter, Exception exception)
{
formatter.AppendLine(exception.StackTrace);
if (null != exception.InnerException)
{
// no point indenting
// since stack traces typically wrap like crazy
formatter.AppendLine();
formatter.AppendLine("-->Inner exception stack trace:");
EmitExceptionStackTrace(formatter, exception.InnerException);
}
}
}
And finally, here's a helper class that manages indentation levels for the output email message:
public class CustomEventFormatter
{
const int TabSpaces = 4;
StringBuilder sb = new StringBuilder();
private int indentLevel;
private bool startingNewLine = true;
public void Indent()
{
++indentLevel;
}
public void RevertIndent()
{
if (indentLevel > 0)
--indentLevel;
}
public void Append(string text)
{
if (startingNewLine)
EmitIndent();
sb.Append(text);
startingNewLine = false;
}
public void AppendLine(string lineOfText)
{
if (startingNewLine)
EmitIndent();
EmitIndent();
sb.AppendLine(lineOfText);
startingNewLine = true;
}
private void EmitIndent()
{
sb.Append(' ', TabSpaces * indentLevel);
}
public void AppendLine()
{
AppendLine(string.Empty);
}
public override string ToString()
{
return sb.ToString();
}
}
Build this into a library application and reference it in your config file. Here's an example:
<healthMonitoring>
<providers>
<add name="mailWebEventProvider"
type="MyMailWebEventProvider"
to="web-fault@fabrikam.com"
from="website@fabrikam.com"
buffer="false"
subjectPrefix="[WEB-ERROR]"
/>
</providers>
<rules>
<add name="All Errors Email"
eventName="All Errors"
provider="mailWebEventProvider"
profile="Default"
minInstances="1"
maxLimit="Infinite"
minInterval="00:01:00"
custom=""/>
</rules>
</healthMonitoring>
Posted
Aug 04 2008, 07:11 AM
by
keith-brown