Log2Net组件代码详解(附开源代码)

上一篇,我们介绍了Log2Net的需求和整体框架,我们接下来介绍我们是如何用代码实现Log2Net组件的功能的。

一、整体介绍

  Log2Net组件本身是一个Dll,供其他系统调用。

  本部分由以下几部分组成:

  1. 日志平台实体定义;
  2. 工具方法定义,包括ComUtil(例如缓存帮助类、序列化帮助类、消息队列帮助类等)和DBUtil(例如Sql server帮助类、Oracle帮助类、MySql帮助类、EF帮助类等);
  3. 日志信息获取类(例如如获取客户端、服务器端信息,写日志数据到消息队列等);
  4. .NetCore中间件定义(例如HttpContext中间件、错误消息处理中间件等);
  5. Config配置类(包括Log2NetConfigurationSectionHandler类、消息队列管理类等);
  6. 日志追加器类(FileAppender、DirectDBAppender、Queue2DBAppender、MQ2DBAppender等);
  7. 外部接口LogApi类(例如组件注册类、写日志类等);

  使用的第三方类库有RabbitMQ访问类库RabbitMQ.Client、InfluxDB访问类库InfluxData.Net、缓存组件CacheManager、对象映射组件AutoMapper、缓存工具Microsoft.Extensions.Caching、Microsoft.AspNetCore.Session等。使用NuGet工具下载安装这些类库,会自动检测和匹配当前.NET版本并安装其他依赖。请尽量不要手动下载类库安装,可能会出现各种各样的不兼容、缺少依赖库的情况。

  本组件使用VS2017开发,为类库项目,支持.net4.5~netCore2.2(此项目初始使用VS2017开发,于2019-10为支持.NetCore3.0改用了VS2019。若您未安装VS2019,将依赖项中的.NetCoreApp 3.0(或项目文件中的netcoreapp3.0)移除即可使用VS2017打开)。若您把源码下载下来,而您的电脑上缺少某个.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

泛型ModelDataTable互转操作类

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 #endif
View 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_StartSession_End事件访客统计的实现思路较为清晰,本组件提供了两种方案:使用Application对象实现、使用缓存实现。具体采用哪种方案由简单工厂决定,默认采用缓存方案。

  对.NetCore平台,不存在Session_StartSession_End事件事件,需要借助于HttpContext中间件来实现。在HttpContext中,保存了所有的SessionID,Session过期,则视为该SessionID离线。据此可以统计出在线人数和历史访客。

4.3 公共类LogCom类 

  本类中定义一些本组件内部使用的公共类,主要是写文件的类、日志实体封装类,实现非常简单,类图如下:

五、日志追加器Appender类库  

  日志追加器用于将封装后的日志实体写到媒介中。根据追加方式的不同,实现方案也不同

  日志追加方式有写到文件、ADO方式写到数据库、通过队列写到数据库、通过消息队列写到数据库四种。相应的有FileAppender、DirectDBAppender、Queue2DBAppender、MQ2DBAppender四种追加器。这四追加器都实现了公共的追加器BaseAppender类。类图如下:

                     

  公共追加器BaseAppender为抽象类中,定义了两个抽象的WriteLog方法,分别用来写用户操作日志和系统运行日志。

  BaseAppender类中还定义了写日志WriteLogAndHandFail方法WriteLogAgain方法,两者的区别在于前者在失败时要写备份日志,参数为集合类型,在初次将日志写到媒介中使用;后者在失败时不进行其他处理,参数为单一实体,在读备份日志到媒介中使用。 

  FileAppender、DirectDBAppender、Queue2DBAppender、MQ2DBAppender这四种追加器实现自己的WriteLog方法。Queue2DBAppender是通过线程安全的ConcurrentQueue队列将数据写到数据,MQ2DBAppender是通过消息队列写数据到数据库,这两者都是通过一个缓冲Buffer写数据到数据库,继承自Buffer2DBAppender抽象类,需要实现各自的数据生产和数据消费的方法。而Buffer2DBAppender类由继承DirectDBAppender的写数据到数据库的方法,还开启了数据消费线程。Queue2DBAppender子类中,通过Enqueue 和TryDequeue方法即可实现数据的生产和消费,而MQ2DBAppender类中的数据生产消费任务较为复杂:需要调用RabbitMQManager的Send和Receive方法来生产和消费数据。

  在写数据库中时,一方面写到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:系统异常时,记录异常日志;
  • IncreaseOnlineVisitNumSession Start时,在线人数和访客人数加1;
  • ReduceOnlineNumSession end时在线人数减1

  以上的生命周期事件中,有些仅在.net中可以使用,.netCore中不存在实现类似的功能,需要使用netCore中间来实现。AddLog2netService和AddLog2netConfigure分别用来注册Log2net服务和Log2net配置。

  本类中定义了4日志的方法:

  • WriteLog方法重载(2个):封装日志实体,调用日志追加器的方法将日志写到媒介中,分别对业务操作和监控数据进行写。
  • WriteMsgToDebugFile:写调试日志写到文件中,可通过bWriteInfoToDebugFile配置是否开启。
  • WriteInfoToFile:将将日记写到本地文件中,记录一些重要但又不必写入log日志媒介的信息。

  LogTraceEdm为操作轨迹类业务实体,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/.netCore2.2/.netCore3.0平台,其他的平台由于版本较旧,功能性能不太完善,使用较少,故不予支持。

  实现多平台建议使用VS2017,将项目配置 .csporj 中的代码<TargetFramework>net45</TargetFramework> 改为 <TargetFrameworks>net45;net451;net452;net46;net461;net462;net47;net471;net472;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;netcoreapp3.0</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来标记某人的离线。

posted on 2019-06-23 21:19  三河散人  阅读(823)  评论(0编辑  收藏  举报