<UPDATE>

There where some uhm… issues with the code in this post. Please go ahead and check the corrected version, complete with explanation and apologies :)

</UPDATE>

Ever found yourself writing quick and dirty logging classes -just to get out of the way- over and over again?
Ever wished you had a neat and decent logging mechanism you could reuse in your projects?  Search no more.

Before we start, you might want to download the code or download the binaries.

A short while ago, I was designing/building a relatively small system for a client and one of the key issues was reusability. Some parts would/might end up in a much bigger system so would I please take that into account when putting a design together.  Nothing new under the sun.

One of the modules was to be a logging module. The system was quite small, so Log4Net was out of the question. Still, I thought that the Log4Net concept was pretty neat (how's that for an understatement?) and tried to use some of the basic ideas for the client's logging module.
Of course, as it so often happens (right?) a big chunk of the small system got discarded and the rest was never used because other pressing matters took over and we all got new projects assigned. More business. Good.

Anyway, I still liked the idea of finishing the logging module so I took it home, rethought it and improved it and here I present it to you in a vitamin rich, highly digestible chunk:

How to build a lightweight, reusable and extensible logging component in C#.

Off we go, then

First off, what do I want the logging module -we'll call it LogManager- to be? I want it to be:

   - Simple
   - Unobtrusive
   - Reusable
   - Extensible: Must support all kinds of loggers (File loggers, console loggers, xml loggers, database loggers, ...)

Pretty common requirements, really. The only uhm... challenge I faced was, how can I make LogManager extensible and scalable without having to recompile the containing assembly every time I -or anyone else for that matter- wants to add a new type of logger?

Here's what I thought out:

image  
LogManager UML Diagram

The idea is that any instances of a concrete ILogger implementation are instantiated by means of LogManager.GetLogger(), which can instantiate any implementation of ILogger whether it resides in the LogManager assembly or not.

To the point, already!

Time to take look at some code, and we'll start with the ILogger interface. Nice and easy, just to make sure we warm up and stretch before we get to the weight lifting part.

public interface ILogger : IDisposable
{
    /// <summary>
    /// Determines the maximum log level.
    /// Example: If LogLevel.Warning, all warning, Info and debug messages will be logged.
    /// </summary>

    LogLevel LogLevel { get; set; }

    /// <summary>
    /// Logs an entry.
    /// </summary>
    /// <param name="text"></param>

    void Log(string text, LogLevel level);

    /// <summary>
    /// Logs an Exception.
    /// </summary>
    /// <param name="e"></param>

    void Log(Exception e);
}
ILogger Interface

The most remarkable thing about this bit of code is probably how nicely coloured it is, but to make sure we're leveled, just say that every class that implements ILogger will have to implement an overloaded Log method, one version for exceptions and another for any other message, and hold an instance of LogLevel implemented as a property.

Side Note: Please forgive me if I'm pointing out the obvious but I'm still geeting the hang of this kind of post. I'm just trying to make it accessible for every one (that would be public, wouldn't it?), beginners and advanced .NETters alike.

And so, without further dilation, we'll quite deliberately skip the LogLevel enumeration and move on to more interesting stuff: the LogManager.

Meet the Manager

LogManager is defined as a static class with only 2 properties and 1 overloaded method designed to instantiate and keep track of ILoggers.

The LogLevel property, unsurprisingly enough, sets the general level of messages to log -that is, when this property is set, all previously instantiated logs will start logging at the newly set level.

Then there's _activeLogs... _activeLogs begun, in my mind, as a single ILogger instance that would be set to the last-instantiated ILogger type. "Why would anybody want to be logging to 25 different kind of logs in one single app?" I thought. "Why wouldn't they?" I answered after a while. And it was in this dangerous fashion that I decided that _activeLogs had to be a List<ILogger>. And so it is.

Every time an ILogger instance is summoned into existence LogManager checks whether that concrete type of ILogger has been instantiated before. If so, it returns the existing instance. Otherwise it tries to create a new instance and returns it.

Time for code.

public static ILogger GetLogger(string logType, string initialization)
{
   Assembly logAssemby = Assembly.GetAssembly(typeof(LogManager));

   return GetLogger(logAssemby.GetName().Name, logType, initialization);
}
LogManager's GetLogger() Method - first overload
public static ILogger GetLogger(string assemblyName, string logType, string initialization)
{
   Type log = Type.GetType(assemblyName + "." + logType);

   // Avoid creating a new instance of an already active ILogger
   ILogger requestedLog = _activeLogs.Find(delegate(ILogger match)
   {
      return match.GetType() == log;
   });

   // Got it? return it.
   if (requestedLog != null)
      return requestedLog;

   // Determine wether the given type implements ILogger
   Type iLogger = log.GetInterface("ILogger");

   if (iLogger != null)
   {
      requestedLog = Activator.CreateInstance(log,
         BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.NonPublic,
         object[] { initialization }, null) as ILogger;

      requestedLog.LogLevel = _level;
      _activeLogs.Add(requestedLog);
   }

   return requestedLog;
}
LogManager's GetLogger() Method - second overload

Assembly, assembly on the wall...

As the more alert of you -the ones actually reading the code instead of only watching the colours- might have noticed, LogManager relies heavily on reflection in order to return a valid ILogger instance.

The first overload is a no-brainer; it simply calls the second overload passing the name of the LogManager assembly as a parameter, and thus it can only be used to initialise ILoggers found in the LogManager assembly.

What the second overload does is: It looks in the ILogger list it maintains to see if the type of ILogger we're trying to instantiate has already been instantiated and if so, it simply returns the found instance.
If no instance is found it checks whether the given type implements the ILogger interface. If it does, the method attempts to create an instance of the given type passing it initialization as a constructor parameter. It finally adds the newly-instantiated ILogger to the _activeLogs list and returns the ILogger.

Over and out

And that's it really. I've actually used the code in a pre-production environment recently (yeah, yeah, I was feeding you untested code) and it performs quite nicely, so let me know of any success (or drama) stories if you decide to use it yourselves!

By the way, I had the decency to include an ILogger implementation with the code (FileLogger), so that it can be used right away.

Enjoy!

kick it on DotNetKicks.com