Devo usar ILogger, ILogger <T>, ILoggerFactory ou ILoggerProvider para uma biblioteca?

113

Isso pode estar relacionado a Pass ILogger ou ILoggerFactory para construtores no AspNet Core? , entretanto, trata-se especificamente do Design da Biblioteca , não de como o aplicativo real que usa essas bibliotecas implementa seu registro.

Estou escrevendo uma biblioteca .net Standard 2.0 que será instalada via Nuget e para permitir que as pessoas que usam essa biblioteca obtenham algumas informações de depuração, estou dependendo do Microsoft.Extensions.Logging.Abstractions para permitir que um Logger padronizado seja injetado.

No entanto, estou vendo várias interfaces e o código de amostra na web às vezes usa ILoggerFactorye cria um logger no ctor da classe. Há também o ILoggerProviderque parece uma versão somente leitura do Factory, mas as implementações podem ou não implementar ambas as interfaces, então eu teria que escolher. (Factory parece mais comum que Provider).

Alguns códigos que eu vi usam a ILoggerinterface não genérica e podem até compartilhar uma instância do mesmo registrador, e alguns pegam um ILogger<T>em seu ctor e esperam que o contêiner de DI suporte tipos genéricos abertos ou registro explícito de cada ILogger<T>variação minha biblioteca usa.

No momento, eu realmente acho que ILogger<T>é a abordagem certa, e talvez um ctor que não aceita esse argumento e apenas passa um Logger Nulo. Dessa forma, se nenhum registro for necessário, nenhum será usado. No entanto, alguns contêineres de DI escolhem o maior ctor e, portanto, falhariam de qualquer maneira.

Estou curioso para saber o que devo fazer aqui para criar o mínimo de dor de cabeça para os usuários e, ao mesmo tempo, permitir o suporte de registro adequado, se desejado.

Michael Stum
fonte

Respostas:

113

Definição

Temos 3 interfaces: ILogger, ILoggerProvidere ILoggerFactory. Vejamos o código-fonte para descobrir suas responsabilidades:

ILogger : é responsável por escrever uma mensagem de log de um determinado nível de log .

ILoggerProvider : é responsável por criar uma instância de ILogger(você não deve usar ILoggerProviderdiretamente para criar um logger)

ILoggerFactory : você pode registrar um ou mais ILoggerProviders com a fábrica, que por sua vez usa todos eles para criar uma instância de ILogger. ILoggerFactorycontém uma coleção de ILoggerProviders.

No exemplo abaixo, estamos registrando 2 provedores (console e arquivo) com a fábrica. Quando criamos um logger, a fábrica usa esses dois provedores para criar uma instância do 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

Portanto, o próprio logger mantém uma coleção de ILoggers e grava a mensagem de log para todos eles. Olhando para o código-fonte do Logger , podemos confirmar que Loggertem uma matriz de ILoggers(ou seja LoggerInformation[]) e, ao mesmo tempo, está implementando a ILoggerinterface.


Injeção de dependência

A documentação da MS fornece 2 métodos para injetar um logger:

1. Injetando a fábrica:

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

cria um Logger com Category = TodoApi.Controllers.TodoController .

2. Injetando um genérico ILogger<T>:

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

cria um logger com categoria = nome de tipo totalmente qualificado de TodoController


Na minha opinião, o que torna a documentação confusa é que não menciona nada sobre a injeção de um não genérico ILogger,. No mesmo exemplo acima, estamos injetando um não genérico ITodoRepositorye, ainda assim, não explica por que não estamos fazendo o mesmo para ILogger.

De acordo com Mark Seemann :

Um construtor de injeção não deve fazer mais do que receber as dependências.

Injetar uma fábrica no Controlador não é uma boa abordagem, porque não é responsabilidade do Controlador inicializar o Logger (violação do SRP). Ao mesmo tempo, injetar um genérico ILogger<T>adiciona ruído desnecessário. Consulte o blog do Simple Injector para obter mais detalhes: O que há de errado com a abstração ASP.NET Core DI?

O que deve ser injetado (pelo menos de acordo com o artigo acima) é um não genérico ILogger, mas isso não é algo que o contêiner DI integrado da Microsoft possa fazer e você precisa usar uma biblioteca de DI de terceiros. Esses dois documentos explicam como você pode usar bibliotecas de terceiros com o .NET Core.


Este é outro artigo de Nikola Malovic, no qual ele explica suas 5 leis de IoC.

4ª lei de IoC de Nikola

Cada construtor de uma classe sendo resolvida não deve ter nenhuma implementação diferente de aceitar um conjunto de suas próprias dependências.

Hooman Bahreini
fonte
3
Sua resposta e o artigo de Steven são muito corretos e deprimentes ao mesmo tempo.
bokibeg
30

Todos esses são válidos, exceto para ILoggerProvider. ILoggere ILogger<T>são o que você deve usar para registrar. Para obter um ILogger, você usa um ILoggerFactory. ILogger<T>é um atalho para obter um logger para uma categoria específica (atalho para o tipo como a categoria).

Quando você usa o ILoggerpara fazer o log, cada registrado ILoggerProvidertem a chance de lidar com essa mensagem de log. Não é realmente válido para consumir código chamar ILoggerProviderdiretamente para o .

