Log2Net元件程式碼詳解(附開原始碼)
上一篇,我們介紹了Log2Net的需求和整體框架,我們接下來介紹我們是如何用程式碼實現Log2Net元件的功能的。
一、整體介紹
Log2Net元件本身是一個Dll,供其他系統呼叫。
本部分由以下幾部分組成:
- 日誌平臺實體定義;
- 工具方法定義,包括ComUtil(例如快取幫助類、序列化幫助類、訊息佇列幫助類等)和DBUtil(例如Sql server幫助類、Oracle幫助類、MySql幫助類、EF幫助類等);
- 日誌資訊獲取類(例如如獲取客戶端、伺服器端資訊,寫日誌資料到訊息佇列等);
- .NetCore中介軟體定義(例如HttpContext中介軟體、錯誤訊息處理中介軟體等);
- Config配置類(包括Log2NetConfigurationSectionHandler類、訊息佇列管理類等);
- 日誌追加器類(FileAppender、DirectDBAppender、MQ2DBAppender等);
- 外部介面LogApi類(例如元件註冊類、寫日誌類等);
使用的第三方類庫有RabbitMQ訪問類庫RabbitMQ.Client、InfluxDB訪問類庫InfluxData.Net、快取元件CacheManager、物件對映元件AutoMapper、快取工具Microsoft.Extensions.Caching、Microsoft.AspNetCore.Session等。使用NuGet工具下載安裝這些類庫,會自動檢測和匹配當前.NET版本並安裝其他依賴。請儘量不要手動下載類庫安裝,可能會出現各種各樣的不相容、缺少依賴庫的情況。
本元件使用VS2017開發,為類庫專案,支援.net4.5~netCore2.1。若您把原始碼下載下來,而您的電腦上某個某個.Net版本,請在csproj檔案中的TargetFrameworks中移除該net版本。
為了測試該元件,分別添加了一個.NET4.5的MVC專案和.netCore2.0的MVC專案。專案檔案圖如下圖所示:
本專案程式碼已開源,地址為 https://github.com/yuchen1030/Log2Net ,您可以參照程式碼理解下述的設計。
二、模型實體Models類庫
模型實體包括定義在ModelsInDB.cs中資料庫中使用的模型、定義在ModelsUI.cs中外部介面使用的模型、定義在ModelsInCode.cs中本類庫程式碼中使用的模型。
系統中的操作軌跡資料的資料庫實體為Log_OperateTrace,監控資料資料庫實體為Log_SystemMonitor。程式碼中以這兩個實體為核心定義了其他資料實體,具體參見程式碼。
三、工具方法Util定義
本部分包括公共工具ComUtil類和DBUtil類。
3.1 ComUtil類
該類庫(Util類庫)中,封裝了了一些公共的方法和類,如下表所示:
檔名 |
用途描述 |
AppConfig |
配置檔案讀寫類 |
AutoMapperHelper |
物件對映幫助類 |
CacheHelper.cs |
快取操作類 |
DtModelConvert.cs |
泛型Model和DataTable互轉操作類 |
LambdaToSqlHelper |
Lambda表示式轉Sql幫助類 |
RabbitMQHelper.cs |
RabbitMQ訊息佇列幫助類 |
SerializerHelper.cs |
序列化反序列化幫助類 |
StringEnum.cs |
字串列舉類 |
XmlSerializeHelper.cs |
Xml和實體轉換類 |
這些類是通用的方法封裝,與具體業務邏輯無關,其他系統可以借鑑使用。
3.2 DBUtil類
這些類是用來訪問各種資料庫的方法的封裝。包括對Sql Server、Oracle、MySql、InfluxDB等4種資料庫的訪問。若您需要新增對其他資料(如Access、SQLite、PostgreSQL等)的支援,請在此部分下新增。
對常用的資料庫,本程式碼中使用了兩種方式進行訪問:ADO.net方式和EF方式,如果您需要使用NHibernate/SqlSugar/Dapper等其他方式,也請在該部分下新增。
3.2.1 AdoNet方式訪問資料庫
該部分是使用ADO.Net方法直接訪問資料庫,因為要支援SqlServer,Oracle,MySql等多種資料庫,支援多個數據庫實體,它們需要遵循相同的介面契約,有一些共同的實現方法,因此定義了泛型介面類和泛型基礎類。類圖如下所示:
在泛型介面IAdoNetBase中,定義了新增和獲取資料的方法,如下所示:
1 internal interface IAdoNetBase<T> where T : class 2 { 3 ExeResEdm Add(string tableName, T model, params string[] skipCols); 4 ExeResEdm GetListByPage(string tableName, PageSerach<T> para); 5 }View Code
資料庫訪問基礎類AdoNetBase為抽象類,定義了各種資料庫共用的一些基礎方法,如下圖所示:
在實現這些公共方法的時候,各種資料庫的實現方法不同,因此需要定義抽象方法,子類需要實現它。
例如介面的public ExeResEdm Add(string tableName, T model, params string[] skipCols)方法需要呼叫私有方法ExecuteNonQuery,而該私有方法的定義如下:
1 ExeResEdm ExecuteNonQuery(string cmdText, params DbParameter[] parameters) 2 { 3 ExeResEdm dBResEdm = SqlCMD(cmdText, cmd => cmd.ExecuteNonQuery(), parameters); 4 if (dBResEdm.ErrCode == 0) 5 { 6 dBResEdm.ExeNum = Convert.ToInt32(dBResEdm.ExeModel); 7 } 8 return dBResEdm; 9 }View Code
該ExecuteNonQuery方法中要呼叫SqlCMD方法,而各種資料庫中SqlCMD方法方法實現不同,因此需要SqlCMD方法為抽象方法,各子類需要各自實現之。以下分別列出SqlServer和MySql中SqlCMD方法的實現:
1 protected override ExeResEdm SqlCMD(string sql, Func<DbCommand, object> fun, params DbParameter[] pms) 2 { 3 ExeResEdm dBResEdm = new ExeResEdm(); 4 try 5 { 6 pms = ParameterPrepare(pms); 7 using (SqlConnection con = new SqlConnection(connstr)) 8 { 9 using (SqlCommand cmd = new SqlCommand(sql, con)) 10 { 11 con.Open(); 12 if (pms != null && pms.Length > 0) 13 { 14 cmd.Parameters.AddRange((pms)); 15 } 16 var res = fun(cmd); 17 dBResEdm.ExeModel = res; 18 return dBResEdm; 19 } 20 } 21 } 22 catch (Exception ex) 23 { 24 dBResEdm.Module = "SqlCMD方法"; 25 dBResEdm.ExBody = ex; 26 dBResEdm.ErrCode = 1; 27 return dBResEdm; 28 } 29 }View Code
1 protected override ExeResEdm SqlCMD(string sql, Func<DbCommand, object> fun, params DbParameter[] pms) 2 { 3 ExeResEdm dBResEdm = new ExeResEdm(); 4 try 5 { 6 pms = ParameterPrepare(pms); 7 using (MySqlConnection con = new MySqlConnection(connstr)) 8 { 9 using (MySqlCommand cmd = new MySqlCommand(sql, con)) 10 { 11 con.Open(); 12 if (pms != null && pms.Length > 0) 13 { 14 cmd.Parameters.AddRange((pms)); 15 } 16 var res = fun(cmd); 17 dBResEdm.ExeModel = res; 18 return dBResEdm; 19 } 20 } 21 } 22 catch (Exception ex) 23 { 24 dBResEdm.Module = "SqlCMD方法"; 25 dBResEdm.ExBody = ex; 26 dBResEdm.ErrCode = 1; 27 return dBResEdm; 28 } 29 }View Code
基礎類中的其他方法也是類似的套路,在此不再贅述,具體請參見原始碼。
在定義了介面和基礎方法之後,各個子類就可以在此基礎上繼承和實現它們了,本程式碼中的子類是SqlServerHelper,OracleHelperBase,MySqlHelper三個,分別實現對SqlServer,oracle,MySql資料庫的訪問。這些子類中的方法就是對基類方法的重寫,例如SqlServerHelper定義如下:
對oracle資料庫,建議使用Oracle.ManagedDataAccess.Client實現的oracle 資料庫訪問類OracleHelper(無需安裝客戶端),無32位/64位之分,使用方便,效能好。但該類庫僅支援Oracle10g及以上,因此又使用System.Data.OracleClient實現的oracle 資料庫訪問類OracleHelperMS。這兩個類的程式碼可以是一模一樣的,只是引用的類庫不同(Oracle.ManagedDataAccess.Client和System.Data.OracleClient)。這兩個類可以合併為一個,只需要新增如下程式碼:
1 //#define MS_OracleClient // 是採用微軟oracle類庫還是oracle自家的類庫 2 3 #if MS_OracleClient 4 using System.Data.OracleClient; 5 #else 6 using Oracle.ManagedDataAccess.Client; 7 #endifView Code
若您還需要支援其他資料庫型別,請繼承和實現 AdoNetBase<T>, IAdoNetBase<T> 即可。
3.2.2 資料訪問層Dal
上面我們定義了ADO.Net訪問資料的方法,EF方法只需引用類庫即可。工具已備好,我們接下來就可以使用ADO.Net方法或EF方法訪問具體的資料庫表了。類圖如下圖所示:
首先,我們定義一個泛型抽象類DBAccessDal(也可以定義為介面),裡面定義了需要實現的獲取資料方法和新增資料的方法:
1 internal abstract class DBAccessDal<T> where T : class 2 { 3 internal abstract ExeResEdm GetAll(PageSerach<T> para); 4 5 internal abstract ExeResEdm Add(AddDBPara<T> dBPara); 6 7 }View Code
然後,分別定義ADO.Net方法訪問資料的基類AdoNetBaseDal和EF方法訪問資料庫的基類EFBaseDal:
最後,根據上一步中的基類,實現Log_OperateTrace和Log_SystemMonitor的資料訪問子類,如下圖:
ADO.Net方式中,基類中已指明瞭資料庫連線物件,Dal中只需要呼叫相關方法即可。EF方式中,前文寫的程式碼很少,但欠債總是要還的,這裡需要額外定義繼承自DbContext的Log_OperateTraceContext和Log_SystemMonitorContext來指定資料庫上下文。
3.2.3 資料庫訪問方式工廠
上文中,介紹了資料庫又ADO.Net方式和EF訪問方式,我們可以在配置檔案中配置使用ADO.Net方式或EF方式,這是通過工廠模式實現的,類圖如下:
例如Log_OperateTraceDBAccessFac定義如下:
1 internal class Log_OperateTraceDBAccessFac : DBAccessFac<Log_OperateTrace> 2 { 3 protected override DBAccessDal<Log_OperateTrace> GetDalByDBAccessType(DBAccessType dbAccessType) 4 { 5 if (dbAccessType == DBAccessType.EF) 6 { 7 Log_OperateTraceEFDal log_OperateTraceDal = new Log_OperateTraceEFDal(new Log_OperateTraceContext()); 8 return log_OperateTraceDal; 9 } 10 else if (dbAccessType == DBAccessType.NH) 11 { 12 throw new Exception("Not define dal methods when DBAccessType = NH"); 13 } 14 else 15 { 16 return new Log_OperateTraceAdoDal(); 17 } 18 19 } 20 }View Code
另外,還有資料庫功能公共類ComDBFun和InfluxDB訪問類InfluxDBHelper的介紹略。
至此,資料庫訪問幫助類介紹完畢,詳情請參閱DBUtil部分程式碼。
四、日誌資訊獲取類
本部分定義了日誌元件使用的基礎方法,如客戶端伺服器資訊ClientServerInfo類、線上人數訪客人數統計VisitOnlineCount 類、日誌元件公共類LogCom.cs。
4.1 ClientServerInfo類
該類庫用於收集客戶端和伺服器端的資訊,包括客戶端資訊子類ClientInfo和伺服器端資訊子類ServerInfo。
ClientInfo類用來獲取客戶端的ip地址、主機名、Mac地址、瀏覽器資訊等。
ServerInfo類用來獲取伺服器端的ip地址、主機名、作業系統、CLR版本、伺服器執行時間、可用硬碟空間、CPU使用率、記憶體使用率等資訊。
4.2 訪客人數統計類VisitOnlineCount類
本類中定義了線上人數和訪客統計抽象類IVisitCount類,具體的類要實現該類中的抽象方法。
對.net平臺,存在Session_Start和Session_End事件,訪客統計的實現思路較為清晰,本元件提供了兩種方案:使用Application物件實現、使用快取實現。具體採用哪種方案由簡單工廠決定,預設採用快取方案。
對.NetCore平臺,不存在Session_Start和Session_End事件事件,需要藉助於HttpContext中介軟體來實現。在HttpContext中,儲存了所有的SessionID,若Session過期,則視為該SessionID離線。據此就可以統計出線上人數和歷史訪客。
4.3 公共類LogCom類
本類中定義一些本元件內部使用的公共類,主要是寫檔案的類、日誌實體封裝類,實現非常簡單,類圖如下:
五、日誌追加器Appender類庫
日誌追加器用於將封裝後的日誌實體寫到媒介中。根據追加方式的不同,實現方案也不同。
日誌追加方式有寫到檔案、ADO方式寫到資料庫、通過訊息佇列寫到資料庫三種。相應的有FileAppender、DirectDBAppender、MQ2DBAppender三種追加器。這三種追加器都實現了公共的追加器BaseAppender類。類圖如下:
公共追加器BaseAppender為抽象類中,定義了兩個抽象的WriteLog方法,分別用來寫使用者操作日誌和系統執行日誌。
BaseAppender類中還定義了寫日誌的WriteLogAndHandFail方法和WriteLogAgain方法,兩者的區別在於前者在失敗時要寫備份日誌,引數為集合型別,在初次將日誌寫到媒介中使用;後者在失敗時不進行其他處理,引數為單一實體,在讀備份日誌到媒介中使用。
FileAppender、DirectDBAppender、MQ2DBAppender這三種追加器實現自己的WriteLog方法。MQ2DBAppender是通過訊息佇列寫到資料庫,除繼承自DirectDBAppender的方法外,還需要開啟訊息佇列消費執行緒,將訊息佇列中的日誌寫到資料庫中:開啟StartWriteTraceDataService執行緒啟動寫trace日誌資料服務,開啟StartWriteMonitorDataService執行緒啟動寫monitor日誌資料服務。在這兩個服務中,都是啟動佇列服務,檢查佇列,若有訊息則寫資料到資料庫。只不過是一個佇列用於寫軌跡日誌,一個佇列用於寫監控日誌。
在寫資料庫中時,一方面寫到SQL資料庫中,便於讀寫分離的實現,另一方面寫到時序資料庫InfluxDB中,便於以後使用Grafana、ELK等工具進行更加靈活優雅的監控。
使用者可以通過配置來決定使用哪一種追加器,程式碼中通過追加器工廠類AppenderFac,得到相應的追加器工廠例項,呼叫該追加器的方法進行日誌的記錄。
六、.NetCore中介軟體DNCMiddleware類庫
日誌追加器用於將封裝後的日誌實體寫到媒介中。根據追加方式的不同,實現方案也不同。
.NetCore中沒有Application_Error事件來捕捉全域性異常,沒有HttpContext.Current來儲存當前請求的資訊,需要我們自定義中介軟體來實現。
61 異常處理中介軟體ErrorHandlingMiddleware
在這裡定義了異常處理中介軟體,在捕捉到異常時,將異常日誌進行記錄。
1 internal class ErrorHandlingMiddleware 2 { 3 private readonly RequestDelegate next; 4 5 public ErrorHandlingMiddleware(RequestDelegate next) 6 { 7 this.next = next; 8 } 9 10 public async Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) 11 { 12 try 13 { 14 await next(context); 15 } 16 catch (Exception ex) 17 { 18 var statusCode = context.Response.StatusCode; 19 if (ex is ArgumentException) 20 { 21 statusCode = 200; 22 } 23 await HandleExceptionAsync(context, statusCode, ex.Message); 24 } 25 finally 26 { 27 var statusCode = context.Response.StatusCode; 28 var msg = ""; 29 if (statusCode == 401) 30 { 31 msg = "未授權"; 32 } 33 else if (statusCode == 404) 34 { 35 msg = "未找到服務"; 36 } 37 else if (statusCode == 502) 38 { 39 msg = "請求錯誤"; 40 } 41 else if (statusCode != 200 && statusCode != 302) 42 { 43 msg = "未知錯誤" + statusCode; 44 } 45 if (!string.IsNullOrWhiteSpace(msg)) 46 { 47 await HandleExceptionAsync(context, statusCode, msg); 48 } 49 } 50 } 51 52 private static Task HandleExceptionAsync(Microsoft.AspNetCore.Http.HttpContext context, int statusCode, string msg) 53 { 54 var data = new { code = statusCode.ToString(), is_success = false, msg = msg }; 55 var result = JsonConvert.SerializeObject(new { data = data }); 56 57 Log_OperateTraceBllEdm exLog = new Log_OperateTraceBllEdm() 58 { 59 Detail = result, 60 LogType = LogType.異常, 61 Remark = "異常時間" + DateTime.Now, 62 TabOrModu = "異常模組", 63 }; 64 LogApi.WriteLog( LogLevel.Error,exLog); 65 66 context.Response.ContentType = "application/json;charset=utf-8"; 67 return context.Response.WriteAsync(result); 68 } 69 } 70 71 72 public static class ErrorHandlingExtensions 73 { 74 public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder) 75 { 76 return builder.UseMiddleware<ErrorHandlingMiddleware>(); 77 } 78 }View Code
6.2 請求上下文中介軟體HttpContext
在這裡,定義了請求上下文的中介軟體,記錄了當前求請求的上下文,模擬了當前上下文的Session資訊,將所有的SessionId儲存起來,將過期的SessionId移除,來實現線上人數統計和歷史訪客統計。
1 internal static class HttpContext 2 { 3 public class SessionEdm 4 { 5 public string Key { get; set; } 6 public string Val { get; set; } 7 public DateTime ExpiresAtTime { get; set; } 8 } 9 10 11 public static Microsoft.AspNetCore.Http.HttpContext Current => _accessor.HttpContext; 12 13 static ConcurrentDictionary<string, SessionEdm> sessionMaps = new ConcurrentDictionary<string, SessionEdm>(); 14 15 static double dncSessionMins = AppConfig.GetDncSessionTimeoutMins(); 16 17 private static IHttpContextAccessor _accessor; 18 internal static void Configure(IHttpContextAccessor accessor) 19 { 20 _accessor = accessor; 21 } 22 23 public static VOEdm GetOnlineVisitNum(int preVisitNum) 24 { 25 if (_accessor.HttpContext != null) 26 { 27 var curSession = _accessor.HttpContext.Session; 28 SessionEdm sessionEdm = new SessionEdm() { Key = curSession.Id, Val = "1", ExpiresAtTime = DateTime.Now.AddMinutes(dncSessionMins) }; 29 sessionMaps.TryAdd(curSession.Id, sessionEdm); 30 } 31 int visitorsNum = sessionMaps.Count; 32 VOEdm vOEdm = new VOEdm() { VisitNum = preVisitNum + visitorsNum }; 33 //將過期session的值變為0,未過期的session的數量為線上人數 34 var keys = sessionMaps.Keys.ToArray(); 35 for (int i = 0; i < sessionMaps.Count; i++) 36 { 37 var cur = sessionMaps[keys[i]]; 38 if (cur.Val == "1" && cur.ExpiresAtTime <= DateTime.Now) //已過期 39 { 40 cur.Val = "0"; 41 } 42 } 43 var onlineNums = sessionMaps.Where(a => a.Value.Val == "1").Count(); 44 vOEdm.OnlineNum = onlineNums; 45 return vOEdm; 46 } 47 48 } 49 50 public static class StaticHttpContextExtensions 51 { 52 public static void AddHttpContextAccessor(this IServiceCollection services) 53 { 54 services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); 55 } 56 57 public static IApplicationBuilder UseStaticHttpContext(this IApplicationBuilder app) 58 { 59 var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(); 60 HttpContext.Configure(httpContextAccessor); 61 return app; 62 } 63 }View Code
七、外部介面類LogApi類
在本類中,呼叫其他類的方法,形成供其他業務系統呼叫的方法。包含以下內容:日誌元件註冊、寫日誌等。各業務系統不需要關心這些方法的具體實現,只需要封裝業務實體,呼叫寫日誌的方法即可。LogApi類會呼叫其他的類,如等來實現日誌記錄的功能。類圖如下:
- RegisterLogInitMsg:註冊日誌元件到本系統,為日誌元件準備基礎資訊:伺服器IP、伺服器主機名,系統名稱等;使用EF自動建立資料;並呼叫WriteServerStartupLog方法寫啟動日誌,呼叫WriteMonitorLogThread方法寫定時監控日誌。
- GetLogWebApplicationsName:從配置檔案中獲取使用者自定義的系統名稱。
- 這裡還包含網站生命週期事件中的日誌記錄,如下:
- WriteServerStartupLog:伺服器啟動時,獲取作業系統,.NET CLR版本;
- WriteFirstVisitLog:網站被初次訪問,記錄記錄IIS版本;
- WriteServerStopLog:伺服器停止時,獲取已執行時間;
- WriteServerStartupLog:系統異常時,記錄異常日誌;
- IncreaseOnlineVisitNum:Session Start時,線上人數和訪客人數加1;
- ReduceOnlineNum:Session end時,線上人數減1。
以上的生命週期事件中,有些僅在.net中可以使用,.netCore中不存在,要實現類似的功能,就需要使用netCore中介軟體來實現。AddLog2netService和AddLog2netConfigure分別用來註冊Log2net服務和Log2net配置。
本類中定義了4個寫日誌的方法:
- WriteLog方法過載(2個):封裝日誌實體,呼叫日誌追加器的方法將日誌寫到媒介中,分別對業務操作和監控資料進行寫。
- WriteMsgToDebugFile:寫除錯日誌寫到檔案中,可通過bWriteInfoToDebugFile配置是否開啟。
- WriteInfoToFile:將將日記寫到本地檔案中,記錄一些重要但又不必寫入log日誌媒介的資訊。
Log_OperateTraceBllEdm為操作軌跡類業務實體,LogMonitorEdm為監控資訊實體,各業務系統將資訊封裝進這兩個實體,然後呼叫WriteLog方法,就能將日誌資料寫到相應媒介中。若寫日誌出現異常,將則該訊息以Json格式備份到本地.log檔案中,並在以後自動將備份寫到相應媒介中。
八、多平臺的設計和實現
日誌元件作為基礎的元件,供不特定的系統使用,所以需要支援.net4.5/.net4.5.1/.net4.5.2/.net4.6/.net4.6.1/.net4.6.2.net4.7/.net4.7.1/.net4.7.2等平臺,支援 .netCore2.0/.netCore2.1平臺,其他的平臺由於版本較舊,功能效能不太完善,使用較少,故不予支援。
實現多平臺建議使用VS2017,將專案配置 .csporj 中的程式碼<TargetFramework>net45</TargetFramework> 改為 <TargetFrameworks>net45;net451;net452;net46;net461;net462;net47;net471;net472;netcoreapp2.0;netcoreapp2.1</TargetFrameworks> ,即可將單目標框架變為多目標框架。然後在專案配置中的ItemGroup 節點中新增 Condition條件,來指明這些引用所適用的框架平臺,具體情況請參見專案配置.csporj 中檔案。最後在專案程式碼中使用 #if #else #endif條件編譯指明各個平臺下適用的編碼。
本元件中主要涉及生命週期事件的多平臺實現、快取的多平臺實現、線上人數的多平臺實現等。
對生命週期事件,.net平臺中有 Application Started、Application Stop、Application Error、Session_Start、Session_End、Application_BeginRequest等事件,而在.netCore平臺中僅有Application Started、Application Stop事件,其他事件需要通過Middleware中介軟體來實現。
對快取,本系統使用http快取和CacheManager快取。http快取中,分別使用HttpRuntime.Cache快取和Microsoft.Extensions.Caching.Memory快取;對CacheManager快取,.net平臺中支援記憶體快取、Memcached快取、redis快取三種,.netCore平臺中僅支援記憶體快取、redis快取兩種。
對線上人數,.net平臺中可以通過Application/快取結合Session_Start、Session_End事件來實現,但在.netCore平臺中,該實現較為麻煩,需要開啟Session、自定義HttpContext中介軟體等,利用SessionId列表來標記歷史訪客,利用Session過期時間來移除過期的SessionId來標記某人的離