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.