Introduction

The Logging Application Block (part of the Microsoft Enterprise Library) provides a great framework for logging and tracing. Recently, I introduced the Logging Application Block on one of our team projects. While I was investigating how to use the Loggin target=_blankg Application Block, I found some areas where I wanted to enforce greater control over how developers on the team make use of the application block. In this article, I will explain the reason for doing this and show you how you can extend the application block.

This article and the associated source code and demo project are based on the Enterprise Library for .NET Framework 2.0, January 2006. This article assumes you already know the basics of the Logging Application Block. To learn about the basics, look at the CodeProject article by Piers Lawson: Get Logging with the Enterprise Library. While that article is based on the version of Enterprise Library for .NET 1.1, it is still relevant for the later version.

Background

One of the features of the Logging Application Block is the ability to control via configuration what messages get logged and where they get logged. Log Entries can be grouped together into Categories. Each Category can be independently configured to route and format Log Entries in a particular way. Categories can also be used to filter Log Entries, allowing certain Categories to be ignored. Log Entries can also be assigned a Priority, which can also be used to filter Log Entries.

In the various classes and methods provided by the Logging Application Block, the Category and Priority properties are loosely typed. Category is a string and Priority is an int. For the purposes of a generic reusable framework, these properties do need to be loosely typed to meet different application requirements. However, for an individual application, it is useful to provide tighter control over the values that can be used for Category and Priority, given their importance in controlling where and what gets logged. I do not want developers on a large team just using Category and Priority values in an ad hoc manner. It is important that Category and Priority are used in a consistent manner.

When I started researching the Logging Application Block, I searched for guidance on values to use for Priority. I found a number of people recommending values such as 3=High, 2=Medium, 1=Low. However, as Piers Lawson mentions in the article Get Logging with the Enterprise Library, Log Entries written by the Tracer have a hard coded Priority of 5. The Tracer class does not expose the Priority as a public property, and the documention on MSDN of the Logging Application Block does not refer to it. The fact that it logs with Priority of 5 is only discovered by looking at the code of the application block. Given the way the Priority Filter works, it is important to provide structure to Priority values that work with the value of 5 for Trace messages.

Another issue is that the operation parameter provided in the constructors of the Tracer class actually map to a Category for the message that is logged. With the operation parameter just being a string, it is easy for developers to make up operation values in an ad hoc way.

It would be desirable to provide a mechanism such as enums to limit the values that can be used for Category and Priority and Operation, and define Priority in context of the hard coded Tracer value of 5.

Strategy

Micorosoft has provided the source code for the Enterprise Library, and it is possible to modify the source code for your own use. However, I did not want to change the source code provided by Microsoft. Instead, I wanted to extend the classes provided by Microsoft, either by inheritance or by providing customised wrappers.

Our strategy to provide a more robust framework for logging and tracing is as follows:

Define enums for Category and Priority values that are appropriate for our application. The names of the Category enum members will match the values of Category in the configuration of the Logging Application Block.

Provide our own classes that wrap the LogEntry, Logger, and Tracer classes provided in the Logging Application Block. These classes provide essentially the same interface as the Enterprise Library classes, but have parameters and properties that are enums rather than strings and integers. Our methods, properties, and constructors call the underlying class after converting types. The operations for the Tracer class use the Category enum.

As you will see below, I needed to make a change to one line of code in the Logging Application Block. This change has been raised with the Patterns & Practices group at Microsoft, and this change is expected to be included in the next release of Enterprise Library.

The EntLibLoggingExtended project in the attached solution provides the implementation of these classes. The enums are defined in the Globals.cs file. Any project that wants to use this approach will need to include the EntLibLoggingExtended assembly and update the enum values as appropriate for the application requirements.

LogEntry Class

The LogEntry class in the Logging Application Block represents a log message. This class contains the common properties that are required for all log messages.

In the EntLibLoggingExtended project, I have created a new LogEntry class that is inherited from the LogEntry class of the Logging Application Block. The new LogEntry class has similar constructors to the base class, except that the types for the category and priority values are the Category and Priority enums. Each constructor calls the base class constructor after converting the enums to equivalent string and int values.

The new LogEntry class also replaces the Category and Priority properties. The type of these properties are the Category and Priority enums. These properties map to the base class properties, after converting between the enum values and the equivalent string and int values.

Dealing with Category collections

The Categories property is a collection. To make the Categories property work correctly, I needed to create my own CategoriesCollection class. When a Category was added to or removed from the collection, it also needed to be added to or removed from the base class collection of strings. The CategoriesCollection class raises events when Categories are added or removed. By handling these events in the LogEntry class, I can add or remove elements from the base class Categories collection of string values.

Logger Class

The Logger class is a facade for writing a log entry. This class in the Logging Application Block is static and therefore also sealed. It cannot be inherited.

In the EntLibLoggingExtended project, I have created a new Logger class that is a wrapper for the Logger class of the Logging Application Block. For every public method in the Logger class of the Logging Application Block, I have implemented the same method in the new Logger class. All the methods that had a category parameter that was a string now have the category parameter as the Category enum. All the methods that had a priority parameter that was an int now have the priority parameter as the Priority enum. Each method ends up calling the appropriate method in the Logger class of the Logging Application Block.

