Browse Source

feat: Optimize the audit log module

- Increase by Specification the query of audit logs.
- Optimize the query of ES audit logs. Use the "search_after" feature when the number of records exceeds 10,000.
pull/1551/head
colin 2 weeks ago
parent
commit
bc293b54f9
  1. 1
      aspnet-core/LINGYUN.MicroService.All.slnx
  2. 374
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs
  3. 294
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs
  4. 7
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs
  5. 4
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IsExternalInit.cs
  6. 13
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreModule.cs
  7. 140
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs
  8. 49
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs
  9. 48
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs
  10. 4
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs
  11. 24
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/IEfCoreAuditLogRepository.cs
  12. 1
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj
  13. 21
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/DefaultAuditLogManager.cs
  14. 14
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs
  15. 2
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs
  16. 4
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs
  17. 1
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj
  18. 34
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs
  19. 86
      aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs

1
aspnet-core/LINGYUN.MicroService.All.slnx

@ -524,6 +524,7 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/LINGYUN.Abp.Aliyun.Tests/LINGYUN.Abp.Aliyun.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.BlobStoring.Aliyun.Tests/LINGYUN.Abp.BlobStoring.Aliyun.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.BlobStoring.Nexus.Tests/LINGYUN.Abp.BlobStoring.Nexus.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.DataProtection.Tests/LINGYUN.Abp.DataProtection.Tests.csproj" />

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

@ -0,0 +1,374 @@
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);
}
}

294
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs

@ -1,4 +1,5 @@
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Mapping;
using Elastic.Clients.Elasticsearch.QueryDsl;
using LINGYUN.Abp.Elasticsearch;
using Microsoft.Extensions.Logging;
@ -11,6 +12,7 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Specifications;
using Volo.Abp.Timing;
namespace LINGYUN.Abp.AuditLogging.Elasticsearch;
@ -39,6 +41,46 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
Logger = NullLogger<ElasticsearchAuditLogManager>.Instance;
}
public async virtual Task<long> GetCountAsync(
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 response = await client.CountAsync<AuditLog>(dsl =>
dsl.Indices(CreateIndex()).Query(query),
cancellationToken);
return response.Count;
}
public async virtual Task<List<AuditLog>> GetListAsync(
ISpecification<AuditLog> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
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 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方案
return skipCount >= 10000
? await SearchAfterAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken)
: await SearchFromSizeAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken);
}
public async virtual Task<long> GetCountAsync(
DateTime? startTime = null,
@ -131,32 +173,12 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
hasException,
httpStatusCode);
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(CreateIndex())
.Query(new BoolQuery
{
Must = querys
})
.Sort(s => s.Field(new FieldSort(GetField(sorting))
{
Order = sortOrder
}))
.From(skipCount)
.Size(maxResultCount);
var query = new BoolQuery { Must = querys };
// 字段过滤
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
return searchResponse.Documents.ToList();
// 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);
}
public async virtual Task<AuditLog?> GetAsync(
@ -318,12 +340,223 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
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,
Query query,
string sorting,
SortOrder sortOrder,
int maxResultCount,
int skipCount,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(CreateIndex())
.Query(query)
.Sort(s => s.Field(new FieldSort(GetField(sorting))
{
Order = sortOrder
}))
.From(skipCount)
.Size(maxResultCount);
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
if (!searchResponse.IsSuccess())
{
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<AuditLog>> SearchAfterAuditLogs(
ElasticsearchClient client,
Query query,
string sorting,
SortOrder sortOrder,
int maxResultCount,
int skipCount,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var searchAfter = await GetSearchAfterValue(
client,
query,
sorting,
sortOrder,
skipCount,
cancellationToken);
if (searchAfter == null || !searchAfter.Any())
{
return [];
}
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(CreateIndex())
.Query(query)
.Sort(s => s.Field(new FieldSort(GetField(sorting))
{
Order = sortOrder
}))
.Size(maxResultCount)
.SearchAfter(searchAfter);
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
if (!searchResponse.IsSuccess())
{
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client,
Query query,
string sorting,
SortOrder sortOrder,
int skipCount,
CancellationToken cancellationToken = default)
{
// 10000以内直接取最后一条数据
if (skipCount < 10000)
{
var response = await client.SearchAsync<AuditLog>(
dsl => dsl.Indices(CreateIndex())
.Query(query)
.Sort(s => s.Field(new FieldSort(GetField(sorting))
{
Order = sortOrder
}))
.SourceIncludes(x => x.Id)
.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<AuditLog>(
dsl => dsl.Indices(CreateIndex())
.Query(query)
.Sort(s => s.Field(new FieldSort(GetField(sorting))
{
Order = sortOrder
}))
.SourceIncludes(x => x.Id)
.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<AuditLog>(
dsl => dsl.Indices(CreateIndex())
.Query(query)
.Sort(s => s.Field(new FieldSort(GetField(sorting))
{
Order = sortOrder
}))
.SourceIncludes(x => x.Id)
.SearchAfter(firstHit.Sort.ToList())
.Size(1),
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()
{
return _indexNameNormalizer.NormalizeIndex("audit-log");
}
private readonly static IDictionary<string, string> _fieldMaps = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
protected virtual string GetField(string field)
{
if (_auditLogFieldMaps.TryGetValue(field, out var mapField))
{
return _elasticsearchOptions.FieldCamelCase ? mapField.ToCamelCase() : mapField.ToPascalCase();
}
return _elasticsearchOptions.FieldCamelCase ? field.ToCamelCase() : field.ToPascalCase();
}
private readonly static IDictionary<string, string> _auditLogFieldMaps = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
{ "Id", "Id.keyword" },
{ "ApplicationName", "ApplicationName.keyword" },
@ -344,13 +577,4 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
{ "ExecutionTime", "ExecutionTime" },
{ "HttpStatusCode", "HttpStatusCode" },
};
protected virtual string GetField(string field)
{
if (_fieldMaps.TryGetValue(field, out var mapField))
{
return _elasticsearchOptions.FieldCamelCase ? mapField.ToCamelCase() : mapField.ToPascalCase();
}
return _elasticsearchOptions.FieldCamelCase ? field.ToCamelCase() : field.ToPascalCase();
}
}

7
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs

@ -34,7 +34,7 @@ public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency
_logger = logger;
}
public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default)
public async virtual Task<string> WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default)
{
var client = _clientFactory.Create();
var auditLog = await _auditLogConverter.ConvertAsync(auditLogInfo);
@ -44,7 +44,7 @@ public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency
.Id(auditLog.Id),
cancellationToken);
if (!response.IsValidResponse)
if (!response.IsSuccess())
{
_logger.LogWarning("Could not save the audit log object: " + Environment.NewLine + auditLog.ToString());
if (response.TryGetOriginalException(out var ex) && ex != null)
@ -55,7 +55,10 @@ public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency
{
_logger.LogWarning(response.ElasticsearchServerError.ToString());
}
return "";
}
return auditLog.Id.ToString();
}
public async virtual Task BulkWriteAsync(IEnumerable<AuditLogInfo> auditLogInfos, CancellationToken cancellationToken = default)

4
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IsExternalInit.cs

@ -0,0 +1,4 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}

13
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreModule.cs

@ -1,12 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AuditLogging.EntityFrameworkCore;
using Volo.Abp.Mapperly;
using Volo.Abp.Modularity;
using VoloAbpAuditLoggingEntityFrameworkCoreModule = Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingEntityFrameworkCoreModule;
using VoloAbpIdentityEntityFrameworkCoreModule = Volo.Abp.Identity.EntityFrameworkCore.AbpIdentityEntityFrameworkCoreModule;
using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog;
namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore;
[DependsOn(
typeof(Volo.Abp.Identity.EntityFrameworkCore.AbpIdentityEntityFrameworkCoreModule),
typeof(Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingEntityFrameworkCoreModule))]
typeof(VoloAbpIdentityEntityFrameworkCoreModule),
typeof(VoloAbpAuditLoggingEntityFrameworkCoreModule))]
[DependsOn(
typeof(AbpAuditLoggingModule),
typeof(AbpMapperlyModule))]
@ -15,5 +19,10 @@ public class AbpAuditLoggingEntityFrameworkCoreModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddMapperlyObjectMapper<AbpAuditLoggingEntityFrameworkCoreModule>();
context.Services.AddAbpDbContext<AbpAuditLoggingDbContext>(options =>
{
options.AddRepository<VoloAuditLog, EfCoreAuditLogRepository>();
});
}
}

