[Abp vNext 源碼分析] - 8. 審計日誌

1、簡要說明

ABP vNext 當中的審計模塊早在 依賴注入與攔截器一文中有所說起,但沒有詳細的對其進行分析。html

審計模塊是 ABP vNext 框架的一個基本組件,它可以提供一些實用日誌記錄。不過這裏的日誌不是說系統日誌,而是說接口每次調用以後的執行狀況(執行時間、傳入參數、異常信息、請求 IP)。數據庫

除了常規的日誌功能之外,關於 實體聚合 的審計字段接口也是存放在審計模塊當中的。(建立人建立時間修改人修改時間刪除人刪除時間app

2、源碼分析

2.1. 審計日誌攔截器

2.1.1 審計日誌攔截器的註冊

Volo.Abp.Auditing 的模塊定義十分簡單,主要是提供了 審計日誌攔截器 的註冊功能。下面代碼即在組件註冊的時候,會調用 AuditingInterceptorRegistrar.RegisterIfNeeded 方法來斷定是否爲實現類型(ImplementationType) 注入審計日誌攔截器。框架

public class AbpAuditingModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.OnRegistred(AuditingInterceptorRegistrar.RegisterIfNeeded);
    }
}

跳轉到具體的實現,能夠看到內部會結合三種類型進行判斷。分別是 AuditedAttributeIAuditingEnabledDisableAuditingAttributeasp.net

前兩個做用是,只要類型標註了 AuditedAttribute 特性,或者是實現了 IAuditingEnable 接口,都會爲該類型注入審計日誌攔截器。async

DisableAuditingAttribute 類型則相反,只要類型上標註了該特性,就不會啓用審計日誌攔截器。某些接口須要 提高性能 的話,能夠嘗試使用該特性禁用掉審計日誌功能。ide

public static class AuditingInterceptorRegistrar
{
    public static void RegisterIfNeeded(IOnServiceRegistredContext context)
    {
        // 知足條件時,將會爲該類型注入審計日誌攔截器。
        if (ShouldIntercept(context.ImplementationType))
        {
            context.Interceptors.TryAdd<AuditingInterceptor>();
        }
    }

    private static bool ShouldIntercept(Type type)
    {
        // 首先判斷類型上面是否使用了輔助類型。
        if (ShouldAuditTypeByDefault(type))
        {
            return true;
        }

        // 若是任意方法上面標註了 AuditedAttribute 特性,則仍然爲該類型注入攔截器。
        if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
        {
            return true;
        }

        return false;
    }

    //TODO: Move to a better place
    public static bool ShouldAuditTypeByDefault(Type type)
    {
        // 下面就是根據三種輔助類型進行判斷,是否爲當前 type 注入審計日誌攔截器。
        if (type.IsDefined(typeof(AuditedAttribute), true))
        {
            return true;
        }

        if (type.IsDefined(typeof(DisableAuditingAttribute), true))
        {
            return false;
        }

        if (typeof(IAuditingEnabled).IsAssignableFrom(type))
        {
            return true;
        }

        return false;
    }
}

2.1.2 審計日誌攔截器的實現

審計日誌攔截器的內部實現,主要使用了三個類型進行協同工做。它們分別是負責管理審計日誌信息的 IAuditingManager,負責建立審計日誌信息的 IAuditingHelper,還有統計接口執行時常的 Stopwatch源碼分析

整個審計日誌攔截器的大致流程以下:性能

  1. 首先是斷定 MVC 審計日誌過濾器是否進行處理。
  2. 再次根據特性,和類型進行二次驗證是否應該建立審計日誌信息。
  3. 根據調用信息,建立 AuditLogInfoAuditLogActionInfo 審計日誌信息。
  4. 調用 StopWatch 的計時方法,若是出現了異常則將異常信息添加到剛纔構建的 AuditLogInfo 對象中。
  5. 不管是否出現異常,都會進入 finally 語句塊,這個時候會調用 StopWatch 實例的中止方法,並統計完成執行時間。
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
    if (!ShouldIntercept(invocation, out var auditLog, out var auditLogAction))
    {
        await invocation.ProceedAsync();
        return;
    }

    // 開始進行計時操做。
    var stopwatch = Stopwatch.StartNew();

    try
    {
        await invocation.ProceedAsync();
    }
    catch (Exception ex)
    {
        // 若是出現了異常,同樣的將異常信息添加到審計日誌結果中。
        auditLog.Exceptions.Add(ex);
        throw;
    }
    finally
    {
        // 統計完成,並將信息加入到審計日誌結果中。
        stopwatch.Stop();
        auditLogAction.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
        auditLog.Actions.Add(auditLogAction);
    }
}

能夠看到,只有當 ShouldIntercept() 方法返回 true 的時候,下面的統計等操做纔會被執行。測試