Davidfowl
fonte
Obrigado. Isso me faz pensar que ILoggerFactoryseria uma ótima maneira de conectar o DI para consumidores por ter apenas 1 dependência ( "Apenas me dê uma fábrica e eu criarei meus próprios loggers" ), mas evitaria o uso de um logger existente ( a menos que o consumidor use algum invólucro). Usar um ILogger- genérico ou não - permite que o consumidor me dê um registrador que ele preparou especialmente, mas torna a configuração de DI potencialmente muito mais complexa (a menos que um contêiner de DI que suporte Open Generics seja usado). Isso soa correto? Nesse caso, acho que vou com a fábrica.
Michael Stum
3
@MichaelStum - Não tenho certeza se estou seguindo sua lógica aqui. Você está esperando que seus consumidores usem um sistema DI, mas então quer assumir e gerenciar manualmente as dependências em sua biblioteca? Por que essa parece a abordagem certa?
Damien_The_Unbeliever
@Damien_The_Unbeliever Esse é um bom ponto. Tomar uma fábrica parece estranho. Acho que em vez de pegar um ILogger<T>, vou pegar um ILogger<MyClass>ou mesmo apenas em ILoggervez disso - dessa forma, o usuário pode conectá-lo com apenas um único registro, e sem exigir genéricos abertos em seu contêiner DI. Inclinando-se para o não genérico ILogger, mas experimentará muito no fim de semana.
Michael Stum
10

O ILogger<T>foi o real que é feito para o DI. O ILogger veio para ajudar a implementar o padrão de fábrica com muito mais facilidade, em vez de você escrever por conta própria toda a DI e lógica de fábrica, que foi uma das decisões mais inteligentes no núcleo do asp.net.

Você pode escolher entre:

ILogger<T>se você precisa usar padrões de fábrica e DI em seu código ou pode usar o ILogger, para implementar o registro simples sem necessidade de DI.

visto que, o The ILoggerProvideré apenas uma ponte para lidar com cada uma das mensagens do log registrado. Não há necessidade de utilizá-lo, pois não tem efeito em nada que você deva intervir no código, ele escuta o ILoggerProvider registrado e trata as mensagens. É sobre isso.

Barr J
fonte
1
O que ILogger vs ILogger <T> tem a ver com DI? Qualquer um seria injetado, não?
Matthew Hostetler
Na verdade, é ILogger <TCategoryName> no MS Docs. Ele deriva de ILogger e não adiciona nenhuma nova funcionalidade, exceto o nome da classe para registrar. Isso também fornece um tipo exclusivo para que o DI injete o logger nomeado corretamente. As extensões da Microsoft estendem o não genérico this ILogger.
Samuel Danielson
8

Atendo-me à pergunta, acredito que ILogger<T>seja a opção certa, considerando o lado negativo de outras opções:

  1. A injeção ILoggerFactoryforça seu usuário a ceder o controle da fábrica mutável do logger global para sua biblioteca de classes. Além disso, ao aceitar ILoggerFactorysua classe, agora você pode escrever no log com qualquer nome de categoria arbitrário com CreateLoggermétodo. Embora ILoggerFactorygeralmente esteja disponível como um singleton no contêiner de DI, eu, como usuário, duvido do motivo de qualquer biblioteca precisar usá-lo.
  2. Embora o método ILoggerProvider.CreateLoggerpareça, não se destina a injeção. É usado ILoggerFactory.AddProviderpara que a fábrica possa criar agregados ILoggerque gravam em vários ILoggercriados a partir de cada provedor registrado. Isso fica claro quando você inspeciona a implementação deLoggerFactory.CreateLogger
  3. Aceitar ILoggertambém parece o caminho a seguir, mas é impossível com o .NET Core DI. Na verdade, esse parece ser o motivo pelo qual eles precisaram fornecer ILogger<T>em primeiro lugar.

Afinal, não temos escolha melhor do ILogger<T>que escolher entre essas classes.

Outra abordagem seria injetar algo mais que envolvesse o não genérico ILogger, que neste caso deveria ser um não genérico. A ideia é que, ao envolvê-lo com sua própria classe, você assume o controle total de como o usuário pode configurá-lo.

tia
fonte
6

A abordagem padrão deve ser ILogger<T>. Isso significa que no log os logs da classe específica estarão claramente visíveis porque incluirão o nome completo da classe como contexto. Por exemplo, se o nome completo da sua classe for, MyLibrary.MyClassvocê obterá isso nas entradas de registro criadas por esta classe. Por exemplo:

MyLibrary.MyClass: Information: My information log

Você deve usar o ILoggerFactoryse quiser especificar seu próprio contexto. Por exemplo, todos os logs de sua biblioteca têm o mesmo contexto de log em vez de todas as classes. Por exemplo:

loggerFactory.CreateLogger("MyLibrary");

E então o log ficará assim:

MyLibrary: Information: My information log

Se você fizer isso em todas as classes, o contexto será apenas MyLibrary para todas as classes. Imagino que você gostaria de fazer isso para uma biblioteca se não quiser expor a estrutura de classe interna nos logs.

Quanto ao registro opcional. Acho que você deve sempre exigir o ILogger ou ILoggerFactory no construtor e deixar para o consumidor da biblioteca desligá-lo ou fornecer um Logger que não faz nada na injeção de dependência se não quiser fazer o log. É muito fácil desligar o registro para um contexto específico na configuração. Por exemplo:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "MyLibrary": "None"
    }
  }
}
AlesD
fonte
5

Para um projeto de biblioteca, uma boa abordagem seria:

1. Não force os consumidores a injetar logger em suas classes. Simplesmente crie outro ctor passando NullLoggerFactory.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

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

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

2. Limite o número de categorias que você usa ao criar registradores para permitir que os consumidores configurem a filtragem de registros facilmente. this._loggerFactory.CreateLogger(Consts.CategoryName)

Ihar Yakimush
fonte