140
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog;
namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore;
/// <summary>
/// 审计日志表达式树转换器
/// </summary>
internal class AuditLogExpressionQueryConverter : ExpressionVisitor
{
private readonly IReadOnlyDictionary<Type, Type> _typeMap;
private readonly Dictionary<ParameterExpression, ParameterExpression> _parameterMap = new();
public AuditLogExpressionQueryConverter()
: this(BuildDefaultTypeMap())
{
}
public AuditLogExpressionQueryConverter(IReadOnlyDictionary<Type, Type> typeMap)
{
_typeMap = typeMap ?? throw new ArgumentNullException(nameof(typeMap));
}
public Expression<Func<VoloAuditLog, bool>> Convert(Expression<Func<AuditLog, bool>> expression)
{
ArgumentNullException.ThrowIfNull(expression);
_parameterMap.Clear();
var rootParameter = Expression.Parameter(typeof(VoloAuditLog), expression.Parameters[0].Name);
_parameterMap[expression.Parameters[0]] = rootParameter;
var body = Visit(expression.Body);
return Expression.Lambda<Func<VoloAuditLog, bool>>(body, rootParameter);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
var parameters = node.Parameters.Select(p =>
{
if (_parameterMap.TryGetValue(p, out var mapped))
{
return mapped;
}
if (_typeMap.TryGetValue(p.Type, out var targetType))
{
mapped = Expression.Parameter(targetType, p.Name);
_parameterMap[p] = mapped;
return mapped;
}
return p;
}).ToArray();
var body = Visit(node.Body);
return Expression.Lambda(body, parameters);
}
protected override Expression VisitParameter(ParameterExpression node)
=> _parameterMap.TryGetValue(node, out var mapped) ? mapped : node;
protected override Expression VisitMember(MemberExpression node)
{
var expression = Visit(node.Expression);
if (expression != null
&& node.Member is PropertyInfo property
&& _typeMap.ContainsKey(property.DeclaringType!))
{
var targetProperty = expression.Type.GetProperty(
property.Name,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
if (targetProperty == null)
{
throw new NotSupportedException(
$"The property {property.Name} could not be found on the target type {expression.Type.FullName} and thus the expression cannot be overridden.");
}
return Expression.MakeMemberAccess(expression, targetProperty);
}
return node.Expression == expression
? node
: Expression.MakeMemberAccess(expression, node.Member);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.IsGenericMethod)
{
var oldArguments = node.Method.GetGenericArguments();
var newArguments = oldArguments
.Select(a => _typeMap.TryGetValue(a, out var mapped) ? mapped : a)
.ToArray();
if (!oldArguments.SequenceEqual(newArguments))
{
var targetMethod = node.Method.GetGenericMethodDefinition().MakeGenericMethod(newArguments);
var instance = node.Object != null ? Visit(node.Object) : null;
var arguments = node.Arguments.Select(Visit).ToArray();
return Expression.Call(instance, targetMethod, arguments!);
}
}
return base.VisitMethodCall(node);
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Quote)
{
var operand = Visit(node.Operand);
return Expression.Quote(operand);
}
return base.VisitUnary(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
if (node.Value is Enum enumValue && _typeMap.TryGetValue(enumValue.GetType(), out var targetType))
{
return Expression.Constant(Enum.ToObject(targetType, System.Convert.ToInt64(enumValue)), targetType);
}
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),
};
}
}

49
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs → aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs

