第16章 ASP.NET MVC 日志篇

   本章主要介绍MVC中内置的错误处理日志以及用来提升性能的监控工具

一、错误处理

    当该网站忙于处理HTTP请求时,很多内容都会出错。幸运的是,MVC让错误处理工作变得相对简单了很多,因为MVC应用是运行在MVC框架之上的,所以可以访问底层框架的核心功能,包括自定义错误处理页面及显示错误状态码。

    其中处理错误有三种基本的方法:

    (1)第一种:配置<customErrors/>节点

  也是最简单的一种,即启用MVC自定义错误处理特性;就是在web.config文件中配置<customErrors/>节点来开启MVC自定义错误处理特性。只需要将<customErrors/>节点的Mode属性设置为On和RemoteOnly即可开启。

    Model属性开启了三种模式:On、Off、RemoteOnly;

    On是启用自定义错误处理功能,当发生错误时可显示不同的自定义错误处理页面。

    Off是关闭自定义错误处理功能,即无论发生什么样的错误,都显示默认的错误诊断页面(通常的黄底红字的错误页面)。

    RemoteOnly也是启用自定义错误处理功能,但是是只针对来自远程机器的请求有效,如果是本地机器访问则还是会报黄底红字的错误页面这样有助于根据错误诊断来调试网站,用户则会看到自定义的错误页面。这里有必要说明一下本地用户和远程用户的概念。当我们访问asp.net应用程时所使用的机器和发布asp.net应用程序所使用的机器为同一台机器时成为本地用户,反之则称之为远程用户。在开发调试阶段为了便于查找错误Mode属性建议设置为Off,而在部署阶段应将Mode属性设置为On或者RemoteOnly,以避免这些详细的错误信息暴露了程序代码细节从而引来黑客的入侵。

  在<customErrors>节点下还包含有<error>子节点,这个节点主要是根据服务器的HTTP错误状态代码而重定向到我们自定义的错误页面,注意要使<error>子节点下的配置生效,必须将<customErrors>节点的Mode属性设置为“On”

 <system.web>
    <customErrors mode="On" defaultRedirect="">
      <error statusCode="500" redirect="~/Error/500.html"/>
      <error statusCode="404" redirect="~/Error/404.html"/>
      <error statusCode="403" redirect="~/Error/403.html"/>
    </customErrors>
  </system.web>

  (2)第二种:重写controller类的onException方法

  重写controller类的onException方式,这种方式最直接了,通常用于一个项目的BaseController中,那么以后的controller都继承这个类即可,可以在cs代码中记录错误日志,但是要定义错误页的action和具体的错误页面。方法中需要设置ExceptionHandled=true,否则错误会被抛到外层,这时候只能通过传统的aspnet错误方式处理了,通常是找<customErrors>节点中配置的错误页,如果没有配置,那就出现一个大黄页了。ExceptionHandled=true这个操作会使逻辑有微妙的变化,后续提到。

protected override void OnException(ExceptionContext filterContext)
{
     // 标记异常已处理
     filterContext.ExceptionHandled = true;
     // 跳转到错误页
     filterContext.Result = new RedirectResult(Url.Action("Error", "Shared"));
}

  (3)第三种:用过滤器HandleErrorAttribute

   虽然启用自定义错误处理功能是可以在网站发生错误的时候显示自定义错误页面,但是有时候只简单的显示自定义错误信息是不够的。随着ASP.NET MVC版本的更新,提供了HandleErrorAttribute标记属性,提供了对操作级别发生错误更细粒度的控制。HandleErrorAttribute使用Filter以AOP的思想实现了针对于Action的异常处理,使用此Filter后,当程序中出现异常的时候,会去封装这些异常信息,然后路由自动转到该文件夹对应的错误页面中,如果此路径下没有改文件,则会到共享视图文件夹shared目录中寻找此文件。

  需要注意的是,HandleErrorAttribute是在customErrors基础之上的,如果想使用HandleErrorAttribute,customErrors的Mode必须要设置为On或RemoteOnly. 否则,HandleErrorAttribute将不起作用

  HandleErrorAttribute属性:ExceptionType、View、Order、Master。

  ExceptionType是要处理异常的类型;

  View是发生该异常时要显示的视图名称;

  Order是执行该异常的顺序;

  Master是异常信息要使用的母版视图;

  HandleErrorAttribute可以作用于Action,也可以作用于Controller还可以设置为全局错误处理器,其使用方法:

