Wie protokolliere ich ALLE Ausnahmen global für eine C # MVC4-WebAPI-App?

175

Hintergrund

Ich entwickle eine API-Service-Schicht für einen Client und wurde aufgefordert, alle Fehler global abzufangen und zu protokollieren.

Während also so etwas wie ein unbekannter Endpunkt (oder eine unbekannte Aktion) einfach zu handhaben ist, verwenden Sie ELMAH oder fügen Sie Folgendes hinzu Global.asax:

protected void Application_Error()
{
     Exception unhandledException = Server.GetLastError();
     //do more stuff
}

. . Nicht behandelte Fehler, die nicht mit dem Routing zusammenhängen, werden nicht protokolliert. Beispielsweise:

public class ReportController : ApiController
{
    public int test()
    {
        var foo = Convert.ToInt32("a");//Will throw error but isn't logged!!
        return foo;
    }
}

Ich habe auch versucht, das [HandleError]Attribut global festzulegen, indem ich diesen Filter registriert habe:

filters.Add(new HandleErrorAttribute());

Das protokolliert aber auch nicht alle Fehler.

Problem / Frage

Wie fange ich Fehler ab, wie sie durch den /testobigen Aufruf generiert wurden, damit ich sie protokollieren kann? Es scheint, dass diese Antwort offensichtlich sein sollte, aber ich habe alles versucht, was mir bisher einfällt.

Im Idealfall möchte ich der Fehlerprotokollierung einige Dinge hinzufügen, z. B. die IP-Adresse des anfordernden Benutzers, Datum, Uhrzeit usw. Ich möchte auch in der Lage sein, dem Support-Personal automatisch eine E-Mail zu senden, wenn ein Fehler auftritt. All dies kann ich tun, wenn ich diese Fehler nur abfangen kann, wenn sie auftreten!

AUFGELÖST!

Dank Darin Dimitrov, dessen Antwort ich akzeptierte, habe ich das herausgefunden. WebAPI behandelt Fehler nicht wie ein normaler MVC-Controller.

Folgendes hat funktioniert:

1) Fügen Sie Ihrem Namespace einen benutzerdefinierten Filter hinzu:

public class ExceptionHandlingAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is BusinessException)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "Exception"
            });

        }

        //Log Critical errors
        Debug.WriteLine(context.Exception);

        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("An error occurred, please try again or contact the administrator."),
            ReasonPhrase = "Critical Exception"
        });
    }
}

2) Registrieren Sie nun den Filter global in der WebApiConfig- Klasse:

public static class WebApiConfig
{
     public static void Register(HttpConfiguration config)
     {
         config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
         config.Filters.Add(new ExceptionHandlingAttribute());
     }
}

ODER Sie können die Registrierung überspringen und einfach einen einzelnen Controller mit dem [ExceptionHandling]Attribut dekorieren .

Matt Cashatt
quelle
Ich habe das gleiche Problem. Nicht behandelte Ausnahmen werden im Ausnahmefilterattribut abgefangen, aber wenn ich eine neue Ausnahme auslöse, wird sie nicht im Ausnahmefilterattribut abgefangen. Gibt es eine Idee dazu?
DaveBM
1
Unbekannte API-Controller-Aufrufe wie myhost / api / undefinedapicontroller- Fehler werden immer noch nicht abgefangen. Der Filtercode Application_error und Exception wird nicht ausgeführt. Wie fange ich sie auch?
Andrus
1
Die globale Fehlerbehandlung wurde zu WebAPI v2.1 hinzugefügt. Siehe meine Antwort hier: stackoverflow.com/questions/17449400/…
DarrellNorton
1
Dies fängt unter bestimmten Umständen keine Fehler ab, z. B. "Ressource nicht gefunden" oder Fehler in einem Controller-Konstruktor. Siehe hier: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/…
Jordan Morris
Hallo Matt. Sie haben die Antwort als Teil der Frage geschrieben, dies ist jedoch keine bewährte Methode in SO. Hier sollten die Antworten von der Frage getrennt sein. Könnten Sie das bitte als separate Antwort schreiben (Sie können die blaue Schaltfläche "Beantworten Sie Ihre eigene Frage" unten verwenden).
Sashoalm

