Logger wrapper best practice
Asked Answered
E

7

95

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.

Enloe answered 13/4, 2011 at 9:6 Comment(3)
It already exists: netcommon.sourceforge.netKelso
Have you checked out The Simple Logging Facade project on Codeplex?Superphysical
Related: #9892637Superman
S
222

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.

Superman answered 13/4, 2011 at 9:11 Comment(25)
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.Twocycle
@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.Superman
@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.Twocycle
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.Merriemerrielle
@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.Superman
I wish I could give you more than +1. This is about as elegant a solution as I think I'll ever find.Amazonite
@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?Kaseykasha
@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.Superman
Steve, Can I get the code for this? Not sure what the log entry object is.Tibiotarsus
@Steve - I figure that but where can I get the LoggerExtensions class? Thanks.Tibiotarsus
@Steve - ok I'm assuming that the 3rd log entry constructor parameter us a format string.Tibiotarsus
this solution does not log the error detail e.g Error-Class, Error-Method, Error-Message etc.Marra
@Marra It's just an example. I'm sure you can come up with an implementation based on this answer that suits your particular needs.Superman
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?Frances
@Superman Is LoggingEventType your own enum? I can't find it anywhere :/Seating
@Frances 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.Allergist
One tweak: I recommend that you use get-only properties instead of fields.Gantt
@Superman this Requires class is an own implementation to check if the input is not empty?Saxhorn
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?Pragmatics
@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.Superman
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.Pragmatics
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)....Fatherless
would be the custom property values...that Common.Logging exposes (see github.com/net-commons/common-logging/blob/master/src/… ) Anyways, thanks for clear cut post.Fatherless
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".Fatherless
My DotNet core example code can be found here : #39610556Fatherless
E
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 
Eldenelder answered 20/2, 2013 at 15:59 Comment(1)
There is an ILogger interface in NLog library by v4.0. You don't need this library anymoreHeywood
H
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.

Heti answered 25/4, 2019 at 9:5 Comment(0)
T
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!

Thermaesthesia answered 9/5, 2016 at 18:31 Comment(1)
I've used this to get past the log4net breaking change issue (yuck) (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.Fatherless
B
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.

Bewilderment answered 13/4, 2011 at 9:13 Comment(5)
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. 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.Crine
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."Crine
I kind of agree with both @Superman 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).Marquardt
@Marquardt 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?Sholley
@Sholley 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.Marquardt
S
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.

Sayette answered 13/4, 2011 at 9:10 Comment(0)
W
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:

Welter answered 14/9, 2016 at 22:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.