Devo prendere ILogger, ILogger <T>, ILoggerFactory o ILoggerProvider per una libreria?


113

Questo può essere in qualche modo correlato a Pass ILogger o ILoggerFactory ai costruttori in AspNet Core? , tuttavia si tratta specificamente di Library Design , non di come l'applicazione effettiva che utilizza quelle librerie implementa la sua registrazione.

Sto scrivendo una libreria .net Standard 2.0 che verrà installata tramite Nuget e per consentire alle persone che la utilizzano di ottenere alcune informazioni di debug, dipendo da Microsoft.Extensions.Logging.Abstractions per consentire l'iniezione di un Logger standardizzato.

Tuttavia, vedo più interfacce e il codice di esempio sul Web a volte utilizza ILoggerFactorye crea un logger nel ctor della classe. C'è anche ILoggerProviderche sembra una versione di sola lettura di Factory, ma le implementazioni possono o meno implementare entrambe le interfacce, quindi dovrei scegliere. (Factory sembra più comune di Provider).

Alcuni codici che ho visto utilizzano l' ILoggerinterfaccia non generica e potrebbero persino condividere un'istanza dello stesso logger, e alcuni prendono un ILogger<T>ctor e si aspettano che il contenitore DI supporti i tipi generici aperti o la registrazione esplicita di ogni ILogger<T>variazione della mia libreria usi.

In questo momento, penso che ILogger<T>sia l'approccio giusto, e forse un ctor che non accetta quell'argomento e passa invece un Null Logger. In questo modo, se non è necessaria alcuna registrazione, non viene utilizzata. Tuttavia, alcuni contenitori DI scelgono il ctor più grande e quindi fallirebbero comunque.

Sono curioso di sapere cosa dovrei fare qui per creare la minima quantità di mal di testa per gli utenti, pur consentendo il supporto appropriato per la registrazione, se lo si desidera.

Risposte:


113

Definizione

Abbiamo 3 interfacce: ILogger, ILoggerProvidere ILoggerFactory. Diamo un'occhiata al codice sorgente per scoprire le loro responsabilità:

ILogger : è responsabile di scrivere un messaggio di log di un determinato livello di log .

ILoggerProvider : è responsabile della creazione di un'istanza di ILogger(non dovresti usarla ILoggerProviderdirettamente per creare un logger)

ILoggerFactory : è possibile registrare uno o più messaggi ILoggerProvidercon la fabbrica, che a sua volta li utilizza tutti per creare un'istanza di ILogger. ILoggerFactorydetiene una raccolta di ILoggerProviders.

Nell'esempio seguente, stiamo registrando 2 provider (console e file) con la fabbrica. Quando creiamo un logger, la fabbrica utilizza entrambi questi provider per creare un'istanza di logger:

ILoggerFactory factory = new LoggerFactory().AddConsole();    // add console provider
factory.AddProvider(new LoggerFileProvider("c:\\log.txt"));   // add file provider
Logger logger = factory.CreateLogger(); // <-- creates a console logger and a file logger

Quindi il logger stesso mantiene una raccolta di ILoggers e scrive il messaggio di log su tutti loro. Guardando il codice sorgente di Logger possiamo confermare che Loggerha un array di ILoggers(ie LoggerInformation[]), e allo stesso tempo sta implementando l' ILoggerinterfaccia.


Iniezione di dipendenza

La documentazione MS fornisce 2 metodi per iniettare un logger:

1. Iniezione in fabbrica:

public TodoController(ITodoRepository todoRepository, ILoggerFactory logger)
{
    _todoRepository = todoRepository;
    _logger = logger.CreateLogger("TodoApi.Controllers.TodoController");
}

crea un logger con Category = TodoApi.Controllers.TodoController .

2. Iniezione di un generico ILogger<T>:

public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger)
{
    _todoRepository = todoRepository;
    _logger = logger;
}

crea un logger con Category = nome del tipo completo di TodoController


A mio parere, ciò che rende la documentazione confusione è che non menziona nulla circa l'iniezione di un non generico, ILogger. Nello stesso esempio sopra, stiamo iniettando un non generico ITodoRepositorye tuttavia, non spiega perché non stiamo facendo lo stesso per ILogger.

