Skip to content

Commit bbc52de

Browse files
Copilotmo-esmp
andauthored
Allow customizing ClientIP property name for OpenTelemetry conventions (#66)
* Initial plan * Add support for custom ClientIP property name Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> * Add documentation for custom ClientIP property name feature Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> * Add null validation for ipAddressPropertyName parameter Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> * Refactor ClientIpEnricher constructor for clarity. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> Co-authored-by: Mohsen Esmailpour <mo.esmp@gmail.com>
1 parent 824b485 commit bbc52de

File tree

4 files changed

+240
-8
lines changed

4 files changed

+240
-8
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,42 @@ Available IP version preferences:
9090
- `PreferIpv6`: Prefer IPv6 addresses when multiple are available, fallback to IPv4
9191
- `Ipv4Only`: Only log IPv4 addresses, ignore IPv6 addresses
9292
- `Ipv6Only`: Only log IPv6 addresses, ignore IPv4 addresses
93+
94+
#### Custom Property Name
95+
You can customize the property name for the logged IP address (default is `ClientIp`). This is useful for following conventions like [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/client/) which recommend `client.address`:
96+
97+
```csharp
98+
Log.Logger = new LoggerConfiguration()
99+
.Enrich.WithClientIp("client.address")
100+
...
101+
```
102+
103+
You can also combine custom property names with IP version preferences:
104+
105+
```csharp
106+
Log.Logger = new LoggerConfiguration()
107+
.Enrich.WithClientIp(IpVersionPreference.Ipv4Only, "client.address")
108+
...
109+
```
110+
111+
or in `appsettings.json` file:
112+
```json
113+
{
114+
"Serilog": {
115+
"MinimumLevel": "Debug",
116+
"Using": [ "Serilog.Enrichers.ClientInfo" ],
117+
"Enrich": [
118+
{
119+
"Name": "WithClientIp",
120+
"Args": {
121+
"ipVersionPreference": "Ipv4Only",
122+
"ipAddressPropertyName": "client.address"
123+
}
124+
}
125+
]
126+
}
127+
}
128+
```
93129

94130
### CorrelationId
95131
For `CorrelationId` enricher you can:

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Net;
1+
using System;
2+
using System.Net;
23
using System.Net.Sockets;
34
using Microsoft.AspNetCore.Http;
45
using Serilog.Core;
@@ -14,6 +15,7 @@ public class ClientIpEnricher : ILogEventEnricher
1415

1516
private readonly IHttpContextAccessor _contextAccessor;
1617
private readonly IpVersionPreference _ipVersionPreference;
18+
private readonly string _ipAddressPropertyName;
1719

1820
/// <summary>
1921
/// Initializes a new instance of the <see cref="ClientIpEnricher" /> class.
@@ -26,16 +28,31 @@ public ClientIpEnricher() : this(new HttpContextAccessor())
2628
/// Initializes a new instance of the <see cref="ClientIpEnricher" /> class.
2729
/// </summary>
2830
/// <param name="ipVersionPreference">The IP version preference for filtering IP addresses.</param>
29-
public ClientIpEnricher(IpVersionPreference ipVersionPreference) : this(new HttpContextAccessor(),
30-
ipVersionPreference)
31+
public ClientIpEnricher(IpVersionPreference ipVersionPreference) : this(new HttpContextAccessor(), ipVersionPreference)
3132
{
3233
}
3334

34-
internal ClientIpEnricher(IHttpContextAccessor contextAccessor,
35-
IpVersionPreference ipVersionPreference = IpVersionPreference.None)
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="ClientIpEnricher" /> class.
37+
/// </summary>
38+
/// <param name="ipVersionPreference">The IP version preference for filtering IP addresses.</param>
39+
/// <param name="ipAddressPropertyName">The custom property name for the IP address log property.</param>
40+
public ClientIpEnricher(IpVersionPreference ipVersionPreference, string ipAddressPropertyName)
41+
{
42+
ArgumentNullException.ThrowIfNull(ipAddressPropertyName, nameof(ipAddressPropertyName));
43+
_contextAccessor = new HttpContextAccessor();
44+
_ipVersionPreference = ipVersionPreference;
45+
_ipAddressPropertyName = ipAddressPropertyName;
46+
}
47+
48+
internal ClientIpEnricher(
49+
IHttpContextAccessor contextAccessor,
50+
IpVersionPreference ipVersionPreference = IpVersionPreference.None,
51+
string ipAddressPropertyName = IpAddressPropertyName)
3652
{
3753
_contextAccessor = contextAccessor;
3854
_ipVersionPreference = ipVersionPreference;
55+
_ipAddressPropertyName = ipAddressPropertyName;
3956
}
4057

4158
/// <inheritdoc />
@@ -57,13 +74,13 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
5774
value is LogEventProperty logEventProperty)
5875
{
5976
if (!((ScalarValue)logEventProperty.Value).Value!.ToString()!.Equals(ipAddress))
60-
logEventProperty = new LogEventProperty(IpAddressPropertyName, new ScalarValue(ipAddress));
77+
logEventProperty = new LogEventProperty(_ipAddressPropertyName, new ScalarValue(ipAddress));
6178

6279
logEvent.AddPropertyIfAbsent(logEventProperty);
6380
return;
6481
}
6582

