【开源】OSharp3.0框架解说系列(6.2):操作日志与数据日志
2015-07-27 18:24
405 查看
OSharp是什么?
OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现。与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现。依赖注入、ORM、对象映射、日志、缓存等等功能,都只定义了一套最基础最通用的抽象封装,提供了一套统一的API、约定与规则,并定义了部分执行流程,主要是让项目在一定的规范下进行开发。所有的功能实现端,都是通过现有的成熟的第三方组件来实现的,除了EntityFramework之外,所有的第三方实现都可以轻松的替换成另一种第三方实现,OSharp框架正是要起隔离作用,保证这种变更不会对业务代码造成影响,使用统一的API来进行业务实现,解除与第三方实现的耦合,保持业务代码的规范与稳定。
本文已同步到系列目录:OSharp快速开发框架解说系列
前言
在《【开源】OSharp框架解说系列(6.1):日志系统设计》中,我们已经设计并实现了一个可扩展的日志系统,只要定义好输出端的Adapter,就可以以任意形式输出日志信息。在系统开发中,有些日志记录需求是常规需要的,比如操作日志,数据变更日志,系统异常日志等,我们希望把这些常规需求都集成到OSharp框架当中。有了内置的支持,在做开发的时候,只需要很简单的配置,就可以实现相关需求。
关于三类日志,这里先简要描述一下:
操作日志:粗略描述系统用户(如管理员、业务人员、会员等)对系统的业务操作,只需要说清楚“XXX用户在XXX时间做了XXX操作”
数据日志:有时候,为了追溯用户的业务操作对系统产生的影响,需要记录数据变更细节,这就是数据日志
系统日志:主要记录系统在运行过程中产生的与业务无关的常规或异常的日志信息,这些日志信息通常由系统维护人员或开发人员查看
日志记录准备
在OSharp框架中,操作日志与数据日志的记录流程如下图所示:这里用文字简单描述一下操作日志与数据日志记录的实现思路:
定义了一个“功能信息记录”的实体,用于提取系统中各个功能点的基础信息(名称、MVC的Area-Controller-Action、功能访问类型(匿名访问-登录访问-特定角色访问)、是否启用功能日志,是否启用数据日志、功能URL等),并配置功能的行为
定义了一个“实体信息记录”的实体,用于提取系统中各个数据实体类型的基础信息(实体类型全名、实体名称、是否启用数据日志,实体属性信息集),并配置实体的行为
系统初始化的时候,通过反射加载的程序集,提取并构建各个功能点(主要是MVC的Controller-Action)的功能信息记录,更新到数据库中
系统初始化的时候,通过反射加载的程序集,提取并构建各个实体类型的实体信息记录,更新到数据库中
利用MVC框架的ActionFilter进行AOP拦截,定义一个专门用于操作日志记录的 OperateLogFilterAttribute ,重写 OnActionExecuted 方法进行操作日志的记录
操作日志与数据日志记录的详细流程如下:
在用户的业务操作执行到保存数据的时候(EF执行SaveChanges时),根据操作涉及的实体获取相应的实体信息记录,确定是否创建数据日志,不需创建则跳过
需要创建时,根据实体的状态(Added-Modified-Deleted),创建各个实体的新增-更新-删除的数据日志信息,并存储到临时缓存中
执行到 OperateLogFilterAttribute 的 OnActionExecuted 方法的时候,根据ActionExecutedContext 中提供的Area,Controller,Action等信息,查询出当前功能的功能信息记录,确定是否记录操作日志,不需记录则返回
需要根据功能信息记录,创建操作日志信息,并指定当前用户为日志操作人。
根据功能信息是否启用数据日志的配置,确定是否记录数据日志,需要记录时,从临时缓存中提取前面创建的数据日志,作为从数据配置到操作日志中
向系统外部保存操作日志信息,完成操作日志的记录
功能信息与实体信息
记录各个功能点的功能信息接口定义如下:/// <summary> /// 功能接口,最小功能信息 /// </summary> public interface IFunction { /// <summary> /// 获取或设置 功能名称 /// </summary> string Name { get; set; } /// <summary> /// 获取或设置 区域名称 /// </summary> string Area { get; set; } /// <summary> /// 获取或设置 控制器名称 /// </summary> string Controller { get; set; } /// <summary> /// 获取或设置 功能名称 /// </summary> string Action { get; set; } /// <summary> /// 获取或设置 功能类型 /// </summary> FunctionType FunctionType { get; set; } /// <summary> /// 获取或设置 是否启用操作日志 /// </summary> bool OperateLogEnabled { get; set; } /// <summary> /// 获取或设置 是否启用数据日志 /// </summary> bool DataLogEnabled { get; set; } /// <summary> /// 获取或设置 是否锁定 /// </summary> bool IsLocked { get; set; } /// <summary> /// 获取或设置 功能地址 /// </summary> string Url { get; set; } }
记录各个数据实体类型的实体信息接口定义如下:
/// <summary> /// 实体数据接口 /// </summary> public interface IEntityInfo { /// <summary> /// 获取 实体数据类型名称 /// </summary> string ClassName { get; } /// <summary> /// 获取 实体数据显示名称 /// </summary> string Name { get; } /// <summary> /// 获取 是否启用数据日志 /// </summary> bool DataLogEnabled { get; } /// <summary> /// 获取 实体属性信息字典 /// </summary> IDictionary<string, string> PropertyNames { get; } }
OSharp框架中,已经派生了 Function 与 EntityInfo 两个实体类型,作为功能信息与实体信息的封装。
功能信息与实体信息的初始化实现,主要定义在 FunctionHandlerBase<TFunction, TKey> 与 EntityInfoHandlerBase<TEntityInfo, TKey> 两个基础中,OSharp中已经派生了 public class DefaultFunctionHandler : FunctionHandlerBase<Function, Guid> 与 public class DefaultEntityInfoHandler : EntityInfoHandlerBase<EntityInfo, Guid> 作为系统初始化时,从程序集中提取并更新功能信息与数据信息的默认实现。
由代码图,我们能很直观的看到实体与处理器之间的关系:
关于这两个处理器的实现流程,不是本文的重点,将在后面讲解OSharp初始化实现时再详述,这里先略过。提取的数据展示如下:
提取的功能信息:
提取的实体数据信息:
操作日志与数据日志实体
操作日志实体定义如下:/// <summary> /// 操作日志信息类 /// </summary> [Description("系统-操作日志信息")] public class OperateLog : EntityBase<int>, ICreatedTime { /// <summary> /// 初始化一个<see cref="OperateLog"/>类型的新实例 /// </summary> public OperateLog() { DataLogs = new List<DataLog>(); } /// <summary> /// 获取或设置 执行的功能名称 /// </summary> [StringLength(100)] public string FunctionName { get; set; } /// <summary> /// 获取或设置 操作人信息 /// </summary> public Operator Operator { get; set; } /// <summary> /// 获取设置 信息创建时间 /// </summary> public DateTime CreatedTime { get; set; } /// <summary> /// 获取或设置 数据日志集合 /// </summary> public virtual ICollection<DataLog> DataLogs { get; set; } }
数据日志实体定义如下:
/// <summary> /// 数据日志信息类 /// </summary> [Description("系统-数据日志信息")] public class DataLog : EntityBase<int> { /// <summary> /// 初始化一个<see cref="DataLog"/>类型的新实例 /// </summary> public DataLog() : this(null, null, OperatingType.Query) { } /// <summary> /// 初始化一个<see cref="DataLog"/>类型的新实例 /// </summary> public DataLog(string entityName, string name, OperatingType operatingType) { EntityName = entityName; Name = name; OperateType = operatingType; LogItems = new List<DataLogItem>(); } /// <summary> /// 获取或设置 类型名称 /// </summary> [StringLength(500)] [Display(Name = "类型名称")] public string EntityName { get; set; } /// <summary> /// 获取或设置 实体名称 /// </summary> [Display(Name = "实体名称")] public string Name { get; set; } /// <summary> /// 获取或设置 数据编号 /// </summary> [StringLength(150)] [DisplayName("主键值")] public string EntityKey { get; set; } /// <summary> /// 获取或设置 操作类型 /// </summary> [Description("操作类型")] public OperatingType OperateType { get; set; } /// <summary> /// 获取或设置 操作日志信息 /// </summary> public virtual OperateLog OperateLog { get; set; } /// <summary> /// 获取或设置 操作明细 /// </summary> public virtual ICollection<DataLogItem> LogItems { get; set; } }
数据日志操作变更明细项
/// <summary> /// 实体操作日志明细 /// </summary> [Description("系统-操作日志明细信息")] public class DataLogItem : EntityBase<Guid> { /// <summary> /// 初始化一个<see cref="DataLogItem"/>类型的新实例 /// </summary> public DataLogItem() : this(null, null) { } /// <summary> ///初始化一个<see cref="DataLogItem"/>类型的新实例 /// </summary> /// <param name="originalValue">旧值</param> /// <param name="newValue">新值</param> public DataLogItem(string originalValue, string newValue) { Id = CombHelper.NewComb(); OriginalValue = originalValue; NewValue = newValue; } /// <summary> /// 获取或设置 字段 /// </summary> public string Field { get; set; } /// <summary> /// 获取或设置 字段名称 /// </summary> public string FieldName { get; set; } /// <summary> /// 获取或设置 旧值 /// </summary> public string OriginalValue { get; set; } /// <summary> /// 获取或设置 新值 /// </summary> public string NewValue { get; set; } /// <summary> /// 获取或设置 数据类型 /// </summary> public string DataType { get; set; } /// <summary> /// 获取或设置 所属数据日志 /// </summary> public virtual DataLog DataLog { get; set; } }
数据日志操作类型的枚举:
/// <summary> /// 实体数据日志操作类型 /// </summary> public enum OperatingType { /// <summary> /// 查询 /// </summary> Query = 0, /// <summary> /// 新建 /// </summary> Insert = 10, /// <summary> /// 更新 /// </summary> Update = 20, /// <summary> /// 删除 /// </summary> Delete = 30 }
下图以较直观的方式显示操作日志与数据日志之间的关系:
数据日志的创建
数据日志,主要记录业务操作过程中涉及到的各个数据实体的变更,而这里的变更,主要是实体的新增、更新、删除三种情况。在EntityFramework的数据操作中,实体经过业务处理之后,都是有状态跟踪的,即是 EntityState 枚举类型:
public enum EntityState { Detached = 1, Unchanged = 2, Added = 4, Deleted = 8, Modified = 16, }
我们要关心的状态,主要是Added、Deleted、Modified三个值,分别对应着新增、删除、更新三种状态,在EntityFramework执行到 SaveChanges 的时候,各个实体的状态已经确定。OSharp将在这个时机获取变更的实体并创建数据日志信息。
/// <summary> /// 提交当前单元操作的更改 /// </summary> /// <param name="validateOnSaveEnabled">提交保存时是否验证实体约束有效性。</param> /// <returns>操作影响的行数</returns> internal virtual int SaveChanges(bool validateOnSaveEnabled) { bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled; try { Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled; //记录实体操作日志 13 List<DataLog> logs = new List<DataLog>(); 14 if (DataLoggingEnabled) 15 { 16 logs = this.GetEntityDataLogs().ToList(); 17 } int count = base.SaveChanges(); 19 if (count > 0 && DataLoggingEnabled) 20 { 21 Logger.Info(logs, true); 22 } TransactionEnabled = false; return count; } catch (DbUpdateException e) { if (e.InnerException != null && e.InnerException.InnerException is SqlException) { SqlException sqlEx = e.InnerException.InnerException as SqlException; string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number); throw new OSharpException("提交数据更新时发生异常:" + msg, sqlEx); } throw; } finally { if (isReturn) { Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled; } } }
以上代码中, DataLoggingEnabled 属性 是当前上下文是否开启数据日志的总开关,当开启数据日志记录功能时,才进行数据日志的创建。
创建数据日志的实现如下,主要是从对象管理器中筛选出指定状态的实体对象,再由实体类型全名获取相应实体的“实体信息记录”,确定是否执行数据日志的创建,然后创建数据日志信息:
/// <summary> /// 获取数据上下文的变更日志信息 /// </summary> public static IEnumerable<DataLog> GetEntityDataLogs(this DbContext dbContext) { ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; ObjectStateManager manager = objectContext.ObjectStateManager; IEnumerable<DataLog> logs = from entry in manager.GetObjectStateEntries(EntityState.Added).Where(entry => entry.Entity != null) let entityInfo = OSharpContext.Current.EntityInfoHandler.GetEntityInfo(entry.Entity.GetType()) where entityInfo != null && entityInfo.DataLogEnabled select GetAddedLog(entry, entityInfo); logs = logs.Concat(from entry in manager.GetObjectStateEntries(EntityState.Modified).Where(entry => entry.Entity != null) let entityInfo = OSharpContext.Current.EntityInfoHandler.GetEntityInfo(entry.Entity.GetType()) where entityInfo != null && entityInfo.DataLogEnabled select GetModifiedLog(entry, entityInfo)); logs = logs.Concat(from entry in manager.GetObjectStateEntries(EntityState.Deleted).Where(entry => entry.Entity != null) let entityInfo = OSharpContext.Current.EntityInfoHandler.GetEntityInfo(entry.Entity.GetType()) where entityInfo != null && entityInfo.DataLogEnabled select GetDeletedLog(entry, entityInfo)); return logs; }
创建“新增”实体的数据日志:
/// <summary> /// 获取添加数据的日志信息 /// </summary> /// <param name="entry">实体状态跟踪信息</param> /// <param name="entityInfo">实体数据信息</param> /// <returns>新增数据日志信息</returns> private static DataLog GetAddedLog(ObjectStateEntry entry, IEntityInfo entityInfo) { DataLog log = new DataLog(entityInfo.ClassName, entityInfo.Name, OperatingType.Insert); for (int i = 0; i < entry.CurrentValues.FieldCount; i++) { string name = entry.CurrentValues.GetName(i); if (name == "Timestamp") { continue; } object value = entry.CurrentValues.GetValue(i); if (name == "Id") { log.EntityKey = value.ToString(); } Type fieldType = entry.CurrentValues.GetFieldType(i); DataLogItem logItem = new DataLogItem() { Field = name, FieldName = entityInfo.PropertyNames[name], NewValue = value == null ? null : value.ToString(), DataType = fieldType == null ? null : fieldType.Name }; log.LogItems.Add(logItem); } return log; }
创建“更新”实体的数据日志:
/// <summary> /// 获取修改数据的日志信息 /// </summary> /// <param name="entry">实体状态跟踪信息</param> /// <param name="entityInfo">实体数据信息</param> /// <returns>修改数据日志信息</returns> private static DataLog GetModifiedLog(ObjectStateEntry entry, IEntityInfo entityInfo) { DataLog log = new DataLog(entityInfo.ClassName, entityInfo.Name, OperatingType.Update); for (int i = 0; i < entry.CurrentValues.FieldCount; i++) { string name = entry.CurrentValues.GetName(i); if (name == "Timestamp") { continue; } object currentValue = entry.CurrentValues.GetValue(i); object originalValue = entry.OriginalValues[name]; if (name == "Id") { log.EntityKey = originalValue.ToString(); } if (currentValue.Equals(originalValue)) { continue; } Type fieldType = entry.CurrentValues.GetFieldType(i); DataLogItem logItem = new DataLogItem() { Field = name, FieldName = entityInfo.PropertyNames[name], NewValue = currentValue == null ? null : currentValue.ToString(), OriginalValue = originalValue == null ? null : originalValue.ToString(), DataType = fieldType == null ? null : fieldType.Name }; log.LogItems.Add(logItem); } return log; }
创建“删除”实体的数据日志:
/// <summary> /// 获取删除数据的日志信息 /// </summary> /// <param name="entry">实体状态跟踪信息</param> /// <param name="entityInfo">实体数据信息</param> /// <returns>删除数据日志信息</returns> private static DataLog GetDeletedLog(ObjectStateEntry entry, IEntityInfo entityInfo) { DataLog log = new DataLog(entityInfo.ClassName, entityInfo.Name, OperatingType.Delete); for (int i = 0; i < entry.OriginalValues.FieldCount; i++) { string name = entry.OriginalValues.GetName(i); if (name == "Timestamp") { continue; } object originalValue = entry.OriginalValues[i]; if (name == "Id") { log.EntityKey = originalValue.ToString(); } Type fieldType = entry.OriginalValues.GetFieldType(i); DataLogItem logItem = new DataLogItem() { Field = name, FieldName = entityInfo.PropertyNames[name], OriginalValue = originalValue == null ? null : originalValue.ToString(), DataType = fieldType == null ? null : fieldType.Name }; log.LogItems.Add(logItem); } return log; }
数据日志的传递
前面我们已经完成了数据日志创建,但数据日志是由数据层的EntityFramework的SaveChanges方法创建的,而创建的数据日志,最终将传递到上层定义的 OperateLogFilterAttribute 中进行使用,这就需要我们通过一定的机制将数据日志往上传递。在这里,使用的是日志组件。OSharp中定义了一个数据日志缓存,专门用于接收数据层创建的数据日志信息:
/// <summary> /// 数据日志缓存接口 /// </summary> public interface IDataLogCache : IDependency { /// <summary> /// 获取 数据日志集合 /// </summary> IEnumerable<DataLog> DataLogs { get; } /// <summary> /// 向缓存中添加数据日志信息 /// </summary> /// <param name="dataLog">数据日志信息</param> void AddDataLog(DataLog dataLog); }
在专用于数据日志记录的 DatabaseLog 的 Write 方法重写时,判断数据是否是 DataLog 类型,并存入 IDataLogCache 中,这里使用MVC的依赖注入功能获取IDataLogCache的实现,以保证其在同一Http请求中,获取的是同一实例:
/// <summary> /// 获取日志输出处理委托实例 /// </summary> /// <param name="level">日志输出级别</param> /// <param name="message">日志消息</param> /// <param name="exception">日志异常</param> /// <param name="isData">是否数据日志</param> protected override void Write(LogLevel level, object message, Exception exception, bool isData = false) { if (!isData) { return; } IEnumerable<DataLog> dataLogs = message as IEnumerable<DataLog>; if (dataLogs == null) { return; } IDataLogCache logCache = DependencyResolver.Current.GetService<IDataLogCache>(); foreach (DataLog dataLog in dataLogs) { logCache.AddDataLog(dataLog); } }
操作日志的记录
定义了一个 OperateLogFilterAttribute 的ActionFilter,专门用于拦截并记录操作日志。/// <summary> /// 操作日志记录过滤器 /// </summary> public class OperateLogFilterAttribute : ActionFilterAttribute { /// <summary> /// 获取或设置 数据日志缓存 /// </summary> public IDataLogCache DataLogCache { get; set; } /// <summary> /// 获取或设置 操作日志输出者 /// </summary> public IOperateLogWriter OperateLogWriter { get; set; } /// <summary> /// Called after the action method executes. /// </summary> /// <param name="filterContext">The filter context.</param> public override void OnActionExecuted(ActionExecutedContext filterContext) { string area = filterContext.GetAreaName(); string controller = filterContext.GetControllerName(); string action = filterContext.GetActionName(); IFunction function = OSharpContext.Current.FunctionHandler.GetFunction(area, controller, action); if (function == null || !function.OperateLogEnabled) { return; } Operator @operator = new Operator() { Ip = filterContext.HttpContext.Request.GetIpAddress(), }; if (filterContext.HttpContext.Request.IsAuthenticated) { ClaimsIdentity identity = filterContext.HttpContext.User.Identity as ClaimsIdentity; if (identity != null) { @operator.UserId = identity.GetClaimValue(ClaimTypes.NameIdentifier); @operator.Name = identity.GetClaimValue(ClaimTypes.Name); @operator.NickName = identity.GetClaimValue(ClaimTypes.GivenName); } } OperateLog operateLog = new OperateLog() { FunctionName = function.Name, Operator = @operator }; if (function.DataLogEnabled) { foreach (DataLog dataLog in DataLogCache.DataLogs) { operateLog.DataLogs.Add(dataLog); } } OperateLogWriter.Write(operateLog); } }
最后,操作日志将由 IOperateLogWriter 进行输出,定义如下:
/// <summary> /// 操作日志输出接口 /// </summary> public interface IOperateLogWriter : IDependency { /// <summary> /// 输出操作日志 /// </summary> /// <param name="operateLog">操作日志信息</param> void Write(OperateLog operateLog); }
默认的,操作日志将被记录到数据库中:
/// <summary> /// 操作日志数据库输出实现 /// </summary> public class DatabaseOperateLogWriter : IOperateLogWriter { private readonly IRepository<OperateLog, int> _operateLogRepository; /// <summary> /// 初始化一个<see cref="DatabaseOperateLogWriter"/>类型的新实例 /// </summary> public DatabaseOperateLogWriter(IRepository<OperateLog, int> operateLogRepository) { _operateLogRepository = operateLogRepository; } /// <summary> /// 输出操作日志 /// </summary> /// <param name="operateLog">操作日志信息</param> public void Write(OperateLog operateLog) { operateLog.CheckNotNull("operateLog" ); _operateLogRepository.Insert(operateLog); } }
操作日志显示
如果一条操作日志中包含有数据日志,那么数据日志将以下级数据的方式展现在操作日志中:开源说明
github.com
OSharp项目已在github.com上开源,地址为:https://github.com/i66soft/osharp,欢迎阅读代码,欢迎 Watch(关注),欢迎 Star(推荐),如果您认同 OSharp 项目的设计思想,欢迎参与 OSharp 项目的开发。在Visual Studio 2013中,可直接获取 OSharp 的最新源代码,获取方式如下,地址为:https://github.com/i66soft/osharp.git
开源项目参与方式
很多童鞋想参与开源项目,为项目做贡献,但又不知道如何做,这里我简单说下参与OSharp的步骤吧:在 https://github.com/i66soft/osharp 右上角 Fork 一下项目源码,在你的账户下会有一份代码的副本
使用VisualStudio Clone 你账户下的代码到本地,更改代码,再提交,就完成代码的更改了
如果觉得有并入 i66soft 主干的价值,可以向主干提交 pull request申请,如果我审核通过,就可以合并到主干了,这就形成了一次开源代码的贡献了
如果我没有接受合并,你也可以在你的账户上按你的风格去发展osharp
我也会经常浏览各个Fork版本对项目的更改,如果觉得有价值,也会主动合并到主干代码中,也能形成一次对开源的贡献
为保证提交的质量,也便于对代码的合并,每次更改与提交应该只做一件事,只提交必要的更改
nuget
OSharp的相关类库已经发布到nuget上,欢迎试用,直接在nuget上搜索 “osharp” 关键字即可找到系列导航
相关文章推荐
- java基础第一讲——JDK、注释、帮助文档、编码规范
- linux下部署多台tomcat
- hdu 1671 Phone List(Trie树)
- C++读取Excel的XLS文件的方法
- 图片旋转,拖拽,缩放,删除一体
- 010--maven项目与myEclipse
- const在基本数据类型和指针类型中的用法小结
- 从join on和where执行顺序认识T-SQL查询处理执行顺序
- 树形DP入门总结
- 美团Android自动化之旅—适配渠道包
- jquery checkbox勾选/取消勾选的诡异问题
- Balanced Numbers(数位+状压)
- 线程池的创建以及使用
- class
- 名字的漂亮度
- JVM 进阶五
- IOS中的block的retain循环
- CPU大小端字节序的检测
- 树型dp入门小节
- 【php】Apache无法自动跳转却显示目录与php无法连接mysql数据库的解决方案