Cómo crear un wrapper de NLog para proyectos DotNet

por May 31, 2020Desarrollo de Software0 Comentarios

Cuando desarrollamos aplicaciones una de las funciones que nunca debe faltar es el log. Es la mejor manera de saber qué ocurre con nuestra aplicación y detectar problemas. En esta ocasión voy a mostrar cómo crear un wrapper de NLog para nuestros proyectos DotNet.

Introducción

Existen múltiples maneras de hacer log, podemos hasta implementar nuestros propios métodos de log para escribir en ficheros o en una base de datos. Pero, ¿para que vamos a dedicar tiempo a algo ya implementado en las librerías de log?

Hay librerías de terceros que incluyen métodos para hacer log en múltiples destinos, funcionan con ficheros de configuración para aislar totalmente el destino de log del código de la aplicación. En .Net hay varias, quizás la más conocida es Log4Net, aunque en la actualidad ya está discontinuada y es antigua.

Una de las más usadas en la actualidad, moderna, con buena documentación y soporte es NLog.

Yo suelo trabajar con soluciones de múltiples proyectos, en estos casos cambiar de librería de log es bastante costoso porque hay que cambiar todas las líneas de código de log. De ahí la utilidad de no usar directamente una librería de log de terceros y crear una clase envolvente.

Esto tiene múltiples ventajas.

  • Nuestro código es independiente de una librería específica de log.
  • Si la librería de log se actualiza no es necesario cambiar todas las referencias en todos los proyectos puesto que el único proyecto que usar la referencia es la envolvente o wrapper de log.
  • Se puede cambiar de librería de log simplemente creando una clase que implemente la interfaz.

A continuación muesto cómo crear un wrapper o envolvente de NLog para hacer independientes nuestros proyectos DotNet de la librería de terceros que se use. Crearemos una interfaz e implementaremos la interfaz con NLog.

Interfaz de Log en DotNet

Para disponer de métodos comunes de log en todos nuestros proyectos DotNet lo primero que haremos será crear una interfaz con los métodos que queremos tener disponibles.

public interface ILogging
{
    /// <summary>
    /// Set properties to log with this logger
    /// </summary>
    /// <param name="props"></param>
    /// <param name="oneTime">Use properties one time and delte after first log. Default: true</param>
    /// <returns></returns>
    ILogging WithProperties(Dictionary<object, object> props, bool oneTime = true);

    void Debug(Exception exception);
    void Debug(string format, params object[] args);
    void Debug(Exception exception, string format, params object[] args);

    void Error(Exception exception);
    void Error(string format, params object[] args);
    void Error(Exception exception, string format, params object[] args);

    void Fatal(Exception exception);
    void Fatal(string format, params object[] args);
    void Fatal(Exception exception, string format, params object[] args);

    //...
    // Lo mismo para todos los niveles de log
}

Crear un wrapper de NLog para DotNet

Normalmente uso NLog en mis proyectos, por ello la implementación de la interfaz será con NLog. No obstante, lo interesante de todo esto es que una vez que hemos hecho la interfaz, el código de nuestra aplicación es independiente de la librería de log que usemos.

Implementar la interfaz de log con NLog

La idea es crear una implementación de la interfaz que hemos creado. Para ello he creado una clase que implemente los métodos de la interfaz creada.

Según las recomendaciones de NLog se han de crear instancias estáticas del logger en cada clase que se quiera usar. Esto es debido al tiempo que lleva la creación de instancias del Logger. Si no se usan instancias estáticas cada vez que se cree nuestra clase se tendrá que crear también el logger.

Siguiendo estas recomendaciones nuestra clase tendrá como atributo el logger de NLog, y cuando vayamos a usar el log en nuestras clases crearemos instancias estáticas de nuestra clase de Log.

Por otra parte, el constructor de nuestra clase es privado, se ha creado un método estático público para la creación de instancias de la clase desde una factoría. El método recibe como parámetro el tipo de la clase que quiere hacer log para que cuando se hagan los registros quede constancia de la clase origen.

Además, la clase que he creado dispone de un método para añadir información adicional al log, incluyendo propiedades en forma de pares clave valor.

class NLogLogging : ILogging
{
    private readonly Type type;
    private readonly NLog.Logger logger;

    private Dictionary<object, object> _props;
    private bool _propsReset;

    public static ILogging GetLogService(Type type)
    {
        NLogLogging logger = new NLogLogging(type);
        return logger;
    }

