Browse Source

feat: Optimize the log module

pull/1553/head
colin 1 week ago
parent
commit
5481ca891a
  1. 374
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs
  2. 2
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLoggingIndexInitializer.cs
  3. 279
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs
  4. 1
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN.Abp.AuditLogging.EntityFrameworkCore.csproj
  5. 15
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs
  6. 4
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreSecurityLogManager.cs
  7. 2
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AbpAuditLoggingModule.cs
  8. 51
      aspnet-core/framework/dynamic-queryable/LINGYUN.Linq.Dynamic.Queryable/LINGYUN/Linq/Dynamic/Queryable/ExpressionQueryConverter.cs
  9. 1
      aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj
  10. 348
      aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs
  11. 2
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN.Abp.Logging.csproj
  12. 2
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/AbpLoggingModule.cs
  13. 20
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs
  14. 12
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs
  15. 17
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogException.cs
  16. 37
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogField.cs
  17. 10
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/LogInfo.cs
  18. 2
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj
  19. 3
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs
  20. 196
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs
  21. 5
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager_Tests.cs
  22. 19
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN.Abp.AuditLogging.Tests.csproj
  23. 8
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestBase.cs
  24. 11
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AbpAuditLoggingTestModule.cs
  25. 198
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN/Abp/AuditLogging/AuditLogManager_Tests.cs
  26. 20
      aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests.csproj
  27. 7
      aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestBase.cs
  28. 51
      aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/AbpLoggingSerilogElasticsearchTestModule.cs
  29. 25
      aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN/Abp/Logging/Serilog/Elasticsearch/ElasticsearchLoggingManager_Tests.cs
  30. 22
      aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN.Abp.Logging.Tests.csproj
  31. 7
      aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestBase.cs
  32. 14
      aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/AbpLoggingTestModule.cs
  33. 107
      aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN/Abp/Logging/LoggingManager_Tests.cs

374
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs

@ -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;
/// <summary>
/// ES DSL表达式翻译器:把 <see cref="Expression{TDelegate}"/> 翻译为
/// Elastic.Clients.Elasticsearch 的 <see cref="Query"/>(QueryDsl)。
/// <para>
/// 支持的算子(超出即抛 <see cref="NotSupportedException"/>,fail loud):
/// <list type="bullet">
/// <item>逻辑:&amp;&amp;、||、!(映射为 bool filter / should+minimum_should_match / must_not)</item>
/// <item>比较:==、!=、&gt;、&gt;=、&lt;、&lt;=(数值与日期映射为 term / range)</item>
/// <item>null 判断:字段 == null / != null(映射为 must_not exists / exists)</item>
/// <item>字符串:Contains / StartsWith / EndsWith(映射为 wildcard)、Equals(映射为 term)</item>
/// <item>集合:x.Actions.Any(predicate)(映射为 nested 查询或扁平字段展开)</item>
/// <item>常量:true / false(映射为 match_all / match_none)</item>
/// </list>
/// </para>
/// </summary>
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<Func<AuditLog, bool>> 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<string>();
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);
}
}

2
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));

