diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index c2a2e81a0..15d577060 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -324,6 +324,10 @@ public WrapperTextWriter(TextWriter innerWriter, string defaultLogLevel) { _minmumLogLevel = LogLevel.Critical; } + else if (string.Equals(envLogLevel, "warn", StringComparison.InvariantCultureIgnoreCase)) + { + _minmumLogLevel = LogLevel.Warning; + } else if (Enum.TryParse(envLogLevel, true, out var result)) { _minmumLogLevel = result; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj index 086a2e226..1e37f5d9b 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj @@ -20,8 +20,8 @@ - - + + @@ -32,7 +32,7 @@ - + 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 759d0245e..a0a67f242 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 @@ -198,9 +198,11 @@ protected async Task InvokeFunctionAsync(IAmazonLambda lambdaCli return await lambdaClient.InvokeAsync(request); } - protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handler, Dictionary environmentVariables = null) + protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handler, Dictionary environmentVariables = null, RuntimeLogLevel? logLevel = null) { - if(environmentVariables == null) + await WaitForFunctionToBeReady(lambdaClient); + + if (environmentVariables == null) { environmentVariables = new Dictionary(); } @@ -216,8 +218,30 @@ protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handl Variables = environmentVariables } }; + + if (logLevel == null) + { + updateFunctionConfigurationRequest.LoggingConfig = new LoggingConfig + { + LogFormat = LogFormat.Text + }; + } + else + { + updateFunctionConfigurationRequest.LoggingConfig = new LoggingConfig + { + ApplicationLogLevel = ConvertRuntimeLogLevel(logLevel.Value), + LogFormat = LogFormat.JSON + }; + } + await lambdaClient.UpdateFunctionConfigurationAsync(updateFunctionConfigurationRequest); + await WaitForFunctionToBeReady(lambdaClient); + } + + private async Task WaitForFunctionToBeReady(IAmazonLambda lambdaClient) + { // Wait for eventual consistency of function change. var getConfigurationRequest = new GetFunctionConfigurationRequest { FunctionName = FunctionName }; GetFunctionConfigurationResponse getConfigurationResponse = null; @@ -344,5 +368,58 @@ protected class NoDeploymentPackageFoundException : Exception { } + + private ApplicationLogLevel ConvertRuntimeLogLevel(RuntimeLogLevel runtimeLogLevel) + { + switch (runtimeLogLevel) + { + case RuntimeLogLevel.Trace: + return ApplicationLogLevel.TRACE; + case RuntimeLogLevel.Debug: + return ApplicationLogLevel.DEBUG; + case RuntimeLogLevel.Information: + return ApplicationLogLevel.INFO; + case RuntimeLogLevel.Warning: + return ApplicationLogLevel.WARN; + case RuntimeLogLevel.Error: + return ApplicationLogLevel.ERROR; + case RuntimeLogLevel.Critical: + return ApplicationLogLevel.FATAL; + default: + throw new ArgumentException("Unknown log level: " + runtimeLogLevel); + } + } + + public enum RuntimeLogLevel + { + /// + /// Trace level logging + /// + Trace = 0, + /// + /// Debug level logging + /// + Debug = 1, + + /// + /// Information level logging + /// + Information = 2, + + /// + /// Warning level logging + /// + Warning = 3, + + /// + /// Error level logging + /// + Error = 4, + + /// + /// Critical level logging + /// + Critical = 5 + } } } 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 75491bbe3..e4af2b9bc 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 @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -92,7 +93,7 @@ protected virtual async Task TestAllHandlersAsync() roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient); // .NET API to address setting memory constraint was added for .NET 8 - if(_targetFramework == TargetFramework.NET8) + if (_targetFramework == TargetFramework.NET8) { await RunMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes"); await RunWithoutMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes"); @@ -102,8 +103,23 @@ protected virtual async Task TestAllHandlersAsync() await RunTestExceptionAsync(lambdaClient, "ExceptionNonAsciiCharacterUnwrappedAsync", "", "Exception", "Unhandled exception with non ASCII character: ♂"); await RunTestSuccessAsync(lambdaClient, "UnintendedDisposeTest", "not-used", "UnintendedDisposeTest-SUCCESS"); await RunTestSuccessAsync(lambdaClient, "LoggingStressTest", "not-used", "LoggingStressTest-success"); - await RunLoggingTestAsync(lambdaClient, "LoggingTest", null); - await RunLoggingTestAsync(lambdaClient, "LoggingTest", "debug"); + + await RunJsonLoggingWithUnhandledExceptionAsync(lambdaClient); + + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Trace, LogConfigSource.LambdaAPI); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Debug, LogConfigSource.LambdaAPI); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Information, LogConfigSource.LambdaAPI); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Warning, LogConfigSource.LambdaAPI); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Error, LogConfigSource.LambdaAPI); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Critical, LogConfigSource.LambdaAPI); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Trace, LogConfigSource.DotnetEnvironment); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Debug, LogConfigSource.DotnetEnvironment); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Information, LogConfigSource.DotnetEnvironment); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Warning, LogConfigSource.DotnetEnvironment); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Error, LogConfigSource.DotnetEnvironment); + await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Critical, LogConfigSource.DotnetEnvironment); + + await RunUnformattedLoggingTestAsync(lambdaClient, "LoggingTest"); await RunTestSuccessAsync(lambdaClient, "ToUpperAsync", "message", "ToUpperAsync-MESSAGE"); @@ -136,6 +152,18 @@ protected virtual async Task TestAllHandlersAsync() } } + private async Task RunJsonLoggingWithUnhandledExceptionAsync(AmazonLambdaClient lambdaClient) + { + await UpdateHandlerAsync(lambdaClient, "ThrowUnhandledApplicationException", null, RuntimeLogLevel.Information); + var invokeResponse = await InvokeFunctionAsync(lambdaClient, JsonConvert.SerializeObject("")); + + var log = System.Text.UTF8Encoding.UTF8.GetString(Convert.FromBase64String(invokeResponse.LogResult)); + var exceptionLog = log.Split('\n').FirstOrDefault(x => x.Contains("System.ApplicationException")); + + Assert.NotNull(exceptionLog); + Assert.Contains("\"level\":\"Error\"", exceptionLog); + } + private async Task RunMaxHeapMemoryCheck(AmazonLambdaClient lambdaClient, string handler) { await UpdateHandlerAsync(lambdaClient, handler); @@ -207,14 +235,17 @@ private async Task RunTestExceptionAsync(AmazonLambdaClient lambdaClient, string } } - private async Task RunLoggingTestAsync(AmazonLambdaClient lambdaClient, string handler, string logLevel) + // The .NET Lambda runtime has a legacy environment variable for configuring the log level. This enum is used in the test to choose + // whether the legacy environment variable should be set or use the new properties in the update configuration api for setting log level. + enum LogConfigSource { LambdaAPI, DotnetEnvironment} + private async Task RunLoggingTestAsync(AmazonLambdaClient lambdaClient, string handler, RuntimeLogLevel? runtimeLogLevel, LogConfigSource configSource) { var environmentVariables = new Dictionary(); - if(!string.IsNullOrEmpty(logLevel)) + if(runtimeLogLevel.HasValue && configSource == LogConfigSource.DotnetEnvironment) { - environmentVariables["AWS_LAMBDA_HANDLER_LOG_LEVEL"] = logLevel; + environmentVariables["AWS_LAMBDA_HANDLER_LOG_LEVEL"] = runtimeLogLevel.Value.ToString().ToLowerInvariant(); } - await UpdateHandlerAsync(lambdaClient, handler, environmentVariables); + await UpdateHandlerAsync(lambdaClient, handler, environmentVariables, configSource == LogConfigSource.LambdaAPI ? runtimeLogLevel : null); var invokeResponse = await InvokeFunctionAsync(lambdaClient, JsonConvert.SerializeObject("")); Assert.True(invokeResponse.HttpStatusCode == System.Net.HttpStatusCode.OK); @@ -222,22 +253,65 @@ private async Task RunLoggingTestAsync(AmazonLambdaClient lambdaClient, string h var log = System.Text.UTF8Encoding.UTF8.GetString(Convert.FromBase64String(invokeResponse.LogResult)); - Assert.Contains("info\tA information log", log); - Assert.Contains("warn\tA warning log", log); - Assert.Contains("fail\tA error log", log); - Assert.Contains("crit\tA critical log", log); + if (!runtimeLogLevel.HasValue) + runtimeLogLevel = RuntimeLogLevel.Information; + + if (runtimeLogLevel <= RuntimeLogLevel.Trace) + { + Assert.Contains("A trace log", log); + } + else + { + Assert.DoesNotContain("A trace log", log); + } + + if (runtimeLogLevel <= RuntimeLogLevel.Debug) + { + Assert.Contains("A debug log", log); + } + else + { + Assert.DoesNotContain("A debug log", log); + } + + if (runtimeLogLevel <= RuntimeLogLevel.Information) + { + Assert.Contains("A information log", log); + Assert.Contains("A stdout info message", log); + } + else + { + Assert.DoesNotContain("A information log", log); + Assert.DoesNotContain("A stdout info message", log); + } - Assert.Contains("info\tA stdout info message", log); + if (runtimeLogLevel <= RuntimeLogLevel.Warning) + { + Assert.Contains("A warning log", log); + } + else + { + Assert.DoesNotContain("A warning log", log); + } - Assert.Contains("fail\tA stderror error message", log); + if (runtimeLogLevel <= RuntimeLogLevel.Error) + { + Assert.Contains("A error log", log); + Assert.Contains("A stderror error message", log); + } + else + { + Assert.DoesNotContain("A error log", log); + Assert.DoesNotContain("A stderror error message", log); + } - if (string.IsNullOrEmpty(logLevel)) + if (runtimeLogLevel <= RuntimeLogLevel.Critical) { - Assert.DoesNotContain($"a {logLevel} log".ToLower(), log.ToLower()); + Assert.Contains("A critical log", log); } else { - Assert.Contains($"a {logLevel} log".ToLower(), log.ToLower()); + Assert.DoesNotContain("A critical log", log); } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs index 60f6a3418..c803a20f6 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs @@ -98,6 +98,10 @@ private static async Task Main(string[] args) case nameof(GetTimezoneNameAsync): bootstrap = new LambdaBootstrap(GetTimezoneNameAsync); break; + case nameof(ThrowUnhandledApplicationException): + handlerWrapper = HandlerWrapper.GetHandlerWrapper((Action)ThrowUnhandledApplicationException); + bootstrap = new LambdaBootstrap(handlerWrapper); + break; default: throw new Exception($"Handler {handler} is not supported."); } @@ -374,6 +378,11 @@ private static void AggregateExceptionNotUnwrapped() throw new AggregateException("AggregateException thrown from a synchronous handler."); } + private static void ThrowUnhandledApplicationException() + { + throw new ApplicationException("Function fail"); + } + private static Task TooLargeResponseBodyAsync(InvocationRequest invocation) { return Task.FromResult(GetInvocationResponse(nameof(TooLargeResponseBodyAsync), SevenMBString.Value));