Comment faire fonctionner ELMAH avec l'attribut ASP.NET MVC [HandleError]?

564

J'essaie d'utiliser ELMAH pour enregistrer les erreurs dans mon application ASP.NET MVC, cependant lorsque j'utilise l'attribut [HandleError] sur mes contrôleurs, ELMAH n'enregistre aucune erreur lorsqu'elles se produisent.

Comme je le suppose, car ELMAH enregistre uniquement les erreurs non gérées et l'attribut [HandleError] gère l'erreur, donc pas besoin de la consigner.

Comment modifier ou comment modifier l'attribut pour qu'ELMAH puisse savoir qu'il y a une erreur et la consigner.

Edit: Permettez-moi de m'assurer que tout le monde comprend, je sais que je peux modifier l'attribut, ce n'est pas la question que je pose ... ELMAH est contourné lors de l'utilisation de l'attribut handleerror, ce qui signifie qu'il ne verra pas qu'il y a eu une erreur car elle a été gérée déjà par l'attribut ... Ce que je demande, c'est qu'il y a un moyen de faire voir ELMAH l'erreur et de l'enregistrer même si l'attribut l'a gérée ... J'ai cherché autour et je ne vois aucune méthode à appeler pour le forcer à se connecter l'erreur....

dswatik
la source
12
Wow, j'espère que Jeff ou Jared répondraient à cette question. Ils utilisent ELMAH pour Stackoverflow;)
Jon Limjap
11
Hmm, étrange - nous n'utilisons pas le HandleErrorAttribute - Elmah est configuré dans la section <modules> de notre web.config. Y a-t-il des avantages à utiliser HandleErrorAttribute?
Jarrod Dixon
9
@Jarrod - ce serait bien de voir ce qui est "personnalisé" sur votre fourche ELMAH.
Scott Hanselman
3
@dswatik Vous pouvez également empêcher les redirections en définissant redirectMode sur ResponseRewrite dans web.config. Voir blog.turlov.com/2009/01/…
Pavel Chuchuva
6
J'ai continué à courir dans la documentation Web et à publier des articles sur l'attribut [HandleError] et Elmah, mais je n'ai pas vu le comportement que cela résout (par exemple, Elmah ne consigne pas l'erreur "traitée") lorsque j'ai configuré le boîtier factice. En effet, depuis Elmah.MVC 2.0.x, ce HandleErrorAttribute personnalisé n'est plus requis; il est inclus dans le paquet nuget.
plyawn

Réponses:

503

Vous pouvez sous HandleErrorAttribute-classer et remplacer son OnExceptionmembre (pas besoin de copier) pour qu'il enregistre l'exception avec ELMAH et uniquement si l'implémentation de base la gère. La quantité minimale de code dont vous avez besoin est la suivante:

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);
    }
}

L'implémentation de base est invoquée en premier, ce qui lui permet de marquer l'exception comme étant gérée. Ce n'est qu'alors que l'exception est signalée. Le code ci-dessus est simple et peut poser des problèmes s'il est utilisé dans un environnement où il HttpContextpeut ne pas être disponible, comme les tests. Par conséquent, vous voudrez du code qui soit plus défensif (au prix d'être légèrement plus long):

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;
    }
}

Cette deuxième version essaiera d'abord d'utiliser la signalisation d'erreur d'ELMAH, ce qui implique le pipeline entièrement configuré comme la journalisation, l'envoi, le filtrage et ce que vous avez. A défaut, il tente de voir si l'erreur doit être filtrée. Sinon, l'erreur est simplement enregistrée. Cette implémentation ne gère pas les notifications par courrier électronique. Si l'exception peut être signalée, un e-mail sera envoyé s'il est configuré pour le faire.

Vous devrez peut-être également vous assurer que si plusieurs HandleErrorAttributeinstances sont en vigueur, la journalisation en double ne se produit pas, mais les deux exemples ci-dessus devraient vous aider à démarrer.

Atif Aziz
la source
1
Excellent. Je n'essayais pas du tout d'implémenter Elmah. J'essayais simplement de brancher mon propre rapport d'erreurs que j'ai utilisé pendant des années d'une manière qui fonctionne bien avec MVC. Votre code m'a donné un point de départ. +1
Steve Wortham
18
Vous n'avez pas besoin de sous-classer HandleErrorAttribute. Vous pouvez simplement avoir une implémentation IExceptionFilter et l'enregistrer avec HandleErrorAttribute. De plus, je ne comprends pas pourquoi vous devez avoir une solution de secours en cas d'échec de ErrorSignal.Raise (..). Si le pipeline est mal configuré, il doit être corrigé. Pour un point de contrôle IExceptionFilter à 5 lignes 4. ici - ivanz.com/2011/05/08/…
Ivan Zlatev
5
Pouvez-vous commenter la réponse ci-dessous par @IvanZlatev en ce qui concerne l'applicabilité, les lacunes, etc. Les gens disent que c'est plus facile / plus court / plus simple et donne le même résultat que votre réponse et, en tant que tel, doit être marqué comme la bonne réponse. Il serait bon d'avoir votre point de vue à ce sujet et de clarifier ces réponses.
Andrew
7
Est-ce toujours pertinent ou ELMAH.MVC s'en occupe?
Romias
2
Même moi, je voudrais savoir si c'est toujours pertinent dans la version d'aujourd'hui
refactor
299