    private NLogLogging(Type type)
    {
        this.logger = NLog.LogManager.GetLogger(type.FullName);
        this.type = type;
    }
}

Cómo indicar a NLog la clase origen del log

Una de las partes que hemos de tener en cuenta, es que al crear una clase wrapper de la librería externa, el origen de todos los mensajes de log será nuestra clase y no las clases que realmente llaman al log. Eso obviamente no es lo que queremos, lo deseable es que el log siempre muestre la clase donde se ha originado el log.

Buscando en la documentación de NLog encontré un método que ya tienen preparado para eliminar la clase wrapper de la pila de llamadas a la hora de hacer el log. Si se pasa como parámetro al método de log el tipo de nuestra clase, se consigue eliminar esta clase de la pila de llamadas.

Lo podemos ver en una de las implementaciones de los métodos de la interfaz.

public void Debug(Exception exception, string format, params object[] args)
{
    LogEventInfo _lovEvt = this.GetLogEvent(type.FullName, LogLevel.Debug, exception, format, args);
    logger.Log(typeof(NLogLogging), _lovEvt);
}

Crear el objeto LogEventInfo

En el método anterior he llamado a un método que no había mostrado, es el método para crear un objeto de la clase LogEventInfo de NLog. Es un objeto que incluye toda la información del evento incluyendo el nivel de log, de esta manera se puede llamar siempre al método Log() de NLog sin necesidad de llamar a los métodos de log de los diferentes niveles.

Para la creación de este objeto he creado el siguiente método. Hago llamadas al mismo en todos los métodos de log de los diferentes niveles que implementan los métodos de la interfaz.

public LogEventInfo GetLogEvent(string loggerName, LogLevel level, Exception exception, string format, object[] args)
{
    LogEventInfo _logEvent = new LogEventInfo(level, loggerName, format);
    _logEvent.Exception = exception;
    _logEvent.Parameters = args;

    if (_props != null && _props.Count > 0)
    {
        foreach (KeyValuePair<object, object> _prop in _props)
        {
            _logEvent.Properties[_prop.Key] = _prop.Value;
        }
        if (_propsReset) _props = null;
    }

    return _logEvent;
}

Clase factoría para instanciar el Logger

Hemos visto la implementación de la interfaz usando NLog. Lo único que falta es crear una clase factoría que nos permita la creación de instancias de la interfaz de log.

public class LogFactory
{
    public static ILogging GetLogger(Type type)
    {
        return NLogLogging.GetLogService(type);
    }
}

Uso del Wrapper de NLog en un proyecto DotNet

Requisitos del proyecto

Para poder usar nuestro wrapper en el proyecto hemos de tener en cuenta varias cosas

  • Añadir como referencia la librería que hemos creado
  • En los binarios del proyecto además tienen que estar las dll de NLog
  • Crear fichero de configuración de NLog en nuestro proyecto

Como aclaración al segundo punto, indicar que la ventaja de crear el wrapper está en hacer independiente nuestro proyecto de la librería de log específica. Dado que no usamos las clases de NLog en nuestro proyecto directamente, no es necesario incluir las clases de NLog como referencias al proyecto. Sin embargo, la librería creada que si hemos incluido como referencia, depende de NLog, por lo que en las dll necesarias de nuestro proyecto deberían estar las de NLog.

Hacer Log en una clase usando el Wrapper

Para hacer log en nuestras clases usando el wrapper de NLog que hemos creado, simplemente habrá que crear la instancia estática de nuestra interfaz de Log y usarla.

Como ya vimos, se recomienda crear instancias estáticas para ahorrar recursos en la creación del objeto logger. Recordemos que al crear instancias de nuestro wrapper, éste tiene como atributo un objeto del logger de NLog que tendrá que crear.

public class MiClase
{
    private static readonly ILogging logger = LogFactory.GetLogger(typeof(MiClase));

    public void MiMetodo()
    {
        logger.Info("mensaje");
    }
}

Como se puede ver no es complicado crear un wrapper de NLog para nuestros proyectos DotNet. Sin embargo, si que me ha llevado tiempo y búsqueda para conseguir una solución lo más polivalente y elegante posible. Por eso he publicado cómo hacerlo, dado que me parece muy interesante y útil.

Espero que os resulte de utilidad, si tenéis algún comentario o duda al respecto no dudéis en comentar.