[codex] Increase structured log relational test coverage (#7451)
* Increase structured log relational test coverage * Address structured log coverage review comments
This commit is contained in:
parent
06b757bc95
commit
6dee7094f0
|
|
@ -37,35 +37,23 @@ public class RelationalStructuredLogMapperTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTimestamp_StoresUtcIso8601Text()
|
||||
public void Map_FromReader_DeserializesPersistedFields()
|
||||
{
|
||||
var timestamp = new DateTimeOffset(2026, 5, 13, 15, 0, 0, TimeSpan.FromHours(2));
|
||||
|
||||
var formatted = RelationalStructuredLogMapper.FormatTimestamp(timestamp);
|
||||
|
||||
Assert.Equal("2026-05-13T13:00:00.0000000+00:00", formatted);
|
||||
Assert.Equal(timestamp.ToUniversalTime(), RelationalStructuredLogMapper.ParseTimestamp(formatted));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapReader_DeserializesStoredJsonAndNullableFields()
|
||||
{
|
||||
var timestamp = new DateTimeOffset(2026, 5, 13, 15, 0, 0, TimeSpan.FromHours(2));
|
||||
var logEvent = new StructuredLogEvent
|
||||
{
|
||||
Id = "event-a",
|
||||
Sequence = 123,
|
||||
Timestamp = timestamp,
|
||||
ReceivedAt = timestamp.AddSeconds(1),
|
||||
Level = StructuredLogLevel.Warning,
|
||||
Sequence = 42,
|
||||
Timestamp = new(2026, 5, 13, 15, 0, 0, TimeSpan.FromHours(2)),
|
||||
ReceivedAt = new(2026, 5, 13, 13, 0, 1, TimeSpan.Zero),
|
||||
Level = StructuredLogLevel.Critical,
|
||||
Category = "Elsa.Tests",
|
||||
EventId = 42,
|
||||
EventName = "TestEvent",
|
||||
Message = "Message",
|
||||
MessageTemplate = "Message {Value}",
|
||||
EventId = 17,
|
||||
EventName = "Failed",
|
||||
Message = "Failed with scope",
|
||||
MessageTemplate = "Failed with {Scope}",
|
||||
Exception = new("System.InvalidOperationException", "Boom", "Stack"),
|
||||
Scopes = new Dictionary<string, string?> { ["Scope"] = "Value" },
|
||||
Properties = new Dictionary<string, string?> { ["Property"] = "Value" },
|
||||
Properties = new Dictionary<string, string?> { ["Property"] = null },
|
||||
TraceId = "trace-a",
|
||||
SpanId = "span-a",
|
||||
CorrelationId = "correlation-a",
|
||||
|
|
@ -74,10 +62,10 @@ public class RelationalStructuredLogMapperTests
|
|||
WorkflowInstanceId = "instance-a",
|
||||
SourceId = "source-a"
|
||||
};
|
||||
var record = _mapper.Map(logEvent);
|
||||
|
||||
using var reader = CreateReader(_mapper.Map(logEvent));
|
||||
using var reader = CreateReader(record);
|
||||
Assert.True(reader.Read());
|
||||
|
||||
var mapped = _mapper.Map(reader);
|
||||
|
||||
Assert.Equal(logEvent.Id, mapped.Id);
|
||||
|
|
@ -90,11 +78,10 @@ public class RelationalStructuredLogMapperTests
|
|||
Assert.Equal(logEvent.EventName, mapped.EventName);
|
||||
Assert.Equal(logEvent.Message, mapped.Message);
|
||||
Assert.Equal(logEvent.MessageTemplate, mapped.MessageTemplate);
|
||||
Assert.Equal(logEvent.Exception.Type, mapped.Exception!.Type);
|
||||
Assert.Equal(logEvent.Exception.Message, mapped.Exception.Message);
|
||||
Assert.Equal(logEvent.Exception.StackTrace, mapped.Exception.StackTrace);
|
||||
Assert.Equal(logEvent.Scopes, mapped.Scopes);
|
||||
Assert.Equal(logEvent.Properties, mapped.Properties);
|
||||
Assert.Equal(logEvent.Exception, mapped.Exception);
|
||||
Assert.Equal("Value", mapped.Scopes["Scope"]);
|
||||
Assert.True(mapped.Properties.TryGetValue("Property", out var propertyValue));
|
||||
Assert.Null(propertyValue);
|
||||
Assert.Equal(logEvent.TraceId, mapped.TraceId);
|
||||
Assert.Equal(logEvent.SpanId, mapped.SpanId);
|
||||
Assert.Equal(logEvent.CorrelationId, mapped.CorrelationId);
|
||||
|
|
@ -105,7 +92,45 @@ public class RelationalStructuredLogMapperTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void MapReader_TreatsNullAndWhitespaceJsonAsEmptyValues()
|
||||
public void Map_FromReader_ReturnsEmptyCollections_WhenJsonFieldsAreNull()
|
||||
{
|
||||
var record = new RelationalStructuredLogRecord
|
||||
{
|
||||
Id = "event-a",
|
||||
Sequence = 1,
|
||||
Timestamp = RelationalStructuredLogMapper.FormatTimestamp(DateTimeOffset.UtcNow),
|
||||
ReceivedAt = RelationalStructuredLogMapper.FormatTimestamp(DateTimeOffset.UtcNow),
|
||||
Level = StructuredLogLevel.Information,
|
||||
Category = "Elsa.Tests",
|
||||
EventId = 0,
|
||||
Message = "Hello",
|
||||
SourceId = "source-a"
|
||||
};
|
||||
|
||||
using var reader = CreateReader(record, useNullJsonPayloads: true);
|
||||
Assert.True(reader.Read());
|
||||
var mapped = _mapper.Map(reader);
|
||||
|
||||
Assert.Null(mapped.Exception);
|
||||
Assert.Empty(mapped.Scopes);
|
||||
Assert.Empty(mapped.Properties);
|
||||
Assert.Null(mapped.EventName);
|
||||
Assert.Null(mapped.MessageTemplate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTimestamp_StoresUtcIso8601Text()
|
||||
{
|
||||
var timestamp = new DateTimeOffset(2026, 5, 13, 15, 0, 0, TimeSpan.FromHours(2));
|
||||
|
||||
var formatted = RelationalStructuredLogMapper.FormatTimestamp(timestamp);
|
||||
|
||||
Assert.Equal("2026-05-13T13:00:00.0000000+00:00", formatted);
|
||||
Assert.Equal(timestamp.ToUniversalTime(), RelationalStructuredLogMapper.ParseTimestamp(formatted));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_FromReader_TreatsWhitespaceJsonAsEmptyValues()
|
||||
{
|
||||
var record = new RelationalStructuredLogRecord
|
||||
{
|
||||
|
|
@ -119,7 +144,7 @@ public class RelationalStructuredLogMapperTests
|
|||
EventName = null,
|
||||
Message = "Message",
|
||||
MessageTemplate = null,
|
||||
ExceptionJson = null,
|
||||
ExceptionJson = " ",
|
||||
ScopesJson = " ",
|
||||
PropertiesJson = "",
|
||||
TraceId = null,
|
||||
|
|
@ -149,7 +174,7 @@ public class RelationalStructuredLogMapperTests
|
|||
Assert.Null(mapped.WorkflowInstanceId);
|
||||
}
|
||||
|
||||
private static DbDataReader CreateReader(RelationalStructuredLogRecord record)
|
||||
private static DbDataReader CreateReader(RelationalStructuredLogRecord record, bool useNullJsonPayloads = false)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("Id", typeof(string));
|
||||
|
|
@ -184,9 +209,9 @@ public class RelationalStructuredLogMapperTests
|
|||
record.EventName ?? (object)DBNull.Value,
|
||||
record.Message,
|
||||
record.MessageTemplate ?? (object)DBNull.Value,
|
||||
record.ExceptionJson ?? (object)DBNull.Value,
|
||||
record.ScopesJson ?? (object)DBNull.Value,
|
||||
record.PropertiesJson ?? (object)DBNull.Value,
|
||||
JsonValue(record.ExceptionJson, useNullJsonPayloads),
|
||||
JsonValue(record.ScopesJson, useNullJsonPayloads),
|
||||
JsonValue(record.PropertiesJson, useNullJsonPayloads),
|
||||
record.TraceId ?? (object)DBNull.Value,
|
||||
record.SpanId ?? (object)DBNull.Value,
|
||||
record.CorrelationId ?? (object)DBNull.Value,
|
||||
|
|
@ -198,6 +223,11 @@ public class RelationalStructuredLogMapperTests
|
|||
return new DisposingDataReader(table, table.CreateDataReader());
|
||||
}
|
||||
|
||||
private static object JsonValue(string? value, bool useNullJsonPayloads)
|
||||
{
|
||||
return useNullJsonPayloads ? DBNull.Value : value ?? (object)DBNull.Value;
|
||||
}
|
||||
|
||||
private sealed class DisposingDataReader(DataTable table, DataTableReader reader) : DbDataReader
|
||||
{
|
||||
public override object this[int ordinal] => reader[ordinal];
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ public class RelationalStructuredLogSqlBuilderTests
|
|||
SourceId = "source-a",
|
||||
WorkflowDefinitionId = "definition-a",
|
||||
WorkflowInstanceId = "instance-a",
|
||||
TenantId = "tenant-a",
|
||||
CorrelationId = "correlation-a",
|
||||
TraceId = "trace-a",
|
||||
SpanId = "span-a",
|
||||
Text = "needle",
|
||||
From = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
To = DateTimeOffset.UtcNow,
|
||||
Take = 42
|
||||
|
|
@ -42,44 +45,30 @@ public class RelationalStructuredLogSqlBuilderTests
|
|||
Assert.Contains("[SourceId] = @SourceId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[WorkflowDefinitionId] = @WorkflowDefinitionId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[WorkflowInstanceId] = @WorkflowInstanceId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[TenantId] = @TenantId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[CorrelationId] = @CorrelationId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[TraceId] = @TraceId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[SpanId] = @SpanId", query.Sql, StringComparison.Ordinal);
|
||||
foreach (var textColumn in new[] { "Message", "MessageTemplate", "Category", "ExceptionJson", "ScopesJson", "PropertiesJson" })
|
||||
Assert.Contains($"[{textColumn}] LIKE @Text", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[Timestamp] >= @TimestampFrom", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[Timestamp] <= @TimestampTo", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("FETCH 42", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal("Elsa.Workflow%", query.Parameters["Category"]);
|
||||
Assert.Equal("%needle%", query.Parameters["Text"]);
|
||||
Assert.Contains("TimestampFrom", query.Parameters.Keys);
|
||||
Assert.Contains("TimestampTo", query.Parameters.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildQuery_AddsTextPredicateAcrossSearchableColumns()
|
||||
{
|
||||
var query = _builder.BuildQuery(new()
|
||||
{
|
||||
Text = "failure",
|
||||
TenantId = "tenant-a",
|
||||
SpanId = "span-a",
|
||||
Take = 25
|
||||
});
|
||||
|
||||
const string expectedTextPredicate = "([Message] LIKE @Text OR [MessageTemplate] LIKE @Text OR [Category] LIKE @Text OR [ExceptionJson] LIKE @Text OR [ScopesJson] LIKE @Text OR [PropertiesJson] LIKE @Text)";
|
||||
|
||||
Assert.Contains(expectedTextPredicate, query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[TenantId] = @TenantId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[SpanId] = @SpanId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal("%failure%", query.Parameters["Text"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "FETCH 100")]
|
||||
[InlineData(-5, "FETCH 0")]
|
||||
[InlineData(-1, "FETCH 0")]
|
||||
[InlineData(2000, "FETCH 1000")]
|
||||
[InlineData(5000, "FETCH 1000")]
|
||||
public void BuildQuery_ClampsTakeToSupportedRange(int take, string expectedLimit)
|
||||
public void BuildQuery_ClampsTakeToSupportedRange(int? take, string expectedLimit)
|
||||
{
|
||||
var query = _builder.BuildQuery(new()
|
||||
{
|
||||
Take = take
|
||||
});
|
||||
var query = _builder.BuildQuery(new() { Take = take });
|
||||
|
||||
Assert.Contains(expectedLimit, query.Sql, StringComparison.Ordinal);
|
||||
}
|
||||
|
|
@ -101,6 +90,25 @@ public class RelationalStructuredLogSqlBuilderTests
|
|||
Assert.Equal("2026-05-13T13:00:00.0000000+00:00", query.Parameters["Cutoff"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildQuery_AddsTextPredicateAcrossSearchableColumns()
|
||||
{
|
||||
var query = _builder.BuildQuery(new()
|
||||
{
|
||||
Text = "failure",
|
||||
TenantId = "tenant-a",
|
||||
SpanId = "span-a",
|
||||
Take = 25
|
||||
});
|
||||
|
||||
const string expectedTextPredicate = "([Message] LIKE @Text OR [MessageTemplate] LIKE @Text OR [Category] LIKE @Text OR [ExceptionJson] LIKE @Text OR [ScopesJson] LIKE @Text OR [PropertiesJson] LIKE @Text)";
|
||||
|
||||
Assert.Contains(expectedTextPredicate, query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[TenantId] = @TenantId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("[SpanId] = @SpanId", query.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal("%failure%", query.Parameters["Text"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDeleteRowsBeyondMax_DelegatesOffsetSyntaxToDialect()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue