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
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# serilog-enrichers-clientinfo [![NuGet](http://img.shields.io/nuget/v/Serilog.Enrichers.ClientInfo.svg?style=flat)](https://www.nuget.org/packages/Serilog.Enrichers.ClientInfo/) [![](https://img.shields.io/nuget/dt/Serilog.Enrichers.ClientInfo.svg?label=nuget%20downloads)](Serilog.Enrichers.ClientInfo)

Enrich logs with client IP, Correlation Id and HTTP request headers.
Enrich logs with client IP, Correlation Id, HTTP request headers, and user claims.

Install the _Serilog.Enrichers.ClientInfo_ [NuGet package](https://www.nuget.org/packages/Serilog.Enrichers.ClientInfo/)

Expand All @@ -19,6 +19,7 @@ Log.Logger = new LoggerConfiguration()
.Enrich.WithClientIp()
.Enrich.WithCorrelationId()
.Enrich.WithRequestHeader("Header-Name1")
.Enrich.WithUserClaims(ClaimTypes.NameIdentifier, ClaimTypes.Email)
// ...other configuration...
.CreateLogger();
```
Expand All @@ -35,6 +36,10 @@ or in `appsettings.json` file:
{
"Name": "WithRequestHeader",
"Args": { "headerName": "User-Agent"}
},
{
"Name": "WithUserClaims",
"Args": { "claimNames": ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] }
}
],
"WriteTo": [
Expand Down Expand Up @@ -178,6 +183,65 @@ Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:u3} {UserAgent} {Message:lj}{NewLine}{Exception}")
```

### UserClaims
The `UserClaims` enricher allows you to log specific user claim values from authenticated users. This is useful for tracking user-specific information in your logs.

#### Basic Usage
```csharp
using System.Security.Claims;

Log.Logger = new LoggerConfiguration()
.Enrich.WithUserClaims(ClaimTypes.NameIdentifier, ClaimTypes.Email)
...
```

or in `appsettings.json` file:
```json
{
"Serilog": {
"MinimumLevel": "Debug",
"Using": [ "Serilog.Enrichers.ClientInfo" ],
"Enrich": [
{
"Name": "WithUserClaims",
"Args": {
"claimNames": [
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
]
}
}
]
}
}
```

#### Features
- **Configurable Claims**: Specify which claims to log by providing claim names as parameters.
- **Null-Safe**: If a claim doesn't exist, it will be logged as `null` instead of throwing an error.
- **Authentication-Aware**: Only logs claims when the user is authenticated. If the user is not authenticated, no claim properties are added to the log.
- **Performance-Optimized**: Claim values are cached per request for better performance.

#### Example with Multiple Claims
```csharp
Log.Logger = new LoggerConfiguration()
.Enrich.WithUserClaims(
ClaimTypes.NameIdentifier,
ClaimTypes.Email,
ClaimTypes.Name,
ClaimTypes.Role)
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] User: {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier} {Message:lj}{NewLine}{Exception}")
...
```

#### Custom Claims
You can also log custom claim types:
```csharp
Log.Logger = new LoggerConfiguration()
.Enrich.WithUserClaims("tenant_id", "organization_id")
...
```

## Installing into an ASP.NET Core Web Application
You need to register the `IHttpContextAccessor` singleton so the enrichers have access to the requests `HttpContext` to extract client IP and client agent.
This is what your `Startup` class should contain in order for this enricher to work as expected:
Expand Down
2 changes: 1 addition & 1 deletion Serilog.Enrichers.ClientInfo.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<tags>serilog enrichers enricher ip correlation-id request header</tags>
<copyright>© 2025 Serilog Contributors</copyright>
<releaseNotes>
- Remove dependency on Microsoft.AspNetCore.App framework.
- Add user claims enrichment support.
</releaseNotes>
<dependencies>
<group targetFramework=".NET8.0">
Expand Down
75 changes: 75 additions & 0 deletions src/Serilog.Enrichers.ClientInfo/Enrichers/UserClaimsEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Serilog.Core;
using Serilog.Events;

namespace Serilog.Enrichers;

/// <inheritdoc />
public class UserClaimsEnricher : ILogEventEnricher
{
private readonly Dictionary<string, string> _claimItemKeys;
private readonly string[] _claimNames;
private readonly IHttpContextAccessor _contextAccessor;

/// <summary>
/// Initializes a new instance of the <see cref="UserClaimsEnricher" /> class.
/// </summary>
/// <param name="claimNames">The names of the claims to log.</param>
public UserClaimsEnricher(params string[] claimNames)
: this(new HttpContextAccessor(), claimNames)
{
}

internal UserClaimsEnricher(IHttpContextAccessor contextAccessor, params string[] claimNames)
{
_contextAccessor = contextAccessor;
_claimNames = claimNames ?? [];
_claimItemKeys = new();

// Pre-compute item keys for each claim
foreach (string claimName in _claimNames)
{
Comment on lines +31 to +33
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The code doesn't validate that individual claim names in the array are not null or empty. If a null claim name is passed (e.g., new UserClaimsEnricher("claim1", null, "claim2")), this could cause issues:

  1. Line 34 would create a cache key like "Serilog_UserClaim_" which could cause collisions
  2. Line 66 in the Enrich method would call user.FindFirst(null) which may throw a System.ArgumentNullException

Consider adding validation:

foreach (string claimName in _claimNames)
{
    if (string.IsNullOrWhiteSpace(claimName))
    {
        throw new ArgumentException("Claim names cannot be null or empty.", nameof(claimNames));
    }
    _claimItemKeys[claimName] = $"Serilog_UserClaim_{claimName}";
}
Suggested change
// Pre-compute item keys for each claim
foreach (string claimName in _claimNames)
{
// Validate claim names and pre-compute item keys for each claim
foreach (string claimName in _claimNames)
{
if (string.IsNullOrWhiteSpace(claimName))
{
throw new ArgumentException("Claim names cannot be null or empty.", nameof(claimNames));
}

Copilot uses AI. Check for mistakes.
_claimItemKeys[claimName] = $"Serilog_UserClaim_{claimName}";
}
}

/// <inheritdoc />
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
HttpContext httpContext = _contextAccessor.HttpContext;
if (httpContext == null)
{
return;
}

ClaimsPrincipal user = httpContext.User;
if (user == null || !user.Identity?.IsAuthenticated == true)
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The authentication check has a subtle bug when user.Identity is null. The expression !user.Identity?.IsAuthenticated == true evaluates as follows:

  • When Identity is null: !(null) = null, and null == true = false, so it doesn't return early and continues to enrich
  • However, a user with a null Identity should be treated as not authenticated and should return early

The intent appears to be checking if the user is authenticated, which should use:

if (user == null || user.Identity?.IsAuthenticated != true)

This correctly handles all cases:

  • When Identity is null: null != true = true (returns early) ✓
  • When IsAuthenticated is false: false != true = true (returns early) ✓
  • When IsAuthenticated is true: true != true = false (continues) ✓
Suggested change
if (user == null || !user.Identity?.IsAuthenticated == true)
if (user == null || user.Identity?.IsAuthenticated != true)

Copilot uses AI. Check for mistakes.
{
return;
}

foreach (string claimName in _claimNames)
{
string itemKey = _claimItemKeys[claimName];

// Check if property already exists in HttpContext.Items
if (httpContext.Items.TryGetValue(itemKey, out object value) &&
value is LogEventProperty logEventProperty)
{
logEvent.AddPropertyIfAbsent(logEventProperty);
continue;
}

// Get claim value (null if not found)
string claimValue = user.FindFirst(claimName)?.Value;

// Create log property with the claim name as the property name
LogEventProperty claimProperty = new(claimName, new ScalarValue(claimValue));
httpContext.Items.Add(itemKey, claimProperty);

logEvent.AddPropertyIfAbsent(claimProperty);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,21 @@ public static LoggerConfiguration WithRequestHeader(this LoggerEnrichmentConfigu

return enrichmentConfiguration.With(new ClientHeaderEnricher(headerName, propertyName));
}

/// <summary>
/// Registers the user claims enricher to enrich logs with specified user claim values.
/// </summary>
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
/// <param name="claimNames">The names of the claims to log.</param>
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
/// <exception cref="ArgumentNullException">claimNames</exception>
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
public static LoggerConfiguration WithUserClaims(this LoggerEnrichmentConfiguration enrichmentConfiguration,
params string[] claimNames)
{
ArgumentNullException.ThrowIfNull(enrichmentConfiguration, nameof(enrichmentConfiguration));
ArgumentNullException.ThrowIfNull(claimNames, nameof(claimNames));

return enrichmentConfiguration.With(new UserClaimsEnricher(claimNames));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<DebugType>embedded</DebugType>
<EmbedAllSources>true</EmbedAllSources>
<Version>2.6.0</Version>
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The Version property is still set to 2.6.0 while AssemblyVersion is being updated to 2.7.0. These should typically be kept in sync for a new release. Consider updating the Version property to 2.7.0 as well:

<Version>2.7.0</Version>
Suggested change
<Version>2.6.0</Version>
<Version>2.7.0</Version>

Copilot uses AI. Check for mistakes.
<AssemblyVersion>2.6.0</AssemblyVersion>
<AssemblyVersion>2.7.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading