Skip to content

Add HttpContext extension method for graceful correlation ID retrieval #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Serilog.Enrichers;
public class CorrelationIdEnricher : ILogEventEnricher
{
private const string CorrelationIdItemKey = "Serilog_CorrelationId";
private const string CorrelationIdValueKey = "Serilog_CorrelationId_Value";
Copy link
Preview

Copilot AI Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key "Serilog_CorrelationId_Value" is duplicated in both the enricher and the extension. Extract it into a shared constant or static class to avoid mismatches and ease future maintenance.

Copilot uses AI. Check for mistakes.

private const string PropertyName = "CorrelationId";
private readonly string _headerKey;
private readonly bool _addValueIfHeaderAbsence;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -76,5 +85,6 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
logEvent.AddOrUpdateProperty(correlationIdProperty);

httpContext.Items.Add(CorrelationIdItemKey, correlationIdProperty);
httpContext.Items.Add(CorrelationIdValueKey, correlationId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;

namespace Serilog.Enrichers;

/// <summary>
/// Extension methods for <see cref="HttpContext"/> to access enriched values.
/// </summary>
public static class HttpContextExtensions
{
private const string CorrelationIdValueKey = "Serilog_CorrelationId_Value";

/// <summary>
/// Retrieves the correlation ID value from the current HTTP context.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>The correlation ID as a string, or null if not available.</returns>
public static string GetCorrelationId(this HttpContext httpContext)
{
return httpContext?.Items[CorrelationIdValueKey] as string;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<AssemblyName>Serilog.Enrichers.ClientInfo</AssemblyName>
<RootNamespace>Serilog</RootNamespace>
<LangVersion>latest</LangVersion>
Expand Down
142 changes: 142 additions & 0 deletions test/Serilog.Enrichers.ClientInfo.Tests/CorrelationIdEnricherTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Serilog.Enrichers;
using Serilog.Events;
using System;
using Xunit;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="nsubstitute" Version="5.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="xunit" Version="2.9.0" />
Expand Down