Todos en mayor o menor medida nos vemos en la necesidad de poder, en un momento dado, conocer como se está comportando nuestra aplicación mediante el uso de trazas. Como me a tocado trabajar en la arquitectura de un buen puñado de aplicaciones, me propongo recopilar aquí unas cuantas reflexiones y buenas prácticas sobre este tema. No se trata de sentar cátedra, en informática rara vez hay una única aproximación buena, sino comentar las tácticas que suelo emplear a la hora de diseñar el traceo y aquellos aspectos clave a los que se debe mirar. Cada aplicación tiene diferentes necesidades en lo que a traceo se refiere. Además me encantaría que se generase cierto debate alrededor de este tema.
Disponemos de un motón de soluciones para llevar a cabo la implementación de esta característica en nuestras aplicaciones: System.Diagnostics, Log4net, el Application Block de Microsoft para este cometido…
El principal objetivo de las trazas es poder conocer a posteriori el comportamiento de nuestra aplicación y los diferentes estados por los que ha pasado. Las trazas son las únicas huellas que la ejecución de nuestra aplicación deja en ocasiones. Generalmente necesitaremos estudiar esta información cuando surjan problemas. No considero aquí las trazas como auditoria de permisos, de uso de la aplicación, de visitas, de uso de características, solo hablo de trazas de diagnostico, que son las que aparecen en una mayor número de aplicaciones.
El traceo debe ser suficientemente eficiente. Es importante que las trazas no afecten demasiado al comportamiento de nuestro aplicación en lo que al rendimiento se refiere. Indudablemente siempre se activemos las trazas de nuestra aplicación tendremos una penalización en el rendimiento de la aplicación, aunque esta penalización es inevitable, debe ser la menor posible. Otro aspecto vital es que cuando las trazas se encuentren desactivadas tenga un impacto cercano a cero sobre el rendimiento de la aplicación. Apoyar nuestro sistema de trazas en una librería ampliamente utilizada o en las posibilidades de traceo que casi todos los frameworks de desarrollo moderno proporcionan suele ser una garantía para lograr un rendimiento aceptable.
El traceo debe ser thread safe. Una tentación habitual es utilizar un hilo adicional para realizar las actividades de traceo de nuestra aplicación. En teoria esto permite que nuestra aplicación sufra un menor impacto en su rendimiento por las trazas. Esta es una aproximación errónea por varios motivos: Primero, porque los cambios de contexto que se producirán al utilizar múltiples hilos con numerosas trazas invalidarán cualquier ventaja de rendimiento y segundo, porque una situación en la que nos resultan vitales las trazas es cuando no encontramos con problemas relacionados con el comportamiento con múltiples hilos de la aplicación (interbloqueos, condiciones de carrera, etc…) y lógicamente en esta situación lo último que necesitamos es que las trazas introduzcan más ruido en el problema. Debemos poder confiar en que las trazas se escriben de manera síncrona a la ejecución del código que las rodea y con el que están relacionadas.
El traceo debe ser simple. Las librerías y clases de traceo nos permiten utilizar una gran cantidad de parámetros de configuración que permiten controlar en gran detalle en nivel de locuacidad de las trazas, su destino, para que módulos están o no activadas y un largo etc… Pero a menudo en nuestras aplicaciones debemos enmascarar y simplificar estos aspectos para hacer utilizable y sencillo nuestro sistema de trazas, tanto para los desarrolladores que lo utilizan como para los administradores de la aplicación. Aunque poder controlar el nivel de locuacidad de las trazas es una gran idea, en la gran mayoría de las ocasiones es suficiente utilizar solo tres niveles: error, advertencia e información. Una idea que suele parecer atractiva es dejar en manos de los operadores de la aplicación la configuración del tipo de salida y de que módulos deben proporcionar traceo, pero en la práctica suele ser mucho más útil, en mi experiencia, que simplemente sea necesario seleccionar una salida y activar el nivel de trazas adecuado. La idea es mantener simple el sistema de trazas pues cuando nuestras aplicaciones tienen problemas, al otro lado de la línea de soporte, no siempre hay un avezado administrador de sistemas que conoce a fondo nuestra aplicación. Es más simple simplemente pedir que se activen todas las trazas, se nos envié el resultado y realizar una análisis detallado y tranquilo de las mismas a base de filtrar lo que no es necesario. Mejor tener información suficiente y filtrarla a posteriori que no contar con la información suficiente para hacer una análisis detallado y tener que pedir de nuevo al administrador de la aplicación que cambie tal o cual parámetro.
Las trazas deben ser facilmente filtrables. Por lo anteriormente expuesto se hace evidente que las trazas deben ser fácilmente filtrables. Para ello todas las trazas que nuestra aplicación emita deben tener la misma estructura, presentar los mismos campos y en esencia ser homogéneas entre sí. Es muy complejo filtrar trazas que proporcionan información heterogénea.
El traceo debe tener como salida las fuentes habituales. A menudo cuando se implementa un sistema de traceo se olvidan ciertas salida que los usuarios de ciertas tecnologías esperan que estén presentes. Por ejemplo, todo administrador de sistemas espera que toda traza de nivel crítico se vea reflejada en el visor de eventos, o los programadores y administradores de aplicaciones Asp.Net esperan poder ver las trazas usando trace.axd, y todos los desarrolladores del mundo esperan poder ver las trazas usando DebugView o la ventana de salida de Visual Studio. Por lo tanto, cuando diseñamos el sistema de traceo de nuestra aplicación debemos asegurarnos de que concemos y utlizamos las salidas esperadas por los desarrolladores y administradores de nuestra aplicación.
Las trazas deben mostrar información suficiente y no excesiva. Tan importante es que la información volcada por las trazas sean suficiente como que no sea excesiva. De todos modos ante la duda, se debe tracear toda la información que pueda resultar relevante, pues siempre podremos filtrar a posteriori: Identificador del hilo que crea la traza, hora de la traza, módulo, función, pila de llamadas, mensaje de la traza, usuario, etc… Es necesario para cada dominio de aplicación hacer un esfuerzo para seleccionar de manera explicita aquella información que por su relevancia es susceptible de ser traceado.
Para terminar, fuertemente relacionado con el tema del traceo se encuentra el manejo de excepciones, quiza os interese hechar un vistazo a unos post que escribí hace algún tiempo sobre antipatrones en el manejo de excepciones (I, II y III), donde recojo algunos errores habituales al logear excepciones.
Espero sugerencias y comentarios sobre como abordáis el traceo en vuestras aplicaciones.