Browse Source

feat: Optimize the log query module

- Use query specifications
- Add type-based index field mapping
pull/1558/head
colin 1 week ago
parent
commit
4b0b267ff8
  1. 459
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs
  2. 32
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs
  3. 8
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/Elastic/Clients/Elasticsearch/SortOptionsExtenssions.cs
  4. 253
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs
  5. 8
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/EmptyDocument.cs
  6. 266
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryService.cs
  7. 2
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs
  8. 85
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs
  9. 3
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
  10. 7
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldMappingInfo.cs
  11. 4
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IIndexMappingProvider.cs
  12. 84
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfo.cs
  13. 29
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfoExtensions.cs
  14. 540
      aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs
  15. 32
      aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs
  16. 1
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs
  17. 1
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs

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

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
@ -67,7 +68,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
var sortingField = sorting;
if (sortingField.IsNullOrWhiteSpace())
{
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken);
var indexMapping = await _indexMappingProvider.GetMappingAsync<AuditLog>(indexName, cancellationToken);
if (indexMapping != null)
{
var sortingFieldMap = indexMapping.Fields
@ -87,13 +88,13 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
sortingField,
maxResultCount,
skipCount,
sourceExcludes: includeDetails == true
sourceExcludes: includeDetails == false
? Fields.FromFields(
[
new Field("Actions"),
new Field("Comments"),
new Field("EntityChanges"),
new Field("Exceptions"),
new Field(nameof(AuditLog.Actions)),
new Field(nameof(AuditLog.Comments)),
new Field(nameof(AuditLog.EntityChanges)),
new Field(nameof(AuditLog.Exceptions)),
])
: null,
cancellationToken: cancellationToken);
@ -116,41 +117,31 @@ 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,
url,
userId,
userName,
applicationName,
correlationId,
clientId,
clientIpAddress,
maxExecutionDuration,
minExecutionDuration,
hasException,
httpStatusCode);
Expression<Func<AuditLog, bool>> expression = _ => true;
expression = expression
.AndIf(startTime.HasValue, x => x.ExecutionTime >= _clock.Normalize(startTime!.Value))
.AndIf(endTime.HasValue, x => x.ExecutionTime <= _clock.Normalize(endTime!.Value))
.AndIf(!httpMethod.IsNullOrWhiteSpace(), x => x.HttpMethod == httpMethod)
.AndIf(!url.IsNullOrWhiteSpace(), x => x.Url!.Contains(url!))
.AndIf(userId.HasValue, x => x.UserId == userId)
.AndIf(!userName.IsNullOrWhiteSpace(), x => x.UserName == userName)
.AndIf(!applicationName.IsNullOrWhiteSpace(), x => x.ApplicationName == applicationName)
.AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.CorrelationId == correlationId)
.AndIf(!clientId.IsNullOrWhiteSpace(), x => x.ClientId == clientId)
.AndIf(!clientIpAddress.IsNullOrWhiteSpace(), x => x.ClientIpAddress == clientIpAddress)
.AndIf(maxExecutionDuration.HasValue, x => x.ExecutionDuration >= maxExecutionDuration)
.AndIf(minExecutionDuration.HasValue, x => x.ExecutionDuration <= minExecutionDuration)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null)
.AndIf(httpStatusCode.HasValue, x => x.HttpStatusCode == (int)httpStatusCode!);
var response = await client.CountAsync<AuditLog>(dsl =>
dsl.Indices(indexName)
.Query(new BoolQuery
{
Must = querys
}),
return await _expressionQueryService.GetCountAsync(
CreateIndex(),
expression,
cancellationToken);
if (response.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query audit log count failed: {errorMessage}", errorMessage);
}
return response.Count;
}
public async virtual Task<List<AuditLog>> GetListAsync(
@ -174,51 +165,47 @@ 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 sorts = GetOrDefaultSort(indexMapping, sorting);
var querys = BuildQueryDescriptor(
indexMapping,
startTime,
endTime,
httpMethod,
url,
userId,
userName,
applicationName,
correlationId,
clientId,
clientIpAddress,
maxExecutionDuration,
minExecutionDuration,
hasException,
httpStatusCode);
if (sorting.IsNullOrWhiteSpace())
{
sorting = $"{nameof(AuditLog.ExecutionTime)} DESC";
}
var query = new BoolQuery { Must = querys };
Expression<Func<AuditLog, bool>> expression = _ => true;
expression = expression
.AndIf(startTime.HasValue, x => x.ExecutionTime >= _clock.Normalize(startTime!.Value))
.AndIf(endTime.HasValue, x => x.ExecutionTime <= _clock.Normalize(endTime!.Value))
.AndIf(!httpMethod.IsNullOrWhiteSpace(), x => x.HttpMethod == httpMethod)
.AndIf(!url.IsNullOrWhiteSpace(), x => x.Url!.Contains(url!))
.AndIf(userId.HasValue, x => x.UserId == userId)
.AndIf(!userName.IsNullOrWhiteSpace(), x => x.UserName == userName)
.AndIf(!applicationName.IsNullOrWhiteSpace(), x => x.ApplicationName == applicationName)
.AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.CorrelationId == correlationId)
.AndIf(!clientId.IsNullOrWhiteSpace(), x => x.ClientId == clientId)
.AndIf(!clientIpAddress.IsNullOrWhiteSpace(), x => x.ClientIpAddress == clientIpAddress)
.AndIf(maxExecutionDuration.HasValue, x => x.ExecutionDuration >= maxExecutionDuration)
.AndIf(minExecutionDuration.HasValue, x => x.ExecutionDuration <= minExecutionDuration)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null)
.AndIf(httpStatusCode.HasValue, x => x.HttpStatusCode == (int)httpStatusCode!);
// ES最大支持10000, 超出这个长度后升级为使用Search_After方案
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);
return await _expressionQueryService.GetListAsync(
CreateIndex(),
expression,
sorting: sorting,
maxResultCount: maxResultCount,
skipCount: skipCount,
sourceExcludes: includeDetails == false
? Fields.FromFields(
[
new Field(nameof(AuditLog.Actions)),
new Field(nameof(AuditLog.Comments)),
new Field(nameof(AuditLog.EntityChanges)),
new Field(nameof(AuditLog.Exceptions)),
])
: null,
cancellationToken: cancellationToken);
}
public async virtual Task<AuditLog?> GetAsync(
@ -271,322 +258,8 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
cancellationToken);
}
protected virtual List<Query> BuildQueryDescriptor(
IndexMappingInfo indexMappingInfo,
DateTime? startTime = null,
DateTime? endTime = null,
string? httpMethod = null,
string? url = null,
Guid? userId = null,
string? userName = null,
string? applicationName = null,
string? correlationId = null,
string? clientId = null,
string? clientIpAddress = null,
int? maxExecutionDuration = null,
int? minExecutionDuration = null,
bool? hasException = null,
HttpStatusCode? httpStatusCode = null)
{
var queries = new List<Query>();
if (startTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionTime)))
{
Gte = _clock.Normalize(startTime.Value)
});
}
if (endTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionTime)))
{
Lte = _clock.Normalize(endTime.Value)
});
}
if (!httpMethod.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.HttpMethod)), httpMethod));
}
if (!url.IsNullOrWhiteSpace())
{
queries.Add(new WildcardQuery(GetField(indexMappingInfo, nameof(AuditLog.Url)))
{
Value = $"*{url}*"
});
}
if (userId.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.UserId)), userId.Value.ToString()));
}
if (!userName.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.UserName)), userName));
}
if (!applicationName.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ApplicationName)), applicationName));
}
if (!correlationId.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.CorrelationId)), correlationId));
}
if (!clientId.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ClientId)), clientId));
}
if (!clientIpAddress.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ClientIpAddress)), clientIpAddress));
}
if (maxExecutionDuration.HasValue)
{
queries.Add(new NumberRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionDuration)))
{
Lte = maxExecutionDuration.Value
});
}
if (minExecutionDuration.HasValue)
{
queries.Add(new NumberRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionDuration)))
{
Gte = minExecutionDuration.Value
});
}
if (hasException.HasValue)
{
if (hasException.Value)
{
queries.Add(new ExistsQuery(GetField(indexMappingInfo, nameof(AuditLog.Exceptions))));
}
else
{
queries.Add(new BoolQuery
{
MustNot = new List<Query>
{
new ExistsQuery(GetField(indexMappingInfo, nameof(AuditLog.Exceptions)))
}
});
}
}
if (httpStatusCode.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.HttpStatusCode)), ((int)httpStatusCode.Value).ToString()));
}
return queries;
}
private async Task<List<AuditLog>> SearchFromSizeAuditLogs(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[]? sorts = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(indexName)
.Query(query)
.From(skipCount)
.Size(maxResultCount);
if (sorts != null)
{
dsl.Sort(sorts);
}
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query audit log failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<AuditLog>> SearchAfterAuditLogs(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[] sorts,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var searchAfter = await GetSearchAfterValue(
client,
indexName,
query,
sorts,
skipCount,
cancellationToken);
if (searchAfter == null || !searchAfter.Any())
{
return [];
}
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.Size(maxResultCount)
.SearchAfter(searchAfter);
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query audit log failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[] sorts,
int skipCount,
CancellationToken cancellationToken = default)
{
// 10000以内直接取最后一条数据
if (skipCount < 10000)
{
var response = await client.SearchAsync<AuditLog>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.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(indexName)
.Query(query)
.Sort(sorts)
.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;
}
// 获取skipCount最近一条数据作为searchAfter
var secondResponse = await client.SearchAsync<AuditLog>(
dsl => dsl.Indices(indexName)
.Query(query)
// 反转排序取第一个数据作为起始索引
.Sort(sorts.ReverseSort()!.ToArray())
.SourceIncludes(x => x.Id)
.SearchAfter(firstHit.Sort.ToList())
.Size(1),
cancellationToken);
if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any())
{
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 static SortOptions[]? GetOrDefaultSort(IndexMappingInfo indexMappingInfo, string? sorting = null)
{
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;
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 sorts;
}
private static string GetField(IndexMappingInfo indexMappingInfo, string fieldFullPath)
{
return indexMappingInfo.GetExactFieldPath(fieldFullPath);
}
}

32
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs

@ -0,0 +1,32 @@
using Volo.Abp.Specifications;
namespace System.Linq.Expressions;
internal static class ExpressionFuncExtensions
{
public static Expression<Func<T, bool>> AndIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.And(first, second);
}
return first;
}
public static Expression<Func<T, bool>> OrIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.Or(first, second);
}
return first;
}
}

