一、背景 在實際項目的開發當中,使用 Abp Zero 自帶的審計日誌功能寫入效率比較低。其次審計日誌數據量中後期十分龐大,不適合與業務數據存放在一起。所以我們可以重新實現 Abp 的 介面,來讓我們的審計日誌數據存儲在 MongoDb 當中。 二、實現 2.0 引入相關包 這裡我們需要在模塊項目引 ...
一、背景
在實際項目的開發當中,使用 Abp Zero 自帶的審計日誌功能寫入效率比較低。其次審計日誌數據量中後期十分龐大,不適合與業務數據存放在一起。所以我們可以重新實現 Abp 的 IAuditingStore
介面,來讓我們的審計日誌數據存儲在 MongoDb 當中。
二、實現
2.0 引入相關包
這裡我們需要在模塊項目引入 Abp 與 mongocsharpdriver 包,引入之後項目如下圖。
2.1 實體封裝
基於 Abp 框架的設計,它許多組件都可以隨時被我們所替換。這裡我們先定義存儲到 MongoDb 資料庫的實體,取名叫做 MongoDbAuditEntity
。下麵就是它的基本定義,它是我從 Zero 裡面單獨扒出來的,是基於 Abp 的審計信息定義重新進行封裝的一個實體。
using System;
using System.Linq;
using Abp.Extensions;
using Abp.Runtime.Validation;
using Abp.UI;
namespace Abp.Auditing.MongoDb
{
/// <summary>
/// 審計日誌記錄實體,僅用於 MongoDb 存儲使用。
/// </summary>
public class MongoDbAuditEntity
{
/// <summary>
/// <see cref="ServiceName"/> 屬性的最大長度。
/// </summary>
public static int MaxServiceNameLength = 256;
/// <summary>
/// <see cref="MethodName"/> 屬性的最大長度。
/// </summary>
public static int MaxMethodNameLength = 256;
/// <summary>
/// <see cref="Parameters"/> 屬性的最大長度。
/// </summary>
public static int MaxParametersLength = 1024;
/// <summary>
/// <see cref="ClientIpAddress"/> 屬性的最大長度。
/// </summary>
public static int MaxClientIpAddressLength = 64;
/// <summary>
/// <see cref="ClientName"/> 屬性的最大長度。
/// </summary>
public static int MaxClientNameLength = 128;
/// <summary>
/// <see cref="BrowserInfo"/> 屬性的最大長度。
/// </summary>
public static int MaxBrowserInfoLength = 512;
/// <summary>
/// <see cref="Exception"/> 屬性的最大長度。
/// </summary>
public static int MaxExceptionLength = 2000;
/// <summary>
/// <see cref="CustomData"/> 屬性的最大長度。
/// </summary>
public static int MaxCustomDataLength = 2000;
/// <summary>
/// 調用介面時用戶的編碼,如果是匿名訪問,則可能為 null。
/// </summary>
public string UserCode { get; set; }
/// <summary>
/// 調用介面時用戶的集團 Id,如果是匿名訪問,則可能為 null。
/// </summary>
public int? GroupId { get; set; }
/// <summary>
/// 調用介面時,請求的應用服務/控制器名稱。
/// </summary>
public string ServiceName { get; set; }
/// <summary>
/// 調用介面時,請求的的具體方法/介面名稱。
/// </summary>
public string MethodName { get; set; }
/// <summary>
/// 調用介面時,傳遞的具體參數。
/// </summary>
public string Parameters { get; set; }
/// <summary>
/// 調用介面的時間,以伺服器的時間進行記錄。
/// </summary>
public DateTime ExecutionTime { get; set; }
/// <summary>
/// 調用介面執行方法時所消耗的時間,以毫秒為單位。
/// </summary>
public int ExecutionDuration { get; set; }
/// <summary>
/// 調用介面時客戶端的 IP 地址。
/// </summary>
public string ClientIpAddress { get; set; }
/// <summary>
/// 調用介面時客戶端的名稱(通常為電腦名)。
/// </summary>
public string ClientName { get; set; }
/// <summary>
/// 調用介面的瀏覽器信息。
/// </summary>
public string BrowserInfo { get; set; }
/// <summary>
/// 調用介面時如果產生了異常,則記錄在本欄位,如果沒有異常則可能 null。
/// </summary>
public string Exception { get; set; }
/// <summary>
/// 自定義數據
/// </summary>
public string CustomData { get; set; }
/// <summary>
/// 從給定的 <see cref="auditInfo"/> 審計信息創建一個新的 MongoDb 審計日誌實體
/// (<see cref="MongoDbAuditEntity"/>)。
/// </summary>
/// <param name="auditInfo">原始審計日誌信息。</param>
/// <returns>創建完成的 <see cref="MongoDbAuditEntity"/> 實體對象。</returns>
public static MongoDbAuditEntity CreateFromAuditInfo(AuditInfo auditInfo)
{
var expMsg = GetAbpClearException(auditInfo.Exception);
return new MongoDbAuditEntity
{
UserCode = auditInfo.UserId?.ToString(),
GroupId = null,
ServiceName = auditInfo.ServiceName.TruncateWithPostfix(MaxServiceNameLength),
MethodName = auditInfo.MethodName.TruncateWithPostfix(MaxMethodNameLength),
Parameters = auditInfo.Parameters.TruncateWithPostfix(MaxParametersLength),
ExecutionTime = auditInfo.ExecutionTime,
ExecutionDuration = auditInfo.ExecutionDuration,
ClientIpAddress = auditInfo.ClientIpAddress.TruncateWithPostfix(MaxClientIpAddressLength),
ClientName = auditInfo.ClientName.TruncateWithPostfix(MaxClientNameLength),
BrowserInfo = auditInfo.BrowserInfo.TruncateWithPostfix(MaxBrowserInfoLength),
Exception = expMsg.TruncateWithPostfix(MaxExceptionLength),
CustomData = auditInfo.CustomData.TruncateWithPostfix(MaxCustomDataLength)
};
}
public override string ToString()
{
return string.Format(
"審計日誌: {0}.{1} 由用戶 {2} 執行,花費了 {3} 毫秒,請求的源 IP 地址為: {4} 。",
ServiceName, MethodName, UserCode, ExecutionDuration, ClientIpAddress
);
}
/// <summary>
/// 創建更加清楚明確的異常信息。
/// </summary>
/// <param name="exception">要處理的異常數據。</param>
private static string GetAbpClearException(Exception exception)
{
var clearMessage = "";
switch (exception)
{
case null:
return null;
case AbpValidationException abpValidationException:
clearMessage = "異常為參數驗證錯誤,一共有 " + abpValidationException.ValidationErrors.Count + "個錯誤:";
foreach (var validationResult in abpValidationException.ValidationErrors)
{
var memberNames = "";
if (validationResult.MemberNames != null && validationResult.MemberNames.Any())
{
memberNames = " (" + string.Join(", ", validationResult.MemberNames) + ")";
}
clearMessage += "\r\n" + validationResult.ErrorMessage + memberNames;
}
break;
case UserFriendlyException userFriendlyException:
clearMessage =
$"業務相關錯誤,錯誤代碼: {userFriendlyException.Code} \r\n 異常詳細信息: {userFriendlyException.Details}";
break;
}
return exception + (string.IsNullOrEmpty(clearMessage) ? "" : "\r\n\r\n" + clearMessage);
}
}
}
2.2 編寫 MongoDb 配置類
一般來說,我們編寫一個 Abp 模塊肯定是需要構建一個配置類的,以便其他開發人員在使用我們的模塊可以進行一些自定義配置。這裡我們的 MongoDb 審計日誌模塊無非就是需要配置兩個信息,第一個就是 MongoDb 資料庫的連接字元串,第二個就是要存儲的庫名稱。
/// <summary>
/// 審計日誌的 MongoDb 存儲模塊。
/// </summary>
public interface IAuditingMongoDbConfiguration
{
/// <summary>
/// MongoDb 連接字元串。
/// </summary>
string ConnectionString { get; set; }
/// <summary>
/// 要連接的 MongoDb 資料庫名稱
/// </summary>
string DataBaseName { get; set; }
}
同理,再編寫一個實現。
public class AuditingMongoDbConfiguration : IAuditingMongoDbConfiguration
{
public string ConnectionString { get; set; }
public string DataBaseName { get; set; }
}
2.3 編寫 IMongoClient 的工廠類
其實你直接 new
也可以,這裡編寫一個工廠類是省去一些構建流程而已,首先為工廠類定義一個介面,該介面只有一個方法,就是創建 IMongoClient
的實例對象。
public interface IMongoClientFactory
{
IMongoClient Create();
}
這個工廠的實現也很簡單,只不過我們在工廠當中註入了 IAuditingMongoDbConfiguration
,方便我們創建實例。
public class MongoClientFactory : IMongoClientFactory
{
private readonly IAuditingMongoDbConfiguration _mongoDbConfiguration;
public MongoClientFactory(IAuditingMongoDbConfiguration mongoDbConfiguration)
{
_mongoDbConfiguration = mongoDbConfiguration;
}
public IMongoClient Create()
{
return new MongoClient(_mongoDbConfiguration.ConnectionString);
}
}
2.4 審計日誌的具體存儲動作
上面幾點都是做一些準備工作,下麵我們需要實現 IAuditingStore
介面,以便將我們的審計日誌存儲在 MongoDb 資料庫當中。IAuditingStore
介面只定義了一個方法,就是 SaveAsync(AuditInfo auditInfo)
方法。該方法是在每次介面請求的時候,通過過濾器/攔截器的時候會被調用。當然整個審計日誌的構成不是這麼簡單的,如果大家有興趣可以查看我的另一篇博客 《[Abp 源碼分析] 十五、自動審計記錄》 ,在這篇博客有詳細講述審計日誌的相關知識。
我們接著繼續,因為 SaveAsync(AuditInfo auditInfo)
方法傳入了一個 AuditInfo
對象,我們就可以基於這個對象來構造我們的數據實體。構造完成之後,將其通過 IMongoClient
對象存儲到 MongoDb 資料庫當中。
/// <summary>
/// <see cref="IAuditingStore"/> 的特殊實現,使用的是 MongoDb 作為持久化存儲。
/// </summary>
public class MongoDbAuditingStore : IAuditingStore
{
private readonly IMongoClientFactory _clientFactory;
private readonly IAuditingMongoDbConfiguration _mongoDbConfiguration;
public MongoDbAuditingStore(IMongoClientFactory clientFactory, IAuditingMongoDbConfiguration mongoDbConfiguration)
{
_clientFactory = clientFactory;
_mongoDbConfiguration = mongoDbConfiguration;
}
public async Task SaveAsync(AuditInfo auditInfo)
{
var entity = MongoDbAuditEntity.CreateFromAuditInfo(auditInfo);
await _clientFactory.Create()
.GetDatabase(_mongoDbConfiguration.DataBaseName)
.GetCollection<MongoDbAuditEntity>(typeof(MongoDbAuditEntity).Name)
.InsertOneAsync(entity);
}
}
可以看到整體代碼還是十分簡單的,直接通過 auditInfo 對象構造好數據實體之後,插入到 MongoDb 資料庫當中。
2.5 編寫模塊類
每一個基於 Abp 的第三方模塊都會有一個模塊類,模塊類的主要作用就是針對於第三方模塊進行一些基本配置,以及對一些組件的替換動作。
using Abp.Auditing.MongoDb.Configuration;
using Abp.Auditing.MongoDb.Infrastructure;
using Abp.Dependency;
using Abp.Modules;
namespace Abp.Auditing.MongoDb
{
[DependsOn(typeof(AbpKernelModule))]
public class AbpAuditingMongoDbModule : AbpModule
{
public override void PreInitialize()
{
IocManager.Register<IAuditingMongoDbConfiguration,AuditingMongoDbConfiguration>();
IocManager.Register<IMongoClientFactory,MongoClientFactory>();
// 替換自帶的審計日誌存儲實現
Configuration.ReplaceService(typeof(IAuditingStore),() =>
{
IocManager.Register<IAuditingStore, MongoDbAuditingStore>(DependencyLifeStyle.Transient);
});
}
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(typeof(AbpAuditingMongoDbModule).Assembly);
}
}
}
2.6 編寫集成的擴展方法
Abp 模塊都會基於 IModuleConfigurations
介面編寫一個擴展方法,這樣其他基於 Abp 框架的項目開發人員就可以很方便地在其啟動模塊的 PreInitialzie()
方法當中通過 Configuration.Modules
來進行配置。
/// <summary>
/// MongoDb 審計日誌存儲提供器的配置類的擴展方法。
/// </summary>
public static class AuditingMongoDbConfigurationExtensions
{
/// <summary>
/// 配置審計日誌的 MongoDb 實現的相關參數。
/// </summary>
/// <param name="modules">模塊配置類</param>
/// <param name="connectString">MongoDb 連接字元串。</param>
/// <param name="dataBaseName">要操作的 MongoDb 資料庫。</param>
public static void ConfigureMongoDbAuditingStore(this IModuleConfigurations modules,string connectString,string dataBaseName)
{
var configuration = modules.AbpConfiguration.Get<IAuditingMongoDbConfiguration>();
configuration.ConnectionString = connectString;
configuration.DataBaseName = dataBaseName;
}
}
三、測試
新建一個項目,並添加對我們庫的引用,在其啟動模塊當中添加對 AbpAuditingMongoDbModule
模塊的依賴,在其 PreInitialize()
方法當中加入以下代碼,以配置審計日誌相關功能。
[DependsOn(typeof(AbpAuditingMongoDbModule))]
public class StartupModule : AbpModule
{
public override void PreInitialize()
{
// 其他代碼...
// 開啟審計日誌記錄
Configuration.Auditing.IsEnabled = true;
// 允許記錄匿名用戶請求
Configuration.Auditing.IsEnabledForAnonymousUsers = true;
// 配置 MonggoDb 資料庫地址與名稱
Configuration.Modules.ConfigureMongoDbAuditingStore("mongodb://username:Zpassword@ip:port","TestDataBase");
// 其他代碼...
}
}
啟動項目之後,我們嘗試訪問測試方法,之後來到 MongoDb 資料庫當中,查看具體的審計日誌信息。
可以看到,所有對介面的請求都被記錄到了 MongoDb 當中,這樣後續可以基於這些數據進行二次分析。
四、結語
Abp.Auditing.MongoDb 包 GitHub 地址