天天看點

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

 、

IAuditingEnabled

DisableAuditingAttribute

 。

前兩個作用是,隻要類型标注了 

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);
        }      

到此就是整個審計日志記錄的核心流程,寫的比較粗糙,後面有時間再進行完善。

繼續閱讀