lunes, 20 de junio de 2011

[Winforms] Control global de Errores – Implementar Log

 

Introducción

He notado en varias oportunidades que en ciertas circunstancias surgen errores no controlador que son difíciles de rastrear

Estos es consecuencia de un descuido, al no agregar correctamente en todos los puntos importantes el try..catch, o simplemente por la lógica de la aplicación por se grande implicaría un gran esfuerzo poner en cada evento el control de errores.

Es por eso que definir un control global a nivel de aplicación podría ayudar, atrapando el problema y registrando que sucedió y donde, esta información es muy preciada cuando se esta perdido y no se sabe que produce el problema, mas cuando se esta en el entorno de producción y no se cuenta con una herramienta de debug

En este artículo se trataran los siguientes temas:

  1. Control global de errores
  2. Log usando System.IO.Log

 

1- Control Global Errores

El implementar la lógica para controlar globalmente los errores no es nada difícil, el truco esta en adjuntarse a un evento, concretamente:

Application.ThreadException

Un buen lugar para realizar esta asignación del evento es el archivos Program.cs, dentro del método Main()

[C#]

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);

    Application.Run(new Form1());
}

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
    MessageBox.Show(e.Exception.Message);
}

Para lograr esto mismo en vb.net requiere de algunos paso adicionales, ya que vb.net no brinda un acceso transparente al método Main, ni permite que este sea asignado como inicio de la aplicación, pero en este articulo

Winforms, realizar tareas antes de inicializar aplicación

explico como destrabar esta situación para poder así implementarlo como lo harían en c#, definiendo el Main() como inicio de la aplicación.

[VB.NET]

<STAThread()> _
Friend Shared Sub Main()

    Application.EnableVisualStyles()
    Application.SetCompatibleTextRenderingDefault(False)

    AddHandler Application.ThreadException, AddressOf Application_ThreadException

    Application.Run(New Form1())

End Sub

Private Shared Sub Application_ThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)

    MessageBox.Show(e.Exception.Message)

End Sub

El código en ambos lenguajes preserva el mismo concepto, pero la forma en como se adjunta el evento difiera bastante.

Entonces, ahora si sucediera algo como lo reflejado en la imagen

imagen1

O sea, si se ingresara caracteres en el calculo, el no atrapar el error en un bloque try..catch, haría que la aplicación finalice de forma brusca cerrándose, pero al tener definido el evento ThreadException, entraría en acción tomando el error y mostrando el mensaje.

Con estos simples paso ya tenemos el control global de errores implementado.

 

2- Log usando System.IO.Log

Si bien en una primer instancia hacer uso de un mensaje podría ayudar a detectar el problema en la aplicación, este podría evolucionar en un log a un archivo para dejar tracking de lo sucedido, en este caso no solo se pondría el mensaje del problema, sino que además se podría agregar el StackTrace para poder analizar que métodos se fueron ejecutando hasta causar el fallo.

En este caso el log a un archivos será muy simple

[C#]

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            using(FileRecordSequence record = new FileRecordSequence("application.log", FileAccess.Write))
            {

                string message = string.Format("[{0}]Message::{1} StackTrace:: {2}", DateTime.Now, 
                                                                                    e.Exception.Message, 
                                                                                    e.Exception.StackTrace);

                record.Append(CreateData(message), SequenceNumber.Invalid, 
                                                            SequenceNumber.Invalid, 
                                                            RecordAppendOptions.ForceFlush);
            }
        }


        private static IList<ArraySegment<byte>> CreateData(string str)
        {
            Encoding enc = Encoding.Unicode;

            byte[] array = enc.GetBytes(str);

            ArraySegment<byte>[] segments = new ArraySegment<byte>[1];
            segments[0] = new ArraySegment<byte>(array);

            return Array.AsReadOnly<ArraySegment<byte>>(segments);
        }

[VB.NET]

