好吧,還是那個社群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();
部落客自知水準有限,如有不對的地方或各位有更好的解決方案,請随意指點,必當虛心請假,希望共同進步....