Best practice per il wrapper del logger


91

Voglio usare un nlogger nella mia applicazione, forse in futuro dovrò cambiare il sistema di registrazione. Quindi voglio usare una facciata di registrazione.

Conosci qualche consiglio per esempi esistenti su come scrivere quelli? Oppure dammi un link ad alcune best practice in questo settore.



Hai controllato il progetto The Simple Logging Facade su Codeplex ?
JefClaes

Risposte:


207

Prima usavo le facciate di logging come Common.Logging (anche per nascondere la mia libreria CuttingEdge.Logging ), ma oggigiorno utilizzo il pattern Dependency Injection e questo mi permette di nascondere i logger dietro la mia (semplice) astrazione che aderisce a entrambe le dipendenze Principio di inversione e principio di segregazione dell'interfaccia(ISP) perché ha un membro e perché l'interfaccia è definita dalla mia applicazione; non una libreria esterna. Riducendo al minimo la conoscenza che le parti principali della tua applicazione hanno sull'esistenza di librerie esterne, meglio è; anche se non hai intenzione di sostituire mai la tua libreria di log. La forte dipendenza dalla libreria esterna rende più difficile testare il codice e complica la tua applicazione con un'API che non è mai stata progettata specificamente per la tua applicazione.

Ecco come appare spesso l'astrazione nelle mie applicazioni:

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

Facoltativamente, questa astrazione può essere estesa con alcuni semplici metodi di estensione (consentendo all'interfaccia di rimanere stretta e continuare ad aderire all'ISP). Questo rende il codice per i consumatori di questa interfaccia molto più semplice:

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

Dal momento che l'interfaccia contiene un solo metodo, è possibile creare facilmente ILoggerun'implementazione che proxy per log4net , a Serilog , Microsoft.Extensions.Logging , NLog o qualsiasi altra libreria di registrazione e configurare il contenitore DI iniettare in classi che hanno una ILoggerloro costruttore.

Si noti che avere metodi di estensione statici su un'interfaccia con un singolo metodo è abbastanza diverso dall'avere un'interfaccia con molti membri. I metodi di estensione sono solo metodi di supporto che creano un LogEntrymessaggio e lo passano attraverso l'unico metodo ILoggersull'interfaccia. I metodi di estensione diventano parte del codice del consumatore; non fa parte dell'astrazione. Ciò non solo consente ai metodi di estensione di evolversi senza la necessità di modificare l'astrazione, i metodi di estensione e ilLogEntryvengono sempre eseguiti quando viene utilizzata l'astrazione del logger, anche quando quel logger viene stubbed / deriso. Ciò fornisce maggiore certezza sulla correttezza delle chiamate al logger durante l'esecuzione in una suite di test. L'interfaccia a un membro rende anche i test molto più semplici; Avere un'astrazione con molti membri rende difficile creare implementazioni (come mock, adattatori e decoratori).

Quando si esegue questa operazione, non c'è quasi mai bisogno di un'astrazione statica che le facciate di registrazione (o qualsiasi altra libreria) potrebbero offrire.


4
@ GabrielEspinoza: Dipende totalmente dallo spazio dei nomi in cui inserisci i metodi di estensione. Se lo posizioni nello stesso spazio dei nomi dell'interfaccia o in uno spazio dei nomi radice del tuo progetto, il problema non esiste.
Steven,

2
@ user1829319 È solo un esempio. Sono sicuro che puoi trovare un'implementazione basata su questa risposta che si adatta alle tue esigenze particolari.
Steven

2
Continuo a non capirlo ... dov'è il vantaggio di avere i 5 metodi Logger come estensioni di ILogger e non essere membri di ILogger?
Elisabeth

3
@Elisabeth Il vantaggio è che puoi adattare l'interfaccia della facciata a QUALSIASI framework di registrazione, semplicemente implementando una singola funzione: "ILogger :: Log". I metodi di estensione assicurano che abbiamo accesso alle API "convenienti" (come "LogError", "LogWarning," ecc.), Indipendentemente dal framework che decidi di utilizzare. È un modo indiretto per aggiungere funzionalità di "classe base" comuni, nonostante si lavori con un'interfaccia C #.
BTownTKD

2
Ho bisogno di enfatizzare nuovamente un motivo per cui questo è fantastico. Up-conversione del codice da DotNetFramework a DotNetCore. I progetti in cui ho fatto questo, ho dovuto scrivere solo un singolo nuovo cemento. Quelli dove non l'ho fatto .... gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Sono contento di aver trovato questo "ritorno".
granadaCoder


8

A partire da ora, la soluzione migliore è utilizzare il pacchetto Microsoft.Extensions.Logging ( come sottolineato da Julian ). La maggior parte dei framework di registrazione può essere utilizzata con questo.

