Skip to content

Commit

Permalink
Logging: added documentation and end to end specs for Serilog formatt…
Browse files Browse the repository at this point in the history
…ing (#451)

* added documentation and end to end specs for Serilog

* added serilog reference to table of contents

* added API approvals

* add default Akka.Persistence config to journal
  • Loading branch information
Aaronontheweb committed Apr 30, 2024
1 parent f0bbbaf commit f61d6a0
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 30 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ See the ["Introduction to Akka.Hosting - HOCON-less, "Pit of Success" Akka.NET R
- [Microsoft.Extensions.Logging Integration](#microsoftextensionslogging-integration)
* [Logger Configuration Support](#logger-configuration-support)
* [Microsoft.Extensions.Logging.ILoggerFactory Logging Support](#microsoftextensionsloggingiloggerfactory-logging-support)
* [Serilog Support](#serilog-support)
* [Microsoft.Extensions.Logging Log Event Filtering](#microsoftextensionslogging-log-event-filtering)

<a id="supported-packages"></a>
Expand Down Expand Up @@ -572,6 +573,48 @@ You can now use `ILoggerFactory` from Microsoft.Extensions.Logging as one of the

[Back to top](#akkahosting)

<a id="serilog-support"></a>
## Serilog Message Formatting Support

If you're interested in using [Akka.Logger.Serilog](https://github.com/akkadotnet/Akka.Logger.Serilog), you can set Akka.NET's default logger and log message formatter to allow for Serilog's semantic logging to be enabled by default:

```csharp
builder.Services.AddAkka("MyActorSystem", configurationBuilder =>
{
configurationBuilder
.ConfigureLoggers(setup =>
{
// Example: This sets the minimum log level
setup.LogLevel = LogLevel.DebugLevel;

// Example: Clear all loggers
setup.ClearLoggers();

// Add Serilog
setup.AddLogger<SerilogLogger>();

// use the default SerilogFormatter everywhere
setup.WithDefaultLogMessageFormatter<SerilogLogMessageFormatter>();
})
.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
});
});
```

This will eliminate the need to have to do `Context.GetLogger<SerilogLoggingAdapter>()` everywhere you want to use it.

[Back to top](#akkahosting)

<a id="microsoftextensionslogging-log-event-filtering"></a>
## Microsoft.Extensions.Logging Log Event Filtering

Expand Down
5 changes: 4 additions & 1 deletion build/_build.csproj.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f9fce829_002De6f4_002D4cb2_002D80f1_002D5497c44f51df/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
Expand All @@ -24,4 +26,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,13 @@ namespace Akka.Hosting
public Akka.Hosting.DebugOptions? DebugOptions { get; set; }
public bool LogConfigOnStart { get; set; }
public Akka.Event.LogLevel LogLevel { get; set; }
[System.Obsolete("Use the WithDefaultLogMessageFormatter<T> method instead")]
public System.Type LogMessageFormatter { get; set; }
public Akka.Hosting.LoggerConfigBuilder AddLogger<T>()
where T : Akka.Dispatch.IRequiresMessageQueue<Akka.Event.ILoggerMessageQueueSemantics> { }
public Akka.Hosting.LoggerConfigBuilder ClearLoggers() { }
public Akka.Hosting.LoggerConfigBuilder WithDefaultLogMessageFormatter<T>()
where T : Akka.Event.ILogMessageFormatter { }
}
public static class LoggingExtensions
{
Expand Down
1 change: 1 addition & 0 deletions src/Akka.Hosting.Tests/Akka.Hosting.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Akka.Logger.Serilog" Version="1.5.12.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Akka.TestKit.Xunit2" Version="$(AkkaVersion)" />
Expand Down
8 changes: 4 additions & 4 deletions src/Akka.Hosting.Tests/Logging/LogMessageFormatterSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public LogMessageFormatterSpec(ITestOutputHelper helper)
[Fact(DisplayName = "ILogMessageFormatter should transform log messages")]
public async Task TransformMessagesTest()
{
using var host = await SetupHost(typeof(TestLogMessageFormatter));
using var host = await SetupHost<TestLogMessageFormatter>();

try
{
Expand All @@ -59,11 +59,11 @@ public async Task TransformMessagesTest()
[Fact(DisplayName = "Invalid LogMessageFormatter property should throw")]
public async Task InvalidLogMessageFormatterThrowsTest()
{
await Awaiting(async () => await SetupHost(typeof(InvalidLogMessageFormatter)))
await Awaiting(async () => await SetupHost<InvalidLogMessageFormatter>())
.Should().ThrowAsync<ConfigurationException>().WithMessage("*must have an empty constructor*");
}

private async Task<IHost> SetupHost(Type formatter)
private async Task<IHost> SetupHost<TFormatter>() where TFormatter : ILogMessageFormatter
{
var host = new HostBuilder()
.ConfigureLogging(builder =>
Expand All @@ -79,7 +79,7 @@ private async Task<IHost> SetupHost(Type formatter)
{
setup.LogLevel = Event.LogLevel.DebugLevel;
setup.AddLoggerFactory();
setup.LogMessageFormatter = formatter;
setup.WithDefaultLogMessageFormatter<TFormatter>();
});
});
}).Build();
Expand Down
111 changes: 111 additions & 0 deletions src/Akka.Hosting.Tests/Logging/SerilogLoggerEnd2EndSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// -----------------------------------------------------------------------
// <copyright file="SerilogLoggerEnd2EndSpecs.cs" company="Akka.NET Project">
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Collections.Concurrent;
using Akka.Event;
using Akka.Logger.Serilog;
using FluentAssertions;
using Serilog;
using Serilog.Core;
using Xunit;
using Xunit.Abstractions;

namespace Akka.Hosting.Tests.Logging;

public class SerilogLoggerEnd2EndSpecs : TestKit.TestKit
{
/// <inheritdoc />
/// <summary>
/// Basic concurrent sink implementation for testing the final output from Serilog
/// </summary>
public sealed class TestSink : ILogEventSink
{
public ConcurrentQueue<Serilog.Events.LogEvent> Writes { get; private set; } = new();

private readonly ITestOutputHelper _output;
private int _count;

public TestSink() : this(null)
{
}

public TestSink(ITestOutputHelper output)
{
_output = output;
}


/// <summary>
/// Resets the contents of the queue
/// </summary>
public void Clear()
{
Writes.Clear();
}

public void Emit(Serilog.Events.LogEvent logEvent)
{
_count++;
_output?.WriteLine($"[{nameof(TestSink)}][{_count}]: {logEvent.RenderMessage()}");
Writes.Enqueue(logEvent);
}
}

private readonly TestSink _sink = new();

protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
Serilog.Log.Logger = new LoggerConfiguration()
.WriteTo.Sink(_sink)
.MinimumLevel.Information()
.CreateLogger();

builder.ConfigureLoggers(setup =>
{
setup.ClearLoggers();
setup.AddLogger<SerilogLogger>();
setup.LogLevel = Event.LogLevel.DebugLevel;
setup.WithDefaultLogMessageFormatter<SerilogLogMessageFormatter>();
});
}

[Theory]
[InlineData(Event.LogLevel.DebugLevel, "test case {0}", new object[] { 1 })]
[InlineData(Event.LogLevel.DebugLevel, "test case {myNum}", new object[] { 1 })]
[InlineData(Event.LogLevel.InfoLevel, "test case {myNum} {myStr}", new object[] { 1, "foo" })]
public void ShouldHandleSerilogFormats(LogLevel level, string formatStr, object[] args)
{
Sys.EventStream.Subscribe(TestActor, typeof(LogEvent));

var logWrite = () =>
{
Sys.Log.Log(level, formatStr, args);
var logEvent = ExpectMsg<LogEvent>();
logEvent.LogLevel().Should().Be(level);
logEvent.ToString().Should().NotBeEmpty();
};

logWrite.Should().NotThrow<FormatException>();
}

[Fact(Skip = "Does not work right now")]
public void ShouldHaveEnrichedContext()
{
Sys.EventStream.Subscribe(TestActor, typeof(LogEvent));

var contextedLogger = Sys.Log.ForContext("TestContext", "Testy");
_sink.Clear();
AwaitCondition(() => _sink.Writes.IsEmpty);

contextedLogger.Info("test case {0}", 1);
AwaitCondition(() => _sink.Writes.Count == 1);

_sink.Writes.TryDequeue(out var logEvent).Should().BeTrue();
logEvent!.Properties.ContainsKey("TestContext").Should().BeTrue();
}
}
14 changes: 13 additions & 1 deletion src/Akka.Hosting/LoggerConfigBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ internal LoggerConfigBuilder(AkkaConfigurationBuilder builder)

public DebugOptions? DebugOptions { get; set; }

[Obsolete("Use the WithDefaultLogMessageFormatter<T> method instead")]
public Type LogMessageFormatter
{
get => _logMessageFormatter;
Expand Down Expand Up @@ -83,6 +84,17 @@ public LoggerConfigBuilder ClearLoggers()
_loggers.Add(logger);
return this;
}

/// <summary>
/// Sets the formatter used by the logger
/// </summary>
public LoggerConfigBuilder WithDefaultLogMessageFormatter<T>() where T: ILogMessageFormatter
{
#pragma warning disable CS0618 // Type or member is obsolete
LogMessageFormatter = typeof(T);
#pragma warning restore CS0618 // Type or member is obsolete
return this;
}

/// <summary>
/// INTERNAL API
Expand Down Expand Up @@ -112,7 +124,7 @@ internal Config ToConfig()
return ConfigurationFactory.ParseString(sb.ToString());
}

private string ParseLogLevel(LogLevel logLevel)
private static string ParseLogLevel(LogLevel logLevel)
=> logLevel switch
{
LogLevel.DebugLevel => "Debug",
Expand Down
Loading

0 comments on commit f61d6a0

Please sign in to comment.