Exception Handling y Logging con Enterprise Library 3.0

lunes, 11 de junio de 2007

En este artículo voy a mostrar como agregar un manejo simple de excepciones, y luego logging de éstas y otros eventos usando Enterprise Library 3.0, sin profundizar en todas las posibilidades que éstas librerías ofrecen.

Usaremos en este ejemplo los siguientes bloques de Enterprise Library 3.0:

  • Exception Handling Application Block
  • Logging Application Block (que registrará Excepciones recibidas por el bloque anterior, y otros mensajes de bitácora)
  • Data Application Block (para permitir al bloque anterior, Logging sobre una base de datos SQL)

(Es condición previa en el ejemplo siguiente, agregar referencias en nuestro proyecto a los assemblies correspondientes)

De la aplicación de ejemplo realizada en C#, nos interesa el siguiente bloque de código:


Vamos a trabajar sobre este método, que resulta crítico para nuestro sistema, por lo cual queremos una bitácora de su ejecución y resultado.



Exception Handling Application Block


Este bloque nos permite delegar el manejo de una excepción a una policy definida en archivos de configuración. En particular nos permitirá encapsular, reemplazar, absorber excepciones, o enviar éstas al Logging Application Block para registrarlas en una bitácora.

Para esto basta con modificar nuestro código de la siguiente manera:



Interceptamos cualquier tipo de Exception, y la manejamos con el método estático:

ExceptionPolicy.HandleException()

Éste recibe la excepción a ser manejada, el nombre de la policy a aplicar (a continuación vamos a definir esta política), y opcionalmente un parámetro out en el que se devuelve la excepción que reemplaza a la original (esto sucede cuando la policy utilizada decide que la excepción debe ser reemplazada o contenida dentro de una nueva), si la excepción no debe ser reemplaza este parámetro toma el valor null.

Este método devuelve true si la excepción (o la excepción que la reemplaza) debe ser relanzada.


Ahora sólo resta definir nuestra "TareasPolicy", para eso alcanza con editar el archivo de configuración (app.config, web.config, *.config) con la herramienta gráfica que incluye Enterprise Library (Enterprise Library Configuration Tool), con la cual resulta trivial lo siguiente:

  1. Agregar un bloque de configuración para Exception Handling, y dentro de éste crear una nueva policy con el nombre "TareasPolicy".
  2. Definir en esta policy para el tipo de excepción Exception (es decir para todas), un Logging Handler, y definimos para este Handler, que las excepciones produzcan entradas de Log de nivel "Error" (lógicamente), y con la categoría "Errores", vamos a ver que implica esto a continuación.

Dentro de una policy podemos definir también los siguientes Exception Handlers, que pueden utilizarse según el tipo de Exception lanzada:

Wrap Handler: Envuelve la excepción lanzada dentro de una nueva.
Replace Handler: Reemplaza la excepción con una nueva.
Logging Handler: Transfiere la excepción al Logging Application Block para poder registrarla en una bitácora.
Custom Handler: Permite manejar una excepción utilizando una clase definida en nuestra aplicación.
Fault Contract Exception Handler: Facilita el manejo de excepciones en aplicaciones SOA.



Logging and Instrumentation Application Block


Necesitamos ahora indicar como serán tratadas en este bloque las excepciones recibidas, pero antes es preciso definir las entidades más importantes en este bloque:

Trace Listener: Manejan los eventos registrándolos o emitiéndolos en distintos medios, son los siguientes:
  • Formatted EventLog: registra los eventos en el EventLog del sistema.
  • Flat File: simplemente almacena los eventos en un archivo de texto plano.
  • Rolling Flat File: utiliza un archivo de texto, que al alcanzar una antigüedad o támaño máximo se cierra y comienza uno nuevo.
  • Xml: en este caso el log generado es un documento xml, asimismo las entradas pueden contener un campo xml.
  • Email: envía notificaciones de eventos a traves de un servidor SMTP.
  • Msmq: utiliza Microsoft Message Query como destino de los eventos.
  • WMI: los eventos se manejan con Windows Management Instrumentation.
  • Database: Cuando recibe un evento, invoca un stored procedure con sus datos como parámetro. (Requiere el Data Access Block, para definir la base de datos a utilizar).
  • Custom: Si los anteriores no son suficientes, puede manejarse los eventos mediente una clase definida nuestra aplicación. Esto nos permitiría por ejemplo, mostrar un MessageBox, enviar el error con un POST HTTP, etc.
