Best Practice für Logger-Wrapper

91

Ich möchte einen nlogger in meiner Anwendung verwenden, möglicherweise muss ich in Zukunft das Protokollierungssystem ändern. Ich möchte also eine Holzfassade verwenden.

Kennen Sie Empfehlungen für vorhandene Beispiele, wie diese geschrieben werden? Oder geben Sie mir einfach einen Link zu einigen Best Practices in diesem Bereich.

Nachtwanderer
quelle
4
Es existiert bereits: netcommon.sourceforge.net
R. Martinho Fernandes
Siehe auch
Steven

Antworten:

206

Früher habe ich Protokollierungsfassaden wie Common.Logging verwendet (sogar um meine eigene CuttingEdge.Logging- Bibliothek auszublenden ), aber heute verwende ich das Abhängigkeitsinjektionsmuster. Dadurch kann ich Protokollierer hinter meiner eigenen (einfachen) Abstraktion ausblenden, die beiden Abhängigkeiten entspricht Inversionsprinzip und das Prinzip der Schnittstellentrennung(ISP), weil es ein Mitglied hat und weil die Schnittstelle von meiner Anwendung definiert wird; keine externe Bibliothek. Je besser das Wissen ist, das die Kernteile Ihrer Anwendung über die Existenz externer Bibliotheken haben, desto besser. auch wenn Sie nicht beabsichtigen, Ihre Protokollierungsbibliothek jemals zu ersetzen. Die starke Abhängigkeit von der externen Bibliothek erschwert das Testen Ihres Codes und erschwert Ihre Anwendung mit einer API, die nie speziell für Ihre Anwendung entwickelt wurde.

So sieht die Abstraktion in meinen Anwendungen oft aus:

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;
    }
}

Optional kann diese Abstraktion mit einigen einfachen Erweiterungsmethoden erweitert werden (so dass die Schnittstelle eng bleibt und weiterhin am ISP festhält). Dies macht den Code für die Benutzer dieser Schnittstelle viel einfacher:

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.
}

Da die Schnittstelle nur eine einzige Methode enthält, können Sie problemlos eine ILoggerImplementierung erstellen, die als Proxy für log4net fungiert , zu Serilog , Microsoft.Extensions.Logging , NLog oder jede andere Logging - Bibliothek und Ihre DI - Container konfigurieren es in Klassen zu injizieren , die eine haben ILoggerin ihren Konstrukteur.

Beachten Sie, dass sich statische Erweiterungsmethoden über einer Schnittstelle mit einer einzelnen Methode erheblich von einer Schnittstelle mit vielen Mitgliedern unterscheiden. Die Erweiterungsmethoden sind nur Hilfsmethoden, die eine LogEntryNachricht erstellen und über die einzige Methode auf der ILoggerSchnittstelle weiterleiten. Die Erweiterungsmethoden werden Teil des Verbrauchercodes. nicht Teil der Abstraktion. Dies ermöglicht nicht nur die Weiterentwicklung der Erweiterungsmethoden, ohne dass die Abstraktion, die Erweiterungsmethoden und die geändert werden müssenLogEntryKonstruktoren werden immer ausgeführt, wenn die Logger-Abstraktion verwendet wird, auch wenn dieser Logger gestubbt / verspottet ist. Dies gibt mehr Sicherheit über die Richtigkeit der Aufrufe des Loggers, wenn er in einer Testsuite ausgeführt wird. Die eingliedrige Oberfläche erleichtert auch das Testen erheblich. Eine Abstraktion mit vielen Mitgliedern macht es schwierig, Implementierungen (wie Mocks, Adapter und Dekoratoren) zu erstellen.

Wenn Sie dies tun, ist kaum eine statische Abstraktion erforderlich, die Protokollierungsfassaden (oder eine andere Bibliothek) bieten könnten.