Antworten:

56

Wenn Ihre Web-API in einer ASP.NET-Anwendung gehostet wird, wird das Application_ErrorEreignis für alle nicht behandelten Ausnahmen in Ihrem Code aufgerufen, einschließlich derjenigen in der von Ihnen gezeigten Testaktion. Sie müssen diese Ausnahme also nur innerhalb des Application_Error-Ereignisses behandeln. In dem Beispielcode, den Sie gezeigt haben, behandeln Sie nur Ausnahmen vom Typ, HttpExceptionwas beim Convert.ToInt32("a")Code offensichtlich nicht der Fall ist . Stellen Sie also sicher, dass Sie alle darin enthaltenen Ausnahmen protokollieren und behandeln:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }

    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int)HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;

            // TODO: don't forget that here you have many other status codes to test 
            // and handle in addition to 401.
        }
        else
        {
            // It was not an HttpException. This will be executed for your test action.
            // Here you should log and handle this case. Use the unhandledException instance here
        }
    }
}

Die Ausnahmebehandlung in der Web-API kann auf verschiedenen Ebenen erfolgen. Hier detailed articleerklären Sie die verschiedenen Möglichkeiten:

  • Benutzerdefiniertes Ausnahmefilterattribut, das als globaler Ausnahmefilter registriert werden kann

    [AttributeUsage(AttributeTargets.All)]
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is BusinessException)
            {
                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent(context.Exception.Message),
                    ReasonPhrase = "Exception"
                });
            }
    
            //Log Critical errors
            Debug.WriteLine(context.Exception);
    
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("An error occurred, please try again or contact the administrator."),
                ReasonPhrase = "Critical Exception"
            });
        }
    }
  • Benutzerdefinierter Aktionsaufrufer

    public class MyApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
    
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.GetBaseException();
    
                if (baseException is BusinessException)
                {
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Error"
    
                    });
                }
                else
                {
                    //Log critical error
                    Debug.WriteLine(baseException);
    
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Critical Error"
                    });
                }
            }
    
            return result;
        }
    }
Darin Dimitrov
quelle
Ich wünschte, es wäre so einfach, aber der Fehler wird immer noch nicht abgefangen. Ich habe die Frage aktualisiert, um Verwirrung zu vermeiden. Vielen Dank.
Matt Cashatt
@MatthewPatrickCashatt: Wenn diese Ausnahme im Application_ErrorEreignis nicht abgefangen wird , bedeutet dies, dass ein anderer Code sie zuvor verwendet. Zum Beispiel könnten Sie einige benutzerdefinierte HandleErrorAttributes, benutzerdefinierte Module, ... haben. Es gibt Unmengen anderer Stellen, an denen Ausnahmen abgefangen und behandelt werden könnten. Der beste Ort dafür ist jedoch das Application_Error-Ereignis, da dort alle nicht behandelten Ausnahmen enden.
Darin Dimitrov
Nochmals vielen Dank, aber egal was /testpassiert , das Beispiel wird nicht getroffen. Ich habe einen Haltepunkt in die erste Zeile gesetzt ( Exception unhandledException = . . .), kann diesen Haltepunkt jedoch im /testSzenario nicht erreichen. Wenn ich jedoch eine falsche URL eingebe, wird der Haltepunkt erreicht.
Matt Cashatt
1
@MatthewPatrickCashatt, du hast vollkommen recht. Das Application_ErrorEreignis ist nicht der richtige Ort, um Ausnahmen für die Web-API zu behandeln, da es nicht in allen Fällen ausgelöst wird. Ich habe einen sehr detaillierten Artikel gefunden, der die verschiedenen Möglichkeiten erklärt, dies zu erreichen: weblogs.asp.net/fredriknormen/archive/2012/06/11/…
Darin Dimitrov
1
@Darin Dimitrov Unbekannte API-Controller-Aufrufe wie myhost / api / undefinedapi- Fehler werden immer noch nicht abgefangen. Der Filtercode Application_error und Exception wird nicht ausgeführt. Wie fange ich sie auch?
Andrus
79

