Tales from the Trenches: First Experiences
On this page: | Download: |
---|---|
My first experiences with the Logging and Exception Handling Blocks | The Logging Block | The Exception Handling Block | Final Recommendations |
My first experiences with the Logging and Exception Handling Blocks
**Contributed by Fabian Fernandez
**Logging and exception handling are two things you always have in mind when starting to build a new project. I was really tired of writing new implementations for these every time I started a project because of the different requirements tied to the specifics of each project. Therefore, when I was the .NET Software Architect at a previous job starting a project from zero, I decided to search for a one-time solution to cover these areas. A solution that would, when we needed to reuse it in another project or satellite application, work like magic with just a couple of configuration changes and as few lines of code as possible.
I had heard of Enterprise Library but never used it, so I began by looking at the documentation, starting with the EntLib50.chm help file that you can find on CodePlex. My first impression was that it is initially a little hard to understand because of the quantity of information, but after you start reading sections, it becomes like a walk in the park. It takes just a couple of hours to understand the basics.
Note
The documentation is also available on MSDN for Enterprise Library 5 and Enterprise Library 6.
The first thing I decided to use was the ability to redirect sections in the configuration file using the FileConfigurationSource type; this gives me the ability to reuse both my logging and exception handling assemblies and their associated configuration with my application specific code. I end up using two assemblies, one for logging, and one for exception handling, each with its own configuration file. When I reference one of the assemblies, I include the configuration file as a linked file so I don't need to make multiple copies of the configuration file all over the place: linked files are powerful, use them!
Here's an example of a web.config/app.config using the redirect sections feature to reuse the configuration file of each block:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="enterpriseLibrary.ConfigurationSource"
type="Microsoft.Practices.EnterpriseLibrary.Common.Configuration
.ConfigurationSourceSection, Microsoft.Practices.EnterpriseLibrary
.Common, Version=5.0.505.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
</configSections>
<enterpriseLibrary.ConfigurationSource
selectedSource="System Configuration Source">
<sources>
<add name="System Configuration Source" type="Microsoft.Practices
.EnterpriseLibrary.Common.Configuration.SystemConfigurationSource,
Microsoft.Practices.EnterpriseLibrary.Common, Version=5.0.505.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add name="Logging configuration" type="Microsoft.Practices
.EnterpriseLibrary.Common.Configuration.FileConfigurationSource,
Microsoft.Practices.EnterpriseLibrary.Common, Version=5.0.505.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"
filePath="./Configs/EnterpriseLibrary.Logging.config" />
<add name="Exception Handling configuration" type="Microsoft.Practices
.EnterpriseLibrary.Common.Configuration.FileConfigurationSource,
Microsoft.Practices.EnterpriseLibrary.Common, Version=5.0.505.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"
filePath="./Configs/EnterpriseLibrary.ExceptionHandling.config" />
</sources>
<redirectSections>
<add sourceName="Logging configuration" name="loggingConfiguration" />
<add sourceName="Exception Handling configuration"
name="exceptionHandling" />
</redirectSections>
</enterpriseLibrary.ConfigurationSource>
</configuration>
The Logging Block
You should log using as many listeners as you can. Why? Because it will help to ensure you have at least one log to check. One thing is certain, don't use just a single listener: file access could be blocked, there could be a database connection problem, an email server could be down, or there may be some other problem depending on the listener you are using.
I created five categories to use for logging: Critical, Error, Warning, Information, and Verbose. These match the values from System.Diagnostics.TraceEventType enumeration.
For the email listener, I use a message formatter that only includes the logged message. This listener just enables me to check for logged messages in my email while on the go, and to be notified as soon as possible of an issue through email synced to my smartphone. If I need to see information about the issue in more detail, then I check one of the other listeners. All the other listeners use the default message formatter, which includes all the information I need.
I use the Email Trace Listener for messages in the Critical category and for the special categories "Logging Errors & Warnings" (which are errors and warnings produced by Enterprise Library code) and the "Unprocessed Category."
I prefer to use the Database Trace Listener to get interesting indicators such as the number of errors in the last 24 hours, last week, last month, or even graphics showing a timeline of how that number changed over time. You can write any query you want against the Logging Database and retrieve any information that you think is important. I use this listener for every category except for Verbose. Typically, I don’t use this listener to check the detailed information for a specific log message, mainly because the message is not formatted, and if you use copy and paste to get it into a text editor it appears as a single line.
For tracing to files, I decided to use three different files, and to use the Rolling Flat File Trace Listener that enables me to configure every aspect of how new files are created. My preference is for a roll interval of one day and a roll size of 5120 KB. This means that the block creates a new file every day, and if a log file size exceeds 5120 KB, the block creates another file for the same day, appending an incremental number to the file name.
When logging to files, I would recommend that you set the Message Header and Message Footer properties to indicate the start and end of each log message with words and symbols. Don’t use the same text for both start and end; use values such as:
- "--- Message START -------------------------"
- "--- Message END ----------------------------"
I use one trace file as the main trace for the Critical, Error, and Warning categories, one trace file for Verbose category, and one trace file for the special category "Logging Errors & Warnings."
The final trace listener I use is the Event Log Trace Listener, which I think is the most practical to check if I need to look at a message in detail. Using the Event Viewer, I can see that each log entry has an icon that makes it very easy to locate, and the message is formatted and readable. I can also create a custom view of the events to display only the events from my application.
I use the Event Log Trace Listener for every category except for Information and Verbose.
My Logging assembly contains a Logger class with a reference to the LogWriter class in the Enterprise Library Logging Application Block. This Logger class has different methods to write messages using the Logging Application Block, but they all use the LogWriter class so I write all my log messages using a consistent format.
Here's an example of a simple implementation of the Logger class:
public class Logger
{
private LogWriter LogWriter;
...
public void Write(string message, TraceEventType traceEventType)
{
message += Environment.NewLine + Environment.NewLine
+ "Additional Info:" + Environment.NewLine + Environment.NewLine
+ "MachineName: " + Environment.MachineName + Environment.NewLine
+ "TimeStamp: " + DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss tt");
this.LogWriter.Write(message, traceEventType.ToString(),
priority: 0, eventId: 10, severity: traceEventType);
}
}
The Exception Handling Block
Given the layered architecture of my application, I used this diagram from the Exception Handling Application Block as guide to implementing the exception handling in my application:
I created one exception handling policy per layer, I named the polices; Data, Business, Service, and UI. I also created a policy for unhandled exceptions at the UI Layer to use, for example, in the Application_Error method in our Global.asax implementation.
The architecture also includes a crosscutting layer, and in order to keep things simple, any layer that uses the crosscutting layer is responsible for handing exceptions from the crosscutting layer. This enables me to reuse the crosscutting code in projects that don't use my exception handling implementation.
The Data Layer Policy and UI Layer Unhandled Policy use a Critical severity and logging category, every other policy uses an Error severity and logging category.
I use five different types of exception to help me identify in which layer they are handled, they are: DataException, BusinessException, ServiceException, UIException and ValidationException.
Every logging handler uses a different Event ID to differentiate which policy is in use and at in which layer the exception occurred.
My exception handling policies log all types of exception except for ValidationException exceptions, which propagate all the way to the top without using an Exception Handling Application Block handler.
This list describes the behavior of each policy:
- Data Layer Policy: wraps the exception with a DataException, unless it's already a DataException created by the application.
- Business Layer Policy: replaces the exception with a BusinessException, unless it's already a BusinessException created by the application.
- Service Layer Policy: replaces the exception with a ServiceException, unless it's already a ServiceException created by the application or a BusinessException (created by the application or by the block).
- UI Layer Policy: replaces the exception with a UIException, unless it's already a UIException created by the application or a ServiceException or BusinessException (created by the application or by the block).
- UI Unhandled Policy: replaces any exception with a UIException.
I handle most exceptions in the public methods; I handle a small number of exceptions relating to edge cases in private and internal methods. I handle the exceptions in public methods because these are the ones used by the boundary layer.
As mentioned before, my code includes a number of custom exception definitions. It also includes an ExceptionPolicy enumeration to match the policy names to help make the code more maintainable and robust, and a custom ExceptionHandler class, which references the Enterprise Library ExceptionManager class, to handle application exceptions.
The ExceptionHandler class has only two public methods HandlerThrow and HandleContinue, and one private method used by the two public methods that adds important information into the Data collection of the exception.
HandleThrow lets the ExceptionManager instance handle the exception and throw if necessary, or it re-throws the original exception if indicated by the block.
HandleContinue uses an overload of the HandleException method of the ExceptionManager class that takes an out parameter of type Exception so the block does not perform a throw. The HandleContinue method then returns the exception indicated by the block to enable execution to continue. This is typically used at the UI Layer. For example, in an ASP.NET MVC web application I have a try/catch block inside the Action method of a Controller class. Inside the catch block, I use the HandleContinue method to handle the exception, capture the message from the returned exception, and add it to a list of errors in the Model class to show to the user.
Here's a simple implementation of the ExceptionHandler class:
public class ExceptionHandler
{
private ExceptionManager ExceptionManager;
...
public void HandleThrow(Exception ex, ExceptionPolicies policy)
{
MethodBase invokerMethod = (new StackTrace()).GetFrame(1).GetMethod();
this.IncludeAdditionalInfo(ex, invokerMethod);
bool rethrow = this.ExceptionManager.HandleException(
ex, policy.ToString());
if (rethrow)
{
throw ex;
}
}
public Exception HandleContinue(Exception ex, ExceptionPolicies policy)
{
MethodBase invokerMethod = (new StackTrace()).GetFrame(1).GetMethod();
this.IncludeAdditionalInfo(ex, invokerMethod);
Exception lastHandlerException = null;
bool rethrow = this.ExceptionManager.HandleException(ex,
policy.ToString(), out lastHandlerException);
if (rethrow == true)
{
if (lastHandlerException == null)
{
return ex;
}
else
{
return lastHandlerException;
}
}
else
{
return null;
}
}
private void IncludeAdditionalInfo(Exception ex, MethodBase invokerMethod)
{
ex.Data["Executing Assembly"] =
invokerMethod.Module.Assembly.FullName;
ex.Data["Module"] = invokerMethod.Module.Name;
ex.Data["Class"] = invokerMethod.DeclaringType;
ex.Data["Method"] = invokerMethod.Name;
}
}
Final Recommendations
Always use the Enterprise Library Configuration Tool to edit your Enterprise Library configuration files: it is very easy to use and understand, and displays the configuration information graphically. For example, if you are configuring the Logging Block you can click on a Trace Listener and instantly see which categories and which Text Formatter it is using, or you can click on a Category and see all the Trace Listeners it uses as a diagram using connector lines. It also displays help text for each element when you hover the mouse over them.
I hope that you now have a better idea of how to start using the Logging and Exception Handling Application Blocks from the Enterprise Library, if you’ve got any questions or just want to keep in touch, follow me at @fabifernandez23 on twitter.