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>





