Implementing Exception Management, Part 1
Applies to: Windows Communication Foundation
Published: June 2011
Author: Alex Culp
This topic contains the following sections.
- Implementing Exception Management
Implementing Exception Management
The Enterprise Library Exception Management application block includes most of the features you need to log an exception. What is missing is the ability to capture a method's parameters. This section discusses a sample architecture for exception management that is built on Enterprise Library 5.0. The architecture does not cover every situation, but it does demonstrate the basic concepts that are outlined in the Exception Management Best Practices
Logging Destinations
This section describes the locations in which you can store exception data.
Event Log
The event log is the most common place to log errors. Although text files work well for services in small companies or in individual departments, they are not nearly as effective a diagnostics tool in a large enterprise. It is much easier to configure an enterprise-level monitoring application, such as SCOM, to monitor event logs than it is to parse text files. However, unless there are only a few small parameters to the service, you will not be able to fit all of the parameter data into a event log entry. The maximum size of an event log entry is 32 KB.
Enterprise Library Database
The Enterprise Library Logging application block includes most of the features you need to log exceptions. You can find the script to the Create the Enterprise Library logging database under EntLib50Src\Blocks\Logging\Src\DatabaseTraceListener\Scripts. If you took the default path when you installed the source code for Enterprise library, the EntLib50Src folder will be in your "My Documents" folder. If you want to capture operation parameters and save them to a database you can use the following script to create the table, and stored procedures.
CREATE TABLE [dbo].[OperationParameter](
[OperationParameterId] [int] NOT NULL,
[ReferenceId] [uniqueidentifier] NOT NULL,
[ServiceName] [varchar](255) NOT NULL,
[OperationName] [varchar](255) NOT NULL,
[DateStamp] [datetime] NOT NULL,
[ParameterData] [varchar](max) NOT NULL,
CONSTRAINT [PK_OperationParameterId] PRIMARY KEY CLUSTERED
(
[OperationParameterId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_ReferenceId] ON [dbo].[OperationParameter]
(
[OperationParameterId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE PROC [dbo].[usp_OperationParameterInsert]
@OperationParameterId int,
@ReferenceId uniqueidentifier,
@ServiceName varchar(255),
@OperationName varchar(255),
@DateStamp datetime,
@ParameterData varchar(MAX)
AS
INSERT INTO [dbo].[OperationParameter] ([OperationParameterId], [ReferenceId], [ServiceName], [OperationName], [DateStamp], [ParameterData])
SELECT @OperationParameterId, @ReferenceId, @ServiceName, @OperationName, @DateStamp, @ParameterData
GO
CREATE PROC [dbo].[usp_OperationParameterSelect]
@ReferenceId uniqueidentifier
AS
SELECT [OperationParameterId], [ReferenceId], [ServiceName], [OperationName], [DateStamp], [ParameterData]
FROM [dbo].[OperationParameter]
WHERE ReferenceId = @ReferenceID
GO
Logging Exceptions in Azure
Using SQL Azure for error logging is probably a very bad idea, unless you like spending a lot of money. Every SQL Azure transaction costs, and the last thing you want is to be charged for logging a series of errors. Instead, if you deploy an application to the cloud, log the service operation parameters to the Windows Azure Table Storage, which stores data much more cheaply. Because the Enterprise Library logging capabilities are built on the System.Diagnostics namespace, you can configure the Enterprise Library Logging Application Block to write to Windows Azure diagnostics by adding it to the trace listeners collection. The following code shows how to do this.
<system.diagnostics>
<trace autoflush="true">
<listeners>
<add type="Microsoft.WindowsAzure.Diagnostics.DiagnosticMonitorTraceListener,
Microsoft.WindowsAzure.Diagnostics, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" name="AzureDiagnostics" />
</listeners>
</trace>
</system.diagnostics>
There are a few more steps involved. For more information, see Tom Hollander's great blog post, "Get Logging in Windows Azure with Enterprise Library" at https://blogs.msdn.com/b/tomholl/archive/2010/09/06/get-logging-in-windows-azure-with-enterprise-library.aspx. You can also read "Take Control of Logging and Tracing in Windows Azure" at https://msdn.microsoft.com/en-us/magazine/ff714589.aspx.
Gathering Data for Analysis As stated in the Exception Management Best Practices section, it is a good idea to log to a database. This allows you to examine error trends and to analyze error data. It is also the best way to analyze a complex series of interrelated errors. Because you will undoubtedly have to examine errors more than once, it might be practical to build a tool that retrieves the relevant data. For information on how to do this, see Logging Exceptions in WCF.
Purging Data Even though Windows Azure table and blob storage is considerably cheaper than SQL Azure to store exception logs, it is not free. Over time, the diagnostic data accumulates. Before you deploy your application to the cloud, develop some procedures or an application (a worker role is ideal) to purge your logs.
Logging Exceptions in WCF
One option for logging exceptions in WCF is to use the first approach that was demonstrated in the Introduction, which wraps each operation's logic inside a try…catch block. While this approach is acceptable, it is prone to bugs, because every operation requires the logic. Instead, consider the second recommendation, which is to use DI www.microsoft.com to insert the exception handling logic.
To use DI, you must create an interception behavior, and register it with the Unity Application Block (Unity).
Note
If you only need to log request parameters, you can use the ExceptionCallHandler class to log exceptions to the appropriate policy. For more information, see https://msdn.microsoft.com/en-us/library/microsoft.practices.enterpriselibrary.exceptionhandling.policyinjection.exceptioncallhandler(v=pandp.50).aspx.
The following code is an example of an interception behavior that handles exceptions for all of a service's operations.
Visual C# Interception Behavior to Handle Exceptions
public class ExceptionInterceptionBehavior<TTypeToIntercept> : IInterceptionBehavior where TTypeToIntercept : class
{
/// <summary>
/// Returns the interfaces required by the behavior for the objects it intercepts.
/// </summary>
/// <returns></returns>
public IEnumerable<Type> GetRequiredInterfaces()
{
return new List<Type> { typeof(TTypeToIntercept) } as IEnumerable<Type>;
}
/// <summary>
/// Handles the invocation of the operation, and looks for any errors, if found deals with them as
/// specified by the exception policy.
/// </summary>
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
var ret = getNext().Invoke(input, getNext);
if (ret.Exception != null)
{
if (input.Arguments != null && input.Arguments.Count > 0)
{
//Log the request
var referenceId = Guid.NewGuid();
LogOperationParameters(input, referenceId);
//Add the error URL for the help link
string url = string.Format(ConfigurationManager.AppSettings["ErrorRestURL"], referenceId);
ret.Exception.HelpLink = url;
}
else
{
ret.Exception.HelpLink = "No logged parameters";
}
//Now, process the exception
Exception toThrow;
bool rethrow = ExceptionPolicy.HandleException(ret.Exception, "GeneralPolicy", out toThrow);
if (rethrow && toThrow != null)
{
//Set the new exception into the return if
ret.Exception = toThrow;
}
else if (!rethrow)
{
//eat the exception, allow null to get returned back from the service
//if that is what the policy asks us to do.
ret.Exception = null;
}
}
return ret;
}
public bool WillExecute
{
get
{
return true;
}
}
private void LogOperationParameters(IMethodInvocation input, Guid referenceId)
{
//Don't do anything if there are no arguments to log
if (input.Arguments == null || input.Arguments.Count == 0)
{
return;
}
//Guid will be the correllation between the exeption and the
//parameters that are logged.
string operationName = input.MethodBase.Name;
string serviceName = typeof(TTypeToIntercept).FullName;
List<string> parameterData = new List<string>();
var db = DatabaseFactory.CreateDatabase("LoggingDB");
//Serialize the parameters to strings
foreach (var arg in input.Arguments)
{
var sb = new StringBuilder();
var sw = new StringWriter(sb);
var xmlWriter = new XmlTextWriter(sw);
var ser = new DataContractSerializer(arg.GetType());
ser.WriteObject(xmlWriter, arg);
xmlWriter.Flush();
using (var cmd = db.GetStoredProcCommand("[dbo].[usp_OperationParameterInsert]"))
{
cmd.Parameters.Add(new SqlParameter("@ReferenceId", referenceId));
cmd.Parameters.Add(new SqlParameter("@ServiceName", serviceName));
cmd.Parameters.Add(new SqlParameter("@OperationName", operationName));
cmd.Parameters.Add(new SqlParameter("@ParameterData", sb.ToString()));
db.ExecuteNonQuery(cmd);
}
}
}
}
Visual Basic Interception Behavior to Handle Exceptions
Public Class ExceptionInterceptionBehavior(Of TTypeToIntercept As Class)
Implements IInterceptionBehavior
''' <summary>
''' Returns the interfaces required by the behavior for the objects it intercepts.
''' </summary>
''' <returns></returns>
Public Function GetRequiredInterfaces() As IEnumerable(Of Type)
Return TryCast(New List(Of Type)() From { _
GetType(TTypeToIntercept) _
}, IEnumerable(Of Type))
End Function
''' <summary>
''' Handles the invocation of the operation, and looks for any errors, if found deals with them as
''' specified by the exception policy.
''' </summary>
Public Function Invoke(input As IMethodInvocation, getNext As GetNextInterceptionBehaviorDelegate) As IMethodReturn
Dim ret = getNext().Invoke(input, getNext)
If ret.Exception IsNot Nothing Then
If input.Arguments IsNot Nothing AndAlso input.Arguments.Count > 0 Then
'Log the request
Dim referenceId = Guid.NewGuid()
LogOperationParameters(input, referenceId)
'Add the error URL for the help link
Dim url As String = String.Format(ConfigurationManager.AppSettings("ErrorRestURL"), referenceId)
ret.Exception.HelpLink = url
Else
ret.Exception.HelpLink = "No logged parameters"
End If
'Now, process the exception
Dim toThrow As Exception
Dim rethrow As Boolean = ExceptionPolicy.HandleException(ret.Exception, "GeneralPolicy", toThrow)
If rethrow AndAlso toThrow IsNot Nothing Then
'Set the new exception into the return if
ret.Exception = toThrow
ElseIf Not rethrow Then
'eat the exception, allow null to get returned back from the service
'if that is what the policy asks us to do.
ret.Exception = Nothing
End If
End If
Return ret
End Function
Public ReadOnly Property WillExecute() As Boolean
Get
Return True
End Get
End Property
Private Sub LogOperationParameters(input As IMethodInvocation, referenceId As Guid)
'Don't do anything if there are no arguments to log
If input.Arguments Is Nothing OrElse input.Arguments.Count = 0 Then
Return
End If
'Guid will be the correllation between the exeption and the
'parameters that are logged.
Dim operationName As String = input.MethodBase.Name
Dim serviceName As String = GetType(TTypeToIntercept).FullName
Dim parameterData As New List(Of String)()
Dim db = DatabaseFactory.CreateDatabase("LoggingDB")
'Serialize the parameters to strings
For Each arg As var In input.Arguments
Dim sb = New StringBuilder()
Dim sw = New StringWriter(sb)
Dim xmlWriter = New XmlTextWriter(sw)
Dim ser = New DataContractSerializer(arg.[GetType]())
ser.WriteObject(xmlWriter, arg)
xmlWriter.Flush()
Using cmd = db.GetStoredProcCommand("[dbo].[usp_OperationParameterInsert]")
cmd.Parameters.Add(New SqlParameter("@ReferenceId", referenceId))
cmd.Parameters.Add(New SqlParameter("@ServiceName", serviceName))
cmd.Parameters.Add(New SqlParameter("@OperationName", operationName))
cmd.Parameters.Add(New SqlParameter("@ParameterData", sb.ToString()))
db.ExecuteNonQuery(cmd)
End Using
Next
End Sub
End Class
Previous article: Exception Management Best Practices
Continue on to the next article: Implementing Exception Management, Part 2