Logger wrapper best practice

Night Walker picture Night Walker · Apr 13, 2011 · Viewed 58.3k times · Source

I want to use a nlogger in my application, maybe in the future I will need to change the logging system. So I want to use a logging facade.

Do you know any recommendations for existing examples how to write those ones ? Or just give me link to some best practice in this area.

Answer

Steven picture Steven · Apr 13, 2011

I used to use logging facades such as Common.Logging (even to hide my own CuttingEdge.Logging library), but nowadays I use the Dependency Injection pattern and this allows me to hide loggers behind my own (simple) abstraction that adheres to both Dependency Inversion Principle and the Interface Segregation Principle (ISP) because it has one member and because the interface is defined by my application; not an external library. Minimizing the knowledge that the core parts of your application have about the existence of external libraries, the better; even if you have no intention to ever replace your logging library. The hard dependency on the external library makes it harder to test your code, and complicates your application with an API that was never designed specifically for your application.

This is what the abstraction often looks like in my applications:

public interface ILogger
{
    void Log(LogEntry entry);
}

public enum LoggingEventType { Debug, Information, Warning, Error, Fatal };

// Immutable DTO that contains the log information.
public class LogEntry 
{
    public readonly LoggingEventType Severity;
    public readonly string Message;
    public readonly Exception Exception;

    public LogEntry(LoggingEventType severity, string message, Exception exception = null)
    {
        if (message == null) throw new ArgumentNullException("message");
        if (message == string.Empty) throw new ArgumentException("empty", "message");

        this.Severity = severity;
        this.Message = message;
        this.Exception = exception;
    }
}

Optionally, this abstraction can be extended with some simple extension methods (allowing the interface to stay narrow and keep adhering to the ISP). This makes the code for the consumers of this interface much simpler:

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message) {
        logger.Log(new LogEntry(LoggingEventType.Information, message));
    }

    public static void Log(this ILogger logger, Exception exception) {
        logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
    }

    // More methods here.
}

Since the interface contains just a single method, you can easily create an ILogger implementation that proxies to log4net, to Serilog, Microsoft.Extensions.Logging, NLog or any other logging library and configure your DI container to inject it in classes that have a ILogger in their constructor.

Do note that having static extension methods on top of an interface with a single method is quite different from having an interface with many members. The extension methods are just helper methods that create a LogEntry message and pass it through the only method on the ILogger interface. The extension methods become part of the consumer's code; not part of the abstraction. Not only does this allow the extension methods to evolve without the need to change the abstraction, the extension methods and the LogEntry constructor are always executed when the logger abstraction is used, even when that logger is stubbed/mocked. This gives more certainty about the correctness of calls to the logger when running in a test suite. The one-membered interface makes testing much easier as well; Having an abstraction with many members makes it hard to create implementations (such as mocks, adapters and decorators).

When you do this, there is hardly ever any need for some static abstraction that logging facades (or any other library) might offer.