diff --git a/src/core/Akka.Tests/Event/FormatterSpecs.cs b/src/core/Akka.Tests/Event/FormatterSpecs.cs
new file mode 100644
index 00000000000..4db5efef36d
--- /dev/null
+++ b/src/core/Akka.Tests/Event/FormatterSpecs.cs
@@ -0,0 +1,120 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (C) 2009-2025 Lightbend Inc.
+// Copyright (C) 2013-2025 .NET Foundation
+//
+//-----------------------------------------------------------------------
+
+using System;
+using Akka.Event;
+using FluentAssertions;
+using Xunit;
+
+namespace Akka.Tests.Event
+{
+ public class FormatterSpecs
+ {
+ [Fact(DisplayName = "SemanticLogMessageFormatter should return diagnostic for mismatched positional args")]
+ public void SemanticLogMessageFormatter_should_return_diagnostic_for_mismatched_positional_args()
+ {
+ var formatter = SemanticLogMessageFormatter.Instance;
+ var result = formatter.Format("{0} {1} {2}", new object[] { "a", "b" });
+
+ result.Should().StartWith("[INVALID LOG FORMAT]");
+ result.Should().Contain("{0} {1} {2}");
+ result.Should().Contain("a");
+ result.Should().Contain("b");
+ }
+
+ [Fact(DisplayName = "SemanticLogMessageFormatter should return diagnostic for mismatched positional args via IReadOnlyList path")]
+ public void SemanticLogMessageFormatter_should_return_diagnostic_for_mismatched_positional_args_via_readonly_list()
+ {
+ var formatter = SemanticLogMessageFormatter.Instance;
+ var args = new LogValues("a", "b");
+ var result = formatter.Format("{0} {1} {2}", args);
+
+ result.Should().StartWith("[INVALID LOG FORMAT]");
+ result.Should().Contain("{0} {1} {2}");
+ }
+
+ [Fact(DisplayName = "DefaultLogMessageFormatter should return diagnostic for mismatched positional args")]
+ public void DefaultLogMessageFormatter_should_return_diagnostic_for_mismatched_positional_args()
+ {
+ var formatter = DefaultLogMessageFormatter.Instance;
+ var result = formatter.Format("{0} {1} {2}", new object[] { "a", "b" });
+
+ result.Should().StartWith("[INVALID LOG FORMAT]");
+ result.Should().Contain("{0} {1} {2}");
+ result.Should().Contain("a");
+ result.Should().Contain("b");
+ }
+
+ [Fact(DisplayName = "DefaultLogMessageFormatter should return diagnostic for mismatched positional args via IEnumerable overload")]
+ public void DefaultLogMessageFormatter_should_return_diagnostic_for_mismatched_positional_args_via_enumerable()
+ {
+ var formatter = DefaultLogMessageFormatter.Instance;
+ var result = formatter.Format("{0} {1} {2}", new object[] { "a", "b" });
+
+ result.Should().StartWith("[INVALID LOG FORMAT]");
+ result.Should().Contain("{0} {1} {2}");
+ }
+
+ [Fact(DisplayName = "LogFilterEvaluator should handle FormatException gracefully")]
+ public void LogFilterEvaluator_should_handle_FormatException_gracefully()
+ {
+ // Use the empty evaluator (no filters) - this is the path third-party loggers hit
+ var evaluator = LogFilterEvaluator.NoFilters;
+
+ // Create a log event with a bad format string
+ var badMessage = new DefaultLogMessage(
+ SemanticLogMessageFormatter.Instance,
+ "{0} {1} {2}",
+ "a", "b");
+
+ var evt = new Warning("test-source", typeof(FormatterSpecs), badMessage);
+
+ var result = evaluator.ShouldTryKeepMessage(evt, out var expandedMessage);
+
+ result.Should().BeTrue();
+ expandedMessage.Should().NotBeNullOrEmpty();
+ expandedMessage.Should().Contain("[INVALID LOG FORMAT]");
+ }
+
+ [Fact(DisplayName = "LogFilterEvaluator with content filters should handle FormatException gracefully")]
+ public void LogFilterEvaluator_with_content_filters_should_handle_FormatException_gracefully()
+ {
+ // Create an evaluator with a content filter to exercise the non-empty filter path
+ var filter = new RegexLogMessageFilter(
+ new System.Text.RegularExpressions.Regex("never_match_anything"));
+ var evaluator = new LogFilterEvaluator(new LogFilterBase[] { filter });
+
+ var badMessage = new DefaultLogMessage(
+ SemanticLogMessageFormatter.Instance,
+ "{0} {1} {2}",
+ "a", "b");
+
+ var evt = new Warning("test-source", typeof(FormatterSpecs), badMessage);
+
+ var result = evaluator.ShouldTryKeepMessage(evt, out var expandedMessage);
+
+ result.Should().BeTrue();
+ expandedMessage.Should().Contain("[INVALID LOG FORMAT]");
+ }
+
+ [Fact(DisplayName = "SemanticLogMessageFormatter should still format valid positional templates correctly")]
+ public void SemanticLogMessageFormatter_should_still_format_valid_positional_templates()
+ {
+ var formatter = SemanticLogMessageFormatter.Instance;
+ var result = formatter.Format("{0} and {1}", new object[] { "hello", "world" });
+ result.Should().Be("hello and world");
+ }
+
+ [Fact(DisplayName = "DefaultLogMessageFormatter should still format valid positional templates correctly")]
+ public void DefaultLogMessageFormatter_should_still_format_valid_positional_templates()
+ {
+ var formatter = DefaultLogMessageFormatter.Instance;
+ var result = formatter.Format("{0} and {1}", new object[] { "hello", "world" });
+ result.Should().Be("hello and world");
+ }
+ }
+}
diff --git a/src/core/Akka.Tests/Loggers/LoggerSpec.cs b/src/core/Akka.Tests/Loggers/LoggerSpec.cs
index a052b94ce3d..c4f9d851338 100644
--- a/src/core/Akka.Tests/Loggers/LoggerSpec.cs
+++ b/src/core/Akka.Tests/Loggers/LoggerSpec.cs
@@ -37,40 +37,28 @@ public LoggerSpec(ITestOutputHelper output) : base(Config, output)
[Fact]
public async Task TestOutputLogger_WithBadFormattingMustNotThrow()
{
- var events = new List();
-
// Need to wait until TestOutputLogger initializes
await Task.Delay(500);
Sys.EventStream.Subscribe(TestActor, typeof(LogEvent));
+ // Bad format strings should now produce diagnostic messages rather than throwing FormatException.
+ // Only the original log event should be published (no secondary Error event).
Sys.Log.Error(new FakeException("BOOM"), Case.t, Case.p);
- events.Add(await ExpectMsgAsync());
- events.Add(await ExpectMsgAsync());
-
- events.All(e => e is Error).Should().BeTrue();
- events.Select(e => e.Cause).Any(c => c is FakeException).Should().BeTrue();
- events.Select(e => e.Cause).Any(c => c is AggregateException).Should().BeTrue();
+ var errorEvt = await ExpectMsgAsync();
+ errorEvt.Cause.Should().BeOfType();
+ errorEvt.ToString().Should().Contain("[INVALID LOG FORMAT]");
- events.Clear();
Sys.Log.Warning(Case.t, Case.p);
- events.Add(await ExpectMsgAsync());
- events.Add(await ExpectMsgAsync());
- events.Any(e => e is Warning).Should().BeTrue();
- events.First(e => e is Error).Cause.Should().BeOfType();
+ var warningEvt = await ExpectMsgAsync();
+ warningEvt.ToString().Should().Contain("[INVALID LOG FORMAT]");
- events.Clear();
Sys.Log.Info(Case.t, Case.p);
- events.Add(await ExpectMsgAsync());
- events.Add(await ExpectMsgAsync());
- events.Any(e => e is Info).Should().BeTrue();
- events.First(e => e is Error).Cause.Should().BeOfType();
+ var infoEvt = await ExpectMsgAsync();
+ infoEvt.ToString().Should().Contain("[INVALID LOG FORMAT]");
- events.Clear();
Sys.Log.Debug(Case.t, Case.p);
- events.Add(await ExpectMsgAsync());
- events.Add(await ExpectMsgAsync());
- events.Any(e => e is Debug).Should().BeTrue();
- events.First(e => e is Error).Cause.Should().BeOfType();
+ var debugEvt = await ExpectMsgAsync();
+ debugEvt.ToString().Should().Contain("[INVALID LOG FORMAT]");
}
[Fact]
diff --git a/src/core/Akka/Event/DefaultLogMessageFormatter.cs b/src/core/Akka/Event/DefaultLogMessageFormatter.cs
index e38e90c82ba..3e982576871 100644
--- a/src/core/Akka/Event/DefaultLogMessageFormatter.cs
+++ b/src/core/Akka/Event/DefaultLogMessageFormatter.cs
@@ -5,6 +5,7 @@
//
//-----------------------------------------------------------------------
+using System;
using System.Collections.Generic;
using System.Linq;
@@ -17,15 +18,32 @@ public class DefaultLogMessageFormatter : ILogMessageFormatter
{
public static readonly DefaultLogMessageFormatter Instance = new();
private DefaultLogMessageFormatter(){}
-
+
public string Format(string format, params object[] args)
{
- return string.Format(format, args);
+ try
+ {
+ return string.Format(format, args);
+ }
+ catch (FormatException)
+ {
+ return $"[INVALID LOG FORMAT] str=[{format}], args=[{string.Join(", ", args)}]. " +
+ "Please fix the format string in the logging call site.";
+ }
}
public string Format(string format, IEnumerable