//作用于Action
 [HandleError(ExceptionType = typeof(System.Data.DataException), View = "Error.cshtml")]
 public ActionResult Index()
{
      return View();
 }
//作用于Controller
 [HandleError(ExceptionType = typeof(System.Data.DataException), View = "Error.cshtml")]
 public class ErrorController : Controller
 {
     //
     // GET: /Error/

     public ActionResult Index()
     {
         return View();
     }

     public ActionResult Error()
     {
         throw new Exception("find a exception");
     }
 }

   为了注册全局错误处理器,打开项目下的App_Start/FilterConfig.cs文件,然后找到RegisterGlobalFilters方法,就可以看到ASP.NET MVC已经在GlobalFilterCollection全局过滤器集合中注册了HandleErrorAttribute。如果需要自定义逻辑,只要把自定义过滤器注册到全局过滤器集合中即可,默认情况下全局过滤器会按照他们的注册顺序执行,所以一定要确保在其他错误过滤器之前注册特定异常类型的错误过滤器,也可使用Order来控制器顺序。代码如下:

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            //注册自定义全局错误
            filters.Add(new HandleErrorAttribute() { ExceptionType = typeof(System.Data.DataException), View = "Error.cshtml",Order=1 });//这里的View的视图文件应该放到当前请求的文件夹或共享视图文件里
            filters.Add(new HandleErrorAttribute());//初始化网站全局错误处理器
        }
    }

  注:如果使用的是HandleErrorAttribute,则此时会忽略web.config里面设置的defaultRedirect和状态码重定向。

  如果以某action方法标记了HandleError属性,同时期所在的controller又重写了OnException方法,最终会怎样处理呢?按照mvc中filter的执行顺序,controller重写的方法会被优先执行,不考虑action中的order顺序,执行完毕之后再执行action标记的filter的方法。ok,有了这个理论之后,再看看之前提到的情况的执行顺序。首先执行OnException中的处理方式,这时候filterContext.ExceptionHandled已经被标记为true了,再执行HandleError属性的方法时,就不会在被执行了,也就是说自定义的错误页白费了,不起作用。这是因为内置的HandleError在执行的时候会先判断filterContext.ExceptionHandled是否为true,为true就不执行了,因此会出现一些很奇怪的bug,明白这个道理就知道如何处理了。

   但是总不能把filterContext.ExceptionHandled = true;这行代码去掉,因为其他action没有标记handle error属性,如果不使filterContext.ExceptionHandled为true, 那么错误还是会抛到外层,又交给CustomerError处理了,还是白搭。因此既要保持基类的OnException方法,又要有action自己个性化的错误页,是不能使用系统内置的方式处理,只能自己再去定义ExceptionFilter 了,就是方式四。

  (4)第四种:在Global.asax中的protected void Application_Error(object sender, EventArgs e)方法

  另外一个相关的是在Global.asax中的protected void Application_Error(object sender, EventArgs e)方法,是捕捉异常的最后一道防线,也就是说,这是最高层次的异常捕获处理逻辑。如果在使用HandleErrorAttribute后,找到了Error.cshtml,则此时异常已经被捕获处理,所以不会再次被Application_Error捕获处理,当出现异常的时候,把异常抛到最顶端,由Application_Error统一处理。这里的统一处理就包括记录日志,重新进行页面定向等。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace LiBlogWebUI.Controllers
{
    public class ErrorController : Controller
    {
        //
        // GET: /Error/

        public ActionResult Index()
        {
            return View();
        }

        public ActionResult NotFound()
        {
            throw new Exception("find a exception");
        }

        public ActionResult Error() 
        {
            return View();
        }
    }
}
using LiBlogWebUI.Controllers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;

