From 1e349fb738dda4ddcfcd728fcc8a6cfeeb14d445 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 20 Nov 2025 14:19:11 -0600 Subject: [PATCH 01/10] feat: add semantic logging support for Akka.NET 1.5.56+ Enhances SerilogLogger to leverage Akka.NET's new semantic logging APIs introduced in version 1.5.56. This change enables structured logging with proper message template and parameter extraction. Changes: - Modified SerilogLogger.GetFormat() and GetArgs() to unwrap SerilogPayload and use semantic logging pattern (checks for LogMessage and extracts template) - Serilog now receives proper message templates instead of formatted strings - Added comprehensive test suite (SemanticLoggingSpecs.cs) with 9 tests covering: * Named and positional template properties * Multiple properties handling * Destructuring operator (@) support * Stringification operator ($) support * Format specifiers handling * ForContext integration * Akka metadata preservation All existing tests pass (765 tests). Backwards compatible with earlier Akka.NET versions through LogMessage type checking. Depends on: Akka.NET >= 1.5.56 --- .../SemanticLoggingSpecs.cs | 220 ++++++++++++++++++ src/Akka.Logger.Serilog/SerilogLogger.cs | 32 +-- 2 files changed, 239 insertions(+), 13 deletions(-) create mode 100644 src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs diff --git a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs new file mode 100644 index 0000000..fed0a60 --- /dev/null +++ b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Akka.Event; +using FluentAssertions; +using Serilog; +using Serilog.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Logger.Serilog.Tests +{ + /// + /// Tests for semantic logging functionality added in Akka.NET 1.5.56. + /// Verifies that structured properties from log message templates are + /// accessible in Serilog's log events. + /// + public class SemanticLoggingSpecs : TestKit.Xunit2.TestKit + { + public static readonly Config Config = +@"akka.loglevel = DEBUG +akka.loggers=[""Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog""] +akka.logger-formatter=""Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog"""; + + private readonly TestSink _sink; + private readonly ILoggingAdapter _loggingAdapter; + + public SemanticLoggingSpecs(ITestOutputHelper helper) : base(Config, output: helper) + { + _sink = new TestSink(helper); + + global::Serilog.Log.Logger = new LoggerConfiguration() + .WriteTo.Sink(_sink) + .MinimumLevel.Debug() + .CreateLogger(); + + var logSource = Sys.Name; + var logClass = typeof(ActorSystem); + + _loggingAdapter = new SerilogLoggingAdapter(Sys.EventStream, logSource, logClass); + } + + [Fact(DisplayName = "Should extract named template properties for Serilog")] + public async Task NamedTemplatePropertiesTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("User {UserId} with email {Email} logged in", 12345, "user@example.com"); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + logEvent.Properties.Should().ContainKey("UserId"); + logEvent.Properties.Should().ContainKey("Email"); + logEvent.Properties["UserId"].ToString().Should().Be("12345"); + logEvent.Properties["Email"].ToString().Should().Be("\"user@example.com\""); + } + + [Fact(DisplayName = "Should extract positional template properties for Serilog")] + public async Task PositionalTemplatePropertiesTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("User {0} logged in from {1}", "Bob", "192.168.1.1"); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + logEvent.Properties.Should().ContainKey("0"); + logEvent.Properties.Should().ContainKey("1"); + logEvent.Properties["0"].ToString().Should().Be("\"Bob\""); + logEvent.Properties["1"].ToString().Should().Be("\"192.168.1.1\""); + } + + [Fact(DisplayName = "Should handle multiple named properties in template")] + public async Task MultipleNamedPropertiesTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("Order {OrderId} for customer {CustomerId}: {Amount} {Currency}", + "ORD-001", "CUST-456", 99.99, "USD"); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + logEvent.Properties.Should().ContainKeys("OrderId", "CustomerId", "Amount", "Currency"); + logEvent.Properties["OrderId"].ToString().Should().Be("\"ORD-001\""); + logEvent.Properties["CustomerId"].ToString().Should().Be("\"CUST-456\""); + logEvent.Properties["Amount"].ToString().Should().Be("99.99"); + logEvent.Properties["Currency"].ToString().Should().Be("\"USD\""); + } + + [Fact(DisplayName = "Should preserve Akka metadata properties alongside semantic logging properties")] + public async Task AkkaMetadataAndSemanticPropertiesTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("User {UserId} action", 999); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + + // Semantic property + logEvent.Properties.Should().ContainKey("UserId"); + logEvent.Properties["UserId"].ToString().Should().Be("999"); + + // Akka metadata properties + logEvent.Properties.Should().ContainKey("ActorPath"); + logEvent.Properties.Should().ContainKey("LogSource"); + logEvent.Properties.Should().ContainKey("Thread"); + } + + [Fact(DisplayName = "Should handle Serilog destructuring operator")] + public async Task DestructuringOperatorTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + var user = new { Name = "Alice", Age = 30, Role = "Admin" }; + _loggingAdapter.Info("Processing user {@User}", user); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + + // Property name is "User" (@ operator removed by Akka's parser) + logEvent.Properties.Should().ContainKey("User"); + + // Serilog should have destructured the object + var userProperty = logEvent.Properties["User"]; + userProperty.Should().BeOfType(); + + var structure = (StructureValue)userProperty; + structure.Properties.Should().Contain(p => p.Name == "Name"); + structure.Properties.Should().Contain(p => p.Name == "Age"); + structure.Properties.Should().Contain(p => p.Name == "Role"); + } + + [Fact(DisplayName = "Should handle Serilog stringification operator")] + public async Task StringificationOperatorTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + var exception = new InvalidOperationException("Test error"); + _loggingAdapter.Info("Error occurred: {$Exception}", exception); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + + // Property name is "Exception" ($ operator removed by Akka's parser) + logEvent.Properties.Should().ContainKey("Exception"); + + // Serilog should have used ToString() instead of destructuring + var exceptionProperty = logEvent.Properties["Exception"]; + exceptionProperty.Should().BeOfType(); + } + + [Fact(DisplayName = "Should handle format specifiers in named templates")] + public async Task FormatSpecifiersInTemplatesTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("Total amount: {Amount:N2}", 1234.5678); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + + // Property name is "Amount" (format specifier removed by Akka's parser) + logEvent.Properties.Should().ContainKey("Amount"); + + // The rendered message should apply the format + logEvent.RenderMessage().Should().Contain("1,234.57"); + } + + [Fact(DisplayName = "Should handle empty/no properties gracefully")] + public async Task NoPropertiesTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("No template properties here"); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + + // Should still have Akka metadata properties + logEvent.Properties.Should().ContainKey("ActorPath"); + logEvent.Properties.Should().ContainKey("LogSource"); + logEvent.Properties.Should().ContainKey("Thread"); + + // Message content should be preserved + logEvent.RenderMessage().Should().Contain("No template properties here"); + } + + [Fact(DisplayName = "Should work with ForContext enrichment")] + public async Task ForContextWithSemanticLoggingTest() + { + _sink.Clear(); + await AwaitConditionAsync(() => _sink.Writes.Count == 0); + + var contextLogger = _loggingAdapter.ForContext("TenantId", "TENANT-123"); + contextLogger.Info("User {UserId} performed action", 456); + await AwaitConditionAsync(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + + // Should have both semantic property and context enrichment + logEvent.Properties.Should().ContainKey("UserId"); + logEvent.Properties["UserId"].ToString().Should().Be("456"); + + logEvent.Properties.Should().ContainKey("TenantId"); + logEvent.Properties["TenantId"].ToString().Should().Be("\"TENANT-123\""); + } + } +} diff --git a/src/Akka.Logger.Serilog/SerilogLogger.cs b/src/Akka.Logger.Serilog/SerilogLogger.cs index 006cd7a..01bc6d4 100644 --- a/src/Akka.Logger.Serilog/SerilogLogger.cs +++ b/src/Akka.Logger.Serilog/SerilogLogger.cs @@ -27,24 +27,30 @@ public class SerilogLogger : ReceiveActor, IRequiresMessageQueue a is not PropertyEnricher).ToArray() + + // Use semantic logging pattern: extract parameters and filter PropertyEnricher objects + var parameters = message is LogMessage logMessage + ? logMessage.Parameters() : new[] { message }; + + return parameters.Where(a => a is not PropertyEnricher).ToArray(); } private static ILogger GetLogger(LogEvent logEvent) { @@ -75,21 +81,21 @@ private static ILogger GetLogger(LogEvent logEvent) { } private static void Handle(Error logEvent) { - GetLogger(logEvent).Error(logEvent.Cause, GetFormat(logEvent.Message), GetArgs(logEvent.Message)); + GetLogger(logEvent).Error(logEvent.Cause, GetFormat(logEvent), GetArgs(logEvent)); } private static void Handle(Warning logEvent) { - GetLogger(logEvent).Warning(logEvent.Cause, GetFormat(logEvent.Message), GetArgs(logEvent.Message)); + GetLogger(logEvent).Warning(logEvent.Cause, GetFormat(logEvent), GetArgs(logEvent)); } private static void Handle(Info logEvent) { - GetLogger(logEvent).Information(logEvent.Cause, GetFormat(logEvent.Message), GetArgs(logEvent.Message)); + GetLogger(logEvent).Information(logEvent.Cause, GetFormat(logEvent), GetArgs(logEvent)); } private static void Handle(Debug logEvent) { - GetLogger(logEvent).Debug(logEvent.Cause, GetFormat(logEvent.Message), GetArgs(logEvent.Message)); + GetLogger(logEvent).Debug(logEvent.Cause, GetFormat(logEvent), GetArgs(logEvent)); } /// From d700c22be42d66be4e77a37532bd7bd05457c5da Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 20 Nov 2025 14:40:46 -0600 Subject: [PATCH 02/10] build: update Akka.NET version to 1.5.56 and enable test discovery - Updated Akka.NET dependency from 1.5.25 to 1.5.56 - Commented out OutputType and StartupObject in test project to enable test discovery - Added xunit.runner.visualstudio package for better IDE integration - Required for semantic logging API support --- .../Akka.Logger.Serilog.Tests.csproj | 11 ++++++++--- src/Directory.Build.props | 2 +- src/Directory.Packages.props | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj b/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj index 872ed7b..cdc85cf 100644 --- a/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj +++ b/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj @@ -1,13 +1,18 @@ - + $(NetFrameworkTestVersion);$(NetCoreTestVersion) - Exe - Akka.Logger.Serilog.Tests.Generator.Program + + false + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6f060b9..611dca1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -22,7 +22,7 @@ And it will work without having to explicitly call `Context.GetLogger<Serilog 10 - 1.5.25 + 1.5.56 1.5.24 net8.0 net471 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8d831dd..02a578b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -15,6 +15,7 @@ + From cbea486b5da76f5f03acb50284369988e9cd7cec Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 3 Dec 2025 18:21:29 +0700 Subject: [PATCH 03/10] Use xunit.runner.json file --- .../Akka.Logger.Serilog.Tests.csproj | 3 +++ src/Akka.Logger.Serilog.Tests/TestSink.cs | 2 -- src/Akka.Logger.Serilog.Tests/xunit.runner.json | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 src/Akka.Logger.Serilog.Tests/xunit.runner.json diff --git a/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj b/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj index cdc85cf..22646c6 100644 --- a/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj +++ b/src/Akka.Logger.Serilog.Tests/Akka.Logger.Serilog.Tests.csproj @@ -24,6 +24,9 @@ Always + + PreserveNewest + diff --git a/src/Akka.Logger.Serilog.Tests/TestSink.cs b/src/Akka.Logger.Serilog.Tests/TestSink.cs index e87e869..f16a4a2 100644 --- a/src/Akka.Logger.Serilog.Tests/TestSink.cs +++ b/src/Akka.Logger.Serilog.Tests/TestSink.cs @@ -4,8 +4,6 @@ using Xunit; using Xunit.Abstractions; -[assembly: CollectionBehavior(DisableTestParallelization = true)] - namespace Akka.Logger.Serilog.Tests { /// diff --git a/src/Akka.Logger.Serilog.Tests/xunit.runner.json b/src/Akka.Logger.Serilog.Tests/xunit.runner.json new file mode 100644 index 0000000..4a73b1e --- /dev/null +++ b/src/Akka.Logger.Serilog.Tests/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://xunit.github.io/schema/current/xunit.runner.schema.json", + "longRunningTestSeconds": 60, + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} \ No newline at end of file From dbbd19dc4595b4db87a20873726c0208473cd253 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 3 Dec 2025 21:46:19 +0700 Subject: [PATCH 04/10] Make sure logger has started --- src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs | 7 +++++++ src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs index 0947bf8..ac3b2d1 100644 --- a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs @@ -3,6 +3,7 @@ using Akka.Configuration; using Akka.Event; using FluentAssertions; +using FluentAssertions.Extensions; using Serilog; using Serilog.Core.Enrichers; using Serilog.Events; @@ -28,6 +29,12 @@ public LogMessageSpecs(ITestOutputHelper helper) : base(Config, output: helper) .MinimumLevel.Debug() .CreateLogger(); _loggingAdapter = Sys.Log; + + AwaitCondition(() => + { + _loggingAdapter.Warning("hi"); + return _sink.Writes.Count > 0; + }, 3.Seconds(), 200.Milliseconds()); } [Fact] diff --git a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs index fed0a60..21f6ac6 100644 --- a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs @@ -41,6 +41,12 @@ public SemanticLoggingSpecs(ITestOutputHelper helper) : base(Config, output: hel var logClass = typeof(ActorSystem); _loggingAdapter = new SerilogLoggingAdapter(Sys.EventStream, logSource, logClass); + + AwaitCondition(() => + { + _loggingAdapter.Warning("hi"); + return _sink.Writes.Count > 0; + }, TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(200)); } [Fact(DisplayName = "Should extract named template properties for Serilog")] From 5834b79a4133e2147cc44e506b6637b1583bb044 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 3 Dec 2025 22:20:36 +0700 Subject: [PATCH 05/10] Make TestSink thread-safe --- src/Akka.Logger.Serilog.Tests/TestSink.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/TestSink.cs b/src/Akka.Logger.Serilog.Tests/TestSink.cs index f16a4a2..b81caf4 100644 --- a/src/Akka.Logger.Serilog.Tests/TestSink.cs +++ b/src/Akka.Logger.Serilog.Tests/TestSink.cs @@ -12,7 +12,7 @@ namespace Akka.Logger.Serilog.Tests /// public sealed class TestSink : ILogEventSink { - public ConcurrentQueue Writes { get; private set; } = new ConcurrentQueue(); + public ConcurrentQueue Writes { get; } = new (); private readonly ITestOutputHelper _output; private int _count; @@ -31,10 +31,11 @@ public TestSink(ITestOutputHelper output) /// public void Clear() { - Writes = new ConcurrentQueue(); + while (Writes.TryDequeue(out _)) + { } } - public void Emit(global::Serilog.Events.LogEvent logEvent) + public void Emit(LogEvent logEvent) { _count++; _output?.WriteLine($"[{nameof(TestSink)}][{_count}]: {logEvent.RenderMessage()}"); From 8b734ab2a72412093a21a5c3e3499f1dd818b6e7 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 3 Dec 2025 22:57:07 +0700 Subject: [PATCH 06/10] Prevent test cross-pollination problem --- .../LogMessageSpecs.cs | 100 ++++++++++-------- .../SemanticLoggingSpecs.cs | 71 +++++++------ 2 files changed, 97 insertions(+), 74 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs index ac3b2d1..1462425 100644 --- a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs @@ -1,9 +1,9 @@ using System; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Event; using FluentAssertions; -using FluentAssertions.Extensions; using Serilog; using Serilog.Core.Enrichers; using Serilog.Events; @@ -12,41 +12,53 @@ namespace Akka.Logger.Serilog.Tests { - public class LogMessageSpecs : TestKit.Xunit2.TestKit + public class LogMessageSpecs: IAsyncLifetime { public static readonly Config Config = @"akka.loglevel = DEBUG akka.loggers=[""Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog""]"; - private readonly ILoggingAdapter _loggingAdapter; + private readonly ITestOutputHelper _helper; private readonly TestSink _sink; + + private TestKit.Xunit2.TestKit _testKit; + private ILoggingAdapter _loggingAdapter; - public LogMessageSpecs(ITestOutputHelper helper) : base(Config, output: helper) + public LogMessageSpecs(ITestOutputHelper helper) { + _helper = helper; _sink = new TestSink(helper); global::Serilog.Log.Logger = new LoggerConfiguration() .WriteTo.Sink(_sink) .MinimumLevel.Debug() .CreateLogger(); - _loggingAdapter = Sys.Log; + } + + public Task InitializeAsync() + { + var sys = ActorSystem.Create("TestActorSystem", Config); + _testKit = new TestKit.Xunit2.TestKit(sys, _helper); + _loggingAdapter = sys.Log; - AwaitCondition(() => - { - _loggingAdapter.Warning("hi"); - return _sink.Writes.Count > 0; - }, 3.Seconds(), 200.Milliseconds()); + return Task.CompletedTask; } + public Task DisposeAsync() + { + _testKit.Shutdown(); + return Task.CompletedTask; + } + [Fact] public void ShouldLogDebugLevelMessage() { var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -59,14 +71,14 @@ public void ShouldLogMessageWithPropertyEnrichers() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("Hi {0}", "Harry Potter", new PropertyEnricher("Address", "No. 4 Privet Drive"), new PropertyEnricher("Town", "Little Whinging"), new PropertyEnricher("County", "Surrey"), new PropertyEnricher("Country", "England")); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -84,10 +96,10 @@ public void ShouldLogDebugLevelMessageWithArgs() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -100,11 +112,11 @@ public void ShouldLogDebugLevelMessageWithException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Debug(exception, "hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -117,11 +129,11 @@ public void ShouldLogDebugLevelMessageWithArgsAndException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Debug(exception, "hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -134,10 +146,10 @@ public void ShouldLogInfoLevelMessage() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Info("hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -150,10 +162,10 @@ public void ShouldLogInfoLevelMessageWithArgs() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Info("hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -166,11 +178,11 @@ public void ShouldLogInfoLevelMessageWithException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Info(exception, "hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -183,11 +195,11 @@ public void ShouldLogInfoLevelMessageWithArgsAndException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Info(exception, "hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -200,10 +212,10 @@ public void ShouldLogWarningLevelMessage() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Warning("hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -216,10 +228,10 @@ public void ShouldLogWarningLevelMessageWithArgs() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Warning("hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -232,11 +244,11 @@ public void ShouldLogWarningLevelMessageWithException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Warning(exception, "hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -249,11 +261,11 @@ public void ShouldLogWarningLevelMessageWithArgsAndException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Warning(exception, "hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -266,10 +278,10 @@ public void ShouldLogErrorLevelMessage() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Error("hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); @@ -282,10 +294,10 @@ public void ShouldLogErrorLevelMessageWithArgs() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Error("hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); @@ -298,11 +310,11 @@ public void ShouldLogErrorLevelMessageWithException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Error(exception, "hi"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); @@ -315,11 +327,11 @@ public void ShouldLogErrorLevelMessageWithArgsAndException() var context = _loggingAdapter; _sink.Clear(); - AwaitCondition(() => _sink.Writes.Count == 0); + _testKit.AwaitCondition(() => _sink.Writes.Count == 0); var exception = new Exception("BOOM!!!"); context.Error(exception, "hi {0}", "test"); - AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); diff --git a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs index 21f6ac6..67156f6 100644 --- a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; @@ -18,45 +16,58 @@ namespace Akka.Logger.Serilog.Tests /// Verifies that structured properties from log message templates are /// accessible in Serilog's log events. /// - public class SemanticLoggingSpecs : TestKit.Xunit2.TestKit + public class SemanticLoggingSpecs : IAsyncLifetime { public static readonly Config Config = @"akka.loglevel = DEBUG akka.loggers=[""Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog""] akka.logger-formatter=""Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog"""; + private readonly ITestOutputHelper _helper; private readonly TestSink _sink; - private readonly ILoggingAdapter _loggingAdapter; + + private TestKit.Xunit2.TestKit _testKit; + private ILoggingAdapter _loggingAdapter; - public SemanticLoggingSpecs(ITestOutputHelper helper) : base(Config, output: helper) + public SemanticLoggingSpecs(ITestOutputHelper helper) { + _helper = helper; _sink = new TestSink(helper); global::Serilog.Log.Logger = new LoggerConfiguration() .WriteTo.Sink(_sink) .MinimumLevel.Debug() .CreateLogger(); + } - var logSource = Sys.Name; + public Task InitializeAsync() + { + var sys = ActorSystem.Create("TestActorSystem", Config); + _testKit = new TestKit.Xunit2.TestKit(sys, _helper); + + var logSource = sys.Name; var logClass = typeof(ActorSystem); - _loggingAdapter = new SerilogLoggingAdapter(Sys.EventStream, logSource, logClass); + _loggingAdapter = new SerilogLoggingAdapter(sys.EventStream, logSource, logClass); + _loggingAdapter = sys.Log; - AwaitCondition(() => - { - _loggingAdapter.Warning("hi"); - return _sink.Writes.Count > 0; - }, TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(200)); + return Task.CompletedTask; } + public Task DisposeAsync() + { + _testKit.Shutdown(); + return Task.CompletedTask; + } + [Fact(DisplayName = "Should extract named template properties for Serilog")] public async Task NamedTemplatePropertiesTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("User {UserId} with email {Email} logged in", 12345, "user@example.com"); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Properties.Should().ContainKey("UserId"); @@ -69,10 +80,10 @@ public async Task NamedTemplatePropertiesTest() public async Task PositionalTemplatePropertiesTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("User {0} logged in from {1}", "Bob", "192.168.1.1"); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Properties.Should().ContainKey("0"); @@ -85,11 +96,11 @@ public async Task PositionalTemplatePropertiesTest() public async Task MultipleNamedPropertiesTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("Order {OrderId} for customer {CustomerId}: {Amount} {Currency}", "ORD-001", "CUST-456", 99.99, "USD"); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Properties.Should().ContainKeys("OrderId", "CustomerId", "Amount", "Currency"); @@ -103,10 +114,10 @@ public async Task MultipleNamedPropertiesTest() public async Task AkkaMetadataAndSemanticPropertiesTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("User {UserId} action", 999); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); @@ -124,11 +135,11 @@ public async Task AkkaMetadataAndSemanticPropertiesTest() public async Task DestructuringOperatorTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); var user = new { Name = "Alice", Age = 30, Role = "Admin" }; _loggingAdapter.Info("Processing user {@User}", user); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); @@ -149,11 +160,11 @@ public async Task DestructuringOperatorTest() public async Task StringificationOperatorTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); var exception = new InvalidOperationException("Test error"); _loggingAdapter.Info("Error occurred: {$Exception}", exception); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); @@ -169,10 +180,10 @@ public async Task StringificationOperatorTest() public async Task FormatSpecifiersInTemplatesTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("Total amount: {Amount:N2}", 1234.5678); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); @@ -187,10 +198,10 @@ public async Task FormatSpecifiersInTemplatesTest() public async Task NoPropertiesTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("No template properties here"); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); @@ -207,11 +218,11 @@ public async Task NoPropertiesTest() public async Task ForContextWithSemanticLoggingTest() { _sink.Clear(); - await AwaitConditionAsync(() => _sink.Writes.Count == 0); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); var contextLogger = _loggingAdapter.ForContext("TenantId", "TENANT-123"); contextLogger.Info("User {UserId} performed action", 456); - await AwaitConditionAsync(() => _sink.Writes.Count == 1); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); From d347203b70de9e03087b2217b23467c824c63eff Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 3 Dec 2025 23:06:35 +0700 Subject: [PATCH 07/10] make doubly sure that ActorSystem has shut down --- src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs | 11 ++++++----- .../SemanticLoggingSpecs.cs | 14 +++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs index 1462425..70628b8 100644 --- a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs @@ -20,6 +20,7 @@ public class LogMessageSpecs: IAsyncLifetime private readonly ITestOutputHelper _helper; private readonly TestSink _sink; + private ActorSystem _sys; private TestKit.Xunit2.TestKit _testKit; private ILoggingAdapter _loggingAdapter; @@ -36,17 +37,17 @@ public LogMessageSpecs(ITestOutputHelper helper) public Task InitializeAsync() { - var sys = ActorSystem.Create("TestActorSystem", Config); - _testKit = new TestKit.Xunit2.TestKit(sys, _helper); - _loggingAdapter = sys.Log; + _sys = ActorSystem.Create("TestActorSystem", Config); + _testKit = new TestKit.Xunit2.TestKit(_sys, _helper); + _loggingAdapter = _sys.Log; return Task.CompletedTask; } - public Task DisposeAsync() + public async Task DisposeAsync() { _testKit.Shutdown(); - return Task.CompletedTask; + await _sys.Terminate(); } [Fact] diff --git a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs index 67156f6..128279c 100644 --- a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs @@ -26,6 +26,7 @@ public class SemanticLoggingSpecs : IAsyncLifetime private readonly ITestOutputHelper _helper; private readonly TestSink _sink; + private ActorSystem _sys; private TestKit.Xunit2.TestKit _testKit; private ILoggingAdapter _loggingAdapter; @@ -42,22 +43,21 @@ public SemanticLoggingSpecs(ITestOutputHelper helper) public Task InitializeAsync() { - var sys = ActorSystem.Create("TestActorSystem", Config); - _testKit = new TestKit.Xunit2.TestKit(sys, _helper); + _sys = ActorSystem.Create("TestActorSystem", Config); + _testKit = new TestKit.Xunit2.TestKit(_sys, _helper); - var logSource = sys.Name; + var logSource = _sys.Name; var logClass = typeof(ActorSystem); - _loggingAdapter = new SerilogLoggingAdapter(sys.EventStream, logSource, logClass); - _loggingAdapter = sys.Log; + _loggingAdapter = new SerilogLoggingAdapter(_sys.EventStream, logSource, logClass); return Task.CompletedTask; } - public Task DisposeAsync() + public async Task DisposeAsync() { _testKit.Shutdown(); - return Task.CompletedTask; + await _sys.Terminate(); } [Fact(DisplayName = "Should extract named template properties for Serilog")] From 2e122dcfeaaa72f4e12d33b6f84106d0324b3b74 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 3 Dec 2025 23:37:10 +0700 Subject: [PATCH 08/10] Harden LogMessageSpecs --- .../LogMessageSpecs.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs index 70628b8..b98f7a3 100644 --- a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs @@ -59,7 +59,7 @@ public void ShouldLogDebugLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -79,7 +79,7 @@ public void ShouldLogMessageWithPropertyEnrichers() new PropertyEnricher("Town", "Little Whinging"), new PropertyEnricher("County", "Surrey"), new PropertyEnricher("Country", "England")); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -100,7 +100,7 @@ public void ShouldLogDebugLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -117,7 +117,7 @@ public void ShouldLogDebugLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Debug(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -134,7 +134,7 @@ public void ShouldLogDebugLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Debug(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Debug); @@ -150,7 +150,7 @@ public void ShouldLogInfoLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Info("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -166,7 +166,7 @@ public void ShouldLogInfoLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Info("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -183,7 +183,7 @@ public void ShouldLogInfoLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Info(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -200,7 +200,7 @@ public void ShouldLogInfoLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Info(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Information); @@ -216,7 +216,7 @@ public void ShouldLogWarningLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Warning("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -232,7 +232,7 @@ public void ShouldLogWarningLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Warning("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -249,7 +249,7 @@ public void ShouldLogWarningLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Warning(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -266,7 +266,7 @@ public void ShouldLogWarningLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Warning(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Warning); @@ -282,7 +282,7 @@ public void ShouldLogErrorLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Error("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); @@ -298,7 +298,7 @@ public void ShouldLogErrorLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Error("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); @@ -315,7 +315,7 @@ public void ShouldLogErrorLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Error(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); @@ -332,7 +332,7 @@ public void ShouldLogErrorLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Error(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count == 1); + _testKit.AwaitCondition(() => _sink.Writes.Count > 0); _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); logEvent.Level.Should().Be(LogEventLevel.Error); From ac63fe7c8fab0f15423e184d7134af6bb93dce59 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 4 Dec 2025 00:28:48 +0700 Subject: [PATCH 09/10] Harden LogMessageSpecs --- .../LogMessageSpecs.cs | 182 ++++++++++-------- 1 file changed, 106 insertions(+), 76 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs index b98f7a3..fcc7f55 100644 --- a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs @@ -9,12 +9,13 @@ using Serilog.Events; using Xunit; using Xunit.Abstractions; +using LogEvent = Serilog.Events.LogEvent; namespace Akka.Logger.Serilog.Tests { public class LogMessageSpecs: IAsyncLifetime { - public static readonly Config Config = @"akka.loglevel = DEBUG + private static readonly Config Config = @"akka.loglevel = DEBUG akka.loggers=[""Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog""]"; private readonly ITestOutputHelper _helper; @@ -29,7 +30,7 @@ public LogMessageSpecs(ITestOutputHelper helper) _helper = helper; _sink = new TestSink(helper); - global::Serilog.Log.Logger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .WriteTo.Sink(_sink) .MinimumLevel.Debug() .CreateLogger(); @@ -59,11 +60,11 @@ public void ShouldLogDebugLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Debug); - logEvent.RenderMessage().Should().Contain("hi"); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Debug + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -79,16 +80,27 @@ public void ShouldLogMessageWithPropertyEnrichers() new PropertyEnricher("Town", "Little Whinging"), new PropertyEnricher("County", "Surrey"), new PropertyEnricher("Country", "England")); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Debug); - logEvent.RenderMessage().Should().Contain("Hi \"Harry Potter\""); - logEvent.Properties.Should().ContainKeys("Address", "Town", "County", "Country"); - logEvent.Properties["Address"].ToString().Should().Be("\"No. 4 Privet Drive\""); - logEvent.Properties["Town"].ToString().Should().Be("\"Little Whinging\""); - logEvent.Properties["County"].ToString().Should().Be("\"Surrey\""); - logEvent.Properties["Country"].ToString().Should().Be("\"England\""); + + _testKit.AwaitCondition(() => + AssertCondition(logEvent => + { + try + { + logEvent.Level.Should().Be(LogEventLevel.Debug); + logEvent.RenderMessage().Should().Contain("Hi \"Harry Potter\""); + logEvent.Properties.Should().ContainKeys("Address", "Town", "County", "Country"); + logEvent.Properties["Address"].ToString().Should().Be("\"No. 4 Privet Drive\""); + logEvent.Properties["Town"].ToString().Should().Be("\"Little Whinging\""); + logEvent.Properties["County"].ToString().Should().Be("\"Surrey\""); + logEvent.Properties["Country"].ToString().Should().Be("\"England\""); + return true; + } + catch + { + return false; + } + }) + ); } [Fact] @@ -100,11 +112,11 @@ public void ShouldLogDebugLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Debug("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Debug); - logEvent.RenderMessage().Should().Contain("hi \"test\""); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Debug + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -117,11 +129,12 @@ public void ShouldLogDebugLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Debug(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Debug); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Debug + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -134,11 +147,12 @@ public void ShouldLogDebugLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Debug(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Debug); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Debug + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -150,11 +164,11 @@ public void ShouldLogInfoLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Info("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Information); - logEvent.RenderMessage().Should().Contain("hi"); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Information + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -166,11 +180,11 @@ public void ShouldLogInfoLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Info("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Information); - logEvent.RenderMessage().Should().Contain("hi \"test\""); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Information + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -183,11 +197,12 @@ public void ShouldLogInfoLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Info(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Information); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Information + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -200,11 +215,12 @@ public void ShouldLogInfoLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Info(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Information); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Information + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -216,11 +232,11 @@ public void ShouldLogWarningLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Warning("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Warning); - logEvent.RenderMessage().Should().Contain("hi"); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Warning + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -232,11 +248,11 @@ public void ShouldLogWarningLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Warning("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Warning); - logEvent.RenderMessage().Should().Contain("hi \"test\""); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Warning + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -249,11 +265,12 @@ public void ShouldLogWarningLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Warning(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Warning); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Warning + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -266,11 +283,12 @@ public void ShouldLogWarningLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Warning(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Warning); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Warning + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -282,11 +300,11 @@ public void ShouldLogErrorLevelMessage() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Error("hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Error); - logEvent.RenderMessage().Should().Contain("hi"); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Error + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -298,11 +316,11 @@ public void ShouldLogErrorLevelMessageWithArgs() _testKit.AwaitCondition(() => _sink.Writes.Count == 0); context.Error("hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Error); - logEvent.RenderMessage().Should().Contain("hi \"test\""); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Error + && logEvent.RenderMessage() == "hi \"test\"") + ); } [Fact] @@ -315,11 +333,12 @@ public void ShouldLogErrorLevelMessageWithException() var exception = new Exception("BOOM!!!"); context.Error(exception, "hi"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Error); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Error + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi") + ); } [Fact] @@ -332,11 +351,22 @@ public void ShouldLogErrorLevelMessageWithArgsAndException() var exception = new Exception("BOOM!!!"); context.Error(exception, "hi {0}", "test"); - _testKit.AwaitCondition(() => _sink.Writes.Count > 0); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Error); - logEvent.Exception.Should().Be(exception); + _testKit.AwaitCondition(() => + AssertCondition(logEvent => logEvent.Level == LogEventLevel.Error + && logEvent.Exception == exception + && logEvent.RenderMessage() == "hi \"test\"") + ); + } + + private bool AssertCondition(Func condition) + { + while (_sink.Writes.TryDequeue(out var logEvent)) + { + if (condition(logEvent)) + return true; + } + return false; } } } \ No newline at end of file From 3ede1ebe6029090e54b40cdd2cf502585dda73a6 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 4 Dec 2025 00:50:48 +0700 Subject: [PATCH 10/10] Harden PropertyEnricherSpec and SemanticLoggingSpecs --- .../PropertyEnricherSpec.cs | 34 +++- .../SemanticLoggingSpecs.cs | 190 ++++++++++-------- 2 files changed, 133 insertions(+), 91 deletions(-) diff --git a/src/Akka.Logger.Serilog.Tests/PropertyEnricherSpec.cs b/src/Akka.Logger.Serilog.Tests/PropertyEnricherSpec.cs index 3d09901..ee2d051 100644 --- a/src/Akka.Logger.Serilog.Tests/PropertyEnricherSpec.cs +++ b/src/Akka.Logger.Serilog.Tests/PropertyEnricherSpec.cs @@ -5,7 +5,6 @@ // // ----------------------------------------------------------------------- -using System; using Akka.Configuration; using Akka.Event; using FluentAssertions; @@ -50,17 +49,30 @@ public void ShouldLogMessageWithPropertyEnrichers() new PropertyEnricher("Town", "Little Whinging"), new PropertyEnricher("County", "Surrey"), new PropertyEnricher("Country", "England")); - AwaitCondition(() => _sink.Writes.Count == 1); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Level.Should().Be(LogEventLevel.Debug); - logEvent.RenderMessage().Should().Contain("Hi \"Harry Potter\""); - logEvent.Properties.Should().ContainKeys("Person", "Address", "Town", "County", "Country"); - logEvent.Properties["Person"].ToString().Should().Be("\"Harry Potter\""); - logEvent.Properties["Address"].ToString().Should().Be("\"No. 4 Privet Drive\""); - logEvent.Properties["Town"].ToString().Should().Be("\"Little Whinging\""); - logEvent.Properties["County"].ToString().Should().Be("\"Surrey\""); - logEvent.Properties["Country"].ToString().Should().Be("\"England\""); + AwaitCondition(() => + { + while (_sink.Writes.TryDequeue(out var logEvent)) + { + try + { + logEvent.Level.Should().Be(LogEventLevel.Debug); + logEvent.RenderMessage().Should().Contain("Hi \"Harry Potter\""); + logEvent.Properties.Should().ContainKeys("Person", "Address", "Town", "County", "Country"); + logEvent.Properties["Person"].ToString().Should().Be("\"Harry Potter\""); + logEvent.Properties["Address"].ToString().Should().Be("\"No. 4 Privet Drive\""); + logEvent.Properties["Town"].ToString().Should().Be("\"Little Whinging\""); + logEvent.Properties["County"].ToString().Should().Be("\"Surrey\""); + logEvent.Properties["Country"].ToString().Should().Be("\"England\""); + return true; + } + catch + { + // no-op + } + } + return false; + }); } } \ No newline at end of file diff --git a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs index 128279c..2ee79f0 100644 --- a/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs @@ -8,6 +8,7 @@ using Serilog.Events; using Xunit; using Xunit.Abstractions; +using LogEvent = Serilog.Events.LogEvent; namespace Akka.Logger.Serilog.Tests { @@ -35,7 +36,7 @@ public SemanticLoggingSpecs(ITestOutputHelper helper) _helper = helper; _sink = new TestSink(helper); - global::Serilog.Log.Logger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .WriteTo.Sink(_sink) .MinimumLevel.Debug() .CreateLogger(); @@ -67,13 +68,15 @@ public async Task NamedTemplatePropertiesTest() await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("User {UserId} with email {Email} logged in", 12345, "user@example.com"); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Properties.Should().ContainKey("UserId"); - logEvent.Properties.Should().ContainKey("Email"); - logEvent.Properties["UserId"].ToString().Should().Be("12345"); - logEvent.Properties["Email"].ToString().Should().Be("\"user@example.com\""); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + logEvent.Properties.Should().ContainKey("UserId"); + logEvent.Properties.Should().ContainKey("Email"); + logEvent.Properties["UserId"].ToString().Should().Be("12345"); + logEvent.Properties["Email"].ToString().Should().Be("\"user@example.com\""); + })); } [Fact(DisplayName = "Should extract positional template properties for Serilog")] @@ -83,13 +86,15 @@ public async Task PositionalTemplatePropertiesTest() await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("User {0} logged in from {1}", "Bob", "192.168.1.1"); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Properties.Should().ContainKey("0"); - logEvent.Properties.Should().ContainKey("1"); - logEvent.Properties["0"].ToString().Should().Be("\"Bob\""); - logEvent.Properties["1"].ToString().Should().Be("\"192.168.1.1\""); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + logEvent.Properties.Should().ContainKey("0"); + logEvent.Properties.Should().ContainKey("1"); + logEvent.Properties["0"].ToString().Should().Be("\"Bob\""); + logEvent.Properties["1"].ToString().Should().Be("\"192.168.1.1\""); + })); } [Fact(DisplayName = "Should handle multiple named properties in template")] @@ -100,14 +105,16 @@ public async Task MultipleNamedPropertiesTest() _loggingAdapter.Info("Order {OrderId} for customer {CustomerId}: {Amount} {Currency}", "ORD-001", "CUST-456", 99.99, "USD"); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - logEvent.Properties.Should().ContainKeys("OrderId", "CustomerId", "Amount", "Currency"); - logEvent.Properties["OrderId"].ToString().Should().Be("\"ORD-001\""); - logEvent.Properties["CustomerId"].ToString().Should().Be("\"CUST-456\""); - logEvent.Properties["Amount"].ToString().Should().Be("99.99"); - logEvent.Properties["Currency"].ToString().Should().Be("\"USD\""); + + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + logEvent.Properties.Should().ContainKeys("OrderId", "CustomerId", "Amount", "Currency"); + logEvent.Properties["OrderId"].ToString().Should().Be("\"ORD-001\""); + logEvent.Properties["CustomerId"].ToString().Should().Be("\"CUST-456\""); + logEvent.Properties["Amount"].ToString().Should().Be("99.99"); + logEvent.Properties["Currency"].ToString().Should().Be("\"USD\""); + })); } [Fact(DisplayName = "Should preserve Akka metadata properties alongside semantic logging properties")] @@ -117,18 +124,19 @@ public async Task AkkaMetadataAndSemanticPropertiesTest() await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("User {UserId} action", 999); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - // Semantic property - logEvent.Properties.Should().ContainKey("UserId"); - logEvent.Properties["UserId"].ToString().Should().Be("999"); - - // Akka metadata properties - logEvent.Properties.Should().ContainKey("ActorPath"); - logEvent.Properties.Should().ContainKey("LogSource"); - logEvent.Properties.Should().ContainKey("Thread"); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // Semantic property + logEvent.Properties.Should().ContainKey("UserId"); + logEvent.Properties["UserId"].ToString().Should().Be("999"); + + // Akka metadata properties + logEvent.Properties.Should().ContainKey("ActorPath"); + logEvent.Properties.Should().ContainKey("LogSource"); + logEvent.Properties.Should().ContainKey("Thread"); + })); } [Fact(DisplayName = "Should handle Serilog destructuring operator")] @@ -139,21 +147,22 @@ public async Task DestructuringOperatorTest() var user = new { Name = "Alice", Age = 30, Role = "Admin" }; _loggingAdapter.Info("Processing user {@User}", user); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - // Property name is "User" (@ operator removed by Akka's parser) - logEvent.Properties.Should().ContainKey("User"); - - // Serilog should have destructured the object - var userProperty = logEvent.Properties["User"]; - userProperty.Should().BeOfType(); - - var structure = (StructureValue)userProperty; - structure.Properties.Should().Contain(p => p.Name == "Name"); - structure.Properties.Should().Contain(p => p.Name == "Age"); - structure.Properties.Should().Contain(p => p.Name == "Role"); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // Property name is "User" (@ operator removed by Akka's parser) + logEvent.Properties.Should().ContainKey("User"); + + // Serilog should have destructured the object + var userProperty = logEvent.Properties["User"]; + userProperty.Should().BeOfType(); + + var structure = (StructureValue)userProperty; + structure.Properties.Should().Contain(p => p.Name == "Name"); + structure.Properties.Should().Contain(p => p.Name == "Age"); + structure.Properties.Should().Contain(p => p.Name == "Role"); + })); } [Fact(DisplayName = "Should handle Serilog stringification operator")] @@ -164,16 +173,17 @@ public async Task StringificationOperatorTest() var exception = new InvalidOperationException("Test error"); _loggingAdapter.Info("Error occurred: {$Exception}", exception); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // Property name is "Exception" ($ operator removed by Akka's parser) + logEvent.Properties.Should().ContainKey("Exception"); - // Property name is "Exception" ($ operator removed by Akka's parser) - logEvent.Properties.Should().ContainKey("Exception"); - - // Serilog should have used ToString() instead of destructuring - var exceptionProperty = logEvent.Properties["Exception"]; - exceptionProperty.Should().BeOfType(); + // Serilog should have used ToString() instead of destructuring + var exceptionProperty = logEvent.Properties["Exception"]; + exceptionProperty.Should().BeOfType(); + })); } [Fact(DisplayName = "Should handle format specifiers in named templates")] @@ -183,15 +193,16 @@ public async Task FormatSpecifiersInTemplatesTest() await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("Total amount: {Amount:N2}", 1234.5678); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - // Property name is "Amount" (format specifier removed by Akka's parser) - logEvent.Properties.Should().ContainKey("Amount"); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // Property name is "Amount" (format specifier removed by Akka's parser) + logEvent.Properties.Should().ContainKey("Amount"); - // The rendered message should apply the format - logEvent.RenderMessage().Should().Contain("1,234.57"); + // The rendered message should apply the format + logEvent.RenderMessage().Should().Contain("1,234.57"); + })); } [Fact(DisplayName = "Should handle empty/no properties gracefully")] @@ -201,17 +212,18 @@ public async Task NoPropertiesTest() await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); _loggingAdapter.Info("No template properties here"); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - - // Should still have Akka metadata properties - logEvent.Properties.Should().ContainKey("ActorPath"); - logEvent.Properties.Should().ContainKey("LogSource"); - logEvent.Properties.Should().ContainKey("Thread"); - - // Message content should be preserved - logEvent.RenderMessage().Should().Contain("No template properties here"); + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // Should still have Akka metadata properties + logEvent.Properties.Should().ContainKey("ActorPath"); + logEvent.Properties.Should().ContainKey("LogSource"); + logEvent.Properties.Should().ContainKey("Thread"); + + // Message content should be preserved + logEvent.RenderMessage().Should().Contain("No template properties here"); + })); } [Fact(DisplayName = "Should work with ForContext enrichment")] @@ -222,16 +234,34 @@ public async Task ForContextWithSemanticLoggingTest() var contextLogger = _loggingAdapter.ForContext("TenantId", "TENANT-123"); contextLogger.Info("User {UserId} performed action", 456); - await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 1); - - _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); - - // Should have both semantic property and context enrichment - logEvent.Properties.Should().ContainKey("UserId"); - logEvent.Properties["UserId"].ToString().Should().Be("456"); + + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // Should have both semantic property and context enrichment + logEvent.Properties.Should().ContainKey("UserId"); + logEvent.Properties["UserId"].ToString().Should().Be("456"); + + logEvent.Properties.Should().ContainKey("TenantId"); + logEvent.Properties["TenantId"].ToString().Should().Be("\"TENANT-123\""); + })); + } - logEvent.Properties.Should().ContainKey("TenantId"); - logEvent.Properties["TenantId"].ToString().Should().Be("\"TENANT-123\""); + private bool AssertCondition(Action assertion) + { + while (_sink.Writes.TryDequeue(out var logEvent)) + { + try + { + assertion(logEvent); + return true; + } + catch + { + // no-op + } + } + return false; } } }