8
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/Elastic/Clients/Elasticsearch/SortOptionsExtenssions.cs

@ -12,7 +12,7 @@ public static class SortOptionsExtenssions
{
return sortOptions;
}
var newSort = sortOptions.Select(sort =>
return sortOptions.Select(sort =>
{
if (sort.Field != null)
{
@ -21,10 +21,6 @@ public static class SortOptionsExtenssions
: SortOrder.Asc;
}
return sort;
}).ToArray();
newSort.Reverse();
return newSort;
}).Reverse();
}
}

253
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs

@ -2,10 +2,11 @@
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileSystemGlobbing.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
@ -27,9 +28,26 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
_cache = cache;
}
public async Task<IndexMappingInfo> GetMappingAsync(string indexPattern, CancellationToken cancellationToken = default)
public async virtual Task<IndexMappingInfo> GetMappingAsync<TDocument>(
string indexPattern,
CancellationToken cancellationToken = default)
{
var cacheKey = $"es_mapping_{indexPattern}";
return await GetMappingAsync(indexPattern, typeof(TDocument), cancellationToken);
}
public async virtual Task<IndexMappingInfo> GetMappingAsync(string indexPattern, CancellationToken cancellationToken = default)
{
return await GetMappingAsync(indexPattern, null, cancellationToken);
}
private async Task<IndexMappingInfo> GetMappingAsync(
string indexPattern,
Type? documentType,
CancellationToken cancellationToken = default)
{
var cacheKey = documentType == null
? $"es_mapping_{indexPattern}"
: $"es_mapping_{indexPattern}_{documentType.FullName}";
var cacheItem = _cache.Get<IndexMappingInfo>(cacheKey);
if (cacheItem == null)
@ -48,7 +66,10 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
throw new AbpException($"Failed to get mapping for index {indexPattern}: {errorMessage}");
}
var indexName = indexPattern.EndsWith("*") ? indexPattern.Substring(0, indexPattern.Length - 1) : indexPattern;
var indexName = indexPattern.EndsWith("*")
? indexPattern.Substring(0, indexPattern.Length - 1)
: indexPattern;
var indexMappings = response.GetMappingFor(indexName);
if (indexMappings == null)
{
@ -66,7 +87,7 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
}
}
cacheItem = ParseMapping(indexMappings, indexPattern);
cacheItem = ParseMapping(indexMappings, indexPattern, documentType);
_cache.Set(cacheKey, cacheItem, _cacheDuration);
}
@ -74,35 +95,68 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
return cacheItem;
}
private IndexMappingInfo ParseMapping(TypeMapping mappings, string indexName)
private static IndexMappingInfo ParseMapping(
TypeMapping mappings,
string indexName,
Type? documentType = null)
{
var mappingInfo = new IndexMappingInfo { IndexName = indexName };
var mappingInfo = new IndexMappingInfo
{
IndexName = indexName,
DocumentType = documentType
};
if (mappings?.Properties != null)
{
ParseProperties(mappings.Properties, mappingInfo, string.Empty);
ParseProperties(
mappings.Properties,
mappingInfo,
string.Empty,
documentType,
string.Empty);
}
return mappingInfo;
}
private void ParseProperties(Properties? properties, IndexMappingInfo mappingInfo, string parentPath)
private static void ParseProperties(
Properties? properties,
IndexMappingInfo mappingInfo,
string esParentPath,
Type? parentClrType,
string clrParentPath)
{
if (properties == null) return;
if (properties == null)
{
return;
}
foreach (var kvp in properties)
{
var propertyName = kvp.Key.ToString();
var property = kvp.Value;
var fullPath = string.IsNullOrEmpty(parentPath)
// ES 完整路径
var esFullPath = string.IsNullOrEmpty(esParentPath)
? propertyName
: $"{esParentPath}.{propertyName}";
// CLR 完整路径
var clrFullPath = string.IsNullOrEmpty(clrParentPath)
? propertyName
: $"{parentPath}.{propertyName}";
: $"{clrParentPath}.{propertyName}";
// 解析 CLR 类型
var clrType = ResolveClrType(parentClrType, propertyName);
var fieldInfo = new FieldMappingInfo
{
Path = fullPath,
Path = esFullPath,
Name = propertyName,
Type = GetPropertyType(property)
Type = GetPropertyType(property),
ClrType = clrType,
ClrPath = clrFullPath
};
switch (property)
@ -110,13 +164,13 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
// Keyword 类型
case KeywordProperty keyword:
fieldInfo.IsKeyword = true;
mappingInfo.KeywordFields.Add(fullPath);
mappingInfo.KeywordFields.Add(esFullPath);
break;
// Text 类型 - 包含多字段支持
case TextProperty text:
fieldInfo.IsText = true;
mappingInfo.TextFields.Add(fullPath);
mappingInfo.TextFields.Add(esFullPath);
// 处理 Text 的 Fields(多字段)
if (text.Fields != null && text.Fields.Count() > 0)
@ -127,29 +181,34 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
{
var subFieldName = subFieldKvp.Key.ToString();
var subFieldProperty = subFieldKvp.Value;
var subFieldPath = $"{fullPath}.{subFieldName}";
var subFieldEsPath = $"{esFullPath}.{subFieldName}";
var subFieldClrPath = $"{clrFullPath}.{subFieldName}";
var subFieldInfo = new FieldMappingInfo
{
Path = subFieldPath,
Path = subFieldEsPath,
Name = subFieldName,
Type = GetPropertyType(subFieldProperty)
Type = GetPropertyType(subFieldProperty),
ClrType = clrType,
ClrPath = subFieldClrPath,
IsMultiField = true
};
// 处理子字段的类型
if (subFieldProperty is KeywordProperty)
{
subFieldInfo.IsKeyword = true;
mappingInfo.KeywordFields.Add(subFieldPath);
mappingInfo.KeywordFields.Add(subFieldEsPath);
}
else if (subFieldProperty is TextProperty)
{
subFieldInfo.IsText = true;
mappingInfo.TextFields.Add(subFieldPath);
mappingInfo.TextFields.Add(subFieldEsPath);
}
fieldInfo.Properties[subFieldName] = subFieldInfo;
mappingInfo.Fields[subFieldPath] = subFieldInfo;
mappingInfo.Fields[subFieldEsPath] = subFieldInfo;
mappingInfo.ClrFields[subFieldClrPath] = subFieldInfo;
}
}
break;
@ -158,14 +217,14 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case DateProperty date:
fieldInfo.IsDate = true;
fieldInfo.Format = date.Format;
mappingInfo.DateFields.Add(fullPath);
mappingInfo.DateFields.Add(esFullPath);
break;
// 日期纳秒类型
case DateNanosProperty dateNanos:
fieldInfo.IsDate = true;
fieldInfo.Format = dateNanos.Format;
mappingInfo.DateFields.Add(fullPath);
mappingInfo.DateFields.Add(esFullPath);
break;
// 数值类型
@ -179,58 +238,71 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case ShortNumberProperty:
case UnsignedLongNumberProperty:
fieldInfo.IsNumeric = true;
mappingInfo.NumericFields.Add(fullPath);
mappingInfo.NumericFields.Add(esFullPath);
break;
// 布尔类型
case BooleanProperty:
fieldInfo.IsBoolean = true;
mappingInfo.BooleanFields.Add(fullPath);
mappingInfo.BooleanFields.Add(esFullPath);
break;
// Nested 类型
case NestedProperty nested:
fieldInfo.IsNested = true;
fieldInfo.IsObject = true;
mappingInfo.NestedFieldPaths.Add(fullPath);
mappingInfo.NestedFieldPaths.Add(esFullPath);
var nestedInfo = new NestedMappingInfo
{
Path = fullPath,
Path = esFullPath,
Name = propertyName,
Properties = new Dictionary<string, FieldMappingInfo>()
};
// 获取 nested 集合的元素类型
var elementType = GetElementType(clrType);
if (nested.Properties != null)
{
// 先递归解析内部字段
ParseProperties(nested.Properties, mappingInfo, fullPath);
ParseProperties(
nested.Properties,
mappingInfo,
esFullPath,
elementType ?? clrType,
clrFullPath);
// 收集 nested 内部的字段信息
foreach (var innerKvp in nested.Properties)
{
var innerName = innerKvp.Key.ToString();
var innerFullPath = $"{fullPath}.{innerName}";
var innerEsFullPath = $"{esFullPath}.{innerName}";
var innerClrFullPath = $"{clrFullPath}.{innerName}";
if (mappingInfo.Fields.TryGetValue(innerFullPath, out var innerFieldInfo))
if (mappingInfo.Fields.TryGetValue(innerEsFullPath, out var innerFieldInfo))
{
nestedInfo.Properties[innerName] = innerFieldInfo;
}
else
{
var innerClrType = ResolveClrType(elementType ?? clrType, innerName);
innerFieldInfo = new FieldMappingInfo
{
Path = innerFullPath,
Path = innerEsFullPath,
Name = innerName,
Type = GetPropertyType(innerKvp.Value)
Type = GetPropertyType(innerKvp.Value),
ClrType = innerClrType,
ClrPath = innerClrFullPath
};
nestedInfo.Properties[innerName] = innerFieldInfo;
mappingInfo.Fields[innerFullPath] = innerFieldInfo;
mappingInfo.Fields[innerEsFullPath] = innerFieldInfo;
mappingInfo.ClrFields[innerClrFullPath] = innerFieldInfo;
}
}
}
mappingInfo.NestedFields[fullPath] = nestedInfo;
mappingInfo.NestedFields[esFullPath] = nestedInfo;
break;
// Object 类型
@ -240,7 +312,12 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
if (obj.Properties != null)
{
ParseProperties(obj.Properties, mappingInfo, fullPath);
ParseProperties(
obj.Properties,
mappingInfo,
esFullPath,
clrType,
clrFullPath);
}
break;
@ -278,7 +355,7 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case MatchOnlyTextProperty matchOnlyText:
fieldInfo.IsText = true;
fieldInfo.Type = "match_only_text";
mappingInfo.TextFields.Add(fullPath);
mappingInfo.TextFields.Add(esFullPath);
// MatchOnlyText 也可能有 Fields
if (matchOnlyText.Fields != null && matchOnlyText.Fields.Count() > 0)
@ -287,20 +364,25 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
foreach (var subFieldKvp in matchOnlyText.Fields)
{
var subFieldName = subFieldKvp.Key.ToString();
var subFieldPath = $"{fullPath}.{subFieldName}";
var subFieldEsPath = $"{esFullPath}.{subFieldName}";
var subFieldClrPath = $"{clrFullPath}.{subFieldName}";
var subFieldInfo = new FieldMappingInfo
{
Path = subFieldPath,
Path = subFieldEsPath,
Name = subFieldName,
Type = GetPropertyType(subFieldKvp.Value)
Type = GetPropertyType(subFieldKvp.Value),
ClrType = clrType,
ClrPath = subFieldClrPath,
IsMultiField = true
};
if (subFieldKvp.Value is KeywordProperty)
{
subFieldInfo.IsKeyword = true;
mappingInfo.KeywordFields.Add(subFieldPath);
mappingInfo.KeywordFields.Add(subFieldEsPath);
}
fieldInfo.Properties[subFieldName] = subFieldInfo;
mappingInfo.Fields[subFieldPath] = subFieldInfo;
mappingInfo.Fields[subFieldEsPath] = subFieldInfo;
mappingInfo.ClrFields[subFieldClrPath] = subFieldInfo;
}
}
break;
@ -308,7 +390,7 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case WildcardProperty:
fieldInfo.IsWildcard = true;
fieldInfo.Type = "wildcard";
mappingInfo.WildcardFields.Add(fullPath);
mappingInfo.WildcardFields.Add(esFullPath);
break;
case CompletionProperty:
@ -344,11 +426,92 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
break;
}
mappingInfo.Fields[fullPath] = fieldInfo;
// 添加到映射集合
mappingInfo.Fields[esFullPath] = fieldInfo;
// 只有在有 CLR 类型信息时才添加到 CLR 字段集合
if (clrType != null || parentClrType != null)
{
mappingInfo.ClrFields[clrFullPath] = fieldInfo;
}
}
}
private static Type? ResolveClrType(Type? parentType, string propertyName)
{
if (parentType == null)
{
return null;
}
var propertyInfo = parentType.GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
return propertyInfo?.PropertyType;
}
private static Type? GetElementType(Type? type)
{
if (type == null)
{
return null;
}
// 处理数组
if (type.IsArray)
{
return type.GetElementType();
}
// 处理 IEnumerable<T>
if (type.IsGenericType)
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(IEnumerable<>) ||
genericTypeDefinition == typeof(ICollection<>) ||
genericTypeDefinition == typeof(IList<>) ||
genericTypeDefinition == typeof(List<>))
{
return type.GetGenericArguments()[0];
}
}
// 处理实现了 IEnumerable<T> 的接口
var enumerableInterface = type.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return enumerableInterface?.GetGenericArguments()[0];
}
private static string GetJsonPropertyName(Type? type, string propertyName)
{
if (type == null)
{
return propertyName;
}
var propertyInfo = type.GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (propertyInfo == null)
{
return propertyName;
}
// 检查 JsonPropertyName 特性
var jsonPropertyNameAttribute = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>();
if (jsonPropertyNameAttribute != null && !string.IsNullOrEmpty(jsonPropertyNameAttribute.Name))
{
return jsonPropertyNameAttribute.Name;
}
return propertyName;
}
private string GetPropertyType(IProperty property)
private static string GetPropertyType(IProperty property)
{
return property switch
{

8
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/EmptyDocument.cs

@ -0,0 +1,8 @@
using System;
namespace LINGYUN.Abp.Elasticsearch;
[Serializable]
public class EmptyDocument
{
}

266
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryService.cs

@ -1,5 +1,7 @@
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
@ -12,15 +14,21 @@ namespace LINGYUN.Abp.Elasticsearch;
public class ExpressionQueryService : IExpressionQueryService, ITransientDependency
{
public ILogger<ExpressionQueryService> Logger { protected get; set; }
protected IElasticsearchClientFactory ClientFactory { get; }
protected IIndexMappingProvider IndexMappingProvider { get; }
protected IExpressionQueryTranslator ExpressionQueryTranslator { get; }
public ExpressionQueryService(
IElasticsearchClientFactory clientFactory,
IIndexMappingProvider indexMappingProvider,
IExpressionQueryTranslator expressionQueryTranslator)
{
ClientFactory = clientFactory;
IndexMappingProvider = indexMappingProvider;
ExpressionQueryTranslator = expressionQueryTranslator;
Logger = NullLogger<ExpressionQueryService>.Instance;
}
public async virtual Task<long> GetCountAsync<TDocument>(
@ -55,19 +63,11 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
SortOptions[]? sorts = null;
if (!sorting.IsNullOrWhiteSpace())
{
var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase)
? SortOrder.Asc : SortOrder.Desc;
sorts = new SortOptions[1]
var indexMapping = await IndexMappingProvider.GetMappingAsync<TDocument>(indexName, cancellationToken);
if (indexMapping != null)
{
new SortOptions
{
Field = new FieldSort(new Field(sorting))
{
Order = sortOrder,
},
}
};
sorts = ResolveDefaultSorts(indexMapping, sorting);
}
}
// 数量超过10000且存在排序时才可以使用SearchAfter特性
@ -126,8 +126,9 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
}
}, cancellationToken);
if (!searchResponse.IsSuccess())
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query document failed: {errorMessage}", errorMessage);
return [];
}
@ -153,7 +154,7 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
}
else
{
searchAfter = await GetSearchAfterValue<TDocument>(
searchAfter = await GetSearchAfterValue(
client,
indexName,
query,
@ -184,15 +185,16 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
}
}, cancellationToken);
if (!searchResponse.IsSuccess())
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query document failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<FieldValue>?> GetSearchAfterValue<TDocument>(
private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client,
string indexName,
Query query,
@ -203,16 +205,19 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
// 10000以内直接取最后一条数据
if (skipCount < 10000)
{
var response = await client.SearchAsync<TDocument>(
var response = await client.SearchAsync<EmptyDocument>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.From(skipCount)
.Size(1),
.Size(1)
.Source(false)
.TrackScores(false),
cancellationToken);
if (!response.IsSuccess() || response.Hits == null || !response.Hits.Any())
if (response.TryGetErrorMessage(out var oneError))
{
Logger.LogWarning("Failed to obtain the {skipCount}th sorting record. error: {error}", skipCount, oneError);
return null;
}
@ -221,48 +226,235 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
}
// 获取第9999条数据Hits作为searchAfter
var firstResponse = await client.SearchAsync<TDocument>(
var firstResponse = await client.SearchAsync<EmptyDocument>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.SourceIncludes([])
.From(9999)
.Size(1),
.Size(1)
.Source(false)
.TrackScores(false),
cancellationToken);
if (!firstResponse.IsSuccess() || firstResponse.Hits == null || !firstResponse.Hits.Any())
if (firstResponse.TryGetErrorMessage(out var firstError))
{
Logger.LogWarning("Failed to obtain the first sorted record after the {skipCount}th item. error: {error}", skipCount, firstError);
return null;
}
var firstHit = firstResponse.Hits.FirstOrDefault();
var firstHit = firstResponse.Hits?.FirstOrDefault();
if (firstHit?.Sort == null || !firstHit.Sort.Any())
{
Logger.LogWarning("The first sorted record after the {skipCount}th item is empty!", skipCount);
return null;
}
// 获取skipCount最近一条数据作为searchAfter
var secondResponse = await client.SearchAsync<TDocument>(
dsl => dsl.Indices(indexName)
.Query(query)
// 反转排序取第一个数据作为起始索引
.Sort(sorts.ReverseSort()!.ToArray())
.SourceIncludes([])
.SearchAfter(firstHit.Sort.ToList())
.Size(1),
var remaining = skipCount - 10000;
return await GetBatchSearchAfterValue(
client,
indexName,
query,
sorts,
[.. firstHit.Sort],
remaining,
remaining > 10000 ? 5000 : 1000,
cancellationToken);
}
private async Task<List<FieldValue>?> GetBatchSearchAfterValue(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[] sorts,
FieldValue[] searchAfter,
int remaining,
int batchSize = 1000,
CancellationToken cancellationToken = default)
{
List<FieldValue>? lastSort = null;
while (remaining > 0)
{
var batch = Math.Min(remaining, batchSize);
var response = await client.SearchAsync<EmptyDocument>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.SearchAfter(searchAfter)
.Size(batch)
.Source(false)
.TrackScores(false),
cancellationToken);
if (response.TryGetErrorMessage(out var error))
{
Logger.LogWarning("Failed to get batch records. remaining: {remaining}, error: {error}", remaining, error);
return null;
}
if (response.Hits == null || !response.Hits.Any())
{
Logger.LogWarning("No more records available. remaining: {remaining}", remaining);
return null;
}
var hits = response.Hits.ToList();
var hitCount = hits.Count;
remaining -= hitCount;
if (remaining <= 0)
{
var targetIndex = hitCount + remaining;
if (targetIndex == hitCount)
{
lastSort = hits.LastOrDefault()?.Sort?.ToList();
}
else if (targetIndex >= 0 && targetIndex < hitCount)
{
lastSort = hits[targetIndex]?.Sort?.ToList();
}
else
{
return null;
}
return lastSort;
}
var lastHit = hits.LastOrDefault();
if (lastHit?.Sort == null || !lastHit.Sort.Any())
{
return null;
}
if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any())
searchAfter = [.. lastHit.Sort];
if (hitCount < batch)
{
return null;
}
}
return lastSort;
}
private static SortOptions[]? ResolveDefaultSorts(IndexMappingInfo indexMappingInfo, string? sorting = null)
{
if (sorting.IsNullOrWhiteSpace())
{
return null;
}
var lastHit = secondResponse.Hits.LastOrDefault();
if (lastHit?.Sort == null || !lastHit.Sort.Any())
// eg: a desc, b.c asc; d+desc; e-asc; +f; -g
var sortFields = sorting.Split([';', ','], StringSplitOptions.RemoveEmptyEntries);
var sorts = new List<SortOptions>();
foreach (var sortField in sortFields)
{
var trimmedSortField = sortField.Trim();
if (trimmedSortField.IsNullOrWhiteSpace())
{
continue;
}
// [a, desc]
// [b.c, asc]
// [d, desc]
// [e, asc]
var parts = trimmedSortField.Split([' ', ':', '-', '+'], StringSplitOptions.RemoveEmptyEntries);
string fieldName;
SortOrder sortOrder;
if (parts.Length >= 2)
{
// b.c
fieldName = parts[0].Trim();
// desc
var orderStr = parts[1].Trim();
sortOrder = orderStr.Equals("desc", StringComparison.InvariantCultureIgnoreCase) ||
orderStr.Equals("descending", StringComparison.InvariantCultureIgnoreCase)
? SortOrder.Desc
: SortOrder.Asc;
}
else
{
fieldName = parts[0].Trim();
// +f
if (fieldName.StartsWith("+"))
{
sortOrder = SortOrder.Asc;
fieldName = fieldName.Substring(1);
}
// -g
else if (fieldName.StartsWith("-"))
{
sortOrder = SortOrder.Desc;
fieldName = fieldName.Substring(1);
}
else
{
sortOrder = SortOrder.Asc;
}
}
var resolvedField = ResolveSortField(indexMappingInfo, fieldName);
if (resolvedField != null)
{
var fieldPath = resolvedField.GetKeywordPath();
if (!sorts.Any(x => x.Field?.Field?.Name == fieldPath))
{
sorts.Add(new SortOptions
{
Field = new FieldSort(Field.FromString(fieldPath))
{
Order = sortOrder,
}
});
}
}
}
return sorts?.ToArray();
}
private static FieldMappingInfo? ResolveSortField(
IndexMappingInfo indexMappingInfo,
string fieldPath)
{
if (fieldPath.IsNullOrWhiteSpace())
{
return null;
}
return lastHit.Sort.ToList();
var directField = indexMappingInfo.GetField(fieldPath);
if (directField != null)
{
return directField;
}
var clrField = indexMappingInfo.GetFieldByClrPath(fieldPath);
if (clrField != null)
{
return clrField;
}
var caseInsensitiveEsField = indexMappingInfo.Fields
.FirstOrDefault(kvp => kvp.Key.Equals(fieldPath, StringComparison.InvariantCultureIgnoreCase))
.Value;
if (caseInsensitiveEsField != null)
{
return caseInsensitiveEsField;
}
var caseInsensitiveClrField = indexMappingInfo.ClrFields
.FirstOrDefault(kvp => kvp.Key.Equals(fieldPath, StringComparison.InvariantCultureIgnoreCase))
.Value;
return caseInsensitiveClrField;
}
}

