天天看點

.NET WebAPI 用ExceptionFilterAttribute實作錯誤(異常)日志的記錄(log4net做寫庫操作)

好吧,還是那個社群APP,非管理系統,使用者行為日志感覺不是很必要的,但是,錯誤日志咱還是得記錄則個。總不能上線後報bug了讓自己手足無措吧,雖然不管有木有錯誤日志報bug都是件很頭疼的事...

我們知道webAPI也有好幾個Filter,上篇文章我們做token與權限用到了ActionFilterAttribute,這次我們用ExceptionFilterAttribute來做異常日志的記錄。首先我們的代碼裡面會主動的捕獲一些異常手動抛出,例如對使用者輸入資料的驗證,權限的驗證,業務的驗證等。也會有一些我們無法預料的異常,可能是代碼的漏洞或者邏輯的漏洞...那麼我們肯定是想能夠在一個切面全部攔截這些異常,記錄到錯誤日志中,以便作分析使用...

部落客使用的log4net做日志的寫庫操作,這裡就不介紹log4net的基本用法了,直接上代碼:

1 <log4net>
  2     <!--<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
  3       <file value="C:\testlog.txt" />
  4       <appendToFile value="true" />
  5       <maxSizeRollBackups value="10" />
  6       <maximumFileSize value="100" />
  7       <rollingStyle value="Date" />
  8       <datePattern value="yyyy-MM-dd" />
  9       <staticLogFileName value="true" />
 10       <layout type="log4net.Layout.PatternLayout">
 11         <conversionPattern value="記錄時間:%date 線程ID:[%thread] 日志級别:%-5level 出錯類:%logger property:[%property{NDC}] - 錯誤描述:%message%newline " />
 12       </layout>
 13     </appender>-->

 14     <appender name="AdoNetAppender_Sqlserver" type="log4net.Appender.AdoNetAppender">
 15       <connectionType value="System.Data.SqlClient.SqlConnection,System.Data, Version=4.0.0.0, Culture=neutral,PublicKeyToken=b77a5c561934e089" />
 21       <connectionString value="Data Source=.;database=RCBLog;Integrated Security=True; MultipleActiveResultSets=True;" />
 22       <commandText value="INSERT INTO ErrorLog (LOGID,LOG_DATE,LOG_MESSAGE,LOG_EXCEPTION,LOG_LEVEL,LOGGER,LOG_SOURCE,OPERATORID,OPERATORACCOUNTNAME) VALUES (@LogId,@log_date,@LogMessage,@LogException,@log_level, @logger, @source,@LogOperator,@OperatorAccountName)" />
 23       <bufferSize value="100" />
 24       <parameter>
 25         <parameterName value="@log_date" />
 26         <dbType value="DateTime" />
 27         <layout type="log4net.Layout.RawTimeStampLayout" />
 28       </parameter>
 29       <parameter>
 30         <parameterName value="@log_level" />
 31         <dbType value="String" />
 32         <size value="100" />
 33         <layout type="log4net.Layout.PatternLayout">
 34           <conversionPattern value="%level" />
 35         </layout>
 36       </parameter>
 37       <parameter>
 38         <parameterName value="@logger" />
 39         <dbType value="String" />
 40         <size value="255" />
 41         <layout type="log4net.Layout.PatternLayout">
 42           <conversionPattern value="%logger" />
 43         </layout>
 44       </parameter>
 45       <parameter>
 46         <parameterName value="@source" />
 47         <dbType value="String" />
 48         <size value="2000" />
 49         <layout type="log4net.Layout.PatternLayout">
 50           <conversionPattern value="%file:%line" />
 51         </layout>
 52       </parameter>
 53       <!--自定義屬性-->
 54       <parameter>
 55         <parameterName value="@LogId" />
 56         <dbType value="String" />
 57         <size value="255" />
 58         <layout type="MP.Infrastructure.SystemLog.CustomLayout,MP.Infrastructure">
 59           <conversionPattern value="%LogId" />
 60         </layout>
 61       </parameter>
 62       <parameter>
 63         <parameterName value="@LogException" />
 64         <dbType value="String" />
 65         <size value="4000" />
 66         <layout type="MP.Infrastructure.SystemLog.CustomLayout,MP.Infrastructure">
 67           <conversionPattern value="%LogException" />
 68         </layout>
 69       </parameter>
 70       <parameter>
 71         <parameterName value="@LogMessage" />
 72         <dbType value="String" />
 73         <size value="4000" />
 74         <layout type="MP.Infrastructure.SystemLog.CustomLayout,MP.Infrastructure">
 75           <conversionPattern value="%LogMessage" />
 76         </layout>
 77       </parameter>
 78       <parameter>
 79         <parameterName value="@LogOperator" />
 80         <dbType value="String" />
 81         <size value="255" />
 82         <layout type="MP.Infrastructure.SystemLog.CustomLayout,MP.Infrastructure">
 83           <conversionPattern value="%OperatorId" />
 84         </layout>
 85       </parameter>
 86       <parameter>
 87         <parameterName value="@OperatorAccountName" />
 88         <dbType value="String" />
 89         <size value="255" />
 90         <layout type="MP.Infrastructure.SystemLog.CustomLayout,MP.Infrastructure">
 91           <conversionPattern value="%OperatorAccountName" />
 92         </layout>
 93       </parameter>
 94     </appender>
 95     <logger name="RCB.Logger.Error">
 96       <level value="ERROR" />
 97       <!--<appender-ref ref="RollingFileAppender" />-->
 98       <appender-ref ref="AdoNetAppender_Sqlserver" />
 99     </logger>
100   </log4net>      

注:

1.<log4net>節點需要在<configuration>節點下

2.注釋掉的2-13行與97行是寫檔案

3.第23行的數值表示緩存值,調試階段可以設定成0,才能及時的在資料庫看到錯誤日志

4.部落客使用到了自定義屬性,就順便說說自定義屬性的用法,先看看部落客的錯誤日志類:

1     /// <summary>
 2     /// 系統錯誤日志
 3     /// </summary>
 4     public class ErrorLog
 5     {
 6         /// <summary>
 7         /// ID(GUID字元串)
 8         /// </summary>
 9         public string LOGID { get; set; }
10 
11         /// <summary>
12         /// 日志時間
13         /// </summary>
14         public DateTime LOG_DATE { get; set; }
15 
16         /// <summary>
17         /// 日志錯誤資訊
18         /// </summary>
19         public string LOG_MESSAGE { get; set; }
20 
21         /// <summary>
22         /// 異常資訊詳情
23         /// </summary>
24         public string LOG_EXCEPTION { get; set; }
25 
26         /// <summary>
27         /// 錯誤級别
28         /// </summary>
29         public string LOG_LEVEL { get; set; }
30 
31         /// <summary>
32         /// 記錄器(PRMMS.Logger)
33         /// </summary>
34         public string LOGGER { get; set; }
35 
36         /// <summary>
37         /// 日志産生位置
38         /// </summary>
39         public string LOG_SOURCE { get; set; }
40 
41         /// <summary>
42         /// 操作人ID
43         /// </summary>
44         public string OperatorId { get; set; }
45 
46         /// <summary>
47         /// 操作賬戶名
48         /// </summary>
49         public string OperatorAccountName { get; set; }
50 
51         /// <summary>
52         /// 自動建立ID
53         /// </summary>
54         public ErrorLog()
55         {
56             this.LOGID = Guid.NewGuid().ToString("N").ToUpper();
57         }
58     }      

其中,LogId、LogMessage、OperatorId、OperatorAccountName、LogException等字段是log4net不帶有的,屬于自定義屬性,需要做一個配置。我們建立一個CustomLayout類,繼承于PatternLayout

1     public class CustomLayout : PatternLayout
 2     {
 3         public CustomLayout()
 4         {
 5             base.AddConverter("LogId", typeof(LogId));
 6             base.AddConverter("LogMessage", typeof(LogMessage));
 7             base.AddConverter("OperatorId", typeof(OperatorId));
 8             base.AddConverter("OperatorAccountName", typeof(OperatorAccountName));
 9             base.AddConverter("LogException", typeof(LogException));
10         }
11     }      

其中,typeof(LogId)中的LogId是需要我們建立類繼承PatternLayoutConverter實作Convert方法的

1     internal sealed class LogId : PatternLayoutConverter
 2     {
 3         protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
 4         {
 5             var content = loggingEvent.MessageObject as ErrorLog;
 6             if (content != null)
 7             {
 8                 writer.Write(content.LOGID);
 9             }
10         }
11     }      

當然,剩餘幾個typeof()做同樣處理即可。

log4net寫庫配置好了,我們還需要一個日志工具類,用來調用log4net寫日志,部落客在這兒簡單寫了幾個方法,其中截取2000長度純屬個人原因,沒有特别意義:

