diff --git a/README.md b/README.md index 959f354..c3c71db 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,26 @@ or } } ``` + +#### Retrieving Correlation ID +You can easily retrieve the correlation ID from `HttpContext` using the `GetCorrelationId()` extension method: + +```csharp +public void SomeControllerAction() +{ + // This will return the correlation ID that was enriched by the CorrelationIdEnricher + var correlationId = HttpContext.GetCorrelationId(); + + // You can use this for error reporting, tracing, etc. + if (!string.IsNullOrEmpty(correlationId)) + { + // Show correlation ID to user for error reporting + // or use it for additional logging/tracing + } +} +``` + +This eliminates the need for manual casting and provides a clean API for accessing correlation IDs. ### RequestHeader You can use multiple `WithRequestHeader` to log different request headers. `WithRequestHeader` accepts two parameters; The first parameter `headerName` is the header name to log and the second parameter is `propertyName` which is the log property name. diff --git a/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs b/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs index dfa82d1..187dcca 100644 --- a/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs +++ b/src/Serilog.Enrichers.ClientInfo/Enrichers/CorrelationIdEnricher.cs @@ -9,6 +9,7 @@ namespace Serilog.Enrichers; public class CorrelationIdEnricher : ILogEventEnricher { private const string CorrelationIdItemKey = "Serilog_CorrelationId"; + private const string CorrelationIdValueKey = "Serilog_CorrelationId_Value"; private const string PropertyName = "CorrelationId"; private readonly string _headerKey; private readonly bool _addValueIfHeaderAbsence; @@ -47,6 +48,14 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) if (httpContext.Items[CorrelationIdItemKey] is LogEventProperty logEventProperty) { logEvent.AddPropertyIfAbsent(logEventProperty); + + // Ensure the string value is also available if not already stored + if (!httpContext.Items.ContainsKey(CorrelationIdValueKey)) + { + var correlationIdValue = ((ScalarValue)logEventProperty.Value).Value as string; + httpContext.Items.Add(CorrelationIdValueKey, correlationIdValue); + } + return; } @@ -76,5 +85,6 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) logEvent.AddOrUpdateProperty(correlationIdProperty); httpContext.Items.Add(CorrelationIdItemKey, correlationIdProperty); + httpContext.Items.Add(CorrelationIdValueKey, correlationId); } } \ No newline at end of file diff --git a/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs b/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000..63eda75 --- /dev/null +++ b/src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; + +namespace Serilog.Enrichers; + +/// +/// Extension methods for to access enriched values. +/// +public static class HttpContextExtensions +{ + private const string CorrelationIdValueKey = "Serilog_CorrelationId_Value"; + + /// + /// Retrieves the correlation ID value from the current HTTP context. + /// + /// The HTTP context. + /// The correlation ID as a string, or null if not available. + public static string GetCorrelationId(this HttpContext httpContext) + { + return httpContext?.Items[CorrelationIdValueKey] as string; + } +} \ No newline at end of file diff --git a/src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj b/src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj index a06cc56..1dbbf2a 100644 --- a/src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj +++ b/src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj @@ -1,6 +1,6 @@ - net8.0;net9.0 + net8.0 Serilog.Enrichers.ClientInfo Serilog latest diff --git a/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs b/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs index 34c1793..0f52e4d 100644 --- a/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs +++ b/test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using NSubstitute; +using Serilog.Enrichers; using Serilog.Events; using System; using Xunit; @@ -170,4 +171,145 @@ public void WithClientIp_ThenLoggerIsCalled_ShouldNotThrowException() // Assert Assert.Null(exception); } + + [Fact] + public void GetCorrelationId_WhenHttpRequestContainCorrelationHeader_ShouldReturnCorrelationIdFromHttpContext() + { + // Arrange + var correlationId = Guid.NewGuid().ToString(); + _contextAccessor.HttpContext!.Request!.Headers[HeaderKey] = correlationId; + var correlationIdEnricher = new CorrelationIdEnricher(HeaderKey, false, _contextAccessor); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .Enrich.With(correlationIdEnricher) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + // Act + log.Information(@"Has a correlation id."); + var retrievedCorrelationId = _contextAccessor.HttpContext!.GetCorrelationId(); + + // Assert + Assert.NotNull(evt); + Assert.Equal(correlationId, retrievedCorrelationId); + } + + [Fact] + public void GetCorrelationId_WhenHttpRequestNotContainCorrelationHeaderAndAddDefaultValueIsTrue_ShouldReturnGeneratedCorrelationIdFromHttpContext() + { + // Arrange + var correlationIdEnricher = new CorrelationIdEnricher(HeaderKey, true, _contextAccessor); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .Enrich.With(correlationIdEnricher) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + // Act + log.Information(@"Has a correlation id."); + var retrievedCorrelationId = _contextAccessor.HttpContext!.GetCorrelationId(); + + // Assert + Assert.NotNull(evt); + Assert.NotNull(retrievedCorrelationId); + Assert.NotEmpty(retrievedCorrelationId); + // Verify it's a valid GUID format + Assert.True(Guid.TryParse(retrievedCorrelationId, out _)); + } + + [Fact] + public void GetCorrelationId_WhenHttpRequestNotContainCorrelationHeaderAndAddDefaultValueIsFalse_ShouldReturnNullFromHttpContext() + { + // Arrange + var correlationIdEnricher = new CorrelationIdEnricher(HeaderKey, false, _contextAccessor); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .Enrich.With(correlationIdEnricher) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + // Act + log.Information(@"Has a correlation id."); + var retrievedCorrelationId = _contextAccessor.HttpContext!.GetCorrelationId(); + + // Assert + Assert.NotNull(evt); + Assert.Null(retrievedCorrelationId); + } + + [Fact] + public void GetCorrelationId_WhenCalledMultipleTimes_ShouldReturnSameCorrelationId() + { + // Arrange + var correlationId = Guid.NewGuid().ToString(); + _contextAccessor.HttpContext!.Request!.Headers[HeaderKey] = correlationId; + var correlationIdEnricher = new CorrelationIdEnricher(HeaderKey, false, _contextAccessor); + + var log = new LoggerConfiguration() + .Enrich.With(correlationIdEnricher) + .WriteTo.Sink(new DelegatingSink(_ => { })) + .CreateLogger(); + + // Act + log.Information(@"First log message."); + var firstRetrieval = _contextAccessor.HttpContext!.GetCorrelationId(); + + log.Information(@"Second log message."); + var secondRetrieval = _contextAccessor.HttpContext!.GetCorrelationId(); + + // Assert + Assert.Equal(correlationId, firstRetrieval); + Assert.Equal(correlationId, secondRetrieval); + Assert.Equal(firstRetrieval, secondRetrieval); + } + + [Fact] + public void GetCorrelationId_WhenHttpContextIsNull_ShouldReturnNull() + { + // Arrange & Act + var result = HttpContextExtensions.GetCorrelationId(null); + + // Assert + Assert.Null(result); + } + + [Fact] + public void EnrichLogWithCorrelationId_BackwardCompatibility_OldRetrievalMethodShouldStillWork() + { + // Arrange + var correlationId = Guid.NewGuid().ToString(); + _contextAccessor.HttpContext!.Request!.Headers[HeaderKey] = correlationId; + var correlationIdEnricher = new CorrelationIdEnricher(HeaderKey, false, _contextAccessor); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .Enrich.With(correlationIdEnricher) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + // Act + log.Information(@"Has a correlation id."); + + // Test that the old way (hacky way) still works + var httpContext = _contextAccessor.HttpContext!; + string retrievedCorrelationIdOldWay = null; + + if (httpContext.Items.TryGetValue("Serilog_CorrelationId", out var correlationIdItem) && + correlationIdItem is LogEventProperty { Name: "CorrelationId" } correlationIdProperty) + { + retrievedCorrelationIdOldWay = ((ScalarValue)correlationIdProperty.Value).Value as string; + } + + // Test that the new way also works + var retrievedCorrelationIdNewWay = httpContext.GetCorrelationId(); + + // Assert + Assert.NotNull(evt); + Assert.Equal(correlationId, retrievedCorrelationIdOldWay); + Assert.Equal(correlationId, retrievedCorrelationIdNewWay); + Assert.Equal(retrievedCorrelationIdOldWay, retrievedCorrelationIdNewWay); + } } \ No newline at end of file diff --git a/test/Serilog.Enrichers.ClientInfo.Tests/Serilog.Enrichers.ClientInfo.Tests.csproj b/test/Serilog.Enrichers.ClientInfo.Tests/Serilog.Enrichers.ClientInfo.Tests.csproj index dc3f541..4b62ef0 100644 --- a/test/Serilog.Enrichers.ClientInfo.Tests/Serilog.Enrichers.ClientInfo.Tests.csproj +++ b/test/Serilog.Enrichers.ClientInfo.Tests/Serilog.Enrichers.ClientInfo.Tests.csproj @@ -1,13 +1,13 @@  - net9.0 + net8.0 false - - + +