Private Shared Sub Application_ThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)

    Using record As New FileRecordSequence("application.log", FileAccess.Write)

        Dim message As String = String.Format("[{0}]Message::{1} StackTrace:: {2}", DateTime.Now, e.Exception.Message, e.Exception.StackTrace)

        record.Append(CreateData(message), SequenceNumber.Invalid, SequenceNumber.Invalid, RecordAppendOptions.ForceFlush)
    End Using

End Sub


Private Shared Function CreateData(ByVal str As String) As IList(Of ArraySegment(Of Byte))

    Dim enc As Encoding = Encoding.Unicode

    Dim _array As Byte() = enc.GetBytes(str)

    Dim segments As ArraySegment(Of Byte)() = New ArraySegment(Of Byte)(0) {}
    segments(0) = New ArraySegment(Of Byte)(_array)

    Return Array.AsReadOnly(Of ArraySegment(Of Byte))(segments)

End Function

Ahora el evento global de errores tiene bastante mas código, en donde se arma un mensaje bastante mas útil, el cual será enviado a la funcionalidad de log para registrar el suceso.

Para recuperar el archivo, solo deben ir a la ubicación donde esta el .exe, en este caso debería esta en la carpeta \bin\Debug del proyecto.

Lo mas probable es que el archivo no se legible a simple vista porque este sistema de log trabaja con el concepto de entradas de registros, es por eso que se confecciono un formulario muy simple para poder recuperar la información de forma visual.