66-
LogEventProperty ipAddressProperty = new(IpAddressPropertyName, new ScalarValue(ipAddress));
83+
LogEventProperty ipAddressProperty = new(_ipAddressPropertyName, new ScalarValue(ipAddress));
6784
httpContext.Items.Add(IpAddressItemKey, ipAddressProperty);
6885
logEvent.AddPropertyIfAbsent(ipAddressProperty);
6986
}
@@ -87,4 +104,4 @@ private IPAddress ApplyIpVersionFilter(IPAddress ipAddress)
87104
_ => ipAddress
88105
};
89106
}
90-
}
107+
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,46 @@ public static LoggerConfiguration WithClientIp(
4343
return enrichmentConfiguration.With(new ClientIpEnricher(ipVersionPreference));
4444
}
4545

46+
/// <summary>
47+
/// Registers the client IP enricher to enrich logs with
48+
/// <see cref="Microsoft.AspNetCore.Http.ConnectionInfo.RemoteIpAddress" /> value.
49+
/// </summary>
50+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
51+
/// <param name="ipAddressPropertyName">The custom property name for the IP address log property.</param>
52+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
53+
/// <exception cref="ArgumentNullException">ipAddressPropertyName</exception>
54+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
55+
public static LoggerConfiguration WithClientIp(
56+
this LoggerEnrichmentConfiguration enrichmentConfiguration,
57+
string ipAddressPropertyName)
58+
{
59+
ArgumentNullException.ThrowIfNull(enrichmentConfiguration, nameof(enrichmentConfiguration));
60+
ArgumentNullException.ThrowIfNull(ipAddressPropertyName, nameof(ipAddressPropertyName));
61+
62+
return enrichmentConfiguration.With(new ClientIpEnricher(IpVersionPreference.None, ipAddressPropertyName));
63+
}
64+
65+
/// <summary>
66+
/// Registers the client IP enricher to enrich logs with
67+
/// <see cref="Microsoft.AspNetCore.Http.ConnectionInfo.RemoteIpAddress" /> value.
68+
/// </summary>
69+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
70+
/// <param name="ipVersionPreference">The IP version preference for filtering IP addresses.</param>
71+
/// <param name="ipAddressPropertyName">The custom property name for the IP address log property.</param>
72+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
73+
/// <exception cref="ArgumentNullException">ipAddressPropertyName</exception>
74+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
75+
public static LoggerConfiguration WithClientIp(
76+
this LoggerEnrichmentConfiguration enrichmentConfiguration,
77+
IpVersionPreference ipVersionPreference,
78+
string ipAddressPropertyName)
79+
{
80+
ArgumentNullException.ThrowIfNull(enrichmentConfiguration, nameof(enrichmentConfiguration));
81+
ArgumentNullException.ThrowIfNull(ipAddressPropertyName, nameof(ipAddressPropertyName));
82+
83+
return enrichmentConfiguration.With(new ClientIpEnricher(ipVersionPreference, ipAddressPropertyName));
84+
}
85+
4686
/// <summary>
4787
/// Registers the correlation id enricher to enrich logs with correlation id with
4888
/// 'x-correlation-id' header information.

