diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs
index 37cdc1c94..e11434477 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs
@@ -133,9 +133,32 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath)
// TODO: For casing parsing to be removed from Logging v2 when we get rid of outputcase without this CorrelationIdPaths.ApiGatewayRest would not work
// TODO: This will be removed and replaced by JMesPath
- var pathWithOutputCase = correlationIdPaths[i].ToCase(_currentConfig.LoggerOutputCase);
- if (!element.TryGetProperty(pathWithOutputCase, out var childElement))
- break;
+ var pathSegment = correlationIdPaths[i];
+ JsonElement childElement;
+
+ // Try original path first (case-sensitive)
+ if (!element.TryGetProperty(pathSegment, out childElement))
+ {
+ // Try with output case transformation
+ var pathWithOutputCase = pathSegment.ToCase(_currentConfig.LoggerOutputCase);
+ if (!element.TryGetProperty(pathWithOutputCase, out childElement))
+ {
+ // Try case-insensitive match as last resort
+ var found = false;
+ foreach (var property in element.EnumerateObject())
+ {
+ if (string.Equals(property.Name, pathSegment, StringComparison.OrdinalIgnoreCase))
+ {
+ childElement = property.Value;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ break;
+ }
+ }
element = childElement;
if (i == correlationIdPaths.Length - 1)
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs
index 771f3ebb4..889ac9478 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using AWS.Lambda.Powertools.Logging.Internal;
using AWS.Lambda.Powertools.Logging.Internal.Helpers;
namespace AWS.Lambda.Powertools.Logging;
@@ -13,6 +14,22 @@ public static partial class Logger
/// The scope.
private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal);
+ ///
+ /// Gets the correlation identifier from the log context.
+ ///
+ /// The correlation identifier, or null if not set.
+ public static string CorrelationId
+ {
+ get
+ {
+ if (Scope.TryGetValue(LoggingConstants.KeyCorrelationId, out var value))
+ {
+ return value?.ToString();
+ }
+ return null;
+ }
+ }
+
///
/// Appending additional key to the log context.
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs
index 9a6cda819..99abbd46f 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs
@@ -170,6 +170,16 @@ public static void Log(this ILogger logger, LogLevel logLevel, Exception excepti
#endregion
+ ///
+ /// Gets the correlation identifier from the log context.
+ ///
+ /// The logger instance.
+ /// The correlation identifier, or null if not set.
+ public static string GetCorrelationId(this ILogger logger)
+ {
+ return Logger.CorrelationId;
+ }
+
///
/// Appending additional key to the log context.
///
diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs
index 5fc3e7179..9b0542f76 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs
@@ -4,6 +4,7 @@
using System.Linq;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.ApplicationLoadBalancerEvents;
+using Amazon.Lambda.CloudWatchEvents;
using Amazon.Lambda.CloudWatchEvents.S3Events;
using Amazon.Lambda.TestUtilities;
using AWS.Lambda.Powertools.Common;
@@ -172,6 +173,7 @@ public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys()
[InlineData(CorrelationIdPaths.ApplicationLoadBalancer)]
[InlineData(CorrelationIdPaths.EventBridge)]
[InlineData("/headers/my_request_id_header")]
+ [InlineData("/detail/correlationId")]
public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationIdPath)
{
// Arrange
@@ -213,6 +215,15 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI
}
});
break;
+ case "/detail/correlationId":
+ _testHandlers.CorrelationCloudWatchEventCustomPath(new CloudWatchEvent
+ {
+ Detail = new CwEvent
+ {
+ CorrelationId = correlationId
+ }
+ });
+ break;
}
// Assert
@@ -346,6 +357,106 @@ public void When_Setting_SamplingRate_Should_Add_Key()
));
}
+ [Fact]
+ public void CorrelationId_Property_Should_Return_CorrelationId()
+ {
+ // Arrange
+ var correlationId = Guid.NewGuid().ToString();
+
+ // Act
+ _testHandlers.CorrelationCloudWatchEventCustomPath(new CloudWatchEvent
+ {
+ Detail = new CwEvent
+ {
+ CorrelationId = correlationId
+ }
+ });
+
+ // Assert - Static Logger property
+ Assert.Equal(correlationId, Logger.CorrelationId);
+ }
+
+ [Fact]
+ public void CorrelationId_Extension_Should_Return_CorrelationId_Via_ILogger()
+ {
+ // Arrange
+ var correlationId = Guid.NewGuid().ToString();
+
+ // Act
+ _testHandlers.CorrelationIdExtensionTest(new CloudWatchEvent
+ {
+ Detail = new CwEvent
+ {
+ CorrelationId = correlationId
+ }
+ });
+
+ // Assert - The test handler will verify the extension method works
+ Assert.Equal(correlationId, Logger.CorrelationId);
+ }
+
+ [Fact]
+ public void CorrelationId_Should_Return_Null_When_Not_Set()
+ {
+ // Arrange - no correlation ID set
+
+ // Act & Assert
+ Assert.Null(Logger.CorrelationId);
+ }
+
+ [Fact]
+ public void OnEntry_WhenPropertyCasingDoesNotMatch_UsesCaseInsensitiveFallback()
+ {
+ // Arrange
+ var correlationId = Guid.NewGuid().ToString();
+
+ // This test uses a path "/detail/CORRELATIONID" (all caps)
+ // but the actual property is "correlationId" (camelCase)
+ // This should trigger the case-insensitive fallback
+
+ // Act
+ _testHandlers.CorrelationIdCaseInsensitiveFallback(new CloudWatchEvent
+ {
+ Detail = new CwEvent
+ {
+ CorrelationId = correlationId
+ }
+ });
+
+ // Assert
+ var allKeys = Logger.GetAllKeys()
+ .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value);
+
+ Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId));
+ Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId);
+ }
+
+ [Fact]
+ public void OnEntry_WhenNestedPropertyCasingDoesNotMatch_UsesCaseInsensitiveFallback()
+ {
+ // Arrange
+ var correlationId = Guid.NewGuid().ToString();
+
+ // This test uses a path with mismatched casing at multiple levels
+ // Path: "/DETAIL/CORRELATIONID" but actual properties are "detail" and "correlationId"
+
+ // Act
+ _testHandlers.CorrelationIdNestedCaseInsensitive(new CloudWatchEvent
+ {
+ Detail = new CwEvent
+ {
+ CorrelationId = correlationId
+ }
+ });
+
+ // Assert
+ var allKeys = Logger.GetAllKeys()
+ .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value);
+
+ Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId));
+ Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId);
+ }
+
[Fact]
public void When_Setting_Service_Should_Update_Key()
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs
index dd332ea4c..31cd5bd65 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs
@@ -66,6 +66,41 @@ public void CorrelationCloudWatchEvent(CloudWatchEvent cwEvent)
{
}
+ [Logging(CorrelationIdPath = "/detail/correlationId")]
+ public void CorrelationCloudWatchEventCustomPath(CloudWatchEvent cwEvent)
+ {
+ }
+
+ [Logging(CorrelationIdPath = "/detail/correlationId")]
+ public void CorrelationIdExtensionTest(CloudWatchEvent cwEvent)
+ {
+ // Test that the ILogger extension method works
+ var logger = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { }).CreateLogger(nameof(TestHandlers));
+ var correlationIdFromExtension = logger.GetCorrelationId();
+
+ // Verify it matches the static property
+ if (correlationIdFromExtension != Logger.CorrelationId)
+ {
+ throw new Exception("Extension method returned different value than static property");
+ }
+ }
+
+ [Logging(CorrelationIdPath = "/detail/CORRELATIONID")]
+ public void CorrelationIdCaseInsensitiveFallback(CloudWatchEvent cwEvent)
+ {
+ // This handler uses all caps "CORRELATIONID" in the path
+ // but the actual JSON property is "correlationId" (camelCase)
+ // This tests the case-insensitive fallback logic
+ }
+
+ [Logging(CorrelationIdPath = "/DETAIL/CORRELATIONID")]
+ public void CorrelationIdNestedCaseInsensitive(CloudWatchEvent cwEvent)
+ {
+ // This handler uses all caps for both path segments
+ // but the actual JSON properties are "detail" and "correlationId"
+ // This tests the case-insensitive fallback at multiple levels
+ }
+
[Logging(CorrelationIdPath = "/headers/my_request_id_header")]
public void CorrelationIdFromString(TestObject testObject)
{
@@ -172,6 +207,21 @@ public enum Pet
}
}
+public class CwEvent
+{
+ [JsonPropertyName("rideId")]
+ public string RideId { get; set; } = string.Empty;
+
+ [JsonPropertyName("riderId")]
+ public string RiderId { get; set; } = string.Empty;
+
+ [JsonPropertyName("riderName")]
+ public string RiderName { get; set; } = string.Empty;
+
+ [JsonPropertyName("correlationId")]
+ public string? CorrelationId { get; set; }
+}
+
public class TestServiceHandler
{
public void LogWithEnv()