Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 18, 2025

Summary

This PR implements a new UserClaimsEnricher for Serilog that allows logging of specific user claim values from authenticated users. This enhancement provides better traceability by capturing relevant user-specific information in logs.

Changes

New Enricher

  • UserClaimsEnricher: A new enricher that logs configurable user claim values
    • Accepts a list of claim names to log via params string[] claimNames
    • Only enriches logs when the user is authenticated
    • Handles missing claims gracefully by logging null values instead of throwing errors or skipping
    • Caches claim values in HttpContext.Items for optimal performance

Extension Method

  • Added WithUserClaims() extension method to ClientInfoLoggerConfigurationExtensions
  • Supports both programmatic configuration and appsettings.json configuration

Usage Examples

Programmatic configuration:

using System.Security.Claims;

Log.Logger = new LoggerConfiguration()
    .Enrich.WithUserClaims(ClaimTypes.NameIdentifier, ClaimTypes.Email)
    .WriteTo.Console()
    .CreateLogger();

Configuration via appsettings.json:

{
  "Serilog": {
    "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 passing claim names as parameters
Null-Safe: Missing claims are logged as null rather than causing errors
Authentication-Aware: Only enriches logs for authenticated users
Performance Optimized: Claims are cached per request to avoid redundant lookups
Consistent API: Follows the same patterns as existing enrichers in the library

Testing

Added comprehensive test coverage with 10 new test cases covering:

  • Authenticated users with valid claims
  • Missing claims (null value handling)
  • Unauthenticated users (no enrichment)
  • Null HttpContext handling
  • Multiple claims
  • Custom claim types
  • Caching behavior
  • Empty claim arrays

All 55 tests pass (45 existing + 10 new).

Documentation

Updated README.md with:

  • UserClaims section with detailed examples
  • Code examples for both C# and JSON configuration
  • Feature descriptions and behavior documentation

Fixes #[issue_number]

Original prompt

This section details on the original issue you should resolve

<issue_title>Implement Enricher to Log User Claims</issue_title>
<issue_description>Implement a new enricher for Serilog that logs user claims. The enricher should:

  • Accept a configurable list of claim names to log.
  • If the user is authenticated, log the values of the specified claims.
  • If a claim value is null, log it as null (do not skip or throw an error).

This will enhance client information logging by capturing relevant claim values for authenticated users.</issue_description>

Comments on the Issue (you are @copilot in this section)

Fixes #62


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits October 18, 2025 23:22
Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com>
Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement enricher to log user claims for Serilog Add UserClaims enricher to log authenticated user claim values Oct 18, 2025
Copilot AI requested a review from mo-esmp October 18, 2025 23:26
Copilot finished work on behalf of mo-esmp October 18, 2025 23:26
@mo-esmp mo-esmp marked this pull request as ready for review November 23, 2025 19:05
Copilot AI review requested due to automatic review settings November 23, 2025 19:05
@mo-esmp mo-esmp merged commit 824b485 into master Nov 23, 2025
5 checks passed
Copilot finished reviewing on behalf of mo-esmp November 23, 2025 19:08
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new UserClaimsEnricher to the Serilog.Enrichers.ClientInfo library that enables logging of specific user claim values from authenticated users. The enricher follows the established patterns in the codebase and includes comprehensive test coverage.

Key Changes

  • New UserClaimsEnricher class that enriches logs with configurable user claim values
  • Extension method WithUserClaims() supporting both programmatic and configuration-based setup
  • Performance optimization through per-request caching of claim values in HttpContext.Items

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/Serilog.Enrichers.ClientInfo/Enrichers/UserClaimsEnricher.cs New enricher implementation with authentication-aware claim logging and caching
src/Serilog.Enrichers.ClientInfo/Extensions/ClientInfoLoggerConfigurationExtensions.cs Added WithUserClaims() extension method for logger configuration
test/Serilog.Enrichers.ClientInfo.Tests/UserClaimsEnricherTests.cs Comprehensive test suite with 10 test cases covering various scenarios
src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj Version bump to 2.7.0 for the new feature
Serilog.Enrichers.ClientInfo.nuspec Updated release notes to document the new feature
README.md Documentation updates with usage examples and feature descriptions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

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.
Comment on lines +31 to +33
// Pre-compute item keys for each claim
foreach (string claimName in _claimNames)
{
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.
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<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.

// Assert
Assert.NotNull(evt);
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
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.

Inefficient use of 'ContainsKey' and indexer.

Copilot uses AI. Check for mistakes.
Assert.NotNull(evt);
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
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.

Inefficient use of 'ContainsKey' and indexer.

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +234
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
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.

Inefficient use of 'ContainsKey' and indexer.

Suggested change
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdentifierValue));
Assert.Equal(userId, nameIdentifierValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue));
Assert.Equal(userEmail, emailValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue));
Assert.Equal(userName, nameValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue));
Assert.Equal(userRole, roleValue.LiteralValue().ToString());

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +234
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
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.

Inefficient use of 'ContainsKey' and indexer.

Suggested change
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdValue));
Assert.Equal(userId, nameIdValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue));
Assert.Equal(userEmail, emailValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue));
Assert.Equal(userName, nameValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue));
Assert.Equal(userRole, roleValue.LiteralValue().ToString());

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +234
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
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.

Inefficient use of 'ContainsKey' and indexer.

Suggested change
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdentifierValue));
Assert.Equal(userId, nameIdentifierValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue));
Assert.Equal(userEmail, emailValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue));
Assert.Equal(userName, nameValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue));
Assert.Equal(userRole, roleValue.LiteralValue().ToString());

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +234
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
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.

Inefficient use of 'ContainsKey' and indexer.

Suggested change
Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier));
Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email));
Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name));
Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString());
Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role));
Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdValue));
Assert.Equal(userId, nameIdValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue));
Assert.Equal(userEmail, emailValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue));
Assert.Equal(userName, nameValue.LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue));
Assert.Equal(userRole, roleValue.LiteralValue().ToString());

Copilot uses AI. Check for mistakes.
Comment on lines +305 to +306
Assert.True(evt.Properties.ContainsKey(customClaimType));
Assert.Equal(customClaimValue, evt.Properties[customClaimType].LiteralValue().ToString());
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.

Inefficient use of 'ContainsKey' and indexer.

Suggested change
Assert.True(evt.Properties.ContainsKey(customClaimType));
Assert.Equal(customClaimValue, evt.Properties[customClaimType].LiteralValue().ToString());
Assert.True(evt.Properties.TryGetValue(customClaimType, out var value));
Assert.Equal(customClaimValue, value.LiteralValue().ToString());

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Enricher to Log User Claims

2 participants