279
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<ElasticsearchAuditLogManager> Logger { protected get; set; }
public ElasticsearchAuditLogManager(
IClock clock,
IElasticsearchClientFactory clientFactory,
IIndexNameNormalizer indexNameNormalizer,
IOptions<AbpElasticsearchOptions> elasticsearchOptions,
IElasticsearchClientFactory clientFactory)
IIndexMappingProvider indexMappingProvider,
IExpressionQueryService expressionQueryService)
{
_clock = clock;
_clientFactory = clientFactory;
_elasticsearchOptions = elasticsearchOptions.Value;
_indexNameNormalizer = indexNameNormalizer;
_indexMappingProvider = indexMappingProvider;
_expressionQueryService = expressionQueryService;
Logger = NullLogger<ElasticsearchAuditLogManager>.Instance;
}
@ -45,16 +46,12 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
ISpecification<AuditLog> 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<AuditLog>(dsl =>
dsl.Indices(CreateIndex()).Query(query),
return await _expressionQueryService.GetCountAsync(
indexName,
specification.ToExpression(),
cancellationToken);
return response.Count;
}
public async virtual Task<List<AuditLog>> 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<long> 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<AuditLog>(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<AuditLog?> GetAsync(
@ -232,6 +267,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
}
protected virtual List<Query> 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<Query>
{
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<bool> GetActionsIsNested(ElasticsearchClient client, CancellationToken cancellationToken = default)
{
var actionsIsNested = false;
var response = await client.Indices.GetMappingAsync<AuditLog>(
d => d.Indices(CreateIndex()),
cancellationToken);
foreach (var mapping in response.Mappings)
{
if (mapping.Value.Mappings?.Properties is IDictionary<PropertyName, IProperty> properties &&
properties.TryGetValue("Actions", out var actionsProperty))
{
actionsIsNested = actionsProperty is NestedProperty;
break;
}
}
return actionsIsNested;
}
private async Task<List<AuditLog>> 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<AuditLog>(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<List<AuditLog>> 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<AuditLog>(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<List<FieldValue>?> 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<AuditLog>(
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<AuditLog>(
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<AuditLog>(
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<string, string> _auditLogFieldMaps = new Dictionary<string, string>(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);
}
}

1
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN.Abp.AuditLogging.EntityFrameworkCore.csproj

@ -20,6 +20,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\dynamic-queryable\LINGYUN.Linq.Dynamic.Queryable\LINGYUN.Linq.Dynamic.Queryable.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.AuditLogging\LINGYUN.Abp.AuditLogging.csproj" />
</ItemGroup>

15
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<Type, Type> _defaultTypeMap = new Dictionary<Type, Type>
{
[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<AbpAuditLoggingEntityFrameworkCoreModule> ObjectMapper { get; }
protected IEfCoreAuditLogRepository AuditLogRepository { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
@ -33,7 +42,7 @@ public class EfCoreAuditLogManager : IAuditLogManager, ITransientDependency
ISpecification<AuditLog> specification,
CancellationToken cancellationToken = default)
{
var converter = new AuditLogExpressionQueryConverter();
var converter = new ExpressionQueryConverter<AuditLog, VoloAuditLog>(_defaultTypeMap);
var resetSpec = new ExpressionSpecification<VoloAuditLog>(
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<AuditLog, VoloAuditLog>(_defaultTypeMap);
var resetSpec = new ExpressionSpecification<VoloAuditLog>(
converter.Convert(specification.ToExpression()));

4
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/SecurityLogManager.cs → 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<AbpAuditLoggingEntityFrameworkCoreModule> ObjectMapper { get; }
protected IIdentitySecurityLogRepository IdentitySecurityLogRepository { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public SecurityLogManager(
public EfCoreSecurityLogManager(
IObjectMapper<AbpAuditLoggingEntityFrameworkCoreModule> objectMapper,
IIdentitySecurityLogRepository identitySecurityLogRepository,
IUnitOfWorkManager unitOfWorkManager)

2
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
{

51
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs → 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;
/// <summary>
/// 审计日志表达式树转换器
/// </summary>
internal class AuditLogExpressionQueryConverter : ExpressionVisitor
public class ExpressionQueryConverter<TSource, TTarget> : ExpressionVisitor
{
private readonly IReadOnlyDictionary<Type, Type> _typeMap;
private readonly Dictionary<ParameterExpression, ParameterExpression> _parameterMap = new();
public AuditLogExpressionQueryConverter()
: this(BuildDefaultTypeMap())
{
}
public AuditLogExpressionQueryConverter(IReadOnlyDictionary<Type, Type> typeMap)
public ExpressionQueryConverter(IReadOnlyDictionary<Type, Type> typeMap)
{
_typeMap = typeMap ?? throw new ArgumentNullException(nameof(typeMap));
}
public Expression<Func<VoloAuditLog, bool>> Convert(Expression<Func<AuditLog, bool>> expression)
public Expression<Func<TTarget, bool>> Convert(Expression<Func<TSource, bool>> 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<Func<VoloAuditLog, bool>>(body, rootParameter);
return Expression.Lambda<Func<TTarget, bool>>(body, rootParameter);
}
protected override Expression VisitLambda<T>(Expression<T> 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<Type, Type> BuildDefaultTypeMap()
{
return new Dictionary<Type, Type>
{
[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),
};
}
}

1
aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj

@ -20,6 +20,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\dynamic-queryable\LINGYUN.Linq.Dynamic.Queryable\LINGYUN.Linq.Dynamic.Queryable.csproj" />
<ProjectReference Include="..\..\elasticsearch\LINGYUN.Abp.Elasticsearch\LINGYUN.Abp.Elasticsearch.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.Logging\LINGYUN.Abp.Logging.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.Serilog.Enrichers.Application\LINGYUN.Abp.Serilog.Enrichers.Application.csproj" />

348
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<Type, Type> _defaultTypeMap = new Dictionary<Type, Type>
{
[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<AbpLoggingSerilogElasticsearchModule> _objectMapper;
public ILogger<SerilogElasticsearchLoggingManager> Logger { protected get; set; }
@ -38,17 +47,74 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
ICurrentTenant currentTenant,
IOptions<AbpLoggingSerilogElasticsearchOptions> options,
IElasticsearchClientFactory clientFactory,
IIndexMappingProvider indexMappingProvider,
IExpressionQueryService expressionQueryService,
IObjectMapper<AbpLoggingSerilogElasticsearchModule> objectMapper)
{
_clock = clock;
_objectMapper = objectMapper;
_currentTenant = currentTenant;
_clientFactory = clientFactory;
_indexMappingProvider = indexMappingProvider;
_expressionQueryService = expressionQueryService;
_options = options.Value;
Logger = NullLogger<SerilogElasticsearchLoggingManager>.Instance;
}
public async virtual Task<long> GetCountAsync(
ISpecification<LogInfo> specification,
CancellationToken cancellationToken = default)
{
var indexName = CreateIndex();
var converter = new ExpressionQueryConverter<LogInfo, SerilogInfo>(_defaultTypeMap);
var expression = converter.Convert(specification.ToExpression());
return await _expressionQueryService.GetCountAsync(
indexName,
expression,
cancellationToken);
}
public async virtual Task<List<LogInfo>> GetListAsync(
ISpecification<LogInfo> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
CancellationToken cancellationToken = default)
{
var indexName = CreateIndex();
var converter = new ExpressionQueryConverter<LogInfo, SerilogInfo>(_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<SerilogInfo>, List<LogInfo>>(serilogLogs);
}
/// <summary>
///
/// </summary>
@ -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<SerilogInfo> 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<SerilogInfo>((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<SerilogInfo>((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<SerilogInfo>, List<LogInfo>>(response.Documents.ToList());
return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs);
}
protected virtual List<Query> 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<Query>
{
new ExistsQuery(GetField("Exceptions"))
new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions"))
}
});
}
@ -383,11 +449,155 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
return queries;
}
private async Task<List<SerilogInfo>> SearchFromSizeSerilogLogs(
ElasticsearchClient client,
Query query,
SortOptions[]? sorts = null,
int maxResultCount = 50,
int skipCount = 0,
CancellationToken cancellationToken = default)
{
var searchResponse = await client.SearchAsync<SerilogInfo>(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<List<SerilogInfo>> 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<SerilogInfo>(dsl =>
{
dsl.Indices(CreateIndex())
.Query(query)
.Sort(sorts)
.Size(maxResultCount)
.SearchAfter(searchAfter);
}, cancellationToken);
if (!searchResponse.IsSuccess())
{
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client,
Query query,
SortOptions[] sorts,
int skipCount,
CancellationToken cancellationToken = default)
{
// 10000以内直接取最后一条数据
if (skipCount < 10000)
{
var response = await client.SearchAsync<SerilogInfo>(
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<SerilogInfo>(
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<SerilogInfo>(
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<string, string> _fieldMaps = new Dictionary<string, string>(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);
}
}

2
aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN.Abp.Logging.csproj

@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Core" />
<PackageReference Include="Volo.Abp.Specifications" />
</ItemGroup>
</Project>

2
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)

20
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<LogInfo>());
}
public Task<long> GetCountAsync(
ISpecification<LogInfo> specification,
CancellationToken cancellationToken = default)
{
Logger.LogDebug("No logging manager is available!");
return Task.FromResult(0L);
}
public Task<List<LogInfo>> GetListAsync(
ISpecification<LogInfo> 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<LogInfo>());
}
}

12
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<long> GetCountAsync(
ISpecification<LogInfo> specification,
CancellationToken cancellationToken = default);
Task<List<LogInfo>> GetListAsync(
ISpecification<LogInfo> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
CancellationToken cancellationToken = default);
}

17
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; }
}

37
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; }
}

10
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<LogException>? Exceptions { get; set; }
}

2
aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj

@ -14,6 +14,6 @@
<ItemGroup>
<ProjectReference Include="..\..\framework\auditing\LINGYUN.Abp.AuditLogging.Elasticsearch\LINGYUN.Abp.AuditLogging.Elasticsearch.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.AuditLogging.Tests\LINGYUN.Abp.AuditLogging.Tests.csproj" />
</ItemGroup>
</Project>

3
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
{

196
aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs

@ -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<IAuditLogWriter>();
_manager = GetRequiredService<IAuditLogManager>();
}
[Fact]
public async Task Save_Audit_Log_Should_Be_Find_By_Id()
{
var mock = new AutoMocker();
var auditLogInfo = mock.CreateInstance<AuditLogInfo>();
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<AuditLog>(x => x.Exceptions != null))).ShouldBe(3);
// 请求参数中包含 AAAAA 应该只有3个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3);
// 正常可以查询7个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.Exceptions == null))).ShouldBe(7);
// POST方法能查到5个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.HttpMethod == "POST"))).ShouldBe(5);
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6);
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4);
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) &&
x.ExecutionTime <= DateTime.Now))).ShouldBe(8);
// 索引5只存在一个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1);
var logs = await _manager.GetListAsync(
new ExpressionSpecification<AuditLog>(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<List<string>> MockcAsync(int count)
{
var mock = new AutoMocker();
var auditLogIds = new List<string>();
for (var i = 1; i <= count; i++)
{
var auditLogInfo = mock.CreateInstance<AuditLogInfo>();
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;
}
}
}

5
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<AbpAuditLoggingElasticsearchTestModule>
{
}

19
aspnet-core/tests/LINGYUN.Abp.AuditLogging.Tests/LINGYUN.Abp.AuditLogging.Tests.csproj

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<IsPackable>false</IsPackable>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq.AutoMock" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\framework\auditing\LINGYUN.Abp.AuditLogging\LINGYUN.Abp.AuditLogging.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" />
</ItemGroup>
</Project>

8
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<AbpAuditLoggingTestModule>
{
}

11
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
{
}

198
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<TStartupModule> : AbpIntegratedTest<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IAuditLogWriter _writer;
private readonly IAuditLogManager _manager;
public AuditLogManager_Tests()
{
_writer = GetRequiredService<IAuditLogWriter>();
_manager = GetRequiredService<IAuditLogManager>();
}
[Fact]
public async Task Save_Audit_Log_Should_Be_Find_By_Id()
{
var mock = new AutoMocker();
var auditLogInfo = mock.CreateInstance<AuditLogInfo>();
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<AuditLog>(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3);
// 异常应该只有3个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.Exceptions != null))).ShouldBe(3);
// 正常可以查询7个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.Exceptions == null))).ShouldBe(7);
// POST方法能查到5个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.HttpMethod == "POST"))).ShouldBe(5);
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6);
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4);
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) &&
x.ExecutionTime <= DateTime.Now))).ShouldBe(8);
// 索引5只存在一个
(await _manager.GetCountAsync(
new ExpressionSpecification<AuditLog>(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1);
var logs = await _manager.GetListAsync(
new ExpressionSpecification<AuditLog>(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<List<string>> MockcAsync(int count)
{
var mock = new AutoMocker();
var auditLogIds = new List<string>();
for (var i = 1; i <= count; i++)
{
var auditLogInfo = mock.CreateInstance<AuditLogInfo>();
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;
}
}

20
aspnet-core/tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests/LINGYUN.Abp.Logging.Serilog.Elasticsearch.Tests.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<IsPackable>false</IsPackable>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq.AutoMock" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\framework\logging\LINGYUN.Abp.Logging.Serilog.Elasticsearch\LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.Logging.Tests\LINGYUN.Abp.Logging.Tests.csproj" />
</ItemGroup>
</Project>

7
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<AbpLoggingSerilogElasticsearchTestModule>
{
}

51
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<IElasticsearchClientFactory>();
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);
}
}
}
}

25
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<AbpLoggingSerilogElasticsearchTestModule>
{
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();
});
}
}

22
aspnet-core/tests/LINGYUN.Abp.Logging.Tests/LINGYUN.Abp.Logging.Tests.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<IsPackable>false</IsPackable>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq.AutoMock" />
<PackageReference Include="Serilog.Sinks.InMemory" />
<PackageReference Include="Serilog.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\framework\logging\LINGYUN.Abp.Logging\LINGYUN.Abp.Logging.csproj" />
<ProjectReference Include="..\..\framework\logging\LINGYUN.Abp.Serilog.Enrichers.UniqueId\LINGYUN.Abp.Serilog.Enrichers.UniqueId.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" />
</ItemGroup>
</Project>

7
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<AbpLoggingTestModule>
{
}

14
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
{
}

107
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<TStartupModule> : AbpIntegratedTest<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly string _context;
private readonly Microsoft.Extensions.Logging.ILogger _logger;
private readonly ILoggingManager _manager;
public LoggingManager_Tests()
{
_manager = GetRequiredService<ILoggingManager>();
_context = GetType().FullName!;
var loggerFactory = GetRequiredService<ILoggerFactory>();
_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<LogInfo>(
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);
}
}
Loading…
Cancel
Save