Skip to content

Commit

Permalink
Add ILoggerFactory logger support (#72)
Browse files Browse the repository at this point in the history
* Add ILoggerFactory logger support

* Rename WithLoggerFactorLogger to WithLoggerFactory

* Add ILoggerFactory sample project

* Make `Akka.Hosting.LoggingDemo.csproj` not packable

Co-authored-by: Aaron Stannard <[email protected]>
  • Loading branch information
Arkatufus and Aaronontheweb authored Jul 18, 2022
1 parent 1352d42 commit c969ceb
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Akka.Hosting.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
38 changes: 38 additions & 0 deletions src/Akka.Hosting/Logging/AkkaLoggerFactoryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// -----------------------------------------------------------------------
// <copyright file="AkkaLoggerFactoryExtensions.cs" company="Akka.NET Project">
// Copyright (C) 2013-2022 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

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<string>();

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}\""))}]");
}

}
}
135 changes: 135 additions & 0 deletions src/Akka.Hosting/Logging/LoggerFactoryLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// -----------------------------------------------------------------------
// <copyright file="LoggerFactoryLogger.cs" company="Akka.NET Project">
// Copyright (C) 2013-2022 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

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<ILoggerMessageQueueSemantics>
{
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<Event.LogLevel>().ToArray();

private readonly ConcurrentDictionary<Type, ILogger> _loggerCache = new ConcurrentDictionary<Type, ILogger>();
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<ILoggerFactory>();
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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Akka.Cluster.Hosting\Akka.Cluster.Hosting.csproj" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions src/Examples/Akka.Hosting.LoggingDemo/Echo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Akka.Hosting.LoggingDemo;

public struct Echo{}
43 changes: 43 additions & 0 deletions src/Examples/Akka.Hosting.LoggingDemo/Program.cs
Original file line number Diff line number Diff line change
@@ -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>(echo); // register for DI
});
});

var app = builder.Build();

app.MapGet("/", async (context) =>
{
var echo = context.RequestServices.GetRequiredService<ActorRegistry>().Get<Echo>();
var body = await echo.Ask<string>(context.TraceIdentifier, context.RequestAborted).ConfigureAwait(false);
await context.Response.WriteAsync(body);
});

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
11 changes: 11 additions & 0 deletions src/Examples/Akka.Hosting.LoggingDemo/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Akka": "Debug"
}
},
"AllowedHosts": "*"
}

0 comments on commit c969ceb

Please sign in to comment.