From c8cd7a7761b5173b7e6fd8f1160e504b74863560 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:35:57 +0000 Subject: [PATCH 1/3] Initial plan From 8a2e9972b51dfeef5dfe29ff071fc0512e0a6e88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:39:48 +0000 Subject: [PATCH 2/3] Initial analysis and build setup for correlation ID retrieval feature Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> --- .../Serilog.Enrichers.ClientInfo.csproj | 2 +- .../Serilog.Enrichers.ClientInfo.Tests.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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/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 - - + + From a8cd0db4dae57a8785b5840d35bde7546ba65200 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:43:55 +0000 Subject: [PATCH 3/3] Add HttpContext extension method for graceful correlation ID retrieval Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com> --- README.md | 20 +++ .../Enrichers/CorrelationIdEnricher.cs | 10 ++ .../Extensions/HttpContextExtensions.cs | 21 +++ .../CorrelationIdEnricherTests.cs | 142 ++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 src/Serilog.Enrichers.ClientInfo/Extensions/HttpContextExtensions.cs 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/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