APB VNext系列(三):审计日志

本篇博文将详细介绍审计日志模块的具体实现,目前我在公司也将这一模块单独抽出来用在了生产项目,也在这里记录一下实现过程。

1.审计日志拦截器

1.1 审计日志拦截器的注册

首先我们需要实现一个日志拦截器的注册类AuditingInterceptorRegistrar,会在项目初次运行时判定是否为实现类型(ImplementationType) 注入审计日志拦截器:

public static class AuditingInterceptorRegistrar
    {
        public static void RegisterIfNeeded(OnServiceRegisteredContext context)
        {
            if (ShouldIntercept(context.ImplementationType))
            {
                context.Interceptors.TryAdd<AuditingInterceptor>();
            }
        }

        private static bool ShouldIntercept(Type type)
        {
            if (ShouldAuditTypeByDefault(type))
            {
                return true;
            }

            // 如果类型的任意方法启用了 Auditied 特性,则应用拦截器
            if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
            {
                return true;
            }

            return false;
        }

        public static bool ShouldAuditTypeByDefault(Type type)
        {
            // 判断类型是否使用了 Audited 特性,使用了则应用审计日志拦截器
            if (type.IsDefined(typeof(AuditedAttribute), true))
            {
                return true;
            }

            // 判断类型是否使用了 DisableAuditing 特性,使用了则不关联拦截器
            if (type.IsDefined(typeof(DisableAuditingAttribute), true))
            {
                return false;
            }

            // 如果类型实现了 IAuditingEnabled 接口,则启用拦截器
            if (typeof(IAuditingEnabled).IsAssignableFrom(type))
            {
                return true;
            }

            return false;
        }
    }

可以看到具体实现中会结合三种类型进行判断。分别是 AuditedAttribute 、IAuditingEnabledDisableAuditingAttribute 。

前两个作用是,只要类型标注了 AuditedAttribute 特性,或者是实现了 IAuditingEnable 接口,都会为该类型注入审计日志拦截器。

而 DisableAuditingAttribute 类型则相反,只要类型上标注了该特性,就不会启用审计日志拦截器。某些接口需要 提升性能 的话,可以尝试使用该特性禁用掉审计日志功能。

我们需要在Startup.cs的ConfigureServices方法中调用:

services.OnRegistered(AuditingInterceptorRegistrar.RegisterIfNeeded);

我们看到这里调用了一个扩展方法OnRegistered,具体实现如下:

    public static class ServiceCollectionRegistrationActionExtensions
    {
        public static void OnRegistered(this IServiceCollection services, Action<OnServiceRegisteredContext> registrationAction)
        {
            GetOrCreateRegistrationActionList(services).Add(registrationAction);
        }

        public static ServiceRegistrationActionList GetRegistrationActionList(this IServiceCollection services)
        {
            return GetOrCreateRegistrationActionList(services);
        }

        private static ServiceRegistrationActionList GetOrCreateRegistrationActionList(IServiceCollection services)
        {
            var actionList = services.GetSingletonInstanceOrNull<IObjectAccessor<ServiceRegistrationActionList>>()?.Value;
            if (actionList == null)
            {
                actionList = new ServiceRegistrationActionList();
                services.AddObjectAccessor(actionList);
            }

            return actionList;
        }
    }

可以看到扩展方法的入参时一个带参数的委托,传进来的委托方法最终会保存到一个委托集合里:ServiceRegistrationActionList,那这个集合时在什么时候被用到呢?

在初次加载的时候,会遍历所有的程序集类、接口,同时调用GetRegistrationActionList方法拿到所有的拦截器注册类,程序会遍历每一个(类、接口)执行拦截器的注册方法,判断时候需要绑定拦截器,

如果需要的话,就会为该类型绑定,其类型与拦截器的对应关系记录在OnServiceRegisteredContext类中,下面是具体实现。

首先我们需要需要实现一个扩展方法UseAutofac,这个扩展方法的核心是AegisAutofacServiceProviderFactory类:

    public static class AegisAutofacHostBuilderExtensions
    {
        public static IHostBuilder UseAutofac(this IHostBuilder hostBuilder)
        {
            return hostBuilder.UseServiceProviderFactory(new AegisAutofacServiceProviderFactory(new ContainerBuilder()));
        }
    }

在Program中对其进行调用:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                }).UseAutofac();
    }

AegisAutofacServiceProviderFactory类:

    /// <summary>
    ///     A factory for creating a <see cref="T:Autofac.ContainerBuilder" /> and an <see cref="T:System.IServiceProvider" />.
    /// </summary>
    public class AegisAutofacServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
    {
        private readonly ContainerBuilder _builder;

        public AegisAutofacServiceProviderFactory(ContainerBuilder builder)
        {
            _builder = builder;
        }

        public ContainerBuilder CreateBuilder(IServiceCollection services)
        {
            _builder.AegisPopulate(services);

            return _builder;
        }

        public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
        {
            return new AutofacServiceProvider(containerBuilder.Build());
        }
    }

