¿Cómo hacer que ELMAH funcione con el atributo ASP.NET MVC [HandleError]?

564

Estoy tratando de usar ELMAH para registrar errores en mi aplicación ASP.NET MVC, sin embargo, cuando uso el atributo [HandleError] en mis controladores, ELMAH no registra ningún error cuando ocurren.

Como supongo que es porque ELMAH solo registra errores no manejados y el atributo [HandleError] está manejando el error, por lo que no es necesario registrarlo.

¿Cómo modifico o cómo modificaría el atributo para que ELMAH pueda saber que hubo un error y registrarlo?

Editar: Permítanme asegurarme de que todos entiendan, sé que puedo modificar el atributo que no es la pregunta que estoy haciendo ... ELMAH se omite cuando se usa el atributo handleerror, lo que significa que no verá que hubo un error porque se manejó ya por el atributo ... Lo que pido es que haya una manera de hacer que ELMAH vea el error y lo registre a pesar de que el atributo lo manejó ... Busqué y no veo ningún método para llamarlo para forzarlo a iniciar sesión el error....

dswatik
fuente
12
Wow, espero que Jeff o Jared respondan esta pregunta. Están usando ELMAH para Stackoverflow;)
Jon Limjap
11
Hmm, extraño, no utilizamos el atributo HandleErrorAttribute. Elmah está configurado en nuestra sección <modules> de web.config. ¿Hay beneficios al usar HandleErrorAttribute?
Jarrod Dixon
99
@Jarrod: sería bueno ver qué hay de "personalizado" en tu horquilla ELMAH
Scott Hanselman el
3
@dswatik También puede evitar los redireccionamientos configurando redirectMode en ResponseRewrite en web.config. Ver blog.turlov.com/2009/01/…
Pavel Chuchuva
66
Seguí encontrando documentación web y publicaciones que hablaban sobre el atributo [HandleError] y Elmah, pero no vi el comportamiento que esto resuelve (por ejemplo, Elmah no registra el error "manejado") cuando configuré el caso ficticio. Esto se debe a que a partir de Elmah.MVC 2.0.x, este HandleErrorAttribute personalizado ya no es necesario; Está incluido en el paquete Nuget.
plyawn

Respuestas:

503

Puede subclasificar HandleErrorAttributey anular su OnExceptionmiembro (no es necesario copiar) para que registre la excepción con ELMAH y solo si la implementación base lo maneja. La cantidad mínima de código que necesita es la siguiente:

using System.Web.Mvc;
using Elmah;

public class HandleErrorAttribute : System.Web.Mvc.HandleErrorAttribute
{
    public override void OnException(ExceptionContext context)
    {
        base.OnException(context);
        if (!context.ExceptionHandled) 
            return;
        var httpContext = context.HttpContext.ApplicationInstance.Context;
        var signal = ErrorSignal.FromContext(httpContext);
        signal.Raise(context.Exception, httpContext);
    }
}

La implementación base se invoca primero, dándole la oportunidad de marcar la excepción como manejada. Solo entonces se señala la excepción. El código anterior es simple y puede causar problemas si se usa en un entorno donde HttpContextpuede no estar disponible, como las pruebas. Como resultado, querrá un código que sea más defensivo (a costa de ser un poco más largo):

using System.Web;
using System.Web.Mvc;
using Elmah;

public class HandleErrorAttribute : System.Web.Mvc.HandleErrorAttribute
{
    public override void OnException(ExceptionContext context)
    {
        base.OnException(context);
        if (!context.ExceptionHandled       // if unhandled, will be logged anyhow
            || TryRaiseErrorSignal(context) // prefer signaling, if possible
            || IsFiltered(context))         // filtered?
            return;

        LogException(context);
    }

    private static bool TryRaiseErrorSignal(ExceptionContext context)
    {
        var httpContext = GetHttpContextImpl(context.HttpContext);
        if (httpContext == null)
            return false;
        var signal = ErrorSignal.FromContext(httpContext);
        if (signal == null)
            return false;
        signal.Raise(context.Exception, httpContext);
        return true;
    }

    private static bool IsFiltered(ExceptionContext context)
    {
        var config = context.HttpContext.GetSection("elmah/errorFilter")
                        as ErrorFilterConfiguration;

        if (config == null)
            return false;

        var testContext = new ErrorFilterModule.AssertionHelperContext(
                              context.Exception, 
                              GetHttpContextImpl(context.HttpContext));
        return config.Assertion.Test(testContext);
    }

    private static void LogException(ExceptionContext context)
    {
        var httpContext = GetHttpContextImpl(context.HttpContext);
        var error = new Error(context.Exception, httpContext);
        ErrorLog.GetDefault(httpContext).Log(error);
    }

