33 changed files with 1064 additions and 808 deletions
@ -1,374 +0,0 @@ |
|||
using Elastic.Clients.Elasticsearch; |
|||
using Elastic.Clients.Elasticsearch.QueryDsl; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.AuditLogging.Elasticsearch; |
|||
|
|||
/// <summary>
|
|||
/// ES DSL表达式翻译器:把 <see cref="Expression{TDelegate}"/> 翻译为
|
|||
/// Elastic.Clients.Elasticsearch 的 <see cref="Query"/>(QueryDsl)。
|
|||
/// <para>
|
|||
/// 支持的算子(超出即抛 <see cref="NotSupportedException"/>,fail loud):
|
|||
/// <list type="bullet">
|
|||
/// <item>逻辑:&&、||、!(映射为 bool filter / should+minimum_should_match / must_not)</item>
|
|||
/// <item>比较:==、!=、>、>=、<、<=(数值与日期映射为 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); |
|||
} |
|||
} |
|||
@ -1,12 +1,27 @@ |
|||
namespace LINGYUN.Abp.Logging; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.Logging; |
|||
|
|||
public class LogException |
|||
{ |
|||
[JsonPropertyName("SourceContext")] |
|||
public int Depth { get; set; } |
|||
|
|||
[JsonPropertyName("ClassName")] |
|||
public string? Class { get; set; } |
|||
|
|||
[JsonPropertyName("Message")] |
|||
public string? Message { get; set; } |
|||
|
|||
[JsonPropertyName("Source")] |
|||
public string? Source { get; set; } |
|||
|
|||
[JsonPropertyName("StackTraceString")] |
|||
public string? StackTrace { get; set; } |
|||
|
|||
[JsonPropertyName("HResult")] |
|||
public int HResult { get; set; } |
|||
|
|||
[JsonPropertyName("HelpURL")] |
|||
public string? HelpURL { get; set; } |
|||
} |
|||
|
|||
@ -1,20 +1,55 @@ |
|||
namespace LINGYUN.Abp.Logging; |
|||
using System; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.Logging; |
|||
|
|||
public class LogField |
|||
{ |
|||
[JsonPropertyName("UniqueId")] |
|||
public string? Id { get; set; } |
|||
|
|||
[JsonPropertyName(AbpLoggingEnricherPropertyNames.MachineName)] |
|||
public string? MachineName { get; set; } |
|||
|
|||
[JsonPropertyName(AbpLoggingEnricherPropertyNames.EnvironmentName)] |
|||
public string? Environment { get; set; } |
|||
|
|||
[JsonPropertyName("ApplicationName")] |
|||
public string? Application { get; set; } |
|||
|
|||
[JsonPropertyName("SourceContext")] |
|||
public string? Context { get; set; } |
|||
|
|||
[JsonPropertyName("ActionId")] |
|||
public string? ActionId { get; set; } |
|||
|
|||
[JsonPropertyName("ActionName")] |
|||
public string? ActionName { get; set; } |
|||
|
|||
[JsonPropertyName("RequestId")] |
|||
public string? RequestId { get; set; } |
|||
|
|||
[JsonPropertyName("RequestPath")] |
|||
public string? RequestPath { get; set; } |
|||
|
|||
[JsonPropertyName("ConnectionId")] |
|||
public string? ConnectionId { get; set; } |
|||
|
|||
[JsonPropertyName("CorrelationId")] |
|||
public string? CorrelationId { get; set; } |
|||
|
|||
[JsonPropertyName("ClientId")] |
|||
public string? ClientId { get; set; } |
|||
|
|||
[JsonPropertyName("UserId")] |
|||
public string? UserId { get; set; } |
|||
|
|||
[JsonPropertyName("TenantId")] |
|||
public Guid? TenantId { get; set; } |
|||
|
|||
[JsonPropertyName("ProcessId")] |
|||
public int? ProcessId { get; set; } |
|||
|
|||
[JsonPropertyName("ThreadId")] |
|||
public int? ThreadId { get; set; } |
|||
} |
|||
|
|||
@ -1,14 +1,24 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.Logging; |
|||
|
|||
public class LogInfo |
|||
{ |
|||
[JsonPropertyName("@timestamp")] |
|||
public DateTime TimeStamp { get; set; } |
|||
|
|||
[JsonPropertyName("level")] |
|||
public LogLevel Level { get; set; } |
|||
|
|||
[JsonPropertyName("message")] |
|||
public string? Message { get; set; } |
|||
|
|||
[JsonPropertyName("fields")] |
|||
public LogField Fields { get; set; } = default!; |
|||
|
|||
[JsonPropertyName("exceptions")] |
|||
public List<LogException>? Exceptions { get; set; } |
|||
} |
|||
|
|||
@ -1,196 +0,0 @@ |
|||
using Moq.AutoMock; |
|||
using Newtonsoft.Json; |
|||
using Shouldly; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Specifications; |
|||
using Xunit; |
|||
|
|||
namespace LINGYUN.Abp.AuditLogging.Elasticsearch |
|||
{ |
|||
public class AuditLogManagerTests : AbpAuditLoggingElasticsearchTestBase |
|||
{ |
|||
private readonly IAuditLogWriter _writer; |
|||
private readonly IAuditLogManager _manager; |
|||
|
|||
public AuditLogManagerTests() |
|||
{ |
|||
_writer = GetRequiredService<IAuditLogWriter>(); |
|||
_manager = GetRequiredService<IAuditLogManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Save_Audit_Log_Should_Be_Find_By_Id() |
|||
{ |
|||
var mock = new AutoMocker(); |
|||
var auditLogInfo = mock.CreateInstance<AuditLogInfo>(); |
|||
|
|||
var id = await _writer.WriteAsync(auditLogInfo); |
|||
id.ShouldNotBeNullOrWhiteSpace(); |
|||
|
|||
var findId = Guid.Parse(id); |
|||
var auditLog = await _manager.GetAsync(findId); |
|||
|
|||
auditLog.ShouldNotBeNull(); |
|||
auditLog.Id.ShouldBe(findId); |
|||
|
|||
await _manager.DeleteAsync(findId); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Save_Audit_Log_Should_Get_List() |
|||
{ |
|||
var count = 10; |
|||
await MockcAsync(count); |
|||
|
|||
// 延迟等待ES索引完成
|
|||
await Task.Delay(5000); |
|||
|
|||
// 异常应该只有3个
|
|||
(await _manager.GetCountAsync( |
|||
hasException: true)).ShouldBe(3); |
|||
|
|||
// 正常可以查询7个
|
|||
(await _manager.GetCountAsync( |
|||
hasException: false)).ShouldBe(7); |
|||
|
|||
// POST方法能查到5个
|
|||
(await _manager.GetCountAsync( |
|||
httpMethod: "POST")).ShouldBe(5); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
startTime: DateTime.Now.AddDays(-1).AddHours(5))).ShouldBe(6); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
endTime: DateTime.Now.AddDays(-1))).ShouldBe(4); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
startTime: DateTime.Now.AddDays(-3).AddHours(1), |
|||
endTime: DateTime.Now)).ShouldBe(8); |
|||
|
|||
// 索引5只存在一个
|
|||
(await _manager.GetCountAsync( |
|||
userName: "_user_5", |
|||
clientId: "_client_5")).ShouldBe(1); |
|||
|
|||
var logs = await _manager.GetListAsync( |
|||
userName: "_user_5", |
|||
clientId: "_client_5", |
|||
maxResultCount: count); |
|||
|
|||
logs.Count.ShouldBe(1); |
|||
logs[0].Url.ShouldBe("_url_5"); |
|||
logs[0].BrowserInfo.ShouldBe("_browser_5"); |
|||
logs[0].ApplicationName.ShouldBe("_app_5"); |
|||
|
|||
await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Save_Audit_Log_Should_Get_List_With_Specification() |
|||
{ |
|||
var count = 10; |
|||
await MockcAsync(count); |
|||
|
|||
// 延迟等待ES索引完成
|
|||
await Task.Delay(5000); |
|||
|
|||
// 异常应该只有3个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.Exceptions != null))).ShouldBe(3); |
|||
|
|||
// 请求参数中包含 AAAAA 应该只有3个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3); |
|||
|
|||
// 正常可以查询7个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.Exceptions == null))).ShouldBe(7); |
|||
|
|||
// POST方法能查到5个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.HttpMethod == "POST"))).ShouldBe(5); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) && |
|||
x.ExecutionTime <= DateTime.Now))).ShouldBe(8); |
|||
|
|||
// 索引5只存在一个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1); |
|||
|
|||
var logs = await _manager.GetListAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.UserName == "_user_5" && x.ClientId == "_client_5"), |
|||
maxResultCount: count); |
|||
|
|||
logs.Count.ShouldBe(1); |
|||
logs[0].Url.ShouldBe("_url_5"); |
|||
logs[0].BrowserInfo.ShouldBe("_browser_5"); |
|||
logs[0].ApplicationName.ShouldBe("_app_5"); |
|||
|
|||
await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); |
|||
} |
|||
|
|||
protected async virtual Task<List<string>> MockcAsync(int count) |
|||
{ |
|||
var mock = new AutoMocker(); |
|||
|
|||
var auditLogIds = new List<string>(); |
|||
|
|||
for (var i = 1; i <= count; i++) |
|||
{ |
|||
var auditLogInfo = mock.CreateInstance<AuditLogInfo>(); |
|||
auditLogInfo.ClientId = $"_client_{i}"; |
|||
auditLogInfo.Url = $"_url_{i}"; |
|||
auditLogInfo.UserName = $"_user_{i}"; |
|||
auditLogInfo.ApplicationName = $"_app_{i}"; |
|||
auditLogInfo.BrowserInfo = $"_browser_{i}"; |
|||
auditLogInfo.ExecutionTime = DateTime.Now; |
|||
if (i % 3 == 0) |
|||
{ |
|||
auditLogInfo.Exceptions.Add(new Exception($"_exception_{i}")); |
|||
auditLogInfo.Actions.Add( |
|||
new AuditLogActionInfo |
|||
{ |
|||
ServiceName = $"_service_{i}", |
|||
MethodName = $"_method_{i}", |
|||
ExecutionTime = DateTime.Now, |
|||
ExecutionDuration = new Random().Next(1, 1000), |
|||
Parameters = JsonConvert.SerializeObject(new |
|||
{ |
|||
Paramter = "AAAAA", |
|||
}), |
|||
}); |
|||
} |
|||
|
|||
if (i % 2 == 0) |
|||
{ |
|||
auditLogInfo.HttpMethod = "POST"; |
|||
} |
|||
|
|||
if (i % 4 == 0) |
|||
{ |
|||
auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-3); |
|||
} |
|||
|
|||
if (i % 5 == 0) |
|||
{ |
|||
auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-2); |
|||
} |
|||
|
|||
auditLogIds.Add(await _writer.WriteAsync(auditLogInfo)); |
|||
} |
|||
|
|||
return auditLogIds; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.AuditLogging.Elasticsearch; |
|||
|
|||
public class ElasticsearchAuditLogManager_Tests : AuditLogManager_Tests<AbpAuditLoggingElasticsearchTestModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net10.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<IsPackable>false</IsPackable> |
|||
<Platforms>AnyCPU</Platforms> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<PackageReference Include="Moq.AutoMock" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\framework\auditing\LINGYUN.Abp.AuditLogging\LINGYUN.Abp.AuditLogging.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,8 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
|
|||
namespace LINGYUN.Abp.AuditLogging; |
|||
|
|||
public abstract class AbpAuditLoggingTestBase : AbpTestsBase<AbpAuditLoggingTestModule> |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.AuditLogging; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpTestsBaseModule), |
|||
typeof(AbpAuditLoggingModule))] |
|||
public class AbpAuditLoggingTestModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,198 @@ |
|||
using Moq.AutoMock; |
|||
using Newtonsoft.Json; |
|||
using Shouldly; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Specifications; |
|||
using Volo.Abp.Testing; |
|||
using Xunit; |
|||
|
|||
namespace LINGYUN.Abp.AuditLogging; |
|||
|
|||
public abstract class AuditLogManager_Tests<TStartupModule> : AbpIntegratedTest<TStartupModule> |
|||
where TStartupModule : IAbpModule |
|||
{ |
|||
private readonly IAuditLogWriter _writer; |
|||
private readonly IAuditLogManager _manager; |
|||
|
|||
public AuditLogManager_Tests() |
|||
{ |
|||
_writer = GetRequiredService<IAuditLogWriter>(); |
|||
_manager = GetRequiredService<IAuditLogManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Save_Audit_Log_Should_Be_Find_By_Id() |
|||
{ |
|||
var mock = new AutoMocker(); |
|||
var auditLogInfo = mock.CreateInstance<AuditLogInfo>(); |
|||
|
|||
var id = await _writer.WriteAsync(auditLogInfo); |
|||
id.ShouldNotBeNullOrWhiteSpace(); |
|||
|
|||
var findId = Guid.Parse(id); |
|||
var auditLog = await _manager.GetAsync(findId); |
|||
|
|||
auditLog.ShouldNotBeNull(); |
|||
auditLog.Id.ShouldBe(findId); |
|||
|
|||
await _manager.DeleteAsync(findId); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Save_Audit_Log_Should_Get_List() |
|||
{ |
|||
var count = 10; |
|||
await MockcAsync(count); |
|||
|
|||
// 延迟等待写入完成
|
|||
await Task.Delay(5000); |
|||
|
|||
// 异常应该只有3个
|
|||
(await _manager.GetCountAsync( |
|||
hasException: true)).ShouldBe(3); |
|||
|
|||
// 正常可以查询7个
|
|||
(await _manager.GetCountAsync( |
|||
hasException: false)).ShouldBe(7); |
|||
|
|||
// POST方法能查到5个
|
|||
(await _manager.GetCountAsync( |
|||
httpMethod: "POST")).ShouldBe(5); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
startTime: DateTime.Now.AddDays(-1).AddHours(5))).ShouldBe(6); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
endTime: DateTime.Now.AddDays(-1))).ShouldBe(4); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
startTime: DateTime.Now.AddDays(-3).AddHours(1), |
|||
endTime: DateTime.Now)).ShouldBe(8); |
|||
|
|||
// 索引5只存在一个
|
|||
(await _manager.GetCountAsync( |
|||
userName: "_user_5", |
|||
clientId: "_client_5")).ShouldBe(1); |
|||
|
|||
var logs = await _manager.GetListAsync( |
|||
userName: "_user_5", |
|||
clientId: "_client_5", |
|||
maxResultCount: count); |
|||
|
|||
logs.Count.ShouldBe(1); |
|||
logs[0].Url.ShouldBe("_url_5"); |
|||
logs[0].BrowserInfo.ShouldBe("_browser_5"); |
|||
logs[0].ApplicationName.ShouldBe("_app_5"); |
|||
|
|||
await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Save_Audit_Log_Should_Get_List_With_Specification() |
|||
{ |
|||
var count = 10; |
|||
await MockcAsync(count); |
|||
|
|||
// 延迟等待写入完成
|
|||
await Task.Delay(5000); |
|||
|
|||
// 请求参数中包含 AAAAA 应该只有3个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3); |
|||
|
|||
// 异常应该只有3个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.Exceptions != null))).ShouldBe(3); |
|||
|
|||
// 正常可以查询7个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.Exceptions == null))).ShouldBe(7); |
|||
|
|||
// POST方法能查到5个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.HttpMethod == "POST"))).ShouldBe(5); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4); |
|||
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) && |
|||
x.ExecutionTime <= DateTime.Now))).ShouldBe(8); |
|||
|
|||
// 索引5只存在一个
|
|||
(await _manager.GetCountAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1); |
|||
|
|||
var logs = await _manager.GetListAsync( |
|||
new ExpressionSpecification<AuditLog>(x => x.UserName == "_user_5" && x.ClientId == "_client_5"), |
|||
maxResultCount: count); |
|||
|
|||
logs.Count.ShouldBe(1); |
|||
logs[0].Url.ShouldBe("_url_5"); |
|||
logs[0].BrowserInfo.ShouldBe("_browser_5"); |
|||
logs[0].ApplicationName.ShouldBe("_app_5"); |
|||
|
|||
await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); |
|||
} |
|||
|
|||
protected async virtual Task<List<string>> MockcAsync(int count) |
|||
{ |
|||
var mock = new AutoMocker(); |
|||
|
|||
var auditLogIds = new List<string>(); |
|||
|
|||
for (var i = 1; i <= count; i++) |
|||
{ |
|||
var auditLogInfo = mock.CreateInstance<AuditLogInfo>(); |
|||
auditLogInfo.ClientId = $"_client_{i}"; |
|||
auditLogInfo.Url = $"_url_{i}"; |
|||
auditLogInfo.UserName = $"_user_{i}"; |
|||
auditLogInfo.ApplicationName = $"_app_{i}"; |
|||
auditLogInfo.BrowserInfo = $"_browser_{i}"; |
|||
auditLogInfo.ExecutionTime = DateTime.Now; |
|||
if (i % 3 == 0) |
|||
{ |
|||
auditLogInfo.Exceptions.Add(new Exception($"_exception_{i}")); |
|||
auditLogInfo.Actions.Add( |
|||
new AuditLogActionInfo |
|||
{ |
|||
ServiceName = $"_service_{i}", |
|||
MethodName = $"_method_{i}", |
|||
ExecutionTime = DateTime.Now, |
|||
ExecutionDuration = new Random().Next(1, 1000), |
|||
Parameters = JsonConvert.SerializeObject(new |
|||
{ |
|||
Paramter = "AAAAA", |
|||
}), |
|||
}); |
|||
} |
|||
|
|||
if (i % 2 == 0) |
|||
{ |
|||
auditLogInfo.HttpMethod = "POST"; |
|||
} |
|||
|
|||
if (i % 4 == 0) |
|||
{ |
|||
auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-3); |
|||
} |
|||
|
|||
if (i % 5 == 0) |
|||
{ |
|||
auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-2); |
|||
} |
|||
|
|||
auditLogIds.Add(await _writer.WriteAsync(auditLogInfo)); |
|||
} |
|||
|
|||
return auditLogIds; |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net10.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<IsPackable>false</IsPackable> |
|||
<Platforms>AnyCPU</Platforms> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<PackageReference Include="Moq.AutoMock" /> |
|||
<PackageReference Include="Serilog.Sinks.Elasticsearch" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\framework\logging\LINGYUN.Abp.Logging.Serilog.Elasticsearch\LINGYUN.Abp.Logging.Serilog.Elasticsearch.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Logging.Tests\LINGYUN.Abp.Logging.Tests.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,7 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
|
|||
namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; |
|||
|
|||
public abstract class AbpLoggingSerilogElasticsearchTestBase : AbpTestsBase<AbpLoggingSerilogElasticsearchTestModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using Elastic.Clients.Elasticsearch; |
|||
using LINGYUN.Abp.Elasticsearch; |
|||
using LINGYUN.Abp.Tests; |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpTestsBaseModule), |
|||
typeof(AbpLoggingTestModule), |
|||
typeof(AbpLoggingSerilogElasticsearchModule))] |
|||
public class AbpLoggingSerilogElasticsearchTestModule : AbpModule |
|||
{ |
|||
private const string UserSecretsId = "11A604D4-3A64-4F92-94C6-5B1525CF63DD"; |
|||
|
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder => |
|||
{ |
|||
builder.AddUserSecrets(UserSecretsId); |
|||
})); |
|||
} |
|||
|
|||
public override void OnPostApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
RemoveTestIndexs(context.ServiceProvider); |
|||
} |
|||
|
|||
public override void OnApplicationShutdown(ApplicationShutdownContext context) |
|||
{ |
|||
RemoveTestIndexs(context.ServiceProvider); |
|||
} |
|||
|
|||
private static void RemoveTestIndexs(IServiceProvider serviceProvider) |
|||
{ |
|||
var clientFactory = serviceProvider.GetRequiredService<IElasticsearchClientFactory>(); |
|||
var client = clientFactory.Create(); |
|||
var indicesResponse = client.Indices.Get("abp-test-logging"); |
|||
if (indicesResponse.IsSuccess()) |
|||
{ |
|||
foreach (var index in indicesResponse.Indices) |
|||
{ |
|||
client.Indices.Delete(index.Key); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using NSubstitute.Extensions; |
|||
using Serilog; |
|||
|
|||
namespace LINGYUN.Abp.Logging.Serilog.Elasticsearch; |
|||
|
|||
public class ElasticsearchLoggingManager_Tests : LoggingManager_Tests<AbpLoggingSerilogElasticsearchTestModule> |
|||
{ |
|||
protected override void BeforeAddApplication(IServiceCollection services) |
|||
{ |
|||
Log.Logger = new LoggerConfiguration() |
|||
.MinimumLevel.Debug() |
|||
.Enrich.FromLogContext() |
|||
.Enrich.WithUniqueId() |
|||
.WriteTo.Elasticsearch( |
|||
nodeUris: "http://localhost:9200", |
|||
indexFormat: "abp-test-logging") |
|||
.CreateLogger(); |
|||
|
|||
services.AddLogging(logging => |
|||
{ |
|||
logging.AddSerilog(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net10.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<IsPackable>false</IsPackable> |
|||
<Platforms>AnyCPU</Platforms> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<PackageReference Include="Moq.AutoMock" /> |
|||
<PackageReference Include="Serilog.Sinks.InMemory" /> |
|||
<PackageReference Include="Serilog.Extensions.Logging" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\framework\logging\LINGYUN.Abp.Logging\LINGYUN.Abp.Logging.csproj" /> |
|||
<ProjectReference Include="..\..\framework\logging\LINGYUN.Abp.Serilog.Enrichers.UniqueId\LINGYUN.Abp.Serilog.Enrichers.UniqueId.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,7 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
|
|||
namespace LINGYUN.Abp.Logging; |
|||
|
|||
public abstract class AbpLoggingTestBase : AbpTestsBase<AbpLoggingTestModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using LINGYUN.Abp.Serilog.Enrichers.UniqueId; |
|||
using LINGYUN.Abp.Tests; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Logging; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpTestsBaseModule), |
|||
typeof(AbpLoggingModule), |
|||
typeof(AbpSerilogEnrichersUniqueIdModule))] |
|||
public class AbpLoggingTestModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Serilog; |
|||
using Serilog.Sinks.InMemory; |
|||
using Shouldly; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Specifications; |
|||
using Volo.Abp.Testing; |
|||
using Xunit; |
|||
|
|||
namespace LINGYUN.Abp.Logging; |
|||
|
|||
public abstract class LoggingManager_Tests<TStartupModule> : AbpIntegratedTest<TStartupModule> |
|||
where TStartupModule : IAbpModule |
|||
{ |
|||
private readonly string _context; |
|||
private readonly Microsoft.Extensions.Logging.ILogger _logger; |
|||
private readonly ILoggingManager _manager; |
|||
|
|||
public LoggingManager_Tests() |
|||
{ |
|||
_manager = GetRequiredService<ILoggingManager>(); |
|||
|
|||
_context = GetType().FullName!; |
|||
var loggerFactory = GetRequiredService<ILoggerFactory>(); |
|||
_logger = loggerFactory.CreateLogger(_context); |
|||
} |
|||
|
|||
protected override void BeforeAddApplication(IServiceCollection services) |
|||
{ |
|||
Log.Logger = new LoggerConfiguration() |
|||
.MinimumLevel.Debug() |
|||
.Enrich.FromLogContext() |
|||
.Enrich.WithUniqueId() |
|||
.WriteTo.InMemory() |
|||
.CreateLogger(); |
|||
|
|||
services.AddLogging(logging => |
|||
{ |
|||
logging.AddSerilog(); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List() |
|||
{ |
|||
_logger.LogDebug("xunit test debug log"); |
|||
_logger.LogInformation("xunit test information log"); |
|||
_logger.LogWarning("xunit test warning log"); |
|||
_logger.LogError("xunit test error log"); |
|||
|
|||
await Log.CloseAndFlushAsync(); |
|||
|
|||
await Task.Delay(5000); |
|||
|
|||
(await _manager.GetCountAsync(context: _context)).ShouldBe(4); |
|||
|
|||
(await _manager.GetCountAsync(level: LogLevel.Information, context: _context)).ShouldBe(1); |
|||
|
|||
var logs = await _manager.GetListAsync(level: LogLevel.Information, context: _context); |
|||
logs.Count.ShouldBe(1); |
|||
logs[0].Level.ShouldBe(LogLevel.Information); |
|||
logs[0].Message.ShouldBe("xunit test information log"); |
|||
logs[0].Fields.ShouldNotBeNull(); |
|||
logs[0].Fields.Id.ShouldNotBeNullOrWhiteSpace(); |
|||
logs[0].Fields.Context.ShouldBe(_context); |
|||
|
|||
var log = await _manager.GetAsync(logs[0].Fields.Id); |
|||
log.Message.ShouldBe("xunit test information log"); |
|||
log.Fields.ShouldNotBeNull(); |
|||
log.Fields.Id.ShouldNotBeNullOrWhiteSpace(); |
|||
log.Fields.Id.ShouldBe(logs[0].Fields.Id); |
|||
log.Fields.Context.ShouldBe(_context); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_With_Specification() |
|||
{ |
|||
_logger.LogDebug("xunit test debug log"); |
|||
_logger.LogInformation("xunit test information log"); |
|||
_logger.LogWarning("xunit test warning log"); |
|||
_logger.LogError("xunit test error log"); |
|||
|
|||
await Log.CloseAndFlushAsync(); |
|||
|
|||
await Task.Delay(5000); |
|||
|
|||
var specification = new ExpressionSpecification<LogInfo>( |
|||
x => x.Level == LogLevel.Information && x.Fields.Context == _context); |
|||
|
|||
var logs = await _manager.GetListAsync(specification); |
|||
logs.Count.ShouldBe(1); |
|||
logs[0].Level.ShouldBe(LogLevel.Information); |
|||
logs[0].Message.ShouldBe("xunit test information log"); |
|||
logs[0].Fields.ShouldNotBeNull(); |
|||
logs[0].Fields.Id.ShouldNotBeNullOrWhiteSpace(); |
|||
logs[0].Fields.Context.ShouldBe(_context); |
|||
|
|||
var log = await _manager.GetAsync(logs[0].Fields.Id); |
|||
log.Message.ShouldBe("xunit test information log"); |
|||
log.Fields.ShouldNotBeNull(); |
|||
log.Fields.Id.ShouldNotBeNullOrWhiteSpace(); |
|||
log.Fields.Id.ShouldBe(logs[0].Fields.Id); |
|||
log.Fields.Context.ShouldBe(_context); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue