Skip to content

Commit 824b485

Browse files
Copilotmo-esmp
andauthored
Add UserClaims enricher to log authenticated user claim values. (#63)
* Initial plan * Add UserClaims enricher with tests and documentation Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> * Remove unused using statement Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> * Polish code. * Update assembly version. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Mohsen Esmailpour <mo.esmp@gmail.com>
1 parent adeeda8 commit 824b485

File tree

6 files changed

+467
-3
lines changed

6 files changed

+467
-3
lines changed

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 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)
22

3-
Enrich logs with client IP, Correlation Id and HTTP request headers.
3+
Enrich logs with client IP, Correlation Id, HTTP request headers, and user claims.
44

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

@@ -19,6 +19,7 @@ Log.Logger = new LoggerConfiguration()
1919
.Enrich.WithClientIp()
2020
.Enrich.WithCorrelationId()
2121
.Enrich.WithRequestHeader("Header-Name1")
22+
.Enrich.WithUserClaims(ClaimTypes.NameIdentifier, ClaimTypes.Email)
2223
// ...other configuration...
2324
.CreateLogger();
2425
```
@@ -35,6 +36,10 @@ or in `appsettings.json` file:
3536
{
3637
"Name": "WithRequestHeader",
3738
"Args": { "headerName": "User-Agent"}
39+
},
40+
{
41+
"Name": "WithUserClaims",
42+
"Args": { "claimNames": ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] }
3843
}
3944
],
4045
"WriteTo": [
@@ -178,6 +183,65 @@ Log.Logger = new LoggerConfiguration()
178183
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:u3} {UserAgent} {Message:lj}{NewLine}{Exception}")
179184
```
180185

186+
### UserClaims
187+
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.
188+
189+
#### Basic Usage
190+
```csharp
191+
using System.Security.Claims;
192+
193+
Log.Logger = new LoggerConfiguration()
194+
.Enrich.WithUserClaims(ClaimTypes.NameIdentifier, ClaimTypes.Email)
195+
...
196+
```
197+
198+
or in `appsettings.json` file:
199+
```json
200+
{
201+
"Serilog": {
202+
"MinimumLevel": "Debug",
203+
"Using": [ "Serilog.Enrichers.ClientInfo" ],
204+
"Enrich": [
205+
{
206+
"Name": "WithUserClaims",
207+
"Args": {
208+
"claimNames": [
209+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
210+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
211+
]
212+
}
213+
}
214+
]
215+
}
216+
}
217+
```
218+
219+
#### Features
220+
- **Configurable Claims**: Specify which claims to log by providing claim names as parameters.
221+
- **Null-Safe**: If a claim doesn't exist, it will be logged as `null` instead of throwing an error.
222+
- **Authentication-Aware**: Only logs claims when the user is authenticated. If the user is not authenticated, no claim properties are added to the log.
223+
- **Performance-Optimized**: Claim values are cached per request for better performance.
224+
225+
#### Example with Multiple Claims
226+
```csharp
227+
Log.Logger = new LoggerConfiguration()
228+
.Enrich.WithUserClaims(
229+
ClaimTypes.NameIdentifier,
230+
ClaimTypes.Email,
231+
ClaimTypes.Name,
232+
ClaimTypes.Role)
233+
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] User: {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier} {Message:lj}{NewLine}{Exception}")
234+
...
235+
```
236+
237+
#### Custom Claims
238+
You can also log custom claim types:
239+
```csharp
240+
Log.Logger = new LoggerConfiguration()
241+
.Enrich.WithUserClaims("tenant_id", "organization_id")
242+
...
243+
```
244+
181245
## Installing into an ASP.NET Core Web Application
182246
You need to register the `IHttpContextAccessor` singleton so the enrichers have access to the requests `HttpContext` to extract client IP and client agent.
183247
This is what your `Startup` class should contain in order for this enricher to work as expected:

Serilog.Enrichers.ClientInfo.nuspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<tags>serilog enrichers enricher ip correlation-id request header</tags>
1717
<copyright>© 2025 Serilog Contributors</copyright>
1818
<releaseNotes>
19-
- Remove dependency on Microsoft.AspNetCore.App framework.
19+
- Add user claims enrichment support.
2020
</releaseNotes>
2121
<dependencies>
2222
<group targetFramework=".NET8.0">
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Collections.Generic;
2+
using System.Security.Claims;
3+
using Microsoft.AspNetCore.Http;
4+
using Serilog.Core;
5+
using Serilog.Events;
6+
7+
namespace Serilog.Enrichers;
8+
9+
/// <inheritdoc />
10+
public class UserClaimsEnricher : ILogEventEnricher
11+
{
12+
private readonly Dictionary<string, string> _claimItemKeys;
13+
private readonly string[] _claimNames;
14+
private readonly IHttpContextAccessor _contextAccessor;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="UserClaimsEnricher" /> class.
18+
/// </summary>
19+
/// <param name="claimNames">The names of the claims to log.</param>
20+
public UserClaimsEnricher(params string[] claimNames)
21+
: this(new HttpContextAccessor(), claimNames)
22+
{
23+
}
24+
25+
internal UserClaimsEnricher(IHttpContextAccessor contextAccessor, params string[] claimNames)
26+
{
27+
_contextAccessor = contextAccessor;
28+
_claimNames = claimNames ?? [];
29+
_claimItemKeys = new();
30+
31+
// Pre-compute item keys for each claim
32+
foreach (string claimName in _claimNames)
33+
{
34+
_claimItemKeys[claimName] = $"Serilog_UserClaim_{claimName}";
35+
}
36+
}
37+
38+
/// <inheritdoc />
39+
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
40+
{
41+
HttpContext httpContext = _contextAccessor.HttpContext;
42+
if (httpContext == null)
43+
{
44+
return;
45+
}
46+
47+
ClaimsPrincipal user = httpContext.User;
48+
if (user == null || !user.Identity?.IsAuthenticated == true)
49+
{
50+
return;
51+
}
52+
53+
foreach (string claimName in _claimNames)
54+
{
55+
string itemKey = _claimItemKeys[claimName];
56+
57+
// Check if property already exists in HttpContext.Items
58+
if (httpContext.Items.TryGetValue(itemKey, out object value) &&
59+
value is LogEventProperty logEventProperty)
60+
{
61+
logEvent.AddPropertyIfAbsent(logEventProperty);
62+
continue;
63+
}
64+
65+
// Get claim value (null if not found)
66+
string claimValue = user.FindFirst(claimName)?.Value;
67+
68+
// Create log property with the claim name as the property name
69+
LogEventProperty claimProperty = new(claimName, new ScalarValue(claimValue));
70+
httpContext.Items.Add(itemKey, claimProperty);
71+
72+
logEvent.AddPropertyIfAbsent(claimProperty);
73+
}
74+
}
75+
}

src/Serilog.Enrichers.ClientInfo/Extensions/ClientInfoLoggerConfigurationExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,21 @@ public static LoggerConfiguration WithRequestHeader(this LoggerEnrichmentConfigu
8585

8686
return enrichmentConfiguration.With(new ClientHeaderEnricher(headerName, propertyName));
8787
}
88+
89+
/// <summary>
90+
/// Registers the user claims enricher to enrich logs with specified user claim values.
91+
/// </summary>
92+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
93+
/// <param name="claimNames">The names of the claims to log.</param>
94+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
95+
/// <exception cref="ArgumentNullException">claimNames</exception>
96+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
97+
public static LoggerConfiguration WithUserClaims(this LoggerEnrichmentConfiguration enrichmentConfiguration,
98+
params string[] claimNames)
99+
{
100+
ArgumentNullException.ThrowIfNull(enrichmentConfiguration, nameof(enrichmentConfiguration));
101+
ArgumentNullException.ThrowIfNull(claimNames, nameof(claimNames));
102+
103+
return enrichmentConfiguration.With(new UserClaimsEnricher(claimNames));
104+
}
88105
}

src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<DebugType>embedded</DebugType>
99
<EmbedAllSources>true</EmbedAllSources>
1010
<Version>2.6.0</Version>
11-
<AssemblyVersion>2.6.0</AssemblyVersion>
11+
<AssemblyVersion>2.7.0</AssemblyVersion>
1212
</PropertyGroup>
1313

1414
<ItemGroup>

0 commit comments

Comments
 (0)