    private static HttpContext GetHttpContextImpl(HttpContextBase context)
    {
        return context.ApplicationInstance.Context;
    }
}

Esta segunda versión intentará usar primero la señalización de error de ELMAH, que involucra la tubería totalmente configurada como el registro, el envío de correos, el filtrado y lo que tenga. De lo contrario, intenta ver si se debe filtrar el error. Si no, el error simplemente se registra. Esta implementación no maneja las notificaciones por correo. Si se puede señalar la excepción, se enviará un correo si está configurado para hacerlo.

También es posible que tenga que tener cuidado de que si hay varias HandleErrorAttributeinstancias vigentes, no se produzca un registro duplicado, pero los dos ejemplos anteriores deberían comenzar.

Atif Aziz
fuente
1
Excelente. No estaba tratando de implementar Elmah en absoluto. Solo estaba tratando de conectar mi propio informe de errores que he usado durante años de una manera que funciona bien con MVC. Tu código me dio un punto de partida. +1
Steve Wortham
18
No necesita subclasificar HandleErrorAttribute. Simplemente puede tener una implementación IExceptionFilter y registrarla junto con HandleErrorAttribute. Además, no entiendo por qué necesita tener un respaldo en caso de que ErrorSignal.Raise (..) falle. Si la tubería está mal configurada, debe repararse. Para un punto de verificación IExceptionFilter de 5 líneas 4 aquí - ivanz.com/2011/05/08/…
Ivan Zlatev
55
Puede comentar la respuesta a continuación por @IvanZlatev con respecto a la aplicabilidad, las deficiencias, etc. Sería bueno tener su perspectiva sobre esto y lograr cierta claridad con estas respuestas.
Andrew
77
¿Sigue siendo relevante o ELMAH.MVC maneja esto?
Romias
2
Incluso me gustaría saber si aún es relevante en la versión de hoy
refactorice el
299

Lo siento, pero creo que la respuesta aceptada es una exageración. Todo lo que necesitas hacer es esto:

public class ElmahHandledErrorLoggerFilter : IExceptionFilter
{
    public void OnException (ExceptionContext context)
    {
        // Log only handled exceptions, because all other will be caught by ELMAH anyway.
        if (context.ExceptionHandled)
            ErrorSignal.FromCurrentContext().Raise(context.Exception);
    }
}

y luego registrarlo (el orden es importante) en Global.asax.cs:

public static void RegisterGlobalFilters (GlobalFilterCollection filters)
{
    filters.Add(new ElmahHandledErrorLoggerFilter());
    filters.Add(new HandleErrorAttribute());
}
Ivan Zlatev
fuente
3
1 Muy agradable, sin necesidad de extender el HandleErrorAttribute, no hay necesidad de anular OnExceptionel BaseController. Esto se supone a la respuesta aceptada.
CallMeLaNN
1
@bigb creo que tendría que envuelva la excepción en su propio tipo de excepción para añadir cosas que el mensaje de excepción, etc. (por ejemplo, new UnhandledLoggedException(Exception thrown)que añade algo a la Messageantes de devolverlo.
Ivan Zlatev
23
Atif Aziz creó ELMAH, iría con su respuesta
jamiebarrow
48
@jamiebarrow No me di cuenta de eso, pero su respuesta tiene ~ 2 años y probablemente la API se ha simplificado para admitir los casos de uso de la pregunta de una manera más breve y autónoma.
Ivan Zlatev
66
@Ivan Zlatev realmente no puede ponerse a trabajar ElmahHandledErrorLoggerFilter()elmah simplemente registrando errores no manejados, pero no manejados. Registré los filtros en el orden correcto como mencionaste, ¿alguna idea?
kuncevic.dev
14

Ahora hay un paquete ELMAH.MVC en NuGet que incluye una solución mejorada de Atif y también un controlador que maneja la interfaz elmah dentro del enrutamiento MVC (ya no es necesario usar ese axd) La mejor solución en mi humilde opinión es crear un filtro que actúe al final de todos los demás filtros y registre los eventos que ya se han manejado. El módulo elmah debe encargarse de registrar los otros errores que la aplicación no controla. Esto también le permitirá usar el monitor de salud y todos los demás módulos que se pueden agregar a asp.net para ver los eventos de error
El problema con esa solución (y con todos los que están aquí) ) es que de una forma u otra el controlador de errores de elmah realmente está manejando el error, ignorando lo que es posible que desee configurar como una etiqueta de error personalizada o mediante ErrorHandler o su propio controlador de errores

Escribí esto mirando con reflector al ErrorHandler dentro de elmah.mvc