test/Serilog.Enrichers.ClientInfo.Tests/ClientIpEnricherTests.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,143 @@ public void EnrichLogWithClientIp_WhenKeyNotInItems_ShouldWorkCorrectly()
111111
Assert.True(evt.Properties.ContainsKey("ClientIp"));
112112
Assert.Equal(IPAddress.Loopback.ToString(), evt.Properties["ClientIp"].LiteralValue());
113113
}
114+
115+
[Fact]
116+
public void EnrichLogWithClientIp_WithCustomPropertyName_ShouldCreateCustomProperty()
117+
{
118+
// Arrange
119+
const string customPropertyName = "client.address";
120+
_contextAccessor.HttpContext.Connection.RemoteIpAddress = IPAddress.Loopback;
121+
ClientIpEnricher ipEnricher = new(_contextAccessor, IpVersionPreference.None, customPropertyName);
122+
123+
LogEvent evt = null;
124+
Logger log = new LoggerConfiguration()
125+
.Enrich.With(ipEnricher)
126+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
127+
.CreateLogger();
128+
129+
// Act
130+
log.Information("Has a custom IP property");
131+
132+
// Assert
133+
Assert.NotNull(evt);
134+
Assert.True(evt.Properties.ContainsKey(customPropertyName));
135+
Assert.False(evt.Properties.ContainsKey("ClientIp"));
136+
Assert.Equal(IPAddress.Loopback.ToString(), evt.Properties[customPropertyName].LiteralValue());
137+
}
138+
139+
[Theory]
140+
[InlineData("::1", "client.address")]
141+
[InlineData("192.168.1.1", "ClientAddress")]
142+
[InlineData("2001:0db8:85a3:0000:0000:8a2e:0370:7334", "RemoteIP")]
143+
public void EnrichLogWithClientIp_WithCustomPropertyName_ShouldUseCorrectName(string ip, string propertyName)
144+
{
145+
// Arrange
146+
IPAddress ipAddress = IPAddress.Parse(ip);
147+
_contextAccessor.HttpContext!.Connection.RemoteIpAddress = ipAddress;
148+
149+
ClientIpEnricher ipEnricher = new(_contextAccessor, IpVersionPreference.None, propertyName);
150+
151+
LogEvent evt = null;
152+
Logger log = new LoggerConfiguration()
153+
.Enrich.With(ipEnricher)
154+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
155+
.CreateLogger();
156+
157+
// Act
158+
log.Information("Has an IP property with custom name");
159+
160+
// Assert
161+
Assert.NotNull(evt);
162+
Assert.True(evt.Properties.ContainsKey(propertyName));
163+
Assert.Equal(ipAddress.ToString(), evt.Properties[propertyName].LiteralValue());
164+
}
165+
166+
[Fact]
167+
public void WithClientIp_WithCustomPropertyName_ThenLoggerIsCalled_ShouldUseCustomProperty()
168+
{
169+
// Arrange
170+
const string customPropertyName = "client.address";
171+
LogEvent evt = null;
172+
Logger logger = new LoggerConfiguration()
173+
.Enrich.WithClientIp(customPropertyName)
174+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
175+
.CreateLogger();
176+
177+
// Act
178+
logger.Information("LOG");
179+
180+
// Assert - Should not throw and property should exist if HttpContext is available
181+
Assert.NotNull(evt);
182+
}
183+
184+
[Fact]
185+
public void WithClientIp_WithIpVersionPreferenceAndCustomPropertyName_ShouldUseCustomProperty()
186+
{
187+
// Arrange
188+
const string customPropertyName = "client.address";
189+
LogEvent evt = null;
190+
Logger logger = new LoggerConfiguration()
191+
.Enrich.WithClientIp(IpVersionPreference.Ipv4Only, customPropertyName)
192+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
193+
.CreateLogger();
194+
195+
// Act
196+
logger.Information("LOG");
197+
198+
// Assert - Should not throw
199+
Assert.NotNull(evt);
200+
}
201+
202+
[Fact]
203+
public void EnrichLogWithClientIp_WithCustomPropertyName_WhenLogMoreThanOnce_ShouldCacheCorrectly()
204+
{
205+
// Arrange
206+
const string customPropertyName = "client.address";
207+
_contextAccessor.HttpContext.Connection.RemoteIpAddress = IPAddress.Loopback;
208+
ClientIpEnricher ipEnricher = new(_contextAccessor, IpVersionPreference.None, customPropertyName);
209+
210+
LogEvent evt = null;
211+
Logger log = new LoggerConfiguration()
212+
.Enrich.With(ipEnricher)
213+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
214+
.CreateLogger();
215+
216+
// Act
217+
log.Information("First log with custom property");
218+
log.Information("Second log with custom property");
219+
220+
// Assert
221+
Assert.NotNull(evt);
222+
Assert.True(evt.Properties.ContainsKey(customPropertyName));
223+
Assert.Equal(IPAddress.Loopback.ToString(), evt.Properties[customPropertyName].LiteralValue());
224+
}
225+
226+
[Fact]
227+
public void ClientIpEnricher_WithNullPropertyName_ShouldThrowArgumentNullException()
228+
{
229+
// Act & Assert
230+
Assert.Throws<ArgumentNullException>(() => new ClientIpEnricher(IpVersionPreference.None, null));
231+
}
232+
233+
[Fact]
234+
public void WithClientIp_WithNullPropertyName_ShouldThrowArgumentNullException()
235+
{
236+
// Arrange
237+
LoggerConfiguration loggerConfiguration = new LoggerConfiguration();
238+
239+
// Act & Assert
240+
Assert.Throws<ArgumentNullException>(() => loggerConfiguration.Enrich.WithClientIp((string)null));
241+
}
242+
243+
[Fact]
244+
public void WithClientIp_WithIpVersionPreferenceAndNullPropertyName_ShouldThrowArgumentNullException()
245+
{
246+
// Arrange
247+
LoggerConfiguration loggerConfiguration = new LoggerConfiguration();
248+
249+
// Act & Assert
250+
Assert.Throws<ArgumentNullException>(() =>
251+
loggerConfiguration.Enrich.WithClientIp(IpVersionPreference.Ipv4Only, null));
252+
}
114253
}

0 commit comments

Comments
 (0)