From ea445ab72ddb68d7637b1ad2d839207f495659ad Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 2 May 2025 14:18:05 -0700 Subject: [PATCH 1/4] Lambda Logging improvements In Amazon.Lambda.Logging.AspNetCore convert the ASP.NET Core log level into the Lambda log level and then pass the level into the logging method Update Amazon.Lambda.TestUtitlies to have implementation of the newer logging methods Add a new static global static logging method that takes in log level, message, parameters and exception. Passing in exception in the global static logger was missing. --- .../16cc3c6a-8fa0-410b-ba57-74221abb086a.json | 48 +++++++ .../src/Amazon.Lambda.Core/LambdaLogger.cs | 41 +++++- .../Amazon.Lambda.Logging.AspNetCore.csproj | 2 +- .../LambdaILogger.cs | 117 +++++++++++------- .../Helpers/ConsoleLoggerWriter.cs | 10 +- .../Amazon.Lambda.TestUtilities.csproj | 2 +- .../TestLambdaLogger.cs | 50 +++++++- .../Amazon.Lambda.Core.Tests.csproj | 5 +- .../LoggingTests.cs | 18 ++- .../BaseCustomRuntimeTest.cs | 4 +- .../CustomRuntimeTests.cs | 16 +++ .../CustomRuntimeFunction.cs | 13 +- .../TestFunctionCSharp.csproj | 2 +- .../TestFunctionFSharp.fsproj | 2 +- 14 files changed, 266 insertions(+), 64 deletions(-) create mode 100644 .autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json diff --git a/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json b/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json new file mode 100644 index 000000000..7a830b4b3 --- /dev/null +++ b/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json @@ -0,0 +1,48 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Patch", + "ChangelogMessages": [ + "Add support for parameterized logging method with exception to global logger LambdaLogger in Amazon.Lambda.Core" + ] + }, + { + "Name": "Amazon.Lambda.Core", + "Type": "Minor", + "ChangelogMessages": [ + "Added log level version of the static logging functions on Amazon.Lambda.Core.LambdaLogger" + ] + } + { + "Name": "Amazon.Lambda.AspNetCoreServer", + "Type": "Patch", + "ChangelogMessages": [ + "Update Amazon.Lambda.Logging.AspNetCore dependency" + ] + }, + { + "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", + "Type": "Patch", + "ChangelogMessages": [ + "Update Amazon.Lambda.Logging.AspNetCore dependency" + ] + }, + { + "Name": "Amazon.Lambda.Logging.AspNetCore", + "Type": "Minor", + "ChangelogMessages": [ + "Add support Lambda log levels", + "Change build target from .NET Standard 2.0 to .NET 6 and NET 8 to match Amazon.Lambda.AspNetCoreServer" + ] + }, + { + "Name": "Amazon.Lambda.TestUtilities", + "Type": "Minor", + "ChangelogMessages": [ + "Update Amazon.Lambda.TestUtitlies to have implementation of the newer logging methods", + "Change build target from .NET Standard 2.0 to .NET 6 and NET 8 to match Amazon.Lambda.AspNetCoreServer" + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs index c6b612bcf..8cb4d128e 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs @@ -42,6 +42,7 @@ public static void Log(string message) // value with an Action that directs the logging into its logging system. #pragma warning disable IDE0044 // Add readonly modifier private static Action _loggingWithLevelAction = LogWithLevelToConsole; + private static Action _loggingWithLevelAndExceptionAction = LogWithLevelAndExceptionToConsole; #pragma warning restore IDE0044 // Add readonly modifier // Logs message to console @@ -65,6 +66,15 @@ private static void LogWithLevelToConsole(string level, string message, params o Console.WriteLine(sb.ToString()); } + private static void LogWithLevelAndExceptionToConsole(string level, Exception exception, string message, params object[] args) + { + // Formatting here is not important, it is used for debugging Amazon.Lambda.Core only. + // In a real scenario Amazon.Lambda.RuntimeSupport will change the value of _loggingWithLevelAction + // to an Action inside it's logging system to handle the real formatting. + LogWithLevelToConsole(level, message, args); + Console.WriteLine(exception); + } + private const string ParameterizedPreviewMessage = "This method has been mark as preview till the Lambda .NET Managed runtime has been updated with the backing implementation of this method. " + "It is possible to use this method while in preview if the Lambda function is deployed as an executable and uses the latest version of Amazon.Lambda.RuntimeSupport."; @@ -78,7 +88,6 @@ private static void LogWithLevelToConsole(string level, string message, params o /// The log level of the message /// Message to log. The message may have format arguments. /// Arguments to format the message with. - [RequiresPreviewFeatures(ParameterizedPreviewMessage)] public static void Log(string level, string message, params object[] args) { _loggingWithLevelAction(level, message, args); @@ -93,8 +102,36 @@ public static void Log(string level, string message, params object[] args) /// The log level of the message /// Message to log. The message may have format arguments. /// Arguments to format the message with. - [RequiresPreviewFeatures(ParameterizedPreviewMessage)] public static void Log(LogLevel level, string message, params object[] args) => Log(level.ToString(), message, args); + + /// + /// Logs a message to AWS CloudWatch Logs. + /// + /// Logging will not be done: + /// If the role provided to the function does not have sufficient permissions. + /// + /// The log level of the message + /// Exception to include with the logging. + /// Message to log. The message may have format arguments. + /// Arguments to format the message with. + [RequiresPreviewFeatures(ParameterizedPreviewMessage)] + public static void Log(string level, Exception exception, string message, params object[] args) + { + _loggingWithLevelAndExceptionAction(level, exception, message, args); + } + + /// + /// Logs a message to AWS CloudWatch Logs. + /// + /// Logging will not be done: + /// If the role provided to the function does not have sufficient permissions. + /// + /// The log level of the message + /// Exception to include with the logging. + /// Message to log. The message may have format arguments. + /// Arguments to format the message with. + [RequiresPreviewFeatures(ParameterizedPreviewMessage)] + public static void Log(LogLevel level, Exception exception, string message, params object[] args) => Log(level.ToString(), exception, message, args); #endif } } diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj index 7d394b38a..86a94e2f5 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj @@ -4,7 +4,7 @@ Amazon Lambda .NET Core support - Logging ASP.NET Core package. - netstandard2.0 + net6.0;net8.0 Amazon.Lambda.Logging.AspNetCore 3.1.1 Amazon.Lambda.Logging.AspNetCore diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs index 7e6de8bab..40e7d5923 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Microsoft.Extensions.Logging @@ -29,53 +29,74 @@ public bool IsEnabled(LogLevel logLevel) _options.Filter(_categoryName, logLevel)); } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - if (formatter == null) - { - throw new ArgumentNullException(nameof(formatter)); - } - - if (!IsEnabled(logLevel)) - { - return; - } - - // Format of the logged text, optional components are in {} - // {[LogLevel] }{ => Scopes : }{Category: }{EventId: }MessageText {Exception}{\n} - - var components = new List(4); - if (_options.IncludeLogLevel) - { - components.Add($"[{logLevel}]"); - } - - GetScopeInformation(components); - - if (_options.IncludeCategory) - { - components.Add($"{_categoryName}:"); - } - if (_options.IncludeEventId) - { - components.Add($"[{eventId}]:"); - } - - var text = formatter.Invoke(state, exception); - components.Add(text); - - if (_options.IncludeException) - { - components.Add($"{exception}"); - } - if (_options.IncludeNewline) - { - components.Add(Environment.NewLine); - } - - var finalText = string.Join(" ", components); - Amazon.Lambda.Core.LambdaLogger.Log(finalText); - } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + if (!IsEnabled(logLevel)) + { + return; + } + + var lambdaLogLevel = ConvertLogLevel(logLevel); + + var components = new List(4); + if (_options.IncludeLogLevel) + { + components.Add($"[{logLevel}]"); + } + + GetScopeInformation(components); + + if (_options.IncludeCategory) + { + components.Add($"{_categoryName}:"); + } + if (_options.IncludeEventId) + { + components.Add($"[{eventId}]:"); + } + + var text = formatter.Invoke(state, exception); + components.Add(text); + + if (_options.IncludeException) + { + components.Add($"{exception}"); + } + if (_options.IncludeNewline) + { + components.Add(Environment.NewLine); + } + + var finalText = string.Join(" ", components); + + Amazon.Lambda.Core.LambdaLogger.Log(lambdaLogLevel, finalText); + } + + private static Amazon.Lambda.Core.LogLevel ConvertLogLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return Amazon.Lambda.Core.LogLevel.Trace; + case LogLevel.Debug: + return Amazon.Lambda.Core.LogLevel.Debug; + case LogLevel.Information: + return Amazon.Lambda.Core.LogLevel.Information; + case LogLevel.Warning: + return Amazon.Lambda.Core.LogLevel.Warning; + case LogLevel.Error: + return Amazon.Lambda.Core.LogLevel.Error; + case LogLevel.Critical: + return Amazon.Lambda.Core.LogLevel.Critical; + default: + return Amazon.Lambda.Core.LogLevel.Information; + } + } private void GetScopeInformation(List logMessageComponents) { diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index 18417e562..95004dd2e 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -247,7 +247,7 @@ private void ConfigureLoggingActionField() var loggingActionField = lambdaLoggerType.GetTypeInfo().GetField("_loggingAction", BindingFlags.NonPublic | BindingFlags.Static); if (loggingActionField != null) { - Action loggingAction = (message => FormattedWriteLine(null, message)); + Action loggingAction = message => FormattedWriteLine(null, message); loggingActionField.SetValue(null, loggingAction); } @@ -255,9 +255,15 @@ private void ConfigureLoggingActionField() var loggingWithLevelActionField = lambdaLoggerType.GetTypeInfo().GetField("_loggingWithLevelAction", BindingFlags.NonPublic | BindingFlags.Static); if (loggingWithLevelActionField != null) { - Action loggingWithLevelAction = ((level, message, args) => FormattedWriteLine(level, message, args)); + Action loggingWithLevelAction = (level, message, args) => FormattedWriteLine(level, message, args); loggingWithLevelActionField.SetValue(null, loggingWithLevelAction); + } + var loggingWithLevelAndExceptionActionField = lambdaLoggerType.GetTypeInfo().GetField("_loggingWithLevelAndExceptionAction", BindingFlags.NonPublic | BindingFlags.Static); + if (loggingWithLevelAndExceptionActionField != null) + { + Action loggingWithLevelAndExceptionAction = (level, exception, message, args) => FormattedWriteLine(level, exception, message, args); + loggingWithLevelAndExceptionActionField.SetValue(null, loggingWithLevelAndExceptionAction); } } diff --git a/Libraries/src/Amazon.Lambda.TestUtilities/Amazon.Lambda.TestUtilities.csproj b/Libraries/src/Amazon.Lambda.TestUtilities/Amazon.Lambda.TestUtilities.csproj index f15727a30..eedc42127 100644 --- a/Libraries/src/Amazon.Lambda.TestUtilities/Amazon.Lambda.TestUtilities.csproj +++ b/Libraries/src/Amazon.Lambda.TestUtilities/Amazon.Lambda.TestUtilities.csproj @@ -4,7 +4,7 @@ Amazon.Lambda.TestUtilties includes stub implementations of interfaces defined in Amazon.Lambda.Core and helper methods. - netstandard2.0 + net6.0;net8.0 Amazon.Lambda.TestUtilities 2.0.0 Amazon.Lambda.TestUtilities diff --git a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs index 20c29564a..e6626aa05 100644 --- a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -37,5 +37,53 @@ public void LogLine(string message) Buffer.AppendLine(message); Console.WriteLine(message); } + + public void Log(string level, string message) + { + var formmattedString = $"{level}: {message}"; + Buffer.AppendLine(formmattedString); + Console.WriteLine(formmattedString); + } + + public void Log(string level, string message, params object[] args) + { + var builder = new StringBuilder(); + builder.Append($"{level}: {message}"); + if (args != null && args.Length > 0) + { + builder.AppendLine(); + foreach (var arg in args) + { + builder.AppendLine($"\t{arg}"); + } + } + + var formmattedString = builder.ToString(); + Buffer.AppendLine(formmattedString); + Console.WriteLine(formmattedString); + } + + public void Log(string level, Exception exception, string message, params object[] args) + { + var builder = new StringBuilder(); + builder.Append($"{level}: {message}"); + if (args != null && args.Length > 0) + { + builder.AppendLine(); + foreach (var arg in args) + { + builder.AppendLine($"\t{arg}"); + } + } + if (exception != null) + { + builder.AppendLine(); + builder.AppendLine(exception.ToString()); + } + + var formmattedString = builder.ToString(); + Buffer.AppendLine(formmattedString); + Console.WriteLine(formmattedString); + } } } diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj b/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj index f06e3ec29..0c1665b85 100644 --- a/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Amazon.Lambda.Core.Tests Amazon.Lambda.Core.Tests true @@ -9,11 +9,12 @@ + - + diff --git a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs index 35c46cb2b..5a8bbecde 100644 --- a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs +++ b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs @@ -76,6 +76,9 @@ public void TestConfiguration() // check that there are no unexpected strings in the text Assert.DoesNotContain(SHOULD_NOT_APPEAR, text); + // Confirm log level was written to log + Assert.Contains("Critical:", text); + // check that all expected strings are in the text for (int i = 0; i < count; i++) { @@ -552,7 +555,20 @@ private static void ConnectLoggingActionToLogger(Action loggingAction) Assert.NotNull(loggingActionField); loggingActionField.SetValue(null, loggingAction); - } + + Action loggingWithLevelAction = (level, message, parameters) => { + var formattedMessage = $"{level}: {message}: parameter count: {parameters?.Length}"; + loggingAction(formattedMessage); + }; + + var loggingWithLevelActionField = lambdaLoggerType + .GetTypeInfo() + .GetField("_loggingWithLevelAction", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(loggingActionField); + + loggingWithLevelActionField.SetValue(null, loggingWithLevelAction); + } + private static int CountOccurences(string text, string substring) { int occurences = 0; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs index 5b2741b5f..e8eb5225d 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs @@ -1,4 +1,4 @@ -using Amazon.IdentityManagement; +using Amazon.IdentityManagement; using Amazon.IdentityManagement.Model; using Amazon.Lambda.Model; using Amazon.S3; @@ -267,7 +267,7 @@ protected async Task CreateFunctionAsync(IAmazonLambda lambdaClient, string buck S3Bucket = bucketName, S3Key = DeploymentZipKey }, - Handler = this.Handler, + Handler = Handler, MemorySize = FUNCTION_MEMORY_MB, Timeout = 30, Runtime = Runtime.Dotnet6, diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs index 387245e63..066428f21 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs @@ -106,6 +106,7 @@ protected virtual async Task TestAllHandlersAsync() await RunTestSuccessAsync(lambdaClient, "UnintendedDisposeTest", "not-used", "UnintendedDisposeTest-SUCCESS"); await RunTestSuccessAsync(lambdaClient, "LoggingStressTest", "not-used", "LoggingStressTest-success"); + await RunGlobalLoggingWithExceptionTestAsync(lambdaClient, "GlobalLoggingWithExceptionTest"); await RunGlobalLoggingTestAsync(lambdaClient, "GlobalLoggingTest"); await RunJsonLoggingWithUnhandledExceptionAsync(lambdaClient); @@ -330,6 +331,21 @@ private async Task RunGlobalLoggingTestAsync(AmazonLambdaClient lambdaClient, st Assert.Contains("This is a global log message with foobar as an argument", log); } + private async Task RunGlobalLoggingWithExceptionTestAsync(AmazonLambdaClient lambdaClient, string handler) + { + await UpdateHandlerAsync(lambdaClient, handler); + + var invokeResponse = await InvokeFunctionAsync(lambdaClient, JsonConvert.SerializeObject("")); + Assert.True(invokeResponse.HttpStatusCode == System.Net.HttpStatusCode.OK); + Assert.True(invokeResponse.FunctionError == null); + + var log = System.Text.UTF8Encoding.UTF8.GetString(Convert.FromBase64String(invokeResponse.LogResult)); + + Assert.Contains("This is a global log message with foobar as an argument", log); + Assert.Contains("ArgumentException", log); + Assert.Contains("This is the wrong argument", log); + } + private async Task RunUnformattedLoggingTestAsync(AmazonLambdaClient lambdaClient, string handler) { var environmentVariables = new Dictionary(); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs index 7cd6ec3b0..6d9c80f91 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs @@ -51,6 +51,9 @@ private static async Task Main(string[] args) case nameof(GlobalLoggingTest): bootstrap = new LambdaBootstrap(GlobalLoggingTest); break; + case nameof(GlobalLoggingWithExceptionTest): + bootstrap = new LambdaBootstrap(GlobalLoggingWithExceptionTest); + break; case nameof(LoggingTest): bootstrap = new LambdaBootstrap(LoggingTest); break; @@ -172,12 +175,18 @@ Task UseLoggerAsync() return Task.FromResult(GetInvocationResponse(nameof(LoggingStressTest), "success")); } + private static Task GlobalLoggingWithExceptionTest(InvocationRequest invocation) + { +#pragma warning disable CA2252 + var exception = new ArgumentException("This is the wrong argument"); + LambdaLogger.Log(LogLevel.Error, exception, "This is a global log message with {argument} as an argument", "foobar"); +#pragma warning restore CA2252 + return Task.FromResult(GetInvocationResponse(nameof(GlobalLoggingWithExceptionTest), true)); + } private static Task GlobalLoggingTest(InvocationRequest invocation) { -#pragma warning disable CA2252 LambdaLogger.Log(LogLevel.Information, "This is a global log message with {argument} as an argument", "foobar"); -#pragma warning restore CA2252 return Task.FromResult(GetInvocationResponse(nameof(GlobalLoggingTest), true)); } diff --git a/Libraries/test/TestFunctionFSharp/TestFunctionCSharp/TestFunctionCSharp.csproj b/Libraries/test/TestFunctionFSharp/TestFunctionCSharp/TestFunctionCSharp.csproj index 47b6d25ee..a2177f269 100644 --- a/Libraries/test/TestFunctionFSharp/TestFunctionCSharp/TestFunctionCSharp.csproj +++ b/Libraries/test/TestFunctionFSharp/TestFunctionCSharp/TestFunctionCSharp.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 TestFunctionCSharp Library false diff --git a/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj b/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj index 6ed1d07f5..094d04fd7 100644 --- a/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj +++ b/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 TestFunctionFSharp Library false From 6e4d92c8a20b0c5f77c30c19556b826300703743 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 2 May 2025 16:57:53 -0700 Subject: [PATCH 2/4] Fix unit test --- .../Bootstrap/UserCodeLoader.cs | 78 +++++++++++++++++++ .../TestUtilitiesLoggingTest.cs | 50 ++++++++++++ .../HandlerTests.cs | 7 +- .../TestHelpers/StringWriterExtensions.cs | 22 +++++- 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.Core.Tests/TestUtilitiesLoggingTest.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs index 946ffbe7c..338d75f50 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs @@ -35,6 +35,8 @@ internal class UserCodeLoader { private const string UserInvokeException = "An exception occurred while invoking customer handler."; private const string LambdaLoggingActionFieldName = "_loggingAction"; + private const string LambdaLoggingWithLevelActionFieldName = "_loggingWithLevelAction"; + private const string LambdaLoggingWithLevelAndExceptionActionFieldName = "_loggingWithLevelAndExceptionAction"; internal const string LambdaCoreAssemblyName = "Amazon.Lambda.Core"; @@ -186,6 +188,82 @@ internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action loggingWithLevelAction, InternalLogger internalLogger) + { + if (coreAssembly == null) + { + throw new ArgumentNullException(nameof(coreAssembly)); + } + + if (loggingWithLevelAction == null) + { + throw new ArgumentNullException(nameof(loggingWithLevelAction)); + } + + internalLogger.LogDebug($"UCL : Retrieving type '{Types.LambdaLoggerTypeName}'"); + var lambdaILoggerType = coreAssembly.GetType(Types.LambdaLoggerTypeName); + if (lambdaILoggerType == null) + { + throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToLocateType, Types.LambdaLoggerTypeName); + } + + internalLogger.LogDebug($"UCL : Retrieving field '{LambdaLoggingWithLevelActionFieldName}'"); + var loggingActionField = lambdaILoggerType.GetTypeInfo().GetField(LambdaLoggingWithLevelActionFieldName, BindingFlags.NonPublic | BindingFlags.Static); + if (loggingActionField == null) + { + throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToRetrieveField, LambdaLoggingWithLevelActionFieldName, Types.LambdaLoggerTypeName); + } + + internalLogger.LogDebug($"UCL : Setting field '{LambdaLoggingWithLevelActionFieldName}'"); + try + { + loggingActionField.SetValue(null, loggingWithLevelAction); + } + catch (Exception e) + { + throw LambdaExceptions.ValidationException(e, Errors.UserCodeLoader.Internal.UnableToSetField, + Types.LambdaLoggerTypeName, LambdaLoggingWithLevelActionFieldName); + } + } + + internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action loggingWithAndExceptionLevelAction, InternalLogger internalLogger) + { + if (coreAssembly == null) + { + throw new ArgumentNullException(nameof(coreAssembly)); + } + + if (loggingWithAndExceptionLevelAction == null) + { + throw new ArgumentNullException(nameof(loggingWithAndExceptionLevelAction)); + } + + internalLogger.LogDebug($"UCL : Retrieving type '{Types.LambdaLoggerTypeName}'"); + var lambdaILoggerType = coreAssembly.GetType(Types.LambdaLoggerTypeName); + if (lambdaILoggerType == null) + { + throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToLocateType, Types.LambdaLoggerTypeName); + } + + internalLogger.LogDebug($"UCL : Retrieving field '{LambdaLoggingWithLevelAndExceptionActionFieldName}'"); + var loggingActionField = lambdaILoggerType.GetTypeInfo().GetField(LambdaLoggingWithLevelAndExceptionActionFieldName, BindingFlags.NonPublic | BindingFlags.Static); + if (loggingActionField == null) + { + throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToRetrieveField, LambdaLoggingWithLevelAndExceptionActionFieldName, Types.LambdaLoggerTypeName); + } + + internalLogger.LogDebug($"UCL : Setting field '{LambdaLoggingWithLevelAndExceptionActionFieldName}'"); + try + { + loggingActionField.SetValue(null, loggingWithAndExceptionLevelAction); + } + catch (Exception e) + { + throw LambdaExceptions.ValidationException(e, Errors.UserCodeLoader.Internal.UnableToSetField, + Types.LambdaLoggerTypeName, LambdaLoggingWithLevelAndExceptionActionFieldName); + } + } + /// /// Constructs customer-specified serializer, specified either on the method, /// the assembly, or not specified at all. diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/TestUtilitiesLoggingTest.cs b/Libraries/test/Amazon.Lambda.Core.Tests/TestUtilitiesLoggingTest.cs new file mode 100644 index 000000000..e1b39c830 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Core.Tests/TestUtilitiesLoggingTest.cs @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; +using System.Linq; +using Amazon.Lambda.TestUtilities; +using Amazon.Lambda.Core; + +namespace Amazon.Lambda.Tests +{ + public class TestUtilitiesLoggingTest + { + [Fact] + public void LogWithLevelAndMessage() + { + ILambdaLogger logger = new TestLambdaLogger(); + logger.Log(LogLevel.Warning, "This is a warning"); + + Assert.Contains("Warning: This is a warning", ((TestLambdaLogger)logger).Buffer.ToString()); + } + + [Fact] + public void LogWithLevelAndMessageAndParameters() + { + ILambdaLogger logger = new TestLambdaLogger(); + logger.Log(LogLevel.Warning, "This is {name}", "garp"); + + Assert.Contains("Warning: This is {name}", ((TestLambdaLogger)logger).Buffer.ToString()); + Assert.Contains("\tgarp", ((TestLambdaLogger)logger).Buffer.ToString()); + } + + [Fact] + public void LogWithLevelAndMessageAndParametersAndExecption() + { + ILambdaLogger logger = new TestLambdaLogger(); + + var exception = new ArgumentException("Bad Name"); + logger.Log(LogLevel.Warning, exception, "This is {name}", "garp"); + + + Assert.Contains("Warning: This is {name}", ((TestLambdaLogger)logger).Buffer.ToString()); + Assert.Contains("\tgarp", ((TestLambdaLogger)logger).Buffer.ToString()); + Assert.Contains("ArgumentException", ((TestLambdaLogger)logger).Buffer.ToString()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs index db99d880b..42e0d6957 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -393,9 +393,10 @@ private async Task ExecHandlerAsync(string handler, string dataIn Client = testRuntimeApiClient }; - var loggerAction = actionWriter.ToLoggingAction(); var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(UserCodeLoader.LambdaCoreAssemblyName)); - UserCodeLoader.SetCustomerLoggerLogAction(assembly, loggerAction, _internalLogger); + UserCodeLoader.SetCustomerLoggerLogAction(assembly, actionWriter.ToLoggingAction(), _internalLogger); + UserCodeLoader.SetCustomerLoggerLogAction(assembly, actionWriter.ToLoggingWithLevelAction(), _internalLogger); + UserCodeLoader.SetCustomerLoggerLogAction(assembly, actionWriter.ToLoggingWithLevelAndExceptionAction(), _internalLogger); if (assertLoggedByInitialize != null) { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/StringWriterExtensions.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/StringWriterExtensions.cs index fdd328b2e..6658363dc 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/StringWriterExtensions.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/StringWriterExtensions.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -67,9 +67,27 @@ public static Action ToLoggingAction(this TextWriter writer) return writer.WriteLine; } + public static Action ToLoggingWithLevelAction(this TextWriter writer) + { + return (l, m, p) => + { + var formattedMessage = $"{l}: {m}"; + writer.WriteLine(formattedMessage); + }; + } + + public static Action ToLoggingWithLevelAndExceptionAction(this TextWriter writer) + { + return (l, e, m, p) => + { + var formattedMessage = $"{l}: {m}\n{e}"; + writer.WriteLine(formattedMessage); + }; + } + public static Action ToLoggingAction(this ITestOutputHelper writer) { return writer.WriteLine; } } -} \ No newline at end of file +} From 8647a46be98fb6cd61200fa29d6d5000a79cd749 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 8 May 2025 16:41:11 -0700 Subject: [PATCH 3/4] Address PR comments --- .../16cc3c6a-8fa0-410b-ba57-74221abb086a.json | 4 ++-- .../LambdaILogger.cs | 16 +++++++++++--- .../Bootstrap/UserCodeLoader.cs | 22 +++++++++++++++++++ .../TestLambdaLogger.cs | 18 +++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json b/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json index 7a830b4b3..b49862a21 100644 --- a/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json +++ b/.autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json @@ -30,7 +30,7 @@ }, { "Name": "Amazon.Lambda.Logging.AspNetCore", - "Type": "Minor", + "Type": "Major", "ChangelogMessages": [ "Add support Lambda log levels", "Change build target from .NET Standard 2.0 to .NET 6 and NET 8 to match Amazon.Lambda.AspNetCoreServer" @@ -38,7 +38,7 @@ }, { "Name": "Amazon.Lambda.TestUtilities", - "Type": "Minor", + "Type": "Major", "ChangelogMessages": [ "Update Amazon.Lambda.TestUtitlies to have implementation of the newer logging methods", "Change build target from .NET Standard 2.0 to .NET 6 and NET 8 to match Amazon.Lambda.AspNetCoreServer" diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs index 40e7d5923..decfeb407 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs @@ -28,7 +28,18 @@ public bool IsEnabled(LogLevel logLevel) _options.Filter == null || _options.Filter(_categoryName, logLevel)); } - + + /// + /// The Log method called by the ILogger framework to log message to logger's target. In the Lambda case the formatted logging will be + /// sent to the Amazon.Lambda.Core.LambdaLogger's Log method. + /// + /// + /// + /// + /// + /// + /// + /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (formatter == null) @@ -41,8 +52,6 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except return; } - var lambdaLogLevel = ConvertLogLevel(logLevel); - var components = new List(4); if (_options.IncludeLogLevel) { @@ -74,6 +83,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except var finalText = string.Join(" ", components); + var lambdaLogLevel = ConvertLogLevel(logLevel); Amazon.Lambda.Core.LambdaLogger.Log(lambdaLogLevel, finalText); } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs index 338d75f50..ff61ff2f5 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs @@ -150,6 +150,13 @@ public void Invoke(Stream lambdaData, ILambdaContext lambdaContext, Stream outSt _invokeDelegate(lambdaData, lambdaContext, outStream); } + /// + /// Sets the backing logger action field in Amazon.Logging.Core to redirect logs into Amazon.Lambda.RuntimeSupport. + /// + /// + /// + /// + /// internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action customerLoggingAction, InternalLogger internalLogger) { if (coreAssembly == null) @@ -188,6 +195,14 @@ internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action + /// Sets the backing logger action field in Amazon.Logging.Core to redirect logs into Amazon.Lambda.RuntimeSupport. + /// + /// + /// + /// + /// + internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action loggingWithLevelAction, InternalLogger internalLogger) { if (coreAssembly == null) @@ -226,6 +241,13 @@ internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action + /// Sets the backing logger action field in Amazon.Logging.Core to redirect logs into Amazon.Lambda.RuntimeSupport. + /// + /// + /// + /// + /// internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action loggingWithAndExceptionLevelAction, InternalLogger internalLogger) { if (coreAssembly == null) diff --git a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs index e6626aa05..1e46b09ca 100644 --- a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaLogger.cs @@ -38,6 +38,11 @@ public void LogLine(string message) Console.WriteLine(message); } + /// + /// Write log messages to the console and the Buffer. + /// + /// + /// public void Log(string level, string message) { var formmattedString = $"{level}: {message}"; @@ -45,6 +50,12 @@ public void Log(string level, string message) Console.WriteLine(formmattedString); } + /// + /// Write log messages to the console and the Buffer. + /// + /// + /// + /// public void Log(string level, string message, params object[] args) { var builder = new StringBuilder(); @@ -63,6 +74,13 @@ public void Log(string level, string message, params object[] args) Console.WriteLine(formmattedString); } + /// + /// Write log messages to the console and the Buffer. + /// + /// + /// + /// + /// public void Log(string level, Exception exception, string message, params object[] args) { var builder = new StringBuilder(); From 63095eb0db9b68262cb85ab4aa181f504055652f Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 2 Jun 2025 17:30:53 -0700 Subject: [PATCH 4/4] Fix formatting of source files using a mix of tabs and spaces to just use spaces. --- .../LambdaILogger.cs | 88 +++++++++---------- .../LambdaLoggerOptions.cs | 8 +- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs index decfeb407..1097f9103 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs @@ -3,31 +3,31 @@ namespace Microsoft.Extensions.Logging { - internal class LambdaILogger : ILogger - { - // Private fields - private readonly string _categoryName; - private readonly LambdaLoggerOptions _options; + internal class LambdaILogger : ILogger + { + // Private fields + private readonly string _categoryName; + private readonly LambdaLoggerOptions _options; - internal IExternalScopeProvider ScopeProvider { get; set; } + internal IExternalScopeProvider ScopeProvider { get; set; } - // Constructor - public LambdaILogger(string categoryName, LambdaLoggerOptions options) - { - _categoryName = categoryName; - _options = options; - } + // Constructor + public LambdaILogger(string categoryName, LambdaLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + } - // ILogger methods - public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? new NoOpDisposable(); + // ILogger methods + public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? new NoOpDisposable(); - public bool IsEnabled(LogLevel logLevel) - { - return ( - _options.Filter == null || - _options.Filter(_categoryName, logLevel)); - } + public bool IsEnabled(LogLevel logLevel) + { + return ( + _options.Filter == null || + _options.Filter(_categoryName, logLevel)); + } /// /// The Log method called by the ILogger framework to log message to logger's target. In the Lambda case the formatted logging will be @@ -108,33 +108,33 @@ private static Amazon.Lambda.Core.LogLevel ConvertLogLevel(LogLevel logLevel) } } - private void GetScopeInformation(List logMessageComponents) - { - var scopeProvider = ScopeProvider; + private void GetScopeInformation(List logMessageComponents) + { + var scopeProvider = ScopeProvider; - if (_options.IncludeScopes && scopeProvider != null) - { - var initialCount = logMessageComponents.Count; + if (_options.IncludeScopes && scopeProvider != null) + { + var initialCount = logMessageComponents.Count; - scopeProvider.ForEachScope((scope, list) => - { - list.Add(scope.ToString()); - }, (logMessageComponents)); + scopeProvider.ForEachScope((scope, list) => + { + list.Add(scope.ToString()); + }, (logMessageComponents)); - if (logMessageComponents.Count > initialCount) - { - logMessageComponents.Add("=>"); - } - } - } + if (logMessageComponents.Count > initialCount) + { + logMessageComponents.Add("=>"); + } + } + } - // Private classes - private class NoOpDisposable : IDisposable - { - public void Dispose() - { - } - } + // Private classes + private class NoOpDisposable : IDisposable + { + public void Dispose() + { + } + } - } + } } diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaLoggerOptions.cs b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaLoggerOptions.cs index 07a07abc3..c15bad4b9 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaLoggerOptions.cs +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaLoggerOptions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Linq; @@ -18,8 +18,8 @@ public class LambdaLoggerOptions private const string INCLUDE_CATEGORY_KEY = "IncludeCategory"; private const string INCLUDE_NEWLINE_KEY = "IncludeNewline"; private const string INCLUDE_EXCEPTION_KEY = "IncludeException"; - private const string INCLUDE_EVENT_ID_KEY = "IncludeEventId"; - private const string INCLUDE_SCOPES_KEY = "IncludeScopes"; + private const string INCLUDE_EVENT_ID_KEY = "IncludeEventId"; + private const string INCLUDE_SCOPES_KEY = "IncludeScopes"; private const string LOG_LEVEL_KEY = "LogLevel"; private const string DEFAULT_CATEGORY = "Default"; @@ -60,7 +60,7 @@ public class LambdaLoggerOptions /// public bool IncludeScopes { get; set; } - /// + /// /// Function used to filter events based on the log level. /// Default value is null and will instruct logger to log everything. ///