public class ElmahMVCErrorFilter : IExceptionFilter
{
   private static ErrorFilterConfiguration _config;

   public void OnException(ExceptionContext context)
   {
       if (context.ExceptionHandled) //The unhandled ones will be picked by the elmah module
       {
           var e = context.Exception;
           var context2 = context.HttpContext.ApplicationInstance.Context;
           //TODO: Add additional variables to context.HttpContext.Request.ServerVariables for both handled and unhandled exceptions
           if ((context2 == null) || (!_RaiseErrorSignal(e, context2) && !_IsFiltered(e, context2)))
           {
            _LogException(e, context2);
           }
       }
   }

   private static bool _IsFiltered(System.Exception e, System.Web.HttpContext context)
   {
       if (_config == null)
       {
           _config = (context.GetSection("elmah/errorFilter") as ErrorFilterConfiguration) ?? new ErrorFilterConfiguration();
       }
       var context2 = new ErrorFilterModule.AssertionHelperContext((System.Exception)e, context);
       return _config.Assertion.Test(context2);
   }

   private static void _LogException(System.Exception e, System.Web.HttpContext context)
   {
       ErrorLog.GetDefault((System.Web.HttpContext)context).Log(new Elmah.Error((System.Exception)e, (System.Web.HttpContext)context));
   }


   private static bool _RaiseErrorSignal(System.Exception e, System.Web.HttpContext context)
   {
       var signal = ErrorSignal.FromContext((System.Web.HttpContext)context);
       if (signal == null)
       {
           return false;
       }
       signal.Raise((System.Exception)e, (System.Web.HttpContext)context);
       return true;
   }
}

Ahora, en su configuración de filtro, desea hacer algo como esto:

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        //These filters should go at the end of the pipeline, add all error handlers before
        filters.Add(new ElmahMVCErrorFilter());
    }

Tenga en cuenta que dejé un comentario allí para recordarle a las personas que si desean agregar un filtro global que realmente maneje la excepción, debe ir ANTES de este último filtro, de lo contrario, se encontrará con el caso en que ElmahMVCErrorFilter ignorará la excepción no controlada porque no se ha manejado y el módulo Elmah debería registrarlo, pero el siguiente filtro marca la excepción como manejada y el módulo la ignora, lo que da como resultado que la excepción nunca se convierta en elmah.

Ahora, asegúrese de que los ajustes de la aplicación para elmah en su configuración web se vean así:

<add key="elmah.mvc.disableHandler" value="false" /> <!-- This handles elmah controller pages, if disabled elmah pages will not work -->
<add key="elmah.mvc.disableHandleErrorFilter" value="true" /> <!-- This uses the default filter for elmah, set to disabled to use our own -->
<add key="elmah.mvc.requiresAuthentication" value="false" /> <!-- Manages authentication for elmah pages -->
<add key="elmah.mvc.allowedRoles" value="*" /> <!-- Manages authentication for elmah pages -->
<add key="elmah.mvc.route" value="errortracking" /> <!-- Base route for elmah pages -->

El importante aquí es "elmah.mvc.disableHandleErrorFilter", si esto es falso, usará el controlador dentro de elmah.mvc que realmente manejará la excepción al usar el HandleErrorHandler predeterminado que ignorará su configuración personalizada de error

Esta configuración le permite configurar sus propias etiquetas ErrorHandler en clases y vistas, mientras sigue registrando esos errores a través de ElmahMVCErrorFilter, agregando una configuración personalizada de error a su web.config a través del módulo elmah, incluso escribiendo sus propios controladores de errores. Lo único que debe hacer es recordar no agregar ningún filtro que realmente maneje el error antes del filtro elmah que hemos escrito. Y olvidé mencionar: no hay duplicados en elmah.

Raul Vejar
fuente
7

Puede tomar el código anterior e ir un paso más allá al introducir una fábrica de controladores personalizados que inyecta el atributo HandleErrorWithElmah en cada controlador.

Para obtener más información, consulte mi serie de blogs sobre cómo iniciar sesión en MVC. El primer artículo cubre cómo configurar y ejecutar a Elmah para MVC.

Hay un enlace al código descargable al final del artículo. Espero que ayude.

http://dotnetdarren.wordpress.com/

Darren
fuente
66
¡Me parece que sería mucho más fácil simplemente pegarlo en una clase de controlador base!
Nathan Taylor
2
¡Vale la pena leer la serie anterior de Darren sobre registro y manejo de excepciones! ¡Muy minucioso!
Ryan Anderson el
6

Soy nuevo en ASP.NET MVC. Me enfrenté al mismo problema, el siguiente es mi viable en mi Erorr.vbhtml (funciona si solo necesita registrar el error usando el registro de Elmah)

