Prática recomendada do logger wrapper

91

Quero usar um nlogger em meu aplicativo, talvez no futuro precise mudar o sistema de registro. Então, eu quero usar uma fachada de registro.

Você conhece alguma recomendação para os exemplos existentes de como escrevê-los? Ou apenas me dê um link para algumas das melhores práticas nesta área.

Night Walker
fonte
4
Já existe: netcommon.sourceforge.net
R. Martinho Fernandes
Você verificou o projeto The Simple Logging Facade no Codeplex ?
JefClaes

Respostas:

206

Eu costumava usar fachadas de registro, como Common.Logging (até mesmo para ocultar minha própria biblioteca CuttingEdge.Logging ), mas hoje em dia eu uso o padrão de injeção de dependência e isso me permite ocultar registradores atrás de minha própria abstração (simples) que adere a ambos Dependency Princípio de Inversão e o Princípio de Segregação de Interface(ISP) porque tem um membro e porque a interface é definida pela minha aplicação; não é uma biblioteca externa. Minimizar o conhecimento que as partes centrais de seu aplicativo têm sobre a existência de bibliotecas externas, melhor; mesmo se você não tiver a intenção de substituir sua biblioteca de registro. A forte dependência da biblioteca externa torna mais difícil testar seu código e complica seu aplicativo com uma API que nunca foi projetada especificamente para seu aplicativo.

É assim que a abstração costuma se parecer em meus aplicativos:

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

Opcionalmente, essa abstração pode ser estendida com alguns métodos de extensão simples (permitindo que a interface permaneça estreita e continue aderindo ao ISP). Isso torna o código para os consumidores desta interface muito mais simples:

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

Uma vez que a interface contém apenas um único método, você pode facilmente criar uma ILoggerimplementação que faz proxy para log4net , Serilog , Microsoft.Extensions.Logging , NLog ou qualquer outra biblioteca de registro e configurar seu contêiner de DI para injetá-lo em classes que têm um ILoggerem seus construtor.

Observe que ter métodos de extensão estáticos no topo de uma interface com um único método é bastante diferente de ter uma interface com muitos membros. Os métodos de extensão são apenas métodos auxiliares que criam uma LogEntrymensagem e a passam pelo único método na ILoggerinterface. Os métodos de extensão tornam-se parte do código do consumidor; não faz parte da abstração. Isso não só permite que os métodos de extensão evoluam sem a necessidade de alterar a abstração, os métodos de extensão e oLogEntryconstrutores são sempre executados quando a abstração do registrador é usada, mesmo quando esse registrador é fragmentado / simulado. Isso dá mais certeza sobre a exatidão das chamadas para o logger durante a execução em um conjunto de testes. A interface de um membro torna o teste muito mais fácil também; Ter uma abstração com muitos membros torna difícil criar implementações (como mocks, adaptadores e decoradores).

Quando você faz isso, dificilmente há necessidade de alguma abstração estática que as fachadas de registro (ou qualquer outra biblioteca) podem oferecer.

Steven
fonte
4
@GabrielEspinoza: Isso depende totalmente do namespace em que você coloca os métodos de extensão. Se você colocá-lo no mesmo namespace da interface ou em um namespace raiz do seu projeto, o problema não existirá.
Steven
2
@ user1829319 É apenas um exemplo. Tenho certeza de que você pode propor uma implementação com base nesta resposta que atenda às suas necessidades específicas.
Steven
2
Ainda não entendi ... onde está a vantagem de ter os 5 métodos Logger como extensões do ILogger e não ser membros do ILogger?
Elisabeth
3
@Elisabeth O benefício é que você pode adaptar a interface de fachada a QUALQUER estrutura de registro, simplesmente implementando uma única função: "ILogger :: Log." Os métodos de extensão garantem que tenhamos acesso a APIs de 'conveniência' (como "LogError", "LogWarning" etc), independentemente da estrutura que você decidir usar. É uma forma indireta de adicionar funcionalidade comum de 'classe base', apesar de trabalhar com uma interface C #.
BTownTKD
2
Preciso enfatizar novamente uma razão pela qual isso é ótimo. Convertendo seu código de DotNetFramework para DotNetCore. Nos projetos em que fiz isso, só tive que escrever um único concreto novo. Aquelas onde eu não ... gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Estou feliz por ter encontrado esse "caminho de volta".
granadaCoder
8

A partir de agora, a melhor aposta é usar o pacote Microsoft.Extensions.Logging ( como apontado por Julian ). A maior parte da estrutura de registro pode ser usada com isso.