Als Ergänzung zu früheren Antworten.

Gestern wurde die ASP.NET Web API 2.1 offiziell veröffentlicht .
Es bietet eine weitere Möglichkeit, Ausnahmen global zu behandeln.
Die Details sind im Beispiel angegeben .

Kurz gesagt, Sie fügen globale Ausnahmeprotokollierer und / oder einen globalen Ausnahmebehandler hinzu (nur einen).
Sie fügen sie der Konfiguration hinzu:

public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes();

  // There can be multiple exception loggers.
  // (By default, no exception loggers are registered.)
  config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());

  // There must be exactly one exception handler.
  // (There is a default one that may be replaced.)
  config.Services.Replace(typeof(IExceptionHandler), new GenericTextExceptionHandler());
}

Und ihre Erkenntnis:

public class ElmahExceptionLogger : ExceptionLogger
{
  public override void Log(ExceptionLoggerContext context)
  {
    ...
  }
}

public class GenericTextExceptionHandler : ExceptionHandler
{
  public override void Handle(ExceptionHandlerContext context)
  {
    context.Result = new InternalServerErrorTextPlainResult(
      "An unhandled exception occurred; check the log for more information.",
      Encoding.UTF8,
      context.Request);
  }
}
Vladimir
quelle
2
Das hat perfekt funktioniert. Ich protokolliere und verarbeite gleichzeitig (weil ich die logID erhalte und sie zurückgebe, damit der Benutzer Kommentare hinzufügen kann), also setze ich Result auf ein neues ResponseMessageResult. Das nervt mich schon eine Weile, danke.
Brett
8

Warum neu werfen usw.? Dies funktioniert und der Servicerückgabestatus 500 usw.

public class LogExceptionFilter : ExceptionFilterAttribute
{
    private static readonly ILog log = LogManager.GetLogger(typeof (LogExceptionFilter));

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        log.Error("Unhandeled Exception", actionExecutedContext.Exception);
        base.OnException(actionExecutedContext);
    }
}
Anders
quelle
2

Haben Sie darüber nachgedacht, so etwas wie einen Handle-Error-Action-Filter wie zu machen?

[HandleError]
public class BaseController : Controller {...}

Sie können auch eine benutzerdefinierte Version erstellen, [HandleError]mit der Sie Fehlerinformationen und alle anderen zu protokollierenden Details schreiben können

KAL ERZÄHLT
quelle
Danke, aber ich habe das schon global eingestellt. Es stellt das gleiche Problem wie oben dar, nicht alle Fehler werden protokolliert.
Matt Cashatt
1

Wickeln Sie das Ganze in einen Versuch / Fang ein, protokollieren Sie die nicht behandelte Ausnahme und geben Sie sie dann weiter. Es sei denn, es gibt eine bessere integrierte Möglichkeit, dies zu tun.

Hier ist eine Referenz Ausnahmen von Catch All (behandelt oder nicht behandelt)

(edit: oh API)

Tim
quelle
Für alle Fälle müsste er auch die Ausnahme erneut auslösen.
DigCamara
@ DigCamara Sorry, das habe ich damit gemeint. werfen; sollte damit umgehen. Ich sagte ursprünglich "Entscheide, ob ich beenden oder neu laden soll" und stellte dann fest, dass er gesagt hatte, es sei eine API. In diesem Fall lassen Sie die App am besten entscheiden, was sie tun möchte, indem Sie sie weitergeben.
Tim
1
Dies ist eine schlechte Antwort, da dies bei jeder Aktion zu einer Menge duplizierten Codes führt.
Jansky