Logger wrapper best practice

63,551

Solution 1

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. This allows me to hide loggers behind an application-defined 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 more difficult to test your code, and it 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 sealed class ConsoleLogger : ILogger
{
    public void Log(LogEntry entry)
}

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

// Immutable DTO that contains the log information.
public struct LogEntry
{
    public LoggingEventType Severity { get; }
    public string Message { get; }
    public Exception Exception { get; }

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

        this.Severity = severity;
        this.Message = msg;
        this.Exception = ex;
    }
}

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 ex) =>
        logger.Log(new LogEntry(LoggingEventType.Error, ex.Message, ex));

    // More methods here.
}

Because the interface contains just a single method, it becomes easily to 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. It is also easy to create an implementation that writes to the console, or a fake implementation that can be used for unit testing, as shown in the listing below:

public class ConsoleLogger : ILogger
{
    public void Log(LogEntry entry) => Console.WriteLine(
      $"[{entry.Severity}] {DateTime.Now} {entry.Message} {entry.Exception}");
}

public class FakeLogger : List<LogEntry>, ILogger
{
    public void Log(LogEntry entry) => this.Add(entry);
}

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. These extension methods themselves contain no Volatile Behavior of themselves and, therefore, won't hinder testability. You can easily test them if you wish, and they 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. I've shot myself in the foot with this many times, where my calls to the used third-party logger abstraction succeeded during my unit test, but still failed when executed in production.

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.

Still, even with this ILogger design, prefer designing your application in such way that only a few classes require a dependency on your ILogger abstraction. This answer talks about this in detail.

Solution 2

I used the small interface wrapper + adapter from https://github.com/uhaciogullari/NLog.Interface that is also available via NuGet:

PM> Install-Package NLog.Interface 

Solution 3

As of now, the best bet is to use the Microsoft.Extensions.Logging package (as pointed out by Julian). Most logging framework can be used with this.

Defining your own interface, as explained in Steven's answer is OK for simple cases, but it misses a few things that I consider important:

  • Structured logging and de-structuring objects (the @ notation in Serilog and NLog)
  • Delayed string construction/formatting: as it takes a string, it has to evaluate/format everything when called, even if in the end the event will not be logged because it's below the threshold (performance cost, see previous point)
  • Conditional checks like IsEnabled(LogLevel) which you might want, for performances reasons once again

You can probably implement all this in your own abstraction, but at that point you'll be reinventing the wheel.

Solution 4

A great solution to this problem has emerged in the form of the LibLog project.

LibLog is a logging abstraction with built-in support for major loggers including Serilog, NLog, Log4net and Enterprise logger. It is installed via the NuGet package manager into a target library as a source (.cs) file instead of a .dll reference. That approach allows the logging abstraction to be included without forcing the library to take on an external dependency. It also allows a library author to include logging without forcing the consuming application to explicitly provide a logger to the library. LibLog uses reflection to figure out what concrete logger is in use and hook up to it up without any explicit wiring code in the library project(s).

So, LibLog is a great solution for logging within library projects. Just reference and configure a concrete logger (Serilog for the win) in your main application or service and add LibLog to your libraries!

Solution 5

Generally I prefer to create an interface like

public interface ILogger
{
 void LogInformation(string msg);
 void LogError(string error);
}

and in the runtime i inject a concrete class that is implemented from this interface.

Share:
63,551
Night Walker
Author by

Night Walker

Updated on July 11, 2022

Comments

  • Night Walker
    Night Walker almost 2 years

    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.