Steven
quelle
4
@GabrielEspinoza: Das hängt ganz vom Namespace ab, in dem Sie die Erweiterungsmethoden platzieren. Wenn Sie sie im selben Namespace wie die Schnittstelle oder in einem Root-Namespace Ihres Projekts platzieren, besteht das Problem nicht.
Steven
2
@ user1829319 Es ist nur ein Beispiel. Ich bin sicher, dass Sie basierend auf dieser Antwort eine Implementierung entwickeln können, die Ihren speziellen Anforderungen entspricht.
Steven
2
Ich verstehe es immer noch nicht ... wo ist der Vorteil, die 5 Logger-Methoden als Erweiterungen von ILogger zu haben und nicht Mitglieder von ILogger zu sein?
Elisabeth
3
@Elisabeth Der Vorteil ist, dass Sie die Fassadenschnittstelle an JEDES Protokollierungsframework anpassen können, indem Sie einfach eine einzige Funktion implementieren: "ILogger :: Log". Die Erweiterungsmethoden stellen sicher, dass wir Zugriff auf "Convenience" -APIs (wie "LogError", "LogWarning" usw.) haben, unabhängig davon, für welches Framework Sie sich entscheiden. Es ist ein Umweg, um trotz der Arbeit mit einer C # -Schnittstelle allgemeine 'Basisklassen'-Funktionen hinzuzufügen.
BTownTKD
2
Ich muss noch einmal einen Grund hervorheben, warum dies großartig ist. Hochkonvertieren Ihres Codes von DotNetFramework zu DotNetCore. Bei den Projekten, bei denen ich das gemacht habe, musste ich nur einen einzigen neuen Beton schreiben. Diejenigen, bei denen ich nicht ... gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Ich bin froh, dass ich diesen "Weg zurück" gefunden habe.
GranadaCoder
8

Ab sofort ist es am besten, das Microsoft.Extensions.Logging- Paket zu verwenden ( wie von Julian hervorgehoben) ). Die meisten Protokollierungsframeworks können damit verwendet werden.

Das Definieren Ihrer eigenen Benutzeroberfläche, wie in Stevens Antwort erläutert , ist für einfache Fälle in Ordnung, es fehlen jedoch einige Dinge, die ich für wichtig halte:

  • Strukturierte Protokollierung und De-Strukturierung von Objekten (die @ -Notation in Serilog und NLog)
  • Verzögerte Zeichenfolgenkonstruktion / -formatierung: Da eine Zeichenfolge benötigt wird, muss beim Aufruf alles ausgewertet / formatiert werden, auch wenn das Ereignis am Ende nicht protokolliert wird, da es unter dem Schwellenwert liegt (Leistungskosten, siehe vorherigen Punkt).
  • Bedingte Prüfungen, wie IsEnabled(LogLevel)Sie sie aus Leistungsgründen noch einmal wünschen könnten

Sie können dies wahrscheinlich alles in Ihrer eigenen Abstraktion implementieren, aber an diesem Punkt werden Sie das Rad neu erfinden.

Philippe
quelle
4

Generell ziehe ich es vor, eine Schnittstelle wie zu erstellen

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

und zur Laufzeit füge ich eine konkrete Klasse ein, die über diese Schnittstelle implementiert wird.

