Abp 框架为咱们自带了审计日志功能,审计日志能够方便地查看每次请求接口所耗的时间,可以帮助咱们快速定位到某些性能有问题的接口。除此以外,审计日志信息还包含有每次调用接口时客户端请求的参数信息,客户端的 IP 与客户端使用的浏览器。有了这些数据以后,咱们就能够很方便地复现接口产生 BUG 时的一些环境信息。html
固然若是你脑洞更大的话,能够根据这些数据来开发一个可视化的图形界面,方便开发与测试人员来快速定位问题。git
PS:github
若是使用了 Abp.Zero 模块则自带的审计记录实现是存储到数据库当中的,可是在使用 EF Core + MySQL(EF Provider 为 Pomelo.EntityFrameworkCore.MySql) 在高并发的状况下会有数据库链接超时的问题,这块推荐是重写实现,本身采用 Redis 或者其余存储方式。数据库
若是须要禁用审计日志功能,则须要在任意模块的预加载方法(PreInitialize()
) 当中增长以下代码关闭审计日志功能。浏览器
public class XXXStartupModule { public override PreInitialize() { // 禁用审计日志 Configuration.Auditing.IsEnabled = false; } }
审计组件与参数校验组件同样,都是经过 MVC 过滤器与 Castle 拦截器来实现记录的。也就是说,在每次调用接口/方法时都会进入 过滤器/拦截器 并将其写入到数据库表 AbpAuditLogs
当中。并发
其核心思想十分简单,就是在执行具体接口方法的时候,先使用 StopWatch 对象来记录执行完一个方法所须要的时间,而且还可以经过 HttpContext
来获取到一些客户端的关键信息。mvc
同上一篇文章所讲的同样,过滤器是在 AddAbp()
方法内部的 ConfigureAspNetCore()
方法注入的。app
private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver) { // ... 其余代码 //Configure MVC services.Configure<MvcOptions>(mvcOptions => { mvcOptions.AddAbp(services); }); // ... 其余代码 }
而下面就是过滤器的注入方法:框架
internal static class AbpMvcOptionsExtensions { public static void AddAbp(this MvcOptions options, IServiceCollection services) { // ... 其余代码 AddFilters(options); // ... 其余代码 } // ... 其余代码 private static void AddFilters(MvcOptions options) { // ... 其余过滤器注入 // 注入审计日志过滤器 options.Filters.AddService(typeof(AbpAuditActionFilter)); // ... 其余过滤器注入 } // ... 其余代码 }
注入拦截器的地方与 DTO 自动验证的拦截器的位置同样,都是在 AbpBootstrapper
对象被构造的时候进行注册。异步
public class AbpBootstrapper : IDisposable { private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null) { // ... 其余代码 if (!options.DisableAllInterceptors) { AddInterceptorRegistrars(); } } // ... 其余代码 // 添加各类拦截器 private void AddInterceptorRegistrars() { ValidationInterceptorRegistrar.Initialize(IocManager); AuditingInterceptorRegistrar.Initialize(IocManager); EntityHistoryInterceptorRegistrar.Initialize(IocManager); UnitOfWorkRegistrar.Initialize(IocManager); AuthorizationInterceptorRegistrar.Initialize(IocManager); } // ... 其余代码 }
转到 AuditingInterceptorRegistrar
的具体实现能够发现,他在内部针对于审计日志拦截器的注入是区分了类型的。
internal static class AuditingInterceptorRegistrar { public static void Initialize(IIocManager iocManager) { iocManager.IocContainer.Kernel.ComponentRegistered += (key, handler) => { // 若是审计日志配置类没有被注入,则直接跳过 if (!iocManager.IsRegistered<IAuditingConfiguration>()) { return; } var auditingConfiguration = iocManager.Resolve<IAuditingConfiguration>(); // 判断当前 DI 所注入的类型是否应该为其绑定审计日志拦截器 if (ShouldIntercept(auditingConfiguration, handler.ComponentModel.Implementation)) { handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuditingInterceptor))); } }; } // 本方法主要用于判断当前类型是否符合绑定拦截器的条件 private static bool ShouldIntercept(IAuditingConfiguration auditingConfiguration, Type type) { // 首先判断当前类型是否在配置类的注册类型之中,若是是,则进行拦截器绑定 if (auditingConfiguration.Selectors.Any(selector => selector.Predicate(type))) { return true; } // 当前类型若是拥有 Audited 特性,则进行拦截器绑定 if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) { return true; } // 若是当前类型内部的全部方法当中有一个方法拥有 Audited 特性,则进行拦截器绑定 if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true))) { return true; } // 都不知足则返回 false,不对当前类型进行绑定 return false; } }
能够看到在判断是否绑定拦截器的时候,Abp 使用了 auditingConfiguration.Selectors
的属性来进行判断,那么默认 Abp 为咱们添加了哪些类型是一定有审计日志的呢?
经过代码追踪,咱们来到了 AbpKernalModule
类的内部,在其预加载方法里面有一个 AddAuditingSelectors()
的方法,该方法的做用就是添加了一个针对于应用服务类型的一个选择器对象。
public sealed class AbpKernelModule : AbpModule { public override void PreInitialize() { // ... 其余代码 AddAuditingSelectors(); // ... 其余代码 } // ... 其余代码 private void AddAuditingSelectors() { Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof(IApplicationService).IsAssignableFrom(type) ) ); } // ... 其余代码 }
咱们先看一下 NamedTypeSelector
的一个做用是什么,其基本类型定义由一个 string
和 Func<Type, bool>
组成,十分简单,重点就出在这个断言委托上面。
public class NamedTypeSelector { // 选择器名称 public string Name { get; set; } // 断言委托 public Func<Type, bool> Predicate { get; set; } public NamedTypeSelector(string name, Func<Type, bool> predicate) { Name = name; Predicate = predicate; } }
回到最开始的地方,当 Abp 为 Selectors 添加了一个名字为 "Abp.ApplicationServices" 的类型选择器。其断言委托的大致意思就是传入的 type 参数是继承自 IApplicationService
接口的话,则返回 true
,不然返回 false
。
这样在程序启动的时候,首先注入类型的时候,会首先进入上文所述的拦截器绑定类当中,这个时候会使用 Selectors 内部的类型选择器来调用这个集合内部的断言委托,只要这些选择器对象有一个返回 true
,那么就直接与当前注入的 type 绑定拦截器。
首先查看这个过滤器的总体类型结构,一个标准的过滤器,确定要实现 IAsyncActionFilter
接口。从下面的代码咱们能够看到其注入了 IAbpAspNetCoreConfiguration
和一个 IAuditingHelper
对象。这两个对象的做用分别是判断是否记录日志,另外一个则是用来真正写入日志所使用的。
public class AbpAuditActionFilter : IAsyncActionFilter, ITransientDependency { // 审计日志组件配置对象 private readonly IAbpAspNetCoreConfiguration _configuration; // 真正用来写入审计日志的工具类 private readonly IAuditingHelper _auditingHelper; public AbpAuditActionFilter(IAbpAspNetCoreConfiguration configuration, IAuditingHelper auditingHelper) { _configuration = configuration; _auditingHelper = auditingHelper; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // ... 代码实现 } // ... 其余代码 }
接着看 AbpAuditActionFilter()
方法内部的实现,进入这个过滤器的时候,经过 ShouldSaveAudit()
方法来判断是否要写审计日志。
以后呢与 DTO 自动验证的过滤器同样,经过 AbpCrossCuttingConcerns.Applying()
方法为当前的对象增长了一个标识,用来告诉拦截器说我已经处理过了,你就不要再重复处理了。
再往下就是建立审计信息,执行具体接口方法,而且若是产生了异常的话,也会存放到审计信息当中。
最后接口不管是否执行成功,仍是说出现了异常信息,都会将其性能计数信息同审计信息一块儿,经过 IAuditingHelper
存储起来。
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 判断是否写日志 if (!ShouldSaveAudit(context)) { await next(); return; } // 为当前类型打上标识 using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Auditing)) { // 构造审计信息(AuditInfo) var auditInfo = _auditingHelper.CreateAuditInfo( context.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(), context.ActionDescriptor.AsControllerActionDescriptor().MethodInfo, context.ActionArguments ); // 开始性能计数 var stopwatch = Stopwatch.StartNew(); try { // 尝试调用接口方法 var result = await next(); // 产生异常以后,将其异常信息存放在审计信息之中 if (result.Exception != null && !result.ExceptionHandled) { auditInfo.Exception = result.Exception; } } catch (Exception ex) { // 产生异常以后,将其异常信息存放在审计信息之中 auditInfo.Exception = ex; throw; } finally { // 中止计数,而且存储审计信息 stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); await _auditingHelper.SaveAsync(auditInfo); } } }
拦截器处理时的整体思路与过滤器相似,其核心都是经过 IAuditingHelper
来建立审计信息和持久化审计信息的。只不过呢因为拦截器不只仅是处理 MVC 接口,也会处理内部的一些类型的方法,因此针对同步方法与异步方法的处理确定会复杂一点。
拦截器呢,咱们关心一下他的核心方法 Intercept()
就好了。
public void Intercept(IInvocation invocation) { // 判断过滤器是否已经处理了过了 if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)) { invocation.Proceed(); return; } // 经过 IAuditingHelper 来判断当前方法是否须要记录审计日志信息 if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget)) { invocation.Proceed(); return; } // 构造审计信息 var auditInfo = _auditingHelper.CreateAuditInfo(invocation.TargetType, invocation.MethodInvocationTarget, invocation.Arguments); // 判断方法的类型,同步方法与异步方法的处理逻辑不同 if (invocation.Method.IsAsync()) { PerformAsyncAuditing(invocation, auditInfo); } else { PerformSyncAuditing(invocation, auditInfo); } } // 同步方法的处理逻辑与 MVC 过滤器逻辑类似 private void PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); try { invocation.Proceed(); } catch (Exception ex) { auditInfo.Exception = ex; throw; } finally { stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } } // 异步方法处理 private void PerformAsyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); invocation.Proceed(); if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally( (Task) invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } else //Task<TResult> { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } } private void SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception) { stopwatch.Stop(); auditInfo.Exception = exception; auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); }
这里异步方法的处理在很早以前的工做单元拦截器就有过讲述,这里就再也不重复说明了。
从代码上咱们就能够看到,不管是拦截器仍是过滤器都是最终都是经过 IAuditingHelper
对象来储存审计日志的。Abp 依旧为咱们实现了一个默认的 AuditingHelper
,实现了其接口的全部方法。咱们先查看一下这个接口的定义:
public interface IAuditingHelper { // 判断当前方法是否须要存储审计日志信息 bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false); // 根据参数集合建立一个审计信息,通常用于拦截器 AuditInfo CreateAuditInfo(Type type, MethodInfo method, object[] arguments); // 根据一个参数字典类来建立一个审计信息,通常用于 MVC 过滤器 AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary<string, object> arguments); // 同步保存审计信息 void Save(AuditInfo auditInfo); // 异步保存审计信息 Task SaveAsync(AuditInfo auditInfo); }
咱们来到其默认实现 AuditingHelper
类型,先看一下其内部注入了哪些接口。
public class AuditingHelper : IAuditingHelper, ITransientDependency { // 日志记录器,用于记录日志 public ILogger Logger { get; set; } // 用于获取当前登陆用户的信息 public IAbpSession AbpSession { get; set; } // 用于持久话审计日志信息 public IAuditingStore AuditingStore { get; set; } // 主要做用是填充审计信息的客户端调用信息 private readonly IAuditInfoProvider _auditInfoProvider; // 审计日志组件的配置相关 private readonly IAuditingConfiguration _configuration; // 在调用 AuditingStore 进行持久化的时候使用,建立一个工做单元 private readonly IUnitOfWorkManager _unitOfWorkManager; // 用于序列化参数信息为 JSON 字符串 private readonly IAuditSerializer _auditSerializer; public AuditingHelper( IAuditInfoProvider auditInfoProvider, IAuditingConfiguration configuration, IUnitOfWorkManager unitOfWorkManager, IAuditSerializer auditSerializer) { _auditInfoProvider = auditInfoProvider; _configuration = configuration; _unitOfWorkManager = unitOfWorkManager; _auditSerializer = auditSerializer; AbpSession = NullAbpSession.Instance; Logger = NullLogger.Instance; AuditingStore = SimpleLogAuditingStore.Instance; } // ... 其余实现的接口 }
首先分析一下其内部的 ShouldSaveAudit()
方法,整个方法的核心做用就是根据传入的方法类型来断定是否为其建立审计信息。
其实在这一串 if 当中,你能够发现有一句代码对方法是否标注了 DisableAuditingAttribute
特性进行了判断,若是标注了该特性,则不为该方法建立审计信息。因此咱们就能够经过该特性来控制本身应用服务类,控制里面的的接口是否要建立审计信息。同理,咱们也能够经过显式标注 AuditedAttribute
特性来让拦截器为这个方法建立审计信息。
public bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false) { if (!_configuration.IsEnabled) { return false; } if (!_configuration.IsEnabledForAnonymousUsers && (AbpSession?.UserId == null)) { return false; } if (methodInfo == null) { return false; } if (!methodInfo.IsPublic) { return false; } if (methodInfo.IsDefined(typeof(AuditedAttribute), true)) { return true; } if (methodInfo.IsDefined(typeof(DisableAuditingAttribute), true)) { return false; } var classType = methodInfo.DeclaringType; if (classType != null) { if (classType.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) { return true; } if (classType.GetTypeInfo().IsDefined(typeof(DisableAuditingAttribute), true)) { return false; } if (_configuration.Selectors.Any(selector => selector.Predicate(classType))) { return true; } } return defaultValue; }
审计信息在建立的时候,就为咱们将当前调用接口时的用户信息存放在了审计信息当中,以后经过 IAuditInfoProvider
的 Fill()
方法填充了客户端 IP 与浏览器信息。
public AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary<string, object> arguments) { // 构建一个审计信息对象 var auditInfo = new AuditInfo { TenantId = AbpSession.TenantId, UserId = AbpSession.UserId, ImpersonatorUserId = AbpSession.ImpersonatorUserId, ImpersonatorTenantId = AbpSession.ImpersonatorTenantId, ServiceName = type != null ? type.FullName : "", MethodName = method.Name, // 将参数转换为 JSON 字符串 Parameters = ConvertArgumentsToJson(arguments), ExecutionTime = Clock.Now }; try { // 填充客户 IP 与浏览器信息等 _auditInfoProvider.Fill(auditInfo); } catch (Exception ex) { Logger.Warn(ex.ToString(), ex); } return auditInfo; }
经过上一小节咱们知道了在调用审计信息保存接口的时候,其实是调用的 IAuditingStore
所提供的 SaveAsync(AuditInfo auditInfo)
方法来持久化这些审计日志信息的。
若是你没有集成 Abp.Zero 项目的话,则使用的是默认的实现,就是简单经过 ILogger
输出审计信息到日志当中。
默认有这两种实现,至于第一种是 Abp 的单元测试项目所使用的。
这里咱们就简单将一下 AuditingStore
这个实现吧,其实很简单的,就是注入了一个仓储,在保存的时候往审计日志表插入一条数据便可。
这里使用了 AuditLog.CreateFromAuditInfo()
方法将 AuditInfo
类型的审计信息转换为数据库实体,用于仓储进行插入操做。
public class AuditingStore : IAuditingStore, ITransientDependency { private readonly IRepository<AuditLog, long> _auditLogRepository; public AuditingStore(IRepository<AuditLog, long> auditLogRepository) { _auditLogRepository = auditLogRepository; } public virtual Task SaveAsync(AuditInfo auditInfo) { // 向表中插入数据 return _auditLogRepository.InsertAsync(AuditLog.CreateFromAuditInfo(auditInfo)); } }
一样,这里建议从新实现一个 AuditingStore
,存储在 Redis 或者其余地方。
前几天发现 Abp 的团队有开了一个新坑,叫作 Abp vNext 框架,该框架所有基于 .NET Core 进行开发,并且会针对微服务项目进行专门的设计,有兴趣的朋友能够持续关注。
其 GitHub 地址为:https://github.com/abpframework/abp/
官方地址为:https://abp.io/