Najlepsza praktyka otoki rejestratora

91

Chcę używać nloggera w mojej aplikacji, być może w przyszłości będę musiał zmienić system logowania. Dlatego chcę użyć elewacji z drewna.

Czy znasz jakieś zalecenia dotyczące istniejących przykładów, jak je napisać? Lub po prostu podaj mi link do najlepszych praktyk w tej dziedzinie.

Night Walker
źródło
4
Już istnieje: netcommon.sourceforge.net
R. Martinho Fernandes
Czy sprawdzałeś projekt The Simple Logging Facade na Codeplex ?
JefClaes

Odpowiedzi:

207

Kiedyś używałem fasad rejestrowania, takich jak Common.Logging (nawet w celu ukrycia mojej własnej biblioteki CuttingEdge.Logging ), ale obecnie używam wzorca Dependency Injection, co pozwala mi ukryć rejestratory za własną (prostą) abstrakcją, która jest zgodna z obydwoma Zależnościami Zasada inwersji i zasada segregacji interfejsów(ISP), ponieważ ma jednego członka i ponieważ interfejs jest zdefiniowany przez moją aplikację; nie jest to biblioteka zewnętrzna. Im mniej wiedzy na temat istnienia bibliotek zewnętrznych, które mają podstawowe części aplikacji, tym lepiej; nawet jeśli nie masz zamiaru kiedykolwiek zastępować swojej biblioteki rejestrowania. Twarda zależność od biblioteki zewnętrznej utrudnia testowanie kodu i komplikuje aplikację za pomocą interfejsu API, który nigdy nie został zaprojektowany specjalnie dla tej aplikacji.

Tak często wygląda abstrakcja w moich aplikacjach:

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

Opcjonalnie tę abstrakcję można rozszerzyć za pomocą kilku prostych metod rozszerzających (pozwalając interfejsowi pozostać wąskim i zachować zgodność z dostawcą usług internetowych). To sprawia, że ​​kod dla użytkowników tego interfejsu jest znacznie prostszy:

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

Ponieważ interfejs zawiera tylko jedną metodę, możesz łatwo utworzyć ILoggerimplementację, która będzie pośredniczyła w log4net , Serilog , Microsoft.Extensions.Logging , NLog lub dowolnej innej bibliotece rejestrowania i skonfiguruj swój kontener DI tak, aby wstrzykiwał go w klasach, które mają ILoggerw swoim konstruktor.

Zwróć uwagę, że posiadanie statycznych metod rozszerzających na interfejsie z pojedynczą metodą różni się od posiadania interfejsu z wieloma składowymi. Metody rozszerzające to tylko metody pomocnicze, które tworzą LogEntrywiadomość i przekazują ją przez jedyną metodę w ILoggerinterfejsie. Metody rozszerzające stają się częścią kodu konsumenta; nie jest częścią abstrakcji. Nie tylko umożliwia to ewolucję metod rozszerzających bez konieczności zmiany abstrakcji, metod rozszerzających i rozszerzeniaLogEntrykonstruktor są zawsze wykonywane, gdy używana jest abstrakcja rejestratora, nawet jeśli ten rejestrator jest ukryty / mockowany. Daje to większą pewność co do poprawności wywołań programu rejestrującego podczas uruchamiania w zestawie testów. Jednoczłonowy interfejs również znacznie ułatwia testowanie; Posiadanie abstrakcji z wieloma członkami utrudnia tworzenie implementacji (takich jak makiety, adaptery i dekoratory).

Kiedy to robisz, prawie nigdy nie ma potrzeby stosowania statycznej abstrakcji, jaką mogą oferować fasady logowania (lub jakakolwiek inna biblioteka).

Steven
źródło
4
@GabrielEspinoza: To całkowicie zależy od przestrzeni nazw, w której umieścisz metody rozszerzające. Jeśli umieścisz je w tej samej przestrzeni nazw, co interfejs lub w głównej przestrzeni nazw projektu, problem nie będzie istniał.
Steven
2
@ user1829319 To tylko przykład. Jestem pewien, że możesz wymyślić implementację opartą na tej odpowiedzi, która odpowiada Twoim konkretnym potrzebom.
Steven
2
Nadal nie rozumiem ... Jaka jest zaleta posiadania metod 5 Logger jako rozszerzeń ILogger i nie bycia członkami ILogger?
Elisabeth
3
@Elisabeth Zaletą jest to, że możesz dostosować interfejs fasady do DOWOLNEGO środowiska logowania, po prostu zaimplementując jedną funkcję: „ILogger :: Log”. Metody rozszerzające zapewniają, że mamy dostęp do „wygodnych” interfejsów API (takich jak „LogError”, „LogWarning” itp.), Niezależnie od tego, którego frameworka zdecydujesz się użyć. Jest to okrężny sposób na dodanie wspólnej funkcjonalności „klasy bazowej”, pomimo pracy z interfejsem C #.
BTownTKD,
2
Muszę jeszcze raz podkreślić powód, dla którego jest to wspaniałe. Konwertowanie kodu z DotNetFramework do DotNetCore. Projekty, w których to robiłem, musiałem napisać tylko jeden nowy beton. Te, w których tego nie zrobiłem… gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Cieszę się, że znalazłem to „w drodze powrotnej”.
granadaCoder
8

Jak na razie najlepiej jest użyć pakietu Microsoft.Extensions.Logging ( jak wskazał Julian ). Do tego można użyć większości struktur rejestrowania.

