天天看點

Abp + MongoDb 改造預設的審計日志存儲位置

一、背景

在實際項目的開發當中,使用 Abp Zero 自帶的審計日志功能寫入效率比較低。其次審計日志資料量中後期十分龐大,不适合與業務資料存放在一起。是以我們可以重新實作 Abp 的

IAuditingStore

接口,來讓我們的審計日志資料存儲在 MongoDb 當中。

二、實作

2.0 引入相關包

這裡我們需要在子產品項目引入 Abp 與 mongocsharpdriver 包,引入之後項目如下圖。

Abp + MongoDb 改造預設的審計日志存儲位置

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 資料庫當中,檢視具體的審計日志資訊。

Abp + MongoDb 改造預設的審計日志存儲位置

可以看到,所有對接口的請求都被記錄到了 MongoDb 當中,這樣後續可以基于這些資料進行二次分析。

四、結語

Abp.Auditing.MongoDb 包下載下傳位址

Abp.Auditing.MongoDb 包 GitHub 位址