Formatter: Se encarga de serializar eventos, para facilitar su almacenamiento, son los siguientes:

  • Text Formatter
  • Binary Formatter
  • Xml Formatter
  • Custom Formatter
Event Sources: Representan fuentes de eventos, se les puede asociar uno o más Trace Listeners.
Pueden definirse segun categoría de los eventos, y están predifinidas adicionalmente las siguientes sources especiales:

  • All Events: Como su nombre lo indica, recibe todos los eventos de la aplicación.
  • Unprocessed category: Eventos de categorías no procesadas.
  • Logging Errors & Warnings: Recibe errores y advertencias producidos al registrar otros eventos (por ejemplo, caída del servidor del SQL, al intentar almacenar un evento en éste), permite una especie de "manotazo de ahogado", lo que hace apropiado asociar a esta fuente el Event Handler más "infalible", por ejemplo, Formatted EventLog.
Filters: Permiten filtrar eventos, según categoría, prioridad, o mediante clases personalizadas.


Ahora utilizamos una vez más la herramienta de edición de archivos de configuración para lo siguiente:

  1. Definir el bloque de configuración para el Logging Application Block.
  2. Definir el Category Source "Errores" como fue definido en el Exception Handling Block, este recibirá las Exceptions interceptadas por éste, y asociaremos a este un Database Handler, para crear nuestra bitácora en una base de datos.
  3. Crearemos (si no existe aún) la configuración para el Data Access Application Block, donde definiremos el connection string.
  4. Referenciar esta base de datos en nuestro Database Trace Listener, e indicar en éste el nombre del stored procedure que debe invocarse para agregar una entrada al log, y el stored procedure que se invoca inmediatemente después para indicar la categoría de ese evento (es por esto que el primer stored procedure, debe devolver el id de la entrada creada).
  5. Dado que nuestra conexión con la base de datos puede fallar (y queremos saber cuando!), agregamos un Formatted Event Log Trace Listener, y asociamos este al "Logging Errors & Warning" Special Source, para registrar en el EventLog los errores que ocurran al intentar usar la base de datos.

Por último aprovecharemos el Logging Application Block, para registrar también información adicional sobre la ejecución de nuestra tarea, para eso asociaremos nuestro Database Trace Listener al Special Source "All Events" (es posible definir un Trace Listener, o una categoría especial de eventos para este caso)

Ahora podemos modificar nuestro código C# de esta manera:


El método estático Logger.Write(), puede recibir también varios parametros adicionales como categoría, prioridad, severidad, etc.



Conclusiones

  • Las políticas de manejo de errores y logging, son totalmente independientes del código fuente: pueden ser establecidas de forma gráfica, en archivos de configuración, sin necesidad de recompilar la aplicación. Es decir, la modificación de estas políticas pueden ser delegada a un usuario administrador.
  • Por la misma razón que el punto anterior, al agregar estos bloques a una aplicación existente, el impacto sobre el código fuente es mínimo.
  • El manejo de Excepciones y Eventos de Log, es extensible definiendo "custom handlers", clases personalizadas que son invocadas para estas tareas.



Links


UPDATE 2009-06-12

Fe de erratas!, Como comenta Ariel (ver comentario en esta entrada), la forma correcta de relanzar una excepción es mediante throw; (de forma de mantener el stack trace), asi que en todos los bloques de codigo donde dice:

throw outEx ?? ex;

debería leerse:

if(outEx!=null)
throw outEx;
else
throw;