Zdefiniowanie własnego interfejsu, jak wyjaśniono w odpowiedzi Stevena, jest w porządku w prostych przypadkach, ale pomija kilka rzeczy, które uważam za ważne:

  • Rejestrowanie strukturalne i usuwanie struktury obiektów (notacja @ w Serilog i NLog)
  • Opóźniona konstrukcja / formatowanie ciągu: ponieważ przyjmuje ciąg, musi ocenić / sformatować wszystko po wywołaniu, nawet jeśli ostatecznie zdarzenie nie zostanie zarejestrowane, ponieważ jest poniżej progu (koszt wydajności, patrz poprzedni punkt)
  • Testy warunkowe, takie jak IsEnabled(LogLevel)te, które możesz chcieć, jeszcze raz ze względu na wydajność

Prawdopodobnie możesz zaimplementować to wszystko we własnej abstrakcji, ale w tym momencie wymyślisz koło na nowo.

Philippe
źródło
4

Generalnie wolę stworzyć interfejs taki jak

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

aw środowisku wykonawczym wstrzykuję konkretną klasę, która jest implementowana z tego interfejsu.

zaszyfrowane
źródło
11
I nie zapomnieć LogWarningi LogCriticalmetod i wszystkie ich przeciążenia. W ten sposób naruszysz zasadę segregacji interfejsów . Preferuj definiowanie ILoggerinterfejsu za pomocą jednej Logmetody.
Steven
2
Bardzo mi przykro, to nie był mój zamiar. Nie ma się czego wstydzić. Widzę ten projekt naprawdę często, ponieważ wielu programistów używa popularnego log4net (który używa dokładnie tego projektu) jako przykładu. Niestety ten projekt nie jest dobry.
Steven
2
Wolę to od odpowiedzi @ Stevena. Wprowadza zależność do LogEntry, a tym samym zależność od LoggingEventType. ILoggerRealizacja musi radzić sobie z nimi LoggingEventTypes, choć zapewne case/switch, co to zapach kod . Po co ukrywać LoggingEventTypeszależność? Realizacja musi obsługiwać poziomy rejestrowania w każdym razie , więc byłoby lepiej, aby jednoznacznie o tym, co należy zrobić, implementacja, zamiast ukrywać je za pomocą jednej metody z ogólnym argumentem.
DharmaTurtle
1
Jako skrajny przykład wyobraźmy sobie, ICommandktóry ma a Handlektóry przyjmuje object. Implementacje muszą case/switchprzekraczać możliwe typy, aby spełnić kontrakt interfejsu. To nie jest idealne. Nie używaj abstrakcji, która ukrywa zależność, którą i tak trzeba obsłużyć. Zamiast tego należy mieć interfejs, który jasno określa, czego się oczekuje: „Oczekuję, że wszystkie programy rejestrujące będą obsługiwać ostrzeżenia, błędy, błędy krytyczne itp.”. Jest to lepsze niż „Oczekuję, że wszystkie programy rejestrujące będą obsługiwać komunikaty zawierające ostrzeżenia, błędy, błędy krytyczne itp.”
DharmaTurtle
W pewnym sensie zgadzam się zarówno z @Steven, jak i @DharmaTurtle. Poza tym LoggingEventTypepowinny być nazywane, LoggingEventLevelponieważ typy są klasami i powinny być zakodowane jako takie w OOP. Dla mnie nie ma różnicy między niestosowaniem metody interfejsu a niestosowaniem odpowiedniej enumwartości. Zamiast tego użyj ErrorLoggger : ILogger, InformationLogger : ILoggergdzie każdy rejestrator definiuje swój własny poziom. Następnie DI musi wstrzyknąć potrzebne rejestratory, prawdopodobnie za pomocą klucza (wyliczenia), ale ten klucz nie jest już częścią interfejsu. (Jesteś teraz SOLIDNY).
Wouter
4

Świetne rozwiązanie tego problemu pojawiło się w postaci projektu LibLog .

LibLog to abstrakcja rejestrowania z wbudowaną obsługą głównych rejestratorów, w tym Serilog, NLog, Log4net i Enterprise logger. Jest instalowany za pośrednictwem menedżera pakietów NuGet w bibliotece docelowej jako plik źródłowy (.cs) zamiast odwołania .dll. Takie podejście umożliwia uwzględnienie abstrakcji rejestrowania bez zmuszania biblioteki do przyjęcia zewnętrznej zależności. Pozwala również autorowi biblioteki na dołączenie rejestrowania bez zmuszania używającej aplikacji do jawnego dostarczania programu rejestrującego do biblioteki. LibLog używa refleksji, aby dowiedzieć się, jaki konkretny rejestrator jest używany i połączyć się z nim bez żadnego jawnego kodu okablowania w projektach biblioteki.

Tak więc LibLog to świetne rozwiązanie do logowania w projektach bibliotek. Po prostu odwołaj się i skonfiguruj konkretny rejestrator (Serilog dla wygranej) w swojej głównej aplikacji lub usłudze i dodaj LibLog do swoich bibliotek!

Rob Davis
źródło
Użyłem tego, aby ominąć problem z zerwaniem zmiany log4net (fuj) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Jeśli otrzymasz to z nuget, w rzeczywistości utworzy plik .cs w kodzie, zamiast dodawać odwołania do prekompilowanych bibliotek dll. Plik .cs jest przestrzenią nazw projektu. Więc jeśli masz różne warstwy (csprojs), będziesz mieć wiele wersji lub musisz skonsolidować do udostępnionego csproj. Zrozumiesz to, gdy spróbujesz go użyć. Ale jak powiedziałem, było to uratowanie życia z problemem zmiany zerwania log4net.
granadaCoder