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()