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 @@
+