我们重点看一下AegisPopulate方法的具体实现,因为这里面实现了拦截器注册类方法的调用逻辑:

public static class AegisAutofacRegistration
    {
        public static void AegisPopulate(this ContainerBuilder builder, IServiceCollection services)
        {
            builder.RegisterType<AutofacServiceProvider>().As<IServiceProvider>().ExternallyOwned();
            builder.RegisterType<AutofacServiceScopeFactory>().As<IServiceScopeFactory>();

            Register(builder, services);
        }

        /// <summary>
        ///     Configures the lifecycle on a service registration.
        /// </summary>
        /// <typeparam name="TActivatorData">The activator data type.</typeparam>
        /// <typeparam name="TRegistrationStyle">The object registration style.</typeparam>
        /// <param name="registrationBuilder">The registration being built.</param>
        /// <param name="lifecycleKind">The lifecycle specified on the service registration.</param>
        /// <returns>
        /// The <paramref name="registrationBuilder" />, configured with the proper lifetime scope,
        /// and available for additional configuration.
        /// </returns>
        private static IRegistrationBuilder<object, TActivatorData, TRegistrationStyle> ConfigureLifecycle<TActivatorData, TRegistrationStyle>(
            this IRegistrationBuilder<object, TActivatorData, TRegistrationStyle> registrationBuilder,
            ServiceLifetime lifecycleKind)
        {
            switch (lifecycleKind)
            {
                case ServiceLifetime.Singleton:
                    registrationBuilder.SingleInstance();
                    break;
                case ServiceLifetime.Scoped:
                    registrationBuilder.InstancePerLifetimeScope();
                    break;
                case ServiceLifetime.Transient:
                    registrationBuilder.InstancePerDependency();
                    break;
            }

            return registrationBuilder;
        }

        /// <summary>
        /// Populates the Autofac container builder with the set of registered service descriptors.
        /// </summary>
        /// <param name="builder">
        /// The <see cref="ContainerBuilder"/> into which the registrations should be made.
        /// </param>
        /// <param name="services">
        /// The set of service descriptors to register in the container.
        /// </param>
        private static void Register(
                ContainerBuilder builder,
                IServiceCollection services)
        {
            var registrationActionList = services.GetRegistrationActionList();
            foreach (var service in services)
            {
                if (service.ImplementationType != null)
                {
                    // Test if the an open generic type is being registered
                    var serviceTypeInfo = service.ServiceType.GetTypeInfo();
                    if (serviceTypeInfo.IsGenericTypeDefinition)
                    {
                        builder
                            .RegisterGeneric(service.ImplementationType)
                            .As(service.ServiceType)
                            .ConfigureLifecycle(service.Lifetime)
                            .ConfigureAegisConventions(registrationActionList);
                    }
                    else
                    {
                        builder
                            .RegisterType(service.ImplementationType)
                            .As(service.ServiceType)
                            .ConfigureLifecycle(service.Lifetime)
                            .ConfigureAegisConventions(registrationActionList);
                    }
                }
                else if (service.ImplementationFactory != null)
                {
                    var registration = RegistrationBuilder.ForDelegate(service.ServiceType, (context, parameters) =>
                    {
                        var serviceProvider = context.Resolve<IServiceProvider>();
                        return service.ImplementationFactory(serviceProvider);
                    })
                    .ConfigureLifecycle(service.Lifetime)
                    .CreateRegistration();
                    //TODO: ConfigureAegisConventions ?

                    builder.RegisterComponent(registration);
                }
                else
                {
                    builder
                        .RegisterInstance(service.ImplementationInstance)
                        .As(service.ServiceType)
                        .ConfigureLifecycle(service.Lifetime);
                }
            }
        }
    }

可以看到Register方法里调用了GetRegistrationActionList方法,拿到了所有的拦截器注册类委托,并且循环遍历所有的类型去执行注册类方法,来判断当前类型(ServiceType)是不是需要绑定拦截器,

ConfigureAegisConventions方法里实现了这一逻辑:

public static class RegistrationBuilderExtensions
    {
        public static IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> ConfigureAegisConventions<TLimit, TActivatorData, TRegistrationStyle>(
            this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registrationBuilder,
            ServiceRegistrationActionList registrationActionList)
            where TActivatorData : ReflectionActivatorData
        {
            var serviceType = registrationBuilder.RegistrationData.Services.OfType<IServiceWithType>().FirstOrDefault()?.ServiceType;
            if (serviceType == null)
            {
                return registrationBuilder;
            }

            var implementationType = registrationBuilder.ActivatorData.ImplementationType;
            if (implementationType == null)
            {
                return registrationBuilder;
            }

            registrationBuilder = registrationBuilder.InvokeRegistrationActions(registrationActionList, serviceType, implementationType);

            return registrationBuilder;
        }

        private static IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> InvokeRegistrationActions<TLimit, TActivatorData, TRegistrationStyle>(this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registrationBuilder, ServiceRegistrationActionList registrationActionList, Type serviceType, Type implementationType)
            where TActivatorData : ReflectionActivatorData
        {
            var serviceRegisteredContext = new OnServiceRegisteredContext(serviceType, implementationType);
            foreach (var registrationAction in registrationActionList)
            {
                registrationAction.Invoke(serviceRegisteredContext);
            }

            if (serviceRegisteredContext.Interceptors.Count > 0)
            {
                registrationBuilder = registrationBuilder.AddInterceptors(
                    serviceType,
                    serviceRegisteredContext.Interceptors
                );
            }

            return registrationBuilder;
        }

        private static IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> AddInterceptors<TLimit, TActivatorData, TRegistrationStyle>(
            this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registrationBuilder,
            Type serviceType,
            IEnumerable<Type> interceptors)
            where TActivatorData : ReflectionActivatorData
        {
            if (serviceType.IsInterface)
            {
                registrationBuilder = registrationBuilder.EnableInterfaceInterceptors();
            }
            else
            {
                (registrationBuilder as IRegistrationBuilder<TLimit, ConcreteReflectionActivatorData, TRegistrationStyle>)?.EnableClassInterceptors();
            }

            foreach (var interceptor in interceptors)
            {
                registrationBuilder.InterceptedBy(
                    typeof(AsyncDeterminationInterceptor<>).MakeGenericType(interceptor)
                );
            }

            return registrationBuilder;
        }
    }

首先ConfigureAegisConventions方法会获取到传入的ServiceType具体类或者接口,然后拿到对应的实现类,传入到InvokeRegistrationActions方法。

InvokeRegistrationActions方法会首先构建一个OnServiceRegisteredContext类对象,记录了ServiceType和实现implementationType,同时包含一个Interceptors,即ServiceType绑定的拦截器,初次创建,Interceptors会是空集合:

    public class OnServiceRegisteredContext
    {
        public virtual ITypeList<IShippingInterceptor> Interceptors { get; }

        public virtual Type ServiceType { get; }

        public virtual Type ImplementationType { get; }

        public OnServiceRegisteredContext(Type serviceType, [NotNull] Type implementationType)
        {
            ServiceType = Check.NotNull(serviceType, nameof(serviceType));
            ImplementationType = Check.NotNull(implementationType, nameof(implementationType));

            Interceptors = new TypeList<IShippingInterceptor>();
        }
    }

创建serviceRegisteredContext对象后,会开始遍历所有拦截器注册类,通过反射的方式方式执行,比如我们最开始注册的AuditingInterceptorRegistrar.RegisterIfNeeded方法会在这里调用

public static void RegisterIfNeeded(OnServiceRegisteredContext context)
        {
            if (ShouldIntercept(context.ImplementationType))
            {
                context.Interceptors.TryAdd<AuditingInterceptor>();
            }
        }

如果满足判断条件,比如当前ServiceType(类、接口)打上了AuditedAttribute标签,那么刚刚创建的serviceRegisteredContext对象的Interceptors集合会被添加当前的拦截器。

            if (serviceRegisteredContext.Interceptors.Count > 0)
            {
                registrationBuilder = registrationBuilder.AddInterceptors(
                    serviceType,
                    serviceRegisteredContext.Interceptors
                );
            }

当Interceptors集合添加拦截器后会执行AddInterceptors扩展方法开启拦截器,对于接口和类分别调用了EnableInterfaceInterceptors和EnableClassInterceptors方法:

        private static IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> AddInterceptors<TLimit, TActivatorData, TRegistrationStyle>(
            this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registrationBuilder,
            Type serviceType,
            IEnumerable<Type> interceptors)
            where TActivatorData : ReflectionActivatorData
        {
            if (serviceType.IsInterface)
            {
                registrationBuilder = registrationBuilder.EnableInterfaceInterceptors();
            }
            else
            {
                (registrationBuilder as IRegistrationBuilder<TLimit, ConcreteReflectionActivatorData, TRegistrationStyle>)?.EnableClassInterceptors();
            }

            foreach (var interceptor in interceptors)
            {
                registrationBuilder.InterceptedBy(
                    typeof(AsyncDeterminationInterceptor<>).MakeGenericType(interceptor)
                );
            }

            return registrationBuilder;
        }

 

到这里我们的拦截器已经成功绑定到对于的类型上了,比如我在IAuditTest的接口打上AuditedAttribute标签,那么就会为该类型绑定AuditingInterceptor拦截器。