namespace LiBlogWebUI
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }

        //当出现异常的时候,把异常抛到最顶端,由Application_Error统一处理。这里的统一处理就包括,记录日志,重新进行页面定向等。
        protected void Application_Error(object sender, EventArgs e)
        {
            var ex = Server.GetLastError();
            Log.Error(ex); //记录日志信息  
            var httpStatusCode = (ex is HttpException) ? (ex as HttpException).GetHttpCode() : 500; //这里仅仅区分两种错误  
            var httpContext = ((MvcApplication)sender).Context;
            httpContext.ClearError();
            httpContext.Response.Clear();
            httpContext.Response.StatusCode = httpStatusCode;
            var shouldHandleException = true;
            HandleErrorInfo errorModel;

            var routeData = new RouteData();
            routeData.Values["controller"] = "Error";

            switch (httpStatusCode)
            {
                case 404:
                    routeData.Values["action"] = "NotFound";
                    errorModel = new HandleErrorInfo(new Exception(string.Format("No page Found", httpContext.Request.UrlReferrer), ex), "Error", "NotFound");
                    break;

                default:
                    routeData.Values["action"] = "Error";
                    Exception exceptionToReplace = null; //这里使用了EntLib的异常处理模块的一些功能  
                    shouldHandleException = ExceptionPolicy.HandleException(ex, "LogAndReplace", out exceptionToReplace);
                    errorModel = new HandleErrorInfo(exceptionToReplace, "Error", "Error");
                    break;
            }

            if (shouldHandleException)
            {
                var controller = new ErrorController();
                controller.ViewData.Model = errorModel; //通过代码路由到指定的路径  
                ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
            }
        }
    }
}

 二、日志

  当网站发生错误时,就需要尽可能多的信息来帮助跟踪、调试错误。虽然现实错误信息是一种很好的通知用户的方式,但是它并不能帮助开发人员去处理问题,为了更好地找出网站的出错原因,可以在网站中记录一些必要的信息以及引起错粗时进行的操作,可以使用日志来记录 。

   (1)使用try catch语句来记录错误

        public ActionResult NotFound()
        {
            try
            {
                throw new Exception("find a exception");
            }
            catch (Exception ex)
            {
                Logger.Error(ex);//自定义的记录日志的类
            } 
return View();
}

   (2)重写controller类的onException方法

        protected override void OnException(ExceptionContext filterContext)
        {
            if (filterContext == null)
                base.OnException(filterContext);

            if (filterContext.HttpContext.IsCustomErrorEnabled)//如果启用了全局错误过滤器,就不执行下面的代码
            {
                filterContext.ExceptionHandled = true;
                this.View("Error").ExecuteResult(this.ControllerContext);
            }
        }

   (3)自定义错误过滤器

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace LiBlogWebUI.Filter
{
    public class ExceptionFilter : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            if (filterContext == null)
                base.OnException(filterContext);

            Logger.Error(filterContext.Exception);//自定义的记录日志的类

            if (filterContext.HttpContext.IsCustomErrorEnabled)//如果启用了全局错误过滤器,就不执行下面的代码
            {
                filterContext.ExceptionHandled = true;
                base.OnException(filterContext);
            }
        }
    }
}