@ -3,23 +3,25 @@ using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.AuditLogging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Specifications;
using Volo.Abp.Uow;
using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog;
namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore;
[Dependency(ReplaceServices = true)]
public class AuditLogManager : IAuditLogManager, ITransientDependency
public class EfCoreAuditLogManager : IAuditLogManager, ITransientDependency
{
protected IObjectMapper<AbpAuditLoggingEntityFrameworkCoreModule> ObjectMapper { get; }
protected IAuditLogRepository AuditLogRepository { get; }
protected IEfCoreAuditLogRepository AuditLogRepository { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public AuditLogManager(
IAuditLogRepository auditLogRepository,
public EfCoreAuditLogManager(
IUnitOfWorkManager unitOfWorkManager,
IEfCoreAuditLogRepository auditLogRepository,
IObjectMapper<AbpAuditLoggingEntityFrameworkCoreModule> objectMapper)
{
ObjectMapper = objectMapper;
@ -27,6 +29,39 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency
UnitOfWorkManager = unitOfWorkManager;
}
public async virtual Task<long> GetCountAsync(
ISpecification<AuditLog> specification,
CancellationToken cancellationToken = default)
{
var converter = new AuditLogExpressionQueryConverter();
var resetSpec = new ExpressionSpecification<VoloAuditLog>(
converter.Convert(specification.ToExpression()));
return await AuditLogRepository.GetCountAsync(resetSpec, cancellationToken);
}
public async virtual Task<List<AuditLog>> GetListAsync(
ISpecification<AuditLog> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var converter = new AuditLogExpressionQueryConverter();
var resetSpec = new ExpressionSpecification<VoloAuditLog>(
converter.Convert(specification.ToExpression()));
var auditLogs = await AuditLogRepository.GetListAsync(
resetSpec,
sorting,
maxResultCount,
skipCount,
includeDetails,
cancellationToken);
return ObjectMapper.Map<List<VoloAuditLog>, List<AuditLog>>(auditLogs);
}
public async virtual Task<long> GetCountAsync(
DateTime? startTime = null,
@ -105,7 +140,7 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency
includeDetails,
cancellationToken);
return ObjectMapper.Map<List<Volo.Abp.AuditLogging.AuditLog>, List<AuditLog>>(auditLogs);
return ObjectMapper.Map<List<VoloAuditLog>, List<AuditLog>>(auditLogs);
}
public async virtual Task<AuditLog?> GetAsync(
@ -115,7 +150,7 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency
{
var auditLog = await AuditLogRepository.GetAsync(id, includeDetails, cancellationToken);
return ObjectMapper.Map<Volo.Abp.AuditLogging.AuditLog, AuditLog>(auditLog);
return ObjectMapper.Map<VoloAuditLog, AuditLog>(auditLog);
}
public async virtual Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)

48
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.AuditLogging;
using Volo.Abp.AuditLogging.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Specifications;
using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog;
namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore;
public class EfCoreAuditLogRepository : Volo.Abp.AuditLogging.EntityFrameworkCore.EfCoreAuditLogRepository, IEfCoreAuditLogRepository
{
public EfCoreAuditLogRepository(
IDbContextProvider<IAuditLoggingDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public async virtual Task<long> GetCountAsync(
ISpecification<VoloAuditLog> specification,
CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.Where(specification.ToExpression())
.LongCountAsync(GetCancellationToken(cancellationToken));
}
public async virtual Task<List<VoloAuditLog>> GetListAsync(
ISpecification<VoloAuditLog> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.IncludeDetails(includeDetails)
.Where(specification.ToExpression())
.OrderBy(sorting.IsNullOrWhiteSpace() ? $"{nameof(VoloAuditLog.ExecutionTime)} DESC" : sorting)
.PageBy(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
}
}

4
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs

@ -31,7 +31,7 @@ public class EfCoreAuditLogWriter : IAuditLogWriter, ITransientDependency
GuidGenerator = guidGenerator;
}
public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default)
public async virtual Task<string> WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default)
{
using (var uow = UnitOfWorkManager.Begin(true))
{
@ -40,6 +40,8 @@ public class EfCoreAuditLogWriter : IAuditLogWriter, ITransientDependency
await AuditLogRepository.InsertAsync(auditLog);
await uow.CompleteAsync();
return auditLog.Id.ToString();
}
}

24
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/IEfCoreAuditLogRepository.cs

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.AuditLogging;
using Volo.Abp.Specifications;
using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog;
namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore;
public interface IEfCoreAuditLogRepository : IAuditLogRepository
{
Task<long> GetCountAsync(
ISpecification<VoloAuditLog> specification,
CancellationToken cancellationToken = default);
Task<List<VoloAuditLog>> GetListAsync(
ISpecification<VoloAuditLog> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default);
}

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

@ -19,6 +19,7 @@
<PackageReference Include="Volo.Abp.Auditing" />
<PackageReference Include="Volo.Abp.Guids" />
<PackageReference Include="Volo.Abp.ExceptionHandling" />
<PackageReference Include="Volo.Abp.Specifications" />
<PackageReference Include="System.Threading.Channels" />
</ItemGroup>

21
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/DefaultAuditLogManager.cs

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Specifications;
namespace LINGYUN.Abp.AuditLogging;
@ -98,4 +99,24 @@ public class DefaultAuditLogManager : IAuditLogManager, ISingletonDependency
Logger.LogDebug("No audit log manager is available!");
return Task.CompletedTask;
}
public virtual Task<long> GetCountAsync(
ISpecification<AuditLog> specification,
CancellationToken cancellationToken = default)
{
Logger.LogDebug("No audit log manager is available!");
return Task.FromResult(0L);
}
public Task<List<AuditLog>> GetListAsync(
ISpecification<AuditLog> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
Logger.LogDebug("No audit log manager is available!");
return Task.FromResult(new List<AuditLog>());
}
}

