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项目。项目文件图如下图所示:

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

  本项目代码已开源,地址为 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等多种数据库,支持多个数据库实体,它们需要遵循相同的接口契约,有一些共同的实现方法,因此定义了泛型接口类和泛型基础类。类图如下所示:

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

  在泛型接口IAdoNetBase中,定义了添加和获取数据的方法,如下所示:

     internal interface IAdoNetBase<T> where T : class
{
ExeResEdm Add(string tableName, T model, params string[] skipCols);
ExeResEdm GetListByPage(string tableName, PageSerach<T> para);
}

  数据库访问基础类AdoNetBase为抽象类,定义了各种数据库共用的一些基础方法,如下图所示:

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

  在实现这些公共方法的时候,各种数据库的实现方法不同,因此需要定义抽象方法,子类需要实现它。

  例如接口的public ExeResEdm Add(string tableName, T model, params string[] skipCols)方法需要调用私有方法ExecuteNonQuery,而该私有方法的定义如下:

         ExeResEdm ExecuteNonQuery(string cmdText, params DbParameter[] parameters)
{
ExeResEdm dBResEdm = SqlCMD(cmdText, cmd => cmd.ExecuteNonQuery(), parameters);
if (dBResEdm.ErrCode == )
{
dBResEdm.ExeNum = Convert.ToInt32(dBResEdm.ExeModel);
}
return dBResEdm;
}

  该ExecuteNonQuery方法中要调用SqlCMD方法,而各种数据库中SqlCMD方法方法实现不同,因此需要SqlCMD方法为抽象方法,各子类需要各自实现之。以下分别列出SqlServer和MySql中SqlCMD方法的实现:

         protected override ExeResEdm SqlCMD(string sql, Func<DbCommand, object> fun, params DbParameter[] pms)
{
ExeResEdm dBResEdm = new ExeResEdm();
try
{
pms = ParameterPrepare(pms);
using (SqlConnection con = new SqlConnection(connstr))
{
using (SqlCommand cmd = new SqlCommand(sql, con))
{
con.Open();
if (pms != null && pms.Length > )
{
cmd.Parameters.AddRange((pms));
}
var res = fun(cmd);
dBResEdm.ExeModel = res;
return dBResEdm;
}
}
}
catch (Exception ex)
{
dBResEdm.Module = "SqlCMD方法";
dBResEdm.ExBody = ex;
dBResEdm.ErrCode = ;
return dBResEdm;
}
}
         protected override ExeResEdm SqlCMD(string sql, Func<DbCommand, object> fun, params DbParameter[] pms)
{
ExeResEdm dBResEdm = new ExeResEdm();
try
{
pms = ParameterPrepare(pms);
using (MySqlConnection con = new MySqlConnection(connstr))
{
using (MySqlCommand cmd = new MySqlCommand(sql, con))
{
con.Open();
if (pms != null && pms.Length > )
{
cmd.Parameters.AddRange((pms));
}
var res = fun(cmd);
dBResEdm.ExeModel = res;
return dBResEdm;
}
}
}
catch (Exception ex)
{
dBResEdm.Module = "SqlCMD方法";
dBResEdm.ExBody = ex;
dBResEdm.ErrCode = ;
return dBResEdm;
}
}

  基础类中的其他方法也是类似的套路,在此不再赘述,具体请参见源码。

  在定义了接口和基础方法之后,各个子类就可以在此基础上继承和实现它们了,本代码中的子类是SqlServerHelper,OracleHelperBase,MySqlHelper三个,分别实现对SqlServer,oracle,MySql数据库的访问。这些子类中的方法就是对基类方法的重写,例如SqlServerHelper定义如下:

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

  对oracle数据库,建议使用Oracle.ManagedDataAccess.Client实现的oracle 数据库访问类OracleHelper(无需安装客户端),无32位/64位之分,使用方便,性能好。但该类库仅支持Oracle10g及以上,因此又使用System.Data.OracleClient实现的oracle 数据库访问类OracleHelperMS。这两个类的代码可以是一模一样的,只是引用的类库不同(Oracle.ManagedDataAccess.Client和System.Data.OracleClient)。这两个类可以合并为一个,只需要添加如下代码:

 //#define MS_OracleClient  // 是采用微软oracle类库还是oracle自家的类库

 #if MS_OracleClient
