diff --git a/README.md b/README.md index 46ee303..34d1dbb 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,51 @@ var log = Context.GetLogger(); // correct log.Info("My boss makes me use {semantic} logging", "semantic"); // serilog semantic logging format ``` +or + +```csharp +var log = MyActorSystem.GetLogger(myContextObject); // correct +log.Info("My boss makes me use {semantic} logging", "semantic"); // serilog semantic logging format +``` + +or +```csharp +var log = MyActorSystem.GetLogger(contextName, contextType); // correct +log.Info("My boss makes me use {semantic} logging", "semantic"); // serilog semantic logging format +``` + This will allow all logging events to be consumed anywhere inside the `ActorSystem`, including places like the Akka.NET TestKit, without throwing `FormatException`s when they encounter semantic logging syntax outside of the `SerilogLogger`. +### Adding Property Enricher To Your Logs + +#### Default Properties +You can add property enrichers to the logging adapter that will be added to all logging calls to that logging adapter. + +```csharp +var log = Context.GetLogger() + .ForContext("Address", "No. 4 Privet Drive") + .ForContext("Town", "Little Whinging") + .ForContext("County", "Surrey") + .ForContext("Country", "England"); +log.Info("My boss makes me use {Semantic} logging", "semantic"); +``` + +All logging done using the `log` `ILoggingAdapter` instance will append "Address", "Town", "County", and "Country" properties into the Serilog log. + +#### One-off Properties + +You can add one-off property to a single log message by appending `PropertyEnricher` instances at the end of your logging calls. + +```csharp +var log = Context.GetLogger(); +log.Info( + "My boss makes me use {Semantic} logging", "semantic", + new PropertyEnricher("County", "Surrey"), + new PropertyEnricher("Country", "England")); +``` + +This log entry will have "County" and "Country" properties added to it. + ## Building this solution To run the build script associated with this solution, execute the following: diff --git a/src/Akka.Logger.Serilog.Tests/ForContextSpecs.cs b/src/Akka.Logger.Serilog.Tests/ForContextSpecs.cs index 93d7244..334361e 100644 --- a/src/Akka.Logger.Serilog.Tests/ForContextSpecs.cs +++ b/src/Akka.Logger.Serilog.Tests/ForContextSpecs.cs @@ -9,6 +9,7 @@ using FluentAssertions; using Serilog; using Serilog.Core; +using Serilog.Core.Enrichers; using Serilog.Events; using Xunit; using Xunit.Abstractions; @@ -33,7 +34,57 @@ public ForContextSpecs(ITestOutputHelper helper) : base(Config, output: helper) var logSource = Sys.Name; var logClass = typeof(ActorSystem); - _loggingAdapter = new SerilogLoggingAdapter(Sys.EventStream, logSource, logClass); + _loggingAdapter = Sys.GetLogger(logSource, logClass); + } + + [Fact] + public void ShouldLogMessageWithContextProperty() + { + var context = _loggingAdapter + .ForContext("Address", "No. 4 Privet Drive") + .ForContext("Town", "Little Whinging") + .ForContext("County", "Surrey") + .ForContext("Country", "England"); + + _sink.Clear(); + AwaitCondition(() => _sink.Writes.Count == 0); + + context.Info("Hi {Person}", "Harry Potter"); + AwaitCondition(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + logEvent.Level.Should().Be(LogEventLevel.Information); + logEvent.RenderMessage().Should().Contain("Hi \"Harry Potter\""); + logEvent.Properties.Should().ContainKeys("Person", "Address", "Town", "County", "Country"); + logEvent.Properties["Person"].ToString().Should().Be("\"Harry Potter\""); + logEvent.Properties["Address"].ToString().Should().Be("\"No. 4 Privet Drive\""); + logEvent.Properties["Town"].ToString().Should().Be("\"Little Whinging\""); + logEvent.Properties["County"].ToString().Should().Be("\"Surrey\""); + logEvent.Properties["Country"].ToString().Should().Be("\"England\""); + } + + [Fact] + public void ShouldLogMessageWithContextPropertyAndPropertyEnricher() + { + var context = _loggingAdapter + .ForContext("Address", "No. 4 Privet Drive") + .ForContext("Town", "Little Whinging"); + + _sink.Clear(); + AwaitCondition(() => _sink.Writes.Count == 0); + + context.Info("Hi {Person}", "Harry Potter", new PropertyEnricher("County", "Surrey"), new PropertyEnricher("Country", "England")); + AwaitCondition(() => _sink.Writes.Count == 1); + + _sink.Writes.TryDequeue(out var logEvent).Should().BeTrue(); + logEvent.Level.Should().Be(LogEventLevel.Information); + logEvent.RenderMessage().Should().Contain("Hi \"Harry Potter\""); + logEvent.Properties.Should().ContainKeys("Person", "Address", "Town", "County", "Country"); + logEvent.Properties["Person"].ToString().Should().Be("\"Harry Potter\""); + logEvent.Properties["Address"].ToString().Should().Be("\"No. 4 Privet Drive\""); + logEvent.Properties["Town"].ToString().Should().Be("\"Little Whinging\""); + logEvent.Properties["County"].ToString().Should().Be("\"Surrey\""); + logEvent.Properties["Country"].ToString().Should().Be("\"England\""); } [Fact] diff --git a/src/Akka.Logger.Serilog/SerilogLogger.cs b/src/Akka.Logger.Serilog/SerilogLogger.cs index 4b8c2d6..006cd7a 100644 --- a/src/Akka.Logger.Serilog/SerilogLogger.cs +++ b/src/Akka.Logger.Serilog/SerilogLogger.cs @@ -29,13 +29,22 @@ public class SerilogLogger : ReceiveActor, IRequiresMessageQueue a is not PropertyEnricher).ToArray() ?? new[] { message }; + // Unwrap SerilogPayload + if (message is SerilogPayload payload) + message = payload.Message; + + return message is LogMessage logMessage + ? logMessage.Parameters().Where(a => a is not PropertyEnricher).ToArray() + : new[] { message }; } private static ILogger GetLogger(LogEvent logEvent) { @@ -46,14 +55,20 @@ private static ILogger GetLogger(LogEvent logEvent) { .ForContext("LogSource", logEvent.LogSource) .ForContext("Thread", logEvent.Thread.ManagedThreadId.ToString("0000")); - if (logEvent.Message is SerilogPayload logMessage) + if (logEvent.Message is SerilogPayload serilogPayload) { - logger = logMessage.Enrichers.OfType().Aggregate(logger, (current, enricher) => current.ForContext(enricher)); + var enrichers = serilogPayload.Enrichers.ToList(); + if (serilogPayload.Message is LogMessage logMessage) + enrichers.AddRange(logMessage.Parameters().OfType()); + if (enrichers.Count > 0) + logger = logger.ForContext(enrichers); } if (logEvent.Message is LogMessage message) { - logger = message.Parameters().Where(a => a is ILogEventEnricher).Aggregate(logger, (current, enricher) => current.ForContext((ILogEventEnricher)enricher)); + var enrichers = message.Parameters().OfType().ToList(); + if (enrichers.Count > 0) + logger = logger.ForContext(enrichers); } return logger; diff --git a/src/Akka.Logger.Serilog/SerilogLoggingAdapterExtensions.cs b/src/Akka.Logger.Serilog/SerilogLoggingAdapterExtensions.cs index e406993..13824b3 100644 --- a/src/Akka.Logger.Serilog/SerilogLoggingAdapterExtensions.cs +++ b/src/Akka.Logger.Serilog/SerilogLoggingAdapterExtensions.cs @@ -31,5 +31,21 @@ public static ILoggingAdapter GetLogger(this IActorContext context) return new SerilogLoggingAdapter(context.System.EventStream, logSource, logClass); } + + public static ILoggingAdapter GetLogger(this ActorSystem system, object logSourceObj) + where T : class, ILoggingAdapter + { + if (logSourceObj is null) + throw new ArgumentNullException(nameof(logSourceObj)); + + var logSource = LogSource.Create(logSourceObj, system); + return new SerilogLoggingAdapter(system.EventStream, logSource.Source, logSource.Type); + } + + public static ILoggingAdapter GetLogger(this ActorSystem system, string logSource, Type logType) + where T : class, ILoggingAdapter + { + return new SerilogLoggingAdapter(system.EventStream, logSource, logType); + } } } \ No newline at end of file