Jak zmusić ELMAH do pracy z atrybutem ASP.NET MVC [HandleError]?

564

Próbuję używać ELMAH do rejestrowania błędów w mojej aplikacji ASP.NET MVC, jednak kiedy używam atrybutu [HandleError] na moich kontrolerach, ELMAH nie rejestruje żadnych błędów, kiedy one wystąpią.

Zgaduję, ponieważ ELMAH rejestruje tylko nieobsługiwane błędy, a atrybut [HandleError] obsługuje błąd, więc nie trzeba go rejestrować.

Jak zmodyfikować lub jak zmienić modyfikację atrybutu, aby ELMAH mógł wiedzieć, że wystąpił błąd i go zarejestrować.

Edycja: pozwól mi upewnić się, że wszyscy rozumieją, wiem, że mogę zmodyfikować atrybut, który nie jest pytaniem, które zadaję ... ELMAH zostaje pominięty podczas używania atrybutu handleerror, co oznacza, że ​​nie zobaczy, że wystąpił błąd, ponieważ został obsłużony już za pomocą atrybutu ... Pytam, czy istnieje sposób, aby ELMAH zobaczył błąd i zarejestrował go, mimo że atrybut go obsłużył ... Rozejrzałem się dookoła i nie widzę żadnych metod wywoływania w celu wymuszenia zalogowania błąd....

dswatik
źródło
12
Wow, mam nadzieję, że Jeff lub Jared odpowiedzą na to pytanie. Używają ELMAH do Stackoverflow;)
Jon Limjap,
11
Hmm, dziwne - nie używamy HandleErrorAttribute - Elmah jest ustawiony w sekcji <modules> naszego web.config. Czy są zalety korzystania z HandleErrorAttribute?
Jarrod Dixon
9
@Jarrod - fajnie byłoby zobaczyć, co jest „niestandardowego” w widelcu ELMAH.
Scott Hanselman
3
@dswatik Można również zapobiec przekierowaniom, ustawiając redirectMode na ResponseRewrite w pliku web.config. Zobacz blog.turlov.com/2009/01/…
Pavel Chuchuva
6
Wciąż przeglądałem dokumentację internetową i posty mówiące o atrybucie [HandleError] i Elmah, ale nie widziałem zachowania, które to rozwiązuje (np. Elmah nie rejestruje błędu „obsłużonego”), gdy konfiguruję atrapę. Jest tak, ponieważ od Elmah.MVC 2.0.x ten niestandardowy atrybut HandleErrorAttribute nie jest już wymagany; jest zawarty w pakiecie nuget.
plyawn

Odpowiedzi:

503

Możesz podklasować HandleErrorAttributei zastępować jego OnExceptionelement członkowski (bez potrzeby kopiowania), aby rejestrował wyjątek w ELMAH i tylko wtedy, gdy podstawowa implementacja go obsługuje. Minimalna ilość potrzebnego kodu jest następująca:

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

Najpierw wywoływana jest implementacja podstawowa, co daje szansę na oznaczenie wyjątku jako obsługiwanego. Dopiero wtedy sygnalizowany jest wyjątek. Powyższy kod jest prosty i może powodować problemy, jeśli jest używany w środowisku, w którym HttpContextmoże nie być dostępny, na przykład podczas testowania. W rezultacie będziesz potrzebować bardziej defensywnego kodu (kosztem nieco dłuższego):

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

Ta druga wersja najpierw spróbuje użyć sygnalizacji błędów z ELMAH, która obejmuje w pełni skonfigurowany potok, taki jak rejestrowanie, wysyłanie pocztą, filtrowanie i co masz. W przeciwnym razie próbuje sprawdzić, czy błąd powinien zostać przefiltrowany. Jeśli nie, błąd jest po prostu rejestrowany. Ta implementacja nie obsługuje powiadomień pocztowych. Jeśli można zasygnalizować wyjątek, wiadomość zostanie wysłana, jeśli jest skonfigurowana do tego.

Być może trzeba będzie zadbać o to, aby w przypadku HandleErrorAttributewystąpienia wielu instancji zduplikowane rejestrowanie nie wystąpiło, ale powyższe dwa przykłady powinny zacząć.

Atif Aziz
źródło
1
Świetny. W ogóle nie próbowałem wdrożyć Elmah. Właśnie próbowałem podłączyć własne raportowanie błędów, z którego korzystałem przez lata, w sposób, który działa dobrze w MVC. Twój kod dał mi punkt wyjścia. +1
Steve Wortham
18
Nie musisz podklasować HandleErrorAttribute. Możesz po prostu mieć implementację IExceptionFilter i zarejestrować ją razem z HandleErrorAttribute. Nie rozumiem też, dlaczego trzeba mieć awarię w przypadku niepowodzenia ErrorSignal.Raise (..). Jeśli rurociąg jest źle skonfigurowany, należy go naprawić. Dla punktu kontrolnego 5-liniowego IExceptionFilter punkt 4. tutaj - ivanz.com/2011/05/08/...
Ivan Zlatev
5
Czy możesz skomentować poniższą odpowiedź @IvanZlatev w odniesieniu do zastosowania, niedociągnięć itp. Ludzie komentują, że jest to łatwiejsze / krótsze / prostsze i osiąga to samo co twoja odpowiedź i jako takie powinno być oznaczone jako poprawna odpowiedź. Dobrze byłoby mieć na to swoje zdanie i uzyskać pewną jasność dzięki tym odpowiedziom.
Andrew
7
Czy to nadal jest istotne, czy ELMAH.MVC obsługuje to?
Romias,
2
Nawet chciałbym wiedzieć, czy jest to nadal aktualne w dzisiejszej wersji
refaktoryzacja
299

