Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 29 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,34 @@ This is the Serilog integration plugin for Akka.NET. Please check out our [docum
Targets [Serilog 2.12.0](https://www.nuget.org/packages/Serilog/2.12.0).

### Semantic Logging Syntax
If you intend on using any of the Serilog semantic logging formats in your logging strings, __you need to use the SerilogLoggingAdapter__ inside your instrumented code or there could be elsewhere inside parts of your `ActorSystem`:
When using [Akka.Hosting](https://github.com/akkadotnet/Akka.Hosting) with `AddSerilogLogging()`, the `SerilogLogMessageFormatter` is automatically configured and semantic logging works with the standard `ILoggingAdapter`:

```csharp
var log = Context.GetLogger<SerilogLoggingAdapter>(); // correct
log.Info("My boss makes me use {semantic} logging", "semantic"); // serilog semantic logging format
var log = Context.GetLogger(); // standard ILoggingAdapter
log.Info("User {UserId} performed {Action}", userId, "login"); // semantic logging works
```

or
If you are configuring Akka.NET without Akka.Hosting, you need to set the `SerilogLogMessageFormatter` in your HOCON config:

```csharp
var log = MyActorSystem.GetLogger<SerilogLoggingAdapter>(myContextObject); // correct
log.Info("My boss makes me use {semantic} logging", "semantic"); // serilog semantic logging format
```hocon
akka.logger-formatter = "Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog"
```

or
### Adding Context Properties To Your Logs

As of Akka.NET 1.5.60, you can use the built-in `WithContext()` method on any `ILoggingAdapter` to add persistent properties to all log messages produced by that adapter. These properties automatically flow through to Serilog as structured properties.

```csharp
var log = MyActorSystem.GetLogger<SerilogLoggingAdapter>(contextName, contextType); // correct
log.Info("My boss makes me use {semantic} logging", "semantic"); // serilog semantic logging format
var log = Context.GetLogger()
.WithContext("TenantId", "TENANT-001")
.WithContext("CorrelationId", correlationId)
.WithContext("Region", "us-east-1");
log.Info("Processing request for {UserId}", userId);
```

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`.
All logging done using the `log` `ILoggingAdapter` instance will include "TenantId", "CorrelationId", and "Region" as Serilog properties, in addition to the "UserId" semantic template property.

`WithContext()` is part of core Akka.NET and works with all logging backends (Serilog, NLog, Microsoft.Extensions.Logging), not just Serilog.

### Log Filtering

Expand All @@ -49,7 +56,7 @@ var bootstrap = BootstrapSetup.Create()
```

**Use Serilog's native filtering when:**
- Filtering on Serilog enricher properties (e.g., `TenantId`, `CorrelationId` from `ForContext`)
- Filtering on Serilog enricher properties (e.g., `TenantId`, `CorrelationId` from `WithContext`)
- Using complex predicate logic
- Filtering on properties added via `LogContext.PushProperty()`

Expand All @@ -62,52 +69,25 @@ Log.Logger = new LoggerConfiguration()
.CreateLogger();
```

**Important limitation:** Serilog enrichers added via `ForContext()` or `LogContext.PushProperty()` are applied *after* Akka's LogFilter runs, so they cannot be filtered using Akka's LogFilter. Use Serilog's native `.Filter.ByExcluding()` for enricher-based filtering.
**Important limitation:** Context properties added via `WithContext()` or `LogContext.PushProperty()` are applied *after* Akka's LogFilter runs, so they cannot be filtered using Akka's LogFilter. Use Serilog's native `.Filter.ByExcluding()` for enricher-based filtering.

### Adding Property Enricher To Your Logs
### Deprecated: SerilogLoggingAdapter and ForContext()

#### 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<SerilogLoggingAdapter>()
.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");
```
> **Note:** `SerilogLoggingAdapter`, the `ForContext()` extension method, and `Context.GetLogger<SerilogLoggingAdapter>()` are deprecated as of Akka.Logger.Serilog 1.5.60. Use the standard `ILoggingAdapter` with `WithContext()` instead.

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.
If you are migrating from `ForContext()`:

```csharp
var log = Context.GetLogger<SerilogLoggingAdapter>();
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.

### Automatically Convert `ILoggingAdapter` into `SerilogLoggingAdapter`

As of Akka.Logger.Serilog v1.5.25, you can now do the following:
// Old (deprecated)
var log = Context.GetLogger<SerilogLoggingAdapter>()
.ForContext("TenantId", "TENANT-001");

```csharp
// New (recommended)
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");
.WithContext("TenantId", "TENANT-001");
```

And it will work without having to explicitly call `Context.GetLogger<SerilogLoggingAdapter>()` first.
The `ForContext()` and `SerilogLoggingAdapter` APIs will continue to work but will be removed in a future major version.

## Building this solution
To run the build script associated with this solution, execute the following:
Expand Down
22 changes: 22 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
#### 1.5.60 February 9 2026 ####

* [Update Akka.NET to 1.5.60](https://github.com/akkadotnet/akka.net/releases/tag/1.5.60)
* [Add WithContext() support and deprecate Serilog-specific ForContext()](https://github.com/akkadotnet/Akka.Logger.Serilog/pull/310)

This release adds support for Akka.NET 1.5.60's built-in `WithContext()` logging context enrichment API. Context properties set via `WithContext()` on any `ILoggingAdapter` now automatically flow through to Serilog as structured properties.

**Breaking Changes:**
- `SerilogLoggingAdapter` class is now marked `[Obsolete]` - use the standard `ILoggingAdapter` with `WithContext()` instead
- `ForContext()` extension method is now marked `[Obsolete]` - use `WithContext()` instead

**Migration:**
```csharp
// Old (deprecated)
var log = Context.GetLogger<SerilogLoggingAdapter>()
.ForContext("TenantId", "TENANT-001");

// New (recommended)
var log = Context.GetLogger()
.WithContext("TenantId", "TENANT-001");
```

#### 1.5.59 January 26 2026 ####

* [Update Akka.NET to 1.5.59](https://github.com/akkadotnet/akka.net/releases/tag/1.5.59)
Expand Down
158 changes: 158 additions & 0 deletions src/Akka.Logger.Serilog.Tests/WithContextSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Akka.Actor;
using Akka.Configuration;
using Akka.Event;
using FluentAssertions;
using Serilog;
using Serilog.Events;
using Xunit;
using Xunit.Abstractions;
using LogEvent = Serilog.Events.LogEvent;

namespace Akka.Logger.Serilog.Tests
{
/// <summary>
/// Tests for core Akka.NET WithContext() logging context enrichment
/// flowing through to Serilog log events.
/// </summary>
public class WithContextSpecs : IAsyncLifetime
{
public static readonly Config Config =
@"akka.loglevel = DEBUG
akka.loggers=[""Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog""]
akka.logger-formatter=""Akka.Logger.Serilog.SerilogLogMessageFormatter, Akka.Logger.Serilog""";

private readonly ITestOutputHelper _helper;
private readonly TestSink _sink;

private ActorSystem _sys;
private TestKit.Xunit2.TestKit _testKit;
private ILoggingAdapter _loggingAdapter;

public WithContextSpecs(ITestOutputHelper helper)
{
_helper = helper;
_sink = new TestSink(helper);

Log.Logger = new LoggerConfiguration()
.WriteTo.Sink(_sink)
.MinimumLevel.Debug()
.CreateLogger();
}

public Task InitializeAsync()
{
_sys = ActorSystem.Create("WithContextTestSystem", Config);
_testKit = new TestKit.Xunit2.TestKit(_sys, _helper);

// Use ActorSystem overload to get BusLogging with the configured SerilogLogMessageFormatter.
// The LoggingBus overload defaults to DefaultLogMessageFormatter which can't handle named templates.
// WithContext() also requires BusLogging (ContextLoggingAdapter delegates to BusLogging.LogWithContext).
_loggingAdapter = Logging.GetLogger(_sys, _sys.Name);

return Task.CompletedTask;
}

public async Task DisposeAsync()
{
_testKit.Shutdown();
await _sys.Terminate();
}

[Fact(DisplayName = "WithContext single property should appear in Serilog properties")]
public async Task WithContext_SingleProperty_AppearsInSerilogProperties()
{
_sink.Clear();

var contextLogger = _loggingAdapter.WithContext("TenantId", "TENANT-001");

await _testKit.AwaitAssertAsync(() =>
{
contextLogger.Info("Processing request");
var logEvent = GetMatchingLogEvent(e => e.Properties.ContainsKey("TenantId"));
logEvent.Should().NotBeNull("TenantId property should be present in Serilog log event");
logEvent!.Properties["TenantId"].ToString().Should().Be("\"TENANT-001\"");
});
}

[Fact(DisplayName = "WithContext multiple properties should all appear in Serilog properties")]
public async Task WithContext_MultipleProperties_AllAppear()
{
_sink.Clear();

var contextLogger = _loggingAdapter
.WithContext("TenantId", "TENANT-002")
.WithContext("CorrelationId", "CORR-123")
.WithContext("Region", "us-east-1");

await _testKit.AwaitAssertAsync(() =>
{
contextLogger.Info("Multi-context request");
var logEvent = GetMatchingLogEvent(e =>
e.Properties.ContainsKey("TenantId") &&
e.Properties.ContainsKey("CorrelationId") &&
e.Properties.ContainsKey("Region"));
logEvent.Should().NotBeNull("all three context properties should be present");
logEvent!.Properties["TenantId"].ToString().Should().Be("\"TENANT-002\"");
logEvent.Properties["CorrelationId"].ToString().Should().Be("\"CORR-123\"");
logEvent.Properties["Region"].ToString().Should().Be("\"us-east-1\"");
});
}

[Fact(DisplayName = "WithContext combined with semantic template should have both context and template properties")]
public async Task WithContext_CombinedWithSemanticTemplate_BothAppear()
{
_sink.Clear();

var contextLogger = _loggingAdapter.WithContext("TenantId", "TENANT-003");

await _testKit.AwaitAssertAsync(() =>
{
contextLogger.Info("User {UserId} performed {Action}", 42, "login");
var logEvent = GetMatchingLogEvent(e =>
e.Properties.ContainsKey("TenantId") &&
e.Properties.ContainsKey("UserId"));
logEvent.Should().NotBeNull("both context and template properties should be present");
logEvent!.Properties["TenantId"].ToString().Should().Be("\"TENANT-003\"");
logEvent.Properties["UserId"].ToString().Should().Be("42");
logEvent.Properties["Action"].ToString().Should().Be("\"login\"");
});
}

[Fact(DisplayName = "WithContext should not pollute unrelated log events")]
public async Task WithContext_DoesNotPolluteUnrelatedLogs()
{
_sink.Clear();

var contextLogger = _loggingAdapter.WithContext("SecretContext", "should-not-leak");
var plainLogger = _loggingAdapter;

await _testKit.AwaitAssertAsync(() =>
{
contextLogger.Info("Context message with marker {Marker}", "CTX");
plainLogger.Info("Plain message with marker {Marker}", "PLAIN");

var contextEvent = GetMatchingLogEvent(e =>
e.Properties.ContainsKey("Marker") &&
e.Properties["Marker"].ToString() == "\"CTX\"");
var plainEvent = GetMatchingLogEvent(e =>
e.Properties.ContainsKey("Marker") &&
e.Properties["Marker"].ToString() == "\"PLAIN\"");

contextEvent.Should().NotBeNull();
plainEvent.Should().NotBeNull();

contextEvent!.Properties.Should().ContainKey("SecretContext");
plainEvent!.Properties.Should().NotContainKey("SecretContext",
"context properties should not leak to loggers without that context");
});
}

private LogEvent? GetMatchingLogEvent(Func<LogEvent, bool> predicate)
{
return _sink.Writes.ToArray().FirstOrDefault(predicate);
}
}
}
11 changes: 11 additions & 0 deletions src/Akka.Logger.Serilog/SerilogLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ private static ILogger GetLogger(LogEvent logEvent) {
logger = logger.ForContext(enrichers);
}

// Add context properties from WithContext() - these come from LogEvent.ContextProperties
// via TryGetProperties(), which merges context + message template properties
if (logEvent.TryGetProperties(out var properties))
{
var contextEnrichers = properties
.Select(p => (ILogEventEnricher)new PropertyEnricher(p.Key, p.Value))
.ToList();
if (contextEnrichers.Count > 0)
logger = logger.ForContext(contextEnrichers);
}

return logger;
}

Expand Down
1 change: 1 addition & 0 deletions src/Akka.Logger.Serilog/SerilogLoggingAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public override string ToString()
/// <summary>
/// Serilog logging adapter.
/// </summary>
[Obsolete("Use the standard ILoggingAdapter with WithContext() instead. This class will be removed in a future version.")]
public class SerilogLoggingAdapter : LoggingAdapterBase
{
private readonly LoggingBus _bus;
Expand Down
21 changes: 15 additions & 6 deletions src/Akka.Logger.Serilog/SerilogLoggingAdapterExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using Akka.Actor;
using Akka.Event;

Expand All @@ -13,8 +13,10 @@ public static class SerilogLoggingAdapterExtensions
/// <param name="propertyName">The name of the property. Must be non-empty.</param>
/// <param name="value">The property value.</param>
/// <param name="destructureObjects">If true, the value will be serialized as a structured object if possible; if false, the object will be recorded as a scalar or simple array.</param>
[Obsolete("Use ILoggingAdapter.WithContext() instead. This method will be removed in a future version.")]
public static ILoggingAdapter ForContext(this ILoggingAdapter adapter, string propertyName, object value, bool destructureObjects = false)
{
#pragma warning disable CS0618 // Type or member is obsolete
if(adapter is SerilogLoggingAdapter customAdapter)
return customAdapter.SetContextProperty(propertyName, value, destructureObjects);

Expand All @@ -23,10 +25,11 @@ public static ILoggingAdapter ForContext(this ILoggingAdapter adapter, string pr
var enrichedAdapter = new SerilogLoggingAdapter(defaultAkkaAdapter.Bus, defaultAkkaAdapter.LogSource, defaultAkkaAdapter.LogClass);
return enrichedAdapter.SetContextProperty(propertyName, value, destructureObjects);
}

// log a warning if the adapter is not a SerilogLoggingAdapter or BusLogging
adapter.Warning($"Cannot enrich log event with property {propertyName} because the adapter is not a {typeof(SerilogLoggingAdapter)} or {typeof(BusLogging)}.");
return adapter;
#pragma warning restore CS0618
}

/// <summary>
Expand All @@ -40,23 +43,29 @@ public static ILoggingAdapter GetLogger<T>(this IActorContext context)
var logSource = context.Self.ToString();
var logClass = context.Props.Type;

#pragma warning disable CS0618 // Type or member is obsolete
return new SerilogLoggingAdapter(context.System.EventStream, logSource, logClass);
#pragma warning restore CS0618
}

public static ILoggingAdapter GetLogger<T>(this ActorSystem system, object logSourceObj)
where T : class, ILoggingAdapter
{
if (logSourceObj is null)
throw new ArgumentNullException(nameof(logSourceObj));

var logSource = LogSource.Create(logSourceObj, system);
#pragma warning disable CS0618 // Type or member is obsolete
return new SerilogLoggingAdapter(system.EventStream, logSource.Source, logSource.Type);
#pragma warning restore CS0618
}

public static ILoggingAdapter GetLogger<T>(this ActorSystem system, string logSource, Type logType)
where T : class, ILoggingAdapter
{
#pragma warning disable CS0618 // Type or member is obsolete
return new SerilogLoggingAdapter(system.EventStream, logSource, logType);
#pragma warning restore CS0618
}
}
}
}
Loading