Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6cdceec
feat: Add native semantic logging support to Akka.NET core
Aaronontheweb Nov 19, 2025
9d10594
perf: optimize semantic logging memory allocations (75% reduction)
Aaronontheweb Nov 20, 2025
f9a2d2c
Enable SemanticLogMessageFormatter as default logger formatter
Aaronontheweb Nov 20, 2025
69c15e5
feat: Add EventFilter support for semantic logging templates
Aaronontheweb Nov 20, 2025
9efe15a
test: Add semantic logging integration tests for log filtering
Aaronontheweb Nov 20, 2025
e973213
fix: Update ConfigurationSpec to expect SemanticLogMessageFormatter a…
Aaronontheweb Nov 20, 2025
3173d24
fix: enable nullable reference types in LogEventExtensions
Aaronontheweb Nov 20, 2025
2a52c6f
test: Add semantic logging edge cases verification test
Aaronontheweb Nov 21, 2025
8a4c843
Update API Approval list
Arkatufus Nov 21, 2025
8befe09
Merge branch 'feature/semantic-logging' of github.com:Aaronontheweb/a…
Arkatufus Nov 21, 2025
1c58a6b
Add new edge case unit tests (failing)
Arkatufus Nov 21, 2025
8e0015f
docs: Add Message Templates spec reference to SemanticLogMessageForma…
Aaronontheweb Nov 22, 2025
66899c4
fix: Correct escaped brace handling in semantic logging per Message T…
Aaronontheweb Nov 22, 2025
c03463b
Merge branch 'dev' into feature/semantic-logging
Aaronontheweb Nov 22, 2025
5d64aa2
fix: Use culture-independent format specifiers in verify test
Aaronontheweb Nov 22, 2025
9b73de9
test: Add escaped brace benchmarks and .NET Framework verified file
Aaronontheweb Nov 22, 2025
6cc8b1f
Add unit tests
Arkatufus Nov 24, 2025
800c319
fix: Implement alignment specifiers and null ToString() handling in S…
Aaronontheweb Nov 24, 2025
66bd3d3
Merge branch 'dev' into feature/semantic-logging
Arkatufus Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
534 changes: 534 additions & 0 deletions src/benchmark/Akka.Benchmarks/Logging/SemanticLoggingBenchmarks.cs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big battery of benchmarks that covers:

  1. Performance of the template cache
  2. Performance of the template formatter / property name extractor
  3. Performance of the parameter value extractor too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, validates that we haven't significantly impacted the performance of our old string.Format calls either

Large diffs are not rendered by default.

62 changes: 57 additions & 5 deletions src/core/Akka.API.Tests/LogFormatSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public DefaultLogFormatSpec() : base(CustomLoggerSetup())
{
_logger = (CustomLogger)Sys.Settings.StdoutLogger;
}

private readonly CustomLogger _logger;

public class CustomLogger : StandardOutLogger
{
protected override void Log(object message)
Expand All @@ -44,13 +44,13 @@ protected override void Log(object message)
{
_events.Add(e);
}

}

private readonly ConcurrentBag<LogEvent> _events = new();
public IReadOnlyCollection<LogEvent> Events => _events;
}

