Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion sample/SampleWebApp/SampleWebApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Enrichers.ClientInfo" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Serilog.Enrichers.ClientInfo\Serilog.Enrichers.ClientInfo.csproj" />
</ItemGroup>
</Project>
47 changes: 22 additions & 25 deletions src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
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;

/// <inheritdoc />
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;

/// <summary>
/// Initializes a new instance of the <see cref="CorrelationIdEnricher" /> class.
Expand All @@ -24,15 +23,22 @@ public class CorrelationIdEnricher : ILogEventEnricher
/// <param name="addValueIfHeaderAbsence">
/// Determines whether to add a new correlation ID value if the header is absent.
/// </param>
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;
}

Expand All @@ -42,39 +48,30 @@ 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);

// 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);

httpContext.Items.Add(CorrelationIdItemKey, correlationIdProperty);
httpContext.Items.Add(Constants.CorrelationIdValueKey, correlationId);
}
}
}
#nullable disable
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Preparers.CorrelationIds;

namespace Serilog.Enrichers;

Expand All @@ -14,4 +16,12 @@ public static class HttpContextExtensions
/// <returns>The correlation ID as a string, or null if not available.</returns>
public static string GetCorrelationId(this HttpContext httpContext)
=> httpContext?.Items[Constants.CorrelationIdValueKey] as string;

/// <summary>
/// Retrieves the correlation ID preparer for processing the current HTTP context.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>Correlation ID preparer.</returns>
internal static ICorrelationIdPreparer GetCorrelationIdPreparer(this HttpContext httpContext)
=> httpContext.RequestServices.GetService<ICorrelationIdPreparer>() ?? new CorrelationIdPreparer();
}
Original file line number Diff line number Diff line change
@@ -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<string?> CorrelationId { get; } = new AsyncLocal<string?>();

/// <inheritdoc/>
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Serilog.Preparers.CorrelationIds
{
/// <summary>
/// Settings for <see cref="ICorrelationIdPreparer"/>.
/// </summary>
public class CorrelationIdPreparerOptions
{
/// <summary>
/// Determines whether to add a new correlation ID value if the header is absent.
/// </summary>
public bool AddValueIfHeaderAbsence { get; }

/// <summary>
/// The header key used to retrieve the correlation ID from the HTTP request or response headers.
/// </summary>
public string HeaderKey { get; }

/// <summary>
/// Initializes a new instance of the <see cref="CorrelationIdPreparerOptions" /> class.
/// </summary>
/// <param name="addValueIfHeaderAbsence">Determines whether to add a new correlation ID value if the header is absent.</param>
/// <param name="headerKey">The header key used to retrieve the correlation ID from the HTTP request or response headers.</param>
public CorrelationIdPreparerOptions(
bool addValueIfHeaderAbsence,
string headerKey)
{
AddValueIfHeaderAbsence = addValueIfHeaderAbsence;
HeaderKey = headerKey;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Http;

#nullable enable
namespace Serilog.Preparers.CorrelationIds
{
/// <summary>
/// Preparer for correlation ID.
/// </summary>
public interface ICorrelationIdPreparer
{
/// <summary>
/// Prepares the correlation ID.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="correlationIdPreparerOptions">Options for preparation.</param>
/// <returns>The correlation ID.</returns>
string? PrepareCorrelationId(
HttpContext httpContext,
CorrelationIdPreparerOptions correlationIdPreparerOptions);
}
}
#nullable disable
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,18 +18,44 @@ public class CorrelationIdEnricherTests

public CorrelationIdEnricherTests()
{
DefaultHttpContext httpContext = new();
IServiceProvider serviceProvider = Substitute.For<IServiceProvider>();
serviceProvider.GetService<ICorrelationIdPreparer>().ReturnsNull();

DefaultHttpContext httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
_contextAccessor = Substitute.For<IHttpContextAccessor>();
_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<ICorrelationIdPreparer>();
IServiceProvider serviceProvider = Substitute.For<IServiceProvider>();

DefaultHttpContext httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
CorrelationIdPreparerOptions correlationIdPreparerOptions = new CorrelationIdPreparerOptions(false, HeaderKey);

correlationIdPreparer.PrepareCorrelationId(
httpContext,
Arg.Is<CorrelationIdPreparerOptions>(x =>
x.AddValueIfHeaderAbsence == correlationIdPreparerOptions.AddValueIfHeaderAbsence &&
x.HeaderKey == correlationIdPreparerOptions.HeaderKey))
.Returns(correlationId);

serviceProvider.GetService<ICorrelationIdPreparer>().Returns(correlationIdPreparer);
IHttpContextAccessor contextAccessor = Substitute.For<IHttpContextAccessor>();
contextAccessor.HttpContext.Returns(httpContext);

CorrelationIdEnricher correlationIdEnricher = new(correlationIdPreparerOptions.HeaderKey, correlationIdPreparerOptions.AddValueIfHeaderAbsence, contextAccessor);

LogEvent evt = null;
Logger log = new LoggerConfiguration()
Expand All @@ -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();
Expand Down