审计日志拦截器创建好之后其实就已经生效了,不过拦截器的逻辑并不是直接去记录日志,而是记录了了请求方法相关的信息,包括请求的路由、入参出参等等。

那么实际日志是如何记录的呢?答案是中间件,这里还需要用到另外三个模块:AegisAuditingMiddleware、AuditingManager、AuditingHelper。

 首先是AegisAuditingMiddleware中间件用来对每一个请求判断是否要记录日志:

public class AegisAuditingMiddleware : IMiddleware
    {
        private readonly IAuditingManager _auditingManager;
        protected AuditingOptions Options { get; }

        public AegisAuditingMiddleware(
            IAuditingManager auditingManager,
            IOptions<AuditingOptions> options)
        {
            _auditingManager = auditingManager;

            Options = options.Value;
        }

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            if (!ShouldWriteAuditLog())
            {
                await next(context);
                return;
            }

            using (var scope = _auditingManager.BeginScope())
            {
                try
                {
                    await next(context);
                }
                finally
                {
                    await scope.SaveAsync();
                }
            }
        }

        private bool ShouldWriteAuditLog()
        {
            if (!Options.IsEnabled)
            {
                return false;
            }

            return true;
        }
    }

内部调用了AuditingManager的SaveAsync方法,方法内部首先会记录请求的耗时,然后会获取我们拦截器内部记录的方法数据(Action),如果当前请求时要被记录的,就会对审计信息进行持久化存储:

protected virtual void BeforeSave(DisposableSaveHandle saveHandle)
        {
            saveHandle.StopWatch.Stop();
            saveHandle.AuditLog.ExecutionDuration = Convert.ToInt32(saveHandle.StopWatch.Elapsed.TotalMilliseconds);
            ExecutePostContributors(saveHandle.AuditLog);
        }

        protected virtual async Task SaveAsync(DisposableSaveHandle saveHandle)
        {
            BeforeSave(saveHandle);

            if (ShouldSave(saveHandle.AuditLog))
            {
                await _auditingStore.SaveAsync(saveHandle.AuditLog);
            }
        }

        protected bool ShouldSave(AuditLogInfo auditLog)
        {
            if (!auditLog.Actions.Any())
            {
                return false;
            }

            return true;
        }

审计日志的持久化存储逻辑时会把要存储的数据放到一个内存集合里,每隔指定时间或者当满足于我们设置的集合大小时就会触发记录操作,这里相比原Abp vNext实际做了异步批量操作的优化:

 public InMemoryTransmitterService(AuditLogBuffer buffer)
        {
            _buffer = buffer;
            _buffer.OnFull = OnBufferFull;

            // Starting the Runner
            Task.Factory.StartNew(Runner, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)
                .ContinueWith(task => { }, TaskContinuationOptions.OnlyOnFaulted);
        }

        /// <summary>
        /// Happens when the in-memory buffer is full. Flushes the in-memory buffer and sends the telemetry items.
        /// </summary>
        private void OnBufferFull()
        {
            _startRunnerEvent.Set();
        }

        /// <summary>
        /// Flushes the in-memory buffer and sends the telemetry items in <see cref="SendingInterval"/> intervals or when 
        /// <see cref="_startRunnerEvent" /> is set.
        /// </summary>
        private void Runner()
        {
            using (_startRunnerEvent = new AutoResetEvent(false))
            {
                while (_enabled)
                {
                    // Pulling all items from the buffer and sending as one transmission.
                    DequeueAndSend(timeout: default); // when default(TimeSpan) is provided, value is ignored and default timeout of 100 sec is used

                    // Waiting for the flush delay to elapse
                    _startRunnerEvent.WaitOne(SendingInterval);
                }
            }
        }

        /// <summary>
        /// Flushes the in-memory buffer and send it.
        /// </summary>
        private void DequeueAndSend(TimeSpan timeout)
        {
            lock (_sendingLockObj)
            {
                IEnumerable<AuditLog> telemetryItems = _buffer.Dequeue();
                try
                {
                    // send request
                    Send(telemetryItems, timeout).Wait();
                }
                catch
                {
                    //ignore
                }
            }
        }

        /// <summary>
        /// Serializes a list of telemetry items and sends them.
        /// </summary>
        private async Task Send(IEnumerable<AuditLog> auditItems, TimeSpan timeout)
        {
            if (auditItems == null)
                return;

            var data = JsonConvert.SerializeObject(auditItems);

            Console.WriteLine(data);
        }

到此就是整个审计日志记录的核心流程,写的比较粗糙,后面有时间再进行完善。

 

posted @ 2021-09-26 17:44  名字都被注册了  阅读(659)  评论(0编辑  收藏  举报