上一篇,咱們介紹了Log2Net的需求和總體框架,咱們接下來介紹咱們是如何用代碼實現Log2Net組件的功能的。git
Log2Net組件自己是一個Dll,供其餘系統調用。github
本部分由如下幾部分組成:redis
使用的第三方類庫有RabbitMQ訪問類庫RabbitMQ.Client、InfluxDB訪問類庫InfluxData.Net、緩存組件CacheManager、對象映射組件AutoMapper、緩存工具Microsoft.Extensions.Caching、Microsoft.AspNetCore.Session等。使用NuGet工具下載安裝這些類庫,會自動檢測和匹配當前.NET版本並安裝其餘依賴。請儘可能不要手動下載類庫安裝,可能會出現各類各樣的不兼容、缺乏依賴庫的狀況。sql
本組件使用VS2017開發,爲類庫項目,支持.net4.5~netCore2.1。若您把源碼下載下來,而您的電腦上某個某個.Net版本,請在csproj文件中的TargetFrameworks中移除該net版本。數據庫
爲了測試該組件,分別添加了一個.NET4.5的MVC項目和.netCore2.0的MVC項目。項目文件圖以下圖所示:json
本項目代碼已開源,地址爲 https://github.com/yuchen1030/Log2Net ,您能夠參照代碼理解下述的設計。瀏覽器
模型實體包括定義在ModelsInDB.cs中數據庫中使用的模型、定義在ModelsUI.cs中外部接口使用的模型、定義在ModelsInCode.cs中本類庫代碼中使用的模型。緩存
系統中的操做軌跡數據的數據庫實體爲Log_OperateTrace,監控數據數據庫實體爲Log_SystemMonitor。代碼中以這兩個實體爲核心定義了其餘數據實體,具體參見代碼。 服務器
本部分包括公共工具ComUtil類和DBUtil類。session
該類庫(Util類庫)中,封裝了了一些公共的方法和類,如下表所示:
文件名 |
用途描述 |
AppConfig |
配置文件讀寫類 |
AutoMapperHelper |
對象映射幫助類 |
CacheHelper.cs |
緩存操做類 |
DtModelConvert.cs |
泛型Model和DataTable互轉操做類 |
LambdaToSqlHelper |
Lambda表達式轉Sql幫助類 |
RabbitMQHelper.cs |
RabbitMQ消息隊列幫助類 |
SerializerHelper.cs |
序列化反序列化幫助類 |
StringEnum.cs |
字符串枚舉類 |
XmlSerializeHelper.cs |
Xml和實體轉換類 |
這些類是通用的方法封裝,與具體業務邏輯無關,其餘系統能夠借鑑使用。
這些類是用來訪問各類數據庫的方法的封裝。包括對Sql Server、Oracle、MySql、InfluxDB等4種數據庫的訪問。若您須要添加對其餘數據(如Access、SQLite、PostgreSQL等)的支持,請在此部分下添加。
對經常使用的數據庫,本代碼中使用了兩種方式進行訪問:ADO.net方式和EF方式,若是您須要使用NHibernate/SqlSugar/Dapper等其餘方式,也請在該部分下添加。
該部分是使用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 }
數據庫訪問基礎類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 }
該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 }
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 }
基礎類中的其餘方法也是相似的套路,在此再也不贅述,具體請參見源碼。
在定義了接口和基礎方法以後,各個子類就能夠在此基礎上繼承和實現它們了,本代碼中的子類是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 #endif
若您還須要支持其餘數據庫類型,請繼承和實現 AdoNetBase<T>, IAdoNetBase<T> 便可。
上面咱們定義了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 }
而後,分別定義ADO.Net方法訪問數據的基類AdoNetBaseDal和EF方法訪問數據庫的基類EFBaseDal:
最後,根據上一步中的基類,實現Log_OperateTrace和Log_SystemMonitor的數據訪問子類,以下圖:
ADO.Net方式中,基類中已指明瞭數據庫鏈接對象,Dal中只須要調用相關方法便可。EF方式中,前文寫的代碼不多,但欠債老是要還的,這裏須要額外定義繼承自DbContext的Log_OperateTraceContext和Log_SystemMonitorContext來指定數據庫上下文。
上文中,介紹了數據庫又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 }
另外,還有數據庫功能公共類ComDBFun和InfluxDB訪問類InfluxDBHelper的介紹略。
至此,數據庫訪問幫助類介紹完畢,詳情請參閱DBUtil部分代碼。
本部分定義了日誌組件使用的基礎方法,如客戶端服務器信息ClientServerInfo類、在線人數訪客人數統計VisitOnlineCount 類、日誌組件公共類LogCom.cs。
該類庫用於收集客戶端和服務器端的信息,包括客戶端信息子類ClientInfo和服務器端信息子類ServerInfo。
ClientInfo類用來獲取客戶端的ip地址、主機名、Mac地址、瀏覽器信息等。
ServerInfo類用來獲取服務器端的ip地址、主機名、操做系統、CLR版本、服務器運行時間、可用硬盤空間、CPU使用率、內存使用率等信息。
本類中定義了在線人數和訪客統計抽象類IVisitCount類,具體的類要實現該類中的抽象方法。
對.net平臺,存在Session_Start和Session_End事件,訪客統計的實現思路較爲清晰,本組件提供了兩種方案:使用Application對象實現、使用緩存實現。具體採用哪一種方案由簡單工廠決定,默認採用緩存方案。
對.NetCore平臺,不存在Session_Start和Session_End事件事件,須要藉助於HttpContext中間件來實現。在HttpContext中,保存了全部的SessionID,若Session過時,則視爲該SessionID離線。據此就能夠統計出在線人數和歷史訪客。
本類中定義一些本組件內部使用的公共類,主要是寫文件的類、日誌實體封裝類,實現很是簡單,類圖以下:
日誌追加器用於將封裝後的日誌實體寫到媒介中。根據追加方式的不一樣,實現方案也不一樣。
日誌追加方式有寫到文件、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中沒有Application_Error事件來捕捉全局異常,沒有HttpContext.Current來保存當前請求的信息,須要咱們自定義中間件來實現。
在這裏定義了異常處理中間件,在捕捉到異常時,將異常日誌進行記錄。
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 }
在這裏,定義了請求上下文的中間件,記錄了當前求請求的上下文,模擬了當前上下文的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 }
在本類中,調用其餘類的方法,造成供其餘業務系統調用的方法。包含如下內容:日誌組件註冊、寫日誌等。各業務系統不須要關心這些方法的具體實現,只須要封裝業務實體,調用寫日誌的方法便可。LogApi類會調用其餘的類,如等來實現日誌記錄的功能。類圖以下:
以上的生命週期事件中,有些僅在.net中可使用,.netCore中不存在,要實現相似的功能,就須要使用netCore中間件來實現。AddLog2netService和AddLog2netConfigure分別用來註冊Log2net服務和Log2net配置。
本類中定義了4個寫日誌的方法:
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來標記某人的離線。