Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customizable ILogMessageFormatter across entire ActorSystem #6413

Merged
merged 10 commits into from
Feb 20, 2023
67 changes: 62 additions & 5 deletions docs/articles/utilities/logging.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
uid: logging
title: Logging
title: Logging in Akka.NET
---

# Logging

For more info see real Akka's documentation: <http://doc.akka.io/docs/akka/2.0/scala/logging.html>
> ![NOTE]
> For information on how to use Serilog with Akka.NET, we have a dedicated page for that: "[Using Serilog for Akka.NET Logging](xref:serilog)."

## How to Log

Expand Down Expand Up @@ -52,13 +53,13 @@ name into the `akka.stdout-logger-class` HOCON settings.
> Note that `MinimalLogger` are __NOT__ interchangeable with other Akka.NET loggers and there can
> only be one `MinimalLogger` registered with the `ActorSystem` in the HOCON settings.

## Contrib Loggers
## Third Party Loggers

These loggers are also available as separate nuget packages

* __Akka.Logger.slf4net__ which logs using [slf4net](https://github.com/englishtown/slf4net)
* __Akka.Logger.Serilog__ which logs using [serilog](http://serilog.net/). See [Detailed instructions on using Serilog](xref:serilog).
* __Akka.Logger.NLog__ which logs using [NLog](http://nlog-project.org/)
* __Microsoft.Extensions.Logging__ - which is [built into Akka.Hosting](https://github.com/akkadotnet/Akka.Hosting#microsoftextensionslogging-integration).

Note that you need to modify the config as explained below.

Expand All @@ -84,6 +85,62 @@ akka {
}
```

Or using [Akka.Hosting](https://github.com/akkadotnet/Akka.Hosting), you can configure loggers programmatically using strongly typed references to the underlying logging classes:

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

// Example: Add the default logger
// NOTE: You can also use setup.AddLogger<DefaultLogger>();
setup.AddDefaultLogger();

// Example: Add the ILoggerFactory logger
// NOTE:
// - You can also use setup.AddLogger<LoggerFactoryLogger>();
// - To use a specific ILoggerFactory instance, you can use setup.AddLoggerFactory(myILoggerFactory);
setup.AddLoggerFactory();

// Example: Adding a serilog logger
setup.AddLogger<SerilogLogger>();
})
.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
});
});
```

### Customizing the `ILogMessageFormatter`

A new feature introduced in [Akka.NET v1.5](xref:akkadotnet-v15-whats-new), you now have the ability to customize the `ILogMessageFormatter` - the component responsible for formatting output written to all `Logger` implementations in Akka.NET.

The primary use case for this is supporting semantic logging across the board in your user-defined actors, which is something that [Akka.Logger.Serilog](xref:serilog) supports quite well.

However, maybe there are certain pieces of data you want to have injected into all of the log messages produced by Akka.NET internally - that's the sort of thing you can accomplish by customizing the `ILogMessageFormatter`:

[!code-csharp[CustomLogMessageFormatter](../../../src/core/Akka.Tests/Loggers/CustomLogFormatterSpec.cs?name=CustomLogFormatter)]

This class will be responsible for formatting all log messages when they're written out to your configured sinks - once we configure it in HOCON using the `akka.logger-formatter` setting:

[!code-csharp[CustomLogMessageFormatter](../../../src/core/Akka.Tests/Loggers/CustomLogFormatterSpec.cs?name=CustomLogFormatterConfig)]

## Logging Unhandled Messages

It is possible to configure akka so that Unhandled messages are logged as Debug log events for debug purposes. This can be achieved using the following configuration setting:
Expand Down Expand Up @@ -113,7 +170,7 @@ akka {
}
```

## Logging All Receive'd Messages
## Logging All Received Messages

It is possible to log all Receive'd messages, usually for debug purposes. This can be achieved by implementing the ILogReceive interface:

Expand Down
53 changes: 31 additions & 22 deletions docs/articles/utilities/serilog.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
uid: serilog
title: Serilog
title: Using Serilog for Akka.NET Logging
---

# Using Serilog
# Using Serilog for Akka.NET Logging

## Setup

Expand Down Expand Up @@ -87,7 +87,28 @@ var log = Context.GetLogger();
log.Info("The value is {Counter}", counter);
```

## Extensions
## Enabling Semantic Logging in Akka.NET v1.5+

In order for logging statements like below to be parsed correctly:

```csharp
var log = Context.GetLogger();
...
log.Info("The value is {Counter}", counter);
```

You need to enable the `Akka.Logger.Serilog.SerilogLogMessageFormatter` across your entire `ActorSystem` - this will replace Akka.NET's default `ILogMessageFormatter` with Serilog's.

You can accomplish this by setting the `akka.logger-formatter` setting like below:

```hocon
akka.logger-formatter="Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog"
```

## Extensions for Akka.NET v1.4 and Older

> ![IMPORTANT]
> This methodology is obsolete as of [Akka.NET v1.5](xref:akkadotnet-v15-whats-new), but we're leaving it documented for legacy purposes.

The package __Akka.Logger.Serilog__ also includes the extension method `ForContext()` for `ILoggingAdapter` (the object returned by `Context.GetLogger()`). This is analogous to Serilog's `ForContext()` but instead of returning a Serilog `ILogger` it returns an Akka.NET `ILoggingAdapter`. This instance acts as contextual logger that will attach a property to all events logged through it.

Expand Down Expand Up @@ -133,26 +154,14 @@ There are few properties that one can use in their `OutputTemplate` for logger c

## HOCON Configuration

In order to be able to change log level without the need to recompile, we need to employ some sort of application configuration. To use Serilog via HOCON configuration, add the following to the __App.config__ of the project.

```xml
<configSections>
<section name="akka" type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" />
</configSections>

...

<akka>
<hocon>
<![CDATA[
akka {
loglevel=INFO,
loggers=["Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog"]
}
]]>
</hocon>
</akka>
In order to be able to change log level without the need to recompile, we need to employ some sort of application configuration. To use Serilog via HOCON configuration, add the following to the HOCON of the project.

```hocon
akka {
loglevel=INFO,
loggers=["Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog"]
logger-formatter="Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog"
}
```

The code can then be updated as follows removing the inline HOCON from the actor system creation code. Note in the following example, if a minimum level is not specified, Information level events and higher will be processed. Please read the documentation for [Serilog](https://serilog.net/) configuration for more details on this. It is also possible to move serilog configuration to the application configuration, for example if using a rolling log file sink, again, browsing the serilog documentation is the best place for details on that feature.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ namespace Akka.Actor
public int LogDeadLetters { get; }
public bool LogDeadLettersDuringShutdown { get; }
public System.TimeSpan LogDeadLettersSuspendDuration { get; }
public Akka.Event.ILogMessageFormatter LogFormatter { get; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposed the ILogMessageFormatter on the Settings class.

public string LogLevel { get; }
public bool LogSerializerOverrideOnStart { get; }
public bool LoggerAsyncStart { get; }
Expand Down Expand Up @@ -3000,7 +3001,7 @@ namespace Akka.Event
public Akka.Actor.IActorRef Sender { get; }
public override string ToString() { }
}
public class BusLogging : Akka.Event.LoggingAdapterBase
public sealed class BusLogging : Akka.Event.LoggingAdapterBase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed BusLogging and I'm considering making the constructor private in a subsequent PR so it can't be new'd up any longer - and will instead have to be populated by the ActorSystem. Considering doing that in order to enforce that configurable components like the ILogMessageFormatter and others are injected consistently across all call sites.

{
public BusLogging(Akka.Event.LoggingBus bus, string logSource, System.Type logClass, Akka.Event.ILogMessageFormatter logMessageFormatter) { }
public override bool IsDebugEnabled { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,7 @@ namespace Akka.Actor
public int LogDeadLetters { get; }
public bool LogDeadLettersDuringShutdown { get; }
public System.TimeSpan LogDeadLettersSuspendDuration { get; }
public Akka.Event.ILogMessageFormatter LogFormatter { get; }
public string LogLevel { get; }
public bool LogSerializerOverrideOnStart { get; }
public bool LoggerAsyncStart { get; }
Expand Down Expand Up @@ -3005,7 +3006,7 @@ namespace Akka.Event
public Akka.Actor.IActorRef Sender { get; }
public override string ToString() { }
}
public class BusLogging : Akka.Event.LoggingAdapterBase
public sealed class BusLogging : Akka.Event.LoggingAdapterBase
{
public BusLogging(Akka.Event.LoggingBus bus, string logSource, System.Type logClass, Akka.Event.ILogMessageFormatter logMessageFormatter) { }
public override bool IsDebugEnabled { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ namespace Akka.Actor
public int LogDeadLetters { get; }
public bool LogDeadLettersDuringShutdown { get; }
public System.TimeSpan LogDeadLettersSuspendDuration { get; }
public Akka.Event.ILogMessageFormatter LogFormatter { get; }
public string LogLevel { get; }
public bool LogSerializerOverrideOnStart { get; }
public bool LoggerAsyncStart { get; }
Expand Down Expand Up @@ -3000,7 +3001,7 @@ namespace Akka.Event
public Akka.Actor.IActorRef Sender { get; }
public override string ToString() { }
}
public class BusLogging : Akka.Event.LoggingAdapterBase
public sealed class BusLogging : Akka.Event.LoggingAdapterBase
{
public BusLogging(Akka.Event.LoggingBus bus, string logSource, System.Type logClass, Akka.Event.ILogMessageFormatter logMessageFormatter) { }
public override bool IsDebugEnabled { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,9 @@ private GraphInterpreter GetInterpreter()
}, _settings.IsFuzzingMode, Self);
}

private BusLogging GetLogger()
private ILoggingAdapter GetLogger()
{
return new BusLogging(Materializer.System.EventStream, Self.ToString(), typeof(GraphInterpreterShell), DefaultLogMessageFormatter.Instance);
return new BusLogging(Materializer.System.EventStream, Self.ToString(), typeof(GraphInterpreterShell), Materializer.System.Settings.LogFormatter);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/core/Akka.Streams/Implementation/Fusing/Ops.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2949,7 +2949,7 @@ public override void PreStart()
try
{
var materializer = ActorMaterializerHelper.Downcast(Materializer);
_log = new BusLogging(materializer.System.EventStream, _stage._name, GetType(), DefaultLogMessageFormatter.Instance);
_log = new BusLogging(materializer.System.EventStream, _stage._name, GetType(), materializer.System.Settings.LogFormatter);
}
catch (Exception ex)
{
Expand Down
1 change: 1 addition & 0 deletions src/core/Akka.Tests/Configuration/ConfigurationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public void The_default_configuration_file_contain_all_configuration_properties(
settings.LogDeadLetters.ShouldBe(10);
settings.LogDeadLettersDuringShutdown.ShouldBeFalse();
settings.LogDeadLettersSuspendDuration.ShouldBe(TimeSpan.FromMinutes(5));
settings.LogFormatter.Should().BeOfType<DefaultLogMessageFormatter>();

settings.ProviderClass.ShouldBe(typeof (LocalActorRefProvider).FullName);
settings.SupervisorStrategyClass.ShouldBe(typeof (DefaultSupervisorStrategy).FullName);
Expand Down
78 changes: 78 additions & 0 deletions src/core/Akka.Tests/Loggers/CustomLogFormatterSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//-----------------------------------------------------------------------
// <copyright file="CustomLogFormatterSpec.cs" company="Akka.NET Project">
// Copyright (C) 2009-2023 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Akka.Configuration;
using Akka.Event;
using Akka.TestKit;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;

namespace Akka.Tests.Loggers
{
public class CustomLogFormatterSpec : AkkaSpec
{
// <CustomLogFormatter>
private class CustomLogFormatter : ILogMessageFormatter
{
public string Format(string format, params object[] args)
{
return string.Format("Custom: " + format, args);
}

public string Format(string format, IEnumerable<object> args)
{
return string.Format("Custom: " + format, args.ToArray());
}
}
// </CustomLogFormatter>

// <CustomLogFormatterConfig>
public static readonly Config Configuration = "akka.logger-formatter = \"Akka.Tests.Loggers.CustomLogFormatterSpec+CustomLogFormatter, Akka.Tests\"";
// </CustomLogFormatterConfig>

public CustomLogFormatterSpec(ITestOutputHelper output) : base(Configuration, output)
{
}

[Fact]
public async Task ShouldUseValidCustomLogFormatter()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validates that custom ILogMessageFormatter output actually makes it into the output events

{
// arrange
var probe = CreateTestProbe();
Sys.EventStream.Subscribe(probe.Ref, typeof(Error));

// act
Sys.Log.Error("This is a test {0}", 1); // formatters aren't used when we're logging const strings

// assert
var msg = await probe.ExpectMsgAsync<Error>();
msg.Message.Should().BeAssignableTo<LogMessage>();
msg.ToString().Should().Contain("Custom: This is a test 1");

await EventFilter.Error(contains: "Custom").ExpectOneAsync(() =>
{
Sys.Log.Error("This is a test {0}", 1);
return Task.CompletedTask;
});
}

[Fact]
public async Task ShouldDetectCustomLogFormatterOutputInEventFilter()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validates that the TestKit's EventFilter can also pick up custom formatter output.

{
// the EventFilter filters on fully formatted output
await EventFilter.Error(contains: "Custom").ExpectOneAsync(() =>
{
Sys.Log.Error("This is a test {0}", 1);
return Task.CompletedTask;
});
}
}
}
2 changes: 1 addition & 1 deletion src/core/Akka/Actor/Internal/ActorSystemImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ private void ConfigureProvider()

private void ConfigureLoggers()
{
_log = new BusLogging(_eventStream, "ActorSystem(" + _name + ")", GetType(), DefaultLogMessageFormatter.Instance);
_log = new BusLogging(_eventStream, "ActorSystem(" + _name + ")", GetType(), _settings.LogFormatter);
}

private void ConfigureDispatchers()
Expand Down