Désolé, mais je pense que la réponse acceptée est exagérée. Tout ce que vous devez faire est le suivant:

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);
    }
}

puis l'enregistrer (l'ordre est important) dans Global.asax.cs:

public static void RegisterGlobalFilters (GlobalFilterCollection filters)
{
    filters.Add(new ElmahHandledErrorLoggerFilter());
    filters.Add(new HandleErrorAttribute());
}
Ivan Zlatev
la source
3
1 Très agréable, pas besoin d'étendre la HandleErrorAttribute, pas besoin de passer outre OnExceptionsur BaseController. C'est supposé à la réponse acceptée.
CallMeLaNN
1
@bigb Je pense que vous devriez encapsuler l'exception dans votre propre type d'exception pour ajouter des choses au message d'exception, etc. (par exemple, new UnhandledLoggedException(Exception thrown)qui ajoute quelque chose au Messageavant de le renvoyer.
Ivan Zlatev
23
Atif Aziz a créé ELMAH, j'irais avec sa réponse
jamiebarrow
48
@jamiebarrow Je ne m'en étais pas rendu compte, mais sa réponse a environ 2 ans et probablement l'API a été simplifiée pour prendre en charge les cas d'utilisation de la question de manière plus courte et autonome.
Ivan Zlatev
6
@Ivan Zlatev ne peut vraiment pas se mettre au travail ElmahHandledErrorLoggerFilter()elmah simplement en enregistrant les erreurs non gérées, mais pas gérées. J'ai enregistré les filtres dans le bon ordre, comme vous l'avez mentionné, des réflexions?
kuncevic.dev
14

Il y a maintenant un package ELMAH.MVC dans NuGet qui inclut une solution améliorée par Atif et également un contrôleur qui gère l'interface elmah dans le routage MVC (plus besoin d'utiliser cet axd)
Le problème avec cette solution (et avec toutes celles ici) ) est que d'une manière ou d'une autre, le gestionnaire d'erreurs elmah gère réellement l'erreur, ignorant ce que vous pouvez configurer en tant que balise customError ou via ErrorHandler ou votre propre gestionnaire d'erreurs
La meilleure solution à mon humble avis est de créer un filtre qui agira à la fin de tous les autres filtres et enregistrera les événements qui ont déjà été traités. Le module elmah doit prendre en charge la journalisation des autres erreurs non gérées par l'application. Cela vous permettra également d'utiliser le moniteur de santé et tous les autres modules qui peuvent être ajoutés à asp.net pour consulter les événements d'erreur

J'ai écrit ceci en regardant avec un réflecteur au ErrorHandler à l'intérieur d'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;
   }
}

Maintenant, dans votre configuration de filtre, vous voulez faire quelque chose comme ceci:

    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());
    }

Notez que j'ai laissé un commentaire là pour rappeler aux gens que s'ils veulent ajouter un filtre global qui gérera réellement l'exception, il devrait aller AVANT ce dernier filtre, sinon vous rencontrez le cas où l'exception non gérée sera ignorée par ElmahMVCErrorFilter car il n'a pas été géré et il devrait être journalisé par le module Elmah mais le filtre suivant marque l'exception comme gérée et le module l'ignore, ce qui fait que l'exception ne se transforme jamais en elmah.

Maintenant, assurez-vous que les paramètres d'application pour Elmah dans votre configuration Web ressemblent à ceci:

<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 -->

L'important ici est "elmah.mvc.disableHandleErrorFilter", si c'est faux, il utilisera le gestionnaire à l'intérieur d'elmah.mvc qui gérera réellement l'exception en utilisant le HandleErrorHandler par défaut qui ignorera vos paramètres d'erreur personnalisés

Cette configuration vous permet de définir vos propres balises ErrorHandler dans les classes et les vues, tout en enregistrant ces erreurs via ElmahMVCErrorFilter, en ajoutant une configuration customError à votre web.config via le module elmah, et même en écrivant vos propres gestionnaires d'erreur. La seule chose que vous devez faire est de ne pas ajouter de filtres qui gèreront réellement l'erreur avant le filtre elmah que nous avons écrit. Et j'ai oublié de mentionner: pas de doublons dans Elmah.

