Skip to content

Commit 5aeaf1e

Browse files
authored
Updated the ClientIpEnricher to support an IP version preference. #48 (#59)
* Updated the ClientIpEnricher to support an IP version preference. #48
1 parent 491a73a commit 5aeaf1e

File tree

8 files changed

+474
-47
lines changed

8 files changed

+474
-47
lines changed

README.md

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ or in `appsettings.json` file:
4848

4949
### ClientIp
5050
`ClientIp` enricher reads client IP from `HttpContext.Connection.RemoteIpAddress`. Since version 2.1, for [security reasons](https://nvd.nist.gov/vuln/detail/CVE-2023-22474), it no longer reads the `x-forwarded-for` header. To handle forwarded headers, configure [ForwardedHeadersOptions](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-7.0#forwarded-headers-middleware-order). If you still want to log `x-forwarded-for`, you can use the `RequestHeader` enricher.
51+
52+
#### Basic Usage
5153
```csharp
5254
Log.Logger = new LoggerConfiguration()
5355
.Enrich.WithClientIp()
@@ -67,6 +69,22 @@ or
6769
}
6870
}
6971
```
72+
73+
#### IP Version Preferences
74+
You can configure the enricher to prefer or filter specific IP versions (IPv4 or IPv6):
75+
76+
```csharp
77+
Log.Logger = new LoggerConfiguration()
78+
.Enrich.WithClientIp(IpVersionPreference.Ipv4Only)
79+
...
80+
```
81+
82+
Available IP version preferences:
83+
- `None` (default): No preference - use whatever IP version is available
84+
- `PreferIpv4`: Prefer IPv4 addresses when multiple are available, fallback to IPv6
85+
- `PreferIpv6`: Prefer IPv6 addresses when multiple are available, fallback to IPv4
86+
- `Ipv4Only`: Only log IPv4 addresses, ignore IPv6 addresses
87+
- `Ipv6Only`: Only log IPv6 addresses, ignore IPv4 addresses
7088
### CorrelationId
7189
For `CorrelationId` enricher you can:
7290
- Configure the header name and default header name is `x-correlation-id`
@@ -94,26 +112,6 @@ or
94112
}
95113
}
96114
```
97-
#### Retrieving Correlation ID
98-
You can easily retrieve the correlation ID from `HttpContext` using the `GetCorrelationId()` extension method:
99-
100-
```csharp
101-
public void SomeControllerAction()
102-
{
103-
// This will return the correlation ID that was enriched by the CorrelationIdEnricher
104-
var correlationId = HttpContext.GetCorrelationId();
105-
106-
// You can use this for error reporting, tracing, etc.
107-
if (!string.IsNullOrEmpty(correlationId))
108-
{
109-
// Show correlation ID to user for error reporting
110-
// or use it for additional logging/tracing
111-
}
112-
}
113-
```
114-
115-
This eliminates the need for manual casting and provides a clean API for accessing correlation IDs.
116-
117115
### RequestHeader
118116
You can use multiple `WithRequestHeader` to log different request headers. `WithRequestHeader` accepts two parameters; The first parameter `headerName` is the header name to log
119117
and the second parameter is `propertyName` which is the log property name.

src/Serilog.Enrichers.ClientInfo/Enrichers/ClientIpEnricher.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
using System.Runtime.CompilerServices;
1+
using System.Net;
2+
using System.Net.Sockets;
23
using Microsoft.AspNetCore.Http;
34
using Serilog.Core;
45
using Serilog.Events;
56

6-
[assembly: InternalsVisibleTo("Serilog.Enrichers.ClientInfo.Tests")]
7-
87
namespace Serilog.Enrichers;
98

109
/// <inheritdoc />
@@ -14,6 +13,7 @@ public class ClientIpEnricher : ILogEventEnricher
1413
private const string IpAddressItemKey = "Serilog_ClientIp";
1514

1615
private readonly IHttpContextAccessor _contextAccessor;
16+
private readonly IpVersionPreference _ipVersionPreference;
1717

1818
/// <summary>
1919
/// Initializes a new instance of the <see cref="ClientIpEnricher" /> class.
@@ -22,9 +22,20 @@ public ClientIpEnricher() : this(new HttpContextAccessor())
2222
{
2323
}
2424

25-
internal ClientIpEnricher(IHttpContextAccessor contextAccessor)
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="ClientIpEnricher" /> class.
27+
/// </summary>
28+
/// <param name="ipVersionPreference">The IP version preference for filtering IP addresses.</param>
29+
public ClientIpEnricher(IpVersionPreference ipVersionPreference) : this(new HttpContextAccessor(),
30+
ipVersionPreference)
31+
{
32+
}
33+
34+
internal ClientIpEnricher(IHttpContextAccessor contextAccessor,
35+
IpVersionPreference ipVersionPreference = IpVersionPreference.None)
2636
{
2737
_contextAccessor = contextAccessor;
38+
_ipVersionPreference = ipVersionPreference;
2839
}
2940

3041
/// <inheritdoc />
@@ -33,12 +44,19 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
3344
HttpContext httpContext = _contextAccessor.HttpContext;
3445
if (httpContext == null) return;
3546

36-
string ipAddress = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
47+
IPAddress remoteIpAddress = httpContext.Connection.RemoteIpAddress;
48+
if (remoteIpAddress == null) return;
49+
50+
// Apply IP version filtering based on preference
51+
IPAddress filteredIpAddress = ApplyIpVersionFilter(remoteIpAddress);
52+
if (filteredIpAddress == null) return; // IP address was filtered out based on preference
53+
54+
string ipAddress = filteredIpAddress.ToString();
3755

3856
if (httpContext.Items.TryGetValue(IpAddressItemKey, out object value) &&
3957
value is LogEventProperty logEventProperty)
4058
{
41-
if (!((ScalarValue)logEventProperty.Value).Value.ToString()!.Equals(ipAddress))
59+
if (!((ScalarValue)logEventProperty.Value).Value!.ToString()!.Equals(ipAddress))
4260
logEventProperty = new LogEventProperty(IpAddressPropertyName, new ScalarValue(ipAddress));
4361

4462
logEvent.AddPropertyIfAbsent(logEventProperty);
@@ -49,4 +67,24 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
4967
httpContext.Items.Add(IpAddressItemKey, ipAddressProperty);
5068
logEvent.AddPropertyIfAbsent(ipAddressProperty);
5169
}
70+
71+
/// <summary>
72+
/// Applies IP version filtering based on the configured preference.
73+
/// </summary>
74+
/// <param name="ipAddress">The IP address to filter.</param>
75+
/// <returns>The filtered IP address, or null if it should be excluded.</returns>
76+
private IPAddress ApplyIpVersionFilter(IPAddress ipAddress)
77+
{
78+
return _ipVersionPreference switch
79+
{
80+
IpVersionPreference.None => ipAddress,
81+
IpVersionPreference
82+
.PreferIpv4 => ipAddress, // For single IP, just return it (preference only matters with multiple IPs)
83+
IpVersionPreference
84+
.PreferIpv6 => ipAddress, // For single IP, just return it (preference only matters with multiple IPs)
85+
IpVersionPreference.Ipv4Only => ipAddress.AddressFamily == AddressFamily.InterNetwork ? ipAddress : null,
86+
IpVersionPreference.Ipv6Only => ipAddress.AddressFamily == AddressFamily.InterNetworkV6 ? ipAddress : null,
87+
_ => ipAddress
88+
};
89+
}
5290
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace Serilog.Enrichers;
2+
3+
/// <summary>
4+
/// Specifies the IP version preference for client IP enrichment.
5+
/// </summary>
6+
public enum IpVersionPreference
7+
{
8+
/// <summary>
9+
/// No preference - use whatever IP version is available (default behavior).
10+
/// </summary>
11+
None = 0,
12+
13+
/// <summary>
14+
/// Prefer IPv4 addresses when available, fallback to IPv6 if IPv4 is not available.
15+
/// </summary>
16+
PreferIpv4 = 1,
17+
18+
/// <summary>
19+
/// Prefer IPv6 addresses when available, fallback to IPv4 if IPv6 is not available.
20+
/// </summary>
21+
PreferIpv6 = 2,
22+
23+
/// <summary>
24+
/// Only log IPv4 addresses, ignore IPv6 addresses.
25+
/// </summary>
26+
Ipv4Only = 3,
27+
28+
/// <summary>
29+
/// Only log IPv6 addresses, ignore IPv4 addresses.
30+
/// </summary>
31+
Ipv6Only = 4
32+
}

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
using Microsoft.AspNetCore.Http;
1+
using System;
2+
using Microsoft.AspNetCore.Http;
23
using Serilog.Configuration;
34
using Serilog.Enrichers;
4-
using System;
55

66
namespace Serilog;
77

88
/// <summary>
9-
/// Extension methods for setting up client IP, client agent and correlation identifier enrichers <see cref="LoggerEnrichmentConfiguration"/>.
9+
/// Extension methods for setting up client IP, client agent and correlation identifier enrichers
10+
/// <see cref="LoggerEnrichmentConfiguration" />.
1011
/// </summary>
1112
public static class ClientInfoLoggerConfigurationExtensions
1213
{
1314
/// <summary>
14-
/// Registers the client IP enricher to enrich logs with <see cref="Microsoft.AspNetCore.Http.ConnectionInfo.RemoteIpAddress"/> value.
15+
/// Registers the client IP enricher to enrich logs with
16+
/// <see cref="Microsoft.AspNetCore.Http.ConnectionInfo.RemoteIpAddress" /> value.
1517
/// </summary>
1618
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
1719
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
@@ -25,17 +27,34 @@ public static LoggerConfiguration WithClientIp(
2527
}
2628

2729
/// <summary>
28-
/// Registers the correlation id enricher to enrich logs with correlation id with
29-
/// 'x-correlation-id' header information.
30+
/// Registers the client IP enricher to enrich logs with
31+
/// <see cref="Microsoft.AspNetCore.Http.ConnectionInfo.RemoteIpAddress" /> value.
32+
/// </summary>
33+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
34+
/// <param name="ipVersionPreference">The IP version preference for filtering IP addresses.</param>
35+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
36+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
37+
public static LoggerConfiguration WithClientIp(
38+
this LoggerEnrichmentConfiguration enrichmentConfiguration,
39+
IpVersionPreference ipVersionPreference)
40+
{
41+
ArgumentNullException.ThrowIfNull(enrichmentConfiguration, nameof(enrichmentConfiguration));
42+
43+
return enrichmentConfiguration.With(new ClientIpEnricher(ipVersionPreference));
44+
}
45+
46+
/// <summary>
47+
/// Registers the correlation id enricher to enrich logs with correlation id with
48+
/// 'x-correlation-id' header information.
3049
/// </summary>
3150
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
3251
/// <param name="headerName">
33-
/// Set the 'X-Correlation-Id' header in case if service is behind proxy server. Default value
34-
/// is 'x-correlation-id'.
52+
/// Set the 'X-Correlation-Id' header in case if service is behind proxy server. Default value
53+
/// is 'x-correlation-id'.
3554
/// </param>
3655
/// <param name="addValueIfHeaderAbsence">
37-
/// Add generated correlation id value if correlation id header not available in
38-
/// <see cref="HttpContext"/> header collection.
56+
/// Add generated correlation id value if correlation id header not available in
57+
/// <see cref="HttpContext" /> header collection.
3958
/// </param>
4059
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
4160
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
@@ -50,7 +69,7 @@ public static LoggerConfiguration WithCorrelationId(
5069
}
5170

5271
/// <summary>
53-
/// Registers the HTTP request header enricher to enrich logs with the header value.
72+
/// Registers the HTTP request header enricher to enrich logs with the header value.
5473
/// </summary>
5574
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
5675
/// <param name="propertyName">The property name of log</param>
@@ -66,4 +85,4 @@ public static LoggerConfiguration WithRequestHeader(this LoggerEnrichmentConfigu
6685

6786
return enrichmentConfiguration.With(new ClientHeaderEnricher(headerName, propertyName));
6887
}
69-
}
88+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
44
<AssemblyName>Serilog.Enrichers.ClientInfo</AssemblyName>
@@ -18,4 +18,8 @@
1818
<ItemGroup>
1919
<PackageReference Include="Serilog" Version="4.3.0" />
2020
</ItemGroup>
21+
22+
<ItemGroup>
23+
<InternalsVisibleTo Include="Serilog.Enrichers.ClientInfo.Tests" />
24+
</ItemGroup>
2125
</Project>
Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
1-
using System.Linq;
1+
using Serilog.Events;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
25
using System.Threading.Tasks;
36
using Xunit;
47

58
namespace Serilog.Enrichers.ClientInfo.Tests;
69

7-
public class ClientIpEnricherIntegrationTests(CustomWebApplicationFactory factory) : IClassFixture<CustomWebApplicationFactory>
10+
public class ClientIpEnricherIntegrationTests(CustomWebApplicationFactory factory)
11+
: IClassFixture<CustomWebApplicationFactory>
812
{
913
[Fact]
1014
public async Task GetRoot_ReturnsHelloWorld()
1115
{
1216
// Arrange
1317
const string ip = "1.2.3.4";
1418

15-
var client = factory.CreateClient();
19+
HttpClient client = factory.CreateClient();
1620
client.DefaultRequestHeaders.Add("x-forwarded-for", ip);
1721

1822
// Act
19-
var response = await client.GetAsync("/");
20-
var logs = DelegatingSink.Logs;
21-
var allClientIpLogs = logs
23+
HttpResponseMessage response = await client.GetAsync("/");
24+
IReadOnlyList<LogEvent> logs = DelegatingSink.Logs;
25+
26+
List<KeyValuePair<string, LogEventPropertyValue>> allClientIpLogs = logs
2227
.SelectMany(l => l.Properties)
2328
.Where(p => p.Key == "ClientIp")
2429
.ToList();
25-
var forwardedClientIpLogs = logs
30+
31+
List<KeyValuePair<string, LogEventPropertyValue>> forwardedClientIpLogs = logs
2632
.SelectMany(l => l.Properties)
2733
.Where(p => p.Key == "ClientIp" && p.Value.LiteralValue().Equals(ip))
2834
.ToList();
2935

3036
// Assert
3137
response.EnsureSuccessStatusCode();
32-
Assert.Equal(forwardedClientIpLogs.Count, allClientIpLogs.Count - 1);
38+
Assert.Equal(allClientIpLogs.Count, forwardedClientIpLogs.Count);
3339
}
3440
}

0 commit comments

Comments
 (0)