Skip to content

Commit f022182

Browse files
Add client header and correlation id enricher.
1 parent 3f2d61a commit f022182

File tree

7 files changed

+446
-32
lines changed

7 files changed

+446
-32
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,5 @@ ASALocalRun/
328328

329329
# MFractors (Xamarin productivity tool) working folder
330330
.mfractor/
331+
332+
.vshistory/
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Serilog.Core;
2+
using Serilog.Events;
3+
4+
#if NETFULL
5+
6+
using Serilog.Enrichers.ClientInfo.Accessors;
7+
8+
#else
9+
using Microsoft.AspNetCore.Http;
10+
#endif
11+
12+
namespace Serilog.Enrichers;
13+
14+
/// <inheritdoc/>
15+
public class ClientHeaderEnricher : ILogEventEnricher
16+
{
17+
private readonly string _clientHeaderItemKey;
18+
private readonly string _headerKey;
19+
private readonly IHttpContextAccessor _contextAccessor;
20+
21+
public ClientHeaderEnricher(string headerKey)
22+
: this(headerKey, new HttpContextAccessor())
23+
{
24+
}
25+
26+
internal ClientHeaderEnricher(string headerKey, IHttpContextAccessor contextAccessor)
27+
{
28+
_headerKey = headerKey;
29+
_clientHeaderItemKey = $"Serilog_{headerKey}";
30+
_contextAccessor = contextAccessor;
31+
}
32+
33+
internal ClientHeaderEnricher(IHttpContextAccessor contextAccessor)
34+
{
35+
_contextAccessor = contextAccessor;
36+
}
37+
38+
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
39+
{
40+
var httpContext = _contextAccessor.HttpContext;
41+
if (httpContext == null)
42+
return;
43+
44+
if (httpContext.Items[_clientHeaderItemKey] is LogEventProperty logEventProperty)
45+
{
46+
logEvent.AddPropertyIfAbsent(logEventProperty);
47+
return;
48+
}
49+
50+
var headerValue = httpContext.Request.Headers[_headerKey].ToString();
51+
headerValue = string.IsNullOrWhiteSpace(headerValue) ? null : headerValue;
52+
53+
var logProperty = new LogEventProperty(_headerKey, new ScalarValue(headerValue));
54+
httpContext.Items.Add(_clientHeaderItemKey, logProperty);
55+
56+
logEvent.AddPropertyIfAbsent(logProperty);
57+
}
58+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Serilog.Core;
2+
using Serilog.Events;
3+
using System;
4+
5+
#if NETFULL
6+
7+
using Serilog.Enrichers.ClientInfo.Accessors;
8+
9+
#else
10+
using Microsoft.AspNetCore.Http;
11+
#endif
12+
13+
namespace Serilog.Enrichers;
14+
15+
/// <inheritdoc/>
16+
public class CorrelationIdEnricher : ILogEventEnricher
17+
{
18+
private const string CorrelationIdItemKey = "Serilog_CorrelationId";
19+
private readonly string _headerKey;
20+
private readonly bool _addValueIfHeaderAbsence;
21+
private readonly IHttpContextAccessor _contextAccessor;
22+
23+
public CorrelationIdEnricher(string headerKey, bool addValueIfHeaderAbsence)
24+
: this(headerKey, addValueIfHeaderAbsence, new HttpContextAccessor())
25+
{
26+
}
27+
28+
internal CorrelationIdEnricher(string headerKey, bool addValueIfHeaderAbsence, IHttpContextAccessor contextAccessor)
29+
{
30+
_headerKey = headerKey;
31+
_addValueIfHeaderAbsence = addValueIfHeaderAbsence;
32+
_contextAccessor = contextAccessor;
33+
}
34+
35+
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
36+
{
37+
var httpContext = _contextAccessor.HttpContext;
38+
if (httpContext == null)
39+
{
40+
return;
41+
}
42+
43+
if (httpContext.Items[CorrelationIdItemKey] is LogEventProperty logEventProperty)
44+
{
45+
logEvent.AddPropertyIfAbsent(logEventProperty);
46+
return;
47+
}
48+
49+
var header = httpContext.Request.Headers[_headerKey].ToString();
50+
var correlationId = !string.IsNullOrWhiteSpace(header)
51+
? header
52+
: (_addValueIfHeaderAbsence ? Guid.NewGuid().ToString() : null);
53+
54+
var correlationIdProperty = new LogEventProperty(_headerKey, new ScalarValue(correlationId));
55+
logEvent.AddOrUpdateProperty(correlationIdProperty);
56+
57+
httpContext.Items.Add(CorrelationIdItemKey, correlationIdProperty);
58+
}
59+
}
Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,111 @@
11
using Serilog.Configuration;
22
using Serilog.Enrichers;
33
using System;
4+
using System.Web;
45

5-
namespace Serilog
6+
#if NETFULL
7+
8+
using Serilog.Enrichers.ClientInfo.Accessors;
9+
10+
#else
11+
using Microsoft.AspNetCore.Http;
12+
#endif
13+
14+
namespace Serilog;
15+
16+
/// <summary>
17+
/// Extension methods for setting up client IP, client agent and correlation identifier enrichers <see cref="LoggerEnrichmentConfiguration"/>.
18+
/// </summary>
19+
public static class ClientInfoLoggerConfigurationExtensions
620
{
721
/// <summary>
8-
/// Extension methods for setting up client IP and client agent enrichers <see cref="LoggerEnrichmentConfiguration"/>.
22+
/// Registers the client IP enricher to enrich logs with client IP with 'X-forwarded-for'
23+
/// header information.
924
/// </summary>
10-
public static class ClientInfoLoggerConfigurationExtensions
25+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
26+
/// <param name="xForwardHeaderName">
27+
/// Set the 'X-Forwarded-For' header in case if service is behind proxy server. Default value
28+
/// is 'X-forwarded-for'.
29+
/// </param>
30+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
31+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
32+
public static LoggerConfiguration WithClientIp(this LoggerEnrichmentConfiguration enrichmentConfiguration, string xForwardHeaderName = null)
1133
{
12-
/// <summary>
13-
/// Registers the client IP enricher to enrich logs with client IP with 'X-forwarded-for' header information.
14-
/// </summary>
15-
/// <param name="enrichmentConfiguration"> The enrichment configuration. </param>
16-
/// <param name="xForwardHeaderName">
17-
/// Set the 'X-Forwarded-For' header in case if service is behind proxy server. Default value is 'X-forwarded-for'.
18-
/// </param>
19-
/// <exception cref="ArgumentNullException"> enrichmentConfiguration </exception>
20-
/// <returns> The logger configuration so that multiple calls can be chained. </returns>
21-
public static LoggerConfiguration WithClientIp(this LoggerEnrichmentConfiguration enrichmentConfiguration, string xForwardHeaderName = null)
22-
{
23-
if (enrichmentConfiguration == null)
24-
throw new ArgumentNullException(nameof(enrichmentConfiguration));
34+
if (enrichmentConfiguration == null)
35+
throw new ArgumentNullException(nameof(enrichmentConfiguration));
36+
37+
if (!string.IsNullOrEmpty(xForwardHeaderName))
38+
ClinetIpConfiguration.XForwardHeaderName = xForwardHeaderName;
2539

26-
if (!string.IsNullOrEmpty(xForwardHeaderName))
27-
ClinetIpConfiguration.XForwardHeaderName = xForwardHeaderName;
40+
return enrichmentConfiguration.With<ClientIpEnricher>();
41+
}
42+
43+
/// <summary>
44+
/// Registers the client Agent enricher to enrich logs with 'User-Agent' header information.
45+
/// </summary>
46+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
47+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
48+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
49+
public static LoggerConfiguration WithClientAgent(this LoggerEnrichmentConfiguration enrichmentConfiguration)
50+
{
51+
if (enrichmentConfiguration == null)
52+
throw new ArgumentNullException(nameof(enrichmentConfiguration));
53+
54+
return enrichmentConfiguration.With<ClientAgentEnricher>();
55+
}
2856

29-
return enrichmentConfiguration.With<ClientIpEnricher>();
57+
/// <summary>
58+
/// Registers the correlation id enricher to enrich logs with correlation id with
59+
/// 'x-correlation-id' header information.
60+
/// </summary>
61+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
62+
/// <param name="headerName">
63+
/// Set the 'X-Correlation-Id' header in case if service is behind proxy server. Default value
64+
/// is 'x-correlation-id'.
65+
/// </param>
66+
/// <param name="addValueIfHeaderAbsence">
67+
/// Add generated correlation id value if correlation id header not available in
68+
/// <see cref="HttpContext"/> header collection.
69+
/// </param>
70+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
71+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
72+
public static LoggerConfiguration WithCorrelationId(
73+
this LoggerEnrichmentConfiguration enrichmentConfiguration,
74+
string headerName = "x-correlation-id",
75+
bool addValueIfHeaderAbsence = false)
76+
{
77+
if (enrichmentConfiguration == null)
78+
{
79+
throw new ArgumentNullException(nameof(enrichmentConfiguration));
3080
}
3181

32-
/// <summary>
33-
/// Registers the client Agent enricher to enrich logs with 'User-Agent' header information.
34-
/// </summary>
35-
/// <param name="enrichmentConfiguration"> The enrichment configuration. </param>
36-
/// <exception cref="ArgumentNullException"> enrichmentConfiguration </exception>
37-
/// <returns> The logger configuration so that multiple calls can be chained. </returns>
38-
public static LoggerConfiguration WithClientAgent(this LoggerEnrichmentConfiguration enrichmentConfiguration)
82+
return enrichmentConfiguration.With(new CorrelationIdEnricher(headerName, addValueIfHeaderAbsence));
83+
}
84+
85+
/// <summary>
86+
/// Registers the HTTP request header enricher to enrich logs with the header value.
87+
/// </summary>
88+
/// <param name="enrichmentConfiguration">The enrichment configuration.</param>
89+
/// <param name="headerName">The header name to log its value</param>
90+
/// <param name="addValueIfHeaderAbsence">
91+
/// Add generated correlation id value if correlation id header not available in
92+
/// <see cref="HttpContext"/> header collection.
93+
/// </param>
94+
/// <exception cref="ArgumentNullException">enrichmentConfiguration</exception>
95+
/// <exception cref="ArgumentNullException">headerName</exception>
96+
/// <returns>The logger configuration so that multiple calls can be chained.</returns>
97+
public static LoggerConfiguration WithRequestHeader(this LoggerEnrichmentConfiguration enrichmentConfiguration, string headerName)
98+
{
99+
if (enrichmentConfiguration == null)
39100
{
40-
if (enrichmentConfiguration == null)
41-
throw new ArgumentNullException(nameof(enrichmentConfiguration));
101+
throw new ArgumentNullException(nameof(enrichmentConfiguration));
102+
}
42103

43-
return enrichmentConfiguration.With<ClientAgentEnricher>();
104+
if (headerName == null)
105+
{
106+
throw new ArgumentNullException(nameof(headerName));
44107
}
108+
109+
return enrichmentConfiguration.With(new ClientHeaderEnricher(headerName));
45110
}
46111
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<AssemblyName>Serilog.Enrichers.ClientInfo</AssemblyName>
4+
<TargetFrameworks>net462;netstandard2.0;netstandard2.1</TargetFrameworks>
5+
<AssemblyName>Serilog.Enrichers.ClientInfo</AssemblyName>
56
<RootNamespace>Serilog</RootNamespace>
6-
<TargetFrameworks>net462;netstandard2.0;netstandard2.1</TargetFrameworks>
7-
<LangVersion>7.3</LangVersion>
7+
<LangVersion>latest</LangVersion>
88
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
9+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
10+
<Version>1.3.0</Version>
911
</PropertyGroup>
1012

1113
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
@@ -30,4 +32,5 @@
3032
<Reference Include="System.Web" />
3133
<PackageReference Include="Serilog" Version="2.4.0" />
3234
</ItemGroup>
35+
3336
</Project>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using Microsoft.AspNetCore.Http;
2+
using NSubstitute;
3+
using Serilog.Events;
4+
using System;
5+
using Xunit;
6+
7+
namespace Serilog.Enrichers.ClientInfo.Tests;
8+
9+
public class ClientHeaderEnricherTests
10+
{
11+
private readonly IHttpContextAccessor _contextAccessor;
12+
13+
public ClientHeaderEnricherTests()
14+
{
15+
var httpContext = new DefaultHttpContext();
16+
_contextAccessor = Substitute.For<IHttpContextAccessor>();
17+
_contextAccessor.HttpContext.Returns(httpContext);
18+
}
19+
20+
[Fact]
21+
public void EnrichLogWithClientHeaderEnricher_WhenHttpRequestContainHeader_ShouldCreateHeaderValueProperty()
22+
{
23+
// Arrange
24+
var headerKey = "RequestId";
25+
var headerValue = Guid.NewGuid().ToString();
26+
_contextAccessor.HttpContext.Request.Headers.Add(headerKey, headerValue);
27+
28+
var clientHeaderEnricher = new ClientHeaderEnricher(headerKey, _contextAccessor);
29+
30+
LogEvent evt = null;
31+
var log = new LoggerConfiguration()
32+
.Enrich.With(clientHeaderEnricher)
33+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
34+
.CreateLogger();
35+
36+
// Act
37+
log.Information(@"First testing log enricher.");
38+
log.Information(@"Second testing log enricher.");
39+
40+
// Assert
41+
Assert.NotNull(evt);
42+
Assert.True(evt.Properties.ContainsKey(headerKey));
43+
Assert.Equal(headerValue, evt.Properties[headerKey].LiteralValue().ToString());
44+
}
45+
46+
[Fact]
47+
public void EnrichLogWithMulitpleClientHeaderEnricher_WhenHttpRequestContainHeaders_ShouldCreateHeaderValuesProperty()
48+
{
49+
// Arrange
50+
var headerKey1 = "Header1";
51+
var headerKey2 = "Header2";
52+
var headerValue1 = Guid.NewGuid().ToString();
53+
var headerValue2 = Guid.NewGuid().ToString();
54+
_contextAccessor.HttpContext.Request.Headers.Add(headerKey1, headerValue1);
55+
_contextAccessor.HttpContext.Request.Headers.Add(headerKey2, headerValue2);
56+
57+
var clientHeaderEnricher1 = new ClientHeaderEnricher(headerKey1, _contextAccessor);
58+
var clientHeaderEnricher2 = new ClientHeaderEnricher(headerKey2, _contextAccessor);
59+
60+
LogEvent evt = null;
61+
var log = new LoggerConfiguration()
62+
.Enrich.With(clientHeaderEnricher1)
63+
.Enrich.With(clientHeaderEnricher2)
64+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
65+
.CreateLogger();
66+
67+
// Act
68+
log.Information(@"First testing log enricher.");
69+
log.Information(@"Second testing log enricher.");
70+
71+
// Assert
72+
Assert.NotNull(evt);
73+
Assert.True(evt.Properties.ContainsKey(headerKey1));
74+
Assert.Equal(headerValue1, evt.Properties[headerKey1].LiteralValue().ToString());
75+
Assert.True(evt.Properties.ContainsKey(headerKey2));
76+
Assert.Equal(headerValue2, evt.Properties[headerKey2].LiteralValue().ToString());
77+
}
78+
79+
[Fact]
80+
public void EnrichLogWithClientHeaderEnricher_WhenHttpRequestNotContainHeader_ShouldCreateHeaderValuePropertyWithNoValue()
81+
{
82+
// Arrange
83+
var headerKey = "RequestId";
84+
var clientHeaderEnricher = new ClientHeaderEnricher(headerKey, _contextAccessor);
85+
86+
LogEvent evt = null;
87+
var log = new LoggerConfiguration()
88+
.Enrich.With(clientHeaderEnricher)
89+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
90+
.CreateLogger();
91+
92+
// Act
93+
log.Information(@"First testing log enricher.");
94+
log.Information(@"Second testing log enricher.");
95+
96+
// Assert
97+
Assert.NotNull(evt);
98+
Assert.True(evt.Properties.ContainsKey(headerKey));
99+
Assert.Null(evt.Properties[headerKey].LiteralValue());
100+
}
101+
}

0 commit comments

Comments
 (0)