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..22646c6 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 + @@ -19,6 +24,9 @@ Always + + PreserveNewest + diff --git a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs index 0947bf8..fcc7f55 100644 --- a/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/LogMessageSpecs.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Event; @@ -8,42 +9,62 @@ using Serilog.Events; using Xunit; using Xunit.Abstractions; +using LogEvent = Serilog.Events.LogEvent; namespace Akka.Logger.Serilog.Tests { - public class LogMessageSpecs : TestKit.Xunit2.TestKit + 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 ILoggingAdapter _loggingAdapter; + private readonly ITestOutputHelper _helper; private readonly TestSink _sink; + + private ActorSystem _sys; + 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() + Log.Logger = new LoggerConfiguration() .WriteTo.Sink(_sink) .MinimumLevel.Debug() .CreateLogger(); - _loggingAdapter = Sys.Log; + } + + public Task InitializeAsync() + { + _sys = ActorSystem.Create("TestActorSystem", Config); + _testKit = new TestKit.Xunit2.TestKit(_sys, _helper); + _loggingAdapter = _sys.Log; + + return Task.CompletedTask; } + public async Task DisposeAsync() + { + _testKit.Shutdown(); + await _sys.Terminate(); + } + [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); - _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] @@ -52,23 +73,34 @@ 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); - - _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] @@ -77,14 +109,14 @@ 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); - _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] @@ -93,15 +125,16 @@ 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); - _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] @@ -110,15 +143,16 @@ 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); - _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] @@ -127,14 +161,14 @@ 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); - _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] @@ -143,14 +177,14 @@ 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); - _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] @@ -159,15 +193,16 @@ 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); - _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] @@ -176,15 +211,16 @@ 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); - _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] @@ -193,14 +229,14 @@ 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); - _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] @@ -209,14 +245,14 @@ 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); - _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] @@ -225,15 +261,16 @@ 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); - _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] @@ -242,15 +279,16 @@ 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); - _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] @@ -259,14 +297,14 @@ 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); - _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] @@ -275,14 +313,14 @@ 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); - _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] @@ -291,15 +329,16 @@ 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); - _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] @@ -308,15 +347,26 @@ 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); - _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 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 new file mode 100644 index 0000000..2ee79f0 --- /dev/null +++ b/src/Akka.Logger.Serilog.Tests/SemanticLoggingSpecs.cs @@ -0,0 +1,267 @@ +using System; +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; +using LogEvent = Serilog.Events.LogEvent; + +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 : 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 ActorSystem _sys; + private TestKit.Xunit2.TestKit _testKit; + private ILoggingAdapter _loggingAdapter; + + public SemanticLoggingSpecs(ITestOutputHelper helper) + { + _helper = helper; + _sink = new TestSink(helper); + + Log.Logger = new LoggerConfiguration() + .WriteTo.Sink(_sink) + .MinimumLevel.Debug() + .CreateLogger(); + } + + public Task InitializeAsync() + { + _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); + + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + _testKit.Shutdown(); + await _sys.Terminate(); + } + + [Fact(DisplayName = "Should extract named template properties for Serilog")] + public async Task NamedTemplatePropertiesTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("User {UserId} with email {Email} logged in", 12345, "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")] + public async Task PositionalTemplatePropertiesTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("User {0} logged in from {1}", "Bob", "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")] + public async Task MultipleNamedPropertiesTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("Order {OrderId} for customer {CustomerId}: {Amount} {Currency}", + "ORD-001", "CUST-456", 99.99, "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")] + public async Task AkkaMetadataAndSemanticPropertiesTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("User {UserId} action", 999); + + 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")] + public async Task DestructuringOperatorTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + var user = new { Name = "Alice", Age = 30, Role = "Admin" }; + _loggingAdapter.Info("Processing user {@User}", user); + + 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")] + public async Task StringificationOperatorTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + var exception = new InvalidOperationException("Test error"); + _loggingAdapter.Info("Error occurred: {$Exception}", exception); + + await _testKit.AwaitConditionAsync(() => + AssertCondition(logEvent => + { + // 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 _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("Total amount: {Amount:N2}", 1234.5678); + + 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"); + })); + } + + [Fact(DisplayName = "Should handle empty/no properties gracefully")] + public async Task NoPropertiesTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + _loggingAdapter.Info("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")] + public async Task ForContextWithSemanticLoggingTest() + { + _sink.Clear(); + await _testKit.AwaitConditionAsync(() => _sink.Writes.Count == 0); + + var contextLogger = _loggingAdapter.ForContext("TenantId", "TENANT-123"); + contextLogger.Info("User {UserId} performed action", 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\""); + })); + } + + private bool AssertCondition(Action assertion) + { + while (_sink.Writes.TryDequeue(out var logEvent)) + { + try + { + assertion(logEvent); + return true; + } + catch + { + // no-op + } + } + return false; + } + } +} diff --git a/src/Akka.Logger.Serilog.Tests/TestSink.cs b/src/Akka.Logger.Serilog.Tests/TestSink.cs index e87e869..b81caf4 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 { /// @@ -14,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; @@ -33,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()}"); 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 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)); } /// 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 @@ +