protected virtual bool ShouldIntercept(
    IAbpMethodInvocation invocation, 
    out AuditLogInfo auditLog, 
    out AuditLogActionInfo auditLogAction)
{
    auditLog = null;
    auditLogAction = null;

    if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Auditing))
    {
        return false;
    }

    // 若是沒有獲取到 Scop,則返回 false。
    var auditLogScope = _auditingManager.Current;
    if (auditLogScope == null)
    {
        return false;
    }

    // 進行二次判斷是否須要存儲審計日誌。
    if (!_auditingHelper.ShouldSaveAudit(invocation.Method))
    {
        return false;
    }

    // 構建審計日誌信息。
    auditLog = auditLogScope.Log;
    auditLogAction = _auditingHelper.CreateAuditLogAction(
        auditLog,
        invocation.TargetObject.GetType(),
        invocation.Method, 
        invocation.Arguments
    );

    return true;
}

2.2 審計日誌的持久化

大致流程和咱們上面說的同樣,不過好像缺乏了重要的一步,那就是 持久化操做。你能夠在 Volo.Abp.Auditing 模塊發現有 IAuditingStore 接口的定義,可是它的 SaveAsync() 方法卻沒有在攔截器內部被調用。一樣在 MVC 的審計日誌過濾器實現,你也會發現沒有調用持久化方法。

那麼咱們的審計日誌是在何時被持久化的呢?找到 SaveAsync() 被調用的地方,發現 ABP vNext 實現了一個審計日誌的 ASP.NET Core 中間件。

在這個中間件內部的實現比較簡單,首先經過一個斷定方法,決定是否爲本次請求執行 IAuditingManager.BeginScope() 方法。若是斷定經過,則執行,不然不只行任何操做。

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

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

能夠看到,在這裏 ABP vNext 使用 IAuditingManager 構建,調用其 BeginScope() 構建了一個 IAuditLogSaveHandle 對象,並使用其提供的 SaveAsync() 方法進行持久化操做。

2.2.1 嵌套的持久化操做

在構造出來的 IAuditLogSaveHandle 對象裏面,仍是使用的 IAuditingManager 的默認實現 AuditingManager 所提供的 SaveAsync() 方法進行持久化

閱讀源碼以後,發現了下面兩個問題:

  1. IAuditingManager 沒有將持久化方法公開 出來,而是做爲一個 protected 級別的方法。
  2. 爲何還要藉助 IAuditLogSaveHandle 間接地調用 管理器的持久化方法。

這就要從中間件的代碼提及了,能夠看到它是構造出了一個能夠被釋放的 IAuditLogSaveHandle 對象。ABP vNext 這樣作的目的,就是能夠嵌套多個 Scope,即 只在某個範圍內 纔將審計日誌記錄下來。這種特性相似於 工做單元 的用法,其底層實現是 以前文章 講過的 IAmbientScopeProvider 對象。

例如在某個應用服務內部,我能夠這樣寫代碼:

using (var scope = _auditingManager.BeginScope())
{
    await myAuditedObject1.DoItAsync(new InputObject { Value1 = "我是內部嵌套測試方法1。", Value2 = 5000 });
    using (var scope2 = _auditingManager.BeginScope())
    {
        await myAuditedObject1.DoItAsync(new InputObject {Value1 = "我是內部嵌套測試方法2。", Value2 = 10000});
        await scope2.SaveAsync();
    }
    await scope.SaveAsync();
}

想一下以前的代碼,在攔截器內部,咱們是經過 IAuditingManager.Current 拿到當前可用的 IAuditLogScope ,而這個 Scope 就是在調用 IAuditingManager.BeginScope() 以後生成的

2.2.3 最終的持久化代碼

經過上述的流程,咱們得知最後的審計日誌信息會經過 IAuditingStore 進行持久化。ABP vNext 爲咱們提供了一個默認的 SimpleLogAuditingStore 實現,其內部就是調用 ILogger 將信息輸出。若是須要將審計日誌持久化到數據庫,你能夠實現 IAUditingStore 接口,覆蓋原有實現 ,或者使用 ABP vNext 提供的 Volo.Abp.AuditLogging 模塊。

2.3 審計日誌的序列化

審計日誌的序列化處理是在 IAuditingHelper 的默認實現內部被使用,能夠看到構建審計日誌的方法內部,經過自定義的序列化器來將 Action 的參數進行序列化處理,方便存儲。

public virtual AuditLogActionInfo CreateAuditLogAction(
    AuditLogInfo auditLog,
    Type type, 
    MethodInfo method, 
    IDictionary<string, object> arguments)
{
    var actionInfo = new AuditLogActionInfo
    {
        ServiceName = type != null
            ? type.FullName
            : "",
        MethodName = method.Name,
        // 序列化參數信息。
        Parameters = SerializeConvertArguments(arguments),
        ExecutionTime = Clock.Now
    };

    //TODO Execute contributors

    return actionInfo;
}

protected virtual string SerializeConvertArguments(IDictionary<string, object> arguments)
{
    try
    {
        if (arguments.IsNullOrEmpty())
        {
            return "{}";
        }

        var dictionary = new Dictionary<string, object>();

        foreach (var argument in arguments)
        {
            // 忽略的代碼,主要做用是構建參數字典。
        }

        // 調用序列化器,序列化 Action 的調用參數。
        return AuditSerializer.Serialize(dictionary);
    }
    catch (Exception ex)
    {
        Logger.LogException(ex, LogLevel.Warning);
        return "{}";
    }
}

