jueves, 22 de diciembre de 2011

Log con Trace y Debug

Aunque Microsoft recomienda usar TraceSource en detrimento de las clases Debug y Trace, en este post veremos estas clases para en un siguiente post poder abordar TraceSource.

¿Qué es una traza?

La traza de un programa indica la secuencia de acciones o instrucciones de su ejecución, así como el valor de las variables utilizadas después de cada acción.

Lo usual es que un conjunto de trazas se guarde en un fichero de log para poder después consultar esta información.

¿Quién genera información de traza?

La información de traza es generada por las clases Debug y Trace del espacio de nombres System.Diagnostics.

¿Cómo se genera la información de traza?

Llamando a los métodos adecuados de la clase Debug y Trace.

Algunos de los métodos más habituales son:

  • Write
  • WriteLine
  • WriteIf
  • WriteLineIf

¿Qué diferencias hay entre Debug y Trace?

Debug sólo genera información de traza cuando estamos en modo de depuración, mientras que Trace la genera tanto en modo de depuración como en modo de despliegue (Release).

Además, ambas clases son iguales y comparten los mismos datos. Esto significa que un cambio en la clase Debug estará automáticamente sincronizado con su propiedad equivalente en la clase Trace e igualmente al revés.

¿Quién consume la información de traza?

La información que generan las clases Debug y Trace es consumida por los TraceListeners, que son clases concretas que heredan de la clase abstracta TraceListener.

Aunque a mi no me gusta, la traducción al español que hace Microsoft de los TraceListeners es “agentes de escucha” y la información de traza la llama “mensajes de seguimiento”.

Tanto la clase Debug como la clase Trace disponen de una referencia a los TraceListeners que consumirán sus datos a través de la propiedad Listeners.

Un TraceListener es un objeto que recibe la información de traza y la vuelca en algún medio. Este medio podría ser una ventana del IDE, un fichero en disco, una base de datos o cualquier otro destino válido.

¿Qué TraceListeners hay disponibles por defecto?

Por defecto, tenemos los siguientes TraceListeners que heredan directamente de la clase TraceListener.

  • DefaultTraceListener
  • EventLogTraceListener
  • EventProviderTraceListener
  • TextWriterTraceListener

Además, hay otra serie de TraceListeners que heredan de la clase TextWriterTraceListener.

  • XmlWriterTraceListener
  • DelimitedListTraceListener
  • ConsoleTraceListener
  • EventSchemaTraceListener

El único TraceListener que se agrega por defecto a cualquier proyecto es DefaultTraceListener.

¿Cómo agregamos TraceListeners a nuestra aplicación?

Hay dos vías posibles: por código o a través de los ficheros de configuración (web.config, app.config, etc.)

La forma preferida es a través de los ficheros de configuración porque esto supone que seremos capaces de agregar o eliminar TraceListeners a nuestra aplicación, una vez ha sido desplegada y sin tener que modificar nuestro código.

¿Puede crear mis propios TraceListeners?

Sí, es tan sencillo como crear una clase que herede de TraceListener e implementar los métodos necesarios para llevar a cabo la tarea de consumir la información de traza y guardarla en algún medio.

El ejemplo más definitivo es crear un TraceListener que guarda la información en una base de datos.

¿Un ejemplo, por favor?

Para nuestro ejemplo partiremos de una aplicación de consola en C# con el siguiente código:

using System;

using System.Collections.Generic;

using System.Text;

using System.Diagnostics;

 

namespace Logging

{

    class Program

    {

        static void Main(string[] args)

        {

            Debug.WriteLine("Primero");

            Trace.WriteLine("Segundo");

            Console.ReadLine();

        }

    }

}


Si ejecutamos el programa, veremos como por defecto nuestros mensajes (tanto Debug como Trace) son volcados a la ventana de resultados del IDE.

clip_image001[4]

Este comportamiento es debido a que por defecto, el programa viene con un TraceListener configurado de la clase DefaultTraceListener, que vuelva la información de traza en la ventana de resultados del IDE.

Podemos ver la lista de TraceListeners ejecutando el siguiente código:

static void PrintListeners()

{

    IEnumerator enumerador = Debug.Listeners.GetEnumerator();

    TraceListener tl;

    while (enumerador.MoveNext())

    {

        tl = (TraceListener)enumerador.Current;

        Console.WriteLine(String.Format("{0}, {1}", tl.Name, tl.GetType().Name));

    }

}

 

clip_image002[4]

Esto lo podemos ver claramente si eliminamos el contenido de la colección Listeners y observamos que ahora nuestra información de traza no es consumida por ningún TraceListener.

Debug.Listeners.Clear();


Una vez hemos visto que tenemos disponible por defecto DefaultTraceListener, es el momento de agregar más TraceListeners a nuestra aplicación, ya que DefaultTraceListener podría resultar útil durante el desarrollo y desde el IDE, pero no parece de mucha utilidad para una aplicación en producción.

Si optamos por la vía de agregar TraceListeners desde código (insisto en que esta no es la opción preferida) tendremos que hacer lo siguiente:

TextWriterTraceListener twl = new TextWriterTraceListener("C:\\log.txt");

Debug.Listeners.Add(twl);

 