14
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Specifications;
namespace LINGYUN.Abp.AuditLogging;
@ -55,8 +56,19 @@ public interface IAuditLogManager
int? maxExecutionDuration = null,
int? minExecutionDuration = null,
bool? hasException = null,
HttpStatusCode? httpStatusCode = null,
HttpStatusCode? httpStatusCode = null,
bool includeDetails = false,
CancellationToken cancellationToken = default);
Task<long> GetCountAsync(
ISpecification<AuditLog> specification,
CancellationToken cancellationToken = default);
Task<List<AuditLog>> GetListAsync(
ISpecification<AuditLog> specification,
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default);
}

2
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs

@ -6,7 +6,7 @@ using Volo.Abp.Auditing;
namespace LINGYUN.Abp.AuditLogging;
public interface IAuditLogWriter
{
Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default);
Task<string> WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default);
Task BulkWriteAsync(IEnumerable<AuditLogInfo> auditLogInfos, CancellationToken cancellationToken = default);
}

4
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs

@ -24,10 +24,10 @@ public class LoggerAuditLogWriter : IAuditLogWriter, ISingletonDependency
}
}
public virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default)
public virtual Task<string> WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default)
{
_logger.LogInformation(auditLogInfo.ToString());
return Task.CompletedTask;
return Task.FromResult("");
}
}

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

@ -4,7 +4,6 @@
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<IsPackable>false</IsPackable>
<Configurations>Debug;Release;PostgreSQL</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>

34
aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs

@ -1,8 +1,10 @@
using Elastic.Clients.Elasticsearch;
using LINGYUN.Abp.Elasticsearch;
using LINGYUN.Abp.Tests;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using Volo.Abp;
using Volo.Abp.Modularity;
@ -13,26 +15,38 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
typeof(AbpAuditLoggingElasticsearchModule))]
public class AbpAuditLoggingElasticsearchTestModule : AbpModule
{
private const string UserSecretsId = "1748BEB4-4C7E-46F2-AE59-23956096B8E3";
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configurationOptions = new AbpConfigurationBuilderOptions
context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder =>
{
BasePath = @"D:\Projects\Development\Abp\AuditLogging\Elasticsearch",
EnvironmentName = "Development"
};
builder.AddUserSecrets(UserSecretsId);
}));
}
context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(configurationOptions));
public override void OnPostApplicationInitialization(ApplicationInitializationContext context)
{
RemoveTestIndexs(context.ServiceProvider);
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpAuditLoggingElasticsearchOptions>>().Value;
var clientFactory = context.ServiceProvider.GetRequiredService<IElasticsearchClientFactory>();
RemoveTestIndexs(context.ServiceProvider);
}
private static void RemoveTestIndexs(IServiceProvider serviceProvider)
{
var options = serviceProvider.GetRequiredService<IOptions<AbpAuditLoggingElasticsearchOptions>>().Value;
var clientFactory = serviceProvider.GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indicesResponse = client.Indices.Get($"{options.IndexPrefix}-security-log");
foreach (var index in indicesResponse.Indices)
var indicesResponse = client.Indices.Get($"{options.IndexPrefix}-audit-log");
if (indicesResponse.IsSuccess())
{
client.Indices.Delete(index.Key);
foreach (var index in indicesResponse.Indices)
{
client.Indices.Delete(index.Key);
}
}
}
}

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

@ -1,19 +1,24 @@
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>();
}
@ -23,7 +28,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
var mock = new AutoMocker();
var auditLogInfo = mock.CreateInstance<AuditLogInfo>();
var id = await _manager.SaveAsync(auditLogInfo);
var id = await _writer.WriteAsync(auditLogInfo);
id.ShouldNotBeNullOrWhiteSpace();
var findId = Guid.Parse(id);
@ -38,7 +43,11 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
[Fact]
public async Task Save_Audit_Log_Should_Get_List()
{
//await MockcAsync(10);
var count = 10;
await MockcAsync(count);
// 延迟等待ES索引完成
await Task.Delay(5000);
// 异常应该只有3个
(await _manager.GetCountAsync(
@ -69,12 +78,66 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
var logs = await _manager.GetListAsync(
userName: "_user_5",
clientId: "_client_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)
@ -83,7 +146,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
var auditLogIds = new List<string>();
for (int i = 1; i <= count; i++)
for (var i = 1; i <= count; i++)
{
var auditLogInfo = mock.CreateInstance<AuditLogInfo>();
auditLogInfo.ClientId = $"_client_{i}";
@ -92,10 +155,21 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
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)
@ -113,7 +187,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-2);
}
auditLogIds.Add(await _manager.SaveAsync(auditLogInfo));
auditLogIds.Add(await _writer.WriteAsync(auditLogInfo));
}
return auditLogIds;

Loading…
Cancel
Save