Mejores prácticas de encapsuladores de registradores

91

Quiero usar un nlogger en mi aplicación, tal vez en el futuro necesite cambiar el sistema de registro. Entonces quiero usar una fachada de registro.

¿Conoce alguna recomendación de ejemplos existentes sobre cómo escribir esos? O simplemente dame un enlace a algunas de las mejores prácticas en esta área.

Caminante nocturno
fuente
4
Ya existe: netcommon.sourceforge.net
R. Martinho Fernandes
¿Ha revisado el proyecto The Simple Logging Facade en Codeplex ?
JefClaes

Respuestas:

207

Solía ​​usar fachadas de registro como Common.Logging (incluso para ocultar mi propia biblioteca CuttingEdge.Logging ), pero hoy en día uso el patrón de Inyección de dependencia y esto me permite ocultar los registradores detrás de mi propia (simple) abstracción que se adhiere tanto a Dependency Principio de inversión y el principio de segregación de interfaces(ISP) porque tiene un miembro y porque mi aplicación define la interfaz; no una biblioteca externa. Minimizando el conocimiento que tienen las partes centrales de su aplicación sobre la existencia de bibliotecas externas, mejor; incluso si no tiene la intención de reemplazar su biblioteca de registro. La fuerte dependencia de la biblioteca externa hace que sea más difícil probar su código y complica su aplicación con una API que nunca fue diseñada específicamente para su aplicación.

Así es como suele verse la abstracción en mis aplicaciones:

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, esta abstracción se puede ampliar con algunos métodos de extensión simples (permitiendo que la interfaz se mantenga estrecha y siga adhiriéndose al ISP). Esto hace que el código para los consumidores de esta interfaz sea mucho más simple:

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

Dado que la interfaz contiene solo un método, puede crear fácilmente una ILoggerimplementación que sustituye al log4net , a Serilog , Microsoft.Extensions.Logging , Nlog o cualquier otra biblioteca de registro y configurar el contenedor de DI para inyectarlo en las clases que tienen una ILoggeren su constructor.

Tenga en cuenta que tener métodos de extensión estáticos en la parte superior de una interfaz con un solo método es bastante diferente de tener una interfaz con muchos miembros. Los métodos de extensión son solo métodos auxiliares que crean un LogEntrymensaje y lo pasan a través del único método en la ILoggerinterfaz. Los métodos de extensión pasan a formar parte del código del consumidor; no forma parte de la abstracción. Esto no solo permite que los métodos de extensión evolucionen sin la necesidad de cambiar la abstracción, los métodos de extensión y laLogEntryconstructor siempre se ejecutan cuando se usa la abstracción del registrador, incluso cuando ese registrador es anulado / simulado. Esto brinda más certeza sobre la exactitud de las llamadas al registrador cuando se ejecuta en un conjunto de pruebas. La interfaz de un miembro también facilita las pruebas; Tener una abstracción con muchos miembros dificulta la creación de implementaciones (como simulacros, adaptadores y decoradores).

Cuando hace esto, casi nunca hay necesidad de alguna abstracción estática que las fachadas de registro (o cualquier otra biblioteca) puedan ofrecer.

Steven
fuente
4
@GabrielEspinoza: Eso depende totalmente del espacio de nombres en el que coloques los métodos de extensión. Si lo colocas en el mismo espacio de nombres que la interfaz o en un espacio de nombres raíz de tu proyecto, el problema no existirá.
Steven
2
@ user1829319 Es solo un ejemplo. Estoy seguro de que puede encontrar una implementación basada en esta respuesta que se adapte a sus necesidades particulares.
Steven
2
Todavía no lo entiendo ... ¿dónde está la ventaja de tener los 5 métodos de Logger como extensiones de ILogger y no ser miembros de ILogger?
Elisabeth
3
@Elisabeth El beneficio es que puede adaptar la interfaz de fachada a CUALQUIER marco de registro, simplemente implementando una sola función: "ILogger :: Log". Los métodos de extensión aseguran que tengamos acceso a API de "conveniencia" (como "LogError", "LogWarning", etc.), independientemente del marco que decida utilizar. Es una forma indirecta de agregar una funcionalidad común de 'clase base', a pesar de trabajar con una interfaz C #.
BTownTKD
2
Necesito volver a enfatizar una razón por la que esto es genial. Conversión ascendente de su código de DotNetFramework a DotNetCore. Los proyectos en los que hice esto, solo tuve que escribir un solo concreto nuevo. Los que no lo hice .... gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Me alegro de haber encontrado este "camino de regreso".
granadaCoder
8

A partir de ahora, la mejor opción es utilizar el paquete Microsoft.Extensions.Logging ( como señaló Julian ). La mayoría de los marcos de registro se pueden usar con esto.

