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