From c969ceb27d846eae3d49cec40e173fb49802c338 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Mon, 18 Jul 2022 19:17:59 +0700 Subject: [PATCH] Add ILoggerFactory logger support (#72) * Add ILoggerFactory logger support * Rename WithLoggerFactorLogger to WithLoggerFactory * Add ILoggerFactory sample project * Make `Akka.Hosting.LoggingDemo.csproj` not packable Co-authored-by: Aaron Stannard --- Akka.Hosting.sln | 7 + .../Logging/AkkaLoggerFactoryExtensions.cs | 38 +++++ .../Logging/LoggerFactoryLogger.cs | 135 ++++++++++++++++++ .../Akka.Hosting.LoggingDemo.csproj | 12 ++ src/Examples/Akka.Hosting.LoggingDemo/Echo.cs | 3 + .../Akka.Hosting.LoggingDemo/Program.cs | 43 ++++++ .../appsettings.Development.json | 9 ++ .../Akka.Hosting.LoggingDemo/appsettings.json | 11 ++ 8 files changed, 258 insertions(+) create mode 100644 src/Akka.Hosting/Logging/AkkaLoggerFactoryExtensions.cs create mode 100644 src/Akka.Hosting/Logging/LoggerFactoryLogger.cs create mode 100644 src/Examples/Akka.Hosting.LoggingDemo/Akka.Hosting.LoggingDemo.csproj create mode 100644 src/Examples/Akka.Hosting.LoggingDemo/Echo.cs create mode 100644 src/Examples/Akka.Hosting.LoggingDemo/Program.cs create mode 100644 src/Examples/Akka.Hosting.LoggingDemo/appsettings.Development.json create mode 100644 src/Examples/Akka.Hosting.LoggingDemo/appsettings.json diff --git a/Akka.Hosting.sln b/Akka.Hosting.sln index 21e96435..ce8668b3 100644 --- a/Akka.Hosting.sln +++ b/Akka.Hosting.sln @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Hosting", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Hosting.Tests", "src\Akka.Persistence.Hosting.Tests\Akka.Persistence.Hosting.Tests.csproj", "{876DE0B6-5FA8-4F79-876E-92EF5E9E7011}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Hosting.LoggingDemo", "src\Examples\Akka.Hosting.LoggingDemo\Akka.Hosting.LoggingDemo.csproj", "{4F79325B-9EE7-4501-800F-7A1F8DFBCC80}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,10 @@ Global {876DE0B6-5FA8-4F79-876E-92EF5E9E7011}.Debug|Any CPU.Build.0 = Debug|Any CPU {876DE0B6-5FA8-4F79-876E-92EF5E9E7011}.Release|Any CPU.ActiveCfg = Release|Any CPU {876DE0B6-5FA8-4F79-876E-92EF5E9E7011}.Release|Any CPU.Build.0 = Release|Any CPU + {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -109,5 +115,6 @@ Global GlobalSection(NestedProjects) = preSolution {5F6A7BE8-6906-46CE-BA1C-72EA11EFA33B} = {EFA970FF-6BCC-4C38-84D8-324D40F2BF03} {2C2C2DE2-5A79-4689-9D1A-D70CCF17545B} = {EFA970FF-6BCC-4C38-84D8-324D40F2BF03} + {4F79325B-9EE7-4501-800F-7A1F8DFBCC80} = {EFA970FF-6BCC-4C38-84D8-324D40F2BF03} EndGlobalSection EndGlobal diff --git a/src/Akka.Hosting/Logging/AkkaLoggerFactoryExtensions.cs b/src/Akka.Hosting/Logging/AkkaLoggerFactoryExtensions.cs new file mode 100644 index 00000000..12721d30 --- /dev/null +++ b/src/Akka.Hosting/Logging/AkkaLoggerFactoryExtensions.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2022 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.Actor; +using Akka.Configuration; +using Akka.Event; +using Microsoft.Extensions.Logging; + +namespace Akka.Hosting.Logging +{ + public static class AkkaLoggerFactoryExtensions + { + public static AkkaConfigurationBuilder WithLoggerFactory(this AkkaConfigurationBuilder builder) + { + return builder.AddHocon("akka.loggers = [\"Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting\"]"); + } + + public static AkkaConfigurationBuilder AddLoggerFactory(this AkkaConfigurationBuilder builder) + { + var loggers = builder.Configuration.HasValue + ? builder.Configuration.Value.GetStringList("akka.loggers") + : new List(); + + if(loggers.Count == 0) + loggers.Add("Akka.Event.DefaultLogger"); + + loggers.Add("Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting"); + return builder.AddHocon($"akka.loggers = [{string.Join(", ", loggers.Select(s => $"\"{s}\""))}]"); + } + + } +} \ No newline at end of file diff --git a/src/Akka.Hosting/Logging/LoggerFactoryLogger.cs b/src/Akka.Hosting/Logging/LoggerFactoryLogger.cs new file mode 100644 index 00000000..3251400c --- /dev/null +++ b/src/Akka.Hosting/Logging/LoggerFactoryLogger.cs @@ -0,0 +1,135 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2022 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using Akka.Actor; +using Akka.Configuration; +using Akka.DependencyInjection; +using Akka.Dispatch; +using Akka.Event; +using Microsoft.Extensions.Logging; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Akka.Hosting.Logging +{ + public class LoggerFactoryLogger: ActorBase, IRequiresMessageQueue + { + public const string DefaultTimeStampFormat = "yy/MM/dd-HH:mm:ss.ffff"; + private const string DefaultMessageFormat = "[{{Timestamp:{0}}}][{{SourceContext}}][{{LogSource}}][{{ActorPath}}][{{Thread:0000}}]: {{Message}}"; + private static readonly Event.LogLevel[] AllLogLevels = Enum.GetValues(typeof(Event.LogLevel)).Cast().ToArray(); + + private readonly ConcurrentDictionary _loggerCache = new ConcurrentDictionary(); + private readonly ILoggingAdapter _log = Akka.Event.Logging.GetLogger(Context.System.EventStream, nameof(LoggerFactoryLogger)); + private ILoggerFactory _loggerFactory; + private readonly string _messageFormat; + + public LoggerFactoryLogger() + { + _messageFormat = string.Format(DefaultMessageFormat, DefaultTimeStampFormat); + } + + protected override void PostStop() + { + _log.Info($"{nameof(LoggerFactoryLogger)} stopped"); + } + + protected override bool Receive(object message) + { + switch (message) + { + case InitializeLogger _: + var resolver = DependencyResolver.For(Context.System); + _loggerFactory = resolver.Resolver.GetService(); + if (_loggerFactory == null) + throw new ConfigurationException("Could not find any ILoggerFactory service inside ServiceProvider"); + + _log.Info($"{nameof(LoggerFactoryLogger)} started"); + Sender.Tell(new LoggerInitialized()); + return true; + + case LogEvent logEvent: + Log(logEvent, Sender.Path); + return true; + + default: + return false; + } + } + + private void Log(LogEvent log, ActorPath path) + { + var logger = _loggerCache.GetOrAdd(log.LogClass, type => _loggerFactory.CreateLogger(type)); + var message = GetMessage(log.Message); + logger.Log(GetLogLevel(log.LogLevel()), log.Cause, _messageFormat, GetArgs(log, path, message)); + } + + private static object[] GetArgs(LogEvent log, ActorPath path, object message) + => new []{ log.Timestamp, log.LogClass.FullName, log.LogSource, path, log.Thread.ManagedThreadId, message }; + + private static object GetMessage(object obj) + { + try + { + return obj is LogMessage m ? string.Format(m.Format, m.Args) : obj; + } + catch (Exception ex) + { + // Formatting/ToString error handling + var sb = new StringBuilder("Exception while recording log: ") + .Append(ex.Message) + .Append(' '); + switch (obj) + { + case LogMessage msg: + var args = msg.Args.Select(o => + { + try + { + return o.ToString(); + } + catch(Exception e) + { + return $"{o.GetType()}.ToString() throws {e.GetType()}: {e.Message}"; + } + }); + sb.Append($"Format: [{msg.Format}], Args: [{string.Join(",", args)}]."); + break; + case string str: + sb.Append($"Message: [{str}]."); + break; + default: + sb.Append($"Failed to invoke {obj.GetType()}.ToString()."); + break; + } + + sb.AppendLine(" Please take a look at the logging call where this occurred and fix your format string."); + sb.Append(ex); + return sb.ToString(); + } + } + + private static LogLevel GetLogLevel(Event.LogLevel level) + { + switch (level) + { + case Event.LogLevel.DebugLevel: + return LogLevel.Debug; + case Event.LogLevel.InfoLevel: + return LogLevel.Information; + case Event.LogLevel.WarningLevel: + return LogLevel.Warning; + case Event.LogLevel.ErrorLevel: + return LogLevel.Warning; + default: + // Should never reach this code path + return LogLevel.Error; + } + } + } +} \ No newline at end of file diff --git a/src/Examples/Akka.Hosting.LoggingDemo/Akka.Hosting.LoggingDemo.csproj b/src/Examples/Akka.Hosting.LoggingDemo/Akka.Hosting.LoggingDemo.csproj new file mode 100644 index 00000000..036ab85a --- /dev/null +++ b/src/Examples/Akka.Hosting.LoggingDemo/Akka.Hosting.LoggingDemo.csproj @@ -0,0 +1,12 @@ + + + net6.0 + enable + enable + false + + + + + + diff --git a/src/Examples/Akka.Hosting.LoggingDemo/Echo.cs b/src/Examples/Akka.Hosting.LoggingDemo/Echo.cs new file mode 100644 index 00000000..9065b98c --- /dev/null +++ b/src/Examples/Akka.Hosting.LoggingDemo/Echo.cs @@ -0,0 +1,3 @@ +namespace Akka.Hosting.LoggingDemo; + +public struct Echo{} \ No newline at end of file diff --git a/src/Examples/Akka.Hosting.LoggingDemo/Program.cs b/src/Examples/Akka.Hosting.LoggingDemo/Program.cs new file mode 100644 index 00000000..80c0f1b4 --- /dev/null +++ b/src/Examples/Akka.Hosting.LoggingDemo/Program.cs @@ -0,0 +1,43 @@ +using Akka.Hosting; +using Akka.Actor; +using Akka.Actor.Dsl; +using Akka.Cluster.Hosting; +using Akka.Event; +using Akka.Hosting.Logging; +using Akka.Hosting.LoggingDemo; +using Akka.Remote.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAkka("MyActorSystem", (configurationBuilder, serviceProvider) => +{ + configurationBuilder + .AddHocon("akka.loglevel = DEBUG") + .WithLoggerFactory() + .WithRemoting("localhost", 8110) + .WithClustering(new ClusterOptions(){ Roles = new[]{ "myRole" }, + SeedNodes = new[]{ Address.Parse("akka.tcp://MyActorSystem@localhost:8110")}}) + .WithActors((system, registry) => + { + var echo = system.ActorOf(act => + { + act.ReceiveAny((o, context) => + { + Logging.GetLogger(context.System, "echo").Info($"Actor received {o}"); + context.Sender.Tell($"{context.Self} rcv {o}"); + }); + }, "echo"); + registry.TryRegister(echo); // register for DI + }); +}); + +var app = builder.Build(); + +app.MapGet("/", async (context) => +{ + var echo = context.RequestServices.GetRequiredService().Get(); + var body = await echo.Ask(context.TraceIdentifier, context.RequestAborted).ConfigureAwait(false); + await context.Response.WriteAsync(body); +}); + +app.Run(); \ No newline at end of file diff --git a/src/Examples/Akka.Hosting.LoggingDemo/appsettings.Development.json b/src/Examples/Akka.Hosting.LoggingDemo/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/src/Examples/Akka.Hosting.LoggingDemo/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Examples/Akka.Hosting.LoggingDemo/appsettings.json b/src/Examples/Akka.Hosting.LoggingDemo/appsettings.json new file mode 100644 index 00000000..d60ba0df --- /dev/null +++ b/src/Examples/Akka.Hosting.LoggingDemo/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Akka": "Debug" + } + }, + "AllowedHosts": "*" +}