Definir su propia interfaz, como se explica en la respuesta de Steven, está bien para casos simples, pero pasa por alto algunas cosas que considero importantes:

  • Objetos de registro y desestructuración estructurados (la notación @ en Serilog y NLog)
  • Construcción / formateo de cadenas retrasado: dado que toma una cadena, tiene que evaluar / formatear todo cuando se llama, incluso si al final el evento no se registrará porque está por debajo del umbral (costo de rendimiento, ver punto anterior)
  • Comprobaciones condicionales como las IsEnabled(LogLevel)que desee, una vez más por motivos de rendimiento

Probablemente puedas implementar todo esto en tu propia abstracción, pero en ese momento estarás reinventando la rueda.

Philippe
fuente
4

Generalmente prefiero crear una interfaz como

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

y en el tiempo de ejecución inyecto una clase concreta que se implementa desde esta interfaz.

encriptado
fuente
11
Y no se olvide de las LogWarningy los LogCriticalmétodos y todos sus sobrecargas. Al hacer esto, violará el principio de segregación de interfaces . Prefiere definir la ILoggerinterfaz con un solo Logmétodo.
Steven
2
Lo siento mucho, esa no era mi intención. No hay por qué avergonzarse. Veo este diseño muy a menudo porque muchos desarrolladores usan el popular log4net (que usa este diseño exacto) como ejemplo. Desafortunadamente, ese diseño no es realmente bueno.
Steven
2
Prefiero esto a la respuesta de @ Steven. Introduce una dependencia y LogEntry, por tanto, una dependencia de LoggingEventType. La ILoggerimplementación debe lidiar con estos LoggingEventTypes, probablemente case/switch, lo cual es un olor a código . ¿Por qué esconder la LoggingEventTypesdependencia? La implementación debe manejar los niveles de registro de todos modos , por lo que sería mejor explicar lo que debería hacer una implementación, en lugar de ocultarlo detrás de un método único con un argumento general.
DharmaTurtle
1
Como ejemplo extremo, imagine un ICommandque tiene un Handleque toma un object. Las implementaciones deben case/switchsuperar los tipos posibles para cumplir con el contrato de la interfaz. Esto no es ideal. No tenga una abstracción que oculte una dependencia que deba manejarse de todos modos. En su lugar, tenga una interfaz que indique claramente lo que se espera: "Espero que todos los registradores manejen Advertencias, Errores, Fatales, etc.". Esto es preferible a "Espero que todos los registradores manejen mensajes que incluyen Advertencias, Errores, Fatales, etc."
DharmaTurtle
Estoy de acuerdo con @Steven y @DharmaTurtle. Además, LoggingEventTypedebe llamarse LoggingEventLevelcomo tipos son clases y debe codificarse como tal en OOP. Para mí, no hay diferencia entre no usar un método de interfaz y no usar el enumvalor correspondiente . En su lugar ErrorLoggger : ILogger, use , InformationLogger : ILoggerdonde cada registrador define su propio nivel. Luego, la DI necesita inyectar los registradores necesarios, probablemente a través de una clave (enumeración), pero esta clave ya no es parte de la interfaz. (Ahora eres SÓLIDO).
Wouter
4

Una gran solución a este problema ha surgido en forma del proyecto LibLog .

LibLog es una abstracción de registro con soporte incorporado para los principales registradores, incluidos Serilog, NLog, Log4net y Enterprise logger. Se instala a través del administrador de paquetes NuGet en una biblioteca de destino como un archivo de origen (.cs) en lugar de una referencia .dll. Ese enfoque permite que se incluya la abstracción del registro sin obligar a la biblioteca a asumir una dependencia externa. También permite al autor de una biblioteca incluir el registro sin forzar a la aplicación consumidora a proporcionar explícitamente un registrador a la biblioteca. LibLog usa la reflexión para averiguar qué registrador de concreto está en uso y conectarse a él sin ningún código de cableado explícito en los proyectos de la biblioteca.

Por lo tanto, LibLog es una gran solución para iniciar sesión en proyectos de biblioteca. ¡Simplemente haga referencia y configure un registrador de concreto (Serilog para ganar) en su aplicación o servicio principal y agregue LibLog a sus bibliotecas!

Rob Davis
fuente
He usado esto para superar el problema de cambio de ruptura de log4net (puaj) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Si obtiene esto de nuget, en realidad creará un archivo .cs en su código en lugar de agregar referencias a dlls precompilados. El archivo .cs tiene un espacio de nombres para su proyecto. Entonces, si tiene diferentes capas (csprojs), tendrá varias versiones o debe consolidar en un csproj compartido. Descubrirás esto cuando intentes usarlo. Pero como dije, esto fue un salvavidas con el problema de cambios rotos de log4net.
granadaCoder