Definir sua própria interface, conforme explicado na resposta de Steven, é adequado para casos simples, mas deixa escapar algumas coisas que considero importantes:

  • Registro estruturado e objetos de desestruturação (a notação @ em Serilog e NLog)
  • Atraso na construção / formatação da string: como leva uma string, tem que avaliar / formatar tudo quando chamado, mesmo que no final o evento não seja registrado por estar abaixo do limite (custo de desempenho, ver ponto anterior)
  • Verificações condicionais como IsEnabled(LogLevel)você pode desejar, por motivos de desempenho, mais uma vez

Você provavelmente pode implementar tudo isso em sua própria abstração, mas nesse ponto você estará reinventando a roda.

Philippe
fonte
4

Geralmente eu prefiro criar uma interface como

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

e no tempo de execução injeto uma classe concreta que é implementada a partir dessa interface.

criptografado
fonte
11
E não se esqueça os LogWarninge LogCriticalmétodos e todos os seus sobrecargas. Ao fazer isso, você violará o Princípio de Segregação de Interface . Prefira definir a ILoggerinterface com um único Logmétodo.
Steven
2
Sinto muito, não era minha intenção. Não precisa ter vergonha. Vejo esse design com frequência porque muitos desenvolvedores usam o popular log4net (que usa esse design exato) como exemplo. Infelizmente, esse design não é muito bom.
Steven
2
Prefiro esta resposta do @Steven. Ele introduz uma dependência para LogEntrye, portanto, uma dependência de LoggingEventType. A ILoggerimplementação deve lidar com isso LoggingEventTypes, provavelmente case/switch, que é um cheiro de código . Por que esconder a LoggingEventTypesdependência? A implementação deve lidar com os níveis de registro de qualquer maneira , portanto, seria melhor explicitar sobre o que uma implementação deve fazer, em vez de ocultá-la atrás de um único método com um argumento geral.
DharmaTurtle
1
Como um exemplo extremo, imagine um ICommandque tem um Handleque leva um object. As implementações devem ser case/switchsobre os tipos possíveis para cumprir o contrato da interface. Isso não é o ideal. Não tem uma abstração que esconde uma dependência que deve ser tratada de qualquer maneira. Em vez disso, tenha uma interface que declara claramente o que é esperado: "Espero que todos os loggers lidem com avisos, erros, fatais, etc". Isso é preferível a "Espero que todos os registradores lidem com mensagens que incluem avisos, erros, fatais, etc."
DharmaTurtle
Eu meio que concordo com @Steven e @DharmaTurtle. Além disso, LoggingEventTypedevem ser chamados LoggingEventLevelporque os tipos são classes e devem ser codificados como tal em OOP. Para mim, não há diferença entre não usar um método de interface e não usar o enumvalor correspondente . Em vez disso ErrorLoggger : ILogger, use , InformationLogger : ILoggeronde cada logger define seu próprio nível. Em seguida, o DI precisa injetar os loggers necessários, provavelmente por meio de uma chave (enum), mas essa chave não faz mais parte da interface. (Você agora está SÓLIDO).
Wouter de
4

Uma ótima solução para este problema surgiu na forma do projeto LibLog .

LibLog é uma abstração de registro com suporte embutido para os principais registradores, incluindo Serilog, NLog, Log4net e Enterprise logger. Ele é instalado por meio do gerenciador de pacotes NuGet em uma biblioteca de destino como um arquivo de origem (.cs) em vez de uma referência .dll. Essa abordagem permite que a abstração de registro seja incluída sem forçar a biblioteca a assumir uma dependência externa. Ele também permite que um autor de biblioteca inclua o log sem forçar o aplicativo de consumo a fornecer explicitamente um logger para a biblioteca. O LibLog usa reflexão para descobrir qual registrador de concreto está em uso e conectar-se a ele sem qualquer código de fiação explícito no (s) projeto (s) da biblioteca.

Portanto, o LibLog é uma ótima solução para registro em projetos de biblioteca. Basta fazer referência e configurar um logger concreto (Serilog for the win) em seu aplicativo ou serviço principal e adicionar LibLog às suas bibliotecas!

Rob Davis
fonte
Eu usei isso para superar o problema de alteração de interrupção do log4net (eca) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Se você receber isso do nuget, ele criará um arquivo .cs em seu código, em vez de adicionar referências a dlls pré-compiladas. O arquivo .cs tem um namespace para o seu projeto. Portanto, se você tiver camadas diferentes (csprojs), terá várias versões ou precisará consolidar em um csproj compartilhado. Você descobrirá isso quando tentar usá-lo. Mas, como eu disse, esse foi um salva-vidas com o problema de alteração de interrupção do log4net.
granadaCoder