using System.Data.OracleClient;
#else
using Oracle.ManagedDataAccess.Client;
#endif

  若您还需要支持其他数据库类型,请继承和实现 AdoNetBase<T>, IAdoNetBase<T> 即可。

3.2.2 数据访问层Dal

  上面我们定义了ADO.Net访问数据的方法,EF方法只需引用类库即可。工具已备好,我们接下来就可以使用ADO.Net方法或EF方法访问具体的数据库表了。类图如下图所示:

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

  首先,我们定义一个泛型抽象类DBAccessDal(也可以定义为接口),里面定义了需要实现的获取数据方法和添加数据的方法:

     internal abstract class DBAccessDal<T> where T : class
{
internal abstract ExeResEdm GetAll(PageSerach<T> para); internal abstract ExeResEdm Add(AddDBPara<T> dBPara); }

  然后,分别定义ADO.Net方法访问数据的基类AdoNetBaseDal和EF方法访问数据库的基类EFBaseDal:

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

  最后,根据上一步中的基类,实现Log_OperateTrace和Log_SystemMonitor的数据访问子类,如下图:

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

  ADO.Net方式中,基类中已指明了数据库连接对象,Dal中只需要调用相关方法即可。EF方式中,前文写的代码很少,但欠债总是要还的,这里需要额外定义继承自DbContext的Log_OperateTraceContext和Log_SystemMonitorContext来指定数据库上下文。

3.2.3 数据库访问方式工厂

  上文中,介绍了数据库又ADO.Net方式和EF访问方式,我们可以在配置文件中配置使用ADO.Net方式或EF方式,这是通过工厂模式实现的,类图如下:

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

  例如Log_OperateTraceDBAccessFac定义如下:

     internal class Log_OperateTraceDBAccessFac : DBAccessFac<Log_OperateTrace>
{
protected override DBAccessDal<Log_OperateTrace> GetDalByDBAccessType(DBAccessType dbAccessType)
{
if (dbAccessType == DBAccessType.EF)
{
Log_OperateTraceEFDal log_OperateTraceDal = new Log_OperateTraceEFDal(new Log_OperateTraceContext());
return log_OperateTraceDal;
}
else if (dbAccessType == DBAccessType.NH)
{
throw new Exception("Not define dal methods when DBAccessType = NH");
}
else
{
return new Log_OperateTraceAdoDal();
} }
}

  另外,还有数据库功能公共类ComDBFun和InfluxDB访问类InfluxDBHelper的介绍略。

  至此,数据库访问帮助类介绍完毕,详情请参阅DBUtil部分代码。

四、日志信息获取类

  本部分定义了日志组件使用的基础方法,如客户端服务器信息ClientServerInfo类、在线人数访客人数统计VisitOnlineCount 类、日志组件公共类LogCom.cs。

4.1 ClientServerInfo类

  该类库用于收集客户端和服务器端的信息,包括客户端信息子类ClientInfo和服务器端信息子类ServerInfo。

  ClientInfo类用来获取客户端的ip地址、主机名、Mac地址、浏览器信息等。

  ServerInfo类用来获取服务器端的ip地址、主机名、操作系统、CLR版本、服务器运行时间、可用硬盘空间、CPU使用率、内存使用率等信息。

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

4.2 访客人数统计类VisitOnlineCount类

  本类中定义了在线人数和访客统计抽象类IVisitCount类,具体的类要实现该类中的抽象方法。

  对.net平台,存在Session_Start和Session_End事件,访客统计的实现思路较为清晰,本组件提供了两种方案:使用Application对象实现、使用缓存实现。具体采用哪种方案由简单工厂决定,默认采用缓存方案。

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

4.3 公共类LogCom类

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

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