三、ASP.NET健康监控

   虽然记录事件日志是监控网站很好的开始,但是一种更好的选择是启用ASP.NET健康监控(ASP.NET health monitoning)功能。ASP.NET健康监控(ASP.NET health monitoning)功能远远超出记录异常日志的范畴,而且还可以记录应用程序或请求生命周期内发生的事件。

  ASP.NET健康监控系统监控以下事件:

  (1)应用程序生命周期事件,包括应用程序开始和停止的事件

  (2)安全事件,例如,登录失败、URL授权请求

  (3)应用程序错误,包括未处理的异常、请求验证异常、编译错误等

  ASP.NET健康监控可以在网站的配置文件web.config的<healthMonitoring/>节点配置,这个节点包含三个子节点。

  eventMappings:定义要监控的事件类型

  providers:定义可用的提供者

  rules:定义在事件和提供者之间的用来记录事件的映射关系

  <system.web>
    <healthMonitoring>
      <eventMappings>
        <clear/>
        <!--记录所有的错误事件-->
        <add name="All Errors" type="System.Web.Management.WebBaseErrorEvent" startEventCode="0" endEventCode="2147483647"/>
        <!--记录应用程序开始和停止事件-->
        <add name="Application Events" type="System.Web.Management.WebApplicationLifetimeEvent" startEventCode="0" endEventCode="2147483647"/>
      </eventMappings>
      <providers>
        <clear/>
        <add connectionStringName="sqlCon" maxEventDetailsLength="1073741823" buffer="false" type="System.Web.Management.SqlWebEventProvider" name="SqlWebEventProvider"/>
      </providers>
      <rules>
        <clear/>
        <add name="All Errors Default" eventName="All Errors" provider="SqlWebEventProvider" profile="Default" minInstances="1" maxLimit="Infinite" minInterval="00:00:00"/>
        <add name="Application Events Default" eventName="Application Events" provider="SqlWebEventProvider" profile="Default" minInstances="1" maxLimit="Infinite" minInterval="00:00:00"/>
      </rules>
    </healthMonitoring>
  </system.web>

  ASP.NET健康监控包含把记录信息保存到Microsoft SQl Server数据库、本地事件日志,以及通过Email通知管理员等几种不同的提供者。当然它也允许我们创建自定义的提供者来记录到其他的数据源上。为了使用微软的SQLServer数据库健康监控提供者,需要在网站数据库的表中添加一些表。可以直接使用.NET命令里的aspnet_regsql.exe工具来实现。

  既然已经启用了ASP.NET健康监控功能,现在就来修改之前的自定义错误过滤器,把异常信息保存到健康监控提供者设置的数据源里。

  因为健康监控系统的System.Web.Management.WebRequestErrorEvent类没有任何公开的构造函数,所以,必须创建一个自定义web请求错误事件类,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Management;//需要引入此命名空间

namespace LiBlogWebUI.Filter
{
    public class WebRequestExceptionEvent : WebRequestErrorEvent
    {
        public WebRequestExceptionEvent(string message, object eventSource, int eventCode, Exception exception)
            : base(message, eventSource, eventCode, exception)
        { }

        public WebRequestExceptionEvent(string message, object eventSource, int eventCode, int eventDetailCode, Exception exception)
            : base(message, eventSource, eventCode, eventDetailCode, exception)
        { }
    }
}

  在创建完这个类后,修改ExceptionFilter类以调用这个自定义Web请求错误类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace LiBlogWebUI.Filter
{
    public class ExceptionFilter : HandleErrorAttribute
    {

        public override void OnException(ExceptionContext filterContext)
        {
            if (filterContext.HttpContext.IsCustomErrorEnabled)//如果启用了全局错误过滤器,就不执行下面的代码
            {
                base.OnException(filterContext);

                new WebRequestExceptionEvent("An unhandled exception han occurred",this,103005,filterContext.Exception).Raise();//通过将事件已发生这一情况通知任何已配置的提供程序来引发事件。
            }
        }
    }
}

   代码已写完,设置好并作为全局错误过滤器进行注册后,ASP.Net MVC网站所有的异常信息都会被路由到ASP.NET健康监控系统中,并且日志被保存起来

  参考文献:1、《ASP.NET MVC4 Web编程》 著:Jess Cbadwick,Todd Snyder,Hrusikesb Panda  译:徐雷 徐杨

 2、http://blog.csdn.net/sundacheng1989/article/details/9000596

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。