@ModelType System.Web.Mvc.HandleErrorInfo

    @Code
        ViewData("Title") = "Error"
        Dim item As HandleErrorInfo = CType(Model, HandleErrorInfo)
        //To log error with Elmah
        Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(New Elmah.Error(Model.Exception, HttpContext.Current))
    End Code

<h2>
    Sorry, an error occurred while processing your request.<br />

    @item.ActionName<br />
    @item.ControllerName<br />
    @item.Exception.Message
</h2> 

¡Es sencillo!

usuario716264
fuente
Esta es, con mucho, la solución más simple. No es necesario escribir o registrar manejadores personalizados y otras cosas. Funciona bien para mí
ThiagoAlves
3
Se ignorará para cualquier respuesta JSON / no HTML.
Craig Stuntz
66
También esto está haciendo la funcionalidad de nivel de servicio en una vista. No pertenece aquí.
Trevor de Koekkoek
6

Una solución completamente alternativa es no usar el MVC HandleErrorAttribute, y confiar en el manejo de errores ASP.Net, con el cual Elmah está diseñado para trabajar.

HandleErrorAttributeDebe eliminar el global predeterminado de App_Start \ FilterConfig (o Global.asax), y luego configurar una página de error en su Web.config:

<customErrors mode="RemoteOnly" defaultRedirect="~/error/" />

Tenga en cuenta que esto puede ser una URL enrutada MVC, por lo que lo anterior redirigiría a ErrorController.Index acción cuando se produzca un error.

Ross McNab
fuente
Esta es la solución más simple con diferencia, y la redirección predeterminada puede ser una acción MVC :)
Jeremy Cook
3
Eso redirigirá para otros tipos de solicitudes, como JSON, etc., no es bueno.
zvolkov
5

Para mí fue muy importante hacer que funcione el registro de correo electrónico. Después de un tiempo descubrí que esto solo necesita 2 líneas de código más en el ejemplo de Atif.

public class HandleErrorWithElmahAttribute : HandleErrorAttribute
{
    static ElmahMVCMailModule error_mail_log = new ElmahMVCMailModule();

    public override void OnException(ExceptionContext context)
    {
        error_mail_log.Init(HttpContext.Current.ApplicationInstance);
        [...]
    }
    [...]
}

Espero que esto ayude a alguien :)

Komio
fuente
2

¡Esto es exactamente lo que necesitaba para la configuración de mi sitio MVC!

Agregué una pequeña modificación al OnExceptionmétodo para manejar múltiples HandleErrorAttributeinstancias, como lo sugiere Atif Aziz:

tenga en cuenta que es posible que tenga que tener cuidado de que si hay varias HandleErrorAttributeinstancias vigentes, no se produzca un registro duplicado.

Simplemente verifico context.ExceptionHandledantes de invocar la clase base, solo para saber si alguien más manejó la excepción antes del controlador actual.
Funciona para mí y publico el código en caso de que alguien más lo necesite y pregunte si alguien sabe si pasé por alto algo.

Espero que sea útil:

public override void OnException(ExceptionContext context)
{
    bool exceptionHandledByPreviousHandler = context.ExceptionHandled;

    base.OnException(context);

    Exception e = context.Exception;
    if (exceptionHandledByPreviousHandler
        || !context.ExceptionHandled  // if unhandled, will be logged anyhow
        || RaiseErrorSignal(e)        // prefer signaling, if possible
        || IsFiltered(context))       // filtered?
        return;

    LogException(e);
}
ilmatte
fuente
No parece tener una declaración "if" alrededor de invocar base.OnException () .... And (exceptionHandledByPreviousHandler ||! Context.ExceptionHandled || ...) se cancelan mutuamente y siempre serán verdaderas. ¿Me estoy perdiendo de algo?
joelvh
Primero verifico si algún otro Handler, invocado antes del actual, manejó la excepción y almaceno el resultado en la variable: exceptionHandlerdByPreviousHandler. Luego le doy la oportunidad al controlador actual de administrar la excepción en sí: base.OnException (contexto).
ilmatte
Primero verifico si algún otro Handler, invocado antes del actual, manejó la excepción y almaceno el resultado en la variable: exceptionHandlerdByPreviousHandler. Luego le doy la oportunidad al controlador actual de administrar la excepción en sí: base.OnException (contexto). Si la excepción no se administró previamente, puede ser: 1: está administrada por el controlador actual, entonces: exceptionHandledByPreviousHandler = false and! Context.ExceptionHandled = false 2: no está administrada por el controlador actual y: exceptionHandledByPreviousHandler = false and! Context. Excepción Manejado verdadero. Solo se registrará el caso 1.
ilmatte