From ec0b73a7ae9ece9b0fed5b8b41009be8fe720635 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:19:19 +0100 Subject: [PATCH 1/5] add new LogLevelJsonConverter and remove generic JsonEnumConverter --- .../Serializers/LogLevelJsonConverter.cs | 19 ++++++++ .../PowertoolsLoggingSerializer.cs | 4 +- .../Formatter/LogFormatterTest.cs | 48 ++++++++++++++++++- .../Handlers/TestHandlers.cs | 23 +++++++++ .../PowertoolsLoggingSerializerTests.cs | 8 ++-- 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LogLevelJsonConverter.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LogLevelJsonConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LogLevelJsonConverter.cs new file mode 100644 index 00000000..934f6f1b --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LogLevelJsonConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Serializers; + +internal class LogLevelJsonConverter : JsonConverter +{ + public override LogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Enum.TryParse(reader.GetString(),true, out var val) ? val : default; + } + + public override void Write(Utf8JsonWriter writer, LogLevel value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 54958829..97aabc06 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -153,9 +153,9 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() _jsonOptions.Converters.Add(new TimeOnlyConverter()); #if NET8_0_OR_GREATER - _jsonOptions.Converters.Add(new JsonStringEnumConverter()); + _jsonOptions.Converters.Add(new LogLevelJsonConverter()); #elif NET6_0 - _jsonOptions.Converters.Add(new JsonStringEnumConverter()); + _jsonOptions.Converters.Add(new LogLevelJsonConverter()); #endif _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index 4a61a2a4..b9bc8708 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -19,6 +19,7 @@ using System.Linq; using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; @@ -42,7 +43,50 @@ public LogFormatterTest() { _testHandler = new TestHandlers(); } - + + [Fact] + public void Serialize_ShouldHandleEnumValues() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + var lambdaContext = new TestLambdaContext + { + FunctionName = "funtionName", + FunctionVersion = "version", + InvokedFunctionArn = "function::arn", + AwsRequestId = "requestId", + MemoryLimitInMB = 128 + }; + + var handler = new TestHandlers(); + handler.TestEnums("fake", lambdaContext); + + consoleOut.Received(1).WriteLine(Arg.Is(i => + i.Contains("\"message\":5") + )); + consoleOut.Received(1).WriteLine(Arg.Is(i => + i.Contains("\"message\":\"Dog\"") + )); + + var json = JsonSerializer.Serialize(Pet.Dog, PowertoolsLoggingSerializer.GetSerializerOptions()); + Assert.Contains("Dog", json); + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum Pet + { + Cat = 1, + Dog = 3, + Lizard = 5 + } + + public enum Thing + { + One = 1, + Three = 3, + Five = 5 + } + [Fact] public void Log_WhenCustomFormatter_LogsCustomFormat() { @@ -204,7 +248,7 @@ public void Should_Log_CustomFormatter_When_Decorated() consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains( - "\"correlation_ids\":{\"aws_request_id\":\"requestId\"},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_mb\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\"")) + "\"correlation_ids\":{\"aws_request_id\":\"requestId\"},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_mb\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\"")) ); #else consoleOut.Received(1).WriteLine( 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 990fa103..d22a2eb1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System.Text.Json.Serialization; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents; @@ -158,4 +159,26 @@ public void TestLogNoDecorator() { Logger.LogInformation("test"); } + + [Logging(Service = "test", LoggerOutputCase = LoggerOutputCase.SnakeCase)] + public void TestEnums(string input, ILambdaContext context) + { + Logger.LogInformation(Pet.Dog); + Logger.LogInformation(Thing.Five); + } + + public enum Thing + { + One = 1, + Three = 3, + Five = 5 + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum Pet + { + Cat = 1, + Dog = 3, + Lizard = 5 + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs index 8c539036..f8e1cd48 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -62,9 +62,9 @@ public void SerializerOptions_ShouldHaveCorrectDefaultSettings() converter => Assert.IsType(converter), converter => Assert.IsType(converter), #if NET8_0_OR_GREATER - converter => Assert.IsType>(converter)); + converter => Assert.IsType(converter)); #elif NET6_0 - converter => Assert.IsType(converter)); + converter => Assert.IsType(converter)); #endif Assert.Equal(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder); @@ -90,9 +90,9 @@ public void SerializerOptions_ShouldHaveCorrectDefaultSettings_WhenDynamic() converter => Assert.IsType(converter), converter => Assert.IsType(converter), #if NET8_0_OR_GREATER - converter => Assert.IsType>(converter)); + converter => Assert.IsType(converter)); #elif NET6_0 - converter => Assert.IsType(converter)); + converter => Assert.IsType(converter)); #endif Assert.Equal(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder); From 1b5cd580a499b0889a3d248c5baa0d016d6cc993 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:34:22 +0100 Subject: [PATCH 2/5] update objecttodictionary to accomodate for enums --- .../Helpers/PowertoolsLoggerHelpers.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs index 4404a3a2..1245f8d6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -13,8 +13,11 @@ * permissions and limitations under the License. */ +using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; @@ -38,18 +41,31 @@ internal static object ObjectToDictionary(object anonymousObject) return new Dictionary(); } - if (anonymousObject.GetType().Namespace is not null) + var type = anonymousObject.GetType(); + + if (type.IsEnum) + { + return anonymousObject; + } + + if (type.Namespace != null && !type.IsEnum) { return anonymousObject; } - return anonymousObject.GetType().GetProperties() + return type.GetProperties() .Where(prop => prop.GetValue(anonymousObject, null) != null) .ToDictionary( prop => prop.Name, prop => { var value = prop.GetValue(anonymousObject, null); - return value != null ? ObjectToDictionary(value) : string.Empty; + if (value == null) + return string.Empty; + + if (value.GetType().IsEnum) + return value; + + return ObjectToDictionary(value); } ); } From fb37a54f7a9e2421dee1b08100a579d180bcaa75 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:20:56 +0100 Subject: [PATCH 3/5] Update version for logging release Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index ded38529..6ea697d7 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "Core": { - "Logging": "1.6.1", + "Logging": "1.6.2", "Metrics": "1.8.0", "Tracing": "1.5.2" }, From 3534700d47d4d790334200ea90835b6ecfe53a27 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:43:11 +0100 Subject: [PATCH 4/5] add more test coverage --- .../Utilities/PowertoolsLoggerHelpersTests.cs | 106 +++++++++++++++--- 1 file changed, 92 insertions(+), 14 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index ed29a772..399792a5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -1,4 +1,3 @@ - #if NET8_0_OR_GREATER using System; @@ -70,57 +69,136 @@ public void ObjectToDictionary_NullObject_Return_New_Dictionary() // Act & Assert Assert.NotNull(() => PowertoolsLoggerHelpers.ObjectToDictionary(null)); } - + [Fact] public void Should_Log_With_Anonymous() { var consoleOut = Substitute.For(); SystemWrapper.Instance.SetOut(consoleOut); - + // Act & Assert - Logger.AppendKey("newKey", new + Logger.AppendKey("newKey", new { name = "my name" }); - + Logger.LogInformation("test"); - + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"new_key\":{\"name\":\"my name\"}")) ); } - + [Fact] public void Should_Log_With_Complex_Anonymous() { var consoleOut = Substitute.For(); SystemWrapper.Instance.SetOut(consoleOut); - + // Act & Assert - Logger.AppendKey("newKey", new + Logger.AppendKey("newKey", new { id = 1, name = "my name", - Adresses = new { + Adresses = new + { street = "street 1", number = 1, - city = new + city = new { name = "city 1", state = "state 1" } } }); - + Logger.LogInformation("test"); - + consoleOut.Received(1).WriteLine( Arg.Is(i => - i.Contains("\"new_key\":{\"id\":1,\"name\":\"my name\",\"adresses\":{\"street\":\"street 1\",\"number\":1,\"city\":{\"name\":\"city 1\",\"state\":\"state 1\"}")) + i.Contains( + "\"new_key\":{\"id\":1,\"name\":\"my name\",\"adresses\":{\"street\":\"street 1\",\"number\":1,\"city\":{\"name\":\"city 1\",\"state\":\"state 1\"}")) ); } + [Fact] + public void ObjectToDictionary_EnumValue_ReturnsOriginalEnum() + { + // Arrange + var enumValue = DayOfWeek.Monday; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(enumValue); + + // Assert + Assert.Equal(enumValue, result); + } + + [Fact] + public void ObjectToDictionary_ObjectWithNullProperty_ExcludesNullProperty() + { + // Arrange + var objectWithNull = new { name = (string)null, age = 30 }; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(objectWithNull); + + // Assert + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Single(dictionary); + Assert.Equal(30, dictionary["age"]); + Assert.False(dictionary.ContainsKey("name")); + } + + [Fact] + public void ObjectToDictionary_EmptyAnonymousObject_ReturnsEmptyDictionary() + { + // Arrange + var emptyObject = new { }; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(emptyObject); + + // Assert + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Empty(dictionary); + } + + [Fact] + public void ObjectToDictionary_ObjectWithNestedEnum_ReturnsDictionaryWithEnum() + { + // Arrange + var objectWithEnum = new { name = "test", day = DayOfWeek.Friday }; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(objectWithEnum); + + // Assert + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Equal(2, dictionary.Count); + Assert.Equal("test", dictionary["name"]); + Assert.Equal(DayOfWeek.Friday, dictionary["day"]); + } + + [Fact] + public void ObjectToDictionary_ObjectWithAllNullProperties_ReturnsEmptyDictionary() + { + // Arrange + var allNullObject = new { name = (string)null, address = (string)null }; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(allNullObject); + + // Assert + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Empty(dictionary); + } + public void Dispose() { PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.Default); From c8684e8962ab0932419a8c3a4c88c1d0f11d62a3 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:47:13 +0100 Subject: [PATCH 5/5] add tests for LogLevelJsonConverter --- .../Utilities/LogLevelJsonConverterTests.cs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/LogLevelJsonConverterTests.cs diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/LogLevelJsonConverterTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/LogLevelJsonConverterTests.cs new file mode 100644 index 00000000..8a58a839 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/LogLevelJsonConverterTests.cs @@ -0,0 +1,141 @@ +using System.Text.Json; +using AWS.Lambda.Powertools.Logging.Serializers; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +public class LogLevelJsonConverterTests +{ + private readonly LogLevelJsonConverter _converter; + private readonly JsonSerializerOptions _options; + + public LogLevelJsonConverterTests() + { + _converter = new LogLevelJsonConverter(); + _options = new JsonSerializerOptions + { + Converters = { _converter } + }; + } + + [Theory] + [InlineData("Information", LogLevel.Information)] + [InlineData("Error", LogLevel.Error)] + [InlineData("Warning", LogLevel.Warning)] + [InlineData("Debug", LogLevel.Debug)] + [InlineData("Trace", LogLevel.Trace)] + [InlineData("Critical", LogLevel.Critical)] + [InlineData("None", LogLevel.None)] + public void Read_ValidLogLevel_ReturnsCorrectEnum(string input, LogLevel expected) + { + // Arrange + var json = $"\"{input}\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("information", LogLevel.Information)] + [InlineData("ERROR", LogLevel.Error)] + [InlineData("Warning", LogLevel.Warning)] + [InlineData("deBUG", LogLevel.Debug)] + public void Read_CaseInsensitive_ReturnsCorrectEnum(string input, LogLevel expected) + { + // Arrange + var json = $"\"{input}\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("")] + [InlineData("InvalidLevel")] + [InlineData("NotALevel")] + public void Read_InvalidLogLevel_ReturnsDefault(string input) + { + // Arrange + var json = $"\"{input}\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(default(LogLevel), result); + } + + [Fact] + public void Read_NullValue_ReturnsDefault() + { + // Arrange + var json = "null"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(default(LogLevel), result); + } + + [Theory] + [InlineData(LogLevel.Information, "Information")] + [InlineData(LogLevel.Error, "Error")] + [InlineData(LogLevel.Warning, "Warning")] + [InlineData(LogLevel.Debug, "Debug")] + [InlineData(LogLevel.Trace, "Trace")] + [InlineData(LogLevel.Critical, "Critical")] + [InlineData(LogLevel.None, "None")] + public void Write_ValidLogLevel_WritesCorrectString(LogLevel input, string expected) + { + // Act + var result = JsonSerializer.Serialize(input, _options); + + // Assert + Assert.Equal($"\"{expected}\"", result); + } + + [Fact] + public void Write_DefaultLogLevel_WritesCorrectString() + { + // Arrange + var input = default(LogLevel); + + // Act + var result = JsonSerializer.Serialize(input, _options); + + // Assert + Assert.Equal($"\"{input}\"", result); + } + + [Fact] + public void Converter_CanConvert_LogLevelType() + { + // Act + var canConvert = _converter.CanConvert(typeof(LogLevel)); + + // Assert + Assert.True(canConvert); + } + + [Fact] + public void SerializeAndDeserialize_RoundTrip_MaintainsValue() + { + // Arrange + var originalValue = LogLevel.Information; + + // Act + var serialized = JsonSerializer.Serialize(originalValue, _options); + var deserialized = JsonSerializer.Deserialize(serialized, _options); + + // Assert + Assert.Equal(originalValue, deserialized); + } +} \ No newline at end of file