[C#] 
[VB.NET] 

37 comentarios:

  1. Muy buen articulo... Gracias por compartir

    ResponderEliminar
  2. Hola, disculpa pero no logro entender como se utiliza tu aplicacion, ejecuto la demo del calculo, ingreso un numero y una letra y el error me dice: "La cadena de entrada no tiene el formato correcto.", luego intento abrir la ventana de "LogViewer", para supuestamente leer el error, pero no veo nada!, que estoy haciendo mal?

    gracias!!

    ResponderEliminar
  3. hola hgjhgj

    que raro, deberias ver el log del error

    si editas con el notepad el archivo donde se registro el error que generaste, puedes ver que hay texto alli, aunque no se entieneda del todo el contenido del archivo, pero hay algo que indique un registro del problema

    has validado que tengas permiso de escritura al archivo del log

    saludos

    ResponderEliminar
  4. La explicación del Application.ThreadException es exactamente lo que estaba buscando, mil gracias!!!

    ResponderEliminar
  5. No tiene nada que ver con el articulo pero tengo una duda:

    tengo que generar un reporte similar a esto:
    campo 1 campo2
    11 4 5
    12 1 2

    donde campo uno y campo 2 son enfermedades y 11 y 12 son tipos de dientes como puedo contar cuantos dientes de tipo 11 tienen la enfermedad tipo 1 y cuantos dientes tipo 11 tienen la enfermedad tipo dos sin tener que hacer una consulta como esta:
    select count(campo1) from tabla campo=12
    para saber cuantos hay celda por celda

    ResponderEliminar
  6. con respecto a la tabla mas claro seria
    campo1 campo2
    11 1 2
    12 3 1

    ResponderEliminar
  7. hola K-loca

    el tema es que sino entendi mal, las enfermedades del duente deberian ser registro y no campo

    porque usas campos para la enfermedad
    no seri mas simple si la tabla es del tipo

    TipoDienteEnfermedades (tabla)
    idtipoenfermedad PK
    TipoDiente
    idEnfermedad FK

    Enfermedades(tabla)
    idenfermedad PK
    descripcion


    entonces los datos podrian ser

    idtipoenfermedad | diente | idEnfermedad
    1 | 11 | 1
    2 | 11 | 2
    3 | 12 | 2


    Nota: no respeta muy bien el diseñod e la tabla por eso use "|" para separar cada campo

    como veras ahora es mas simple de contar porque als enfermedades son un campo relacionado con la tabla de enfermedades

    ahora si puedes agrupar

    SELECT E.descripcion, COUNT(*) FROM DienteEnfermedades DE INNER JOIN Enfermedades E
    ON DE.idenfermedad = E.idenfermendad
    WHERE tipodiente = 11


    saludos

    ResponderEliminar
  8. sobre mi duda a ver si me pude explicar mejor aqui esta:

    http://jjcaloca.blogspot.com/

    ResponderEliminar
  9. como haría para agregar a este log de errores por ejemplo el nombre del formulario donde se originó el error, para poder identificar mejor la fuente del mismo..???

    ResponderEliminar
  10. hola FEMAVEL

    si logueas el StackTrace del error alli deberia indicar en que clase (o sea en que form) se genero el error

    solo que el StackTrace menciona toda la pila de llamadas incluyendo los metodos que se invocaron, se que no es el form directamente, pero es que no se puede determinar el form que genera el error

    salvo que en cada metodo del form definas un try...catch y crees un propio exception, o sea

    try{
    } catch(Exception ex){
    throw new Exception(string.Format("{0} - Form:{1}", ex.Message, this.Name), ex)
    }

    esto lo deberias poner en cada evento del form, para asi con el this.Name armas un mensaje custom poniendo el nombre del formulario que genera el problema

    saludos

    ResponderEliminar
    Respuestas
    1. Hola Leandro, una consulta, y si es desde un método desde una clase que genera el error no me reconoce el THIS.NAME , que otra alternativa hay?

      Eliminar
  11. hola buenas estoy queriendo poner en practica tu ejemplo en vb creo una clase igual a la del ejemplo y creo el archivo log pero no logro comprender en que momento ejecutas la clase ya que en mi caso me muestra el error pero no logro sincronizar la clase
    estado probando en guardar este es el evento
    Private Sub bt_guardar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bt_guardar.Click
    Try
    If validarmelascajas(ErrorProvider1, txt_codiagronomo, txt_apellidos, txt_nombres, txt_telefono, txt_zona) Then
    Exit Sub '<-- sale del subprocedimiento
    End If

    Dim result As DialogResult
    result = MessageBox.Show("¿Seguro de Guardar el agronomo?", "Mensaje", MessageBoxButtons.OKCancel, MessageBoxIcon.Question)
    If result = DialogResult.OK Then
    cn.Open()
    Dim guardar As New SqlCommand("SP_AGRONOMO_GUARDAR", cn)
    guardar.CommandType = CommandType.StoredProcedure
    guardar.Parameters.AddWithValue("@CODIAGRON", txt_codiagronomo.Text)
    guardar.Parameters.AddWithValue("@APELLIDO_AGRONOMO", txt_apellidos.Text)
    guardar.Parameters.AddWithValue("@NOMBRE_AGRONOMO", txt_nombres.Text)
    guardar.Parameters.AddWithValue("@TELEF_AGRONOMO", txt_telefono.Text)
    guardar.Parameters.AddWithValue("@ZONA", txt_zona.Text)
    guardar.Parameters.AddWithValue("@ESTADO_AGRONOMO", ck_estado.Checked)
    'parate para guardar la accion del usuario
    Dim FECH As Date = Frm_Principal2.fecha.Text
    guardar.Parameters.AddWithValue("@USER_CREA", Frm_Principal2.coduser.Text)
    guardar.Parameters.AddWithValue("@FECHA_CREA", FECH)
    Dim msgparam As New SqlParameter("@msg", SqlDbType.VarChar, 100)
    msgparam.Direction = ParameterDirection.Output
    guardar.Parameters.Add(msgparam)
    Dim rowsAffected As Integer = guardar.ExecuteNonQuery()
    Dim mensaje As String = ""

    If rowsAffected > 0 Then
    mensaje = Convert.ToString(guardar.Parameters("@msg").Value)
    MessageBox.Show(mensaje, "Mensaje", MessageBoxButtons.OK)
    Else
    mensaje = Convert.ToString(guardar.Parameters("@msg").Value)
    MessageBox.Show(mensaje, "Mensaje", MessageBoxButtons.OK)
    End If
    cn.Close()
    bt_cancelar_Click(Nothing, Nothing)
    Else
    bt_cancelar_Click(Nothing, Nothing)
    End If
    Catch ex As Exception
    cn.Close()
    MessageBox.Show(ex.Message, "Mensaje", MessageBoxButtons.OK)

    Finally
    Frm_Principal2.fecha.Text = Date.Now
    End Try
    End Sub

    el error que me genera en el siguiente
    conversion strin a date ese error ya se a que se debe, pero me gustaria ocupar la forma que tu lo haces para que el usuario solo vea un mensaje prediseñado y un pueda ver el ultimo error generado o secrea un archivo por frm

    ResponderEliminar
  12. hola Fredy

    recuerda que si ejecutas el codigo desde el propio VS este mostrara igualmente el error porque estas depurando, ejecuta sin debug desde el VS (o sino desde el .exe que se genera) y seguramente ya no tendras ningun mensaje de error

    si quieres poner el control centralizado no definas ningun try...catch en el evento o metodo que desarrolles deja que se encargue el evento global que asignas en el Main() asi podras tomar el error loguearlo a un archivo y mostrar un mensjae estandar, en resumen quita el try..catch del codigo

    saludos

    ResponderEliminar
  13. hola Leandro.
    estaba en otras cosas, pero ya le saque varias galladas a la aplicacion que hay que mejorar, y estado probando y no puedo aplicar tu codigo en el proyecto, cuando genero un excepcion no crea el archivo log, como podrias revisar el proyecto para que veas que es lo q estoy realizando mal

    ResponderEliminar
  14. hola Fredy

    para crear un archivo de log quizas no es buena idea usar la que implemente en el articulo, sino usar algun framework de log como ser log4net o sino System.Diagnostics

    http://www.3engine.net/wp/2011/01/como-escribir-facilmente-un-fichero-log-en-net-framework/

    esta es una mejor alternativa

    saludos

    ResponderEliminar
  15. Hola Leandro, Estoy trabajando en capas. Mi pregunta es, con esto me aseguro que se atrapen todas las excepciones de mi aplicación?.

    Pregunta adicional:
    Yo en mi capa de negocios suelo lanzar excepciones con throw new por lo cual lo atrapo con un try catch desde mi capa de presentación.. con esto que planteas aca ya no deberia tener los try catch en mi capa de presentacion?

    ResponderEliminar
  16. Si Señor

    exacto, todos los errores que lleguen a la UI y que no fueorn controlados por un try...catch entraran por el control global de errores

    puedes ponerlo si algun caso particular lo requiere, pero ya no sera necesario definirlo en todos los metodo de los eventos

    saludos

    ResponderEliminar
  17. Hola Leandro,
    Perfecto, lo probé y es todo como vos decís!!

    saludos

    ResponderEliminar
  18. Hola leandro, te pregunto algo mas que me quedo..
    Tengo un método para desencriptar campos de la BD.
    El algortimo es AES

    el Application.ThreadException me detecta los errores de todos los eventos menos de este método.. a que se debe?

    El error:
    Longitud no valida para una matriz o cadena de caracteres Base-64

    Este error aparece por que cambio los caracteres de la encriptacion.

    Si en este método yo coloco

    try

    'implementacion

    catch ex as exception

    msgbox(ex.message)

    end try

    el error me lo reconoce y lo muestra.. Lo dejo así?

    ResponderEliminar
  19. hola Si Señor

    lo que veo alli falla no es la desencriptacion sino la aplicacion del Base64, validaste que el campo no este nullable ? o que el campo no tenga datos
    quizas si no viene datos y aplicar la desencriptacion en ese momento falle

    ademas es raro que el try..catch no tome el error, estas seguro que se produce dentro de la definicion de este bloque de codigo, no sera que se produce fuera

    saludos

    ResponderEliminar
  20. Hola Leandro, ahi me reconoce el error.

    Esto que planteas aca solo sirve para los errores de los formularios?

    Porque recien haciendo una prueba de conexion a mi base de datos no me tomo la excepcion sqlexception

    hay alguna forma de controlar tambien estos errores?

    ResponderEliminar
  21. hola Si Señor

    esta tecnica controla los errores de toda la aplicacion, si se produce en alguna otra libreria que es usada desde la UI tambien la controlara

    las SqlException deberia tambien atraparlas, pero ojo que no engañe el hecho que el VS se detenga en la exception, porque esto sucede al estar dentro del VS, lo importante es si dejas seguir el codigo que ingrese al evento que controla de forma global los errores

    saludos

    ResponderEliminar
  22. Hola Leandro,
    No me toma las excepciones que ocurren dentro del submain

    tengo esto

    AddHandler Application.ThreadException, AddressOf Application_ThreadException

    'Rutina para validar registros de la BD (Aca tira error no controlado)

    Application.Run(New Form1())

    despues al iniciar la aplicacion si me detecta todos los errores!!

    solamente el del submain no!

    ResponderEliminar
  23. hola Si Señor

    intenta asignando el evento

    AppDomain.UnhandledException (Evento)

    en el link hay ejemplos de como usarlo

    saludos

    ResponderEliminar
  24. Hola @Leandro como se llama el archivo de errores.

    ResponderEliminar
  25. hola Pedro

    no te preocupes por el archivo, la parte del log no tienes que implementarla solo asigna los eventos al Application.ThreadException
    y muestra el mensaje en un messagebox

    la idea es ver si se puede capturar el problema desde alli, despues ves cual es la mejor forma de loguearlo
    saludos

    ResponderEliminar
  26. Me marca un error en "FileRecordSequence".

    He de añadir una directiva using o una referencia de ensamblado.

    ¿Cómo?

    Gracias.

    ResponderEliminar
  27. hola Dudas

    si tienes que agregar la referencia a System.IO.Log, seguro veras en los ejemplos que tengo ese using definido

    igualmente si es para un log recomendaria usar

    Cómo escribir facilmente un fichero Log en .NET Framework

    es mas facil de crear el log
    saludos

    ResponderEliminar
  28. Otra opción es NLog, les dejo este link con un turial básico pero explicado para principiantes, cortesía de Franklin (el autor): http://redk33.wordpress.com/2012/06/22/agregando-logs-a-nuestro-proyecto/#comment-39

    ResponderEliminar
  29. hola César

    si es verdad que la funcionalidad de System.IO no es muy practica para loguear

    esta libreria que comentas parece interesante, aunque suelo utilizar log4net

    saludos

    ResponderEliminar
  30. hola mi problema es que me sale un error de windows xp que dice asi error in main app- se produjo una excepcion en el inicializador de tipo de y me aparece un cuadrado entre comillas que es?

    ResponderEliminar
  31. hola mark

    la verdad que no tengo ni idea, quizas si tomas una imagen del problema ayude

    esta ventana tiene un boton de detalle?
    has creando un control de errores como explico en el articulo

    se produce cuando ejecutas desde el VS o cuando lo haces desde el .exe?

    saludos

    ResponderEliminar
  32. Leandro tuttini no sabe cuanto le agradesco este aporte, ha sido de gran ayuda a mi proyeto, gracias!!!!!

    ResponderEliminar
  33. Mil gracias esto es lo que estaba buscando... !!!
    Sldos.

    ResponderEliminar
  34. Hola Leandro, me nace una consulta, si le agregara un botón "Limpiar" como podría limpiar ese log que esta en el archivo?

    ResponderEliminar
    Respuestas
    1. hola
      En realidad esta forma que expongo en el articulo no es la mejor forma de loguear en un archivo
      Cómo escribir facilmente un fichero Log en .NET Framework
      usando las librerias de .net es mas simple de loguear y configurar
      saludos

      Eliminar