diff --git a/sample/SampleWebApp/SampleWebApp.csproj b/sample/SampleWebApp/SampleWebApp.csproj index 1d6aebc..95007a8 100644 --- a/sample/SampleWebApp/SampleWebApp.csproj +++ b/sample/SampleWebApp/SampleWebApp.csproj @@ -8,9 +8,12 @@ - + + + + diff --git a/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs b/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs index 9083eda..a3219f1 100644 --- a/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs +++ b/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs @@ -1,9 +1,9 @@ -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; +using Microsoft.AspNetCore.Http; using Serilog.Core; using Serilog.Events; +using Serilog.Preparers.CorrelationIds; +#nullable enable namespace Serilog.Enrichers; /// @@ -11,9 +11,8 @@ public class CorrelationIdEnricher : ILogEventEnricher { private const string CorrelationIdItemKey = "Serilog_CorrelationId"; private const string PropertyName = "CorrelationId"; - private readonly bool _addValueIfHeaderAbsence; private readonly IHttpContextAccessor _contextAccessor; - private readonly string _headerKey; + private readonly CorrelationIdPreparerOptions _options; /// /// Initializes a new instance of the class. @@ -24,15 +23,22 @@ public class CorrelationIdEnricher : ILogEventEnricher /// /// Determines whether to add a new correlation ID value if the header is absent. /// - public CorrelationIdEnricher(string headerKey, bool addValueIfHeaderAbsence) - : this(headerKey, addValueIfHeaderAbsence, new HttpContextAccessor()) + public CorrelationIdEnricher( + string headerKey, + bool addValueIfHeaderAbsence) + : this( + headerKey, + addValueIfHeaderAbsence, + new HttpContextAccessor()) { } - internal CorrelationIdEnricher(string headerKey, bool addValueIfHeaderAbsence, IHttpContextAccessor contextAccessor) + internal CorrelationIdEnricher( + string headerKey, + bool addValueIfHeaderAbsence, + IHttpContextAccessor contextAccessor) { - _headerKey = headerKey; - _addValueIfHeaderAbsence = addValueIfHeaderAbsence; + _options = new CorrelationIdPreparerOptions(addValueIfHeaderAbsence, headerKey); _contextAccessor = contextAccessor; } @@ -42,7 +48,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) HttpContext httpContext = _contextAccessor.HttpContext; if (httpContext == null) return; - if (httpContext.Items.TryGetValue(CorrelationIdItemKey, out object value) && + if (httpContext.Items.TryGetValue(CorrelationIdItemKey, out object? value) && value is LogEventProperty logEventProperty) { logEvent.AddPropertyIfAbsent(logEventProperty); @@ -50,26 +56,16 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) // Ensure the string value is also available if not already stored if (!httpContext.Items.ContainsKey(Constants.CorrelationIdValueKey)) { - string correlationIdValue = ((ScalarValue)logEventProperty.Value).Value as string; + string? correlationIdValue = ((ScalarValue)logEventProperty.Value).Value as string; httpContext.Items.Add(Constants.CorrelationIdValueKey, correlationIdValue); } return; } - StringValues requestHeader = httpContext.Request.Headers[_headerKey]; - StringValues responseHeader = httpContext.Response.Headers[_headerKey]; + ICorrelationIdPreparer correlationIdPreparer = httpContext.GetCorrelationIdPreparer(); - string correlationId; - - if (!string.IsNullOrWhiteSpace(requestHeader)) - correlationId = requestHeader; - else if (!string.IsNullOrWhiteSpace(responseHeader)) - correlationId = responseHeader; - else if (_addValueIfHeaderAbsence) - correlationId = Guid.NewGuid().ToString(); - else - correlationId = null; + string? correlationId = correlationIdPreparer.PrepareCorrelationId(httpContext, _options); LogEventProperty correlationIdProperty = new(PropertyName, new ScalarValue(correlationId)); logEvent.AddOrUpdateProperty(correlationIdProperty); @@ -77,4 +73,5 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) httpContext.Items.Add(CorrelationIdItemKey, correlationIdProperty); httpContext.Items.Add(Constants.CorrelationIdValueKey, correlationId); } -} \ No newline at end of file +} +#nullable disable \ No newline at end of file diff --git a/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs b/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs index babe1c9..16add24 100644 --- a/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs +++ b/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Serilog.Preparers.CorrelationIds; namespace Serilog.Enrichers; @@ -14,4 +16,12 @@ public static class HttpContextExtensions /// The correlation ID as a string, or null if not available. public static string GetCorrelationId(this HttpContext httpContext) => httpContext?.Items[Constants.CorrelationIdValueKey] as string; + + /// + /// Retrieves the correlation ID preparer for processing the current HTTP context. + /// + /// The HTTP context. + /// Correlation ID preparer. + internal static ICorrelationIdPreparer GetCorrelationIdPreparer(this HttpContext httpContext) + => httpContext.RequestServices.GetService() ?? new CorrelationIdPreparer(); } \ No newline at end of file diff --git a/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/CorrelationIdPreparer.cs b/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/CorrelationIdPreparer.cs new file mode 100644 index 0000000..6adbbab --- /dev/null +++ b/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/CorrelationIdPreparer.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +#nullable enable +namespace Serilog.Preparers.CorrelationIds +{ + internal class CorrelationIdPreparer : ICorrelationIdPreparer + { + protected AsyncLocal CorrelationId { get; } = new AsyncLocal(); + + /// + public string? PrepareCorrelationId( + HttpContext httpContext, + CorrelationIdPreparerOptions correlationIdPreparerOptions) + { + if (string.IsNullOrEmpty(CorrelationId.Value)) + { + CorrelationId.Value = PrepareValueForCorrelationId(httpContext, correlationIdPreparerOptions); + } + + return CorrelationId.Value; + } + + protected string? PrepareValueForCorrelationId( + HttpContext httpContext, + CorrelationIdPreparerOptions correlationIdPreparerOptions) + { + StringValues requestHeader = httpContext.Request.Headers[correlationIdPreparerOptions.HeaderKey]; + + if (!string.IsNullOrWhiteSpace(requestHeader)) + { + return requestHeader; + } + + StringValues responseHeader = httpContext.Response.Headers[correlationIdPreparerOptions.HeaderKey]; + + if (!string.IsNullOrWhiteSpace(responseHeader)) + { + return responseHeader; + } + + if (correlationIdPreparerOptions.AddValueIfHeaderAbsence) + { + return Guid.NewGuid().ToString(); + } + + return null; + } + } +} +#nullable disable \ No newline at end of file diff --git a/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/CorrelationIdPreparerOptions.cs b/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/CorrelationIdPreparerOptions.cs new file mode 100644 index 0000000..dbc688f --- /dev/null +++ b/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/CorrelationIdPreparerOptions.cs @@ -0,0 +1,31 @@ +namespace Serilog.Preparers.CorrelationIds +{ + /// + /// Settings for . + /// + public class CorrelationIdPreparerOptions + { + /// + /// Determines whether to add a new correlation ID value if the header is absent. + /// + public bool AddValueIfHeaderAbsence { get; } + + /// + /// The header key used to retrieve the correlation ID from the HTTP request or response headers. + /// + public string HeaderKey { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Determines whether to add a new correlation ID value if the header is absent. + /// The header key used to retrieve the correlation ID from the HTTP request or response headers. + public CorrelationIdPreparerOptions( + bool addValueIfHeaderAbsence, + string headerKey) + { + AddValueIfHeaderAbsence = addValueIfHeaderAbsence; + HeaderKey = headerKey; + } + } +} diff --git a/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/ICorrelationIdPreparer.cs b/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/ICorrelationIdPreparer.cs new file mode 100644 index 0000000..0efde0b --- /dev/null +++ b/src/Serilog.Enrichers.ClientInfo/Preparers/CorrelationIds/ICorrelationIdPreparer.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; + +#nullable enable +namespace Serilog.Preparers.CorrelationIds +{ + /// + /// Preparer for correlation ID. + /// + public interface ICorrelationIdPreparer + { + /// + /// Prepares the correlation ID. + /// + /// The HTTP context. + /// Options for preparation. + /// The correlation ID. + string? PrepareCorrelationId( + HttpContext httpContext, + CorrelationIdPreparerOptions correlationIdPreparerOptions); + } +} +#nullable disable \ No newline at end of file diff --git a/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs b/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs index 77021e0..9f1b11f 100644 --- a/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs +++ b/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs @@ -1,8 +1,11 @@ -using Microsoft.AspNetCore.Http; +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Serilog.Core; using Serilog.Events; -using System; +using Serilog.Preparers.CorrelationIds; using Xunit; namespace Serilog.Enrichers.ClientInfo.Tests; @@ -15,18 +18,44 @@ public class CorrelationIdEnricherTests public CorrelationIdEnricherTests() { - DefaultHttpContext httpContext = new(); + IServiceProvider serviceProvider = Substitute.For(); + serviceProvider.GetService().ReturnsNull(); + + DefaultHttpContext httpContext = new DefaultHttpContext + { + RequestServices = serviceProvider + }; _contextAccessor = Substitute.For(); _contextAccessor.HttpContext.Returns(httpContext); } [Fact] - public void EnrichLogWithCorrelationId_WhenHttpRequestContainCorrelationHeader_ShouldCreateCorrelationIdProperty() + public void EnrichLogWithCorrelationId_WhenRequestServicesContainsICorrelationIdPreparer_ShouldUseCorrelationIdPreparerFromRequestServices() { // Arrange string correlationId = Guid.NewGuid().ToString(); - _contextAccessor.HttpContext!.Request!.Headers[HeaderKey] = correlationId; - CorrelationIdEnricher correlationIdEnricher = new(HeaderKey, false, _contextAccessor); + + ICorrelationIdPreparer correlationIdPreparer = Substitute.For(); + IServiceProvider serviceProvider = Substitute.For(); + + DefaultHttpContext httpContext = new DefaultHttpContext + { + RequestServices = serviceProvider + }; + CorrelationIdPreparerOptions correlationIdPreparerOptions = new CorrelationIdPreparerOptions(false, HeaderKey); + + correlationIdPreparer.PrepareCorrelationId( + httpContext, + Arg.Is(x => + x.AddValueIfHeaderAbsence == correlationIdPreparerOptions.AddValueIfHeaderAbsence && + x.HeaderKey == correlationIdPreparerOptions.HeaderKey)) + .Returns(correlationId); + + serviceProvider.GetService().Returns(correlationIdPreparer); + IHttpContextAccessor contextAccessor = Substitute.For(); + contextAccessor.HttpContext.Returns(httpContext); + + CorrelationIdEnricher correlationIdEnricher = new(correlationIdPreparerOptions.HeaderKey, correlationIdPreparerOptions.AddValueIfHeaderAbsence, contextAccessor); LogEvent evt = null; Logger log = new LoggerConfiguration() @@ -44,8 +73,7 @@ public void EnrichLogWithCorrelationId_WhenHttpRequestContainCorrelationHeader_S } [Fact] - public void - EnrichLogWithCorrelationId_WhenHttpRequestContainCorrelationHeader_ShouldCreateCorrelationIdPropertyHasValue() + public void EnrichLogWithCorrelationId_WhenHttpRequestContainCorrelationHeader_ShouldCreateCorrelationIdProperty() { // Arrange string correlationId = Guid.NewGuid().ToString();