To improve consistency of logging even further, the extended Logger class, as well as providing wrappers for the methods of the application block Logger class, also provides a number of additional methods for common types of messages, such as logging of assertions, debug messages, and trace messages, and also for writing Information, Warning, and Error messages.

Tracer Class

The Tracer class represents a performance tracing class to log method entry/exit and duration. The lifetime of the Tracer object will determine the beginning and the end of the trace. One trace message is written by the constructor and a second trace message is written when the object is disposed (the class implements IDisposable).

In the EntLibLoggingExtended project, I have created a new Tracer class that is inherited from the Tracer class of the Logging Application Block. The new Tracer class has similar constructors to the base class, except that the string parameters called operation have been replaced by Category enum parameters called category. Each constructor calls the base class constructor after converting the enums to equivalent string values.

A default constructor

The Tracer class in the Enterprise Library does not provide a default constructor. Every time the class is used, the developer must provide an operation (i.e., Category) in the constructor. In nearly every circumstance where we would use the Tracer class, we just want a default value for the category. I have provided a default constructor for the extended Tracer class. This creates a Tracer with the Category of “Trace”. Only occasionaly do we create a Tracer with another Category.

Reducing overhead when not tracing

In the implementation of the Tracer class in the Enterprise Library, the class must do some work before deciding whether tracing is enabled and whether the categories for trace messages are being logged. Our intention is to include tracing in just about every method, and I had some concerns about the potential overhead of the work required on creation of each instance of the Tracer class. To avoid this overhead in production, in Release builds of the EntLibLoggingExtended assembly, the whole implementation of our Tracer is compiled out of the code. Tracer becomes very lightweight. The public interface of the Tracer class is not changed. It is just the internals that are removed. This means that application code that uses the Tracer does not need to be changed between Debug and Release builds.

You will see in the code for the Tracer class that in Release builds, the class does not use the Tracer class of the Logging Application Block as a base class, and that the implementation of the constructors that call the base class constructors is removed. However, code that uses the Tracer class expect the class to be disposable, and being disposable was provided by the base class. Therefore, in Release builds, the class needs to implement IDisposable directly.

This does mean that a Debug build of the EntLibLoggingExtended assembly is required for tracing purposes. However, it makes Tracer very lightweight in release builds, and avoids any performance impact of determining whether tracing is enabled. There is negligible overhead of wrapping the contents of every method in a using (new Tracer()) {...} block.

To work in conjunction with the default constructor, we have created a code snippet that inserts the following code:

using (new Tracer())
{
}

Every time a new method is created, the snippet is immediately used and the implementation of the method is then added inside this block.

A Problem

The Tracer class needs to determine the method being traced. That is, the method in which the new instance of the Tracer class was created. The Tracer class in the Logging Application Block does this by working backwards through the current stack trace until it finds a method that is not in the Tracer class itself. The line of code that does this is the following if statement:

if (method.DeclaringType != GetType())

This works if the method being traced creates an instance of the Tracer class provided in the Logging Application Block. However, when the method being traced creates an instance of the Tracer class in the EntLibLoggingExtended assembly, the trace message always reports the method being traced as the constructor of the Tracer class in the EntLibLoggingExtended assembly. This is not very helpful.

A Solution

To overcome this problem, I needed to make one very minor change (one line) to the Tracer class in the Logging Application Block. The line of code shown above is in the GetExecutingMethodName method in the Tracer class. This is line 264 of the Tracer.cs file in the Microsoft.Practices.EnterpriseLibrary.Logging assembly of the Enterprise Library Logging Application Block (Jan 2006). This line is changed from:

if (method.DeclaringType != GetType())

to

if (!typeof(Tracer).IsAssignableFrom(method.DeclaringType))

With this change, the executing method found by the GetExecutingMethodName method will be the method that invoked our sub-class. With the original code, the if test would be satisfied for the first method going back in the stack trace that was not defined in the base Tracer class. With the new code, the if test would only be satisfied for the first method going back in the stack trace that was not defined in the base Tracer class or one of its sub-classes (including our derived Tracer class).

Demo Visual Studio Solution

The demo Visual Studio solution demonstrates the change to the Tracer class in the Logging Application Block, the new wrapper classes, and an updated version of the Logging Quick Start (provided with the Enterprise Library) that uses our wrapper classes.

There are three projects in the solution:

Logging – this the Enterprise Library Logging Application Block with the one line code change.

EntLibLoggingExtended – this is a class library with the new wrapper classes.

EntLibLoggingExQuickStart – this is the Enterprise Library Logging Quick Start modified to use the wrapper classes.

Using the Code

If you have not already done so, download and install the Enterprise Library from here.

Modify line line 264 of the Tracer.cs file in the Logging project of the Enterprise Library, as shown above, and recompile the Enterprise Library.

Use the EntLibLoggingExtended class in the demo solution as the basis for your own logging assembly. Ensure you include the LogEntry, Logger, and Tracer classes. Modify the Priority and Category enums in the Globals.cs file to suit your requirements.

Add references in your own application projects to your logging assembly and also to the Enterprise Library.

Modify the logging configuration settings in the config file for your application using the Enterprise Library configuration tool. Ensure the Category values in the configuration and the values of the Category enum are consistent.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.