diff --git a/.autover/changes/d0822afd-daf4-437a-9437-2a9492c135b7.json b/.autover/changes/d0822afd-daf4-437a-9437-2a9492c135b7.json new file mode 100644 index 000000000..28f238342 --- /dev/null +++ b/.autover/changes/d0822afd-daf4-437a-9437-2a9492c135b7.json @@ -0,0 +1,18 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Minor", + "ChangelogMessages": [ + "Add support for parameterized logging method to global logger LambdaLogger in Amazon.Lambda.Core" + ] + }, + { + "Name": "Amazon.Lambda.Core", + "Type": "Patch", + "ChangelogMessages": [ + "Add support for parameterized logging method to global logger LambdaLogger. Method is marked as preview till new version of Amazon.Lambda.RuntimeSupport is deployed to managed runtime." + ] + } + ] +} \ 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 554d3b4e1..c6b612bcf 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs @@ -1,4 +1,7 @@ -using System; +using System; +using System.Reflection.Emit; +using System.Runtime.Versioning; +using System.Text; namespace Amazon.Lambda.Core { @@ -9,8 +12,11 @@ namespace Amazon.Lambda.Core /// public static class LambdaLogger { - // Logging action, logs to Console by default + // The name of this field must not change or be readonly because Amazon.Lambda.RuntimeSupport will use reflection to replace the + // value with an Action that directs the logging into its logging system. +#pragma warning disable IDE0044 // Add readonly modifier private static Action _loggingAction = LogToConsole; +#pragma warning restore IDE0044 // Add readonly modifier // Logs message to console private static void LogToConsole(string message) @@ -29,5 +35,66 @@ public static void Log(string message) { _loggingAction(message); } + +#if NET6_0_OR_GREATER + + // The name of this field must not change or be readonly because Amazon.Lambda.RuntimeSupport will use reflection to replace the + // value with an Action that directs the logging into its logging system. +#pragma warning disable IDE0044 // Add readonly modifier + private static Action _loggingWithLevelAction = LogWithLevelToConsole; +#pragma warning restore IDE0044 // Add readonly modifier + + // Logs message to console + private static void LogWithLevelToConsole(string level, 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. + var sb = new StringBuilder(); + sb.Append(level).Append(": ").Append(message); + if (args?.Length > 0) + { + sb.Append(" Arguments:"); + foreach(var arg in args) + { + sb.Append(" \""); + sb.Append(arg); + sb.Append("\""); + } + } + Console.WriteLine(sb.ToString()); + } + + 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."; + + /// + /// 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 + /// 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); + } + + /// + /// 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 + /// 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); +#endif } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index 15d577060..18417e562 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -240,16 +240,25 @@ private void Initialize(TextWriter stdOutWriter, TextWriter stdErrorWriter) /// private void ConfigureLoggingActionField() { - var lambdaILoggerType = typeof(Amazon.Lambda.Core.LambdaLogger); - if (lambdaILoggerType == null) + var lambdaLoggerType = typeof(Amazon.Lambda.Core.LambdaLogger); + if (lambdaLoggerType == null) return; - var loggingActionField = lambdaILoggerType.GetTypeInfo().GetField("_loggingAction", BindingFlags.NonPublic | BindingFlags.Static); - if (loggingActionField == null) - return; + var loggingActionField = lambdaLoggerType.GetTypeInfo().GetField("_loggingAction", BindingFlags.NonPublic | BindingFlags.Static); + if (loggingActionField != null) + { + Action loggingAction = (message => FormattedWriteLine(null, message)); + loggingActionField.SetValue(null, loggingAction); + } + - Action callback = (message => FormattedWriteLine(null, message)); - loggingActionField.SetValue(null, callback); + var loggingWithLevelActionField = lambdaLoggerType.GetTypeInfo().GetField("_loggingWithLevelAction", BindingFlags.NonPublic | BindingFlags.Static); + if (loggingWithLevelActionField != null) + { + Action loggingWithLevelAction = ((level, message, args) => FormattedWriteLine(level, message, args)); + loggingWithLevelActionField.SetValue(null, loggingWithLevelAction); + + } } /// 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 4ab778a30..387245e63 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 @@ -1,4 +1,4 @@ -/* +/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -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 RunGlobalLoggingTestAsync(lambdaClient, "GlobalLoggingTest"); await RunJsonLoggingWithUnhandledExceptionAsync(lambdaClient); await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Trace, LogConfigSource.LambdaAPI); @@ -121,7 +122,6 @@ protected virtual async Task TestAllHandlersAsync() 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"); @@ -317,6 +317,19 @@ private async Task RunLoggingTestAsync(AmazonLambdaClient lambdaClient, string h } } + private async Task RunGlobalLoggingTestAsync(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); + } + 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 c803a20f6..7cd6ec3b0 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunction.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Core; +using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; using Amazon.Lambda.Serialization.SystemTextJson; using System; @@ -48,6 +48,9 @@ private static async Task Main(string[] args) case nameof(LoggingStressTest): bootstrap = new LambdaBootstrap(LoggingStressTest); break; + case nameof(GlobalLoggingTest): + bootstrap = new LambdaBootstrap(GlobalLoggingTest); + break; case nameof(LoggingTest): bootstrap = new LambdaBootstrap(LoggingTest); break; @@ -169,6 +172,15 @@ Task UseLoggerAsync() return Task.FromResult(GetInvocationResponse(nameof(LoggingStressTest), "success")); } + + 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)); + } + private static Task LoggingTest(InvocationRequest invocation) { invocation.LambdaContext.Logger.LogTrace("A trace log");