五、日志追加器Appender类库  

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

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

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

  公共追加器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

  在这里定义了异常处理中间件,在捕捉到异常时,将异常日志进行记录。

     internal class ErrorHandlingMiddleware
{
private readonly RequestDelegate next; public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
} public async Task Invoke(Microsoft.AspNetCore.Http.HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
var statusCode = context.Response.StatusCode;
if (ex is ArgumentException)
{
statusCode = ;
}
await HandleExceptionAsync(context, statusCode, ex.Message);
}
finally
{
var statusCode = context.Response.StatusCode;
var msg = "";
if (statusCode == )
{
msg = "未授权";
}
else if (statusCode == )
{
msg = "未找到服务";
}
else if (statusCode == )
{
msg = "请求错误";
}
else if (statusCode != && statusCode != )
{
msg = "未知错误" + statusCode;
}
if (!string.IsNullOrWhiteSpace(msg))
{
await HandleExceptionAsync(context, statusCode, msg);
}
}
} private static Task HandleExceptionAsync(Microsoft.AspNetCore.Http.HttpContext context, int statusCode, string msg)
{
var data = new { code = statusCode.ToString(), is_success = false, msg = msg };
var result = JsonConvert.SerializeObject(new { data = data }); Log_OperateTraceBllEdm exLog = new Log_OperateTraceBllEdm()
{
Detail = result,
LogType = LogType.异常,
Remark = "异常时间" + DateTime.Now,
TabOrModu = "异常模块",
};
LogApi.WriteLog( LogLevel.Error,exLog); context.Response.ContentType = "application/json;charset=utf-8";
return context.Response.WriteAsync(result);
}
} public static class ErrorHandlingExtensions
{
public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlingMiddleware>();
}
}

6.2 请求上下文中间件HttpContext

  在这里,定义了请求上下文的中间件,记录了当前求请求的上下文,模拟了当前上下文的Session信息,将所有的SessionId保存起来,将过期的SessionId移除,来实现在线人数统计和历史访客统计。

     internal static class HttpContext
{
public class SessionEdm
{
public string Key { get; set; }
public string Val { get; set; }
public DateTime ExpiresAtTime { get; set; }
} public static Microsoft.AspNetCore.Http.HttpContext Current => _accessor.HttpContext; static ConcurrentDictionary<string, SessionEdm> sessionMaps = new ConcurrentDictionary<string, SessionEdm>(); static double dncSessionMins = AppConfig.GetDncSessionTimeoutMins(); private static IHttpContextAccessor _accessor;
internal static void Configure(IHttpContextAccessor accessor)
{
_accessor = accessor;
} public static VOEdm GetOnlineVisitNum(int preVisitNum)
{
if (_accessor.HttpContext != null)
{
var curSession = _accessor.HttpContext.Session;
SessionEdm sessionEdm = new SessionEdm() { Key = curSession.Id, Val = "", ExpiresAtTime = DateTime.Now.AddMinutes(dncSessionMins) };
sessionMaps.TryAdd(curSession.Id, sessionEdm);
}
int visitorsNum = sessionMaps.Count;
VOEdm vOEdm = new VOEdm() { VisitNum = preVisitNum + visitorsNum };
//将过期session的值变为0,未过期的session的数量为在线人数
var keys = sessionMaps.Keys.ToArray();
for (int i = ; i < sessionMaps.Count; i++)
{
var cur = sessionMaps[keys[i]];
if (cur.Val == "" && cur.ExpiresAtTime <= DateTime.Now) //已过期
{
cur.Val = "";
}
}
var onlineNums = sessionMaps.Where(a => a.Value.Val == "").Count();
vOEdm.OnlineNum = onlineNums;
return vOEdm;
} } public static class StaticHttpContextExtensions
{
public static void AddHttpContextAccessor(this IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
} public static IApplicationBuilder UseStaticHttpContext(this IApplicationBuilder app)
{
var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
HttpContext.Configure(httpContextAccessor);
return app;
}
}

七、外部接口类LogApi类

  在本类中,调用其他类的方法,形成供其他业务系统调用的方法。包含以下内容:日志组件注册、写日志等。各业务系统不需要关心这些方法的具体实现,只需要封装业务实体,调用写日志的方法即可。LogApi类会调用其他的类,如等来实现日志记录的功能。类图如下:

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

  • 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日志媒介的信息。

  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来标记某人的离线。

上一篇:vmware虚拟机网络自动断开的问题


下一篇:rtpMIDI Tutorial