Definire la propria interfaccia, come spiegato nella risposta di Steven, va bene per casi semplici, ma manca di alcune cose che considero importanti:

  • Oggetti di registrazione e destrutturazione strutturati (la notazione @ in Serilog e NLog)
  • Costruzione / formattazione della stringa ritardata: poiché prende una stringa, deve valutare / formattare tutto quando viene chiamato, anche se alla fine l'evento non verrà registrato perché è al di sotto della soglia (costo della prestazione, vedi punto precedente)
  • Controlli condizionali come quello IsEnabled(LogLevel)che potresti desiderare, ancora una volta per motivi di prestazioni

Probabilmente puoi implementare tutto questo nella tua astrazione, ma a quel punto reinventerai la ruota.


4

In genere preferisco creare un'interfaccia come

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

e nel runtime inserisco una classe concreta che viene implementata da questa interfaccia.


11
E non dimenticate le LogWarninge LogCriticalmetodi e tutti i loro sovraccarichi. Facendo ciò violerai il principio di segregazione dell'interfaccia . Preferisco definire l' ILoggerinterfaccia con un unico Logmetodo.
Steven

2
Mi dispiace davvero, non era mia intenzione. Non c'è bisogno di vergognarsi. Vedo questo design molto spesso perché molti sviluppatori usano il popolare log4net (che usa questo design esatto) come esempio. Sfortunatamente quel design non è davvero buono.
Steven

2
Preferisco questo alla risposta di @Steven. Introduce una dipendenza da LogEntry, e quindi una dipendenza da LoggingEventType. L' ILoggerimplementazione deve occuparsi di questi LoggingEventTypes, probabilmente case/switch, però , che è un odore di codice . Perché nascondere la LoggingEventTypesdipendenza? L'attuazione deve gestire i livelli di registrazione in ogni caso , quindi sarebbe meglio esplicito su ciò che un'implementazione dovrebbe fare, piuttosto che nascondersi dietro un unico metodo con un argomento generale.
DharmaTurtle

1
Come esempio estremo, immagina un ICommandche ha un Handleche prende un object. Le implementazioni devono case/switchsuperare i tipi possibili per adempiere al contratto dell'interfaccia. Questo non è l'ideale. Non avere un'astrazione che nasconda una dipendenza che deve essere gestita comunque. Hanno invece un'interfaccia che afferma chiaramente cosa ci si aspetta: "Mi aspetto che tutti i logger gestiscano avvisi, errori, fatali, ecc.". Questo è preferibile a "Mi aspetto che tutti i logger gestiscano messaggi che includono avvisi, errori, fatali, ecc."
DharmaTurtle

Sono d'accordo sia con @Steven che con @DharmaTurtle. Inoltre LoggingEventTypedovrebbe essere chiamato LoggingEventLevelcome i tipi sono classi e dovrebbero essere codificati come tali in OOP. Per me non c'è differenza tra non usare un metodo di interfaccia e non usare il enumvalore corrispondente . Utilizzare invece ErrorLoggger : ILogger, InformationLogger : ILoggerdove ogni logger definisce il proprio livello. Quindi il DI deve iniettare i logger necessari, probabilmente tramite una chiave (enum), ma questa chiave non fa più parte dell'interfaccia. (Ora sei SOLIDO).
Wouter

4

Un'ottima soluzione a questo problema è emersa nella forma del progetto LibLog .

LibLog è un'astrazione di registrazione con supporto integrato per i principali logger tra cui Serilog, NLog, Log4net e Enterprise logger. Viene installato tramite il gestore dei pacchetti NuGet in una libreria di destinazione come file di origine (.cs) anziché come riferimento .dll. Questo approccio consente di includere l'astrazione della registrazione senza costringere la libreria ad assumere una dipendenza esterna. Consente inoltre all'autore di una libreria di includere la registrazione senza costringere l'applicazione in uso a fornire esplicitamente un logger alla libreria. LibLog utilizza la riflessione per capire quale logger concreto è in uso e collegarsi ad esso senza alcun codice di cablaggio esplicito nei progetti di libreria.

Quindi, LibLog è un'ottima soluzione per la registrazione all'interno di progetti di libreria. Basta fare riferimento e configurare un logger concreto (Serilog for the win) nella tua applicazione o servizio principale e aggiungi LibLog alle tue librerie!


L'ho usato per superare il problema del cambiamento di rottura di log4net (schifo) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Se lo ottieni da nuget, creerà effettivamente un file .cs nel codice anziché aggiungere riferimenti a DLL precompilate. Il file .cs ha lo spazio dei nomi per il progetto. Quindi, se disponi di livelli diversi (csprojs), avrai più versioni o dovrai eseguire il consolidamento in un csproj condiviso. Lo scoprirai quando proverai a usarlo. Ma come ho detto, questo è stato un salvavita con il problema del cambiamento di rottura di log4net.
granadaCoder


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.