public static ActorSystemSetup CustomLoggerSetup()
{
var hocon = @$"
Expand Down Expand Up @@ -115,6 +115,58 @@ await AwaitConditionAsync(() =>
await Verifier.Verify(text);
}

[Fact]
public async Task ShouldHandleSemanticLogEdgeCases()
{
// arrange
var filePath = Path.GetTempFileName();

// act
using (new OutputRedirector(filePath))
{
// Named properties
Sys.Log.Debug("User {UserId} logged in from {IpAddress}", 12345, "192.168.1.1");
Sys.Log.Info("Processing order {OrderId} for customer {CustomerId}", "ORD-001", "CUST-999");

// Positional properties (old style)
Sys.Log.Warning("Processing item {0} of {1}", 5, 10);

// Mixed types - use F2 instead of C for culture-independent output
Sys.Log.Info("Order total is ${Amount:F2} with {ItemCount} items", 123.45m, 3);

// Edge cases
Sys.Log.Debug("Empty template");
Sys.Log.Info("Single property {Value}", 42);
Sys.Log.Warning("Null value: {NullValue}", null);
Sys.Log.Error("Exception occurred for user {UserId}", 999);

// Special characters and escaping
Sys.Log.Debug("Path: {FilePath}, Size: {FileSize} bytes", @"C:\temp\file.txt", 1024);

// Boolean and date types - use explicit date format for culture-independent output
Sys.Log.Info("User {Username} is active: {IsActive}, joined on {JoinDate:yyyy-MM-dd}", "john.doe", true, DateTime.Parse("2024-01-15"));

// Long strings and alignment
Sys.Log.Debug("Request from {RemoteAddress} to endpoint {Endpoint} took {DurationMs}ms", "192.168.1.100:54321", "/api/v1/users", 250);

// force all logs to be received - wait for the last log message
await AwaitConditionAsync(() => Task.FromResult(_logger.Events.Any(e => e.Message.ToString()!.Contains("took 250ms"))), TimeSpan.FromSeconds(5));
}

// assert
// ReSharper disable once MethodHasAsyncOverload
var text = File.ReadAllText(filePath);

// need to sanitize the thread id and timestamps
text = SanitizeDateTime(text);
text = SanitizeThreadNumber(text);
text = SanitizeTestEventListener(text);
text = SanitizeDefaultLoggersStarted(text);
text = SanitizeCustomLoggerRemoved(text);

await Verifier.Verify(text);
}

private static string SanitizeDefaultLoggersStarted(string logs)
{
var pattern = @"^.*Default Loggers started.*$\r?\n?";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3472,6 +3472,17 @@ namespace Akka.Event
public abstract Akka.Event.LogLevel LogLevel();
public override string ToString() { }
}
[System.Runtime.CompilerServices.NullableAttribute(0)]
public class static LogEventExtensions
{
public static System.Collections.Generic.IEnumerable<object> GetParameters(this Akka.Event.LogEvent evt) { }
public static System.Collections.Generic.IReadOnlyList<string> GetPropertyNames(this Akka.Event.LogEvent evt) { }
public static string GetTemplate(this Akka.Event.LogEvent evt) { }
public static bool TryGetProperties(this Akka.Event.LogEvent evt, [System.Runtime.CompilerServices.NullableAttribute(new byte[] {
2,
1,
1})] out System.Collections.Generic.IReadOnlyDictionary<string, object> properties) { }
}
public abstract class LogFilterBase : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression
{
protected LogFilterBase() { }
Expand Down Expand Up @@ -3531,6 +3542,8 @@ namespace Akka.Event
protected readonly Akka.Event.ILogMessageFormatter Formatter;
public LogMessage(Akka.Event.ILogMessageFormatter formatter, string format) { }
public string Format { get; }
public System.Collections.Generic.IReadOnlyList<string> PropertyNames { get; }
public System.Collections.Generic.IReadOnlyDictionary<string, object> GetProperties() { }
[Akka.Annotations.InternalApiAttribute()]
public abstract System.Collections.Generic.IEnumerable<object> Parameters();
[Akka.Annotations.InternalApiAttribute()]
Expand Down Expand Up @@ -3708,6 +3721,12 @@ namespace Akka.Event
public override Akka.Event.LogFilterType FilterType { get; }
public override Akka.Event.LogFilterDecision ShouldKeepMessage(Akka.Event.LogEvent content, [System.Runtime.CompilerServices.NullableAttribute(2)] string expandedMessage = null) { }
}
public sealed class SemanticLogMessageFormatter : Akka.Event.ILogMessageFormatter
{
public static readonly Akka.Event.SemanticLogMessageFormatter Instance;
public string Format(string format, params object[] args) { }
public string Format(string format, System.Collections.Generic.IEnumerable<object> args) { }
}
public class StandardOutLogger : Akka.Event.MinimalLogger
{
public StandardOutLogger() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3483,6 +3483,17 @@ namespace Akka.Event
public abstract Akka.Event.LogLevel LogLevel();
public override string ToString() { }
}
[System.Runtime.CompilerServices.NullableAttribute(0)]
public class static LogEventExtensions
{
public static System.Collections.Generic.IEnumerable<object> GetParameters(this Akka.Event.LogEvent evt) { }
public static System.Collections.Generic.IReadOnlyList<string> GetPropertyNames(this Akka.Event.LogEvent evt) { }
public static string GetTemplate(this Akka.Event.LogEvent evt) { }
public static bool TryGetProperties(this Akka.Event.LogEvent evt, [System.Runtime.CompilerServices.NullableAttribute(new byte[] {
2,
1,
1})] out System.Collections.Generic.IReadOnlyDictionary<string, object> properties) { }
}
public abstract class LogFilterBase : Akka.Actor.INoSerializationVerificationNeeded, Akka.Event.IDeadLetterSuppression
{
protected LogFilterBase() { }
Expand Down Expand Up @@ -3542,6 +3553,8 @@ namespace Akka.Event
protected readonly Akka.Event.ILogMessageFormatter Formatter;
public LogMessage(Akka.Event.ILogMessageFormatter formatter, string format) { }
public string Format { get; }
public System.Collections.Generic.IReadOnlyList<string> PropertyNames { get; }
public System.Collections.Generic.IReadOnlyDictionary<string, object> GetProperties() { }
[Akka.Annotations.InternalApiAttribute()]
public abstract System.Collections.Generic.IEnumerable<object> Parameters();
[Akka.Annotations.InternalApiAttribute()]
Expand Down Expand Up @@ -3717,6 +3730,12 @@ namespace Akka.Event
public override Akka.Event.LogFilterType FilterType { get; }
public override Akka.Event.LogFilterDecision ShouldKeepMessage(Akka.Event.LogEvent content, [System.Runtime.CompilerServices.NullableAttribute(2)] string expandedMessage = null) { }
}
public sealed class SemanticLogMessageFormatter : Akka.Event.ILogMessageFormatter
{
public static readonly Akka.Event.SemanticLogMessageFormatter Instance;
public string Format(string format, params object[] args) { }
public string Format(string format, System.Collections.Generic.IEnumerable<object> args) { }
}
public class StandardOutLogger : Akka.Event.MinimalLogger
{
public StandardOutLogger() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] User 12345 logged in from 192.168.1.1
[INFO][DateTime][Thread 0001][ActorSystem(test)] Processing order ORD-001 for customer CUST-999
[WARNING][DateTime][Thread 0001][ActorSystem(test)] Processing item 5 of 10
[INFO][DateTime][Thread 0001][ActorSystem(test)] Order total is $123.45 with 3 items
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] Empty template
[INFO][DateTime][Thread 0001][ActorSystem(test)] Single property 42
[WARNING][DateTime][Thread 0001][ActorSystem(test)] Null value: {NullValue}
[ERROR][DateTime][Thread 0001][ActorSystem(test)] Exception occurred for user 999
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] Path: C:\temp\file.txt, Size: 1024 bytes
[INFO][DateTime][Thread 0001][ActorSystem(test)] User john.doe is active: True, joined on 2024-01-15
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] Request from 192.168.1.100:54321 to endpoint /api/v1/users took 250ms
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] User 12345 logged in from 192.168.1.1
[INFO][DateTime][Thread 0001][ActorSystem(test)] Processing order ORD-001 for customer CUST-999
[WARNING][DateTime][Thread 0001][ActorSystem(test)] Processing item 5 of 10
[INFO][DateTime][Thread 0001][ActorSystem(test)] Order total is $123.45 with 3 items
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] Empty template
[INFO][DateTime][Thread 0001][ActorSystem(test)] Single property 42
[WARNING][DateTime][Thread 0001][ActorSystem(test)] Null value: {NullValue}
[ERROR][DateTime][Thread 0001][ActorSystem(test)] Exception occurred for user 999
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] Path: C:\temp\file.txt, Size: 1024 bytes
[INFO][DateTime][Thread 0001][ActorSystem(test)] User john.doe is active: True, joined on 2024-01-15
[DEBUG][DateTime][Thread 0001][ActorSystem(test)] Request from 192.168.1.100:54321 to endpoint /api/v1/users took 250ms
20 changes: 19 additions & 1 deletion src/core/Akka.TestKit/EventFilter/Internal/EventFilterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,26 @@ protected virtual void OnEventMatched(LogEvent logEvent)
/// <returns>TBD</returns>
protected bool InternalDoMatch(string src, object? msg)
{
// Check source matcher first (fast path)
if (!_sourceMatcher.IsMatch(src))
return false;

// For semantic logging support, try matching against both the formatted message
// and the unformatted template pattern
if (msg is LogMessage logMessage)
{
// Try matching against the template pattern first (e.g., "User {UserId} logged in")
if (_messageMatcher.IsMatch(logMessage.Format))
return true;

// Fall back to matching the formatted message (e.g., "User 12345 logged in")
var formattedMsg = logMessage.ToString() ?? "null";
return _messageMatcher.IsMatch(formattedMsg);
}

// Non-semantic logging or legacy messages
var msgstr = msg == null ? "null" : msg.ToString() ?? "null";
return _sourceMatcher.IsMatch(src) && _messageMatcher.IsMatch(msgstr);
return _messageMatcher.IsMatch(msgstr);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/core/Akka.Tests/Configuration/ConfigurationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void The_default_configuration_file_contain_all_configuration_properties(
settings.LogDeadLetters.ShouldBe(10);
settings.LogDeadLettersDuringShutdown.ShouldBeFalse();
settings.LogDeadLettersSuspendDuration.ShouldBe(TimeSpan.FromMinutes(5));
settings.LogFormatter.Should().BeOfType<DefaultLogMessageFormatter>();
settings.LogFormatter.Should().BeOfType<SemanticLogMessageFormatter>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we now use the semantic log message formatter by default


settings.ProviderClass.ShouldBe(typeof (LocalActorRefProvider).FullName);
settings.SupervisorStrategyClass.ShouldBe(typeof (DefaultSupervisorStrategy).FullName);
Expand Down
Loading
Loading