Debug.WriteLine("Primero");

Trace.WriteLine("Segundo");

 

twl.Flush();


Con este código ya tendremos un fichero log.txt en nuestra raíz de C:\ que tendrá el texto “Primero Segundo” si estamos en Debug o “Segundo” si estamos en Release.

También es importante ver cómo es necesario llamar al método Flush() para que los cambios se guarden en disco.

Cómo hemos dicho antes, la opción preferida es agregar o eliminar los TraceListeners desde los ficheros de configuración.

Un fichero de configuración para agregar TextWriterTraceListener a nuestra aplicación, sería el siguiente:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <system.diagnostics>

    <trace autoflush="true">

      <listeners>

        <add name="TextWriterTraceListener"

         type="System.Diagnostics.TextWriterTraceListener" initializeData="C:\log.txt"/>

      </listeners>

    </trace>

  </system.diagnostics>

</configuration>


¿Qué es un Switch?

La clase Switch es la clase abstracta base para las clases concretas BooleanSwitch y TraceSwitch.

Un Switch representa una clase con la podemos modificar el comportamiento de nuestro registro de traza en una aplicación en producción.

De nuevo, la traducción al español que hace Microsoft de Swtich es “modificadores”.

Hasta ahora hemos volcado a disco de forma incondicional toda la información de traza que hemos recibido, pero:

  • ¿Cómo podría desactivar el registro de la traza?
  • ¿Cómo podría sólo registrar cierta información?

Estas preguntas obtienen respuesta utilizando Switches.

Primero abordaremos la clase BooleanSwitch que representa un valor booleano para poder determinar desde nuestro código si está o no activo el registro de la traza.

Para utilizar BooleanSwitch tenemos que:

  • Agregar BooleanSwitch a nuestro fichero de configuración.
  • Leer la configuración de BooleanSwitch desde nuestro código.
  • Registrar la información de traza en función de la propiedad Enabled de BooleanSwitch.

Veamos un ejemplo.

Primero mostraremos el fichero de configuración:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <system.diagnostics>

    <switches>

      <add name="Activated" value="1" />

    </switches>

    <trace autoflush="true">

      <listeners>

        <add name="TextWriterTraceListener"

         type="System.Diagnostics.TextWriterTraceListener" initializeData="C:\log.txt"/>

      </listeners>

    </trace>

  </system.diagnostics>

</configuration>


Fíjate que no hemos especificado el tipo de Switch porque será en nuestro código donde después haremos el casting al tipo adecuado. Además, el nombre del Switch puede ser cualquiera y la única imposición es que value debe ser 1 o 0.

Ahora nuestro código del programa:

BooleanSwitch bs = new BooleanSwitch("Activated", "Registro activo");

 

Debug.WriteLineIf(bs.Enabled, "Primero");

Trace.WriteLineIf(bs.Enabled, "Segundo");


Como podemos ver, casteamos el Switch con nombre “Activated” a un BooleanSwitch y después utilizamos el método WriteLineIf que espera una condición como primer parámetro.

Después de ver BooleanSwitch (que lo cierto es que es muy sencilla) veremos ahora TraceSwitch que nos permite afinar no sólo si está o no activo el registro, sino también que información queremos registrar.

El único cambio que haremos en nuestro fichero app.config será en la sección de switches. Ahora pasaremos a tener lo siguiente:

<add name="WhatInfo" value="2" />


En realidad, agregar un TraceSwitch no difiere en exceso de agregar un BooleanSwitch. La única diferencia es que la propiedad value ahora es un enumerado con los siguientes posibles valores:

  • Off. 0
  • Error. 1
  • Warning. 2
  • Info. 3
  • Verbose. 4

Con TraceSwitch lo que conseguimos es determinar qué nivel de información queremos guardar.

Podemos desactivar directamente el registro y conseguir el mismo efecto que con BooleanSwitch si utilizamos el valor 0 (Off), pero también podemos determinar que nivel de información queremos registrar. Cabe mencionar al respecto que cualquier nivel de información incluye automáticamente a niveles inferiores. Es decir, si elegimos Warning (2), también registraremos Error (1).

Los distintos niveles de traza son expuestos con propiedades booleanas en TraceSwitch (por ejemplo, TraceError, TraceWarning, etc.)

Ahora bien, que hayamos agregado un TraceSwitch a nuestro fichero app.config no significa que ocurra magia y, es por ello, que somos nosotros mismos quienes desde código tenemos que utilizar TraceSwitch para registrar el nivel de información especificado.

Veamos un ejemplo:

TraceSwitch ts = new TraceSwitch("WhatError", "¿Qué registrar?");

 

Debug.WriteLineIf(ts.TraceError, "Error");           

Debug.WriteLineIf(ts.TraceWarning, "Advertencia");

Debug.WriteLineIf(ts.TraceInfo, "Información");

Debug.WriteLineIf(ts.TraceVerbose, "Ampliado");


Podemos ver que si no acompañamos nuestro TraceSwitch con métodos que esperan una condición como WriteLineIf, el efecto no será ninguno. Siendo así, la conclusión es que debemos preparar nuestro código para poder trabajar con los distintos Switches.

Un saludo!

No hay comentarios:

Publicar un comentario