Raul Vejar
la source
7

Vous pouvez prendre le code ci-dessus et aller plus loin en introduisant une fabrique de contrôleurs personnalisés qui injecte l'attribut HandleErrorWithElmah dans chaque contrôleur.

Pour plus d'informations, consultez ma série de blogs sur la connexion à MVC. Le premier article traite de la configuration et du fonctionnement d'Elmah pour MVC.

Il y a un lien vers le code téléchargeable à la fin de l'article. J'espère que cela pourra aider.

http://dotnetdarren.wordpress.com/

Darren
la source
6
Il me semble qu'il serait beaucoup plus facile de simplement le coller sur une classe de contrôleur de base!
Nathan Taylor
2
La série de Darren ci-dessus sur la journalisation et la gestion des exceptions vaut bien la lecture !!! Très complet!
Ryan Anderson
6

Je suis nouveau dans ASP.NET MVC. J'ai rencontré le même problème, ce qui suit est mon réalisable dans mon Erorr.vbhtml (cela fonctionne si vous avez seulement besoin de consigner l'erreur en utilisant le journal 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> 

C'est tout simplement!

user716264
la source
C'est de loin la solution la plus simple. Pas besoin d'écrire ou d'enregistrer des gestionnaires et des trucs personnalisés. Fonctionne bien pour moi
ThiagoAlves
3
Sera ignoré pour toutes les réponses JSON / non HTML.
Craig Stuntz
6
cela fait également une fonctionnalité de niveau de service dans une vue. N'appartient pas ici.
Trevor de Koekkoek
6

Une solution complètement alternative consiste à ne pas utiliser le MVC HandleErrorAttributeet à s'appuyer à la place sur la gestion des erreurs ASP.Net, avec laquelle Elmah est conçu pour fonctionner.

Vous devez supprimer le global par défaut HandleErrorAttributed'App_Start \ FilterConfig (ou Global.asax), puis configurer une page d'erreur dans votre Web.config:

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

Notez que cela peut être une URL routée MVC, donc ce qui précède serait redirigé vers l' ErrorController.Indexaction lorsqu'une erreur se produit.

Ross McNab
la source
C'est de loin la solution la plus simple, et la redirection par défaut peut être une action MVC :)
Jeremy Cook
3
Cela redirigera pour d'autres types de demandes, comme JSON, etc. - pas bon.
zvolkov
5

Pour moi, il était très important de faire fonctionner la journalisation des e-mails. Après un certain temps, je découvre que cela n'a besoin que de 2 lignes de code de plus dans l'exemple 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);
        [...]
    }
    [...]
}

J'espère que cela aidera quelqu'un :)

Komio
la source
2

C'est exactement ce dont j'avais besoin pour ma configuration de site MVC!

J'ai ajouté une petite modification à la OnExceptionméthode pour gérer plusieurs HandleErrorAttributeinstances, comme suggéré par Atif Aziz:

gardez à l'esprit que vous devrez peut-être veiller à ce que si plusieurs HandleErrorAttributeinstances sont en vigueur, la journalisation en double ne se produise pas.

Je vérifie simplement context.ExceptionHandledavant d'appeler la classe de base, juste pour savoir si quelqu'un d'autre a géré l'exception avant le gestionnaire actuel.
Cela fonctionne pour moi et je poste le code au cas où quelqu'un d'autre en aurait besoin et pour demander si quelqu'un sait si j'ai oublié quelque chose.

J'espère que c'est utile:

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
la source
Vous ne semblez pas avoir une instruction "if" autour de l'appel de base.OnException () .... Et (exceptionHandledByPreviousHandler ||! Context.ExceptionHandled || ...) s'annulent et seront toujours vrais. Suis-je en train de manquer quelque chose?
joelvh
Je vérifie d'abord si un autre gestionnaire, appelé avant le courant, a géré l'exception et je stocke le résultat dans la variable: exceptionHandlerdByPreviousHandler. Ensuite, je donne la chance au gestionnaire actuel de gérer l'exception elle-même: base.OnException (context).
ilmatte
Je vérifie d'abord si un autre gestionnaire, appelé avant le courant, a géré l'exception et je stocke le résultat dans la variable: exceptionHandlerdByPreviousHandler. Ensuite, je donne la chance au gestionnaire actuel de gérer l'exception elle-même: base.OnException (context). Si l'exception n'a pas été gérée précédemment, elle peut être: 1 - Elle est gérée par le gestionnaire actuel, alors: exceptionHandledByPreviousHandler = false et! Context.ExceptionHandled = false 2 - Elle n'est pas gérée par le gestionnaire actuel et: exceptionHandledByPreviousHandler = false et! Context. ExceptionHandled true. Seul le cas 1 sera enregistré.
ilmatte