天天看點

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

一、簡要說明

ABP vNext 當中的審計子產品早在 依賴注入與攔截器一文中有所提及,但沒有詳細的對其進行分析。

審計子產品是 ABP vNext 架構的一個基本元件,它能夠提供一些實用日志記錄。不過這裡的日志不是說系統日志,而是說接口每次調用之後的執行情況(執行時間、傳入參數、異常資訊、請求 IP)。

除了正常的日志功能以外,關于 實體 和 聚合 的審計字段接口也是存放在審計子產品當中的。(建立人、建立時間、修改人、修改時間、删除人、删除時間)

二、源碼分析

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

跳轉到具體的實作,可以看到内部會結合三種類型進行判斷。分别是

AuditedAttribute

IAuditingEnabled

DisableAuditingAttribute

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

AuditedAttribute

特性,或者是實作了

IAuditingEnable

接口,都會為該類型注入審計日志攔截器。

DisableAuditingAttribute

類型則相反,隻要類型上标注了該特性,就不會啟用審計日志攔截器。某些接口需要 提升性能 的話,可以嘗試使用該特性禁用掉審計日志功能。

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. 根據調用資訊,建立

    AuditLogInfo

    AuditLogActionInfo

    審計日志資訊。
  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 中間件。

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

在這個中間件内部的實作比較簡單,首先通過一個判定方法,決定是否為本次請求執行

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 會自動維護實作的接口字段,不需要開發人員自己再進行處理。

這些接口包括建立實體操作的相關資訊

IHasCreationTime

IMayHaveCreator

ICreationAuditedObject

以及删除實體時,需要記錄的相關資訊接口

IHasDeletionTime

IDeletionAuditedObject

等。除了審計日志子產品定義的類型以外,在 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 方法大同小異,那我們看一下有哪些地方使用到了上述三個方法。

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

可以看到使用者就包含有 EF Core 子產品和 MongoDB 子產品,這裡我以 EF Core 子產品為例,猜測應該是傳入了實體對象過來。

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

果不其然...檢視這個方法的調用鍊,發現是 DbContext 每次進行

SaveChanges/SaveChangesAsync

的時候,就會對實體進行審計字段自動指派操作。

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

三、總結

審計日志是 ABP vNext 為我們提供的一個可選元件,當開啟審計日志功能後,我們可以根據審計日志資訊快速定位問題。但審計日志的開啟,也會較大的影響性能,因為每次請求都會建立審計日志資訊,之後再進行持久化。是以在使用審計日志功能時,可以結合

DisableAuditingAttribute

特性和

IAuditingManager.BeginScope()

,按需開啟審計日志功能。

四、點選我跳轉到文章目錄