diff --git a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCore.verified.txt b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCore.verified.txt index 4d77d95a..3b804939 100644 --- a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCore.verified.txt +++ b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCore.verified.txt @@ -131,6 +131,7 @@ namespace Akka.Hosting public Akka.Hosting.DebugOptions? DebugOptions { get; set; } public bool LogConfigOnStart { get; set; } public Akka.Event.LogLevel LogLevel { get; set; } + public System.Type LogMessageFormatter { get; set; } public Akka.Hosting.LoggerConfigBuilder AddLogger() where T : Akka.Dispatch.IRequiresMessageQueue { } public Akka.Hosting.LoggerConfigBuilder ClearLoggers() { } diff --git a/src/Akka.Hosting.Tests/Logging/LogMessageFormatterSpec.cs b/src/Akka.Hosting.Tests/Logging/LogMessageFormatterSpec.cs new file mode 100644 index 00000000..804a1940 --- /dev/null +++ b/src/Akka.Hosting.Tests/Logging/LogMessageFormatterSpec.cs @@ -0,0 +1,117 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2023 Lightbend Inc. +// Copyright (C) 2013-2023 .NET Foundation +// +// ----------------------------------------------------------------------- + +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 Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using static FluentAssertions.FluentActions; + +namespace Akka.Hosting.Tests.Logging; + +public class LogMessageFormatterSpec +{ + private ITestOutputHelper _helper; + + public LogMessageFormatterSpec(ITestOutputHelper helper) + { + _helper = helper; + } + + [Fact(DisplayName = "ILogMessageFormatter should transform log messages")] + public async Task TransformMessagesTest() + { + using var host = await SetupHost(typeof(TestLogMessageFormatter)); + + try + { + var sys = host.Services.GetRequiredService(); + var testKit = new TestKit.Xunit2.TestKit(sys); + + var probe = testKit.CreateTestProbe(); + sys.EventStream.Subscribe(probe, typeof(Error)); + sys.Log.Error("This is a test {0}", 1); + + var msg = probe.ExpectMsg(); + msg.Message.Should().BeAssignableTo(); + msg.ToString().Should().Contain("++TestLogMessageFormatter++"); + } + finally + { + await host.StopAsync(); + } + } + + [Fact(DisplayName = "Invalid LogMessageFormatter property should throw")] + public async Task InvalidLogMessageFormatterThrowsTest() + { + await Awaiting(async () => await SetupHost(typeof(InvalidLogMessageFormatter))) + .Should().ThrowAsync().WithMessage("*must have an empty constructor*"); + } + + private async Task SetupHost(Type formatter) + { + var host = new HostBuilder() + .ConfigureLogging(builder => + { + builder.AddProvider(new XUnitLoggerProvider(_helper, LogLevel.Information)); + }) + .ConfigureServices(collection => + { + collection.AddAkka("TestSys", configurationBuilder => + { + configurationBuilder + .ConfigureLoggers(setup => + { + setup.LogLevel = Event.LogLevel.DebugLevel; + setup.AddLoggerFactory(); + setup.LogMessageFormatter = formatter; + }); + }); + }).Build(); + await host.StartAsync(); + return host; + } +} + +public class TestLogMessageFormatter : ILogMessageFormatter +{ + public string Format(string format, params object[] args) + { + return string.Format($"++TestLogMessageFormatter++{format}", args); + } + + public string Format(string format, IEnumerable args) + => Format(format, args.ToArray()); +} + +public class InvalidLogMessageFormatter : ILogMessageFormatter +{ + public InvalidLogMessageFormatter(string doesNotMatter) + { + } + + public string Format(string format, params object[] args) + { + throw new NotImplementedException(); + } + + public string Format(string format, IEnumerable args) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting/LoggerConfigBuilder.cs b/src/Akka.Hosting/LoggerConfigBuilder.cs index b525cea1..d1c2b8bb 100644 --- a/src/Akka.Hosting/LoggerConfigBuilder.cs +++ b/src/Akka.Hosting/LoggerConfigBuilder.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using Akka.Configuration; using Akka.Dispatch; @@ -17,6 +18,7 @@ namespace Akka.Hosting public sealed class LoggerConfigBuilder { private readonly List _loggers = new List { typeof(DefaultLogger) }; + private Type _logMessageFormatter = typeof(DefaultLogMessageFormatter); internal AkkaConfigurationBuilder Builder { get; } internal LoggerConfigBuilder(AkkaConfigurationBuilder builder) @@ -45,6 +47,22 @@ internal LoggerConfigBuilder(AkkaConfigurationBuilder builder) public DebugOptions? DebugOptions { get; set; } + public Type LogMessageFormatter + { + get => _logMessageFormatter; + set + { + if (!typeof(ILogMessageFormatter).IsAssignableFrom(value)) + throw new ConfigurationException($"{nameof(LogMessageFormatter)} must implement {nameof(ILogMessageFormatter)}"); + + var ctor = value.GetConstructor(new Type[]{}); + if (ctor is null) + throw new ConfigurationException($"{nameof(LogMessageFormatter)} Type must have an empty constructor"); + + _logMessageFormatter = value; + } + } + /// /// Clear all loggers currently registered. /// @@ -83,7 +101,9 @@ internal Config ToConfig() var sb = new StringBuilder() .Append("akka.loglevel=").AppendLine(ParseLogLevel(LogLevel)) .Append("akka.loggers=[").Append(string.Join(",", _loggers.Select(t => $"\"{t.AssemblyQualifiedName}\""))).AppendLine("]") - .Append("akka.log-config-on-start=").AppendLine(LogConfigOnStart ? "true" : "false"); + .Append("akka.log-config-on-start=").AppendLine(LogConfigOnStart ? "true" : "false") + .Append("akka.logger-formatter=").AppendLine(_logMessageFormatter.AssemblyQualifiedName.ToHocon()); + if (DebugOptions is { }) sb.AppendLine(DebugOptions.ToString()); if (DeadLetterOptions is { })