Secondo Mark Seemann :

Un Injection Constructor non dovrebbe fare altro che ricevere le dipendenze.

L'iniezione di una fabbrica nel controller non è un buon approccio, perché non è responsabilità del controller inizializzare il logger (violazione dell'SRP). Allo stesso tempo l'iniezione di un generico ILogger<T>aggiunge rumore inutile. Per ulteriori dettagli, vedere il blog di Simple Injector : Cosa c'è di sbagliato nell'astrazione DI ASP.NET Core?

Ciò che dovrebbe essere iniettato (almeno secondo l'articolo sopra) è un non generico ILogger, ma poi, non è qualcosa che il contenitore DI integrato di Microsoft può fare e devi utilizzare una libreria DI di terze parti. Questi due documenti spiegano come usare le librerie di terze parti con .NET Core.


Questo è un altro articolo di Nikola Malovic, in cui spiega le sue 5 leggi dell'IoC.

La quarta legge dell'IoC di Nikola

Ogni costruttore di una classe che viene risolta non dovrebbe avere alcuna implementazione diversa dall'accettazione di un insieme delle proprie dipendenze.


3
La tua risposta e l'articolo di Steven sono più corretti e più deprimenti allo stesso tempo.
bokibeg

30

Sono tutti validi tranne ILoggerProvider. ILoggere ILogger<T>sono ciò che dovresti usare per la registrazione. Per ottenere un ILogger, usi un file ILoggerFactory. ILogger<T>è una scorciatoia per ottenere un logger per una particolare categoria (scorciatoia per il tipo come categoria).

Quando si utilizza ILoggerper eseguire la registrazione, ogni registrato ha la ILoggerProviderpossibilità di gestire quel messaggio di registro. Non è realmente valido per il consumo di codice da chiamare ILoggerProviderdirettamente nel file.


Grazie. Questo mi fa pensare che ILoggerFactorysarebbe fantastico come un modo semplice per collegare DI per i consumatori avendo solo 1 dipendenza ( "Dammi solo una Factory e creerò i miei logger" ), ma impedirebbe di utilizzare un logger esistente ( a meno che il consumatore non utilizzi un wrapper). Prendere un ILogger- generico o meno - consente al consumatore di darmi un logger che ha preparato appositamente, ma rende la configurazione DI potenzialmente molto più complessa (a meno che non venga utilizzato un contenitore DI che supporta Open Generics). Sembra corretto? In tal caso, penso che andrò con la fabbrica.
Michael Stum

3
@MichaelStum - Non sono sicuro di seguire la tua logica qui. Ti aspetti che i tuoi consumatori utilizzino un sistema DI ma poi vuoi prendere il controllo e gestire manualmente le dipendenze all'interno della tua libreria? Perché questo sembra l'approccio giusto?
Damien_The_Unbeliever

@Damien_The_Unbeliever Questo è un buon punto. Prendere una fabbrica sembra strano. Penso che invece di prendere un ILogger<T>, ne prenderò uno ILogger<MyClass>o anche solo ILoggerinvece - in questo modo, l'utente può collegarlo con una sola registrazione e senza richiedere generici aperti nel loro contenitore DI. Appoggiandosi al non generico ILogger, ma sperimenterà molto nel fine settimana.
Michael Stum

10

Quello ILogger<T>era quello reale che è fatto per DI. ILogger è nato per aiutare a implementare il pattern factory molto più facilmente, invece di scrivere da soli tutta la logica DI e Factory, questa è stata una delle decisioni più intelligenti in asp.net core.

Puoi scegliere tra:

ILogger<T>se hai bisogno di usare modelli di fabbrica e DI nel tuo codice o puoi usare il ILogger, per implementare una registrazione semplice senza bisogno di DI.

dato che, ILoggerProviderè solo un ponte per gestire ciascuno dei messaggi del registro registrato. Non è necessario utilizzarlo, in quanto non effettua nulla che si debba intervenire nel codice, ascolta l'ILoggerProvider registrato e gestisce i messaggi. Questo è tutto.


1
Cosa hanno a che fare ILogger e ILogger <T> con DI? O verrebbero iniettati, no?
Matthew Hostetler

In realtà è ILogger <TCategoryName> in MS Docs. Deriva da ILogger e non aggiunge nuove funzionalità ad eccezione del nome della classe da registrare. Ciò fornisce anche un tipo univoco in modo che il DI inietti il ​​logger denominato correttamente. Le estensioni Microsoft estendono il non generico this ILogger.
Samuel Danielson,

8

Restando fedeli alla domanda, credo che ILogger<T>sia l'opzione giusta, considerando lo svantaggio di altre opzioni:

  1. Injecting ILoggerFactoryforza il tuo utente a cedere il controllo della fabbrica di logger globale mutabile alla tua libreria di classi. Inoltre, accettando la ILoggerFactorytua classe ora puoi scrivere nel log con qualsiasi nome di categoria arbitrario con CreateLoggermetodo. Sebbene ILoggerFactorysia solitamente disponibile come singleton nel contenitore DI, io come utente dubiterei del motivo per cui qualsiasi libreria avrebbe bisogno di usarlo.
  2. Sebbene il metodo abbia questo ILoggerProvider.CreateLoggeraspetto, non è inteso per l'iniezione. Viene utilizzato in ILoggerFactory.AddProvidermodo che la fabbrica possa creare aggregati ILoggerche scrivono su più ILoggercreati da ogni provider registrato. Questo è chiaro quando si ispeziona l'implementazione diLoggerFactory.CreateLogger
  3. Anche accettare ILoggersembra la strada da percorrere, ma è impossibile con .NET Core DI. Questo in realtà suona come il motivo per cui avevano bisogno di fornire ILogger<T>in primo luogo.

Quindi, dopotutto, non abbiamo scelta migliore che ILogger<T>se dovessimo scegliere tra quelle classi.

Un altro approccio potrebbe essere quello di iniettare qualcos'altro che avvolge non generico ILogger, che in questo caso dovrebbe essere non generico. L'idea è che avvolgendolo con la tua classe, prendi il pieno controllo di come l'utente può configurarlo.


6

L'approccio predefinito dovrebbe essere ILogger<T>. Ciò significa che nel registro i registri della classe specifica saranno chiaramente visibili perché includeranno il nome completo della classe come contesto. Ad esempio, se il nome completo della tua classe è MyLibrary.MyClass, lo otterrai nelle voci di registro create da questa classe. Per esempio:

MyLibrary.MyClass: Informazioni: il mio registro delle informazioni

Dovresti usare il ILoggerFactoryse vuoi specificare il tuo contesto. Ad esempio, tutti i log della tua libreria hanno lo stesso contesto di log invece di ogni classe. Per esempio:

loggerFactory.CreateLogger("MyLibrary");

E poi il registro sarà simile a questo:

MyLibrary: Informazioni: il mio registro delle informazioni

Se lo fai in tutte le classi, il contesto sarà solo MyLibrary per tutte le classi. Immagino che tu voglia farlo per una libreria se non vuoi esporre la struttura della classe interna nei log.

Per quanto riguarda la registrazione opzionale. Penso che dovresti sempre richiedere ILogger o ILoggerFactory nel costruttore e lasciarlo al consumatore della libreria per disattivarlo o fornire un Logger che non fa nulla nell'inserimento delle dipendenze se non vogliono la registrazione. È molto facile disattivare la registrazione per un contesto specifico nella configurazione. Per esempio:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "MyLibrary": "None"
    }
  }
}

5

Per la progettazione di biblioteche un buon approccio sarebbe:

1.Non costringere i consumatori a iniettare il logger nelle tue classi. Crea semplicemente un altro ctor passando NullLoggerFactory.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

    public MyClass():this(NullLoggerFactory.Instance)
    {

    }
    public MyClass(ILoggerFactory loggerFactory)
    {
      this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
    }
}

2. Limita il numero di categorie che utilizzi quando crei i logger per consentire ai consumatori di configurare facilmente il filtraggio dei log. this._loggerFactory.CreateLogger(Consts.CategoryName)

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.