Przepraszam, ale myślę, że zaakceptowana odpowiedź to przesada. Wszystko, co musisz zrobić, to:

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

a następnie zarejestruj go (ważna jest kolejność) w Global.asax.cs:

public static void RegisterGlobalFilters (GlobalFilterCollection filters)
{
    filters.Add(new ElmahHandledErrorLoggerFilter());
    filters.Add(new HandleErrorAttribute());
}
Ivan Zlatev
źródło
3
+1 Bardzo ładny, nie ma potrzeby, aby przedłużyć HandleErrorAttribute, nie ma potrzeby, aby zastąpić OnExceptionna BaseController. Przypuszcza to przyjętą odpowiedź.
CallMeLaNN,
1
@bigb Myślę, że musiałbyś zawrzeć wyjątek we własnym typie wyjątku, aby dołączyć rzeczy do komunikatu o wyjątku itp. (np. new UnhandledLoggedException(Exception thrown)który dołącza coś Messageprzed
zwrotem)
23
Atif Aziz stworzył ELMAH, pójdę z jego odpowiedzią
jamiebarrow
48
@jamiebarrow Nie zdawałem sobie z tego sprawy, ale jego odpowiedź ma około 2 lata i prawdopodobnie interfejs API został uproszczony, aby obsługiwać przypadki użycia pytania w krótszy i bardziej niezależny sposób.
Ivan Zlatev
6
@ Ivan Zlatev naprawdę nie może zabrać się do pracy ElmahHandledErrorLoggerFilter()Elmah po prostu rejestruje nieobsługiwane błędy, ale nie jest obsługiwany. Zarejestrowałem filtry we właściwej kolejności, jak wspomniałeś, jakieś myśli?
kuncevic.dev
14

W NuGet jest teraz pakiet ELMAH.MVC, który zawiera ulepszone rozwiązanie firmy Atif, a także kontroler, który obsługuje interfejs elmah w ramach routingu MVC (nie trzeba już używać tego axd)
. Problem z tym rozwiązaniem (i wszystkimi tymi tutaj ) jest to, że w ten czy inny sposób moduł obsługi błędów Elmah faktycznie obsługuje błąd, ignorując to, co możesz ustawić jako niestandardowy tagError lub za pośrednictwem narzędzia ErrorHandler lub własnego programu obsługi błędów
Najlepszym rozwiązaniem IMHO jest utworzenie filtra, który będzie działał na końcu wszystkich innych filtrów i rejestrował zdarzenia, które zostały już obsłużone. Moduł elmah powinien zająć się rejestrowaniem innych błędów, które nie są obsługiwane przez aplikację. Umożliwi to również użycie monitora kondycji i wszystkich innych modułów, które można dodać do asp.net w celu sprawdzenia zdarzeń błędów

Napisałem to patrząc z odbłyśnikiem na ErrorHandler wewnątrz 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;
   }
}

Teraz w konfiguracji filtra chcesz zrobić coś takiego:

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

Zauważ, że zostawiłem tam komentarz, aby przypomnieć ludziom, że jeśli chcą dodać filtr globalny, który faktycznie obsłuży wyjątek, powinien przejść PRZED tym ostatnim filtrem, w przeciwnym razie natrafisz na przypadek, w którym nieobsługiwany wyjątek zostanie zignorowany przez filtr ElmahMVCError, ponieważ nie został obsłużony i powinien zostać zarejestrowany przez moduł Elmah, ale następnie następny filtr oznacza wyjątek jako obsłużony, a moduł go ignoruje, co powoduje, że wyjątek nigdy nie przekształca się w elmah.

Teraz upewnij się, że ustawienia elmah w konfiguracji sieciowej wyglądają mniej więcej tak:

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

Ważnym tutaj jest „elmah.mvc.disableHandleErrorFilter”, jeśli jest to fałsz, użyje on modułu obsługi w elmah.mvc, który faktycznie obsłuży wyjątek, używając domyślnego HandleErrorHandler, który zignoruje twoje ustawienia customError

Ta konfiguracja pozwala ustawić własne tagi ErrorHandler w klasach i widokach, jednocześnie rejestrując te błędy za pomocą ElmahMVCErrorFilter, dodając konfigurację customError do pliku web.config za pośrednictwem modułu elmah, a nawet pisząc własne programy obsługi błędów. Jedyne, co musisz zrobić, to pamiętać, aby nie dodawać żadnych filtrów, które faktycznie obsługiwałyby błąd przed napisanym przez nas filtrem elmah. I zapomniałem wspomnieć: nie ma duplikatów w elmie.