1     /// <summary>
 2     /// 日志工具類
 3     /// </summary>
 4     public class LogUtils
 5     {
 6         private static readonly log4net.ILog errorLog = log4net.LogManager.GetLogger("RCB.Logger.Error");
 7 
 8         /// <summary>
 9         /// 将指定的<see cref="Exception"/>執行個體詳細資訊寫入錯誤日志。
10         /// </summary>
11         /// <returns></returns>
12         public static void ErrorLog(Guid userId, string userName, Exception exception)
13         {
14             if (exception != null)
15             {
16                 var exceptionString = exception.ToString();
17                 if (exceptionString.Length > 2000)
18                 {
19                     exceptionString = exceptionString.Substring(0, 1999);
20                 }
21                 errorLog.Error(new ErrorLog
22                 {
23                     OperatorId = userId.ToString("N").ToUpper(),
24                     OperatorAccountName = userName,
25                     LOG_MESSAGE = exception.Message,
26                     LOG_EXCEPTION = exceptionString
27                 });
28             }
29         }
30 
31         /// <summary>
32         /// 将指定的<see cref="Exception"/>執行個體詳細資訊寫入錯誤日志。
33         /// 
34         /// 記錄IP位址
35         /// </summary>
36         /// <returns></returns>
37         public static void ErrorLog(string userIp, Exception exception)
38         {
39             if (exception != null)
40             {
41                 var exceptionString = exception.ToString();
42                 if (exceptionString.Length > 2000)
43                 {
44                     exceptionString = exceptionString.Substring(0, 1999);
45                 }
46                 errorLog.Error(new ErrorLog
47                 {
48                     OperatorId = userIp,
49                     LOG_MESSAGE = exception.Message,
50                     LOG_EXCEPTION = exceptionString
51                 });
52             }
53         }
54 
55         /// <summary>
56         /// 将指定的<see cref="Exception"/>執行個體詳細資訊寫入錯誤日志。
57         /// </summary>
58         /// <returns></returns>
59         public static void ErrorLog(Exception exception)
60         {
61             if (exception != null)
62             {
63                 var exceptionString = exception.ToString();
64                 if (exceptionString.Length > 2000)
65                 {
66                     exceptionString = exception.ToString().Substring(0, 1999);
67                 }
68                 errorLog.Error(new ErrorLog
69                 {
70                     LOG_MESSAGE = exception.Message,
71                     LOG_EXCEPTION = exceptionString
72                 });
73             }
74         }
75     }      

接下來,我們就是要寫Filter了。建立一個ExceptionFilter類,繼承于ExceptionFilterAttribute

1     /// <summary>
 2     /// 異常攔截器
 3     /// </summary>
 4     public class ExceptionFilter : ExceptionFilterAttribute
 5     {
 6         private HttpResponseMessage GetResponse(int code, string message)
 7         {
 8             var resultModel = new ApiModelsBase() { Code = code, Message = message };
 9 
10             return new HttpResponseMessage()
11             {
12                 Content = new ObjectContent<ApiModelsBase>(
13                     resultModel,
14                     new JsonMediaTypeFormatter(),
15                     "application/json"
16                     )
17             };
18         }
19 
20         public override void OnException(HttpActionExecutedContext actionExecutedContext)
21         {
22             var code = -1;
23             var message = "請求失敗!";
24 
25             if (actionExecutedContext.Exception is UserDisplayException)
26             {
27                 message = actionExecutedContext.Exception.Message;
28             }
29             if (actionExecutedContext.Exception is UserLoginException)
30             {
31                 code = -2;
32                 message = actionExecutedContext.Exception.Message;
33             }
34 
35             if (actionExecutedContext.Response == null)
36             {
37                 actionExecutedContext.Response = GetResponse(code, message);
38             }
39 
40             //記錄錯誤日志
41             LogUtils.ErrorLog(SecurityHelper.GetUserIp(), actionExecutedContext.Exception);
42 
43             base.OnException(actionExecutedContext);
44         }
45     }      

1.部落客單獨把登入異常通過特定code值傳回,友善用戶端分辨處理

2.GetResponse()方法主要是填充json資料到response

到這一步,還是沒辦法寫日志的,為什麼呢???因為我們的ExceptionFilter還沒有注冊,在App_Start檔案夾下WebApiConfig.cs檔案Register方法添加下句代碼:

config.Filters.Add(new ExceptionFilter());      

OK,至此我們的錯誤日志記錄就算搞定了。隻需要在代碼中抛出手動捕獲的異常,或者意料之外未捕獲的異常都會記錄在錯誤日志中,并友好回報到用戶端。

當然,log4net的配置資訊也是需要注冊的,千萬别忘了在Global.asax中Application_Start方法加上這樣一句代碼

log4net.Config.XmlConfigurator.Configure();      

部落客自知水準有限,如有不對的地方或各位有更好的解決方案,請随意指點,必當虛心請假,希望共同進步....