94

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.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Night Walker
  • 20,638
  • 52
  • 151
  • 228

7 Answers7

221

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 more detail.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • How is this about ISP? You moved the variance from "knowing about multiple methods" to "knowing about multiple `LogEntry` constructors". Since you need to pass a `LogEntry`, you still need to know how to create one. So basically the interface is still as broad as before. The wikipedia article you link to states "ISP splits interfaces which are very large into smaller and more specific ones (...)" - there is no splitting going on here. There is merging - which i agree to be quite beneficial for the logging abstraction. – BatteryBackupUnit Mar 17 '14 at 11:24
  • @BatteryBackupUnit: The `ILogger` interface adheres to the ISP. It has to be, since it only contains one method. But remember: I didn't say anything about `LoggerExtensions` and how it relates to the ISP :-). Also note that consumers typically don't have to worry about creating the `LogEntry` class, since creation is done for you by the extension methods. – Steven Mar 20 '14 at 10:42
  • 1
    @Steven: That's like saying: umm instead of an interface with a 1000 methods let's just have one method with um, lets say, 2500 parameters,.. oh that's a bad idea and there's even an anti pattern named after it? oh yeah let's just create a class containing the 2500 parameters and use that class instead. Now we got one method with one parameter, so it adheres to ISP! We're so freaking cool! I'm going on a limb here and state that the interface you propose is better, but not because of ISP but rather because of how the actual logging components interfaces usually are designed. – BatteryBackupUnit Mar 20 '14 at 16:02
  • 1
    I would disagree on using Extension Methods because you will have to include the correct "using" statement in order to appear, this might confuse the developer. I would go for the abstract class. – Gabriel Espinoza Sep 21 '14 at 03:21
  • 4
    @GabrielEspinoza: That totally depends on the namespace yu place the extension methods in. If yu place it in the same namespace as the interface or in a root namespace of your project the problem won't exist. – Steven Sep 21 '14 at 05:51
  • 1
    I wish I could give you more than +1. This is about as elegant a solution as I think I'll ever find. – Khanzor Nov 16 '14 at 05:06
  • 1
    @Steven: The implementation of ILogger should just have a switch-statement on the LoggingEventType enum or have I misunderstood how this would integrate with error, info, trace and other logging options that e.g. NLog provides? – janhartmann Dec 03 '14 at 10:06
  • @meep: You will typically have to translate the `LoggingEventType` enum to something that is specific for the underlying logging framework. There are several ways to do this; a switch-case is one of them. – Steven Dec 03 '14 at 10:12
  • Steve, Can I get the code for this? Not sure what the log entry object is. – Paul Speranza Dec 12 '14 at 13:18
  • @Steve - I figure that but where can I get the LoggerExtensions class? Thanks. – Paul Speranza Dec 12 '14 at 21:19
  • @Steve - ok I'm assuming that the 3rd log entry constructor parameter us a format string. – Paul Speranza Dec 13 '14 at 13:02
  • this solution does not log the error detail e.g Error-Class, Error-Method, Error-Message etc. – user1829319 Jun 23 '15 at 10:25
  • 2
    @user1829319 It's just an example. I'm sure you can come up with an implementation based on this answer that suits your particular needs. – Steven Jun 23 '15 at 10:28
  • 2
    I still dont get it... where is the advantage of having the 5 Logger methods as extenstions of ILogger and not being members of ILogger? – Elisabeth Jun 26 '15 at 19:50
  • @Steven Is `LoggingEventType` your own enum? I can't find it anywhere :/ – Marshall Dec 14 '15 at 12:15
  • 3
    @Elisabeth The benefit is that you can adapt the facade interface to ANY logging framework, simply by implementing a single function: "ILogger::Log." The extension methods ensure that we have access to 'convenience' APIs (such as "LogError," "LogWarning," etc), regardless which framework you decide to use. It's a roundabout way to add common 'base class' functionality, despite working with a C# interface. – BTownTKD Jan 27 '16 at 16:44
  • 1
    One tweak: I recommend that you use get-only properties instead of fields. – ErikE Nov 23 '16 at 19:24
  • 1
    @Steven this Requires class is an own implementation to check if the input is not empty? – rogaa Jul 16 '17 at 11:43
  • Very elegant solution. It may just generate useless `LogEntry` objects when the severity is disabled. So, the insterface should contain something like `IsEnabledFor(LoggingEventType severity)`. And maybe `struct LogEntry` could be more efficient, too? – xmedeko Aug 27 '17 at 11:24
  • @xmedeko. This extra amount of GC objects should not be a problem for the majority of applications and having to have IsEnabledFor is a design smell itself. If performance is really an issue after you meusured this, make it a struct instead. – Steven Aug 27 '17 at 11:55
  • Anyway, some `IsDebugEnabled` is necessary in the interface when you need to construct some costly, special parameters (strings) for the debug logging and want to disable it in the production. – xmedeko Aug 27 '17 at 12:54
  • I love this approach. Simple. My one question/concern is the extension methods. I see how ILogger can be injected into any class who "depends on being able to log something".......but the extension methods are static. I haven't code tested, but most times static methods are not unit-test friendly (in most unit-test frameworks, not all). I would probably put 3 methods on the interface. The "flexible" one (already exist) and then the 2 most commonly used ones. Regardless of that nitpick, this is very clean, and keeps the code clean. The one feature that would be (cont on next comment).... – granadaCoder Sep 12 '18 at 20:35
  • would be the custom property values...that Common.Logging exposes (see https://github.com/net-commons/common-logging/blob/master/src/Common.Logging.Core/Logging/IVariablesContext.cs ) Anyways, thanks for clear cut post. – granadaCoder Sep 12 '18 at 20:37
  • 2
    I need to re-emphasize again a reason why this is great. Up-converting your code from DotNetFramework to DotNetCore. The projects where I did this, I only had to write a single new concrete. The ones where I didn't....gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! I'm glad i found this "way back". – granadaCoder Nov 16 '18 at 22:01
  • My DotNet core example code can be found here : https://stackoverflow.com/questions/39610056/implementation-and-usage-of-logger-wrapper-for-microsoft-extensions-logging/53395019#53395019 – granadaCoder Nov 20 '18 at 14:22
10

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 
Jon Adams
  • 24,464
  • 18
  • 82
  • 120
  • 11
    There is an ILogger interface in NLog library by v4.0. You don't need this library anymore – Jowen Aug 11 '15 at 12:41
10

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.

Philippe
  • 3,945
  • 3
  • 38
  • 56
5

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!

Rob Davis
  • 1,299
  • 1
  • 10
  • 22
  • 1
    I've used this to get past the log4net breaking change issue (yuck) (https://www.wiktorzychla.com/2012/03/pathetic-breaking-change-between.html) If you get this from nuget, it will actually create a .cs file in your code rather than adding references to precompiled dlls. The .cs file is namespaced to your project. So if you have different layers (csprojs), you'll either have multiple versions, or you need to consolidate to a shared csproj. You'll figure this out when you try to use it. But like I said, this was a lifesaver with the log4net breaking change issue. – granadaCoder Sep 12 '18 at 20:23
4

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.

crypted
  • 10,118
  • 3
  • 39
  • 52
  • 2
    I prefer this to @Steven's answer. He introduces a dependency to `LogEntry`, and thus a dependency on `LoggingEventType`. The `ILogger` implementation must deal with these `LoggingEventTypes`, probably though `case/switch`, which is a [code smell](http://wiki.c2.com/?SwitchStatementsSmell). Why hide the `LoggingEventTypes` dependency? The implementation must handle the logging levels *anyway*, so it would be better to *explicit* about what an implementation should do, rather than hiding it behind a single method with a general argument. – DharmaTurtle Feb 19 '17 at 05:59
  • 1
    As an extreme example, imagine an `ICommand` which has a `Handle` which takes an `object`. Implementations must `case/switch` over possible types in order to fulfill the interface's contract. This isn't ideal. Don't have an abstraction which hides a dependency that must be handled anyway. Instead have an interface that states plainly what is expected: "I expect all loggers to handle Warnings, Errors, Fatals, etc". This is preferable to "I expect all loggers to handle *messages which include* Warnings, Errors, Fatals, etc." – DharmaTurtle Feb 19 '17 at 06:01
  • I kind of agree with both @Steven and @DharmaTurtle. Besides `LoggingEventType` should be called `LoggingEventLevel` as types are classes and should be coded as such in OOP. For me there is no difference between not using an interface method v.s. not using the corresponding `enum` value. Instead use `ErrorLoggger : ILogger`, `InformationLogger : ILogger` where every logger defines it's own level. Then the DI needs to inject the needed loggers, probably via a key (enum), but this key is no longer part of the interface. (You are now SOLID). – Wouter Jun 10 '19 at 11:50
  • @Wouter I will not discount SOLID but you have to be pragmatic as there is no universal truth in programming only tradeoffs using different designs. But how will you with your example/design switch logging level at runtime which is a very common thing to do in e.g. Nlog, log4net etc? – Jesper Nov 08 '19 at 14:25
  • @Jesper the loglevel is the concrete ILogger type, the logger is either enabled or disabled. The loggers can share configuration for enabling or disabling based on a logLevelValue. Now if the loggers share a common configuration model you can enable or disable loggers based on a name and value, similar to log4net. – Wouter Nov 08 '19 at 19:56
3

Instead of writing your own facade, you could either use Castle Logging Services or Simple Logging Façade.

Both include adapters for NLog and Log4net.

Jonas Kongslund
  • 5,058
  • 2
  • 28
  • 27
2

Since 2015 you could also use .NET Core Logging if you're building .NET core applications.

The package for NLog to hook into is:

Julian
  • 33,915
  • 22
  • 119
  • 174