From c99100ed28ed87d202fb0ddc51fec4946ed3f7a5 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:21:42 +0000 Subject: [PATCH] Enhance exception logging to include inner exceptions and add corresponding unit tests --- .../Internal/Converters/ExceptionConverter.cs | 63 ++++++++------ .../Converters/ExceptionPropertyExtractor.cs | 1 + .../PowertoolsLoggerTest.cs | 84 +++++++++++++++++++ 3 files changed, 122 insertions(+), 26 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs index e57e4159..0f31e871 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs @@ -57,39 +57,50 @@ public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, Js /// The JsonSerializer options. public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) { - var exceptionType = value.GetType(); - var properties = ExceptionPropertyExtractor.ExtractProperties(value); + void WriteException(Utf8JsonWriter w, Exception ex) + { + var exceptionType = ex.GetType(); + var properties = ExceptionPropertyExtractor.ExtractProperties(ex); - if (options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull) - properties = properties.Where(prop => prop.Value != null); + if (options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull) + properties = properties.Where(prop => prop.Value != null); - var props = properties.ToArray(); - if (props.Length == 0) - return; + var props = properties.ToArray(); + if (props.Length == 0) + return; - writer.WriteStartObject(); - writer.WriteString(ApplyPropertyNamingPolicy("Type", options), exceptionType.FullName); - - foreach (var prop in props) - { - switch (prop.Value) + w.WriteStartObject(); + w.WriteString(ApplyPropertyNamingPolicy("Type", options), exceptionType.FullName); + + foreach (var prop in props) { - case IntPtr intPtr: - writer.WriteNumber(ApplyPropertyNamingPolicy(prop.Key, options), intPtr.ToInt64()); - break; - case UIntPtr uIntPtr: - writer.WriteNumber(ApplyPropertyNamingPolicy(prop.Key, options), uIntPtr.ToUInt64()); - break; - case Type propType: - writer.WriteString(ApplyPropertyNamingPolicy(prop.Key, options), propType.FullName); - break; - case string propString: - writer.WriteString(ApplyPropertyNamingPolicy(prop.Key, options), propString); - break; + switch (prop.Value) + { + case IntPtr intPtr: + w.WriteNumber(ApplyPropertyNamingPolicy(prop.Key, options), intPtr.ToInt64()); + break; + case UIntPtr uIntPtr: + w.WriteNumber(ApplyPropertyNamingPolicy(prop.Key, options), uIntPtr.ToUInt64()); + break; + case Type propType: + w.WriteString(ApplyPropertyNamingPolicy(prop.Key, options), propType.FullName); + break; + case string propString: + w.WriteString(ApplyPropertyNamingPolicy(prop.Key, options), propString); + break; + } } + + if (ex.InnerException != null) + { + w.WritePropertyName(ApplyPropertyNamingPolicy("InnerException", options)); + WriteException(w, ex.InnerException); + } + + w.WriteEndObject(); } - writer.WriteEndObject(); + WriteException(writer, value); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs index 80751d3e..1a6d59b3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs @@ -40,5 +40,6 @@ private static IEnumerable> GetBaseExceptionPropert yield return new KeyValuePair(nameof(Exception.Message), ex.Message); yield return new KeyValuePair(nameof(Exception.Source), ex.Source); yield return new KeyValuePair(nameof(Exception.StackTrace), ex.StackTrace); + yield return new KeyValuePair(nameof(Exception.InnerException), ex.InnerException); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index 1267e090..b8ab659b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -1090,6 +1090,90 @@ public void Log_WhenException_LogsExceptionDetails() s.Contains("\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"TestError\",\"source\":\"AWS.Lambda.Powertools.Logging.Tests\",\"stack_trace\":\" at AWS.Lambda.Powertools.Logging.Tests.PowertoolsLoggerTest.Log_WhenException_LogsExceptionDetails()") )); } + + [Fact] + public void Log_Inner_Exception() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(service), "Very important inner exception message")); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = Substitute.For(); + configurations.Service.Returns(service); + configurations.LogLevel.Returns(logLevel.ToString()); + + var systemWrapper = Substitute.For(); + systemWrapper.GetRandom().Returns(randomSampleRate); + + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); + + logger.LogError( + error, + "Something went wrong and we logged an exception itself with an inner exception. This is a param {arg}", + 12345); + + // Assert + systemWrapper.Received(1).LogLine(Arg.Is(s => + s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + + error.Message + "\"") + )); + + systemWrapper.Received(1).LogLine(Arg.Is(s => + s.Contains("\"level\":\"Error\",\"service\":\"" + service+ "\",\"name\":\"" + loggerName + "\",\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"Very important inner exception message (Parameter 'service')\"}}}") + )); + } + + [Fact] + public void Log_Nested_Inner_Exception() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(service), + new Exception("Very important nested inner exception message"))); + + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = Substitute.For(); + configurations.Service.Returns(service); + configurations.LogLevel.Returns(logLevel.ToString()); + + var systemWrapper = Substitute.For(); + systemWrapper.GetRandom().Returns(randomSampleRate); + + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); + + + logger.LogError( + error, + "Something went wrong and we logged an exception itself with an inner exception. This is a param {arg}", + 12345); + + // Assert + systemWrapper.Received(1).LogLine(Arg.Is(s => + s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"service\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Very important nested inner exception message\"}}}}") + )); + } [Fact] public void Log_WhenNestedException_LogsExceptionDetails()