Skip to content
Draft
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
69 changes: 69 additions & 0 deletions src/Serilog.Enrichers.ClientInfo/Enrichers/UserClaimsEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 IHttpContextAccessor _contextAccessor;
private readonly string[] _claimNames;
private readonly Dictionary<string, string> _claimItemKeys;

/// <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 Dictionary<string, string>();

// Pre-compute item keys for each claim
foreach (string claimName in _claimNames)
{
_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) 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));
}
}
Loading
Loading