From 5481ca891ad8d47ba68a0f915f168f100772d26e Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 21 Aug 2026 13:13:58 +0800 Subject: [PATCH] feat: Optimize the log module --- .../AuditLogExpressionQueryTranslator.cs | 374 ------------------ .../AuditLoggingIndexInitializer.cs | 2 +- .../ElasticsearchAuditLogManager.cs | 279 ++++++------- ...bp.AuditLogging.EntityFrameworkCore.csproj | 1 + .../EfCoreAuditLogManager.cs | 15 +- ...Manager.cs => EfCoreSecurityLogManager.cs} | 4 +- .../Abp/AuditLogging/AbpAuditLoggingModule.cs | 2 + .../Queryable/ExpressionQueryConverter.cs} | 51 ++- ...N.Abp.Logging.Serilog.Elasticsearch.csproj | 1 + .../SerilogElasticsearchLoggingManager.cs | 348 +++++++++++++--- .../LINGYUN.Abp.Logging.csproj | 2 +- .../Abp/AuditLogging/AbpLoggingModule.cs | 2 + .../Abp/AuditLogging/DefaultLoggingManager.cs | 20 + .../Abp/AuditLogging/ILoggingManager.cs | 12 + .../LINGYUN/Abp/AuditLogging/LogException.cs | 17 +- .../LINGYUN/Abp/AuditLogging/LogField.cs | 37 +- .../LINGYUN/Abp/AuditLogging/LogInfo.cs | 10 + ...bp.AuditLogging.Elasticsearch.Tests.csproj | 2 +- .../AbpAuditLoggingElasticsearchTestModule.cs | 3 +- .../Elasticsearch/AuditLogManagerTests.cs | 196 --------- .../ElasticsearchAuditLogManager_Tests.cs | 5 + .../LINGYUN.Abp.AuditLogging.Tests.csproj | 19 + .../AuditLogging/AbpAuditLoggingTestBase.cs | 8 + .../AuditLogging/AbpAuditLoggingTestModule.cs | 11 + .../Abp/AuditLogging/AuditLogManager_Tests.cs | 198 ++++++++++ ...Logging.Serilog.Elasticsearch.Tests.csproj | 20 + .../AbpLoggingSerilogElasticsearchTestBase.cs | 7 + ...bpLoggingSerilogElasticsearchTestModule.cs | 51 +++ .../ElasticsearchLoggingManager_Tests.cs | 25 ++ .../LINGYUN.Abp.Logging.Tests.csproj | 22 ++ .../LINGYUN/Abp/Logging/AbpLoggingTestBase.cs | 7 + .../Abp/Logging/AbpLoggingTestModule.cs | 14 + .../Abp/Logging/LoggingManager_Tests.cs | 107 +++++ 33 files changed, 1064 insertions(+), 808 deletions(-) delete mode 100644 aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs rename aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/{SecurityLogManager.cs => EfCoreSecurityLogManager.cs} (97%) rename aspnet-core/framework/{auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs => dynamic-queryable/LINGYUN.Linq.Dynamic.Queryable/LINGYUN/Linq/Dynamic/Queryable/ExpressionQueryConverter.cs} (75%) delete mode 100644 aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager_Tests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN.Abp.AuditLogging.Tests.csproj create mode 100644 aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestBase.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestModule.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AuditLogManager_Tests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests.csproj create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestBase.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestModule.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/ElasticsearchLoggingManager_Tests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN.Abp.Logging.Tests.csproj create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestBase.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestModule.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/LoggingManager_Tests.cs diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs deleted file mode 100644 index 2a5847043..000000000 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs +++ /dev/null @@ -1,374 +0,0 @@ -using Elastic.Clients.Elasticsearch; -using Elastic.Clients.Elasticsearch.QueryDsl; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Volo.Abp; - -namespace LINGYUN.Abp.AuditLogging.Elasticsearch; - -/// -/// ES DSL表达式翻译器:把 翻译为 -/// Elastic.Clients.Elasticsearch 的 (QueryDsl)。 -/// -/// 支持的算子(超出即抛 ,fail loud): -/// -/// 逻辑:&&、||、!(映射为 bool filter / should+minimum_should_match / must_not) -/// 比较:==、!=、>、>=、<、<=(数值与日期映射为 term / range) -/// null 判断:字段 == null / != null(映射为 must_not exists / exists) -/// 字符串:Contains / StartsWith / EndsWith(映射为 wildcard)、Equals(映射为 term) -/// 集合:x.Actions.Any(predicate)(映射为 nested 查询或扁平字段展开) -/// 常量:true / false(映射为 match_all / match_none) -/// -/// -/// -internal class AuditLogExpressionQueryTranslator -{ - private bool _actionsIsNested; - private bool _caseInsensitiveWildcard; - private bool _appendKeywordForStringEquality; - - public AuditLogExpressionQueryTranslator( - bool actionsIsNested = false, - bool caseInsensitiveWildcard = true, - bool appendKeywordForStringEquality = true) - { - _actionsIsNested = actionsIsNested; - _caseInsensitiveWildcard = caseInsensitiveWildcard; - _appendKeywordForStringEquality = appendKeywordForStringEquality; - } - - public Query Translate(Expression> expression) - { - Check.NotNull(expression, nameof(expression)); - - return TranslateNode(expression.Body, prefix: null); - } - - private Query TranslateNode(Expression node, string? prefix) - { - return node switch - { - ConstantExpression { Value: bool value } => - value ? new MatchAllQuery() : new MatchNoneQuery(), - UnaryExpression { NodeType: ExpressionType.Not } unary => - (!TranslateNode(unary.Operand, prefix))!, - UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary => - TranslateNode(unary.Operand, prefix), - BinaryExpression binary => TranslateBinary(binary, prefix), - MethodCallExpression method => TranslateMethodCall(method, prefix), - MemberExpression member when member.Type == typeof(bool) => - new TermQuery { Field = ResolveField(member, prefix).Path, Value = true }, - _ => throw new NotSupportedException($"Unsupported expression node {node.NodeType}: {node}"), - }; - } - - private Query TranslateBinary(BinaryExpression node, string? prefix) - { - return node.NodeType switch - { - ExpressionType.AndAlso or ExpressionType.And => (Query)new BoolQuery - { - Filter = new Query[] { TranslateNode(node.Left, prefix), TranslateNode(node.Right, prefix) }, - }!, - ExpressionType.OrElse or ExpressionType.Or => (Query)new BoolQuery - { - Should = new Query[] { TranslateNode(node.Left, prefix), TranslateNode(node.Right, prefix) }, - MinimumShouldMatch = 1, - }!, - ExpressionType.Equal => TranslateComparison(node, prefix), - ExpressionType.NotEqual => (!TranslateComparison(node, prefix))!, - ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or ExpressionType.LessThan or ExpressionType.LessThanOrEqual => TranslateComparison(node, prefix), - _ => throw new NotSupportedException($"Unsupported binary operator {node.NodeType}: {node}"), - }; - } - - private Query TranslateComparison(BinaryExpression node, string? prefix) - { - var (fieldExpression, valueExpression) = ResolveOperands(node); - var field = ResolveField(fieldExpression, prefix); - - if (IsNullConstant(valueExpression)) - { - return new BoolQuery - { - MustNot = new Query[] { new ExistsQuery { Field = field.Path } }, - }; - } - - var value = Evaluate(valueExpression); - - return value == null - ? throw new NotSupportedException("The null value is only supported for the == null / != null comparison.") - : node.NodeType switch - { - ExpressionType.Equal => BuildEquality(field, value), - ExpressionType.GreaterThan => BuildRange(field, greaterThan: value), - ExpressionType.GreaterThanOrEqual => BuildRange(field, greaterThanOrEqualTo: value), - ExpressionType.LessThan => BuildRange(field, lessThan: value), - ExpressionType.LessThanOrEqual => BuildRange(field, lessThanOrEqualTo: value), - _ => throw new NotSupportedException($"Unsupported comparison operator {node.NodeType}"), - }; - } - - private static (Expression Field, Expression Value) ResolveOperands(BinaryExpression node) - { - var leftIsField = IsFieldLike(node.Left); - var rightIsField = IsFieldLike(node.Right); - if (leftIsField == rightIsField) - { - throw new NotSupportedException($"The comparison expression must have one side as a field and the other side as a value: {node}"); - } - return leftIsField ? (node.Left, node.Right) : (node.Right, node.Left); - } - - private static bool IsFieldLike(Expression expression) - { - var current = expression; - while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) - { - current = unary.Operand; - } - - while (current is MemberExpression member) - { - current = member.Expression!; - } - - return current is ParameterExpression; - } - - private static bool IsNullConstant(Expression expression) - { - while (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) - { - expression = unary.Operand; - } - - return expression is ConstantExpression { Value: null }; - } - - private Query BuildEquality(FieldRef field, object value) - { - if (field.Type == typeof(string)) - { - var fieldName = _appendKeywordForStringEquality - ? field.Path + ".keyword" - : field.Path; - return new TermQuery - { - Field = fieldName, - Value = (string)value, - CaseInsensitive = _caseInsensitiveWildcard, - }; - } - - if (field.Type == typeof(DateTime) || field.Type == typeof(DateTime?)) - { - var date = (DateTime)value; - return new DateRangeQuery - { - Field = field.Path, - Gte = date, - Lte = date, - }; - } - - return new TermQuery { Field = field.Path, Value = NormalizeValue(value) }; - } - - private static Query BuildRange( - FieldRef field, - object? greaterThan = null, - object? greaterThanOrEqualTo = null, - object? lessThan = null, - object? lessThanOrEqualTo = null) - { - if (field.Type == typeof(DateTime) || field.Type == typeof(DateTime?)) - { - var range = new DateRangeQuery { Field = field.Path }; - if (greaterThan != null) - { - range.Gt = (DateTime)greaterThan; - } - - if (greaterThanOrEqualTo != null) - { - range.Gte = (DateTime)greaterThanOrEqualTo; - } - - if (lessThan != null) - { - range.Lt = (DateTime)lessThan; - } - - if (lessThanOrEqualTo != null) - { - range.Lte = (DateTime)lessThanOrEqualTo; - } - - return range; - } - - var numberRange = new NumberRangeQuery { Field = field.Path }; - if (greaterThan != null) - { - numberRange.Gt = ToNumber(greaterThan); - } - - if (greaterThanOrEqualTo != null) - { - numberRange.Gte = ToNumber(greaterThanOrEqualTo); - } - - if (lessThan != null) - { - numberRange.Lt = ToNumber(lessThan); - } - - if (lessThanOrEqualTo != null) - { - numberRange.Lte = ToNumber(lessThanOrEqualTo); - } - - return numberRange; - } - - private Query BuildWildcard(string fieldName, string pattern) - { - return new WildcardQuery - { - Field = fieldName, - Value = pattern, - CaseInsensitive = _caseInsensitiveWildcard, - }; - } - - private Query TranslateMethodCall(MethodCallExpression node, string? prefix) - { - if (node.Method.DeclaringType == typeof(Enumerable) && node.Method.Name == nameof(Enumerable.Any)) - { - var collectionField = ResolveField(node.Arguments[0], prefix); - - Query inner; - if (node.Arguments.Count == 1) - { - inner = new ExistsQuery { Field = collectionField.Path }; - } - else - { - var predicate = UnwrapLambda(node.Arguments[1]); - inner = TranslateNode(predicate.Body, prefix: collectionField.Path); - } - - return _actionsIsNested - ? new NestedQuery(collectionField.Path, inner) - : inner; - } - - if (node.Method.DeclaringType == typeof(string) && node.Object != null) - { - var field = ResolveField(node.Object, prefix); - var value = (string)Evaluate(node.Arguments[0])!; - return node.Method.Name switch - { - nameof(string.Contains) => BuildWildcard(field.Path, "*" + EscapeWildcard(value) + "*"), - nameof(string.StartsWith) => BuildWildcard(field.Path, EscapeWildcard(value) + "*"), - nameof(string.EndsWith) => BuildWildcard(field.Path, "*" + EscapeWildcard(value)), - _ => throw new NotSupportedException($"Unsupported string method {node.Method.Name}"), - }; - } - - if (node.Method.Name == nameof(string.Equals)) - { - var fieldExpression = node.Object ?? node.Arguments[0]; - var valueExpression = node.Object != null ? node.Arguments[0] : node.Arguments[1]; - var field = ResolveField(fieldExpression, prefix); - return BuildEquality(field, (string)Evaluate(valueExpression)!); - } - - throw new NotSupportedException( - $"Unsupported method invocation {node.Method.DeclaringType?.Name}.{node.Method.Name}"); - } - - private static LambdaExpression UnwrapLambda(Expression expression) - { - while (expression is UnaryExpression { NodeType: ExpressionType.Quote } unary) - { - expression = unary.Operand; - } - return (LambdaExpression)expression; - } - - private static object? Evaluate(Expression expression) - { - return expression is ConstantExpression constant - ? constant.Value - : Expression.Lambda(expression).Compile().DynamicInvoke(); - } - - private static string EscapeWildcard(string value) - { - return value.Replace("\\", "\\\\").Replace("*", "\\*").Replace("?", "\\?"); - } - - private static FieldValue NormalizeValue(object value) - { - return value switch - { - string s => s, - bool b => b, - int i => i, - long l => l, - double d => d, - Guid g => g.ToString(), - Enum e => Convert.ToInt64(e), - _ => Convert.ToDouble(value), - }; - } - - private static Number? ToNumber(object? value) - { - if (value == null) - { - return null; - } - return value is double d ? d : Convert.ToInt64(value); - } - - private readonly record struct FieldRef(string Path, Type Type); - - private static FieldRef ResolveField(Expression expression, string? prefix) - { - var current = expression; - while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) - { - current = unary.Operand; - } - - var names = new Stack(); - while (current is MemberExpression member) - { - names.Push(member.Member.Name); - current = member.Expression!; - } - - if (current is not ParameterExpression) - { - throw new NotSupportedException($"Unable to parse as field path: {expression}"); - } - - var path = string.Join(".", names); - if (!string.IsNullOrEmpty(prefix) && path.Length > 0) - { - path = prefix + "." + path; - } - else if (path.Length == 0) - { - path = prefix ?? string.Empty; - } - - return new FieldRef(path, expression.Type); - } -} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLoggingIndexInitializer.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLoggingIndexInitializer.cs index 6a51f1a42..16c1c2aea 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLoggingIndexInitializer.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLoggingIndexInitializer.cs @@ -134,7 +134,7 @@ public class AuditLoggingIndexInitializer : IAuditLoggingIndexInitializer, ISing npd.Keyword(nameof(AuditLogAction.AuditLogId), p => p.IgnoreAbove(36)); npd.Text(nameof(AuditLogAction.ServiceName), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(256)))); npd.Text(nameof(AuditLogAction.MethodName), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(256)))); - npd.Text(nameof(AuditLogAction.Parameters), p => p.Norms(false).IndexOptions(IndexOptions.Docs)); + npd.Wildcard(nameof(AuditLogAction.Parameters)); npd.Date(nameof(AuditLogAction.ExecutionTime), d => d.Format(dateTimeFormat)); npd.IntegerNumber(nameof(AuditLogAction.ExecutionDuration)); npd.Flattened(nameof(AuditLogAction.ExtraProperties), f => f.DepthLimit(5).EagerGlobalOrdinals(false)); 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 f7e7b3f42..1cbff1dfd 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 @@ -1,10 +1,8 @@ using Elastic.Clients.Elasticsearch; -using Elastic.Clients.Elasticsearch.Mapping; using Elastic.Clients.Elasticsearch.QueryDsl; using LINGYUN.Abp.Elasticsearch; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; @@ -20,23 +18,26 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch; [Dependency(ReplaceServices = true)] public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependency { - private readonly AbpElasticsearchOptions _elasticsearchOptions; private readonly IIndexNameNormalizer _indexNameNormalizer; private readonly IElasticsearchClientFactory _clientFactory; + private readonly IIndexMappingProvider _indexMappingProvider; + private readonly IExpressionQueryService _expressionQueryService; private readonly IClock _clock; public ILogger Logger { protected get; set; } public ElasticsearchAuditLogManager( IClock clock, + IElasticsearchClientFactory clientFactory, IIndexNameNormalizer indexNameNormalizer, - IOptions elasticsearchOptions, - IElasticsearchClientFactory clientFactory) + IIndexMappingProvider indexMappingProvider, + IExpressionQueryService expressionQueryService) { _clock = clock; _clientFactory = clientFactory; - _elasticsearchOptions = elasticsearchOptions.Value; _indexNameNormalizer = indexNameNormalizer; + _indexMappingProvider = indexMappingProvider; + _expressionQueryService = expressionQueryService; Logger = NullLogger.Instance; } @@ -45,16 +46,12 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen ISpecification specification, CancellationToken cancellationToken = default) { - var client = _clientFactory.Create(); - var actionsIsNested = await GetActionsIsNested(client, cancellationToken); - var translator = new AuditLogExpressionQueryTranslator(actionsIsNested); - var query = translator.Translate(specification.ToExpression()); + var indexName = CreateIndex(); - var response = await client.CountAsync(dsl => - dsl.Indices(CreateIndex()).Query(query), + return await _expressionQueryService.GetCountAsync( + indexName, + specification.ToExpression(), cancellationToken); - - return response.Count; } public async virtual Task> GetListAsync( @@ -65,21 +62,41 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen bool includeDetails = false, CancellationToken cancellationToken = default) { - var client = _clientFactory.Create(); - var actionsIsNested = await GetActionsIsNested(client, cancellationToken); - var translator = new AuditLogExpressionQueryTranslator(actionsIsNested); - var query = translator.Translate(specification.ToExpression()); + var indexName = CreateIndex(); - var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) - ? SortOrder.Asc : SortOrder.Desc; - sorting = !sorting.IsNullOrWhiteSpace() - ? sorting.Split()[0] - : nameof(AuditLog.ExecutionTime); - // ES最大支持10000, 超出这个长度后升级为使用Search_After方案 + var sortingField = sorting; + if (sortingField.IsNullOrWhiteSpace()) + { + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); + if (indexMapping != null) + { + var sortingFieldMap = indexMapping.Fields + .Where(x => x.Key.Equals(sortingField, StringComparison.CurrentCultureIgnoreCase)) + .Select(x => x.Value) + .FirstOrDefault(); + if (sortingFieldMap != null) + { + sortingField = sortingFieldMap.Path; + } + } + } - return skipCount >= 10000 - ? await SearchAfterAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken) - : await SearchFromSizeAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken); + return await _expressionQueryService.GetListAsync( + indexName, + specification.ToExpression(), + sortingField, + maxResultCount, + skipCount, + sourceExcludes: includeDetails == true + ? Fields.FromFields( + [ + new Field("Actions"), + new Field("Comments"), + new Field("EntityChanges"), + new Field("Exceptions"), + ]) + : null, + cancellationToken: cancellationToken); } public async virtual Task GetCountAsync( @@ -99,9 +116,12 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen HttpStatusCode? httpStatusCode = null, CancellationToken cancellationToken = default) { + var indexName = CreateIndex(); var client = _clientFactory.Create(); + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); var querys = BuildQueryDescriptor( + indexMapping, startTime, endTime, httpMethod, @@ -118,7 +138,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen httpStatusCode); var response = await client.CountAsync(dsl => - dsl.Indices(CreateIndex()) + dsl.Indices(indexName) .Query(new BoolQuery { Must = querys @@ -149,15 +169,14 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen bool includeDetails = false, CancellationToken cancellationToken = default) { + var indexName = CreateIndex(); var client = _clientFactory.Create(); + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); - var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) - ? SortOrder.Asc : SortOrder.Desc; - sorting = !sorting.IsNullOrWhiteSpace() - ? sorting.Split()[0] - : nameof(AuditLog.ExecutionTime); + var sorts = GetOrDefaultSort(indexMapping, sorting); var querys = BuildQueryDescriptor( + indexMapping, startTime, endTime, httpMethod, @@ -176,9 +195,25 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen var query = new BoolQuery { Must = querys }; // ES最大支持10000, 超出这个长度后升级为使用Search_After方案 - return skipCount >= 10000 - ? await SearchAfterAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken) - : await SearchFromSizeAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken); + return skipCount >= 10000 && sorts != null + ? await SearchAfterAuditLogs( + client, + indexName, + query, + sorts, + maxResultCount, + skipCount, + includeDetails, + cancellationToken) + : await SearchFromSizeAuditLogs( + client, + indexName, + query, + sorts, + maxResultCount, + skipCount, + includeDetails, + cancellationToken); } public async virtual Task GetAsync( @@ -232,6 +267,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen } protected virtual List BuildQueryDescriptor( + IndexMappingInfo indexMappingInfo, DateTime? startTime = null, DateTime? endTime = null, string? httpMethod = null, @@ -251,63 +287,63 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen if (startTime.HasValue) { - queries.Add(new DateRangeQuery(GetField(nameof(AuditLog.ExecutionTime))) + queries.Add(new DateRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionTime))) { Gte = _clock.Normalize(startTime.Value) }); } if (endTime.HasValue) { - queries.Add(new DateRangeQuery(GetField(nameof(AuditLog.ExecutionTime))) + queries.Add(new DateRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionTime))) { Lte = _clock.Normalize(endTime.Value) }); } if (!httpMethod.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.HttpMethod)), httpMethod)); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.HttpMethod)), httpMethod)); } if (!url.IsNullOrWhiteSpace()) { - queries.Add(new WildcardQuery(GetField(nameof(AuditLog.Url))) + queries.Add(new WildcardQuery(GetField(indexMappingInfo, nameof(AuditLog.Url))) { Value = $"*{url}*" }); } if (userId.HasValue) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.UserId)), userId.Value.ToString())); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.UserId)), userId.Value.ToString())); } if (!userName.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.UserName)), userName)); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.UserName)), userName)); } if (!applicationName.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.ApplicationName)), applicationName)); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ApplicationName)), applicationName)); } if (!correlationId.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.CorrelationId)), correlationId)); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.CorrelationId)), correlationId)); } if (!clientId.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.ClientId)), clientId)); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ClientId)), clientId)); } if (!clientIpAddress.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.ClientIpAddress)), clientIpAddress)); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ClientIpAddress)), clientIpAddress)); } if (maxExecutionDuration.HasValue) { - queries.Add(new NumberRangeQuery(GetField(nameof(AuditLog.ExecutionDuration))) + queries.Add(new NumberRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionDuration))) { Lte = maxExecutionDuration.Value }); } if (minExecutionDuration.HasValue) { - queries.Add(new NumberRangeQuery(GetField(nameof(AuditLog.ExecutionDuration))) + queries.Add(new NumberRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionDuration))) { Gte = minExecutionDuration.Value }); @@ -318,7 +354,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen { if (hasException.Value) { - queries.Add(new ExistsQuery(GetField("Exceptions"))); + queries.Add(new ExistsQuery(GetField(indexMappingInfo, nameof(AuditLog.Exceptions)))); } else { @@ -326,7 +362,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen { MustNot = new List { - new ExistsQuery(GetField("Exceptions")) + new ExistsQuery(GetField(indexMappingInfo, nameof(AuditLog.Exceptions))) } }); } @@ -334,53 +370,32 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen if (httpStatusCode.HasValue) { - queries.Add(new TermQuery(GetField(nameof(AuditLog.HttpStatusCode)), ((int)httpStatusCode.Value).ToString())); + queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.HttpStatusCode)), ((int)httpStatusCode.Value).ToString())); } return queries; } - private async Task GetActionsIsNested(ElasticsearchClient client, CancellationToken cancellationToken = default) - { - var actionsIsNested = false; - - var response = await client.Indices.GetMappingAsync( - d => d.Indices(CreateIndex()), - cancellationToken); - - foreach (var mapping in response.Mappings) - { - if (mapping.Value.Mappings?.Properties is IDictionary properties && - properties.TryGetValue("Actions", out var actionsProperty)) - { - actionsIsNested = actionsProperty is NestedProperty; - break; - } - } - - return actionsIsNested; - } - private async Task> SearchFromSizeAuditLogs( ElasticsearchClient client, + string indexName, Query query, - string sorting, - SortOrder sortOrder, - int maxResultCount, - int skipCount, + SortOptions[]? sorts = null, + int maxResultCount = 50, + int skipCount = 0, bool includeDetails = false, CancellationToken cancellationToken = default) { var searchResponse = await client.SearchAsync(dsl => { - dsl.Indices(CreateIndex()) + dsl.Indices(indexName) .Query(query) - .Sort(s => s.Field(new FieldSort(GetField(sorting)) - { - Order = sortOrder - })) .From(skipCount) .Size(maxResultCount); + if (sorts != null) + { + dsl.Sort(sorts); + } if (!includeDetails) { @@ -402,19 +417,19 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen private async Task> SearchAfterAuditLogs( ElasticsearchClient client, + string indexName, Query query, - string sorting, - SortOrder sortOrder, - int maxResultCount, - int skipCount, + SortOptions[] sorts, + int maxResultCount = 50, + int skipCount = 0, bool includeDetails = false, CancellationToken cancellationToken = default) { var searchAfter = await GetSearchAfterValue( client, + indexName, query, - sorting, - sortOrder, + sorts, skipCount, cancellationToken); @@ -425,12 +440,9 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen var searchResponse = await client.SearchAsync(dsl => { - dsl.Indices(CreateIndex()) + dsl.Indices(indexName) .Query(query) - .Sort(s => s.Field(new FieldSort(GetField(sorting)) - { - Order = sortOrder - })) + .Sort(sorts) .Size(maxResultCount) .SearchAfter(searchAfter); @@ -454,9 +466,9 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen private async Task?> GetSearchAfterValue( ElasticsearchClient client, + string indexName, Query query, - string sorting, - SortOrder sortOrder, + SortOptions[] sorts, int skipCount, CancellationToken cancellationToken = default) { @@ -464,12 +476,9 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen if (skipCount < 10000) { var response = await client.SearchAsync( - dsl => dsl.Indices(CreateIndex()) + dsl => dsl.Indices(indexName) .Query(query) - .Sort(s => s.Field(new FieldSort(GetField(sorting)) - { - Order = sortOrder - })) + .Sort(sorts) .SourceIncludes(x => x.Id) .From(skipCount) .Size(1), @@ -486,12 +495,9 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen // 获取第9999条数据Hits作为searchAfter var firstResponse = await client.SearchAsync( - dsl => dsl.Indices(CreateIndex()) + dsl => dsl.Indices(indexName) .Query(query) - .Sort(s => s.Field(new FieldSort(GetField(sorting)) - { - Order = sortOrder - })) + .Sort(sorts) .SourceIncludes(x => x.Id) .From(9999) .Size(1), @@ -511,12 +517,9 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen var remaining = skipCount - 10000; // 获取skipCount最近一条数据作为searchAfter var secondResponse = await client.SearchAsync( - dsl => dsl.Indices(CreateIndex()) + dsl => dsl.Indices(indexName) .Query(query) - .Sort(s => s.Field(new FieldSort(GetField(sorting)) - { - Order = sortOrder - })) + .Sort(sorts) .SourceIncludes(x => x.Id) .SearchAfter(firstHit.Sort.ToList()) .Size(remaining), @@ -546,35 +549,45 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen return _indexNameNormalizer.NormalizeIndex("audit-log"); } - protected virtual string GetField(string field) + private static SortOptions[]? GetOrDefaultSort(IndexMappingInfo indexMappingInfo, string? sorting = null) { - if (_auditLogFieldMaps.TryGetValue(field, out var mapField)) + var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) + ? SortOrder.Asc : SortOrder.Desc; + sorting = !sorting.IsNullOrWhiteSpace() + ? sorting.Split()[0] + : nameof(AuditLog.ExecutionTime); + + SortOptions[]? sorts = null; + if (sorting.IsNullOrWhiteSpace()) { - return _elasticsearchOptions.FieldCamelCase ? mapField.ToCamelCase() : mapField.ToPascalCase(); + var sortingFieldMap = indexMappingInfo.Fields + .Where(x => x.Key.Equals(sorting, StringComparison.CurrentCultureIgnoreCase)) + .Select(x => x.Value) + .FirstOrDefault(); + if (sortingFieldMap != null) + { + sorting = sortingFieldMap.Path; + } + if (!sorting.IsNullOrWhiteSpace()) + { + sorts = new SortOptions[1] + { + new SortOptions + { + Field = new FieldSort(new Field(sorting)) + { + Order = sortOrder, + }, + } + }; + } } - return _elasticsearchOptions.FieldCamelCase ? field.ToCamelCase() : field.ToPascalCase(); + return sorts; } - private readonly static IDictionary _auditLogFieldMaps = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + private static string GetField(IndexMappingInfo indexMappingInfo, string fieldFullPath) { - { "Id", "Id.keyword" }, - { "ApplicationName", "ApplicationName.keyword" }, - { "UserId", "UserId.keyword" }, - { "UserName", "UserName.keyword" }, - { "TenantId", "TenantId.keyword" }, - { "TenantName", "TenantName.keyword" }, - { "ImpersonatorUserId", "ImpersonatorUserId.keyword" }, - { "ImpersonatorTenantId", "ImpersonatorTenantId.keyword" }, - { "ClientName", "ClientName.keyword" }, - { "ClientIpAddress", "ClientIpAddress.keyword" }, - { "ClientId", "ClientId.keyword" }, - { "CorrelationId", "CorrelationId.keyword" }, - { "BrowserInfo", "BrowserInfo.keyword" }, - { "HttpMethod", "HttpMethod.keyword" }, - { "Url", "Url.keyword" }, - { "ExecutionDuration", "ExecutionDuration" }, - { "ExecutionTime", "ExecutionTime" }, - { "HttpStatusCode", "HttpStatusCode" }, - }; + return indexMappingInfo.GetExactFieldPath(fieldFullPath); + } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN.Abp.AuditLogging.EntityFrameworkCore.csproj b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN.Abp.AuditLogging.EntityFrameworkCore.csproj index 8ca477253..2a9824260 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN.Abp.AuditLogging.EntityFrameworkCore.csproj +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN.Abp.AuditLogging.EntityFrameworkCore.csproj @@ -20,6 +20,7 @@ + diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs index e3b99b723..d282b7e46 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs @@ -1,4 +1,5 @@ -using System; +using LINGYUN.Linq.Dynamic.Queryable; +using System; using System.Collections.Generic; using System.Net; using System.Threading; @@ -15,6 +16,14 @@ namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; [Dependency(ReplaceServices = true)] public class EfCoreAuditLogManager : IAuditLogManager, ITransientDependency { + private readonly static Dictionary _defaultTypeMap = new Dictionary + { + [typeof(AuditLog)] = typeof(VoloAuditLog), + [typeof(AuditLogAction)] = typeof(Volo.Abp.AuditLogging.AuditLogAction), + [typeof(EntityChange)] = typeof(Volo.Abp.AuditLogging.EntityChange), + [typeof(EntityPropertyChange)] = typeof(Volo.Abp.AuditLogging.EntityPropertyChange), + }; + protected IObjectMapper ObjectMapper { get; } protected IEfCoreAuditLogRepository AuditLogRepository { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } @@ -33,7 +42,7 @@ public class EfCoreAuditLogManager : IAuditLogManager, ITransientDependency ISpecification specification, CancellationToken cancellationToken = default) { - var converter = new AuditLogExpressionQueryConverter(); + var converter = new ExpressionQueryConverter(_defaultTypeMap); var resetSpec = new ExpressionSpecification( converter.Convert(specification.ToExpression())); @@ -48,7 +57,7 @@ public class EfCoreAuditLogManager : IAuditLogManager, ITransientDependency bool includeDetails = false, CancellationToken cancellationToken = default) { - var converter = new AuditLogExpressionQueryConverter(); + var converter = new ExpressionQueryConverter(_defaultTypeMap); var resetSpec = new ExpressionSpecification( converter.Convert(specification.ToExpression())); 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/EfCoreSecurityLogManager.cs similarity index 97% rename from aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/SecurityLogManager.cs rename to aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreSecurityLogManager.cs index c3b94df0e..fb37d5e2a 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/EfCoreSecurityLogManager.cs @@ -10,13 +10,13 @@ using Volo.Abp.Uow; namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; [Dependency(ReplaceServices = true)] -public class SecurityLogManager : ISecurityLogManager, ITransientDependency +public class EfCoreSecurityLogManager : ISecurityLogManager, ITransientDependency { protected IObjectMapper ObjectMapper { get; } protected IIdentitySecurityLogRepository IdentitySecurityLogRepository { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } - public SecurityLogManager( + public EfCoreSecurityLogManager( IObjectMapper objectMapper, IIdentitySecurityLogRepository identitySecurityLogRepository, IUnitOfWorkManager unitOfWorkManager) 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 7fcdf53f9..e0dcf12a4 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 @@ -9,6 +9,7 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.ExceptionHandling; using Volo.Abp.Guids; using Volo.Abp.Modularity; +using Volo.Abp.Specifications; using Volo.Abp.Threading; namespace LINGYUN.Abp.AuditLogging; @@ -16,6 +17,7 @@ namespace LINGYUN.Abp.AuditLogging; [DependsOn( typeof(AbpAuditingModule), typeof(AbpGuidsModule), + typeof(AbpSpecificationsModule), typeof(AbpExceptionHandlingModule))] public class AbpAuditLoggingModule : AbpModule { diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs b/aspnet-core/framework/dynamic-queryable/LINGYUN.Linq.Dynamic.Queryable/LINGYUN/Linq/Dynamic/Queryable/ExpressionQueryConverter.cs similarity index 75% rename from aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs rename to aspnet-core/framework/dynamic-queryable/LINGYUN.Linq.Dynamic.Queryable/LINGYUN/Linq/Dynamic/Queryable/ExpressionQueryConverter.cs index 243ad5869..c2bff1748 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs +++ b/aspnet-core/framework/dynamic-queryable/LINGYUN.Linq.Dynamic.Queryable/LINGYUN/Linq/Dynamic/Queryable/ExpressionQueryConverter.cs @@ -4,38 +4,31 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; +namespace LINGYUN.Linq.Dynamic.Queryable; -namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; - -/// -/// 审计日志表达式树转换器 -/// -internal class AuditLogExpressionQueryConverter : ExpressionVisitor +public class ExpressionQueryConverter : ExpressionVisitor { private readonly IReadOnlyDictionary _typeMap; private readonly Dictionary _parameterMap = new(); - public AuditLogExpressionQueryConverter() - : this(BuildDefaultTypeMap()) - { - } - - public AuditLogExpressionQueryConverter(IReadOnlyDictionary typeMap) + public ExpressionQueryConverter(IReadOnlyDictionary typeMap) { _typeMap = typeMap ?? throw new ArgumentNullException(nameof(typeMap)); } - public Expression> Convert(Expression> expression) + public Expression> Convert(Expression> expression) { - ArgumentNullException.ThrowIfNull(expression); + if (expression == null) + { + throw new ArgumentNullException(nameof(expression)); + } _parameterMap.Clear(); - var rootParameter = Expression.Parameter(typeof(VoloAuditLog), expression.Parameters[0].Name); + var rootParameter = Expression.Parameter(typeof(TTarget), expression.Parameters[0].Name); _parameterMap[expression.Parameters[0]] = rootParameter; var body = Visit(expression.Body); - return Expression.Lambda>(body, rootParameter); + return Expression.Lambda>(body, rootParameter); } protected override Expression VisitLambda(Expression node) @@ -115,6 +108,19 @@ internal class AuditLogExpressionQueryConverter : ExpressionVisitor var operand = Visit(node.Operand); return Expression.Quote(operand); } + + if (node.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked) + { + var operand = Visit(node.Operand); + var resultType = _typeMap.TryGetValue(node.Type, out var mappedType) ? mappedType : node.Type; + + if (operand.Type == resultType) + { + return operand; + } + return Expression.Convert(operand, resultType); + } + return base.VisitUnary(node); } @@ -126,15 +132,4 @@ internal class AuditLogExpressionQueryConverter : ExpressionVisitor } return base.VisitConstant(node); } - - private static Dictionary BuildDefaultTypeMap() - { - return new Dictionary - { - [typeof(AuditLog)] = typeof(VoloAuditLog), - [typeof(AuditLogAction)] = typeof(Volo.Abp.AuditLogging.AuditLogAction), - [typeof(EntityChange)] = typeof(Volo.Abp.AuditLogging.EntityChange), - [typeof(EntityPropertyChange)] = typeof(Volo.Abp.AuditLogging.EntityPropertyChange), - }; - } } diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj b/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj index c7b40a454..18c69ef0e 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj @@ -20,6 +20,7 @@ + diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs index 9f392e58f..c1b1005d0 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs @@ -1,8 +1,7 @@ using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.QueryDsl; using LINGYUN.Abp.Elasticsearch; -using LINGYUN.Abp.Serilog.Enrichers.Application; -using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Linq.Dynamic.Queryable; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -16,6 +15,7 @@ using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectMapping; +using Volo.Abp.Specifications; using Volo.Abp.Timing; namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; @@ -23,12 +23,21 @@ namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; [Dependency(ReplaceServices = true)] public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDependency { - private readonly static Regex IndexFormatRegex = new Regex(@"^(.*)(?:\{0\:.+\})(.*)$"); + private readonly static Regex _indexFormatRegex = new Regex(@"^(.*)(?:\{0\:.+\})(.*)$"); + private readonly static Dictionary _defaultTypeMap = new Dictionary + { + [typeof(LogInfo)] = typeof(SerilogInfo), + [typeof(LogLevel)] = typeof(string), + [typeof(LogField)] = typeof(SerilogField), + [typeof(LogException)] = typeof(SerilogException), + }; private readonly IClock _clock; private readonly ICurrentTenant _currentTenant; private readonly AbpLoggingSerilogElasticsearchOptions _options; private readonly IElasticsearchClientFactory _clientFactory; + private readonly IIndexMappingProvider _indexMappingProvider; + private readonly IExpressionQueryService _expressionQueryService; private readonly IObjectMapper _objectMapper; public ILogger Logger { protected get; set; } @@ -38,17 +47,74 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep ICurrentTenant currentTenant, IOptions options, IElasticsearchClientFactory clientFactory, + IIndexMappingProvider indexMappingProvider, + IExpressionQueryService expressionQueryService, IObjectMapper objectMapper) { _clock = clock; _objectMapper = objectMapper; _currentTenant = currentTenant; _clientFactory = clientFactory; + _indexMappingProvider = indexMappingProvider; + _expressionQueryService = expressionQueryService; _options = options.Value; Logger = NullLogger.Instance; } + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + var indexName = CreateIndex(); + var converter = new ExpressionQueryConverter(_defaultTypeMap); + var expression = converter.Convert(specification.ToExpression()); + + return await _expressionQueryService.GetCountAsync( + indexName, + expression, + cancellationToken); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + var indexName = CreateIndex(); + var converter = new ExpressionQueryConverter(_defaultTypeMap); + var expression = converter.Convert(specification.ToExpression()); + + var sortingField = sorting; + if (sortingField.IsNullOrWhiteSpace()) + { + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); + if (indexMapping != null) + { + var sortingFieldMap = indexMapping.Fields + .Where(x => x.Key.Equals(sortingField, StringComparison.CurrentCultureIgnoreCase)) + .Select(x => x.Value) + .FirstOrDefault(); + if (sortingFieldMap != null) + { + sortingField = sortingFieldMap.Path; + } + } + } + + var serilogLogs = await _expressionQueryService.GetListAsync( + indexName, + expression, + sortingField, + maxResultCount, + skipCount, + cancellationToken: cancellationToken); + + return _objectMapper.Map, List>(serilogLogs); + } + /// /// /// @@ -59,7 +125,9 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep string id, CancellationToken cancellationToken = default) { + var indexName = CreateIndex(); var client = _clientFactory.Create(); + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); SearchResponse response; @@ -94,9 +162,9 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep (q) => q.Bool( (b) => b.Must( (s) => s.Term( - (t) => t.Field(GetField(nameof(SerilogInfo.Fields.UniqueId))).Value(id)), + (t) => t.Field(GetField(indexMapping, "fields.UniqueId")).Value(id)), (s) => s.Term( - (t) => t.Field(GetField(nameof(SerilogInfo.Fields.TenantId))).Value(_currentTenant.GetId().ToString()))))) + (t) => t.Field(GetField(indexMapping, "fields.TenantId")).Value(_currentTenant.GetId().ToString()))))) .Size(1), cancellationToken); } @@ -124,7 +192,7 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep (q) => q.Bool( (b) => b.Must( (s) => s.Term( - (t) => t.Field(GetField(nameof(SerilogInfo.Fields.UniqueId))).Value(id))))) + (t) => t.Field(GetField(indexMapping, "fields.UniqueId")).Value(id))))) .Size(1), cancellationToken); } @@ -148,9 +216,12 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep bool? hasException = null, CancellationToken cancellationToken = default) { + var indexName = CreateIndex(); var client = _clientFactory.Create(); + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); var querys = BuildQueryDescriptor( + indexMapping, startTime, endTime, level, @@ -166,7 +237,7 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep hasException); var response = await client.CountAsync((dsl) => - dsl.Indices(CreateIndex()) + dsl.Indices(indexName) .Query(log => log.Bool(b => b.Must(querys.ToArray()))), cancellationToken); @@ -215,15 +286,14 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep bool includeDetails = false, CancellationToken cancellationToken = default) { + var indexName = CreateIndex(); var client = _clientFactory.Create(); + var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); - var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) - ? SortOrder.Asc : SortOrder.Desc; - sorting = !sorting.IsNullOrWhiteSpace() - ? sorting.Split()[0] - : nameof(SerilogInfo.TimeStamp); + var sorts = GetOrDefaultSort(indexMapping, sorting); var querys = BuildQueryDescriptor( + indexMapping, startTime, endTime, level, @@ -238,21 +308,17 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep threadId, hasException); - var response = await client.SearchAsync((dsl) => - dsl.Indices(CreateIndex()) - .Query(log => - log.Bool(b => - b.Must(querys.ToArray()))) - .SourceExcludes(se => se.Exceptions) - .Sort(log => log.Field(GetField(sorting), sortOrder)) - .From(skipCount) - .Size(maxResultCount), - cancellationToken); + var query = new BoolQuery { Must = querys }; + + var serilogLogs = skipCount >= 10000 && sorts != null + ? await SearchAfterSerilogLogs(client, query, sorts, maxResultCount, skipCount, cancellationToken) + : await SearchFromSizeSerilogLogs(client, query, sorts, maxResultCount, skipCount, cancellationToken); - return _objectMapper.Map, List>(response.Documents.ToList()); + return _objectMapper.Map, List>(serilogLogs); } protected virtual List BuildQueryDescriptor( + IndexMappingInfo indexMappingInfo, DateTime? startTime = null, DateTime? endTime = null, LogLevel? level = null, @@ -271,30 +337,30 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep if (_currentTenant.IsAvailable) { - queries.Add(new TermQuery(GetField(nameof(SerilogInfo.Fields.TenantId)), _currentTenant.GetId().ToString())); + queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.TenantId"), _currentTenant.GetId().ToString())); } if (startTime.HasValue) { - queries.Add(new DateRangeQuery(GetField(nameof(SerilogInfo.TimeStamp))) + queries.Add(new DateRangeQuery(GetField(indexMappingInfo, "@timestamp")) { Gte = _clock.Normalize(startTime.Value), }); } if (endTime.HasValue) { - queries.Add(new DateRangeQuery(GetField(nameof(SerilogInfo.TimeStamp))) + queries.Add(new DateRangeQuery(GetField(indexMappingInfo, "@timestamp")) { Lte = _clock.Normalize(endTime.Value), }); } if (level.HasValue) { - queries.Add(new TermQuery(GetField(nameof(SerilogInfo.Level)), GetLogEventLevel(level.Value).ToString())); + queries.Add(new TermQuery(GetField(indexMappingInfo, "level"), GetLogEventLevel(level.Value).ToString())); } if (!machineName.IsNullOrWhiteSpace()) { // 模糊匹配 - queries.Add(new WildcardQuery(GetField(nameof(SerilogInfo.Fields.MachineName))) + queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.MachineName")) { Value = $"*{machineName}*" }); @@ -302,7 +368,7 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep if (!environment.IsNullOrWhiteSpace()) { // 模糊匹配 - queries.Add(new WildcardQuery(GetField(nameof(SerilogInfo.Fields.Environment))) + queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.EnvironmentName")) { Value = $"*{environment}*" }); @@ -310,39 +376,39 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep if (!application.IsNullOrWhiteSpace()) { // 模糊匹配 - queries.Add(new WildcardQuery(GetField(nameof(SerilogInfo.Fields.Application))) + queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.ApplicationName")) { Value = $"*{application}*" }); } if (!context.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(SerilogInfo.Fields.Context)), context)); + queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.SourceContext"), context)); } if (!requestId.IsNullOrWhiteSpace()) { - queries.Add(new TermQuery(GetField(nameof(SerilogInfo.Fields.RequestId)), requestId)); + queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.RequestId"), requestId)); } if (!requestPath.IsNullOrWhiteSpace()) { // 前缀匹配 - queries.Add(new MatchPhrasePrefixQuery(GetField(nameof(SerilogInfo.Fields.RequestPath)), requestPath)); + queries.Add(new MatchPhrasePrefixQuery(GetField(indexMappingInfo, "fields.RequestPath"), requestPath)); } if (!correlationId.IsNullOrWhiteSpace()) { // 模糊匹配 - queries.Add(new WildcardQuery(GetField(nameof(SerilogInfo.Fields.CorrelationId))) + queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.CorrelationId")) { Value = $"*{correlationId}*" }); } if (processId.HasValue) { - queries.Add(new TermQuery(GetField(nameof(SerilogInfo.Fields.ProcessId)), FieldValue.FromValue(processId.Value))); + queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.ProcessId"), FieldValue.FromValue(processId.Value))); } if (threadId.HasValue) { - queries.Add(new TermQuery(GetField(nameof(SerilogInfo.Fields.ThreadId)), FieldValue.FromValue(threadId.Value))); + queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.ThreadId"), FieldValue.FromValue(threadId.Value))); } if (hasException.HasValue) @@ -354,7 +420,7 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep "field": "exceptions" } */ - queries.Add(new ExistsQuery(GetField("Exceptions"))); + queries.Add(new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions"))); } else { @@ -374,7 +440,7 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep { MustNot = new List { - new ExistsQuery(GetField("Exceptions")) + new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions")) } }); } @@ -383,11 +449,155 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep return queries; } + private async Task> SearchFromSizeSerilogLogs( + ElasticsearchClient client, + Query query, + SortOptions[]? sorts = null, + int maxResultCount = 50, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + var searchResponse = await client.SearchAsync(dsl => + { + dsl.Indices(CreateIndex()) + .Query(query) + .From(skipCount) + .Size(maxResultCount); + if (sorts != null) + { + dsl.Sort(sorts); + } + }, cancellationToken); + + if (!searchResponse.IsSuccess()) + { + return []; + } + + return searchResponse.Documents.ToList(); + } + + private async Task> SearchAfterSerilogLogs( + ElasticsearchClient client, + Query query, + SortOptions[] sorts, + int maxResultCount = 50, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + var searchAfter = await GetSearchAfterValue( + client, + query, + sorts, + skipCount, + cancellationToken); + + if (searchAfter == null || !searchAfter.Any()) + { + return []; + } + + var searchResponse = await client.SearchAsync(dsl => + { + dsl.Indices(CreateIndex()) + .Query(query) + .Sort(sorts) + .Size(maxResultCount) + .SearchAfter(searchAfter); + }, cancellationToken); + + if (!searchResponse.IsSuccess()) + { + return []; + } + + return searchResponse.Documents.ToList(); + } + + private async Task?> GetSearchAfterValue( + ElasticsearchClient client, + Query query, + SortOptions[] sorts, + int skipCount, + CancellationToken cancellationToken = default) + { + // 10000以内直接取最后一条数据 + if (skipCount < 10000) + { + var response = await client.SearchAsync( + dsl => dsl.Indices(CreateIndex()) + .Query(query) + .Sort(sorts) + .SourceIncludes(x => x.Level) + .From(skipCount) + .Size(1), + cancellationToken); + + if (!response.IsSuccess() || response.Hits == null || !response.Hits.Any()) + { + return null; + } + + var hit = response.Hits.FirstOrDefault(); + return hit?.Sort?.ToList(); + } + + // 获取第9999条数据Hits作为searchAfter + var firstResponse = await client.SearchAsync( + dsl => dsl.Indices(CreateIndex()) + .Query(query) + .Sort(sorts) + .SourceIncludes(x => x.Level) + .From(9999) + .Size(1), + cancellationToken); + + if (!firstResponse.IsSuccess() || firstResponse.Hits == null || !firstResponse.Hits.Any()) + { + return null; + } + + var firstHit = firstResponse.Hits.FirstOrDefault(); + if (firstHit?.Sort == null || !firstHit.Sort.Any()) + { + return null; + } + + var remaining = skipCount - 10000; + // 获取skipCount最近一条数据作为searchAfter + var secondResponse = await client.SearchAsync( + dsl => dsl.Indices(CreateIndex()) + .Query(query) + .Sort(sorts) + .SourceIncludes(x => x.Level) + .SearchAfter(firstHit.Sort.ToList()) + .Size(remaining), + cancellationToken); + + if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any()) + { + return null; + } + + if (secondResponse.Hits.Count < remaining) + { + return null; + } + + var lastHit = secondResponse.Hits.LastOrDefault(); + if (lastHit?.Sort == null || !lastHit.Sort.Any()) + { + return null; + } + + return lastHit.Sort.ToList(); + } + protected virtual string CreateIndex(DateTimeOffset? offset = null) { if (!offset.HasValue) { - return IndexFormatRegex.Replace(_options.IndexFormat, @"$1*$2"); + return _indexFormatRegex.Replace(_options.IndexFormat, @"$1*$2"); } return string.Format(_options.IndexFormat, offset.Value).ToLowerInvariant(); } @@ -405,37 +615,45 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep }; } - private readonly static IDictionary _fieldMaps = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + private static SortOptions[]? GetOrDefaultSort(IndexMappingInfo indexMappingInfo, string? sorting = null) { - { "timestamp", "@timestamp" }, - { "level", "level.keyword" }, - { "machinename", $"fields.{AbpLoggingEnricherPropertyNames.MachineName}.keyword" }, - { "environment", $"fields.{AbpLoggingEnricherPropertyNames.EnvironmentName}.keyword" }, - { "application", $"fields.{AbpSerilogEnrichersConsts.ApplicationNamePropertyName}.keyword" }, - { "context", "fields.SourceContext.keyword" }, - { "actionid", "fields.ActionId.keyword" }, - { "actionname", "fields.ActionName.keyword" }, - { "requestid", "fields.RequestId.keyword" }, - { "requestpath", "fields.RequestPath" }, - { "connectionid", "fields.ConnectionId" }, - { "correlationid", "fields.CorrelationId.keyword" }, - { "clientid", "fields.ClientId.keyword" }, - { "userid", "fields.UserId.keyword" }, - { "processid", "fields.ProcessId" }, - { "threadid", "fields.ThreadId" }, - { "id", $"fields.{AbpSerilogUniqueIdConsts.UniqueIdPropertyName}" }, - { "uniqueid", $"fields.{AbpSerilogUniqueIdConsts.UniqueIdPropertyName}" }, - }; - protected virtual string GetField(string field) - { - foreach (var fieldMap in _fieldMaps) + var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) + ? SortOrder.Asc : SortOrder.Desc; + sorting = !sorting.IsNullOrWhiteSpace() + ? sorting.Split()[0] + : nameof(SerilogInfo.TimeStamp); + + SortOptions[]? sorts = null; + if (sorting.IsNullOrWhiteSpace()) { - if (field.ToLowerInvariant().Contains(fieldMap.Key)) + var sortingFieldMap = indexMappingInfo.Fields + .Where(x => x.Key.Equals(sorting, StringComparison.CurrentCultureIgnoreCase)) + .Select(x => x.Value) + .FirstOrDefault(); + if (sortingFieldMap != null) { - return fieldMap.Value; + sorting = sortingFieldMap.Path; + } + if (!sorting.IsNullOrWhiteSpace()) + { + sorts = new SortOptions[1] + { + new SortOptions + { + Field = new FieldSort(new Field(sorting)) + { + Order = sortOrder, + }, + } + }; } } - return field; + return sorts; + } + + private static string GetField(IndexMappingInfo indexMappingInfo, string fieldFullPath) + { + return indexMappingInfo.GetExactFieldPath(fieldFullPath); } } diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN.Abp.Logging.csproj b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN.Abp.Logging.csproj index 800090740..29265cbfd 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN.Abp.Logging.csproj +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN.Abp.Logging.csproj @@ -14,7 +14,7 @@ - + diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/AbpLoggingModule.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/AbpLoggingModule.cs index 25d46980a..59b9311b0 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/AbpLoggingModule.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/AbpLoggingModule.cs @@ -1,8 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Modularity; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.Logging; +[DependsOn(typeof(AbpSpecificationsModule))] public class AbpLoggingModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs index fc1725cff..01e50f8fc 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.Logging; @@ -68,4 +69,23 @@ public class DefaultLoggingManager : ILoggingManager, ISingletonDependency Logger.LogDebug("No logging manager is available!"); return Task.FromResult(new List()); } + + public Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + Logger.LogDebug("No logging manager is available!"); + return Task.FromResult(0L); + } + + public Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + Logger.LogDebug("No logging manager is available!"); + return Task.FromResult(new List()); + } } diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs index aa0e37ee6..c81f5e1c9 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.Logging; @@ -47,4 +48,15 @@ public interface ILoggingManager bool? hasException = null, bool includeDetails = false, CancellationToken cancellationToken = default); + + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + CancellationToken cancellationToken = default); } diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogException.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogException.cs index 666b2d8c8..c7a83be3a 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogException.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogException.cs @@ -1,12 +1,27 @@ -namespace LINGYUN.Abp.Logging; +using System.Text.Json.Serialization; + +namespace LINGYUN.Abp.Logging; public class LogException { + [JsonPropertyName("SourceContext")] public int Depth { get; set; } + + [JsonPropertyName("ClassName")] public string? Class { get; set; } + + [JsonPropertyName("Message")] public string? Message { get; set; } + + [JsonPropertyName("Source")] public string? Source { get; set; } + + [JsonPropertyName("StackTraceString")] public string? StackTrace { get; set; } + + [JsonPropertyName("HResult")] public int HResult { get; set; } + + [JsonPropertyName("HelpURL")] public string? HelpURL { get; set; } } diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogField.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogField.cs index 494884d03..281dca1dd 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogField.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogField.cs @@ -1,20 +1,55 @@ -namespace LINGYUN.Abp.Logging; +using System; +using System.Text.Json.Serialization; + +namespace LINGYUN.Abp.Logging; public class LogField { + [JsonPropertyName("UniqueId")] public string? Id { get; set; } + + [JsonPropertyName(AbpLoggingEnricherPropertyNames.MachineName)] public string? MachineName { get; set; } + + [JsonPropertyName(AbpLoggingEnricherPropertyNames.EnvironmentName)] public string? Environment { get; set; } + + [JsonPropertyName("ApplicationName")] public string? Application { get; set; } + + [JsonPropertyName("SourceContext")] public string? Context { get; set; } + + [JsonPropertyName("ActionId")] public string? ActionId { get; set; } + + [JsonPropertyName("ActionName")] public string? ActionName { get; set; } + + [JsonPropertyName("RequestId")] public string? RequestId { get; set; } + + [JsonPropertyName("RequestPath")] public string? RequestPath { get; set; } + + [JsonPropertyName("ConnectionId")] public string? ConnectionId { get; set; } + + [JsonPropertyName("CorrelationId")] public string? CorrelationId { get; set; } + + [JsonPropertyName("ClientId")] public string? ClientId { get; set; } + + [JsonPropertyName("UserId")] public string? UserId { get; set; } + + [JsonPropertyName("TenantId")] + public Guid? TenantId { get; set; } + + [JsonPropertyName("ProcessId")] public int? ProcessId { get; set; } + + [JsonPropertyName("ThreadId")] public int? ThreadId { get; set; } } diff --git a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogInfo.cs b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogInfo.cs index e285d6d8e..460858612 100644 --- a/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogInfo.cs +++ b/aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogInfo.cs @@ -1,14 +1,24 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace LINGYUN.Abp.Logging; public class LogInfo { + [JsonPropertyName("@timestamp")] public DateTime TimeStamp { get; set; } + + [JsonPropertyName("level")] public LogLevel Level { get; set; } + + [JsonPropertyName("message")] public string? Message { get; set; } + + [JsonPropertyName("fields")] public LogField Fields { get; set; } = default!; + + [JsonPropertyName("exceptions")] public List? Exceptions { get; set; } } diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj index a2cd57d31..f85e9f7a9 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj @@ -14,6 +14,6 @@ - + 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 3b42d7145..ce5ef4e23 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 @@ -1,6 +1,5 @@ using Elastic.Clients.Elasticsearch; using LINGYUN.Abp.Elasticsearch; -using LINGYUN.Abp.Tests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -11,7 +10,7 @@ using Volo.Abp.Modularity; namespace LINGYUN.Abp.AuditLogging.Elasticsearch { [DependsOn( - typeof(AbpTestsBaseModule), + typeof(AbpAuditLoggingTestModule), typeof(AbpAuditLoggingElasticsearchModule))] public class AbpAuditLoggingElasticsearchTestModule : AbpModule { 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 deleted file mode 100644 index 6fdc26be4..000000000 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs +++ /dev/null @@ -1,196 +0,0 @@ -using Moq.AutoMock; -using Newtonsoft.Json; -using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Auditing; -using Volo.Abp.Specifications; -using Xunit; - -namespace LINGYUN.Abp.AuditLogging.Elasticsearch -{ - public class AuditLogManagerTests : AbpAuditLoggingElasticsearchTestBase - { - private readonly IAuditLogWriter _writer; - private readonly IAuditLogManager _manager; - - public AuditLogManagerTests() - { - _writer = GetRequiredService(); - _manager = GetRequiredService(); - } - - [Fact] - public async Task Save_Audit_Log_Should_Be_Find_By_Id() - { - var mock = new AutoMocker(); - var auditLogInfo = mock.CreateInstance(); - - var id = await _writer.WriteAsync(auditLogInfo); - id.ShouldNotBeNullOrWhiteSpace(); - - var findId = Guid.Parse(id); - var auditLog = await _manager.GetAsync(findId); - - auditLog.ShouldNotBeNull(); - auditLog.Id.ShouldBe(findId); - - await _manager.DeleteAsync(findId); - } - - [Fact] - public async Task Save_Audit_Log_Should_Get_List() - { - var count = 10; - await MockcAsync(count); - - // 延迟等待ES索引完成 - await Task.Delay(5000); - - // 异常应该只有3个 - (await _manager.GetCountAsync( - hasException: true)).ShouldBe(3); - - // 正常可以查询7个 - (await _manager.GetCountAsync( - hasException: false)).ShouldBe(7); - - // POST方法能查到5个 - (await _manager.GetCountAsync( - httpMethod: "POST")).ShouldBe(5); - - (await _manager.GetCountAsync( - startTime: DateTime.Now.AddDays(-1).AddHours(5))).ShouldBe(6); - - (await _manager.GetCountAsync( - endTime: DateTime.Now.AddDays(-1))).ShouldBe(4); - - (await _manager.GetCountAsync( - startTime: DateTime.Now.AddDays(-3).AddHours(1), - endTime: DateTime.Now)).ShouldBe(8); - - // 索引5只存在一个 - (await _manager.GetCountAsync( - userName: "_user_5", - clientId: "_client_5")).ShouldBe(1); - - var logs = await _manager.GetListAsync( - userName: "_user_5", - clientId: "_client_5", - maxResultCount: count); - - logs.Count.ShouldBe(1); - logs[0].Url.ShouldBe("_url_5"); - logs[0].BrowserInfo.ShouldBe("_browser_5"); - logs[0].ApplicationName.ShouldBe("_app_5"); - - await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); - } - - [Fact] - public async Task Save_Audit_Log_Should_Get_List_With_Specification() - { - var count = 10; - await MockcAsync(count); - - // 延迟等待ES索引完成 - await Task.Delay(5000); - - // 异常应该只有3个 - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.Exceptions != null))).ShouldBe(3); - - // 请求参数中包含 AAAAA 应该只有3个 - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3); - - // 正常可以查询7个 - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.Exceptions == null))).ShouldBe(7); - - // POST方法能查到5个 - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.HttpMethod == "POST"))).ShouldBe(5); - - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6); - - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4); - - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) && - x.ExecutionTime <= DateTime.Now))).ShouldBe(8); - - // 索引5只存在一个 - (await _manager.GetCountAsync( - new ExpressionSpecification(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1); - - var logs = await _manager.GetListAsync( - new ExpressionSpecification(x => x.UserName == "_user_5" && x.ClientId == "_client_5"), - maxResultCount: count); - - logs.Count.ShouldBe(1); - logs[0].Url.ShouldBe("_url_5"); - logs[0].BrowserInfo.ShouldBe("_browser_5"); - logs[0].ApplicationName.ShouldBe("_app_5"); - - await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); - } - - protected async virtual Task> MockcAsync(int count) - { - var mock = new AutoMocker(); - - var auditLogIds = new List(); - - for (var i = 1; i <= count; i++) - { - var auditLogInfo = mock.CreateInstance(); - auditLogInfo.ClientId = $"_client_{i}"; - auditLogInfo.Url = $"_url_{i}"; - auditLogInfo.UserName = $"_user_{i}"; - auditLogInfo.ApplicationName = $"_app_{i}"; - auditLogInfo.BrowserInfo = $"_browser_{i}"; - auditLogInfo.ExecutionTime = DateTime.Now; - if (i % 3 == 0) - { - auditLogInfo.Exceptions.Add(new Exception($"_exception_{i}")); - auditLogInfo.Actions.Add( - new AuditLogActionInfo - { - ServiceName = $"_service_{i}", - MethodName = $"_method_{i}", - ExecutionTime = DateTime.Now, - ExecutionDuration = new Random().Next(1, 1000), - Parameters = JsonConvert.SerializeObject(new - { - Paramter = "AAAAA", - }), - }); - } - - if (i % 2 == 0) - { - auditLogInfo.HttpMethod = "POST"; - } - - if (i % 4 == 0) - { - auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-3); - } - - if (i % 5 == 0) - { - auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-2); - } - - auditLogIds.Add(await _writer.WriteAsync(auditLogInfo)); - } - - return auditLogIds; - } - } -} diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager_Tests.cs new file mode 100644 index 000000000..7e3d0b5ec --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager_Tests.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AuditLogging.Elasticsearch; + +public class ElasticsearchAuditLogManager_Tests : AuditLogManager_Tests +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN.Abp.AuditLogging.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN.Abp.AuditLogging.Tests.csproj new file mode 100644 index 000000000..10733d813 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN.Abp.AuditLogging.Tests.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + + false + AnyCPU + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestBase.cs new file mode 100644 index 000000000..345fb46aa --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestBase.cs @@ -0,0 +1,8 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.AuditLogging; + +public abstract class AbpAuditLoggingTestBase : AbpTestsBase +{ + +} diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestModule.cs new file mode 100644 index 000000000..f0da4f9e7 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestModule.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.Tests; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AuditLogging; + +[DependsOn( + typeof(AbpTestsBaseModule), + typeof(AbpAuditLoggingModule))] +public class AbpAuditLoggingTestModule : AbpModule +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AuditLogManager_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AuditLogManager_Tests.cs new file mode 100644 index 000000000..6b9778917 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AuditLogManager_Tests.cs @@ -0,0 +1,198 @@ +using Moq.AutoMock; +using Newtonsoft.Json; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Auditing; +using Volo.Abp.Modularity; +using Volo.Abp.Specifications; +using Volo.Abp.Testing; +using Xunit; + +namespace LINGYUN.Abp.AuditLogging; + +public abstract class AuditLogManager_Tests : AbpIntegratedTest + where TStartupModule : IAbpModule +{ + private readonly IAuditLogWriter _writer; + private readonly IAuditLogManager _manager; + + public AuditLogManager_Tests() + { + _writer = GetRequiredService(); + _manager = GetRequiredService(); + } + + [Fact] + public async Task Save_Audit_Log_Should_Be_Find_By_Id() + { + var mock = new AutoMocker(); + var auditLogInfo = mock.CreateInstance(); + + var id = await _writer.WriteAsync(auditLogInfo); + id.ShouldNotBeNullOrWhiteSpace(); + + var findId = Guid.Parse(id); + var auditLog = await _manager.GetAsync(findId); + + auditLog.ShouldNotBeNull(); + auditLog.Id.ShouldBe(findId); + + await _manager.DeleteAsync(findId); + } + + [Fact] + public async Task Save_Audit_Log_Should_Get_List() + { + var count = 10; + await MockcAsync(count); + + // 延迟等待写入完成 + await Task.Delay(5000); + + // 异常应该只有3个 + (await _manager.GetCountAsync( + hasException: true)).ShouldBe(3); + + // 正常可以查询7个 + (await _manager.GetCountAsync( + hasException: false)).ShouldBe(7); + + // POST方法能查到5个 + (await _manager.GetCountAsync( + httpMethod: "POST")).ShouldBe(5); + + (await _manager.GetCountAsync( + startTime: DateTime.Now.AddDays(-1).AddHours(5))).ShouldBe(6); + + (await _manager.GetCountAsync( + endTime: DateTime.Now.AddDays(-1))).ShouldBe(4); + + (await _manager.GetCountAsync( + startTime: DateTime.Now.AddDays(-3).AddHours(1), + endTime: DateTime.Now)).ShouldBe(8); + + // 索引5只存在一个 + (await _manager.GetCountAsync( + userName: "_user_5", + clientId: "_client_5")).ShouldBe(1); + + var logs = await _manager.GetListAsync( + userName: "_user_5", + clientId: "_client_5", + maxResultCount: count); + + logs.Count.ShouldBe(1); + logs[0].Url.ShouldBe("_url_5"); + logs[0].BrowserInfo.ShouldBe("_browser_5"); + logs[0].ApplicationName.ShouldBe("_app_5"); + + await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); + } + + [Fact] + public async Task Save_Audit_Log_Should_Get_List_With_Specification() + { + var count = 10; + await MockcAsync(count); + + // 延迟等待写入完成 + await Task.Delay(5000); + + // 请求参数中包含 AAAAA 应该只有3个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3); + + // 异常应该只有3个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.Exceptions != null))).ShouldBe(3); + + // 正常可以查询7个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.Exceptions == null))).ShouldBe(7); + + // POST方法能查到5个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.HttpMethod == "POST"))).ShouldBe(5); + + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6); + + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4); + + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) && + x.ExecutionTime <= DateTime.Now))).ShouldBe(8); + + // 索引5只存在一个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1); + + var logs = await _manager.GetListAsync( + new ExpressionSpecification(x => x.UserName == "_user_5" && x.ClientId == "_client_5"), + maxResultCount: count); + + logs.Count.ShouldBe(1); + logs[0].Url.ShouldBe("_url_5"); + logs[0].BrowserInfo.ShouldBe("_browser_5"); + logs[0].ApplicationName.ShouldBe("_app_5"); + + await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); + } + + protected async virtual Task> MockcAsync(int count) + { + var mock = new AutoMocker(); + + var auditLogIds = new List(); + + for (var i = 1; i <= count; i++) + { + var auditLogInfo = mock.CreateInstance(); + auditLogInfo.ClientId = $"_client_{i}"; + auditLogInfo.Url = $"_url_{i}"; + auditLogInfo.UserName = $"_user_{i}"; + auditLogInfo.ApplicationName = $"_app_{i}"; + auditLogInfo.BrowserInfo = $"_browser_{i}"; + auditLogInfo.ExecutionTime = DateTime.Now; + if (i % 3 == 0) + { + auditLogInfo.Exceptions.Add(new Exception($"_exception_{i}")); + auditLogInfo.Actions.Add( + new AuditLogActionInfo + { + ServiceName = $"_service_{i}", + MethodName = $"_method_{i}", + ExecutionTime = DateTime.Now, + ExecutionDuration = new Random().Next(1, 1000), + Parameters = JsonConvert.SerializeObject(new + { + Paramter = "AAAAA", + }), + }); + } + + if (i % 2 == 0) + { + auditLogInfo.HttpMethod = "POST"; + } + + if (i % 4 == 0) + { + auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-3); + } + + if (i % 5 == 0) + { + auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-2); + } + + auditLogIds.Add(await _writer.WriteAsync(auditLogInfo)); + } + + return auditLogIds; + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests.csproj new file mode 100644 index 000000000..a38587e92 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + + false + AnyCPU + + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestBase.cs new file mode 100644 index 000000000..844c91503 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestBase.cs @@ -0,0 +1,7 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; + +public abstract class AbpLoggingSerilogElasticsearchTestBase : AbpTestsBase +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestModule.cs new file mode 100644 index 000000000..8fa1b3475 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestModule.cs @@ -0,0 +1,51 @@ +using Elastic.Clients.Elasticsearch; +using LINGYUN.Abp.Elasticsearch; +using LINGYUN.Abp.Tests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using Volo.Abp; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; + +[DependsOn( + typeof(AbpTestsBaseModule), + typeof(AbpLoggingTestModule), + typeof(AbpLoggingSerilogElasticsearchModule))] +public class AbpLoggingSerilogElasticsearchTestModule : AbpModule +{ + private const string UserSecretsId = "11A604D4-3A64-4F92-94C6-5B1525CF63DD"; + + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder => + { + builder.AddUserSecrets(UserSecretsId); + })); + } + + public override void OnPostApplicationInitialization(ApplicationInitializationContext context) + { + RemoveTestIndexs(context.ServiceProvider); + } + + public override void OnApplicationShutdown(ApplicationShutdownContext context) + { + RemoveTestIndexs(context.ServiceProvider); + } + + private static void RemoveTestIndexs(IServiceProvider serviceProvider) + { + var clientFactory = serviceProvider.GetRequiredService(); + var client = clientFactory.Create(); + var indicesResponse = client.Indices.Get("abp-test-logging"); + if (indicesResponse.IsSuccess()) + { + foreach (var index in indicesResponse.Indices) + { + client.Indices.Delete(index.Key); + } + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/ElasticsearchLoggingManager_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/ElasticsearchLoggingManager_Tests.cs new file mode 100644 index 000000000..16fa8483f --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/ElasticsearchLoggingManager_Tests.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using NSubstitute.Extensions; +using Serilog; + +namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; + +public class ElasticsearchLoggingManager_Tests : LoggingManager_Tests +{ + protected override void BeforeAddApplication(IServiceCollection services) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + .Enrich.WithUniqueId() + .WriteTo.Elasticsearch( + nodeUris: "http://localhost:9200", + indexFormat: "abp-test-logging") + .CreateLogger(); + + services.AddLogging(logging => + { + logging.AddSerilog(); + }); + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN.Abp.Logging.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN.Abp.Logging.Tests.csproj new file mode 100644 index 000000000..dec2b41e4 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN.Abp.Logging.Tests.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + + false + AnyCPU + + + + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestBase.cs new file mode 100644 index 000000000..c69652127 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestBase.cs @@ -0,0 +1,7 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.Logging; + +public abstract class AbpLoggingTestBase : AbpTestsBase +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestModule.cs new file mode 100644 index 000000000..e41b102eb --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestModule.cs @@ -0,0 +1,14 @@ +using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Abp.Tests; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Logging; + +[DependsOn( + typeof(AbpTestsBaseModule), + typeof(AbpLoggingModule), + typeof(AbpSerilogEnrichersUniqueIdModule))] +public class AbpLoggingTestModule : AbpModule +{ + +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/LoggingManager_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/LoggingManager_Tests.cs new file mode 100644 index 000000000..49ae03bd8 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/LoggingManager_Tests.cs @@ -0,0 +1,107 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Sinks.InMemory; +using Shouldly; +using System.Threading.Tasks; +using Volo.Abp.Modularity; +using Volo.Abp.Specifications; +using Volo.Abp.Testing; +using Xunit; + +namespace LINGYUN.Abp.Logging; + +public abstract class LoggingManager_Tests : AbpIntegratedTest + where TStartupModule : IAbpModule +{ + private readonly string _context; + private readonly Microsoft.Extensions.Logging.ILogger _logger; + private readonly ILoggingManager _manager; + + public LoggingManager_Tests() + { + _manager = GetRequiredService(); + + _context = GetType().FullName!; + var loggerFactory = GetRequiredService(); + _logger = loggerFactory.CreateLogger(_context); + } + + protected override void BeforeAddApplication(IServiceCollection services) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + .Enrich.WithUniqueId() + .WriteTo.InMemory() + .CreateLogger(); + + services.AddLogging(logging => + { + logging.AddSerilog(); + }); + } + + [Fact] + public async Task Should_Get_List() + { + _logger.LogDebug("xunit test debug log"); + _logger.LogInformation("xunit test information log"); + _logger.LogWarning("xunit test warning log"); + _logger.LogError("xunit test error log"); + + await Log.CloseAndFlushAsync(); + + await Task.Delay(5000); + + (await _manager.GetCountAsync(context: _context)).ShouldBe(4); + + (await _manager.GetCountAsync(level: LogLevel.Information, context: _context)).ShouldBe(1); + + var logs = await _manager.GetListAsync(level: LogLevel.Information, context: _context); + logs.Count.ShouldBe(1); + logs[0].Level.ShouldBe(LogLevel.Information); + logs[0].Message.ShouldBe("xunit test information log"); + logs[0].Fields.ShouldNotBeNull(); + logs[0].Fields.Id.ShouldNotBeNullOrWhiteSpace(); + logs[0].Fields.Context.ShouldBe(_context); + + var log = await _manager.GetAsync(logs[0].Fields.Id); + log.Message.ShouldBe("xunit test information log"); + log.Fields.ShouldNotBeNull(); + log.Fields.Id.ShouldNotBeNullOrWhiteSpace(); + log.Fields.Id.ShouldBe(logs[0].Fields.Id); + log.Fields.Context.ShouldBe(_context); + } + + [Fact] + public async Task Should_Get_List_With_Specification() + { + _logger.LogDebug("xunit test debug log"); + _logger.LogInformation("xunit test information log"); + _logger.LogWarning("xunit test warning log"); + _logger.LogError("xunit test error log"); + + await Log.CloseAndFlushAsync(); + + await Task.Delay(5000); + + var specification = new ExpressionSpecification( + x => x.Level == LogLevel.Information && x.Fields.Context == _context); + + var logs = await _manager.GetListAsync(specification); + logs.Count.ShouldBe(1); + logs[0].Level.ShouldBe(LogLevel.Information); + logs[0].Message.ShouldBe("xunit test information log"); + logs[0].Fields.ShouldNotBeNull(); + logs[0].Fields.Id.ShouldNotBeNullOrWhiteSpace(); + logs[0].Fields.Context.ShouldBe(_context); + + var log = await _manager.GetAsync(logs[0].Fields.Id); + log.Message.ShouldBe("xunit test information log"); + log.Fields.ShouldNotBeNull(); + log.Fields.Id.ShouldNotBeNullOrWhiteSpace(); + log.Fields.Id.ShouldBe(logs[0].Fields.Id); + log.Fields.Context.ShouldBe(_context); + } +}