Raul Vejar
źródło
7

Możesz wziąć powyższy kod i pójść o krok dalej, wprowadzając niestandardową fabrykę kontrolera, która wstrzykuje atrybut HandleErrorWithElmah do każdego kontrolera.

Aby uzyskać więcej informacji, sprawdź moją serię blogów na temat logowania do MVC. Pierwszy artykuł obejmuje konfigurację Elmah i uruchomienie dla MVC.

Na końcu artykułu znajduje się link do kodu do pobrania. Mam nadzieję, że to pomaga.

http://dotnetdarren.wordpress.com/

Darren
źródło
6
Wydaje mi się, że o wiele łatwiej byłoby po prostu przykleić go do podstawowej klasy kontrolera!
Nathan Taylor
2
Warto przeczytać powyższą serię Darrena dotyczącą logowania i obsługi wyjątków !!! Bardzo dokładny!
Ryan Anderson
6

Jestem nowy w ASP.NET MVC. Napotkałem ten sam problem, poniższe informacje są dostępne w moim pliku Erorr.vbhtml (działa, jeśli potrzebujesz tylko zalogować błąd przy użyciu dziennika 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> 

To jest po prostu!

użytkownik716264
źródło
To zdecydowanie najprostsze rozwiązanie. Nie trzeba pisać ani rejestrować niestandardowych programów obsługi i innych rzeczy. Działa dobrze dla mnie
ThiagoAlves
3
Zostanie zignorowany w przypadku odpowiedzi JSON / innych niż HTML.
Craig Stuntz
6
robi to również funkcjonalność poziomu usług w widoku. Nie należy tutaj.
Trevor de Koekkoek
6

Całkowicie alternatywnym rozwiązaniem jest nieużywanie MVC HandleErrorAttribute, a zamiast tego poleganie na obsłudze błędów ASP.Net, z którą Elmah ma współpracować.

Musisz usunąć domyślny globalny HandleErrorAttributez App_Start \ FilterConfig (lub Global.asax), a następnie skonfigurować stronę błędu w pliku Web.config:

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

Uwaga: może to być adres URL przekierowany przez MVC, więc powyższe przekieruje do ErrorController.Indexakcji, gdy wystąpi błąd.

Ross McNab
źródło
Jest to zdecydowanie najprostsze rozwiązanie, a domyślnym przekierowaniem może być akcja MVC :)
Jeremy Cook
3
Spowoduje to przekierowanie dla innych typów żądań, takich jak JSON itp. - niedobrze.
zvolkov
5

Dla mnie bardzo ważne było, aby rejestrowanie wiadomości e-mail działało. Po pewnym czasie odkryłem, że w przykładzie Atif potrzeba tylko 2 wierszy kodu.

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

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

Mam nadzieję, że to komuś pomoże :)

Komio
źródło
2

Właśnie tego potrzebowałem do konfiguracji mojej witryny MVC!

Dodałem małą modyfikację OnExceptionmetody do obsługi wielu HandleErrorAttributeinstancji, zgodnie z sugestią Atif Aziz:

pamiętaj, że być może trzeba będzie zadbać o to, aby w przypadku HandleErrorAttributewystąpienia wielu wystąpień zduplikowane rejestrowanie nie występowało.

Po prostu sprawdzam context.ExceptionHandledprzed wywołaniem klasy podstawowej, aby wiedzieć, czy ktoś inny obsłużył wyjątek przed bieżącym modułem obsługi.
Działa dla mnie i wysyłam kod na wypadek, gdyby ktoś go potrzebował, i pytam, czy ktoś wie, czy coś przeoczyłem.

Mam nadzieję, że jest to przydatne:

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
źródło
Wydaje się, że nie ma instrukcji „if” wokół wywoływania base.OnException () .... I (wyjątekHandledByPreviousHandler ||! Context.ExceptionHandled || ...) anuluje się nawzajem i zawsze będzie prawdą. Czy coś brakuje?
joelvh
Najpierw sprawdzam, czy jakikolwiek inny moduł obsługi, wywołany przed bieżącym, zarządzał wyjątkiem i zapisuję wynik w zmiennej: wyjątekHandlerdByPreviousHandler. Następnie daję bieżącemu handlerowi możliwość zarządzania samym wyjątkiem: base.OnException (kontekst).
ilmatte
Najpierw sprawdzam, czy jakikolwiek inny moduł obsługi, wywołany przed bieżącym, zarządzał wyjątkiem i zapisuję wynik w zmiennej: wyjątekHandlerdByPreviousHandler. Następnie daję bieżącemu handlerowi możliwość zarządzania samym wyjątkiem: base.OnException (kontekst). Jeśli wyjątek nie był wcześniej zarządzany, może to być: 1 - Jest zarządzany przez bieżący moduł obsługi, a następnie: wyjątekHandledByPreviousHandler = false i! Wyjątek Obsługiwana prawda. Rejestruje się tylko przypadek 1.
ilmatte