lunes, 16 de julio de 2007

¿Cómo crear un TraceListener?

Surgió la necesidad de crear un trace listener especial, con algunas características, además de escribir en una base de datos los datos que se necesitaban eran muy particulares. Vamos a ver cómo lo resolvemos.

Creando un trace listener

Primero, para poder utilizar un trace listener hecho por nosotros es necesario que nuestra clase herede de Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.CustomTraceListener y tenga el atributo [ConfigurationElementType(typeof(CustomTraceListenerData))], de este modo

   1:  [ConfigurationElementType(typeof(CustomTraceListenerData))] 
   2:  public class MyCustomTraceListener: Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.CustomTraceListener 

Bueno, lo primero es obviamente para respetar los métodos a los cuales responde la clase, y para que la EntLib lo pueda recuperar con el Object Builder. Lo segundo es para que se pueda utilizar el configurador de EntLib.
Lo primero que vemos es que Visual Studio nos sugiere implementar la clase abstracta CustomTraceListener, si seguimos su sugerencia nos escribe dos métodos Write y WriteLine, vamos a verlos.

   1:  public override void Write(string message) 
   2:  { 
   3:    throw new Exception("The method or operation is not implemented."); 
   4:  } 
   5:  public override void WriteLine(string message) 
   6:  { 
   7:    throw new Exception("The method or operation is not implemented."); 
   8:  } 

Viéndolos sospechamos que no nos van a ayudar en nuestra tarea, qué es lo que tenemos que hace, veamos la definición de CustomTraceListener.

   1:  public abstract class CustomTraceListener : TraceListener 
   2:  { 
   3:    protected CustomTraceListener(); 
   4:    public ILogFormatter Formatter { get; set; } 
   5:  } 

No vemos nada interesante, excepto que hereda de TraceListener, entonces veamos su definición

   1:  public abstract class TraceListener : MarshalByRefObject, IDisposable 
   2:  { 
   3:  protected TraceListener(); 
   4:  public virtual void Close(); 
   5:  public void Dispose(); 
   6:  protected virtual void Dispose(bool disposing); 
   7:  public virtual void Fail(string message); 
   8:  public virtual void Fail(string message, string detailMessage); 
   9:  protected internal virtual string[] GetSupportedAttributes(); 
  10:  [ComVisible(false)] 
  11:  public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data); [ComVisible(false)] 
  12:  public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data); 
  13:  [ComVisible(false)] 
  14:  public virtual void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id); 

Nota: El código está incompleto.

Aquí vemos el método que nos va a ayudar, se trata de TraceData, veámoslo con más detenimiento.

   1:  [ComVisible(false)] 
   2:  public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data); 
   3:  [ComVisible(false)] 
   4:  public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data); 

Concentrémonos en la primera sobrecarga, vemos que recibe un parámetro object llamado data, este parámetro es el que va a contener el objeto LogEntry con los datos del evento. Hagamos una primera implementación.

   1:  [ConfigurationElementType(typeof(CustomTraceListenerData))] 
   2:  public class MyCustomTraceListener: Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.CustomTraceListener
   3:  { 
   4:   public override void Write(string message)
   5:   { 
   6:     Console.WriteLine(message);
   7:   }
   8:   
   9:   public override void WriteLine(string message)
  10:   { 
  11:     Console.WriteLine(message); 
  12:   }
  13:   
  14:   public override void TraceData(System.Diagnostics.TraceEventCache eventCache,  string source, System.Diagnostics.TraceEventType eventType, int id, object data) 
  15:   { 
  16:     if (data is LogEntry && this.Formatter != null) 
  17:     { 
  18:       this.WriteLine(this.Formatter.Format(data as LogEntry)); 
  19:     } 
  20:     else 
  21:     { 
  22:       this.WriteLine(data.ToString()); 
  23:     }
  24:    }
  25:   }
  26:   

En la línea 16 vemos que se pregunta si data es del tipo LogEntry y el Formatter no es nulo, este es un ejemplo típico que en realidad no nos es de mucha ayuda, primero porque no nos interesa tener un Formatter, segundo porque no queremos escribir los datos del LogEntry sino que nuestro escenario es más complejo, sin embargo si lo pensamos un poco y observamos la definición de LogEntry tal vez podamos hacer uso de este modo de implementación. Bueno, es el momento de pensar cómo lo implementamos, vamos a ver la implementación de LogEntry a ver si con eso y la ayuda del Formatter podemos hacer lo que queremos.

   1:  public LogEntry(); 
   2:  public LogEntry(object message, ICollection categories, int priority, int eventId, TraceEventType severity, string title, IDictionary properties); 
   3:  public LogEntry(object message, string category, int priority, int eventId, TraceEventType severity, string title, IDictionary properties); 
   4:  public Guid ActivityId { get; set; } 
   5:  public string ActivityIdString { get; } 
   6:  public string AppDomainName { get; set; } 
   7:  public ICollection Categories { get; set; } 
   8:  public string[] CategoriesStrings { get; } 
   9:  public string ErrorMessages { get; } 
  10:  public IDictionary ExtendedProperties { get; set; } 
  11:  public string LoggedSeverity { get; } 
  12:  public string MachineName { get; set; } 
  13:  public string ProcessId { get; set; } 

Evidentemente la propiedad ExtendedProperties es una excelente candidata para colocar dentro un objeto con nuestros datos.
Manos a la obra

Comenzado con la implementación

Primero creamos un entidad de datos para contener lo que necesitamos grabar en la base de datos.



Es evidente que vamos a necesitar indicarle un par de cosas a nuestro trace listener, como el connection string de la base de datos y el stored procedure que se encargará de insertar los datos. Vamos a ver cómo se hace esto. Según leí en Codeplex el método GetAttributes le indica a EnLib los atributos que están permitidos para declarar en el archivo de configuración para el trace listener, la firma es esta.

protected internal virtual string[] GetSupportedAttributes();

Entonces si lo sobreescribimos y le decimos que tenemos dos atributos, ConnectionString y StoredProcedureName vamos a poder agregarlos al app.config

   1:  protected override string[] GetSupportedAttributes() 
   2:  { 
   3:    return new string[] {"ConnectionString","StoredProcedureName"}; 
   4:  } 

Ok, ahora en el app.config podemos agregar los atributos, ¿de qué modo?, a manopla o con el editor de EntLib
.


Ahora, para leerlo no hacemos más que

string connectionString = Attributes["ConnectionString"];

Hay que tener cuidado porque si el usuario no definió el atributo (EntLib no hace ningún chequeo de esto) nos va a devolver un string vacío, lo mismo si escribimos mal el nombre.

¿Qué tenemos hasta ahora?

Hasta ahora sabemos como hacer para crear nuestro TraceListener, sabemos qué métodos sobreescribir, sabemos cómo vamos a pasarle nuestro datos (por extended properies) y sabemos cómo pasarle parámetro a través de la configuración. Estamos listo, en el próximo post lo escribimos.