下面就是具體序列化器的代碼:

public class JsonNetAuditSerializer : IAuditSerializer, ITransientDependency
{
    protected AbpAuditingOptions Options;

    public JsonNetAuditSerializer(IOptions<AbpAuditingOptions> options)
    {
        Options = options.Value;
    }

    public string Serialize(object obj)
    {
        // 使用 JSON.NET 進行序列化操做。
        return JsonConvert.SerializeObject(obj, GetSharedJsonSerializerSettings());
    }

    // ... 省略的代碼。
}

2.4 審計日誌的配置參數

針對審計日誌相關的配置參數的定義,都存放在 AbpAuditingOptions 當中。下面我會針對各個參數的用途,對其進行詳細的說明。

public class AbpAuditingOptions
{
    //TODO: Consider to add an option to disable auditing for application service methods?

    // 該參數目前版本暫未使用,爲保留參數。
    public bool HideErrors { get; set; }

    // 是否啓用審計日誌功能,默認值爲 true。
    public bool IsEnabled { get; set; }

    // 審計日誌的應用程序名稱,默認值爲 null,主要在構建 AuditingInfo 被使用。
    public string ApplicationName { get; set; }

    // 是否爲匿名請求記錄審計日誌默認值 true。
    public bool IsEnabledForAnonymousUsers { get; set; }

    // 審計日誌功能的協做者集合,默認添加了 AspNetCoreAuditLogContributor 實現。
    public List<AuditLogContributor> Contributors { get; }

    // 默認的忽略類型,主要在序列化時使用。
    public List<Type> IgnoredTypes { get; }

    // 實體類型選擇器。
    public IEntityHistorySelectorList EntityHistorySelectors { get; }

    //TODO: Move this to asp.net core layer or convert it to a more dynamic strategy?
    // 是否爲 Get 請求記錄審計日誌,默認值 false。
    public bool IsEnabledForGetRequests { get; set; }

    public AbpAuditingOptions()
    {
        IsEnabled = true;
        IsEnabledForAnonymousUsers = true;
        HideErrors = true;

        Contributors = new List<AuditLogContributor>();

        IgnoredTypes = new List<Type>
        {
            typeof(Stream),
            typeof(Expression)
        };

        EntityHistorySelectors = new EntityHistorySelectorList();
    }
}

2.4 實體相關的審計信息

在文章開始就談到,除了對 HTTP 請求有審計日誌記錄之外,ABP vNext 還提供了實體審計信息的記錄功能。所謂的實體的審計信息,指的就是實體繼承了 ABP vNext 提供的接口以後,ABP vNext 會自動維護實現的接口字段,不須要開發人員本身再進行處理。

這些接口包括建立實體操做的相關信息 IHasCreationTimeIMayHaveCreatorICreationAuditedObject 以及刪除實體時,須要記錄的相關信息接口 IHasDeletionTimeIDeletionAuditedObject 等。除了審計日誌模塊定義的類型之外,在 Volo.Abp.Ddd.Domain 模塊的 Auditing 裏面也有不少審計實體的默認實現。

我在這裏就再也不一一列舉,下面僅快速講解一下 ABP vNext 是如何經過這些接口,實現對審計字段的自動維護的。

在審計日誌模塊的內部,咱們看到一個接口名字叫作 IAuditPropertySetter,它提供了三個方法,分別是:

public interface IAuditPropertySetter
{
    void SetCreationProperties(object targetObject);

    void SetModificationProperties(object targetObject);

    void SetDeletionProperties(object targetObject);
}

因此,這幾個方法就是用於設置建立信息、修改信息、刪除信息的。如今跳轉到默認實現 AuditPropertySetter,隨便找一個 SetCreationTime() 方法。該方法內部首先是判斷傳入的 object 是否實現了 IHasCreationTime 接口,若是實現了對其進行強制類型轉換,而後賦值便可。

private void SetCreationTime(object targetObject)
{
    if (!(targetObject is IHasCreationTime objectWithCreationTime))
    {
        return;
    }

    if (objectWithCreationTime.CreationTime == default)
    {
        objectWithCreationTime.CreationTime = Clock.Now;
    }
}

其餘幾個 Set 方法大同小異,那咱們看一下有哪些地方使用到了上述三個方法。

能夠看到使用者就包含有 EF Core 模塊和 MongoDB 模塊,這裏我以 EF Core 模塊爲例,猜想應該是傳入了實體對象過來。

果不其然...查看這個方法的調用鏈,發現是 DbContext 每次進行 SaveChanges/SaveChangesAsync 的時候,就會對實體進行審計字段自動賦值操做。

3、總結

審計日誌是 ABP vNext 爲咱們提供的一個可選組件,當開啓審計日誌功能後,咱們能夠根據審計日誌信息快速定位問題。但審計日誌的開啓,也會較大的影響性能,由於每次請求都會建立審計日誌信息,以後再進行持久化。所以在使用審計日誌功能時,能夠結合 DisableAuditingAttribute 特性和 IAuditingManager.BeginScope(),按需開啓審計日誌功能。

4、點擊我跳轉到文章目錄

相關文章
相關標籤/搜索