verschlüsselt
quelle
11
Und nicht zu vergessen die LogWarningund LogCriticalMethoden und alle ihre Überlastungen. Wenn Sie dies tun, verletzen Sie das Prinzip der Schnittstellentrennung . Definieren Sie die ILoggerSchnittstelle lieber mit einer einzigen LogMethode.
Steven
2
Es tut mir wirklich leid, das war nicht meine Absicht. Sie müssen sich nicht schämen. Ich sehe dieses Design sehr oft, weil viele Entwickler das beliebte log4net (das genau dieses Design verwendet) als Beispiel verwenden. Leider ist das Design nicht wirklich gut.
Steven
2
Ich ziehe dies der Antwort von @ Steven vor. Er führt eine Abhängigkeit von LogEntryund damit eine Abhängigkeit von ein LoggingEventType. Die ILoggerImplementierung muss sich LoggingEventTypeswahrscheinlich mit diesen befassen case/switch, was ein Codegeruch ist . Warum die LoggingEventTypesAbhängigkeit verbergen ? Die Implementierung muss ohnehin mit den Protokollierungsstufen umgehen , daher ist es besser, explizit anzugeben, was eine Implementierung tun soll, als sie hinter einer einzelnen Methode mit einem allgemeinen Argument zu verbergen.
DharmaTurtle
1
Stellen Sie sich als extremes Beispiel ein, ICommanddas ein hat, das ein Handlenimmt object. Implementierungen müssen case/switchüber mögliche Typen hinausgehen, um den Vertrag der Schnittstelle zu erfüllen. Das ist nicht ideal. Keine Abstraktion, die eine Abhängigkeit verbirgt, die sowieso behandelt werden muss. Verwenden Sie stattdessen eine Schnittstelle, die klar angibt, was erwartet wird: "Ich erwarte, dass alle Logger Warnungen, Fehler, Fatals usw. verarbeiten." Dies ist vorzuziehen gegenüber "Ich erwarte, dass alle Logger Nachrichten verarbeiten , die Warnungen, Fehler, Fatals usw. enthalten."
DharmaTurtle
Ich stimme sowohl @Steven als auch @DharmaTurtle zu. Außerdem LoggingEventTypesollte aufgerufen werden, LoggingEventLevelda Typen Klassen sind und in OOP als solche codiert werden sollten. Für mich gibt es keinen Unterschied zwischen der Nichtverwendung einer Schnittstellenmethode und der Nichtverwendung des entsprechenden enumWerts. Verwenden Sie stattdessen ErrorLoggger : ILogger, InformationLogger : ILoggerwo jeder Logger seine eigene Ebene definiert. Dann muss der DI die benötigten Logger einspeisen, wahrscheinlich über einen Schlüssel (enum), aber dieser Schlüssel ist nicht mehr Teil der Schnittstelle. (Sie sind jetzt fest).
Wouter
4

Eine großartige Lösung für dieses Problem ist das LibLog- Projekt.

LibLog ist eine Protokollierungsabstraktion mit integrierter Unterstützung für wichtige Protokollierer, einschließlich Serilog, NLog, Log4net und Enterprise Logger. Es wird über den NuGet-Paketmanager in einer Zielbibliothek als Quelldatei (.cs) anstelle einer DLL-Referenz installiert. Dieser Ansatz ermöglicht das Einbeziehen der Protokollierungsabstraktion, ohne dass die Bibliothek gezwungen wird, eine externe Abhängigkeit anzunehmen. Außerdem kann ein Bibliotheksautor die Protokollierung einschließen, ohne dass die konsumierende Anwendung gezwungen ist, der Bibliothek explizit einen Protokollierer bereitzustellen. LibLog verwendet Reflection, um herauszufinden, welcher konkrete Logger verwendet wird, und um ihn ohne expliziten Verdrahtungscode in den Bibliotheksprojekten anzuschließen.

LibLog ist also eine großartige Lösung für die Protokollierung in Bibliotheksprojekten. Referenzieren und konfigurieren Sie einfach einen konkreten Logger (Serilog für den Gewinn) in Ihrer Hauptanwendung oder Ihrem Hauptdienst und fügen Sie LibLog zu Ihren Bibliotheken hinzu!

Rob Davis
quelle
Ich habe dies verwendet, um das Problem der Änderung von log4net ( yuck ) zu überwinden ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ). Wenn Sie dies von nuget erhalten, wird tatsächlich eine CS-Datei erstellt in Ihrem Code, anstatt Verweise auf vorkompilierte DLLs hinzuzufügen. Die CS-Datei hat einen Namespace für Ihr Projekt. Wenn Sie also verschiedene Ebenen (csprojs) haben, haben Sie entweder mehrere Versionen oder Sie müssen zu einem gemeinsam genutzten csproj konsolidieren. Sie werden dies herausfinden, wenn Sie versuchen, es zu verwenden. Aber wie gesagt, dies war ein Lebensretter mit dem Problem der Änderung von log4net.
GranadaCoder