From 44ce226f4d4e6b209b7a440e5cc06994430c09b4 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 4 Feb 2026 10:30:47 +0800 Subject: [PATCH 1/2] feat: use auditlogging queue --- Directory.Packages.props | 1 + aspnet-core/LINGYUN.MicroService.Aspire.sln | 7 + ...GYUN.Abp.AuditLogging.Elasticsearch.csproj | 1 + .../AbpAuditLoggingElasticsearchModule.cs | 6 - .../AbpAuditLoggingElasticsearchOptions.cs | 12 +- .../ElasticsearchAuditLogManager.cs | 89 +------- .../ElasticsearchAuditLogWriter.cs | 114 ++++++++++ .../ElasticsearchSecurityLogManager.cs | 56 +---- .../ElasticsearchSecurityLogWriter.cs | 122 +++++++++++ .../EntityFrameworkCore/AuditLogManager.cs | 53 +---- .../EfCoreAuditLogWriter.cs | 62 ++++++ .../EfCoreSecurityLogWriter.cs | 57 +++++ .../EntityFrameworkCore/SecurityLogManager.cs | 34 +-- .../IP/Location/IPLocationAuditingStore.cs | 7 +- .../IP/Location/IPLocationSecurityLogStore.cs | 8 +- .../LINGYUN.Abp.AuditLogging.csproj | 1 + .../Abp/AuditLogging/AbpAuditLoggingModule.cs | 67 +++++- .../AuditLogging/AbpAuditLoggingOptions.cs | 54 +++++ .../AuditLogInfoToAuditLogConverter.cs | 0 .../LINGYUN/Abp/AuditLogging/AuditLogQueue.cs | 50 +++++ .../Abp/AuditLogging/AuditLoggingQueue.cs | 196 ++++++++++++++++++ .../LINGYUN/Abp/AuditLogging/AuditingStore.cs | 33 ++- .../IAuditLogInfoToAuditLogConverter.cs | 0 .../Abp/AuditLogging/IAuditLogManager.cs | 5 - .../Abp/AuditLogging/IAuditLogQueue.cs | 9 + .../Abp/AuditLogging/IAuditLogWriter.cs | 12 ++ .../Abp/AuditLogging/ISecurityLogManager.cs | 5 - .../Abp/AuditLogging/ISecurityLogQueue.cs | 9 + .../Abp/AuditLogging/ISecurityLogWriter.cs | 12 ++ .../Abp/AuditLogging/LoggerAuditLogWriter.cs | 33 +++ .../AuditLogging/LoggerSecurityLogWriter.cs | 33 +++ .../Abp/AuditLogging/SecurityLogQueue.cs | 50 +++++ .../Abp/AuditLogging/SecurityLogStore.cs | 36 +++- aspnet-core/framework/auditing/README.md | 23 +- .../AbpAuditLoggingElasticsearchTestBase.cs | 6 - .../AbpAuditLoggingElasticsearchTestModule.cs | 6 +- .../Elasticsearch/AuditLogManagerTests.cs | 1 + 37 files changed, 988 insertions(+), 282 deletions(-) create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreSecurityLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingOptions.cs rename aspnet-core/framework/auditing/{LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch => LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging}/AuditLogInfoToAuditLogConverter.cs (100%) create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLogQueue.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLoggingQueue.cs rename aspnet-core/framework/auditing/{LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch => LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging}/IAuditLogInfoToAuditLogConverter.cs (100%) create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogQueue.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogQueue.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerSecurityLogWriter.cs create mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogQueue.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 60258d5f0..23eb7e1f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -359,6 +359,7 @@ + diff --git a/aspnet-core/LINGYUN.MicroService.Aspire.sln b/aspnet-core/LINGYUN.MicroService.Aspire.sln index 643cb28c3..953bd9423 100644 --- a/aspnet-core/LINGYUN.MicroService.Aspire.sln +++ b/aspnet-core/LINGYUN.MicroService.Aspire.sln @@ -925,6 +925,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.MicroService.Ap EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.BackgroundTasks.Quartz", "modules\task-management\LINGYUN.Abp.BackgroundTasks.Quartz\LINGYUN.Abp.BackgroundTasks.Quartz.csproj", "{ABCAB030-29ED-8219-F1DB-39D16098805F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Elasticsearch.Jobs", "modules\task-management\LINGYUN.Abp.Elasticsearch.Jobs\LINGYUN.Abp.Elasticsearch.Jobs.csproj", "{C4F5372D-3127-A2CA-3B22-B343D5E54EB5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2439,6 +2441,10 @@ Global {ABCAB030-29ED-8219-F1DB-39D16098805F}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABCAB030-29ED-8219-F1DB-39D16098805F}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABCAB030-29ED-8219-F1DB-39D16098805F}.Release|Any CPU.Build.0 = Release|Any CPU + {C4F5372D-3127-A2CA-3B22-B343D5E54EB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4F5372D-3127-A2CA-3B22-B343D5E54EB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4F5372D-3127-A2CA-3B22-B343D5E54EB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4F5372D-3127-A2CA-3B22-B343D5E54EB5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2889,6 +2895,7 @@ Global {B856BFF0-8D0E-4C4A-97C7-5406E1B7613C} = {FAB71536-FCDB-4135-B61A-732B72FDA6A6} {410E0FFF-705D-4471-9E52-FF495096A945} = {B856BFF0-8D0E-4C4A-97C7-5406E1B7613C} {ABCAB030-29ED-8219-F1DB-39D16098805F} = {77ED7922-BF30-4436-8A85-78F812583913} + {C4F5372D-3127-A2CA-3B22-B343D5E54EB5} = {77ED7922-BF30-4436-8A85-78F812583913} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN.Abp.AuditLogging.Elasticsearch.csproj b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN.Abp.AuditLogging.Elasticsearch.csproj index 7fad23128..b1b4a2035 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN.Abp.AuditLogging.Elasticsearch.csproj +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN.Abp.AuditLogging.Elasticsearch.csproj @@ -15,6 +15,7 @@ + diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs index e1d02371e..4c31091a2 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs @@ -35,10 +35,4 @@ public class AbpAuditLoggingElasticsearchModule : AbpModule var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(_cancellationTokenSource.Token); } - - public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) - { - _cancellationTokenSource.Cancel(); - return Task.CompletedTask; - } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchOptions.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchOptions.cs index e3976e330..1701762ec 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchOptions.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchOptions.cs @@ -14,18 +14,10 @@ public class AbpAuditLoggingElasticsearchOptions /// public bool ThrowIfIndexInitFailed { get; set; } /// - /// 是否启用审计日志记录 - /// - public bool IsAuditLogEnabled { get; set; } - /// /// 审计日志索引设置 /// public IndexSettings AuditLogSettings { get; set; } /// - /// 是否启用安全日志记录 - /// - public bool IsSecurityLogEnabled { get; set; } - /// /// 安全日志索引设置 /// public IndexSettings SecurityLogSettings { get; set; } @@ -33,8 +25,8 @@ public class AbpAuditLoggingElasticsearchOptions public AbpAuditLoggingElasticsearchOptions() { IndexPrefix = DefaultIndexPrefix; - IsAuditLogEnabled = true; ThrowIfIndexInitFailed = true; + AuditLogSettings = new IndexSettings() { NumberOfReplicas = 1, @@ -55,7 +47,7 @@ public class AbpAuditLoggingElasticsearchOptions }, } }; - IsSecurityLogEnabled = true; + SecurityLogSettings = new IndexSettings(); } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs index f3276e0cb..bd5beee38 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs @@ -8,11 +8,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Auditing; using Volo.Abp.DependencyInjection; using Volo.Abp.Timing; @@ -21,12 +18,9 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch; [Dependency(ReplaceServices = true)] public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependency { - private readonly AbpAuditingOptions _auditingOptions; private readonly AbpElasticsearchOptions _elasticsearchOptions; - private readonly AbpAuditLoggingElasticsearchOptions _loggingEsOptions; private readonly IIndexNameNormalizer _indexNameNormalizer; private readonly IElasticsearchClientFactory _clientFactory; - private readonly IAuditLogInfoToAuditLogConverter _converter; private readonly IClock _clock; public ILogger Logger { protected get; set; } @@ -35,18 +29,12 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen IClock clock, IIndexNameNormalizer indexNameNormalizer, IOptions elasticsearchOptions, - IElasticsearchClientFactory clientFactory, - IOptions auditingOptions, - IAuditLogInfoToAuditLogConverter converter, - IOptionsMonitor loggingEsOptions) + IElasticsearchClientFactory clientFactory) { _clock = clock; - _converter = converter; _clientFactory = clientFactory; - _auditingOptions = auditingOptions.Value; _elasticsearchOptions = elasticsearchOptions.Value; _indexNameNormalizer = indexNameNormalizer; - _loggingEsOptions = loggingEsOptions.CurrentValue; Logger = NullLogger.Instance; } @@ -221,81 +209,6 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen cancellationToken); } - public async virtual Task SaveAsync( - AuditLogInfo auditInfo, - CancellationToken cancellationToken = default) - { - if (!_loggingEsOptions.IsAuditLogEnabled) - { - Logger.LogInformation(auditInfo.ToString()); - return ""; - } - - if (!_auditingOptions.HideErrors) - { - return await SaveLogAsync(auditInfo, cancellationToken); - } - - try - { - return await SaveLogAsync(auditInfo, cancellationToken); - } - catch (Exception ex) - { - Logger.LogWarning("Could not save the audit log object: " + Environment.NewLine + auditInfo.ToString()); - Logger.LogException(ex, LogLevel.Error); - } - return ""; - } - - protected async virtual Task SaveLogAsync( - AuditLogInfo auditLogInfo, - CancellationToken cancellationToken = default) - { - var client = _clientFactory.Create(); - - var auditLog = await _converter.ConvertAsync(auditLogInfo); - - //var response = await client.IndexAsync( - // auditLog, - // (x) => x.Index(CreateIndex()) - // .Id(auditLog.Id), - // cancellationToken); - - // 使用 Bulk 命令传输可能存在参数庞大的日志结构 - var response = await client.BulkAsync( - dsl => dsl.Index(CreateIndex()) - .Create(auditLog, ct => ct.Id(auditLog.Id))); - if (!response.IsValidResponse) - { - if (response.TryGetOriginalException(out var ex)) - { - throw ex; - } - else if (response.ElasticsearchServerError != null) - { - throw new AbpException(response.ElasticsearchServerError.ToString()); - } - else if (response.ItemsWithErrors.Any()) - { - var reasonBuilder = new StringBuilder(); - foreach (var itemError in response.ItemsWithErrors) - { - if (itemError.Error?.Reason.IsNullOrWhiteSpace() == false) - { - reasonBuilder.AppendLine(itemError.Error.Reason); - } - } - if (reasonBuilder.Length > 0) - { - throw new AbpException(reasonBuilder.ToString()); - } - } - } - - return response.Items?.FirstOrDefault()?.Id; - } - protected virtual List BuildQueryDescriptor( DateTime? startTime = null, DateTime? endTime = null, diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs new file mode 100644 index 000000000..c88aa3d51 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs @@ -0,0 +1,114 @@ +using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.Core.Bulk; +using LINGYUN.Abp.Elasticsearch; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AuditLogging.Elasticsearch; + +[Dependency(ReplaceServices = true)] +public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency +{ + private readonly IAuditLogInfoToAuditLogConverter _auditLogConverter; + private readonly IElasticsearchClientFactory _clientFactory; + private readonly IIndexNameNormalizer _indexNameNormalizer; + + private readonly ILogger _logger; + + public ElasticsearchAuditLogWriter( + IAuditLogInfoToAuditLogConverter auditLogConverter, + IElasticsearchClientFactory clientFactory, + IIndexNameNormalizer indexNameNormalizer, + ILogger logger) + { + _auditLogConverter = auditLogConverter; + _clientFactory = clientFactory; + _indexNameNormalizer = indexNameNormalizer; + _logger = logger; + } + + public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + { + var client = _clientFactory.Create(); + var auditLog = await _auditLogConverter.ConvertAsync(auditLogInfo); + var response = await client.IndexAsync( + auditLog, + dsl => dsl.Index(CreateIndex()) + .Id(auditLog.Id), + cancellationToken); + + if (!response.IsValidResponse) + { + _logger.LogWarning("Could not save the audit log object: " + Environment.NewLine + auditLog.ToString()); + if (response.TryGetOriginalException(out var ex)) + { + _logger.LogWarning(ex, ex.Message); + } + else if (response.ElasticsearchServerError != null) + { + _logger.LogWarning(response.ElasticsearchServerError.ToString()); + } + } + } + + public async virtual Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default) + { + if (!auditLogInfos.Any()) + { + return; + } + + var client = _clientFactory.Create(); + + var indexName = CreateIndex(); + + var bulkOperations = new List(); + foreach (var auditLogInfo in auditLogInfos) + { + var auditLog = await _auditLogConverter.ConvertAsync(auditLogInfo); + bulkOperations.Add(new BulkCreateOperation(auditLog) + { + Id = auditLog.Id.ToString() + }); + } + + var bulkRequest = new BulkRequest + { + Operations = [.. bulkOperations], + Index = indexName + }; + + var response = await client.BulkAsync(bulkRequest, cancellationToken); + + if (!response.IsValidResponse) + { + await HandleBulkErrorsAsync(response, auditLogInfos); + } + } + + private Task HandleBulkErrorsAsync(BulkResponse response, IEnumerable auditLogInfos) + { + foreach (var itemWithError in response.ItemsWithErrors) + { + _logger.LogError($"Failed to write audit log: {itemWithError.Error?.Reason}"); + } + + foreach (var auditLog in auditLogInfos) + { + _logger.LogInformation(auditLog.ToString()); + } + + return Task.CompletedTask; + } + + protected virtual string CreateIndex() + { + return _indexNameNormalizer.NormalizeIndex("audit-log"); + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogManager.cs index 396bdd4ab..5a5a72930 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogManager.cs @@ -10,8 +10,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -using Volo.Abp.Guids; -using Volo.Abp.SecurityLog; using Volo.Abp.Timing; namespace LINGYUN.Abp.AuditLogging.Elasticsearch; @@ -19,11 +17,8 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch; [Dependency(ReplaceServices = true)] public class ElasticsearchSecurityLogManager : ISecurityLogManager, ITransientDependency { - private readonly AbpSecurityLogOptions _securityLogOptions; private readonly AbpElasticsearchOptions _elasticsearchOptions; - private readonly AbpAuditLoggingElasticsearchOptions _loggingEsOptions; private readonly IIndexNameNormalizer _indexNameNormalizer; - private readonly IGuidGenerator _guidGenerator; private readonly IElasticsearchClientFactory _clientFactory; private readonly IClock _clock; @@ -31,67 +26,18 @@ public class ElasticsearchSecurityLogManager : ISecurityLogManager, ITransientDe public ElasticsearchSecurityLogManager( IClock clock, - IGuidGenerator guidGenerator, IIndexNameNormalizer indexNameNormalizer, - IOptions securityLogOptions, IOptions elasticsearchOptions, - IElasticsearchClientFactory clientFactory, - IOptionsMonitor loggingEsOptions) + IElasticsearchClientFactory clientFactory) { _clock = clock; - _guidGenerator = guidGenerator; _clientFactory = clientFactory; _indexNameNormalizer = indexNameNormalizer; - _securityLogOptions = securityLogOptions.Value; _elasticsearchOptions = elasticsearchOptions.Value; - _loggingEsOptions = loggingEsOptions.CurrentValue; Logger = NullLogger.Instance; } - public async virtual Task SaveAsync( - SecurityLogInfo securityLogInfo, - CancellationToken cancellationToken = default) - { - // TODO: 框架不把这玩意儿放在 ISecurityLogManager? - if (!_securityLogOptions.IsEnabled) - { - return; - } - - - if (!_loggingEsOptions.IsSecurityLogEnabled) - { - Logger.LogInformation(securityLogInfo.ToString()); - return; - } - - var client = _clientFactory.Create(); - - var securityLog = new SecurityLog( - _guidGenerator.Create(), - securityLogInfo); - - var response = await client.IndexAsync( - securityLog, - (x) => x.Index(CreateIndex()) - .Id(securityLog.Id), - cancellationToken); - - if (!response.IsValidResponse) - { - Logger.LogWarning("Could not save the security log object: " + Environment.NewLine + securityLogInfo.ToString()); - if (response.TryGetOriginalException(out var ex)) - { - Logger.LogWarning(ex, ex.Message); - } - else if (response.ElasticsearchServerError != null) - { - Logger.LogWarning(response.ElasticsearchServerError.ToString()); - } - } - } - public async virtual Task GetAsync( Guid id, bool includeDetails = false, diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogWriter.cs new file mode 100644 index 000000000..20f9e56b0 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchSecurityLogWriter.cs @@ -0,0 +1,122 @@ +using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.Core.Bulk; +using LINGYUN.Abp.Elasticsearch; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.SecurityLog; + +namespace LINGYUN.Abp.AuditLogging.Elasticsearch; + +[Dependency(ReplaceServices = true)] +public class ElasticsearchSecurityLogWriter : ISecurityLogWriter, ITransientDependency +{ + private readonly IElasticsearchClientFactory _clientFactory; + private readonly IIndexNameNormalizer _indexNameNormalizer; + private readonly IGuidGenerator _guidGenerator; + + private readonly ILogger _logger; + + public ElasticsearchSecurityLogWriter( + IElasticsearchClientFactory clientFactory, + IIndexNameNormalizer indexNameNormalizer, + IGuidGenerator guidGenerator, + ILogger logger) + { + _clientFactory = clientFactory; + _indexNameNormalizer = indexNameNormalizer; + _guidGenerator = guidGenerator; + _logger = logger; + } + + public async virtual Task WriteAsync(SecurityLogInfo securityLogInfo, CancellationToken cancellationToken = default) + { + var client = _clientFactory.Create(); + + var securityLog = new SecurityLog( + _guidGenerator.Create(), + securityLogInfo); + + var response = await client.IndexAsync( + securityLog, + dsl => dsl.Index(CreateIndex()) + .Id(securityLog.Id), + cancellationToken); + + if (!response.IsValidResponse) + { + _logger.LogWarning("Could not save the security log object: " + Environment.NewLine + securityLog.ToString()); + if (response.TryGetOriginalException(out var ex)) + { + _logger.LogWarning(ex, ex.Message); + } + else if (response.ElasticsearchServerError != null) + { + _logger.LogWarning(response.ElasticsearchServerError.ToString()); + } + } + } + + public async virtual Task BulkWriteAsync(IEnumerable securityLogInfos, CancellationToken cancellationToken = default) + { + if (!securityLogInfos.Any()) + { + return; + } + + var client = _clientFactory.Create(); + + var indexName = CreateIndex(); + + var bulkOperations = new List(); + foreach (var securityLogInfo in securityLogInfos) + { + var securityLog = new SecurityLog( + _guidGenerator.Create(), + securityLogInfo); + + bulkOperations.Add(new BulkCreateOperation(securityLog) + { + Id = securityLog.Id.ToString() + }); + } + + var bulkRequest = new BulkRequest + { + Operations = [.. bulkOperations], + Index = indexName + }; + + var response = await client.BulkAsync(bulkRequest, cancellationToken); + + if (!response.IsValidResponse) + { + await HandleBulkErrorsAsync(response, securityLogInfos); + } + } + + private Task HandleBulkErrorsAsync(BulkResponse response, IEnumerable securityLogInfos) + { + foreach (var itemWithError in response.ItemsWithErrors) + { + _logger.LogError($"Failed to write security log: {itemWithError.Error?.Reason}"); + } + + foreach (var auditLog in securityLogInfos) + { + _logger.LogInformation(auditLog.ToString()); + } + + return Task.CompletedTask; + } + + protected virtual string CreateIndex() + { + return _indexNameNormalizer.NormalizeIndex("security-log"); + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs index b254d10ca..c803e4525 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs @@ -1,12 +1,8 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using System; +using System; using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; -using Volo.Abp.Auditing; using Volo.Abp.AuditLogging; using Volo.Abp.DependencyInjection; using Volo.Abp.ObjectMapping; @@ -20,25 +16,15 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency protected IObjectMapper ObjectMapper { get; } protected IAuditLogRepository AuditLogRepository { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } - protected AbpAuditingOptions Options { get; } - protected IAuditLogInfoToAuditLogConverter Converter { get; } - - public ILogger Logger { protected get; set; } public AuditLogManager( IAuditLogRepository auditLogRepository, IUnitOfWorkManager unitOfWorkManager, - IOptions options, - IAuditLogInfoToAuditLogConverter converter, IObjectMapper objectMapper) { ObjectMapper = objectMapper; AuditLogRepository = auditLogRepository; UnitOfWorkManager = unitOfWorkManager; - Converter = converter; - Options = options.Value; - - Logger = NullLogger.Instance; } @@ -149,41 +135,4 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency await uow.CompleteAsync(); } } - - public async virtual Task SaveAsync( - AuditLogInfo auditInfo, - CancellationToken cancellationToken = default) - { - if (!Options.HideErrors) - { - return await SaveLogAsync(auditInfo, cancellationToken); - } - - try - { - return await SaveLogAsync(auditInfo, cancellationToken); - } - catch (Exception ex) - { - Logger.LogWarning("Could not save the audit log object: " + Environment.NewLine + auditInfo.ToString()); - Logger.LogException(ex, LogLevel.Error); - } - return ""; - } - - protected async virtual Task SaveLogAsync( - AuditLogInfo auditInfo, - CancellationToken cancellationToken = default) - { - using (var uow = UnitOfWorkManager.Begin(true)) - { - var auditLog = await AuditLogRepository.InsertAsync( - await Converter.ConvertAsync(auditInfo), - false, - cancellationToken); - await uow.CompleteAsync(); - - return auditLog.Id.ToString(); - } - } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs new file mode 100644 index 000000000..df0181d5b --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Auditing; +using Volo.Abp.AuditLogging; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Uow; +using IVoloAuditLogInfoToAuditLogConverter = Volo.Abp.AuditLogging.IAuditLogInfoToAuditLogConverter; +using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; + +namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; + +[Dependency(ReplaceServices = true)] +public class EfCoreAuditLogWriter : IAuditLogWriter, ITransientDependency +{ + protected IVoloAuditLogInfoToAuditLogConverter AuditLogConverter { get; } + protected IAuditLogRepository AuditLogRepository { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IGuidGenerator GuidGenerator { get; } + + public EfCoreAuditLogWriter( + IVoloAuditLogInfoToAuditLogConverter auditLogConverter, + IAuditLogRepository auditLogRepository, + IUnitOfWorkManager unitOfWorkManager, + IGuidGenerator guidGenerator) + { + AuditLogConverter = auditLogConverter; + AuditLogRepository = auditLogRepository; + UnitOfWorkManager = unitOfWorkManager; + GuidGenerator = guidGenerator; + } + + public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + { + using (var uow = UnitOfWorkManager.Begin(true)) + { + var auditLog = await AuditLogConverter.ConvertAsync(auditLogInfo); + + await AuditLogRepository.InsertAsync(auditLog); + + await uow.CompleteAsync(); + } + } + + public async virtual Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default) + { + using (var uow = UnitOfWorkManager.Begin(true)) + { + var auditLogs = new List(); + foreach (var auditLogInfo in auditLogInfos) + { + var auditLog = await AuditLogConverter.ConvertAsync(auditLogInfo); + auditLogs.Add(auditLog); + } + + await AuditLogRepository.InsertManyAsync(auditLogs); + + await uow.CompleteAsync(); + } + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreSecurityLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreSecurityLogWriter.cs new file mode 100644 index 000000000..d9b27a0f5 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreSecurityLogWriter.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Identity; +using Volo.Abp.SecurityLog; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; + +[Dependency(ReplaceServices = true)] +public class EfCoreSecurityLogWriter : ISecurityLogWriter, ITransientDependency +{ + protected IIdentitySecurityLogRepository IdentitySecurityLogRepository { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IGuidGenerator GuidGenerator { get; } + + public EfCoreSecurityLogWriter( + IIdentitySecurityLogRepository identitySecurityLogRepository, + IUnitOfWorkManager unitOfWorkManager, + IGuidGenerator guidGenerator) + { + IdentitySecurityLogRepository = identitySecurityLogRepository; + UnitOfWorkManager = unitOfWorkManager; + GuidGenerator = guidGenerator; + } + + public async virtual Task BulkWriteAsync(IEnumerable securityLogInfos, CancellationToken cancellationToken = default) + { + using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) + { + var securityLogs = securityLogInfos.Select(securityLogInfo => + new IdentitySecurityLog(GuidGenerator, securityLogInfo)); + + await IdentitySecurityLogRepository.InsertManyAsync( + securityLogs, + false, + cancellationToken); + + await uow.CompleteAsync(); + } + } + + public async virtual Task WriteAsync(SecurityLogInfo securityLogInfo, CancellationToken cancellationToken = default) + { + using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) + { + await IdentitySecurityLogRepository.InsertAsync( + new IdentitySecurityLog(GuidGenerator, securityLogInfo), + false, + cancellationToken); + await uow.CompleteAsync(); + } + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/SecurityLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/SecurityLogManager.cs index 759350042..00df36963 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/SecurityLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/SecurityLogManager.cs @@ -1,14 +1,10 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -using Volo.Abp.Guids; using Volo.Abp.Identity; using Volo.Abp.ObjectMapping; -using Volo.Abp.SecurityLog; using Volo.Abp.Uow; namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; @@ -16,26 +12,17 @@ namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; [Dependency(ReplaceServices = true)] public class SecurityLogManager : ISecurityLogManager, ITransientDependency { - public ILogger Logger { get; set; } protected IObjectMapper ObjectMapper { get; } - protected AbpSecurityLogOptions SecurityLogOptions { get; } protected IIdentitySecurityLogRepository IdentitySecurityLogRepository { get; } - protected IGuidGenerator GuidGenerator { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } public SecurityLogManager( IObjectMapper objectMapper, - ILogger logger, - IOptions securityLogOptions, IIdentitySecurityLogRepository identitySecurityLogRepository, - IGuidGenerator guidGenerator, IUnitOfWorkManager unitOfWorkManager) { - Logger = logger; ObjectMapper = objectMapper; - SecurityLogOptions = securityLogOptions.Value; IdentitySecurityLogRepository = identitySecurityLogRepository; - GuidGenerator = guidGenerator; UnitOfWorkManager = unitOfWorkManager; } @@ -49,25 +36,6 @@ public class SecurityLogManager : ISecurityLogManager, ITransientDependency } } - public async virtual Task SaveAsync( - SecurityLogInfo securityLogInfo, - CancellationToken cancellationToken = default) - { - if (!SecurityLogOptions.IsEnabled) - { - return; - } - - using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) - { - await IdentitySecurityLogRepository.InsertAsync( - new IdentitySecurityLog(GuidGenerator, securityLogInfo), - false, - cancellationToken); - await uow.CompleteAsync(); - } - } - public async virtual Task GetAsync( Guid id, bool includeDetails = false, diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationAuditingStore.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationAuditingStore.cs index ca078097d..18545ca72 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationAuditingStore.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationAuditingStore.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.IP.Location; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -12,8 +13,10 @@ public class IPLocationAuditingStore : AuditingStore public IPLocationAuditingStore( IOptionsMonitor options, IIPLocationResolver iPLocationResolver, - IAuditLogManager manager) - : base(manager) + IOptionsMonitor loggingOptions, + IAuditLogQueue auditLogQueue, + ILogger logger) + : base(loggingOptions, auditLogQueue, logger) { _options = options.CurrentValue; _iPLocationResolver = iPLocationResolver; diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationSecurityLogStore.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationSecurityLogStore.cs index ca65cfaed..c961d782f 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationSecurityLogStore.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.IP.Location/LINGYUN/Abp/AuditLogging/IP/Location/IPLocationSecurityLogStore.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.IP.Location; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -13,8 +14,11 @@ public class IPLocationSecurityLogStore : SecurityLogStore public IPLocationSecurityLogStore( IOptionsMonitor options, IIPLocationResolver iPLocationResolver, - ISecurityLogManager manager) - : base(manager) + IOptionsMonitor securityLogOptions, + IOptionsMonitor loggingOptions, + ISecurityLogQueue securityLogQueue, + ILogger logger) + : base(securityLogOptions, loggingOptions, securityLogQueue, logger) { _options = options.CurrentValue; _iPLocationResolver = iPLocationResolver; diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj index 3684d96a7..e99d9c8d5 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj @@ -19,6 +19,7 @@ + diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingModule.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingModule.cs index 1de4e8c42..7fcdf53f9 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingModule.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingModule.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; using Volo.Abp.ExceptionHandling; using Volo.Abp.Guids; using Volo.Abp.Modularity; +using Volo.Abp.Threading; namespace LINGYUN.Abp.AuditLogging; @@ -13,12 +19,71 @@ namespace LINGYUN.Abp.AuditLogging; typeof(AbpExceptionHandlingModule))] public class AbpAuditLoggingModule : AbpModule { + private readonly CancellationTokenSource _cancellationTokenSource = new(); public override void ConfigureServices(ServiceConfigurationContext context) { + var configuration = context.Services.GetConfiguration(); + Configure(configuration.GetSection("AuditLogging")); + Configure(options => { options.IgnoredTypes.AddIfNotContains(typeof(CancellationToken)); options.IgnoredTypes.AddIfNotContains(typeof(CancellationTokenSource)); }); } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context)); + } + + public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var options = context.ServiceProvider.GetRequiredService>(); + + if (options.Value.UseAuditLogQueue) + { + var auditLogQueue = rootServiceProvider.GetRequiredService(); + if (auditLogQueue is AuditLogQueue queue1) + { + await queue1.StartAsync(_cancellationTokenSource.Token); + } + } + + if (options.Value.UseSecurityLogQueue) + { + var securityLogQueue = rootServiceProvider.GetRequiredService(); + if (securityLogQueue is SecurityLogQueue queue2) + { + await queue2.StartAsync(_cancellationTokenSource.Token); + } + } + } + + public async override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) + { + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var options = context.ServiceProvider.GetRequiredService>(); + + if (options.Value.UseAuditLogQueue) + { + var auditLogQueue = rootServiceProvider.GetRequiredService(); + if (auditLogQueue is AuditLogQueue queue1) + { + await queue1.StopAsync(_cancellationTokenSource.Token); + } + } + + if (options.Value.UseSecurityLogQueue) + { + var securityLogQueue = rootServiceProvider.GetRequiredService(); + if (securityLogQueue is SecurityLogQueue queue2) + { + await queue2.StopAsync(_cancellationTokenSource.Token); + } + } + + _cancellationTokenSource.Cancel(); + } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingOptions.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingOptions.cs new file mode 100644 index 000000000..d19d06fe3 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingOptions.cs @@ -0,0 +1,54 @@ +namespace LINGYUN.Abp.AuditLogging; +public class AbpAuditLoggingOptions +{ + /// + /// 是否启用审计日志记录 + /// + public bool IsAuditLogEnabled { get; set; } + /// + /// 使用审计日志队列 + /// + /// + /// 不启用队列则直接写入到持久化设施 + /// + public bool UseAuditLogQueue { get; set; } + /// + /// 审计日志最大队列大小 + /// + public int MaxAuditLogQueueSize { get; set; } + /// + /// 一次处理审计日志队列大小 + /// + public int BatchAuditLogSize { get; set; } + /// + /// 是否启用安全日志记录 + /// + public bool IsSecurityLogEnabled { get; set; } + /// + /// 使用安全日志队列 + /// + /// + /// 不启用队列则直接写入到持久化设施 + /// + public bool UseSecurityLogQueue { get; set; } + /// + /// 安全日志最大队列大小 + /// + public int MaxSecurityLogQueueSize { get; set; } + /// + /// 一次处理安全日志队列大小 + /// + public int BatchSecurityLogSize { get; set; } + public AbpAuditLoggingOptions() + { + IsAuditLogEnabled = true; + UseAuditLogQueue = true; + BatchAuditLogSize = 100; + MaxAuditLogQueueSize = 10000; + + IsSecurityLogEnabled = true; + UseSecurityLogQueue = true; + BatchSecurityLogSize = 100; + MaxSecurityLogQueueSize = 10000; + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogInfoToAuditLogConverter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLogInfoToAuditLogConverter.cs similarity index 100% rename from aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogInfoToAuditLogConverter.cs rename to aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLogInfoToAuditLogConverter.cs diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLogQueue.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLogQueue.cs new file mode 100644 index 000000000..99ee5cc85 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLogQueue.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AuditLogging; +public class AuditLogQueue : AuditLoggingQueue, IAuditLogQueue, ISingletonDependency +{ + private readonly IAuditLogWriter _auditLogWriter; + private readonly ICurrentTenant _currentTenant; + public AuditLogQueue( + IOptions options, + IAuditLogWriter auditLogWriter, + ICurrentTenant currentTenant, + ILogger logger) + : base( + "AuditLog", + options.Value.MaxAuditLogQueueSize, + options.Value.BatchAuditLogSize, + logger) + { + _auditLogWriter = auditLogWriter; + _currentTenant = currentTenant; + } + + protected async override Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default) + { + var tenantAuditlogGroup = auditLogInfos.GroupBy(x => x.ImpersonatorTenantId ?? x.TenantId); + foreach (var tenantAuditlogs in tenantAuditlogGroup) + { + using (_currentTenant.Change(tenantAuditlogs.Key)) + { + await _auditLogWriter.BulkWriteAsync(tenantAuditlogs, cancellationToken); + } + } + } + + protected async override Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + { + using (_currentTenant.Change(auditLogInfo.ImpersonatorTenantId ?? auditLogInfo.TenantId)) + { + await _auditLogWriter.WriteAsync(auditLogInfo, cancellationToken); + } + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLoggingQueue.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLoggingQueue.cs new file mode 100644 index 000000000..3b615f6f2 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLoggingQueue.cs @@ -0,0 +1,196 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AuditLogging; +public abstract class AuditLoggingQueue +{ + private readonly ILogger> _logger; + private readonly Channel _channel; + private readonly string _logName; + private readonly int _batchSize; + private readonly int _maxConcurrency; + + private volatile Task _consumerTask; + private readonly SemaphoreSlim _flushSemaphore; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + protected AuditLoggingQueue( + string logName, + int maxQueueSize, + int batchSize, + ILogger> logger) + { + _logName = logName; + _batchSize = batchSize; + _logger = logger; + + var channelOptions = new BoundedChannelOptions(maxQueueSize) + { + FullMode = BoundedChannelFullMode.Wait, + SingleWriter = false, + SingleReader = true, + }; + _channel = Channel.CreateBounded(channelOptions); + + var calculatedConcurrency = (int)Math.Ceiling(Math.Max(1, maxQueueSize) / (double)Math.Max(1, batchSize)); + var defaultConcurrency = Environment.ProcessorCount * 2; + _maxConcurrency = Math.Min(calculatedConcurrency, defaultConcurrency); + _flushSemaphore = new SemaphoreSlim(_maxConcurrency, _maxConcurrency); + } + + public virtual Task EnqueueAsync(TLog log, CancellationToken cancellationToken = default) + { + try + { + if (!_channel.Writer.TryWrite(log)) + { + _logger.LogWarning("{logName} channel is full; {logName} is recorded in the log.", _logName, _logName); + _logger.LogInformation(log!.ToString()); + } + + return Task.CompletedTask; + } + catch (ChannelClosedException) + { + _logger.LogWarning("{logName} channel is closed; dropping it.", _logName); + } + catch (OperationCanceledException) + { + _logger.LogDebug("EnqueueAsync was canceled while waiting to write to {logName} channel.", _logName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception while enqueuing {logName}.", _logName); + } + + return Task.CompletedTask; + } + + public virtual Task StartAsync(CancellationToken cancellationToken) + { + _consumerTask = Task.Factory.StartNew( + () => ConsumeBatchesAsync(), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default) + .Unwrap(); + + return Task.CompletedTask; + } + + public async virtual Task StopAsync(CancellationToken cancellationToken) + { + _channel.Writer.TryComplete(); + + if (_consumerTask != null) + { + try + { + await Task.WhenAny(_consumerTask, Task.Delay(TimeSpan.FromSeconds(5), cancellationToken)); + _cts.Cancel(); + _logger.LogInformation("Stopped consumer loop: {logName}.", _logName); + } + catch (OperationCanceledException) + { + _logger.LogWarning("StopAsync was canceled while waiting for consumer loop: {logName} to finish.", _logName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while stopping consumer loop: {logName}.", _logName); + } + finally + { + _cts.Dispose(); + } + } + } + + private async Task ConsumeBatchesAsync() + { + var reader = _channel.Reader; + var batchSize = Math.Max(1, _batchSize); + var batchSendTaskList = new List(_maxConcurrency * 2); + + _logger.LogInformation("Starting {logName} consumer loop. BatchSize={batchSize}", _logName, batchSize); + + try + { + while (await reader.WaitToReadAsync(_cts.Token)) + { + var buffer = new List(batchSize); + + while (buffer.Count < batchSize && reader.TryRead(out var item)) + { + buffer.Add(item); + } + + if (buffer.Count > 0) + { + batchSendTaskList.Add(SendBatchAsync(buffer, _cts.Token)); + if (batchSendTaskList.Count >= _maxConcurrency) + { + try + { + var completedTask = await Task.WhenAny(batchSendTaskList); + batchSendTaskList.Remove(completedTask); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background send {logName} task failed.", _logName); + } + } + } + } + + await Task.WhenAll(batchSendTaskList); + } + catch (OperationCanceledException) + { + // ignore + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception in {logName} consumer loop.", _logName); + } + } + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + if (batch == null || batch.Count == 0) + { + return; + } + + await _flushSemaphore.WaitAsync(cancellationToken); + + try + { + if (batch.Count == 1) + { + await WriteAsync(batch[0], cancellationToken); + } + else + { + await BulkWriteAsync(batch, cancellationToken); + } + } + catch (OperationCanceledException) + { + _logger.LogDebug("SendBatchAsync canceled while writing {logName}.", _logName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to write {logName} batch to Elasticsearch. Items={count}", _logName, batch.Count); + } + finally + { + _flushSemaphore.Release(); + } + } + + protected abstract Task WriteAsync(TLog log, CancellationToken cancellationToken = default); + protected abstract Task BulkWriteAsync(IEnumerable logs, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditingStore.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditingStore.cs index 660b820f8..218672e08 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditingStore.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditingStore.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Threading.Tasks; using Volo.Abp.Auditing; using Volo.Abp.DependencyInjection; @@ -7,16 +9,37 @@ namespace LINGYUN.Abp.AuditLogging; [Dependency(ReplaceServices = true)] public class AuditingStore : IAuditingStore, ITransientDependency { - private readonly IAuditLogManager _manager; + private readonly AbpAuditLoggingOptions _loggingOptions; + private readonly IAuditLogWriter _auditLogWriter; + private readonly IAuditLogQueue _auditLogQueue; + private readonly ILogger _logger; public AuditingStore( - IAuditLogManager manager) + IOptionsMonitor loggingOptions, + IAuditLogWriter auditLogWriter, + IAuditLogQueue auditLogQueue, + ILogger logger) { - _manager = manager; + _loggingOptions = loggingOptions.CurrentValue; + _auditLogWriter = auditLogWriter; + _auditLogQueue = auditLogQueue; + _logger = logger; } public async virtual Task SaveAsync(AuditLogInfo auditInfo) { - await _manager.SaveAsync(auditInfo); + if (!_loggingOptions.IsAuditLogEnabled) + { + _logger.LogInformation(auditInfo.ToString()); + return; + } + if (_loggingOptions.UseAuditLogQueue) + { + await _auditLogQueue.EnqueueAsync(auditInfo); + } + else + { + await _auditLogWriter.WriteAsync(auditInfo); + } } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IAuditLogInfoToAuditLogConverter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogInfoToAuditLogConverter.cs similarity index 100% rename from aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IAuditLogInfoToAuditLogConverter.cs rename to aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogInfoToAuditLogConverter.cs diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs index e140cdf32..f689a463b 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; -using Volo.Abp.Auditing; namespace LINGYUN.Abp.AuditLogging; @@ -22,10 +21,6 @@ public interface IAuditLogManager List ids, CancellationToken cancellationToken = default); - Task SaveAsync( - AuditLogInfo auditInfo, - CancellationToken cancellationToken = default); - Task GetCountAsync( DateTime? startTime = null, DateTime? endTime = null, diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogQueue.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogQueue.cs new file mode 100644 index 000000000..ca22f4312 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogQueue.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Auditing; + +namespace LINGYUN.Abp.AuditLogging; +public interface IAuditLogQueue +{ + Task EnqueueAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs new file mode 100644 index 000000000..0fc2fb0b0 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Auditing; + +namespace LINGYUN.Abp.AuditLogging; +public interface IAuditLogWriter +{ + Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default); + + Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogManager.cs index f8d48320e..e031c0b51 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Volo.Abp.SecurityLog; namespace LINGYUN.Abp.AuditLogging; @@ -21,10 +20,6 @@ public interface ISecurityLogManager List ids, CancellationToken cancellationToken = default); - Task SaveAsync( - SecurityLogInfo securityLogInfo, - CancellationToken cancellationToken = default); - Task> GetListAsync( string? sorting = null, int maxResultCount = 50, diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogQueue.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogQueue.cs new file mode 100644 index 000000000..05de095a6 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogQueue.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.SecurityLog; + +namespace LINGYUN.Abp.AuditLogging; +public interface ISecurityLogQueue +{ + Task EnqueueAsync(SecurityLogInfo securityLogInfo, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogWriter.cs new file mode 100644 index 000000000..ef249b9b5 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/ISecurityLogWriter.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.SecurityLog; + +namespace LINGYUN.Abp.AuditLogging; +public interface ISecurityLogWriter +{ + Task WriteAsync(SecurityLogInfo securityLogInfo, CancellationToken cancellationToken = default); + + Task BulkWriteAsync(IEnumerable securityLogInfos, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs new file mode 100644 index 000000000..873f28be2 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AuditLogging; + +[Dependency(TryRegister = true)] +public class LoggerAuditLogWriter : IAuditLogWriter, ISingletonDependency +{ + private readonly ILogger _logger; + public LoggerAuditLogWriter(ILogger logger) + { + _logger = logger; + } + + public async virtual Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default) + { + foreach (var auditLogInfo in auditLogInfos) + { + await WriteAsync(auditLogInfo, cancellationToken); + } + } + + public virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + { + _logger.LogInformation(auditLogInfo.ToString()); + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerSecurityLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerSecurityLogWriter.cs new file mode 100644 index 000000000..b19e426a8 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerSecurityLogWriter.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.SecurityLog; + +namespace LINGYUN.Abp.AuditLogging; + +[Dependency(TryRegister = true)] +public class LoggerSecurityLogWriter : ISecurityLogWriter, ISingletonDependency +{ + private readonly ILogger _logger; + public LoggerSecurityLogWriter(ILogger logger) + { + _logger = logger; + } + + public async virtual Task BulkWriteAsync(IEnumerable securityLogInfos, CancellationToken cancellationToken = default) + { + foreach (var securityLogInfo in securityLogInfos) + { + await WriteAsync(securityLogInfo, cancellationToken); + } + } + + public virtual Task WriteAsync(SecurityLogInfo securityLogInfo, CancellationToken cancellationToken = default) + { + _logger.LogInformation(securityLogInfo.ToString()); + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogQueue.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogQueue.cs new file mode 100644 index 000000000..e45714962 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogQueue.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; +using Volo.Abp.SecurityLog; + +namespace LINGYUN.Abp.AuditLogging; +public class SecurityLogQueue : AuditLoggingQueue, ISecurityLogQueue, ISingletonDependency +{ + private readonly ISecurityLogWriter _securityLogWriter; + private readonly ICurrentTenant _currentTenant; + public SecurityLogQueue( + IOptions options, + ISecurityLogWriter securityLogWriter, + ICurrentTenant currentTenant, + ILogger logger) + : base( + "SecurityLog", + options.Value.MaxSecurityLogQueueSize, + options.Value.BatchSecurityLogSize, + logger) + { + _securityLogWriter = securityLogWriter; + _currentTenant = currentTenant; + } + + protected async override Task BulkWriteAsync(IEnumerable securityLogInfos, CancellationToken cancellationToken = default) + { + var tenantSecurityLogGroup = securityLogInfos.GroupBy(x => x.TenantId); + foreach (var tenantSecurityLogs in tenantSecurityLogGroup) + { + using (_currentTenant.Change(tenantSecurityLogs.Key)) + { + await _securityLogWriter.BulkWriteAsync(tenantSecurityLogs, cancellationToken); + } + } + } + + protected async override Task WriteAsync(SecurityLogInfo securityLogInfo, CancellationToken cancellationToken = default) + { + using (_currentTenant.Change(securityLogInfo.TenantId)) + { + await _securityLogWriter.WriteAsync(securityLogInfo, cancellationToken); + } + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogStore.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogStore.cs index 1918ccc71..6b1642dca 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogStore.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/SecurityLogStore.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.SecurityLog; @@ -7,16 +9,40 @@ namespace LINGYUN.Abp.AuditLogging; [Dependency(ReplaceServices = true)] public class SecurityLogStore : ISecurityLogStore, ITransientDependency { - private readonly ISecurityLogManager _manager; + private readonly AbpSecurityLogOptions _securityLogOptions; + private readonly AbpAuditLoggingOptions _loggingOptions; + private readonly ISecurityLogWriter _securityLogWriter; + private readonly ISecurityLogQueue _securityLogQueue; + private readonly ILogger _logger; public SecurityLogStore( - ISecurityLogManager manager) + IOptionsMonitor securityLogOptions, + IOptionsMonitor loggingOptions, + ISecurityLogWriter securityLogWriter, + ISecurityLogQueue securityLogQueue, + ILogger logger) { - _manager = manager; + _securityLogOptions = securityLogOptions.CurrentValue; + _loggingOptions = loggingOptions.CurrentValue; + _securityLogWriter = securityLogWriter; + _securityLogQueue = securityLogQueue; + _logger = logger; } public async virtual Task SaveAsync(SecurityLogInfo securityLogInfo) { - await _manager.SaveAsync(securityLogInfo); + if (!_loggingOptions.IsAuditLogEnabled || !_securityLogOptions.IsEnabled) + { + _logger.LogInformation(securityLogInfo.ToString()); + return; + } + if (_loggingOptions.UseSecurityLogQueue) + { + await _securityLogQueue.EnqueueAsync(securityLogInfo); + } + else + { + await _securityLogWriter.WriteAsync(securityLogInfo); + } } } diff --git a/aspnet-core/framework/auditing/README.md b/aspnet-core/framework/auditing/README.md index 2f21293a5..91d54667f 100644 --- a/aspnet-core/framework/auditing/README.md +++ b/aspnet-core/framework/auditing/README.md @@ -15,8 +15,11 @@ ### 存储支持 -- EntityFrameworkCore 实现 -- Elasticsearch 实现 +- EntityFrameworkCore 实现 +- Elasticsearch 实现 + +> 注意: Elastic库限制, 兼容8.x; 9.x版本, 如需使用10.x版本, 请切换 **Elastic.Clients.Elasticsearch** 为9.x版本 +> 参考: https://www.nuget.org/packages/Elastic.Clients.Elasticsearch#readme-body-tab ## 模块引用 @@ -62,6 +65,17 @@ public class YouProjectModule : AbpModule "IsEnabledForAnonymousUsers": true, // 是否为匿名用户启用审计日志 "IsEnabledForGetRequests": false, // 是否为GET请求启用审计日志 "ApplicationName": null // 应用程序名称 + }, + // 审计日志增强配置 + "AuditLogging": { + "IsAuditLogEnabled": true, // 是否启用审计日志记录 + "UseAuditLogQueue": true, // 使用审计日志队列, 不启用队列则直接写入到持久化设施 + "MaxAuditLogQueueSize": 10000, // 审计日志最大队列大小, 默认10000 + "BatchAuditLogSize": 100, // 一次处理审计日志队列大小, 队列中同时写入100条记录后立即写入到持久化设施中 + "IsSecurityLogEnabled": true, // 是否启用安全日志记录 + "UseSecurityLogQueue": true, // 使用安全日志队列, 不启用队列则直接写入到持久化设施 + "MaxSecurityLogQueueSize": 10000, // 安全日志最大队列大小, 默认10000 + "BatchSecurityLogSize": 100 // 一次处理安全日志队列大小, 队列中同时写入100条记录后立即写入到持久化设施中 } } ``` @@ -72,7 +86,8 @@ public class YouProjectModule : AbpModule { "AuditLogging": { "Elasticsearch": { - "IndexPrefix": "auditlogging" // 索引前缀 + "IndexPrefix": "auditlogging", // 索引前缀 + "ThrowIfIndexInitFailed": true // 索引初始化失败抛出异常, 默认为: true, 索引初始化失败后应用程序停止运行 } } } @@ -110,4 +125,4 @@ Configure(options => ## 特殊说明 - Elasticsearch 实现支持跨租户,将根据租户自动切换索引 -- EntityFrameworkCore 实现主要作为桥梁,具体实现交由 Abp 官方模块管理 +- EntityFrameworkCore 实现主要作为桥梁,具体实现交由 Abp 官方模块管理 \ No newline at end of file diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestBase.cs index f27f8eb41..873443ef1 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestBase.cs +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestBase.cs @@ -1,10 +1,4 @@ using LINGYUN.Abp.Tests; -using Moq.AutoMock; -using Nest; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Auditing; namespace LINGYUN.Abp.AuditLogging.Elasticsearch { diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs index a787b3ccc..8fd3da9ff 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs @@ -29,10 +29,10 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch var options = context.ServiceProvider.GetRequiredService>().Value; var clientFactory = context.ServiceProvider.GetRequiredService(); var client = clientFactory.Create(); - var indicesResponse = client.Cat.Indices(i => i.Index($"{options.IndexPrefix}-security-log")); - foreach (var index in indicesResponse.Records) + var indicesResponse = client.Indices.Get($"{options.IndexPrefix}-security-log"); + foreach (var index in indicesResponse.Indices) { - client.Indices.Delete(index.Index); + client.Indices.Delete(index.Key); } } } diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs index 507d0f13d..befb33bb1 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs @@ -17,6 +17,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch _manager = GetRequiredService(); } + [Fact] public async Task Save_Audit_Log_Should_Be_Find_By_Id() { var mock = new AutoMocker(); From 53ff70b806fa5f5a475748fd83b85eaa4b510189 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 4 Feb 2026 10:33:09 +0800 Subject: [PATCH 2/2] feat: Module shotdown and cancels the initialization token --- .../Elasticsearch/AbpAuditLoggingElasticsearchModule.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs index 4c31091a2..e1d02371e 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchModule.cs @@ -35,4 +35,10 @@ public class AbpAuditLoggingElasticsearchModule : AbpModule var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(_cancellationTokenSource.Token); } + + public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) + { + _cancellationTokenSource.Cancel(); + return Task.CompletedTask; + } }