2
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs

@ -39,7 +39,7 @@ public partial class ExpressionQueryTranslator
IndexMappingInfo? mappingInfo)
{
// 限定string方法调用
if (methodCall.Method.DeclaringType != typeof(string))
if (methodCall.Method.DeclaringType == typeof(string))
{
return methodCall.Method.Name switch
{

85
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs

@ -298,45 +298,34 @@ public partial class ExpressionQueryTranslator
string? prefix,
IndexMappingInfo? mappingInfo)
{
var compareValue = Evaluate(constantExpression)?.ToString() ?? string.Empty;
// CompareTo 返回值:
// 0: 相等
// > 0: 当前字符串在排序顺序中位于参数之后
// < 0: 当前字符串在排序顺序中位于参数之前
var (field, compareValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var compareResult = Convert.ToInt32(Evaluate(constantExpression));
switch (comparisonType)
{
case ExpressionType.Equal:
case ExpressionType.Equal when compareResult == 0:
// CompareTo == 0 表示相等
var (field, value) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
return BuildEquality(field, value);
return BuildEquality(field, compareValue);
case ExpressionType.NotEqual:
case ExpressionType.NotEqual when compareResult == 0:
// CompareTo != 0 表示不相等
var (notEqualField, notEqualValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var notEqualQuery = BuildEquality(notEqualField, notEqualValue);
return new BoolQuery
{
MustNot = new Query[] { notEqualQuery }
};
return BuildNotEqualQuery(field, compareValue);
case ExpressionType.GreaterThan:
case ExpressionType.GreaterThan when compareResult == 0:
// CompareTo > 0 表示当前字段值大于比较值
// 这可以简化处理,但这里先返回 null 让默认处理逻辑处理
return null;
return BuildRange(field, greaterThan: compareValue);
case ExpressionType.GreaterThanOrEqual:
case ExpressionType.GreaterThanOrEqual when compareResult == 0:
// CompareTo >= 0 表示当前字段值大于或等于比较值
return null;
return BuildRange(field, greaterThanOrEqualTo: compareValue);
case ExpressionType.LessThan:
case ExpressionType.LessThan when compareResult == 0:
// CompareTo < 0 表示当前字段值小于比较值
return null;
return BuildRange(field, lessThan: compareValue);
case ExpressionType.LessThanOrEqual:
case ExpressionType.LessThanOrEqual when compareResult == 0:
// CompareTo <= 0 表示当前字段值小于或等于比较值
return null;
return BuildRange(field, lessThanOrEqualTo: compareValue);
default:
return null;
@ -353,46 +342,28 @@ public partial class ExpressionQueryTranslator
string? prefix,
IndexMappingInfo? mappingInfo)
{
var indexValue = Evaluate(constantExpression)?.ToString() ?? string.Empty;
// IndexOf 返回值:
// >= 0: 找到了子字符串
// < 0: 没有找到子字符串
// 获取字段和搜索值
var (field, searchValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var indexResult = Convert.ToInt32(Evaluate(constantExpression));
var fieldMapping = mappingInfo?.GetField(field.Path);
switch (comparisonType)
{
case ExpressionType.GreaterThanOrEqual:
case ExpressionType.GreaterThan:
// IndexOf >= 0 表示包含
var (field, value) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var fieldMapping = mappingInfo?.GetField(field.Path);
return TranslateStringContains(field, fieldMapping, value);
case ExpressionType.LessThan:
case ExpressionType.LessThanOrEqual:
// IndexOf < 0 表示不包含
var (notContainsField, notContainsValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var notContainsFieldMapping = mappingInfo?.GetField(notContainsField.Path);
var containsQuery = TranslateStringContains(notContainsField, notContainsFieldMapping, notContainsValue);
case ExpressionType.GreaterThanOrEqual when indexResult >= 0:
case ExpressionType.GreaterThan when indexResult > -1:
case ExpressionType.NotEqual when indexResult == -1:
// IndexOf >= 0, IndexOf > -1, IndexOf != -1 表示包含
return TranslateStringContains(field, fieldMapping, searchValue);
case ExpressionType.LessThan when indexResult <= 0:
case ExpressionType.Equal when indexResult == -1:
// IndexOf == -1, IndexOf < 0 表示不包含
var containsQuery = TranslateStringContains(field, fieldMapping, searchValue);
return new BoolQuery
{
MustNot = new Query[] { containsQuery }
};
case ExpressionType.Equal:
// IndexOf == 某个值,这里简化处理,只处理 == -1(不包含)的情况
if (indexValue == "-1")
{
var (eqField, eqValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var eqFieldMapping = mappingInfo?.GetField(eqField.Path);
var eqContainsQuery = TranslateStringContains(eqField, eqFieldMapping, eqValue);
return new BoolQuery
{
MustNot = new Query[] { eqContainsQuery }
};
}
return null;
default:
return null;
}

3
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
@ -34,7 +33,7 @@ public partial class ExpressionQueryTranslator : IExpressionQueryTranslator, ISi
Check.NotNullOrWhiteSpace(indexName, nameof(indexName));
Check.NotNull(expression, nameof(expression));
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName);
var indexMapping = await _indexMappingProvider.GetMappingAsync<TDocument>(indexName);
return TranslateNode(expression.Body, prefix: null, indexMapping);
}

7
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldMappingInfo.cs

@ -9,7 +9,9 @@ public class FieldMappingInfo
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public Type? ClrType { get; set; }
public string ClrPath { get; set; } = string.Empty;
public bool IsMultiField { get; set; }
public bool IsKeyword { get; set; }
public bool IsText { get; set; }
public bool IsWildcard { get; set; }
@ -32,7 +34,10 @@ public class FieldMappingInfo
public string GetKeywordPath()
{
if (IsKeyword) return Path;
if (IsKeyword)
{
return Path;
}
// 如果是 text 类型且有 keyword 子字段
if (IsText && Properties?.ContainsKey("keyword") == true)

4
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IIndexMappingProvider.cs

@ -8,4 +8,8 @@ public interface IIndexMappingProvider
Task<IndexMappingInfo> GetMappingAsync(
string indexPattern,
CancellationToken cancellationToken = default);
Task<IndexMappingInfo> GetMappingAsync<TDocument>(
string indexPattern,
CancellationToken cancellationToken = default);
}

84
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfo.cs

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
/// <summary>
@ -12,15 +14,25 @@ public class IndexMappingInfo
/// </summary>
public string IndexName { get; set; } = string.Empty;
/// <summary>
/// 文档类型
/// </summary>
public Type? DocumentType { get; set; }
/// <summary>
/// 所有字段映射(扁平化)
/// </summary>
public Dictionary<string, FieldMappingInfo> Fields { get; set; } = new();
public Dictionary<string, FieldMappingInfo> Fields { get; set; } = new Dictionary<string, FieldMappingInfo>(StringComparer.CurrentCultureIgnoreCase);
/// <summary>
/// 按 CLR 属性路径索引的字段映射
/// </summary>
public Dictionary<string, FieldMappingInfo> ClrFields { get; set; } = new Dictionary<string, FieldMappingInfo>(StringComparer.CurrentCultureIgnoreCase);
/// <summary>
/// Nested 字段映射
/// </summary>
public Dictionary<string, NestedMappingInfo> NestedFields { get; set; } = new();
public Dictionary<string, NestedMappingInfo> NestedFields { get; set; } = new Dictionary<string, NestedMappingInfo>(StringComparer.CurrentCultureIgnoreCase);
/// <summary>
/// Keyword 字段列表
@ -64,6 +76,33 @@ public class IndexMappingInfo
return Fields.GetOrDefault(path);
}
/// <summary>
/// 根据 CLR 属性路径获取字段映射信息
/// </summary>
public FieldMappingInfo? GetFieldByClrPath(string clrPath)
{
return ClrFields.GetOrDefault(clrPath);
}
/// <summary>
/// 根据 CLR 属性表达式获取字段映射信息
/// </summary>
public FieldMappingInfo? GetFieldByExpression<TDocument>(Expression<Func<TDocument, object?>> expression)
{
var clrPath = GetPropertyPath(expression);
return GetFieldByClrPath(clrPath);
}
/// <summary>
/// 根据 CLR 属性表达式获取 ES 字段路径
/// </summary>
public string? GetElasticsearchFieldPath<TDocument>(Expression<Func<TDocument, object?>> expression)
{
var clrPath = GetPropertyPath(expression);
var field = GetFieldByClrPath(clrPath);
return field?.Path;
}
/// <summary>
/// 判断是否为 Nested 字段
/// </summary>
@ -136,4 +175,43 @@ public class IndexMappingInfo
return null;
}
/// <summary>
/// 将 CLR 属性路径转换为 ES 字段路径
/// </summary>
public string? ConvertClrPathToElasticsearchPath(string clrPath)
{
var field = GetFieldByClrPath(clrPath);
return field?.Path;
}
/// <summary>
/// 将 ES 字段路径转换为 CLR 属性路径
/// </summary>
public string? ConvertElasticsearchPathToClrPath(string esPath)
{
var field = GetField(esPath);
return field?.ClrPath;
}
private static string GetPropertyPath<TDocument>(Expression<Func<TDocument, object?>> expression)
{
var parts = new List<string>();
var currentExpression = expression.Body;
while (currentExpression is MemberExpression memberExpression)
{
parts.Insert(0, memberExpression.Member.Name);
currentExpression = memberExpression.Expression!;
}
// 处理转换表达式
if (currentExpression is UnaryExpression unaryExpression &&
unaryExpression.NodeType == ExpressionType.Convert &&
unaryExpression.Operand is MemberExpression convertMemberExpression)
{
parts.Insert(0, convertMemberExpression.Member.Name);
}
return string.Join(".", parts);
}
}

29
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfoExtensions.cs

@ -0,0 +1,29 @@
using System;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
public static class IndexMappingInfoExtensions
{
public static string? GetElasticsearchFieldPath<TDocument>(
this IndexMappingInfo mappingInfo,
Expression<Func<TDocument, object?>> expression)
{
return mappingInfo.GetElasticsearchFieldPath(expression);
}
public static FieldMappingInfo? GetFieldMapping<TDocument>(
this IndexMappingInfo mappingInfo,
Expression<Func<TDocument, object?>> expression)
{
return mappingInfo.GetFieldByExpression(expression);
}
public static string? GetKeywordPath<TDocument>(
this IndexMappingInfo mappingInfo,
Expression<Func<TDocument, object?>> expression)
{
var field = mappingInfo.GetFieldByExpression(expression);
return field?.GetKeywordPath();
}
}

540
aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs

@ -1,5 +1,5 @@
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Elastic.Transport.Diagnostics.Auditing;
using LINGYUN.Abp.Elasticsearch;
using LINGYUN.Linq.Dynamic.Queryable;
using Microsoft.Extensions.Logging;
@ -10,6 +10,7 @@ using Serilog.Formatting.Elasticsearch;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@ -37,7 +38,6 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
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;
@ -48,7 +48,6 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
ICurrentTenant currentTenant,
IOptions<AbpLoggingSerilogElasticsearchOptions> options,
IElasticsearchClientFactory clientFactory,
IIndexMappingProvider indexMappingProvider,
IExpressionQueryService expressionQueryService,
IObjectMapper<AbpLoggingSerilogElasticsearchModule> objectMapper)
{
@ -56,7 +55,6 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
_objectMapper = objectMapper;
_currentTenant = currentTenant;
_clientFactory = clientFactory;
_indexMappingProvider = indexMappingProvider;
_expressionQueryService = expressionQueryService;
_options = options.Value;
@ -67,12 +65,11 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
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,
CreateIndex(),
expression,
cancellationToken);
}
@ -82,35 +79,28 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
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())
if (sorting.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;
}
}
sorting = ElasticsearchJsonFormatter.TimestampPropertyName;
}
var serilogLogs = await _expressionQueryService.GetListAsync(
indexName,
CreateIndex(),
expression,
sortingField,
sorting,
maxResultCount,
skipCount,
sourceExcludes: includeDetails == false
? Fields.FromFields(
[
new Field("exceptions"),
])
: null,
cancellationToken: cancellationToken);
return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs);
@ -126,83 +116,18 @@ 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;
if (_currentTenant.IsAvailable)
{
/*
"query": {
"bool": {
"must": [
{
"term": {
"fields.TenantId.keyword": {
"value": _currentTenant.GetId()
}
}
},
{
"term": {
"fields.UniqueId": {
"value": "1474021081433481216"
}
}
}
]
}
}
*/
response = await client.SearchAsync<SerilogInfo>(
dsl =>
dsl.Indices(CreateIndex())
.Query(
(q) => q.Bool(
(b) => b.Must(
(s) => s.Term(
(t) => t.Field(GetField(indexMapping, "fields.UniqueId")).Value(id)),
(s) => s.Term(
(t) => t.Field(GetField(indexMapping, "fields.TenantId")).Value(_currentTenant.GetId().ToString())))))
.Size(1),
cancellationToken);
}
else
{
/*
"query": {
"bool": {
"must": [
{
"term": {
"fields.UniqueId": {
"value": "1474021081433481216"
}
}
}
]
}
}
*/
response = await client.SearchAsync<SerilogInfo>(
dsl =>
dsl.Indices(CreateIndex())
.Query(
(q) => q.Bool(
(b) => b.Must(
(s) => s.Term(
(t) => t.Field(GetField(indexMapping, "fields.UniqueId")).Value(id)))))
.Size(1),
cancellationToken);
if (response.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query logs failed: {errorMessage}", errorMessage);
}
}
Expression<Func<SerilogInfo, bool>> expression = x => x.Fields.UniqueId == long.Parse(id);
expression = expression.AndIf(_currentTenant.IsAvailable, x => x.Fields.TenantId == _currentTenant.Id);
var serilogs = await _expressionQueryService.GetListAsync<SerilogInfo>(
CreateIndex(),
x => x.Fields.UniqueId == long.Parse(id),
sorting: $"{ElasticsearchJsonFormatter.TimestampPropertyName} DESC",
maxResultCount: 1,
cancellationToken: cancellationToken);
return _objectMapper.Map<SerilogInfo?, LogInfo?>(response.Documents.FirstOrDefault());
return _objectMapper.Map<SerilogInfo?, LogInfo?>(serilogs.FirstOrDefault());
}
public async virtual Task<long> GetCountAsync(
@ -221,36 +146,30 @@ 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,
machineName,
environment,
application,
context,
requestId,
requestPath,
correlationId,
processId,
threadId,
hasException);
Expression<Func<SerilogInfo, bool>> expression = _ => true;
expression = expression
.AndIf(startTime.HasValue, x => x.TimeStamp >= _clock.Normalize(startTime!.Value))
.AndIf(endTime.HasValue, x => x.TimeStamp <= _clock.Normalize(endTime!.Value))
.AndIf(level.HasValue, x => x.Level == GetLogEventLevel(level!.Value))
.AndIf(!machineName.IsNullOrWhiteSpace(), x => x.Fields.MachineName!.Contains(machineName!))
.AndIf(!environment.IsNullOrWhiteSpace(), x => x.Fields.Environment!.Contains(environment!))
.AndIf(!application.IsNullOrWhiteSpace(), x => x.Fields.Application!.Contains(application!))
.AndIf(!context.IsNullOrWhiteSpace(), x => x.Fields.Context == context)
.AndIf(!requestId.IsNullOrWhiteSpace(), x => x.Fields.RequestId == requestId)
.AndIf(!requestPath.IsNullOrWhiteSpace(), x => x.Fields.RequestPath!.StartsWith(requestPath!))
.AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.Fields.CorrelationId!.Contains(correlationId!))
.AndIf(processId.HasValue, x => x.Fields.ProcessId == processId)
.AndIf(threadId.HasValue, x => x.Fields.ThreadId == threadId)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null);
var response = await client.CountAsync<SerilogInfo>((dsl) =>
dsl.Indices(indexName)
.Query(log => log.Bool(b => b.Must(querys.ToArray()))),
return await _expressionQueryService.GetCountAsync(
CreateIndex(),
expression,
cancellationToken);
if (response.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query log count failed: {errorMessage}", errorMessage);
}
return response.Count;
}
/// <summary>
@ -295,308 +214,52 @@ 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 sorts = GetOrDefaultSort(indexMapping, sorting);
var querys = BuildQueryDescriptor(
indexMapping,
startTime,
endTime,
level,
machineName,
environment,
application,
context,
requestId,
requestPath,
correlationId,
processId,
threadId,
hasException);
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>>(serilogLogs);
}
protected virtual List<Query> BuildQueryDescriptor(
IndexMappingInfo indexMappingInfo,
DateTime? startTime = null,
DateTime? endTime = null,
LogLevel? level = null,
string? machineName = null,
string? environment = null,
string? application = null,
string? context = null,
string? requestId = null,
string? requestPath = null,
string? correlationId = null,
int? processId = null,
int? threadId = null,
bool? hasException = null)
{
var queries = new List<Query>();
if (_currentTenant.IsAvailable)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.TenantId"), _currentTenant.GetId().ToString()));
}
if (startTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, "@timestamp"))
{
Gte = _clock.Normalize(startTime.Value),
});
}
if (endTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, "@timestamp"))
{
Lte = _clock.Normalize(endTime.Value),
});
}
if (level.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "level"), GetLogEventLevel(level.Value).ToString()));
}
if (!machineName.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.MachineName"))
{
Value = $"*{machineName}*"
});
}
if (!environment.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.EnvironmentName"))
{
Value = $"*{environment}*"
});
}
if (!application.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.ApplicationName"))
{
Value = $"*{application}*"
});
}
if (!context.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.SourceContext"), context));
}
if (!requestId.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.RequestId"), requestId));
}
if (!requestPath.IsNullOrWhiteSpace())
{
// 前缀匹配
queries.Add(new MatchPhrasePrefixQuery(GetField(indexMappingInfo, "fields.RequestPath"), requestPath));
}
if (!correlationId.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.CorrelationId"))
{
Value = $"*{correlationId}*"
});
}
if (processId.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.ProcessId"), FieldValue.FromValue(processId.Value)));
}
if (threadId.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.ThreadId"), FieldValue.FromValue(threadId.Value)));
}
if (hasException.HasValue)
{
if (hasException.Value)
{
/* exceptions字段则就是有异常信息
* "exists": {
"field": "exceptions"
}
*/
queries.Add(new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions")));
}
else
{
// 不存在 exceptions字段就是没有异常信息的消息
/*
* "bool": {
"must_not": [
{
"exists": {
"field": "exceptions"
}
}
]
}
*/
queries.Add(new BoolQuery
{
MustNot = new List<Query>
{
new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions"))
}
});
}
}
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.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query log failed: {errorMessage}", errorMessage);
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.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query log failed: {errorMessage}", errorMessage);
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())
if (sorting.IsNullOrWhiteSpace())
{
return null;
sorting = $"{ElasticsearchJsonFormatter.TimestampPropertyName} DESC";
}
var firstHit = firstResponse.Hits.FirstOrDefault();
if (firstHit?.Sort == null || !firstHit.Sort.Any())
// 额外处理一下timestamp字段
else if (sorting.Contains("timestamp", StringComparison.CurrentCultureIgnoreCase))
{
return null;
sorting = sorting
.Replace("timestamp", ElasticsearchJsonFormatter.TimestampPropertyName, StringComparison.CurrentCultureIgnoreCase)
.Replace("@@", "@");
}
// 获取skipCount最近一条数据作为searchAfter
var secondResponse = await client.SearchAsync<SerilogInfo>(
dsl => dsl.Indices(CreateIndex())
.Query(query)
// 反转排序取第一个数据作为起始索引
.Sort(sorts.ReverseSort()!.ToArray())
.SourceIncludes(x => x.Level)
.SearchAfter(firstHit.Sort.ToList())
.Size(1),
cancellationToken);
if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any())
{
return null;
}
Expression<Func<SerilogInfo, bool>> expression = _ => true;
expression = expression
.AndIf(startTime.HasValue, x => x.TimeStamp >= _clock.Normalize(startTime!.Value))
.AndIf(endTime.HasValue, x => x.TimeStamp <= _clock.Normalize(endTime!.Value))
.AndIf(level.HasValue, x => x.Level == GetLogEventLevel(level!.Value))
.AndIf(!machineName.IsNullOrWhiteSpace(), x => x.Fields.MachineName!.Contains(machineName!))
.AndIf(!environment.IsNullOrWhiteSpace(), x => x.Fields.Environment!.Contains(environment!))
.AndIf(!application.IsNullOrWhiteSpace(), x => x.Fields.Application!.Contains(application!))
.AndIf(!context.IsNullOrWhiteSpace(), x => x.Fields.Context == context)
.AndIf(!requestId.IsNullOrWhiteSpace(), x => x.Fields.RequestId == requestId)
.AndIf(!requestPath.IsNullOrWhiteSpace(), x => x.Fields.RequestPath!.StartsWith(requestPath!))
.AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.Fields.CorrelationId!.Contains(correlationId!))
.AndIf(processId.HasValue, x => x.Fields.ProcessId == processId)
.AndIf(threadId.HasValue, x => x.Fields.ThreadId == threadId)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null);
var lastHit = secondResponse.Hits.LastOrDefault();
if (lastHit?.Sort == null || !lastHit.Sort.Any())
{
return null;
}
var serilogLogs = await _expressionQueryService.GetListAsync(
CreateIndex(),
expression,
sorting: sorting,
maxResultCount: maxResultCount,
skipCount: skipCount,
sourceExcludes: includeDetails == false
? Fields.FromFields(
[
new Field("exceptions"),
])
: null,
cancellationToken: cancellationToken);
return lastHit.Sort.ToList();
return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs);
}
protected virtual string CreateIndex(DateTimeOffset? offset = null)
@ -620,43 +283,4 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
_ => LogEventLevel.Verbose,
};
}
private static SortOptions[]? GetOrDefaultSort(IndexMappingInfo indexMappingInfo, string? sorting = null)
{
var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase)
? SortOrder.Asc : SortOrder.Desc;
sorting = !sorting.IsNullOrWhiteSpace()
? sorting.Split()[0]
: ElasticsearchJsonFormatter.TimestampPropertyName;
SortOptions[]? sorts = null;
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 sorts;
}
private static string GetField(IndexMappingInfo indexMappingInfo, string fieldFullPath)
{
return indexMappingInfo.GetExactFieldPath(fieldFullPath);
}
}

32
aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs

@ -0,0 +1,32 @@
using Volo.Abp.Specifications;
namespace System.Linq.Expressions;
internal static class ExpressionFuncExtensions
{
public static Expression<Func<T, bool>> AndIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.And(first, second);
}
return first;
}
public static Expression<Func<T, bool>> OrIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.Or(first, second);
}
return first;
}
}

1
aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs

@ -83,6 +83,7 @@ public class DefaultLoggingManager : ILoggingManager, ISingletonDependency
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
Logger.LogDebug("No logging manager is available!");

1
aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs

@ -58,5 +58,6 @@ public interface ILoggingManager
string? sorting = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default);
}

Loading…
Cancel
Save