Tuesday, November 27, 2012

NLog and logging abstractions

Consider following example: in your project you use third-party logging library and your developers are trying to log everything - so as a result it might be thousands of calls to that library in the code. Everything works well until something unexpected happens... For example it appears that new standards are set in your company and according to them only another logging library can be used. Or a critical issue in your library has been found and it forces you to switch from it to another implementation. It does not matter what causes change requirement - and changes like these are usual things in software development - usually you have to cope with them with minimal efforts.

I reckon it is good practice to introduce abstraction layer on top of a widely used third-party components such as logging. In fact this is a realization of letter "D" of SOLID principles. Depend upon Abstractions. Do not depend upon concretions - it says. And following this principle will ensure you that switching between third-party or our own components will be fast and painless.

Lets consider abstraction for my favorite .NET logging library NLog. First implementation that comes to mind is:
public interface ILogger
{
void Info(string message);
void Info(string message, Exception exc);
void Debug(string message);
void Debug(string message, Exception exc);
void Warn(string message);
void Warn(string message, Exception exc);
void Error(string message);
void Error(string message, Exception exc);
void Fatal(string message);
void Fatal(string message, Exception exc);
void Trace(string message);
void Trace(string message, Exception exc);
}
view raw ILogger.cs hosted with ❤ by GitHub

This abstraction gives possibility to create log messages with different priority and add exception information.
However if you implement this interface using NLog one important logger property will remain uninitialized - Name. This property is vital for identifying loggers and is often used for routing log messages and getting location in code where log message was created. While usually logger is initialized by calling LogManager.GetCurrentClassLogger() and this makes logger to have its Name referencing to a class where logger instance was initialized, there will be no use of this method when it is called inside of implementation of ILogger - all loggers will have same name.

The most straightforward approach is to add logger name parameter to abstraction and set it every time when call to logger is made. This requires extra coding and introduces complexity to your code. Not the best variant for utility services like logger.

Another solution of this problem is to set name with dependency injection framework. This is a good example how it can be done with Ninject.

Third approach became available after release of .NET 4.5 when set of caller attributes has been introduced. It does not require lot of coding and use of third-party libraries - you simply add string parameter with CallerFilePath attribute defined to all methods of the interface and its realization and initialize logger using processed value of this parameter. Resulting abstraction will be:
public interface ILogger
{
void Info(string message, [CallerFilePath] string sourceFilePath = null);
void Info(string message, Exception exc, [CallerFilePath] string sourceFilePath = null);
void Debug(string message, [CallerFilePath] string sourceFilePath = null);
void Debug(string message, Exception exc, [CallerFilePath] string sourceFilePath = null);
void Warn(string message, [CallerFilePath] string sourceFilePath = null);
void Warn(string message, Exception exc, [CallerFilePath] string sourceFilePath = null);
void Error(string message, [CallerFilePath] string sourceFilePath = null);
void Error(string message, Exception exc, [CallerFilePath] string sourceFilePath = null);
void Fatal(string message, [CallerFilePath] string sourceFilePath = null);
void Fatal(string message, Exception exc, [CallerFilePath] string sourceFilePath = null);
void Trace(string message, [CallerFilePath] string sourceFilePath = null);
void Trace(string message, Exception exc, [CallerFilePath] string sourceFilePath = null);
}
view raw ILogger.cs hosted with ❤ by GitHub
And Implementation:
public class Logger : ILogger
{
private static NLog.Logger GetInnerLogger(string sourceFilePath)
{
var logger = sourceFilePath == null ? LogManager.GetCurrentClassLogger() : LogManager.GetLogger(Path.GetFileName(sourceFilePath));
return logger;
}
public void Info(string message, [CallerFilePath] string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).Info(message);
}
public void Info(string message, Exception exc, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).InfoException(message, exc);
}
public void Debug(string message, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).Debug(message);
}
public void Debug(string message, Exception exc, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).DebugException(message,exc);
}
public void Warn(string message, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).Warn(message);
}
public void Warn(string message, Exception exc, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).WarnException(message, exc);
}
public void Error(string message, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).Error(message);
}
public void Error(string message, Exception exc, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).ErrorException(message, exc);
}
public void Fatal(string message, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).Fatal(message);
}
public void Fatal(string message, Exception exc, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).FatalException(message, exc);
}
public void Trace(string message, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).Trace(message);
}
public void Trace(string message, Exception exc, [CallerFilePath]string sourceFilePath = null)
{
GetInnerLogger(sourceFilePath).TraceException(message, exc);
}
}
view raw Logger.cs hosted with ❤ by GitHub
When you create log message by calling for example Info() method you don't have to set value to sourceFilePath parameter - a compiler will put current path of the source file in the time of the compilation. All you need is to define your strategy how to match the path to your source file and logger's name- in my example I simply use short file name as a name of the logger.

No comments:

Post a Comment