Implementing Exception Management, Part 1

Applies to: Windows Communication Foundation

Published: June 2011

Author: Alex Culp

Referenced Image

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 trycatch 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