Abp 審計模塊源碼解讀
Abp 框架為我們自帶了審計日志功能,審計日志可以方便地查看每次請求接口所耗的時間,能夠幫助我們快速定位到某些性能有問題的接口。除此之外,審計日志信息還包含有每次調用接口時客戶端請求的參數信息,客戶端的 IP 與客戶端使用的瀏覽器。有了這些數據之后,我們就可以很方便地復現接口產生 BUG 時的一些環境信息。
源碼地址Abp版本:5.1.3
初探
我通過abp腳手架創建了一個Acme.BookStore項目在BookStoreWebModule類使用了
app.UseAuditing()
拓展方法。
我們通過F12可以看到AbpApplicationBuilderExtensions中間件拓展類源碼地址如下代碼AbpAuditingMiddleware中間件。
public static IApplicationBuilder UseAuditing(this IApplicationBuilder app)
{
return app
.UseMiddleware<AbpAuditingMiddleware>();
}
我們繼續查看AbpAuditingMiddleware中間件源碼源碼地址下面我把代碼貼上來一一解釋(先從小方法解釋)
- 請求過濾(因為不是所以方法我們都需要記錄,比如用戶登錄/用戶支付)
// 判斷當前請求路徑是否需要過濾
private bool IsIgnoredUrl(HttpContext context)
{
// AspNetCoreAuditingOptions.IgnoredUrls是abp維護了一個過濾URL的一個容器
return context.Request.Path.Value != null &&
AspNetCoreAuditingOptions.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x));
}
- 是否保存審計日志
private bool ShouldWriteAuditLog(HttpContext httpContext, bool hasError)
{
// 是否記錄報錯的審計日志
if (AuditingOptions.AlwaysLogOnException && hasError)
{
return true;
}
// 是否記錄未登錄產生的審計日志
if (!AuditingOptions.IsEnabledForAnonymousUsers && !CurrentUser.IsAuthenticated)
{
return false;
}
// 是否記錄get請求產生的審計日志
if (!AuditingOptions.IsEnabledForGetRequests &&
string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
- 執行審計模塊中間件
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 判斷審計模塊是否開啟,IsIgnoredUrl就是我們上面說的私有方法了。
if (!AuditingOptions.IsEnabled || IsIgnoredUrl(context))
{
await next(context);
return;
}
// 是否出現報錯
var hasError = false;
// 審計模塊管理
using (var saveHandle = _auditingManager.BeginScope())
{
Debug.Assert(_auditingManager.Current != null);
try
{
await next(context);
// 審計模塊是否有記錄錯誤到日志
if (_auditingManager.Current.Log.Exceptions.Any())
{
hasError = true;
}
}
catch (Exception ex)
{
hasError = true;
// 判斷當前錯誤信息是否已經記錄了
if (!_auditingManager.Current.Log.Exceptions.Contains(ex))
{
_auditingManager.Current.Log.Exceptions.Add(ex);
}
throw;
}
finally
{
// 判斷是否記錄
if (ShouldWriteAuditLog(context, hasError))
{
// 判斷是否有工作單元(這里主要就是防止因為記錄日志信息報錯了,會影響主要的業務流程)
if (UnitOfWorkManager.Current != null)
{
await UnitOfWorkManager.Current.SaveChangesAsync();
}
// 執行保存
await saveHandle.SaveAsync();
}
}
}
}
上面我們主要梳理了審計模塊的中間件邏輯,到這里我們對審計日志的配置會有一些印象了,AuditingOptions
我們需要着重的注意,因為關系到審計模塊一些使用細節。(這里我說說我的看法不管是在學習Abp的那一個模塊,我們都需要知道對於的配置類中,每個屬性的作用以及使用場景。)
深入
我們前面了解到審計模塊的使用方式,為了了解其中的原理我們需要查看源碼
Volo.Abp.Auditing
類庫源碼地址。
AbpAuditingOptions配置類
public class AbpAuditingOptions
{
/// <summary>
/// 隱藏錯誤,默認值:true (沒有看到使用)
/// </summary>
public bool HideErrors { get; set; }
/// <summary>
/// 啟用審計模塊,默認值:true
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// 審計日志的應用程序名稱,默認值:null
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// 是否為匿名請求記錄審計日志,默認值:true
/// </summary>
public bool IsEnabledForAnonymousUsers { get; set; }
/// <summary>
/// 記錄所以報錯,默認值:true(在上面中間件代碼有用到)
/// </summary>
public bool AlwaysLogOnException { get; set; }
/// <summary>
/// 審計日志功能的協作者集合,默認添加了 AspNetCoreAuditLogContributor 實現。
/// </summary>
public List<AuditLogContributor> Contributors { get; }
/// <summary>
/// 默認的忽略類型,主要在序列化時使用。
/// </summary>
public List<Type> IgnoredTypes { get; }
/// <summary>
/// 實體類型選擇器。上下文中SaveChangesAsync有使用到
/// </summary>
public IEntityHistorySelectorList EntityHistorySelectors { get; }
/// <summary>
/// Get請求是否啟用,默認值:false
/// </summary>
public bool IsEnabledForGetRequests { get; set; }
public AbpAuditingOptions()
{
IsEnabled = true;
IsEnabledForAnonymousUsers = true;
HideErrors = true;
AlwaysLogOnException = true;
Contributors = new List<AuditLogContributor>();
IgnoredTypes = new List<Type>
{
typeof(Stream),
typeof(Expression)
};
EntityHistorySelectors = new EntityHistorySelectorList();
}
}
AbpAuditingModule模塊入口
下面代碼即在組件注冊的時候,會調用 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 (DynamicProxyIgnoreTypes.Contains(type))
{
return false;
}
// 是否啟用審計
if (ShouldAuditTypeByDefaultOrNull(type) == true)
{
return true;
}
// 該類型是否存在方法使用了AuditedAttribut特性
if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
{
return true;
}
return false;
}
public static bool? ShouldAuditTypeByDefaultOrNull(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 null;
}
}
AuditingManager審計管理
上面我們講了審計模塊中間件,審計模塊配置,以及特殊過濾配置,接下來我們就要繼續深入到實現細節部分,前面中間件
AuditingManager.BeginScope()
代碼是我們的入口,那就從這里開始下手源碼地址。
從下面的代碼我們可以知道其實就是創建一個DisposableSaveHandle
代理類。(我們需要注意構造參數的值)
- 第一個this主要是將當前對象傳入方法中
- 第二個
ambientScope
重點是_auditingHelper.CreateAuditLogInfo()
創建AuditLogInfo
類(對應Current.log)
- 第三個
Current.log
當前AuditLogInfo信息 - 第四個
Stopwatch.StartNew()
計時器
public IAuditLogSaveHandle BeginScope()
{
// 創建AuditLogInfo類復制到Current.Log中(其實是維護了一個內部的字典)
var ambientScope = _ambientScopeProvider.BeginScope(
AmbientContextKey,
new AuditLogScope(_auditingHelper.CreateAuditLogInfo())
);
return new DisposableSaveHandle(this, ambientScope, Current.Log, Stopwatch.StartNew());
}
_auditingHelper.CreateAuditLogInfo()
從http請求上下文中獲取,當前的url/請求參數/請求瀏覽器/ip.....
// 從http請求上下文中獲取,當前的url/請求參數/請求瀏覽器/ip.....
public virtual AuditLogInfo CreateAuditLogInfo()
{
var auditInfo = new AuditLogInfo
{
ApplicationName = Options.ApplicationName,
TenantId = CurrentTenant.Id,
TenantName = CurrentTenant.Name,
UserId = CurrentUser.Id,
UserName = CurrentUser.UserName,
ClientId = CurrentClient.Id,
CorrelationId = CorrelationIdProvider.Get(),
ExecutionTime = Clock.Now,
ImpersonatorUserId = CurrentUser.FindImpersonatorUserId(),
ImpersonatorUserName = CurrentUser.FindImpersonatorUserName(),
ImpersonatorTenantId = CurrentUser.FindImpersonatorTenantId(),
ImpersonatorTenantName = CurrentUser.FindImpersonatorTenantName(),
};
ExecutePreContributors(auditInfo);
return auditInfo;
}
DisposableSaveHandle
代理類中提供了一個SaveAsync()
方法,調用AuditingManager.SaveAsync()
當然這個SaveAsync()
方法大家還是有一點點印象的吧,畢竟中間件最后完成之后就會調用該方法。
protected class DisposableSaveHandle : IAuditLogSaveHandle
{
public AuditLogInfo AuditLog { get; }
public Stopwatch StopWatch { get; }
private readonly AuditingManager _auditingManager;
private readonly IDisposable _scope;
public DisposableSaveHandle(
AuditingManager auditingManager,
IDisposable scope,
AuditLogInfo auditLog,
Stopwatch stopWatch)
{
_auditingManager = auditingManager;
_scope = scope;
AuditLog = auditLog;
StopWatch = stopWatch;
}
// 包裝AuditingManager.SaveAsync方法
public async Task SaveAsync()
{
await _auditingManager.SaveAsync(this);
}
public void Dispose()
{
_scope.Dispose();
}
}
AuditingManager.SaveAsync()
主要做的事情也主要是組建AuditLogInfo
信息,然后調用SimpleLogAuditingStore.SaveAsync()
,SimpleLogAuditingStore 實現,其內部就是調用 ILogger 將信息輸出。如果需要將審計日志持久化到數據庫,你可以實現 IAUditingStore 接口,覆蓋原有實現 ,或者使用 ABP vNext 提供的 Volo.Abp.AuditLogging 模塊。
protected virtual async Task SaveAsync(DisposableSaveHandle saveHandle)
{
// 獲取審計記錄
BeforeSave(saveHandle);
// 調用AuditingStore.SaveAsync
await _auditingStore.SaveAsync(saveHandle.AuditLog);
}
// 獲取審計記錄
protected virtual void BeforeSave(DisposableSaveHandle saveHandle)
{
saveHandle.StopWatch.Stop();
saveHandle.AuditLog.ExecutionDuration = Convert.ToInt32(saveHandle.StopWatch.Elapsed.TotalMilliseconds);
// 獲取請求返回Response.StatusCode
ExecutePostContributors(saveHandle.AuditLog);
// 獲取實體變化
MergeEntityChanges(saveHandle.AuditLog);
}
// 獲取請求返回Response.StatusCode
protected virtual void ExecutePostContributors(AuditLogInfo auditLogInfo)
{
using (var scope = ServiceProvider.CreateScope())
{
var context = new AuditLogContributionContext(scope.ServiceProvider, auditLogInfo);
foreach (var contributor in Options.Contributors)
{
try
{
contributor.PostContribute(context);
}
catch (Exception ex)
{
Logger.LogException(ex, LogLevel.Warning);
}
}
}
}
總結
首先審計模塊的一些設計思路YYDS,審計模塊的作用顯而易見,但是在使用過程中注意利弊,好處就是方便我們進行錯誤排除,實時監控系統的健康。但是同時也會導致我們接口變慢(畢竟要記錄日志信息),當然還要提到一點就是我們在閱讀源碼的過程中先了解模塊是做什么的,然后了解基礎的配置信息,再然后就是通過代碼入口一層一層剖析就好了。