From 6f50d34f43a0d9913c28ec1e6ebc557a4f700d46 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:09:19 +0100 Subject: [PATCH 01/32] Initial commit. New Serialization context. .NET 8 serialization specific paths. New helper method to convert anonymous types to dictionary, use for source generation .NET 8 --- .../AWS.Lambda.Powertools.Logging.csproj | 2 + .../Internal/Converters/ExceptionConverter.cs | 6 +- .../Helpers/PowertoolsLoggerHelpers.cs | 32 +++++++++ .../Internal/LoggingAspectHandler.cs | 37 +++++++--- .../Internal/PowertoolsConfigurations.cs | 2 +- .../Internal/PowertoolsLogger.cs | 71 +++++++++++++------ .../LoggingSerializationContext.cs | 54 ++++++++++++++ libraries/src/Directory.Packages.props | 2 + .../PowertoolsLoggerTest.cs | 6 +- 9 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj index 6feca1a2..6cc22db7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj @@ -11,6 +11,8 @@ + + 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 7a85abf7..db74bc0c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging.Serializers; namespace AWS.Lambda.Powertools.Logging.Internal.Converters; @@ -84,9 +85,8 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize case Type propType: writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propType.FullName); break; - default: - writer.WritePropertyName(ApplyPropertyNamingPolicy(prop.Name, options)); - JsonSerializer.Serialize(writer, prop.Value, options); + case string propString: + writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propString); break; } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs new file mode 100644 index 00000000..dd9ba457 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Linq; + +namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; + +internal sealed class PowertoolsLoggerHelpers +{ + internal static object ObjectToDictionary(object anonymousObject) + { + if (anonymousObject.GetType().Namespace is not null) + { + return anonymousObject; + } + + return anonymousObject.GetType().GetProperties() + .ToDictionary(prop => prop.Name, prop => ObjectToDictionary(prop.GetValue(anonymousObject, null))); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs index 441cdc22..ac92098b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -14,6 +14,7 @@ */ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.ExceptionServices; @@ -21,6 +22,7 @@ using System.Text.Json.Serialization; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Converters; +using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -61,7 +63,7 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// The log level /// private readonly LogLevel? _logLevel; - + /// /// The logger output case /// @@ -91,17 +93,17 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// The is context initialized /// private bool _isContextInitialized; - + /// /// Specify to clear Lambda Context on exit /// private bool _clearLambdaContext; - + /// /// The JsonSerializer options /// private static JsonSerializerOptions _jsonSerializerOptions; - + /// /// Get JsonSerializer options. /// @@ -168,7 +170,7 @@ public void OnEntry(AspectEventArgs eventArgs) Logger.LoggerProvider = new LoggerProvider(loggerConfig); break; case LoggerProvider: - ((LoggerProvider) Logger.LoggerProvider).Configure(loggerConfig); + ((LoggerProvider)Logger.LoggerProvider).Configure(loggerConfig); break; } @@ -274,7 +276,7 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) _systemWrapper.LogLine( "Skipping Lambda Context injection because ILambdaContext context parameter not found."); } - + /// /// Builds JsonSerializer options. /// @@ -290,6 +292,11 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() jsonOptions.Converters.Add(new ConstantClassConverter()); jsonOptions.Converters.Add(new DateOnlyConverter()); jsonOptions.Converters.Add(new TimeOnlyConverter()); + +#if NET8_0_OR_GREATER + jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default; +#endif + return jsonOptions; } @@ -297,6 +304,9 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() /// Captures the correlation identifier. /// /// The event argument. + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", + Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Everything is ok with serialization")] private void CaptureCorrelationId(object eventArg) { if (string.IsNullOrWhiteSpace(_correlationIdPath)) @@ -319,7 +329,14 @@ private void CaptureCorrelationId(object eventArg) try { var correlationId = string.Empty; - var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions)); + +#if NET8_0_OR_GREATER + var jsonDoc = + JsonDocument.Parse(JsonSerializer.Serialize(eventArg, eventArg.GetType(), JsonSerializerOptions)); +#else + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions)); +#endif + var element = jsonDoc.RootElement; for (var i = 0; i < correlationIdPaths.Length; i++) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs index 4d39cb16..232d7c49 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs @@ -67,7 +67,7 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati return LoggingConstants.DefaultLoggerOutputCase; } - private static Dictionary AwsLogLevelMapper = new() + private static readonly Dictionary AwsLogLevelMapper = new() { { "TRACE", "TRACE" }, { "DEBUG", "DEBUG" }, diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index e723ed22..a1733728 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -15,11 +15,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Converters; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -40,7 +43,7 @@ internal sealed class PowertoolsLogger : ILogger /// The name /// private readonly string _name; - + /// /// The current configuration /// @@ -55,7 +58,7 @@ internal sealed class PowertoolsLogger : ILogger /// The system wrapper /// private readonly ISystemWrapper _systemWrapper; - + /// /// The JsonSerializer options /// @@ -80,10 +83,10 @@ public PowertoolsLogger( { (_name, _powertoolsConfigurations, _systemWrapper, _getCurrentConfig) = (name, powertoolsConfigurations, systemWrapper, getCurrentConfig); - + _powertoolsConfigurations.SetExecutionEnvironment(this); _currentConfig = GetCurrentConfig(); - + if (_lambdaLogLevelEnabled && _logLevel < _lambdaLogLevel) { var message = @@ -148,10 +151,10 @@ internal void EndScope() private static Dictionary GetScopeKeys(TState state) { var keys = new Dictionary(); - - if (state is null) + + if (state is null) return keys; - + switch (state) { case IEnumerable> pairs: @@ -161,6 +164,7 @@ private static Dictionary GetScopeKeys(TState state) if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } + break; } case IEnumerable> pairs: @@ -170,6 +174,7 @@ private static Dictionary GetScopeKeys(TState state) if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } + break; } default: @@ -178,10 +183,11 @@ private static Dictionary GetScopeKeys(TState state) { keys.TryAdd(property.Name, property.GetValue(state)); } + break; } } - + return keys; } @@ -204,6 +210,11 @@ public bool IsEnabled(LogLevel logLevel) /// The exception related to this entry. /// Function to create a message of the and . /// The type of the object to be written. + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", + Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] + [UnconditionalSuppressMessage("AOT", + "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", + Justification = "Everything is ok with serialization")] public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { @@ -219,11 +230,16 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except : formatter(state, exception); var logFormatter = Logger.GetFormatter(); - var logEntry = logFormatter is null? - GetLogEntry(logLevel, timestamp, message, exception) : - GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); + var logEntry = logFormatter is null + ? GetLogEntry(logLevel, timestamp, message, exception) + : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); + +#if NET8_0_OR_GREATER + _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, typeof(object), JsonSerializerOptions)); +#else _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions)); +#endif } /// @@ -361,7 +377,11 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec var logObject = logFormatter.FormatLogEntry(logEntry); if (logObject is null) throw new LogFormatException($"{logFormatter.GetType().FullName} returned Null value."); +#if NET8_0_OR_GREATER + return PowertoolsLoggerHelpers.ObjectToDictionary(logObject); +#else return logObject; +#endif } catch (Exception e) { @@ -369,6 +389,8 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec $"{logFormatter.GetType().FullName} raised an exception: {e.Message}.", e); } } + + /// /// Clears the configuration. @@ -387,16 +409,16 @@ private LoggerConfiguration GetCurrentConfig() var currConfig = _getCurrentConfig(); _logLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel); var samplingRate = currConfig?.SamplingRate ?? _powertoolsConfigurations.LoggerSampleRate; - var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase); + var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase); _lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); _lambdaLogLevelEnabled = _lambdaLogLevel != LogLevel.None; - + var minLogLevel = _logLevel; if (_lambdaLogLevelEnabled) { minLogLevel = _lambdaLogLevel; } - + var config = new LoggerConfiguration { Service = currConfig?.Service, @@ -445,8 +467,13 @@ private static bool CustomFormatter(TState state, Exception exception, o if (exception is not null) return false; +#if NET8_0_OR_GREATER + var stateKeys = (state as IEnumerable>)? + .ToDictionary(i => i.Key, i => PowertoolsLoggerHelpers.ObjectToDictionary(i.Value)); +#else var stateKeys = (state as IEnumerable>)? .ToDictionary(i => i.Key, i => i.Value); +#endif if (stateKeys is null || stateKeys.Count != 2) return false; @@ -458,9 +485,10 @@ private static bool CustomFormatter(TState state, Exception exception, o return false; message = stateKeys.First(k => k.Key != "{OriginalFormat}").Value; + return true; } - + /// /// Builds JsonSerializer options. /// @@ -490,9 +518,12 @@ private JsonSerializerOptions BuildJsonSerializerOptions() jsonOptions.Converters.Add(new ConstantClassConverter()); jsonOptions.Converters.Add(new DateOnlyConverter()); jsonOptions.Converters.Add(new TimeOnlyConverter()); - + jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; - +#if NET8_0_OR_GREATER + jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default; +#endif + return jsonOptions; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs new file mode 100644 index 00000000..bfa62789 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; + +namespace AWS.Lambda.Powertools.Logging.Serializers; + +#if NET8_0_OR_GREATER + +/// +/// Custom JSON serializer context for AWS.Lambda.Powertools.Logging +/// +// [JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(Int32))] +[JsonSerializable(typeof(Double))] +[JsonSerializable(typeof(DateOnly))] +[JsonSerializable(typeof(TimeOnly))] +[JsonSerializable(typeof(InvalidOperationException))] +[JsonSerializable(typeof(Exception))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Byte[]))] +[JsonSerializable(typeof(MemoryStream))] +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(ApplicationLoadBalancerRequest))] +[JsonSerializable(typeof(LogEntry))] +internal partial class LoggingSerializationContext : JsonSerializerContext +{ + +} + +#endif \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index d9c404bd..cae7db9b 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -4,6 +4,8 @@ + + diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index c49852af..4714cbec 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -1251,6 +1251,10 @@ public void Log_Should_Serialize_DateOnly() { PropOne = "Value 1", PropTwo = "Value 2", + PropThree = new + { + PropFour = 1 + }, Date = new DateOnly(2022, 1, 1) }; @@ -1259,7 +1263,7 @@ public void Log_Should_Serialize_DateOnly() // Assert systemWrapper.Received(1).LogLine( Arg.Is(s => - s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"date\":\"2022-01-01\"}") + s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}}") ) ); } From 34070d6e25f5894dcf8d250060da6685cf821fd6 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:07:08 +0100 Subject: [PATCH 02/32] move to static PowertoolsLoggerHelpers --- .../Internal/Helpers/PowertoolsLoggerHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd9ba457..55bc7053 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -17,7 +17,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; -internal sealed class PowertoolsLoggerHelpers +internal static class PowertoolsLoggerHelpers { internal static object ObjectToDictionary(object anonymousObject) { From cd9babf4cd360fd24338e62ae99dae9078b73460 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:57:31 +0100 Subject: [PATCH 03/32] Refactor case logic. new lambdacontext using LambdaCore. Tests, docs update --- docs/core/logging.md | 4 + .../Core/PowertoolsLambdaContext.cs | 129 ------------------ .../AWS.Lambda.Powertools.Logging.csproj | 1 + .../Internal/LoggingAspectHandler.cs | 72 +++++----- .../Internal/LoggingLambdaContext.cs | 104 ++++++++++++++ .../Internal/PowertoolsConfigurations.cs | 55 +++++++- .../Internal/PowertoolsLogger.cs | 64 ++------- .../LoggingSerializationContext.cs | 5 + libraries/src/Directory.Build.props | 2 +- .../Core/PowertoolsLambdaContextTest.cs | 65 --------- .../LambdaContextTest.cs | 120 ++++++++++++++++ .../LogFormatterTest.cs | 39 +++--- .../LoggingAttributeTest.cs | 3 +- 13 files changed, 359 insertions(+), 304 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs delete mode 100644 libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs diff --git a/docs/core/logging.md b/docs/core/logging.md index 1e615362..7439c79d 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -166,6 +166,10 @@ When debugging in non-production environments, you can instruct Logger to log th You can set a Correlation ID using `CorrelationIdPath` parameter by passing a [JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"}. +!!! Attention + The JSON Pointer expression is `case sensitive`. In the bellow example `/headers/my_request_id_header` would work but `/Headers/my_request_id_header` would not find the element. + + === "Function.cs" ```c# hl_lines="6" diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs deleted file mode 100644 index 7f9b3fee..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; - -namespace AWS.Lambda.Powertools.Common; - -internal class PowertoolsLambdaContext -{ - /// - /// The AWS request ID associated with the request. - /// This is the same ID returned to the client that called invoke(). - /// This ID is reused for retries on the same request. - /// - internal string AwsRequestId { get; private set; } - - /// Name of the Lambda function that is running. - internal string FunctionName { get; private set; } - - /// - /// The Lambda function version that is executing. - /// If an alias is used to invoke the function, then this will be - /// the version the alias points to. - /// - internal string FunctionVersion { get; private set; } - - /// - /// The ARN used to invoke this function. - /// It can be function ARN or alias ARN. - /// An unqualified ARN executes the $LATEST version and aliases execute - /// the function version they are pointing to. - /// - internal string InvokedFunctionArn { get; private set; } - - /// - /// The CloudWatch log group name associated with the invoked function. - /// It can be null if the IAM user provided does not have permission for - /// CloudWatch actions. - /// - internal string LogGroupName { get; private set; } - - /// - /// The CloudWatch log stream name for this function execution. - /// It can be null if the IAM user provided does not have permission - /// for CloudWatch actions. - /// - internal string LogStreamName { get; private set; } - - /// - /// Memory limit, in MB, you configured for the Lambda function. - /// - internal int MemoryLimitInMB { get; private set; } - - /// - /// The instance - /// - internal static PowertoolsLambdaContext Instance { get; private set; } - - /// - /// Extract the lambda context from Lambda handler arguments. - /// - /// - /// The instance containing the - /// event data. - /// - internal static bool Extract(AspectEventArgs eventArgs) - { - if (Instance is not null) - return false; - - if (eventArgs?.Args is null) - return false; - - foreach (var arg in eventArgs.Args) - { - if (arg is null) - continue; - - var argType = arg.GetType(); - if (!argType.Name.EndsWith("LambdaContext")) - continue; - - Instance = new PowertoolsLambdaContext(); - - foreach (var prop in argType.GetProperties()) - { - if (prop.Name.Equals("AwsRequestId", StringComparison.CurrentCultureIgnoreCase)) - { - Instance.AwsRequestId = prop.GetValue(arg) as string; - } - else if (prop.Name.Equals("FunctionName", StringComparison.CurrentCultureIgnoreCase)) - { - Instance.FunctionName = prop.GetValue(arg) as string; - } - else if (prop.Name.Equals("FunctionVersion", StringComparison.CurrentCultureIgnoreCase)) - { - Instance.FunctionVersion = prop.GetValue(arg) as string; - } - else if (prop.Name.Equals("InvokedFunctionArn", StringComparison.CurrentCultureIgnoreCase)) - { - Instance.InvokedFunctionArn = prop.GetValue(arg) as string; - } - else if (prop.Name.Equals("LogGroupName", StringComparison.CurrentCultureIgnoreCase)) - { - Instance.LogGroupName = prop.GetValue(arg) as string; - } - else if (prop.Name.Equals("LogStreamName", StringComparison.CurrentCultureIgnoreCase)) - { - Instance.LogStreamName = prop.GetValue(arg) as string; - } - else if (prop.Name.Equals("MemoryLimitInMB", StringComparison.CurrentCultureIgnoreCase)) - { - var propVal = prop.GetValue(arg); - if (propVal is null || !int.TryParse(propVal.ToString(), out var intVal)) continue; - Instance.MemoryLimitInMB = intVal; - } - } - - return true; - } - - return false; - } - - /// - /// Clear the extracted lambda context. - /// - internal static void Clear() - { - Instance = null; - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj index 6cc22db7..cfa87700 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj @@ -13,6 +13,7 @@ + diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs index ac92098b..a436b645 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs @@ -19,10 +19,7 @@ using System.Linq; using System.Runtime.ExceptionServices; using System.Text.Json; -using System.Text.Json.Serialization; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal.Converters; -using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -108,8 +105,8 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// Get JsonSerializer options. /// /// The current configuration. - private static JsonSerializerOptions JsonSerializerOptions => - _jsonSerializerOptions ??= BuildJsonSerializerOptions(); + private JsonSerializerOptions JsonSerializerOptions => + _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(_loggerOutputCase); /// /// Initializes a new instance of the class. @@ -230,7 +227,7 @@ public void OnExit(AspectEventArgs eventArgs) if (!_isContextInitialized) return; if (_clearLambdaContext) - PowertoolsLambdaContext.Clear(); + LoggingLambdaContext.Clear(); if (_clearState) Logger.RemoveAllKeys(); _initializeContext = true; @@ -271,42 +268,21 @@ private void CaptureXrayTraceId() /// private void CaptureLambdaContext(AspectEventArgs eventArgs) { - _clearLambdaContext = PowertoolsLambdaContext.Extract(eventArgs); - if (PowertoolsLambdaContext.Instance is null && IsDebug()) + _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs); + if (LoggingLambdaContext.Instance is null && IsDebug()) _systemWrapper.LogLine( "Skipping Lambda Context injection because ILambdaContext context parameter not found."); } - /// - /// Builds JsonSerializer options. - /// - private static JsonSerializerOptions BuildJsonSerializerOptions() - { - var jsonOptions = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - jsonOptions.Converters.Add(new ByteArrayConverter()); - jsonOptions.Converters.Add(new ExceptionConverter()); - jsonOptions.Converters.Add(new MemoryStreamConverter()); - jsonOptions.Converters.Add(new ConstantClassConverter()); - jsonOptions.Converters.Add(new DateOnlyConverter()); - jsonOptions.Converters.Add(new TimeOnlyConverter()); - -#if NET8_0_OR_GREATER - jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default; -#endif - - return jsonOptions; - } - /// /// Captures the correlation identifier. /// /// The event argument. [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Everything is ok with serialization")] + [UnconditionalSuppressMessage("AOT", + "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", + Justification = "Everything is ok with serialization")] private void CaptureCorrelationId(object eventArg) { if (string.IsNullOrWhiteSpace(_correlationIdPath)) @@ -336,12 +312,12 @@ private void CaptureCorrelationId(object eventArg) #else var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions)); #endif - var element = jsonDoc.RootElement; for (var i = 0; i < correlationIdPaths.Length; i++) { - if (!element.TryGetProperty(correlationIdPaths[i], out var childElement)) + var pathWithOutputCase = ConvertToOutputCase(correlationIdPaths[i]); + if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; element = childElement; @@ -360,6 +336,32 @@ private void CaptureCorrelationId(object eventArg) } } + private string ConvertToOutputCase(string correlationIdPath) + { + return _loggerOutputCase switch + { + LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), + LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), + _ => ToSnakeCase(correlationIdPath), // default snake_case + }; + } + + private string ToSnakeCase(string correlationIdPath) + { + return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())) + .ToLowerInvariant(); + } + + private string ToPascalCase(string correlationIdPath) + { + return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); + } + + private string ToCamelCase(string correlationIdPath) + { + return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); + } + /// /// Logs the event. /// @@ -407,7 +409,7 @@ internal static void ResetForTest() { _isColdStart = true; _initializeContext = true; - PowertoolsLambdaContext.Clear(); + LoggingLambdaContext.Clear(); Logger.LoggerProvider = null; Logger.RemoveAllKeys(); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs new file mode 100644 index 00000000..a8846b15 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs @@ -0,0 +1,104 @@ +using System; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Common; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Lambda Context +/// +public class LoggingLambdaContext +{ + /// + /// The AWS request ID associated with the request. + /// This is the same ID returned to the client that called invoke(). + /// This ID is reused for retries on the same request. + /// + internal string AwsRequestId { get; private set; } + + /// Name of the Lambda function that is running. + internal string FunctionName { get; private set; } + + /// + /// The Lambda function version that is executing. + /// If an alias is used to invoke the function, then this will be + /// the version the alias points to. + /// + internal string FunctionVersion { get; private set; } + + /// + /// The ARN used to invoke this function. + /// It can be function ARN or alias ARN. + /// An unqualified ARN executes the $LATEST version and aliases execute + /// the function version they are pointing to. + /// + internal string InvokedFunctionArn { get; private set; } + + /// + /// The CloudWatch log group name associated with the invoked function. + /// It can be null if the IAM user provided does not have permission for + /// CloudWatch actions. + /// + internal string LogGroupName { get; private set; } + + /// + /// The CloudWatch log stream name for this function execution. + /// It can be null if the IAM user provided does not have permission + /// for CloudWatch actions. + /// + internal string LogStreamName { get; private set; } + + /// + /// Memory limit, in MB, you configured for the Lambda function. + /// + internal int MemoryLimitInMB { get; private set; } + + /// + /// The instance + /// + internal static LoggingLambdaContext Instance { get; private set; } + + /// + /// Gets the Lambda context + /// + /// + /// + public static bool Extract(AspectEventArgs args) + { + if (Instance is not null) + return false; + + if (args?.Args is null) + return false; + if (args.Method is null) + return false; + + var index = Array.FindIndex(args.Method.GetParameters(), p => p.ParameterType == typeof(ILambdaContext)); + if (index >= 0) + { + var x = (ILambdaContext)args.Args[index]; + + Instance = new LoggingLambdaContext + { + AwsRequestId = x.AwsRequestId, + FunctionName = x.FunctionName, + FunctionVersion = x.FunctionVersion, + InvokedFunctionArn = x.InvokedFunctionArn, + LogGroupName = x.LogGroupName, + LogStreamName = x.LogStreamName, + MemoryLimitInMB = x.MemoryLimitInMB + }; + return true; + } + + return false; + } + + /// + /// Clear the extracted lambda context. + /// + internal static void Clear() + { + Instance = null; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs index 232d7c49..8f407aab 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -15,7 +15,11 @@ using System; using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Converters; +using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -42,10 +46,11 @@ internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsCo return LoggingConstants.DefaultLogLevel; } - + internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations) { - AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), out var awsLogLevel); + AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), + out var awsLogLevel); if (Enum.TryParse(awsLogLevel, true, out LogLevel result)) { @@ -54,7 +59,7 @@ internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powert return LogLevel.None; } - + internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, LoggerOutputCase? loggerOutputCase = null) { @@ -76,4 +81,42 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati { "ERROR", "ERROR" }, { "FATAL", "CRITICAL" } }; + + internal static JsonSerializerOptions BuildJsonSerializerOptions( + this IPowertoolsConfigurations powertoolsConfigurations, LoggerOutputCase? loggerOutputCase = null) + { + var jsonOptions = powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch + { + LoggerOutputCase.CamelCase => new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + }, + LoggerOutputCase.PascalCase => new JsonSerializerOptions + { + PropertyNamingPolicy = PascalCaseNamingPolicy.Instance, + DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance + }, + _ => new JsonSerializerOptions //defaults to snake_case + { + PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance, + DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance + } + }; + + jsonOptions.Converters.Add(new ByteArrayConverter()); + jsonOptions.Converters.Add(new ExceptionConverter()); + jsonOptions.Converters.Add(new MemoryStreamConverter()); + jsonOptions.Converters.Add(new ConstantClassConverter()); + jsonOptions.Converters.Add(new DateOnlyConverter()); + jsonOptions.Converters.Add(new TimeOnlyConverter()); + + jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + +#if NET8_0_OR_GREATER + jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default; +#endif + + return jsonOptions; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index a1733728..a2867168 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -118,7 +118,7 @@ public PowertoolsLogger( /// /// The current configuration. private JsonSerializerOptions JsonSerializerOptions => - _jsonSerializerOptions ??= BuildJsonSerializerOptions(); + _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(CurrentConfig.LoggerOutputCase); internal PowertoolsLoggerScope CurrentScope { get; private set; } @@ -259,13 +259,13 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(key, value); // Add Lambda Context Keys - if (PowertoolsLambdaContext.Instance is not null) + if (LoggingLambdaContext.Instance is not null) { - logEntry.TryAdd(LoggingConstants.KeyFunctionName, PowertoolsLambdaContext.Instance.FunctionName); - logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, PowertoolsLambdaContext.Instance.FunctionVersion); - logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, PowertoolsLambdaContext.Instance.MemoryLimitInMB); - logEntry.TryAdd(LoggingConstants.KeyFunctionArn, PowertoolsLambdaContext.Instance.InvokedFunctionArn); - logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, PowertoolsLambdaContext.Instance.AwsRequestId); + logEntry.TryAdd(LoggingConstants.KeyFunctionName, LoggingLambdaContext.Instance.FunctionName); + logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, LoggingLambdaContext.Instance.FunctionVersion); + logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, LoggingLambdaContext.Instance.MemoryLimitInMB); + logEntry.TryAdd(LoggingConstants.KeyFunctionArn, LoggingLambdaContext.Instance.InvokedFunctionArn); + logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, LoggingLambdaContext.Instance.AwsRequestId); } // Add Extra Fields @@ -360,15 +360,15 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec logEntry.ExtraKeys = extraKeys; // Add Lambda Context Keys - if (PowertoolsLambdaContext.Instance is not null) + if (LoggingLambdaContext.Instance is not null) { logEntry.LambdaContext = new LogEntryLambdaContext { - FunctionName = PowertoolsLambdaContext.Instance.FunctionName, - FunctionVersion = PowertoolsLambdaContext.Instance.FunctionVersion, - MemoryLimitInMB = PowertoolsLambdaContext.Instance.MemoryLimitInMB, - InvokedFunctionArn = PowertoolsLambdaContext.Instance.InvokedFunctionArn, - AwsRequestId = PowertoolsLambdaContext.Instance.AwsRequestId, + FunctionName = LoggingLambdaContext.Instance.FunctionName, + FunctionVersion = LoggingLambdaContext.Instance.FunctionVersion, + MemoryLimitInMB = LoggingLambdaContext.Instance.MemoryLimitInMB, + InvokedFunctionArn = LoggingLambdaContext.Instance.InvokedFunctionArn, + AwsRequestId = LoggingLambdaContext.Instance.AwsRequestId, }; } @@ -488,42 +488,4 @@ private static bool CustomFormatter(TState state, Exception exception, o return true; } - - /// - /// Builds JsonSerializer options. - /// - private JsonSerializerOptions BuildJsonSerializerOptions() - { - var jsonOptions = CurrentConfig.LoggerOutputCase switch - { - LoggerOutputCase.CamelCase => new JsonSerializerOptions(JsonSerializerDefaults.Web) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase - }, - LoggerOutputCase.PascalCase => new JsonSerializerOptions - { - PropertyNamingPolicy = PascalCaseNamingPolicy.Instance, - DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance - }, - _ => new JsonSerializerOptions - { - PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance, - DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance - } - }; - jsonOptions.Converters.Add(new ByteArrayConverter()); - jsonOptions.Converters.Add(new ExceptionConverter()); - jsonOptions.Converters.Add(new MemoryStreamConverter()); - jsonOptions.Converters.Add(new ConstantClassConverter()); - jsonOptions.Converters.Add(new DateOnlyConverter()); - jsonOptions.Converters.Add(new TimeOnlyConverter()); - - jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; -#if NET8_0_OR_GREATER - jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default; -#endif - - return jsonOptions; - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs index bfa62789..a03de4f2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -44,8 +44,13 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; [JsonSerializable(typeof(Byte[]))] [JsonSerializable(typeof(MemoryStream))] [JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(APIGatewayProxyResponse))] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] [JsonSerializable(typeof(LogEntry))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext))] internal partial class LoggingSerializationContext : JsonSerializerContext { diff --git a/libraries/src/Directory.Build.props b/libraries/src/Directory.Build.props index 8f412fa4..ebe62090 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -21,7 +21,7 @@ - + true true true diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs deleted file mode 100644 index d6f87fae..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Xunit; - -namespace AWS.Lambda.Powertools.Common.Tests; - -public class PowertoolsLambdaContextTest -{ - private class TestLambdaContext - { - public string AwsRequestId { get; set; } - public string FunctionName { get; set; } - public string FunctionVersion { get; set; } - public string InvokedFunctionArn { get; set; } - public string LogGroupName { get; set; } - public string LogStreamName { get; set; } - public int MemoryLimitInMB { get; set; } - } - - private static TestLambdaContext NewLambdaContext() - { - return new TestLambdaContext - { - AwsRequestId = Guid.NewGuid().ToString(), - FunctionName = Guid.NewGuid().ToString(), - FunctionVersion = Guid.NewGuid().ToString(), - InvokedFunctionArn = Guid.NewGuid().ToString(), - LogGroupName = Guid.NewGuid().ToString(), - LogStreamName = Guid.NewGuid().ToString(), - MemoryLimitInMB = new Random().Next() - }; - } - - [Fact] - public void Extract_WhenHasLambdaContextArgument_InitializesLambdaContextInfo() - { - // Arrange - var lambdaContext = NewLambdaContext(); - var eventArg = new {Source = "Test"}; - var eventArgs = new AspectEventArgs - { - Name = Guid.NewGuid().ToString(), - Args = new object [] - { - eventArg, - lambdaContext - } - }; - - // Act && Assert - PowertoolsLambdaContext.Clear(); - Assert.Null(PowertoolsLambdaContext.Instance); - Assert.True(PowertoolsLambdaContext.Extract(eventArgs)); - Assert.NotNull(PowertoolsLambdaContext.Instance); - Assert.False(PowertoolsLambdaContext.Extract(eventArgs)); - Assert.Equal(PowertoolsLambdaContext.Instance.AwsRequestId, lambdaContext.AwsRequestId); - Assert.Equal(PowertoolsLambdaContext.Instance.FunctionName, lambdaContext.FunctionName); - Assert.Equal(PowertoolsLambdaContext.Instance.FunctionVersion, lambdaContext.FunctionVersion); - Assert.Equal(PowertoolsLambdaContext.Instance.InvokedFunctionArn, lambdaContext.InvokedFunctionArn); - Assert.Equal(PowertoolsLambdaContext.Instance.LogGroupName, lambdaContext.LogGroupName); - Assert.Equal(PowertoolsLambdaContext.Instance.LogStreamName, lambdaContext.LogStreamName); - Assert.Equal(PowertoolsLambdaContext.Instance.MemoryLimitInMB, lambdaContext.MemoryLimitInMB); - PowertoolsLambdaContext.Clear(); - Assert.Null(PowertoolsLambdaContext.Instance); - } -} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs new file mode 100644 index 00000000..55a8f928 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs @@ -0,0 +1,120 @@ +using System; +using System.Reflection; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests; + +public class LambdaContextTest +{ + [Fact] + public void Extract_WhenHasLambdaContextArgument_InitializesLambdaContextInfo() + { + // Arrange + var lambdaContext = new TestLambdaContext + { + AwsRequestId = Guid.NewGuid().ToString(), + FunctionName = Guid.NewGuid().ToString(), + FunctionVersion = Guid.NewGuid().ToString(), + InvokedFunctionArn = Guid.NewGuid().ToString(), + LogGroupName = Guid.NewGuid().ToString(), + LogStreamName = Guid.NewGuid().ToString(), + MemoryLimitInMB = new Random().Next() + }; + + var args = Substitute.For(); + var method = Substitute.For(); + var parameter = Substitute.For(); + + // Setup parameter + parameter.ParameterType.Returns(typeof(ILambdaContext)); + + // Setup method + method.GetParameters().Returns(new[] { parameter }); + + // Setup args + args.Method = method; + args.Args = new object[] { lambdaContext }; + + // Act && Assert + LoggingLambdaContext.Clear(); + Assert.Null(LoggingLambdaContext.Instance); + Assert.True(LoggingLambdaContext.Extract(args)); + Assert.NotNull(LoggingLambdaContext.Instance); + Assert.Equal(LoggingLambdaContext.Instance.AwsRequestId, lambdaContext.AwsRequestId); + Assert.Equal(LoggingLambdaContext.Instance.FunctionName, lambdaContext.FunctionName); + Assert.Equal(LoggingLambdaContext.Instance.FunctionVersion, lambdaContext.FunctionVersion); + Assert.Equal(LoggingLambdaContext.Instance.InvokedFunctionArn, lambdaContext.InvokedFunctionArn); + Assert.Equal(LoggingLambdaContext.Instance.LogGroupName, lambdaContext.LogGroupName); + Assert.Equal(LoggingLambdaContext.Instance.LogStreamName, lambdaContext.LogStreamName); + Assert.Equal(LoggingLambdaContext.Instance.MemoryLimitInMB, lambdaContext.MemoryLimitInMB); + LoggingLambdaContext.Clear(); + Assert.Null(LoggingLambdaContext.Instance); + } + + [Fact] + public void Extract_When_Args_Null_Returns_False() + { + // Arrange + var args = Substitute.For(); + + // Act && Assert + LoggingLambdaContext.Clear(); + Assert.Null(LoggingLambdaContext.Instance); + Assert.False(LoggingLambdaContext.Extract(args)); + } + + [Fact] + public void Extract_When_Method_Null_Returns_False() + { + // Arrange + var args = Substitute.For(); + args.Args = Array.Empty(); + + // Act && Assert + LoggingLambdaContext.Clear(); + Assert.Null(LoggingLambdaContext.Instance); + Assert.False(LoggingLambdaContext.Extract(args)); + } + + [Fact] + public void Extract_WhenInstance_Already_Created_Returns_False() + { + // Arrange + var lambdaContext = new TestLambdaContext + { + AwsRequestId = Guid.NewGuid().ToString(), + FunctionName = Guid.NewGuid().ToString(), + FunctionVersion = Guid.NewGuid().ToString(), + InvokedFunctionArn = Guid.NewGuid().ToString(), + LogGroupName = Guid.NewGuid().ToString(), + LogStreamName = Guid.NewGuid().ToString(), + MemoryLimitInMB = new Random().Next() + }; + + var args = Substitute.For(); + var method = Substitute.For(); + var parameter = Substitute.For(); + + // Setup parameter + parameter.ParameterType.Returns(typeof(ILambdaContext)); + + // Setup method + method.GetParameters().Returns(new[] { parameter }); + + // Setup args + args.Method = method; + args.Args = new object[] { lambdaContext }; + + // Act && Assert + LoggingLambdaContext.Clear(); + Assert.Null(LoggingLambdaContext.Instance); + Assert.True(LoggingLambdaContext.Extract(args)); + Assert.NotNull(LoggingLambdaContext.Instance); + Assert.False(LoggingLambdaContext.Extract(args)); + } +} diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs index 158d8fe8..620ebe5e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs @@ -16,14 +16,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text.Json; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; using Xunit; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace AWS.Lambda.Powertools.Logging.Tests { @@ -57,7 +60,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() }; Logger.AppendKeys(globalExtraKeys); - var lambdaContext = new LogEntryLambdaContext + var lambdaContext = new TestLambdaContext { FunctionName = Guid.NewGuid().ToString(), FunctionVersion = Guid.NewGuid().ToString(), @@ -65,21 +68,25 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() AwsRequestId = Guid.NewGuid().ToString(), MemoryLimitInMB = (new Random()).Next() }; + + var args = Substitute.For(); + var method = Substitute.For(); + var parameter = Substitute.For(); + + // Setup parameter + parameter.ParameterType.Returns(typeof(ILambdaContext)); - var eventArgs = new AspectEventArgs - { - Name = Guid.NewGuid().ToString(), - Args = new object[] - { - new - { - Source = "Test" - }, - lambdaContext - } - }; - PowertoolsLambdaContext.Extract(eventArgs); + // Setup method + method.GetParameters().Returns(new[] { parameter }); + // Setup args + args.Method = method; + args.Args = new object[] { lambdaContext }; + + // Act + + LoggingLambdaContext.Extract(args); + var logFormatter = Substitute.For(); var formattedLogEntry = new { @@ -161,7 +168,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() //Clean up Logger.UseDefaultFormatter(); Logger.RemoveAllKeys(); - PowertoolsLambdaContext.Clear(); + LoggingLambdaContext.Clear(); LoggingAspectHandler.ResetForTest(); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs index 29fd9a40..cb0cad09 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs @@ -20,6 +20,7 @@ using Amazon.Lambda.ApplicationLoadBalancerEvents; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -236,7 +237,7 @@ protected void OnEntry_WhenEventArgExists_CapturesCorrelationIdBase(string corre var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var configurations = Substitute.For(); + var configurations = new PowertoolsConfigurations(new SystemWrapperMock(new PowertoolsEnvironment())); var systemWrapper = Substitute.For(); var eventArgs = new AspectEventArgs From 19e82b86ef7a9ab22dbc0e835158b692d8ba6e15 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:43:58 +0100 Subject: [PATCH 04/32] Fix AOT warnings, new way to extract properties from Exceptions. New serialization context with property names --- .../Internal/Converters/ExceptionConverter.cs | 14 ++-- .../Converters/ExceptionPropertyExtractor.cs | 78 +++++++++++++++++++ .../LoggingSerializationContext.cs | 10 ++- .../PowertoolsLoggerTest.cs | 3 + 4 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs 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 db74bc0c..e57e4159 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs @@ -58,15 +58,13 @@ public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, Js public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) { var exceptionType = value.GetType(); - var properties = exceptionType.GetProperties() - .Where(prop => prop.Name != nameof(Exception.TargetSite)) - .Select(prop => new { prop.Name, Value = prop.GetValue(value) }); + var properties = ExceptionPropertyExtractor.ExtractProperties(value); if (options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull) properties = properties.Where(prop => prop.Value != null); var props = properties.ToArray(); - if (!props.Any()) + if (props.Length == 0) return; writer.WriteStartObject(); @@ -77,16 +75,16 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize switch (prop.Value) { case IntPtr intPtr: - writer.WriteNumber(ApplyPropertyNamingPolicy(prop.Name, options), intPtr.ToInt64()); + writer.WriteNumber(ApplyPropertyNamingPolicy(prop.Key, options), intPtr.ToInt64()); break; case UIntPtr uIntPtr: - writer.WriteNumber(ApplyPropertyNamingPolicy(prop.Name, options), uIntPtr.ToUInt64()); + writer.WriteNumber(ApplyPropertyNamingPolicy(prop.Key, options), uIntPtr.ToUInt64()); break; case Type propType: - writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propType.FullName); + writer.WriteString(ApplyPropertyNamingPolicy(prop.Key, options), propType.FullName); break; case string propString: - writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propString); + writer.WriteString(ApplyPropertyNamingPolicy(prop.Key, options), propString); break; } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs new file mode 100644 index 00000000..3de40280 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; + +namespace AWS.Lambda.Powertools.Logging.Internal.Converters; + +/// +/// Class ExceptionPropertyExtractor. +/// This class is used to extract properties from an exception object. +/// It uses a dictionary of type to function mappings to extract specific properties based on the exception type. +/// If no specific extractor is found, it falls back to the base Exception extractor. +/// +internal static class ExceptionPropertyExtractor +{ + + // Note: Leave code comments for future reference + + /// + /// The property extractors + /// + private static readonly Dictionary>>> PropertyExtractors = new() + { + { typeof(Exception), GetBaseExceptionProperties }, + // Add more specific exception types here + // { typeof(ArgumentException), GetArgumentExceptionProperties }, + // { typeof(InvalidOperationException), GetInvalidOperationExceptionProperties }, + // { typeof(NullReferenceException), GetBaseExceptionProperties }, + // ... add more as needed + }; + + /// + /// Use this method to extract properties from and Exception based type + /// This method is used when building for native AOT + /// + /// + /// + public static IEnumerable> ExtractProperties(Exception exception) + { + // var exceptionType = exception.GetType(); + + // if (PropertyExtractors.TryGetValue(exceptionType, out var extractor)) + // { + // return extractor(exception); + // } + + // If we don't have a specific extractor, use the base Exception extractor + return GetBaseExceptionProperties(exception); + } + + /// + /// Get the base Exception properties + /// + /// + /// + private static IEnumerable> GetBaseExceptionProperties(Exception ex) + { + 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); + // Add any other properties you want to extract + } + + // private static IEnumerable> GetArgumentExceptionProperties(Exception ex) + // { + // var argEx = (ArgumentException)ex; + // foreach (var prop in GetBaseExceptionProperties(ex)) + // { + // yield return prop; + // } + // yield return new KeyValuePair(nameof(ArgumentException.ParamName), argEx.ParamName); + // } + // + // private static IEnumerable> GetInvalidOperationExceptionProperties(Exception ex) + // { + // // InvalidOperationException doesn't have any additional properties + // // but we include it here as an example + // return GetBaseExceptionProperties(ex); + // } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs index a03de4f2..f68eed5a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -27,7 +27,7 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; /// /// Custom JSON serializer context for AWS.Lambda.Powertools.Logging /// -// [JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(bool))] [JsonSerializable(typeof(string))] @@ -45,12 +45,16 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; [JsonSerializable(typeof(MemoryStream))] [JsonSerializable(typeof(APIGatewayProxyRequest))] [JsonSerializable(typeof(APIGatewayProxyResponse))] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext), TypeInfoPropertyName = "APIGatewayProxyRequestContext")] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert), TypeInfoPropertyName = "APIGatewayProxyRequestProxyRequestClientCert")] +[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity), TypeInfoPropertyName = "APIGatewayProxyRequestClientCertValidity")] [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] [JsonSerializable(typeof(LogEntry))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestContext")] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestProxyRequestClientCert")] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestClientCertValidity")] internal partial class LoggingSerializationContext : JsonSerializerContext { diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index 4714cbec..a600194f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -1039,6 +1039,9 @@ public void Log_WhenException_LogsExceptionDetails() s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); + systemWrapper.Received(1).LogLine(Arg.Is(s => + 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() in /Users/henrigra/work/aws-lambda-powertools-dotnet/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs:line 1030\"}}") + )); } [Fact] From 521645094a6231b63d97f835446704c1480dfbe9 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:09:03 +0100 Subject: [PATCH 05/32] remove non-deterministic string from tests --- .../AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index a600194f..e1229701 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -1040,7 +1040,7 @@ public void Log_WhenException_LogsExceptionDetails() error.Message + "\"") )); systemWrapper.Received(1).LogLine(Arg.Is(s => - 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() in /Users/henrigra/work/aws-lambda-powertools-dotnet/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs:line 1030\"}}") + 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()") )); } From 35e613b0d3b22ea304adad0ab0a74509e951e844 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:11:14 +0100 Subject: [PATCH 06/32] remove comments --- .../Converters/ExceptionPropertyExtractor.cs | 34 ------------------- 1 file changed, 34 deletions(-) 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 3de40280..80751d3e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs @@ -11,20 +11,12 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Converters; /// internal static class ExceptionPropertyExtractor { - - // Note: Leave code comments for future reference - /// /// The property extractors /// private static readonly Dictionary>>> PropertyExtractors = new() { { typeof(Exception), GetBaseExceptionProperties }, - // Add more specific exception types here - // { typeof(ArgumentException), GetArgumentExceptionProperties }, - // { typeof(InvalidOperationException), GetInvalidOperationExceptionProperties }, - // { typeof(NullReferenceException), GetBaseExceptionProperties }, - // ... add more as needed }; /// @@ -35,14 +27,6 @@ internal static class ExceptionPropertyExtractor /// public static IEnumerable> ExtractProperties(Exception exception) { - // var exceptionType = exception.GetType(); - - // if (PropertyExtractors.TryGetValue(exceptionType, out var extractor)) - // { - // return extractor(exception); - // } - - // If we don't have a specific extractor, use the base Exception extractor return GetBaseExceptionProperties(exception); } @@ -56,23 +40,5 @@ 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); - // Add any other properties you want to extract } - - // private static IEnumerable> GetArgumentExceptionProperties(Exception ex) - // { - // var argEx = (ArgumentException)ex; - // foreach (var prop in GetBaseExceptionProperties(ex)) - // { - // yield return prop; - // } - // yield return new KeyValuePair(nameof(ArgumentException.ParamName), argEx.ParamName); - // } - // - // private static IEnumerable> GetInvalidOperationExceptionProperties(Exception ex) - // { - // // InvalidOperationException doesn't have any additional properties - // // but we include it here as an example - // return GetBaseExceptionProperties(ex); - // } } \ No newline at end of file From 42138f7db885de2e408a2350a732a3f7c07ea6a4 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:59:22 +0100 Subject: [PATCH 07/32] refactor, change the way we create Logger in tests to use LoggerProvider --- .../Helpers/PowertoolsLoggerHelpers.cs | 60 ++++ .../Internal/LoggerProvider.cs | 42 ++- .../Internal/LoggingAspectHandler.cs | 90 +---- .../Internal/PowertoolsConfigurations.cs | 122 ------- .../PowertoolsConfigurationsExtension.cs | 163 +++++++++ .../Internal/PowertoolsLogger.cs | 126 +------ .../AWS.Lambda.Powertools.Logging/Logger.cs | 3 - .../LoggingAttribute.cs | 27 +- .../LoggingSerializationContext.cs | 22 +- .../Serializers/PowertoolsLambdaSerializer.cs | 79 +++++ .../PowertoolsLoggingSerializer.cs | 182 ++++++++++ .../LogFormatterTest.cs | 67 ++-- .../LoggingAttributeTest.cs | 45 ++- .../PowertoolsLoggerTest.cs | 334 ++++++++++-------- 14 files changed, 825 insertions(+), 537 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs 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 55bc7053..428deb68 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -14,11 +14,23 @@ */ using System.Linq; +using AWS.Lambda.Powertools.Common; namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; +/// +/// Class PowertoolsLoggerHelpers. +/// internal static class PowertoolsLoggerHelpers { + /// + /// Converts an object to a dictionary. + /// + /// The object to convert. + /// + /// If the object has a namespace, returns the object as-is. + /// Otherwise, returns a dictionary representation of the object's properties. + /// internal static object ObjectToDictionary(object anonymousObject) { if (anonymousObject.GetType().Namespace is not null) @@ -29,4 +41,52 @@ internal static object ObjectToDictionary(object anonymousObject) return anonymousObject.GetType().GetProperties() .ToDictionary(prop => prop.Name, prop => ObjectToDictionary(prop.GetValue(anonymousObject, null))); } + + /// + /// Converts the input string to the configured output case. + /// + /// The string to convert. + /// + /// The input string converted to the configured case (camel, pascal, or snake case). + /// + internal static string ConvertToOutputCase(string correlationIdPath) + { + return PowertoolsConfigurations.Instance.GetLoggerOutputCase() switch + { + LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), + LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), + _ => ToSnakeCase(correlationIdPath), // default snake_case + }; + } + + /// + /// Converts a string to snake_case. + /// + /// The string to convert. + /// The input string converted to snake_case. + private static string ToSnakeCase(string correlationIdPath) + { + return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())) + .ToLowerInvariant(); + } + + /// + /// Converts a string to PascalCase. + /// + /// The string to convert. + /// The input string converted to PascalCase. + private static string ToPascalCase(string correlationIdPath) + { + return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); + } + + /// + /// Converts a string to camelCase. + /// + /// The string to convert. + /// The input string converted to camelCase. + private static string ToCamelCase(string correlationIdPath) + { + return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index 5b18ff97..e11791c8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -27,12 +27,21 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// public sealed class LoggerProvider : ILoggerProvider { + /// + /// The powertools configurations + /// + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + + /// + /// The system wrapper + /// + private readonly ISystemWrapper _systemWrapper; + /// /// The loggers /// private readonly ConcurrentDictionary _loggers = new(); - /// /// The current configuration /// @@ -42,9 +51,14 @@ public sealed class LoggerProvider : ILoggerProvider /// Initializes a new instance of the class. /// /// The configuration. - public LoggerProvider(IOptions config) + /// + /// + public LoggerProvider(IOptions config, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) { - _currentConfig = config?.Value; + _powertoolsConfigurations = powertoolsConfigurations; + _systemWrapper = systemWrapper; + + _currentConfig= powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper); } /// @@ -56,9 +70,9 @@ public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger(name, - PowertoolsConfigurations.Instance, - SystemWrapper.Instance, - GetCurrentConfig)); + _powertoolsConfigurations, + _systemWrapper, + _currentConfig)); } /// @@ -69,14 +83,6 @@ public void Dispose() _loggers.Clear(); } - /// - /// Gets the current configuration. - /// - /// LoggerConfiguration. - private LoggerConfiguration GetCurrentConfig() - { - return _currentConfig; - } /// /// Configures the loggers. @@ -91,4 +97,4 @@ internal void Configure(IOptions config) foreach (var logger in _loggers.Values) logger.ClearConfig(); } -} +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs index a436b645..f3564bda 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs @@ -14,12 +14,13 @@ */ using System; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.ExceptionServices; using System.Text.Json; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -61,26 +62,11 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// private readonly LogLevel? _logLevel; - /// - /// The logger output case - /// - private readonly LoggerOutputCase? _loggerOutputCase; - /// /// The Powertools for AWS Lambda (.NET) configurations /// private readonly IPowertoolsConfigurations _powertoolsConfigurations; - /// - /// The sampling rate - /// - private readonly double? _samplingRate; - - /// - /// Service name - /// - private readonly string _service; - /// /// The system wrapper /// @@ -97,24 +83,15 @@ internal class LoggingAspectHandler : IMethodAspectHandler private bool _clearLambdaContext; /// - /// The JsonSerializer options + /// The configuration /// - private static JsonSerializerOptions _jsonSerializerOptions; - - /// - /// Get JsonSerializer options. - /// - /// The current configuration. - private JsonSerializerOptions JsonSerializerOptions => - _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(_loggerOutputCase); + private readonly LoggerConfiguration _config; /// /// Initializes a new instance of the class. /// - /// Service name + /// /// The log level. - /// The logger output case. - /// The sampling rate. /// if set to true [log event]. /// The correlation identifier path. /// if set to true [clear state]. @@ -122,10 +99,8 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// The system wrapper. internal LoggingAspectHandler ( - string service, + LoggerConfiguration config, LogLevel? logLevel, - LoggerOutputCase? loggerOutputCase, - double? samplingRate, bool? logEvent, string correlationIdPath, bool clearState, @@ -133,15 +108,13 @@ internal LoggingAspectHandler ISystemWrapper systemWrapper ) { - _service = service; _logLevel = logLevel; - _loggerOutputCase = loggerOutputCase; - _samplingRate = samplingRate; _logEvent = logEvent; _clearState = clearState; _correlationIdPath = correlationIdPath; _powertoolsConfigurations = powertoolsConfigurations; _systemWrapper = systemWrapper; + _config = config; } /// @@ -153,21 +126,13 @@ ISystemWrapper systemWrapper /// public void OnEntry(AspectEventArgs eventArgs) { - var loggerConfig = new LoggerConfiguration - { - Service = _service, - MinimumLevel = _logLevel, - SamplingRate = _samplingRate, - LoggerOutputCase = _loggerOutputCase - }; - switch (Logger.LoggerProvider) { case null: - Logger.LoggerProvider = new LoggerProvider(loggerConfig); + Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper ); break; case LoggerProvider: - ((LoggerProvider)Logger.LoggerProvider).Configure(loggerConfig); + ((LoggerProvider)Logger.LoggerProvider).Configure(_config); break; } @@ -278,11 +243,6 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) /// Captures the correlation identifier. /// /// The event argument. - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", - Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] - [UnconditionalSuppressMessage("AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = "Everything is ok with serialization")] private void CaptureCorrelationId(object eventArg) { if (string.IsNullOrWhiteSpace(_correlationIdPath)) @@ -308,15 +268,15 @@ private void CaptureCorrelationId(object eventArg) #if NET8_0_OR_GREATER var jsonDoc = - JsonDocument.Parse(JsonSerializer.Serialize(eventArg, eventArg.GetType(), JsonSerializerOptions)); + JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); #else - var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions)); + var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg)); #endif var element = jsonDoc.RootElement; for (var i = 0; i < correlationIdPaths.Length; i++) { - var pathWithOutputCase = ConvertToOutputCase(correlationIdPaths[i]); + var pathWithOutputCase = PowertoolsLoggerHelpers.ConvertToOutputCase(correlationIdPaths[i]); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; @@ -336,32 +296,6 @@ private void CaptureCorrelationId(object eventArg) } } - private string ConvertToOutputCase(string correlationIdPath) - { - return _loggerOutputCase switch - { - LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), - LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), - _ => ToSnakeCase(correlationIdPath), // default snake_case - }; - } - - private string ToSnakeCase(string correlationIdPath) - { - return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())) - .ToLowerInvariant(); - } - - private string ToPascalCase(string correlationIdPath) - { - return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); - } - - private string ToCamelCase(string correlationIdPath) - { - return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); - } - /// /// Logs the event. /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs deleted file mode 100644 index 8f407aab..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -using System; -using System.Collections.Generic; -using System.Text.Encodings.Web; -using System.Text.Json; -using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal.Converters; -using AWS.Lambda.Powertools.Logging.Serializers; -using Microsoft.Extensions.Logging; - -namespace AWS.Lambda.Powertools.Logging.Internal; - -/// -/// Class PowertoolsConfigurationsExtension. -/// -internal static class PowertoolsConfigurationsExtension -{ - /// - /// Gets the log level. - /// - /// The Powertools for AWS Lambda (.NET) configurations. - /// The log level. - /// LogLevel. - internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations, - LogLevel? logLevel = null) - { - if (logLevel.HasValue) - return logLevel.Value; - - if (Enum.TryParse((powertoolsConfigurations.LogLevel ?? "").Trim(), true, out LogLevel result)) - return result; - - return LoggingConstants.DefaultLogLevel; - } - - internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations) - { - AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), - out var awsLogLevel); - - if (Enum.TryParse(awsLogLevel, true, out LogLevel result)) - { - return result; - } - - return LogLevel.None; - } - - internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, - LoggerOutputCase? loggerOutputCase = null) - { - if (loggerOutputCase.HasValue) - return loggerOutputCase.Value; - - if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result)) - return result; - - return LoggingConstants.DefaultLoggerOutputCase; - } - - private static readonly Dictionary AwsLogLevelMapper = new() - { - { "TRACE", "TRACE" }, - { "DEBUG", "DEBUG" }, - { "INFO", "INFORMATION" }, - { "WARN", "WARNING" }, - { "ERROR", "ERROR" }, - { "FATAL", "CRITICAL" } - }; - - internal static JsonSerializerOptions BuildJsonSerializerOptions( - this IPowertoolsConfigurations powertoolsConfigurations, LoggerOutputCase? loggerOutputCase = null) - { - var jsonOptions = powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch - { - LoggerOutputCase.CamelCase => new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase - }, - LoggerOutputCase.PascalCase => new JsonSerializerOptions - { - PropertyNamingPolicy = PascalCaseNamingPolicy.Instance, - DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance - }, - _ => new JsonSerializerOptions //defaults to snake_case - { - PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance, - DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance - } - }; - - jsonOptions.Converters.Add(new ByteArrayConverter()); - jsonOptions.Converters.Add(new ExceptionConverter()); - jsonOptions.Converters.Add(new MemoryStreamConverter()); - jsonOptions.Converters.Add(new ConstantClassConverter()); - jsonOptions.Converters.Add(new DateOnlyConverter()); - jsonOptions.Converters.Add(new TimeOnlyConverter()); - - jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; - -#if NET8_0_OR_GREATER - jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default; -#endif - - return jsonOptions; - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs new file mode 100644 index 00000000..d31caba9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -0,0 +1,163 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Serializers; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Class PowertoolsConfigurationsExtension. +/// +internal static class PowertoolsConfigurationsExtension +{ + /// + /// Gets the log level. + /// + /// The Powertools for AWS Lambda (.NET) configurations. + /// The log level. + /// LogLevel. + internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations, + LogLevel? logLevel = null) + { + if (logLevel.HasValue) + return logLevel.Value; + + if (Enum.TryParse((powertoolsConfigurations.LogLevel ?? "").Trim(), true, out LogLevel result)) + return result; + + return LoggingConstants.DefaultLogLevel; + } + + /// + /// Lambda Log Level Mapper + /// + /// + /// + internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations) + { + AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), + out var awsLogLevel); + + if (Enum.TryParse(awsLogLevel, true, out LogLevel result)) + { + return result; + } + + return LogLevel.None; + } + + /// + /// Determines the logger output case based on configuration and input. + /// + /// The Powertools configurations. + /// Optional explicit logger output case. + /// The determined LoggerOutputCase. + internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, + LoggerOutputCase? loggerOutputCase = null) + { + if (loggerOutputCase.HasValue) + return loggerOutputCase.Value; + + if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result)) + return result; + + return LoggingConstants.DefaultLoggerOutputCase; + } + + /// + /// Maps AWS log level to .NET log level + /// + private static readonly Dictionary AwsLogLevelMapper = new() + { + { "TRACE", "TRACE" }, + { "DEBUG", "DEBUG" }, + { "INFO", "INFORMATION" }, + { "WARN", "WARNING" }, + { "ERROR", "ERROR" }, + { "FATAL", "CRITICAL" } + }; + + /// + /// Gets the current configuration. + /// + /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. + public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, + LoggerConfiguration config, ISystemWrapper systemWrapper) + { + config ??= new LoggerConfiguration(); + + var logLevel = powertoolsConfigurations.GetLogLevel(config?.MinimumLevel); + var lambdaLogLevel = powertoolsConfigurations.GetLambdaLogLevel(); + var lambdaLogLevelEnabled = powertoolsConfigurations.LambdaLogLevelEnabled(); + + if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) + { + systemWrapper.LogLine( + $"Current log level ({logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); + } + + var samplingRate = config?.SamplingRate ?? powertoolsConfigurations.LoggerSampleRate; + var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config?.LoggerOutputCase); + var service = config?.Service ?? powertoolsConfigurations.Service; + + + var minLogLevel = logLevel; + if (lambdaLogLevelEnabled) + { + minLogLevel = lambdaLogLevel; + } + + config.Service = service; + config.MinimumLevel = minLogLevel; + config.SamplingRate = samplingRate; + config.LoggerOutputCase = loggerOutputCase; + + PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); + + if (!samplingRate.HasValue) + return config; + + if (samplingRate.Value < 0 || samplingRate.Value > 1) + { + if (minLogLevel is LogLevel.Debug or LogLevel.Trace) + systemWrapper.LogLine( + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}"); + config.SamplingRate = null; + return config; + } + + if (samplingRate.Value == 0) + return config; + + var sample = systemWrapper.GetRandom(); + if (samplingRate.Value > sample) + { + systemWrapper.LogLine( + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}."); + config.MinimumLevel = LogLevel.Debug; + } + + return config; + } + + internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powertoolsConfigurations) + { + return powertoolsConfigurations.GetLambdaLogLevel() != LogLevel.None; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index a2867168..0c37a6e2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -15,12 +15,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text.Encodings.Web; -using System.Text.Json; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal.Converters; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; @@ -34,11 +30,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal sealed class PowertoolsLogger : ILogger { - /// - /// The get current configuration - /// - private readonly Func _getCurrentConfig; - /// /// The name /// @@ -47,7 +38,7 @@ internal sealed class PowertoolsLogger : ILogger /// /// The current configuration /// - private LoggerConfiguration _currentConfig; + private static LoggerConfiguration _currentConfig; /// /// The Powertools for AWS Lambda (.NET) configurations @@ -59,67 +50,41 @@ internal sealed class PowertoolsLogger : ILogger /// private readonly ISystemWrapper _systemWrapper; - /// - /// The JsonSerializer options - /// - private JsonSerializerOptions _jsonSerializerOptions; - - private LogLevel _lambdaLogLevel; - private LogLevel _logLevel; - private bool _lambdaLogLevelEnabled; - /// /// Initializes a new instance of the class. /// /// The name. /// The Powertools for AWS Lambda (.NET) configurations. /// The system wrapper. - /// The get current configuration. + /// public PowertoolsLogger( string name, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, - Func getCurrentConfig) + LoggerConfiguration currentConfig) { - (_name, _powertoolsConfigurations, _systemWrapper, _getCurrentConfig) = (name, - powertoolsConfigurations, systemWrapper, getCurrentConfig); + (_name, _powertoolsConfigurations, _systemWrapper, _currentConfig) = (name, + powertoolsConfigurations, systemWrapper, currentConfig); _powertoolsConfigurations.SetExecutionEnvironment(this); - _currentConfig = GetCurrentConfig(); - - if (_lambdaLogLevelEnabled && _logLevel < _lambdaLogLevel) - { - var message = - $"Current log level ({_logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({_lambdaLogLevel}). This can lead to data loss, consider adjusting them."; - this.LogWarning(message); - } } - private LoggerConfiguration CurrentConfig => _currentConfig ??= GetCurrentConfig(); - /// /// Sets the minimum level. /// /// The minimum level. private LogLevel MinimumLevel => - CurrentConfig.MinimumLevel ?? LoggingConstants.DefaultLogLevel; + _currentConfig.MinimumLevel ?? LoggingConstants.DefaultLogLevel; /// /// Sets the service. /// /// The service. private string Service => - !string.IsNullOrWhiteSpace(CurrentConfig.Service) - ? CurrentConfig.Service + !string.IsNullOrWhiteSpace(_currentConfig.Service) + ? _currentConfig.Service : _powertoolsConfigurations.Service; - /// - /// Get JsonSerializer options. - /// - /// The current configuration. - private JsonSerializerOptions JsonSerializerOptions => - _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(CurrentConfig.LoggerOutputCase); - internal PowertoolsLoggerScope CurrentScope { get; private set; } /// @@ -210,11 +175,6 @@ public bool IsEnabled(LogLevel logLevel) /// The exception related to this entry. /// Function to create a message of the and . /// The type of the object to be written. - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", - Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] - [UnconditionalSuppressMessage("AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = "Everything is ok with serialization")] public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { @@ -236,9 +196,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except #if NET8_0_OR_GREATER - _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, typeof(object), JsonSerializerOptions)); + _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); #else - _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions)); + _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry)); #endif } @@ -279,8 +239,10 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } var keyLogLevel = LoggingConstants.KeyLogLevel; + var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); + // If ALC is enabled and PascalCase we need to convert Level to LogLevel for it to be parsed and sent to CW - if (_lambdaLogLevelEnabled && CurrentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase) + if (lambdaLogLevelEnabled && _currentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase) { keyLogLevel = "LogLevel"; } @@ -291,8 +253,8 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (CurrentConfig.SamplingRate.HasValue) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value); + if (_currentConfig.SamplingRate.HasValue) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate.Value); if (exception != null) logEntry.TryAdd(LoggingConstants.KeyException, exception); @@ -321,7 +283,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec Name = _name, Message = message, Exception = exception, - SamplingRate = CurrentConfig.SamplingRate, + SamplingRate = _currentConfig.SamplingRate, }; var extraKeys = new Dictionary(); @@ -389,8 +351,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec $"{logFormatter.GetType().FullName} raised an exception: {e.Message}.", e); } } - - + /// /// Clears the configuration. @@ -400,59 +361,6 @@ internal void ClearConfig() _currentConfig = null; } - /// - /// Gets the current configuration. - /// - /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. - private LoggerConfiguration GetCurrentConfig() - { - var currConfig = _getCurrentConfig(); - _logLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel); - var samplingRate = currConfig?.SamplingRate ?? _powertoolsConfigurations.LoggerSampleRate; - var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase); - _lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); - _lambdaLogLevelEnabled = _lambdaLogLevel != LogLevel.None; - - var minLogLevel = _logLevel; - if (_lambdaLogLevelEnabled) - { - minLogLevel = _lambdaLogLevel; - } - - var config = new LoggerConfiguration - { - Service = currConfig?.Service, - MinimumLevel = minLogLevel, - SamplingRate = samplingRate, - LoggerOutputCase = loggerOutputCase - }; - - if (!samplingRate.HasValue) - return config; - - if (samplingRate.Value < 0 || samplingRate.Value > 1) - { - if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - _systemWrapper.LogLine( - $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}"); - config.SamplingRate = null; - return config; - } - - if (samplingRate.Value == 0) - return config; - - var sample = _systemWrapper.GetRandom(); - if (samplingRate.Value > sample) - { - _systemWrapper.LogLine( - $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}."); - config.MinimumLevel = LogLevel.Debug; - } - - return config; - } - /// /// Formats message for a log entry. /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 5b55acc5..ad3611cb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Generic; using System.Linq; -using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -65,8 +64,6 @@ public static ILogger Create(string categoryName) if (string.IsNullOrWhiteSpace(categoryName)) throw new ArgumentNullException(nameof(categoryName)); - LoggerProvider ??= new LoggerProvider(null); - return LoggerProvider.CreateLogger(categoryName); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index d60ee045..43537b59 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -128,10 +128,6 @@ public class LoggingAttribute : MethodAspectAttribute /// private LogLevel? _logLevel; - /// - /// The logger output case - /// - private LoggerOutputCase? _loggerOutputCase; /// /// The sampling rate @@ -195,17 +191,13 @@ public bool LogEvent /// /// true if [clear state]; otherwise, false. public bool ClearState { get; set; } = false; - + /// /// Specify output case for logging (SnakeCase, by default). /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. /// /// The log level. - public LoggerOutputCase LoggerOutputCase - { - get => _loggerOutputCase ?? LoggingConstants.DefaultLoggerOutputCase; - set => _loggerOutputCase = value; - } + public LoggerOutputCase? LoggerOutputCase { get; set; } /// /// Creates the handler. @@ -213,13 +205,18 @@ public LoggerOutputCase LoggerOutputCase /// IMethodAspectHandler. protected override IMethodAspectHandler CreateHandler() { + var config = new LoggerConfiguration + { + Service = Service, + LoggerOutputCase = LoggerOutputCase, + SamplingRate = SamplingRate, + }; + return new LoggingAspectHandler ( - Service, - _logLevel, - _loggerOutputCase, - _samplingRate, - _logEvent, + config, + LogLevel, + LogEvent, CorrelationIdPath, ClearState, PowertoolsConfigurations.Instance, diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs index f68eed5a..022b0a5d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -45,19 +45,25 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; [JsonSerializable(typeof(MemoryStream))] [JsonSerializable(typeof(APIGatewayProxyRequest))] [JsonSerializable(typeof(APIGatewayProxyResponse))] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext), TypeInfoPropertyName = "APIGatewayProxyRequestContext")] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert), TypeInfoPropertyName = "APIGatewayProxyRequestProxyRequestClientCert")] -[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity), TypeInfoPropertyName = "APIGatewayProxyRequestClientCertValidity")] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext), + TypeInfoPropertyName = "APIGatewayProxyRequestContext")] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert), + TypeInfoPropertyName = "APIGatewayProxyRequestProxyRequestClientCert")] +[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity), + TypeInfoPropertyName = "APIGatewayProxyRequestClientCertValidity")] [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] [JsonSerializable(typeof(LogEntry))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestContext")] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestProxyRequestClientCert")] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestClientCertValidity")] -internal partial class LoggingSerializationContext : JsonSerializerContext +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext), + TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestContext")] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert), + TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestProxyRequestClientCert")] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity), + TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestClientCertValidity")] +public partial class PowertoolsLoggingSerializationContext : JsonSerializerContext { - } + #endif \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs new file mode 100644 index 00000000..1ff6588f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.Lambda.Core; + +namespace AWS.Lambda.Powertools.Logging.Serializers; + +#if NET8_0_OR_GREATER +/// +/// Provides a custom Lambda serializer that combines multiple JsonSerializerContexts. +/// +public class PowertoolsLambdaSerializer : ILambdaSerializer +{ + /// + /// Initializes a new instance of PowertoolsLambdaSerializer. + /// + /// The customer's JsonSerializerContext. + public PowertoolsLambdaSerializer(JsonSerializerContext customerContext) + { + PowertoolsLoggingSerializer.AddSerializerContext(customerContext); + } + + /// + /// Deserializes the input stream to the specified type. + /// + public T Deserialize(Stream requestStream) + { + if (!requestStream.CanSeek) + { + using var ms = new MemoryStream(); + requestStream.CopyTo(ms); + ms.Position = 0; + requestStream = ms; + } + + var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T)); + if (typeInfo == null) + { + throw new InvalidOperationException( + $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); + } + + return (T)JsonSerializer.Deserialize(requestStream, typeInfo)!; + } + + /// + /// Serializes the specified object and writes the result to the output stream. + /// + public void Serialize(T response, Stream responseStream) + { + var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T)); + if (typeInfo == null) + { + throw new InvalidOperationException( + $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); + } + + using var writer = new Utf8JsonWriter(responseStream, new JsonWriterOptions { SkipValidation = true }); + JsonSerializer.Serialize(writer, response, typeInfo); + } +} + +#endif \ 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 new file mode 100644 index 00000000..1344b8fa --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -0,0 +1,182 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Converters; + +namespace AWS.Lambda.Powertools.Logging.Serializers; + +/// +/// Provides serialization functionality for Powertools logging. +/// +internal class PowertoolsLoggingSerializer +{ + private static JsonSerializerOptions _serializerOptions; + private static readonly List _additionalContexts = new List(); + private static LoggerOutputCase _currentOutputCase = LoggerOutputCase.SnakeCase; + + /// + /// Gets the JsonSerializerOptions instance. + /// + internal static JsonSerializerOptions SerializerOptions + { + get + { + if (_serializerOptions == null) + { + if (_serializerOptions == null) + { + _serializerOptions = BuildJsonSerializerOptions(); + } + } + + return _serializerOptions; + } + } + + /// + /// Configures the naming policy for the serializer. + /// + /// The case to use for serialization. + public static void ConfigureNamingPolicy(LoggerOutputCase? loggerOutputCase) + { + if (loggerOutputCase == null || loggerOutputCase == _currentOutputCase) + { + return; + } + + _currentOutputCase = loggerOutputCase.Value; + var newOptions = BuildJsonSerializerOptions(); + +#if NET8_0_OR_GREATER + foreach (var context in _additionalContexts) + { + newOptions.TypeInfoResolverChain.Add(context); + } +#endif + + _serializerOptions = newOptions; + } + +#if NET8_0_OR_GREATER + + /// + /// Serializes an object to a JSON string. + /// + /// The object to serialize. + /// The type of the object to serialize. + /// A JSON string representation of the object. + /// Thrown when the input type is not known to the serializer. + public static string Serialize(object value, Type inputType) + { + var typeInfo = GetTypeInfo(inputType); + + if (typeInfo == null) + { + throw new InvalidOperationException( + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); + } + + return JsonSerializer.Serialize(value, typeInfo); + } + + /// + /// Adds a JsonSerializerContext to the serializer options. + /// + /// The JsonSerializerContext to add. + /// Thrown when the context is null. + internal static void AddSerializerContext(JsonSerializerContext context) + { + ArgumentNullException.ThrowIfNull(context); + + if (!_additionalContexts.Contains(context)) + { + _additionalContexts.Add(context); + _serializerOptions?.TypeInfoResolverChain.Add(context); + } + } + + /// + /// Gets the JsonTypeInfo for a given type. + /// + /// The type to get information for. + /// The JsonTypeInfo for the specified type, or null if not found. + internal static JsonTypeInfo GetTypeInfo(Type type) + { + return SerializerOptions.TypeInfoResolver?.GetTypeInfo(type, SerializerOptions); + } +#endif + +#if NET6_0 + /// + /// Serializes an object to a JSON string. + /// + /// The object to serialize. + /// A JSON string representation of the object. + public static string Serialize(object value) + { + return JsonSerializer.Serialize(value, SerializerOptions); + } +#endif + + /// + /// Builds and configures the JsonSerializerOptions. + /// + /// A configured JsonSerializerOptions instance. + private static JsonSerializerOptions BuildJsonSerializerOptions() + { + var jsonOptions = new JsonSerializerOptions(); + + switch (_currentOutputCase) + { + case LoggerOutputCase.CamelCase: + jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + break; + case LoggerOutputCase.PascalCase: + jsonOptions.PropertyNamingPolicy = PascalCaseNamingPolicy.Instance; + jsonOptions.DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance; + break; + default: // Snake case + jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; + jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; + break; + } + + jsonOptions.Converters.Add(new ByteArrayConverter()); + jsonOptions.Converters.Add(new ExceptionConverter()); + jsonOptions.Converters.Add(new MemoryStreamConverter()); + jsonOptions.Converters.Add(new ConstantClassConverter()); + jsonOptions.Converters.Add(new DateOnlyConverter()); + jsonOptions.Converters.Add(new TimeOnlyConverter()); + + jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + +#if NET8_0_OR_GREATER + jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); + foreach (var context in _additionalContexts) + { + jsonOptions.TypeInfoResolverChain.Add(context); + } +#endif + return jsonOptions; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs index 620ebe5e..aadb3a2f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -22,6 +22,7 @@ using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; @@ -53,6 +54,13 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() var configurations = Substitute.For(); configurations.Service.Returns(service); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = minimumLevel, + LoggerOutputCase = LoggerOutputCase.PascalCase + }; + var globalExtraKeys = new Dictionary { { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, @@ -68,11 +76,11 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() AwsRequestId = Guid.NewGuid().ToString(), MemoryLimitInMB = (new Random()).Next() }; - + var args = Substitute.For(); var method = Substitute.For(); var parameter = Substitute.For(); - + // Setup parameter parameter.ParameterType.Returns(typeof(ILambdaContext)); @@ -81,12 +89,12 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() // Setup args args.Method = method; - args.Args = new object[] { lambdaContext }; - + args.Args = new object[] { lambdaContext }; + // Act - + LoggingLambdaContext.Extract(args); - + var logFormatter = Substitute.For(); var formattedLogEntry = new { @@ -116,13 +124,9 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = service, - MinimumLevel = minimumLevel, - LoggerOutputCase = LoggerOutputCase.PascalCase - }); + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var scopeExtraKeys = new Dictionary { @@ -146,16 +150,16 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() x.Message.ToString() == message && x.Exception == null && x.ExtraKeys != null && ( - x.ExtraKeys.Count != globalExtraKeys.Count + scopeExtraKeys.Count || ( - x.ExtraKeys.Count == globalExtraKeys.Count + scopeExtraKeys.Count && - x.ExtraKeys.ContainsKey(globalExtraKeys.First().Key) && - x.ExtraKeys[globalExtraKeys.First().Key] == globalExtraKeys.First().Value && - x.ExtraKeys.ContainsKey(globalExtraKeys.Last().Key) && - x.ExtraKeys[globalExtraKeys.Last().Key] == globalExtraKeys.Last().Value && - x.ExtraKeys.ContainsKey(scopeExtraKeys.First().Key) && - x.ExtraKeys[scopeExtraKeys.First().Key] == scopeExtraKeys.First().Value && - x.ExtraKeys.ContainsKey(scopeExtraKeys.Last().Key) && - x.ExtraKeys[scopeExtraKeys.Last().Key] == scopeExtraKeys.Last().Value ) ) && + x.ExtraKeys.Count != globalExtraKeys.Count + scopeExtraKeys.Count || ( + x.ExtraKeys.Count == globalExtraKeys.Count + scopeExtraKeys.Count && + x.ExtraKeys.ContainsKey(globalExtraKeys.First().Key) && + x.ExtraKeys[globalExtraKeys.First().Key] == globalExtraKeys.First().Value && + x.ExtraKeys.ContainsKey(globalExtraKeys.Last().Key) && + x.ExtraKeys[globalExtraKeys.Last().Key] == globalExtraKeys.Last().Value && + x.ExtraKeys.ContainsKey(scopeExtraKeys.First().Key) && + x.ExtraKeys[scopeExtraKeys.First().Key] == scopeExtraKeys.First().Value && + x.ExtraKeys.ContainsKey(scopeExtraKeys.Last().Key) && + x.ExtraKeys[scopeExtraKeys.Last().Key] == scopeExtraKeys.Last().Value)) && x.LambdaContext != null && x.LambdaContext.FunctionName == lambdaContext.FunctionName && x.LambdaContext.FunctionVersion == lambdaContext.FunctionVersion && @@ -163,8 +167,9 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() x.LambdaContext.InvokedFunctionArn == lambdaContext.InvokedFunctionArn && x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId )); - systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); + systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); + //Clean up Logger.UseDefaultFormatter(); Logger.RemoveAllKeys(); @@ -172,7 +177,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() LoggingAspectHandler.ResetForTest(); } } - + [Collection("Sequential")] public class LogFormatterNullTest { @@ -192,7 +197,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, @@ -212,7 +217,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() Logger.UseDefaultFormatter(); } } - + [Collection("Sequential")] public class LogFormatterExceptionTest { @@ -233,7 +238,7 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs index cb0cad09..c13c9759 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs @@ -49,9 +49,13 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() Args = Array.Empty() }; + var config = new LoggerConfiguration + { + Service = service + }; + LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, null, true, configurations, - systemWrapper); + var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, systemWrapper); // Act handler.OnEntry(eventArgs); @@ -93,9 +97,14 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu Name = methodName, Args = Array.Empty() }; + + var config = new LoggerConfiguration + { + Service = service + }; LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, null, true, configurations, + var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, systemWrapper); // Act @@ -139,9 +148,14 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() Name = methodName, Args = Array.Empty() }; + + var config = new LoggerConfiguration + { + Service = service + }; LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, null, true, configurations, + var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, systemWrapper); // Act @@ -172,9 +186,14 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() Name = methodName, Args = Array.Empty() }; + + var config = new LoggerConfiguration + { + Service = service + }; LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, null, true, configurations, + var handler = new LoggingAspectHandler(config, logLevel, true, null, true, configurations, systemWrapper); // Act @@ -205,9 +224,14 @@ public void OnExit_WhenHandler_ClearKeys() Name = methodName, Args = Array.Empty() }; + + var config = new LoggerConfiguration + { + Service = service + }; LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, null, true, configurations, + var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, systemWrapper); // Act @@ -246,9 +270,14 @@ protected void OnEntry_WhenEventArgExists_CapturesCorrelationIdBase(string corre Args = new[] { eventArg } }; + var config = new LoggerConfiguration + { + Service = service + }; + LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, false, correlationIdPath, - true, configurations, systemWrapper); + var handler = new LoggingAspectHandler(config, logLevel, null, correlationIdPath, false, + configurations, systemWrapper); // Act handler.OnEntry(eventArgs); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index e1229701..95a6bb96 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -50,7 +50,7 @@ private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, @@ -102,7 +102,7 @@ private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logL // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, @@ -274,12 +274,16 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + configurations.SetCurrentConfig(loggerConfiguration, systemWrapper); + + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, + loggerConfiguration); logger.LogInformation("Test"); @@ -309,13 +313,20 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); + + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + //var (_, output) = configurations.SetCurrentConfig(loggerConfiguration); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var logger = provider.CreateLogger("test"); + // var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, + // loggerConfiguration); logger.LogInformation("Test"); @@ -344,12 +355,14 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); @@ -379,12 +392,14 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -418,13 +433,15 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null, - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -459,12 +476,14 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -498,13 +517,15 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null, - LoggerOutputCase = LoggerOutputCase.PascalCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.PascalCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -537,7 +558,7 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = null, @@ -574,13 +595,15 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null, - LoggerOutputCase = LoggerOutputCase.SnakeCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -612,12 +635,14 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -645,7 +670,7 @@ public void BeginScope_WhenScopeIsObject_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, @@ -686,7 +711,7 @@ public void BeginScope_WhenScopeIsObjectDictionary_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, @@ -727,7 +752,7 @@ public void BeginScope_WhenScopeIsStringDictionary_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = service, @@ -781,12 +806,13 @@ public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLeve configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = service, - MinimumLevel = LogLevel.Trace, - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Trace, + }; + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary { @@ -863,12 +889,14 @@ public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLeve configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = service, - MinimumLevel = LogLevel.Trace, - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Trace, + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary { @@ -945,12 +973,14 @@ public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool lo configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = service, - MinimumLevel = LogLevel.Trace, - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Trace, + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new { @@ -1018,7 +1048,7 @@ public void Log_WhenException_LogsExceptionDetails() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = null, @@ -1061,7 +1091,7 @@ public void Log_WhenNestedException_LogsExceptionDetails() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = null, @@ -1102,12 +1132,13 @@ public void Log_WhenByteArray_LogsByteArrayNumbers() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Bytes = bytes }); @@ -1140,7 +1171,7 @@ public void Log_WhenMemoryStream_LogsBase64String() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = null, @@ -1180,7 +1211,7 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, new LoggerConfiguration { Service = null, @@ -1212,7 +1243,7 @@ public void Log_Set_Execution_Environment_Context() var wrapper = new SystemWrapper(env); var conf = new PowertoolsConfigurations(wrapper); - var logger = new PowertoolsLogger(loggerName, conf, wrapper, () => + var logger = new PowertoolsLogger(loggerName, conf, wrapper, new LoggerConfiguration { Service = null, @@ -1242,13 +1273,15 @@ public void Log_Should_Serialize_DateOnly() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null, - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1287,13 +1320,15 @@ public void Log_Should_Serialize_TimeOnly() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null, - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1329,13 +1364,15 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, () => - new LoggerConfiguration - { - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var loggerConfiguration = new LoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1349,13 +1386,13 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo // Assert Assert.True(logger.IsEnabled(logLevel)); - Assert.Equal(logLevel, configuration.GetLogLevel()); + Assert.Equal(logLevel, configurations.GetLogLevel()); Assert.Equal(willLog, systemWrapper.LogMethodCalled); } [Theory] [InlineData(true, "WARN", LogLevel.Warning)] - [InlineData(false, "Fatal", LogLevel.Critical)] + [InlineData(true, "Fatal", LogLevel.Critical)] public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, string awsLogLevel, LogLevel logLevel) { // Arrange @@ -1367,13 +1404,15 @@ public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, strin environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, () => - new LoggerConfiguration - { - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var loggerConfiguration = new LoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.CamelCase, + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1387,8 +1426,8 @@ public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, strin // Assert Assert.True(logger.IsEnabled(logLevel)); - Assert.Equal(LogLevel.Information, configuration.GetLogLevel()); //default - Assert.Equal(logLevel, configuration.GetLambdaLogLevel()); + Assert.Equal(LogLevel.Information, configurations.GetLogLevel()); //default + Assert.Equal(logLevel, configurations.GetLambdaLogLevel()); Assert.Equal(willLog, systemWrapper.LogMethodCalled); } @@ -1403,22 +1442,24 @@ public void Log_Should_Show_Warning_When_AWS_Lambda_Log_Level_Enabled() environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Warn"); var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(systemWrapper); + + var loggerConfiguration = new LoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.CamelCase + }; - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, () => - new LoggerConfiguration - { - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); - var logLevel = configuration.GetLogLevel(); - var lambdaLogLevel = configuration.GetLambdaLogLevel(); + var logLevel = configurations.GetLogLevel(); + var lambdaLogLevel = configurations.GetLambdaLogLevel(); // Assert Assert.True(logger.IsEnabled(LogLevel.Warning)); Assert.Equal(LogLevel.Debug, logLevel); Assert.Equal(LogLevel.Warning, lambdaLogLevel); - Assert.True(systemWrapper.LogMethodCalled); + Assert.Contains($"Current log level ({logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them.", systemWrapper.LogMethodCalledWithArgument); } @@ -1437,12 +1478,13 @@ public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Le environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Info"); var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, () => - new LoggerConfiguration - { - LoggerOutputCase = LoggerOutputCase.PascalCase - }); + var configurations = new PowertoolsConfigurations(systemWrapper); + var loggerConfiguration = new LoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.PascalCase + }; + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1470,7 +1512,7 @@ public void Log_CamelCase_Outputs_Level_When_AWS_Lambda_Log_Level_Enabled(Logger var systemWrapper = new SystemWrapperMock(environment); var configuration = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, () => + var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, new LoggerConfiguration { LoggerOutputCase = casing @@ -1523,13 +1565,15 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(logLevel.ToString()); var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, () => - new LoggerConfiguration - { - LoggerOutputCase = LoggerOutputCase.CamelCase - }); + var loggerConfiguration = new LoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1543,7 +1587,7 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel // Assert Assert.True(logger.IsEnabled(logLevel)); - Assert.Equal(logLevel.ToString(), configuration.LogLevel); + Assert.Equal(logLevel.ToString(), configurations.LogLevel); Assert.Equal(willLog, systemWrapper.LogMethodCalled); } } From c7e95b48e53cc195c6a769f0a538314f0f26a5ec Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:12:18 +0100 Subject: [PATCH 08/32] refactor configuration and sampling rate code --- .../PowertoolsConfigurationsExtension.cs | 75 ++++++++++++------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index d31caba9..6234bca8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -112,50 +112,75 @@ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfiguration $"Current log level ({logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); } - var samplingRate = config?.SamplingRate ?? powertoolsConfigurations.LoggerSampleRate; - var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config?.LoggerOutputCase); + // set service var service = config?.Service ?? powertoolsConfigurations.Service; + config.Service = service; + + // set output case + var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config?.LoggerOutputCase); + config.LoggerOutputCase = loggerOutputCase; + PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); - + // log level var minLogLevel = logLevel; if (lambdaLogLevelEnabled) { minLogLevel = lambdaLogLevel; } - config.Service = service; config.MinimumLevel = minLogLevel; - config.SamplingRate = samplingRate; - config.LoggerOutputCase = loggerOutputCase; - - PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); + + // set sampling rate + config = SetSamplingRate(powertoolsConfigurations, config, systemWrapper, minLogLevel); + return config; + } - if (!samplingRate.HasValue) - return config; + /// + /// Set sampling rate + /// + /// + /// + /// + /// + /// + private static LoggerConfiguration SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, + LoggerConfiguration config, ISystemWrapper systemWrapper, LogLevel minLogLevel) + { + var samplingRate = config.SamplingRate ?? powertoolsConfigurations.LoggerSampleRate; + config.SamplingRate = samplingRate; - if (samplingRate.Value < 0 || samplingRate.Value > 1) + switch (samplingRate) { - if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - systemWrapper.LogLine( - $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}"); - config.SamplingRate = null; - return config; + case null: + return config; + case < 0 or > 1: + { + if (minLogLevel is LogLevel.Debug or LogLevel.Trace) + systemWrapper.LogLine( + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}"); + config.SamplingRate = null; + return config; + } + case 0: + return config; } - if (samplingRate.Value == 0) - return config; - var sample = systemWrapper.GetRandom(); - if (samplingRate.Value > sample) - { - systemWrapper.LogLine( - $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}."); - config.MinimumLevel = LogLevel.Debug; - } + + if (!(samplingRate.Value > sample)) return config; + + systemWrapper.LogLine( + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}."); + config.MinimumLevel = LogLevel.Debug; return config; } + /// + /// Determines whether [is lambda log level enabled]. + /// + /// + /// internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powertoolsConfigurations) { return powertoolsConfigurations.GetLambdaLogLevel() != LogLevel.None; From 27e7433857aab054613d9ed3dd100602ed1ee2f4 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:26:29 +0100 Subject: [PATCH 09/32] address sonar issues --- .../PowertoolsConfigurationsExtension.cs | 8 +-- .../Internal/PowertoolsLogger.cs | 2 +- .../PowertoolsLoggingSerializer.cs | 49 +++++++------------ 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 6234bca8..bc14d0b1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -102,7 +102,7 @@ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfiguration { config ??= new LoggerConfiguration(); - var logLevel = powertoolsConfigurations.GetLogLevel(config?.MinimumLevel); + var logLevel = powertoolsConfigurations.GetLogLevel(config.MinimumLevel); var lambdaLogLevel = powertoolsConfigurations.GetLambdaLogLevel(); var lambdaLogLevelEnabled = powertoolsConfigurations.LambdaLogLevelEnabled(); @@ -113,11 +113,11 @@ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfiguration } // set service - var service = config?.Service ?? powertoolsConfigurations.Service; + var service = config.Service ?? powertoolsConfigurations.Service; config.Service = service; // set output case - var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config?.LoggerOutputCase); + var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); config.LoggerOutputCase = loggerOutputCase; PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); @@ -167,7 +167,7 @@ private static LoggerConfiguration SetSamplingRate(IPowertoolsConfigurations pow var sample = systemWrapper.GetRandom(); - if (!(samplingRate.Value > sample)) return config; + if ((samplingRate.Value <= sample)) return config; systemWrapper.LogLine( $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}."); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 0c37a6e2..82052a60 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -38,7 +38,7 @@ internal sealed class PowertoolsLogger : ILogger /// /// The current configuration /// - private static LoggerConfiguration _currentConfig; + private LoggerConfiguration _currentConfig; /// /// The Powertools for AWS Lambda (.NET) configurations diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 1344b8fa..cab1a9d6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -27,10 +27,10 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; /// /// Provides serialization functionality for Powertools logging. /// -internal class PowertoolsLoggingSerializer +internal static class PowertoolsLoggingSerializer { private static JsonSerializerOptions _serializerOptions; - private static readonly List _additionalContexts = new List(); + private static readonly List AdditionalContexts = new List(); private static LoggerOutputCase _currentOutputCase = LoggerOutputCase.SnakeCase; /// @@ -38,18 +38,7 @@ internal class PowertoolsLoggingSerializer /// internal static JsonSerializerOptions SerializerOptions { - get - { - if (_serializerOptions == null) - { - if (_serializerOptions == null) - { - _serializerOptions = BuildJsonSerializerOptions(); - } - } - - return _serializerOptions; - } + get { return _serializerOptions ??= BuildJsonSerializerOptions(); } } /// @@ -67,7 +56,7 @@ public static void ConfigureNamingPolicy(LoggerOutputCase? loggerOutputCase) var newOptions = BuildJsonSerializerOptions(); #if NET8_0_OR_GREATER - foreach (var context in _additionalContexts) + foreach (var context in AdditionalContexts) { newOptions.TypeInfoResolverChain.Add(context); } @@ -97,6 +86,18 @@ public static string Serialize(object value, Type inputType) return JsonSerializer.Serialize(value, typeInfo); } + +#if NET6_0 + /// + /// Serializes an object to a JSON string. + /// + /// The object to serialize. + /// A JSON string representation of the object. + public static string Serialize(object value) + { + return JsonSerializer.Serialize(value, SerializerOptions); + } +#endif /// /// Adds a JsonSerializerContext to the serializer options. @@ -107,9 +108,9 @@ internal static void AddSerializerContext(JsonSerializerContext context) { ArgumentNullException.ThrowIfNull(context); - if (!_additionalContexts.Contains(context)) + if (!AdditionalContexts.Contains(context)) { - _additionalContexts.Add(context); + AdditionalContexts.Add(context); _serializerOptions?.TypeInfoResolverChain.Add(context); } } @@ -125,18 +126,6 @@ internal static JsonTypeInfo GetTypeInfo(Type type) } #endif -#if NET6_0 - /// - /// Serializes an object to a JSON string. - /// - /// The object to serialize. - /// A JSON string representation of the object. - public static string Serialize(object value) - { - return JsonSerializer.Serialize(value, SerializerOptions); - } -#endif - /// /// Builds and configures the JsonSerializerOptions. /// @@ -172,7 +161,7 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() #if NET8_0_OR_GREATER jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); - foreach (var context in _additionalContexts) + foreach (var context in AdditionalContexts) { jsonOptions.TypeInfoResolverChain.Add(context); } From 1a97e37f0008c98fb7391364ecc5a4b9c4f9021f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:31:20 +0100 Subject: [PATCH 10/32] add Serialize method to the correct place --- .../PowertoolsLoggingSerializer.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index cab1a9d6..7a107390 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -65,6 +65,18 @@ public static void ConfigureNamingPolicy(LoggerOutputCase? loggerOutputCase) _serializerOptions = newOptions; } +#if NET6_0 + /// + /// Serializes an object to a JSON string. + /// + /// The object to serialize. + /// A JSON string representation of the object. + public static string Serialize(object value) + { + return JsonSerializer.Serialize(value, SerializerOptions); + } +#endif + #if NET8_0_OR_GREATER /// @@ -86,18 +98,6 @@ public static string Serialize(object value, Type inputType) return JsonSerializer.Serialize(value, typeInfo); } - -#if NET6_0 - /// - /// Serializes an object to a JSON string. - /// - /// The object to serialize. - /// A JSON string representation of the object. - public static string Serialize(object value) - { - return JsonSerializer.Serialize(value, SerializerOptions); - } -#endif /// /// Adds a JsonSerializerContext to the serializer options. From 6c23b035b7c55163ebc1768380c8cb52c8948712 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:33:09 +0100 Subject: [PATCH 11/32] add tests for serializer --- .../PowertoolsConfigurationsExtension.cs | 2 +- .../LoggingAttribute.cs | 10 +- .../PowertoolsLoggingSerializer.cs | 32 ++-- .../PowertoolsLambdaSerializerTests.cs | 142 ++++++++++++++++++ .../PowertoolsLoggingSerializerTests.cs | 130 ++++++++++++++++ 5 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index bc14d0b1..787be09e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -119,7 +119,7 @@ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfiguration // set output case var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); config.LoggerOutputCase = loggerOutputCase; - PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); + PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase ?? LoggingConstants.DefaultLoggerOutputCase); // log level var minLogLevel = logLevel; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 43537b59..67aa49aa 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -134,6 +134,11 @@ public class LoggingAttribute : MethodAspectAttribute /// private double? _samplingRate; + /// + /// The logger output case + /// + private LoggerOutputCase? _loggerOutputCase; + /// /// Service name is used for logging. /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. @@ -197,7 +202,10 @@ public bool LogEvent /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. /// /// The log level. - public LoggerOutputCase? LoggerOutputCase { get; set; } + public LoggerOutputCase LoggerOutputCase { + get => _loggerOutputCase ?? LoggingConstants.DefaultLoggerOutputCase; + set => _loggerOutputCase = value; + } /// /// Creates the handler. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 7a107390..2e290fa5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -21,6 +21,7 @@ using System.Text.Json.Serialization.Metadata; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Converters; +using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Serializers; @@ -45,14 +46,9 @@ internal static JsonSerializerOptions SerializerOptions /// Configures the naming policy for the serializer. /// /// The case to use for serialization. - public static void ConfigureNamingPolicy(LoggerOutputCase? loggerOutputCase) + public static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) { - if (loggerOutputCase == null || loggerOutputCase == _currentOutputCase) - { - return; - } - - _currentOutputCase = loggerOutputCase.Value; + _currentOutputCase = loggerOutputCase; var newOptions = BuildJsonSerializerOptions(); #if NET8_0_OR_GREATER @@ -76,9 +72,9 @@ public static string Serialize(object value) return JsonSerializer.Serialize(value, SerializerOptions); } #endif - + #if NET8_0_OR_GREATER - + /// /// Serializes an object to a JSON string. /// @@ -111,7 +107,6 @@ internal static void AddSerializerContext(JsonSerializerContext context) if (!AdditionalContexts.Contains(context)) { AdditionalContexts.Add(context); - _serializerOptions?.TypeInfoResolverChain.Add(context); } } @@ -157,6 +152,12 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() jsonOptions.Converters.Add(new DateOnlyConverter()); jsonOptions.Converters.Add(new TimeOnlyConverter()); +#if NET8_0_OR_GREATER + jsonOptions.Converters.Add(new JsonStringEnumConverter()); +#elif NET6_0 + jsonOptions.Converters.Add(new JsonStringEnumConverter()); +#endif + jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; #if NET8_0_OR_GREATER @@ -168,4 +169,15 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() #endif return jsonOptions; } + +#if NET8_0_OR_GREATER + internal static bool HasContext(JsonSerializerContext customContext) + { + return AdditionalContexts.Contains(customContext); + } + internal static void ClearContext() + { + AdditionalContexts.Clear(); + } +#endif } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs new file mode 100644 index 00000000..1c88f261 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -0,0 +1,142 @@ + +#if NET8_0_OR_GREATER + +using AWS.Lambda.Powertools.Logging.Serializers; +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; + +[JsonSerializable(typeof(TestObject))] +public partial class TestJsonContext : JsonSerializerContext +{ +} + +public class TestObject +{ + public string FullName { get; set; } + public int Age { get; set; } +} + +public class PowertoolsLambdaSerializerTests : IDisposable +{ + [Fact] + public void Constructor_ShouldNotThrowException() + { + // Arrange & Act & Assert + var exception = Record.Exception(() => new PowertoolsLambdaSerializer(TestJsonContext.Default)); + Assert.Null(exception); + } + + [Fact] + public void Constructor_ShouldAddCustomerContext() + { + // Arrange + var customerContext = new TestJsonContext(); + + // Act + var serializer = new PowertoolsLambdaSerializer(customerContext); + + // Assert + Assert.True(PowertoolsLoggingSerializer.HasContext(customerContext)); + } + + // [Fact] + // public void Deserialize_ValidJson_ShouldReturnDeserializedObject() + // { + // // Arrange + // var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + // var json = "{\"Name\":\"John\",\"Age\":30}"; + // var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + // + // // Act + // var result = serializer.Deserialize(stream); + // + // // Assert + // Assert.NotNull(result); + // Assert.Equal("John", result.Name); + // Assert.Equal(30, result.Age); + // } + + [Theory] + [InlineData(LoggerOutputCase.CamelCase,"{\"fullName\":\"John\",\"age\":30}", "John", 30)] + [InlineData(LoggerOutputCase.PascalCase,"{\"FullName\":\"Jane\",\"Age\":25}", "Jane", 25)] + [InlineData(LoggerOutputCase.SnakeCase,"{\"full_name\":\"Jane\",\"age\":25}", "Jane", 25)] + public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCase outputCase,string json, string expectedName, int expectedAge) + { + // Arrange + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + // Act + var result = serializer.Deserialize(stream); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedName, result.FullName); + Assert.Equal(expectedAge, result.Age); + } + + [Fact] + public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() + { + // Arrange + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); + + var json = "{\"FullName\":\"John\",\"Age\":30}"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + // Act & Assert + Assert.Throws(() => serializer.Deserialize(stream)); + } + + [Fact] + public void Serialize_ValidObject_ShouldSerializeToStream() + { + // Arrange + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); + + var testObject = new TestObject { FullName = "Jane", Age = 25 }; + var stream = new MemoryStream(); + + // Act + serializer.Serialize(testObject, stream); + + // Assert + stream.Position = 0; + var result = new StreamReader(stream).ReadToEnd(); + Assert.Contains("\"FullName\":\"Jane\"", result); + Assert.Contains("\"Age\":25", result); + } + + [Fact] + public void Serialize_InvalidType_ShouldThrowInvalidOperationException() + { + // Arrange + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var unknownObject = new UnknownType(); + var stream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => serializer.Serialize(unknownObject, stream)); + } + + private class UnknownType { } + + public void Dispose() + { + PowertoolsLoggingSerializer.ClearContext(); + } +} +#endif + diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs new file mode 100644 index 00000000..ffe264a6 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging.Internal.Converters; +using AWS.Lambda.Powertools.Logging.Serializers; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; + +public class PowertoolsLoggingSerializerTests : IDisposable +{ + [Fact] + public void SerializerOptions_ShouldNotBeNull() + { + var options = PowertoolsLoggingSerializer.SerializerOptions; + Assert.NotNull(options); + } + + [Fact] + public void SerializerOptions_ShouldHaveCorrectDefaultSettings() + { + var options = PowertoolsLoggingSerializer.SerializerOptions; + + Assert.Collection(options.Converters, + converter => Assert.IsType(converter), + converter => Assert.IsType(converter), + converter => Assert.IsType(converter), + converter => Assert.IsType(converter), + converter => Assert.IsType(converter), + converter => Assert.IsType(converter), +#if NET8_0_OR_GREATER + converter => Assert.IsType>(converter)); +#elif NET6_0 + converter => Assert.IsType(converter)); +#endif + + Assert.Equal(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder); + +#if NET8_0_OR_GREATER + Assert.Collection(options.TypeInfoResolverChain, + resolver => Assert.IsType(resolver)); +#endif + } + + [Fact] + public void SerializerOptions_ShouldUseSnakeCaseByDefault() + { + var json = SerializeTestObject(null); + Assert.Contains("\"cold_start\"", json); + } + + [Theory] + [InlineData(LoggerOutputCase.SnakeCase, "cold_start")] + [InlineData(LoggerOutputCase.CamelCase, "coldStart")] + [InlineData(LoggerOutputCase.PascalCase, "ColdStart")] + public void ConfigureNamingPolicy_ShouldUseCorrectNamingConvention(LoggerOutputCase outputCase, + string expectedPropertyName) + { + var json = SerializeTestObject(outputCase); + Assert.Contains($"\"{expectedPropertyName}\"", json); + } + + [Fact] + public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedNull() + { + var originalJson = SerializeTestObject(LoggerOutputCase.SnakeCase); + var newJson = SerializeTestObject(null); + Assert.Equal(originalJson, newJson); + } + + [Fact] + public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedSameCase() + { + var originalJson = SerializeTestObject(LoggerOutputCase.SnakeCase); + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + var newJson = SerializeTestObject(LoggerOutputCase.SnakeCase); + Assert.Equal(originalJson, newJson); + } + + [Fact] + public void Serialize_ShouldHandleNestedObjects() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + + var testObject = new LogEntry + { + ColdStart = true, + ExtraKeys = new Dictionary + { + { "NestedObject", new Dictionary { { "PropertyName", "Value" } } } + } + }; + + var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.SerializerOptions); + Assert.Contains("\"cold_start\":true", json); + Assert.Contains("\"nested_object\":{\"property_name\":\"Value\"}", json); + } + + [Fact] + public void Serialize_ShouldHandleEnumValues() + { + var testObject = new LogEntry + { + Level = LogLevel.Error + }; + var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.SerializerOptions); + Assert.Contains("\"level\":\"Error\"", json); + } + + private string SerializeTestObject(LoggerOutputCase? outputCase) + { + if (outputCase.HasValue) + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase.Value); + } + + LogEntry testObject = new LogEntry { ColdStart = true }; + return JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.SerializerOptions); + } + + public void Dispose() + { +#if NET8_0_OR_GREATER + PowertoolsLoggingSerializer.ClearContext(); +#endif + } +} \ No newline at end of file From f65d3cba23762a26e7a70ad16daff5a1c6458c64 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 11 Sep 2024 23:01:26 +0100 Subject: [PATCH 12/32] refactoring and serializer tests --- .../Helpers/PowertoolsLoggerHelpers.cs | 49 ------ .../Internal/LoggingAspectHandler.cs | 2 +- .../PowertoolsConfigurationsExtension.cs | 127 +++++++++++++++ .../Serializers/PowertoolsLambdaSerializer.cs | 8 +- .../PowertoolsLambdaSerializerTests.cs | 82 +++++++--- .../PowertoolsLoggingSerializerTests.cs | 38 +++++ .../Utilities/PowertoolsLoggerHelpersTests.cs | 152 ++++++++++++++++++ 7 files changed, 387 insertions(+), 71 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs 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 428deb68..c0d63d89 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -14,7 +14,6 @@ */ using System.Linq; -using AWS.Lambda.Powertools.Common; namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; @@ -41,52 +40,4 @@ internal static object ObjectToDictionary(object anonymousObject) return anonymousObject.GetType().GetProperties() .ToDictionary(prop => prop.Name, prop => ObjectToDictionary(prop.GetValue(anonymousObject, null))); } - - /// - /// Converts the input string to the configured output case. - /// - /// The string to convert. - /// - /// The input string converted to the configured case (camel, pascal, or snake case). - /// - internal static string ConvertToOutputCase(string correlationIdPath) - { - return PowertoolsConfigurations.Instance.GetLoggerOutputCase() switch - { - LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), - LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), - _ => ToSnakeCase(correlationIdPath), // default snake_case - }; - } - - /// - /// Converts a string to snake_case. - /// - /// The string to convert. - /// The input string converted to snake_case. - private static string ToSnakeCase(string correlationIdPath) - { - return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())) - .ToLowerInvariant(); - } - - /// - /// Converts a string to PascalCase. - /// - /// The string to convert. - /// The input string converted to PascalCase. - private static string ToPascalCase(string correlationIdPath) - { - return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); - } - - /// - /// Converts a string to camelCase. - /// - /// The string to convert. - /// The input string converted to camelCase. - private static string ToCamelCase(string correlationIdPath) - { - return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1); - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs index f3564bda..206f9331 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs @@ -276,7 +276,7 @@ private void CaptureCorrelationId(object eventArg) for (var i = 0; i < correlationIdPaths.Length; i++) { - var pathWithOutputCase = PowertoolsLoggerHelpers.ConvertToOutputCase(correlationIdPaths[i]); + var pathWithOutputCase = _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i]); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 787be09e..c5caa423 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -15,6 +15,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; @@ -185,4 +187,129 @@ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powert { return powertoolsConfigurations.GetLambdaLogLevel() != LogLevel.None; } + + /// + /// Converts the input string to the configured output case. + /// + /// + /// The string to convert. + /// + /// + /// The input string converted to the configured case (camel, pascal, or snake case). + /// + internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, string correlationIdPath, LoggerOutputCase? loggerOutputCase = null) + { + return powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch + { + LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), + LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), + _ => ToSnakeCase(correlationIdPath), // default snake_case + }; + } + + /// + /// Converts a string to snake_case. + /// + /// + /// The input string converted to snake_case. + private static string ToSnakeCase(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var result = new StringBuilder(input.Length + 10); + bool lastCharWasUnderscore = false; + bool lastCharWasUpper = false; + + for (int i = 0; i < input.Length; i++) + { + char currentChar = input[i]; + + if (currentChar == '_') + { + result.Append('_'); + lastCharWasUnderscore = true; + lastCharWasUpper = false; + } + else if (char.IsUpper(currentChar)) + { + if (i > 0 && !lastCharWasUnderscore && + (!lastCharWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) + { + result.Append('_'); + } + result.Append(char.ToLowerInvariant(currentChar)); + lastCharWasUnderscore = false; + lastCharWasUpper = true; + } + else + { + result.Append(char.ToLowerInvariant(currentChar)); + lastCharWasUnderscore = false; + lastCharWasUpper = false; + } + } + + return result.ToString(); + } + + + + + /// + /// Converts a string to PascalCase. + /// + /// + /// The input string converted to PascalCase. + private static string ToPascalCase(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var words = input.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); + var result = new StringBuilder(); + + foreach (var word in words) + { + if (word.Length > 0) + { + // Capitalize the first character of each word + result.Append(char.ToUpperInvariant(word[0])); + + // Handle the rest of the characters + if (word.Length > 1) + { + // If the word is all uppercase, convert the rest to lowercase + if (word.All(char.IsUpper)) + { + result.Append(word.Substring(1).ToLowerInvariant()); + } + else + { + // Otherwise, keep the original casing + result.Append(word.Substring(1)); + } + } + } + } + + return result.ToString(); + } + + /// + /// Converts a string to camelCase. + /// + /// The string to convert. + /// The input string converted to camelCase. + private static string ToCamelCase(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + // First, convert to PascalCase + string pascalCase = ToPascalCase(input); + + // Then convert the first character to lowercase + return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs index 1ff6588f..c1ff586d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs @@ -41,12 +41,13 @@ public PowertoolsLambdaSerializer(JsonSerializerContext customerContext) /// public T Deserialize(Stream requestStream) { + Stream streamToUse = requestStream; if (!requestStream.CanSeek) { - using var ms = new MemoryStream(); + var ms = new MemoryStream(); requestStream.CopyTo(ms); ms.Position = 0; - requestStream = ms; + streamToUse = ms; } var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T)); @@ -56,9 +57,10 @@ public T Deserialize(Stream requestStream) $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); } - return (T)JsonSerializer.Deserialize(requestStream, typeInfo)!; + return (T)JsonSerializer.Deserialize(streamToUse, typeInfo)!; } + /// /// Serializes the specified object and writes the result to the output stream. /// diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index 1c88f261..505f6500 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -1,3 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ #if NET8_0_OR_GREATER @@ -7,6 +21,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging.Internal; using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; @@ -45,23 +60,6 @@ public void Constructor_ShouldAddCustomerContext() Assert.True(PowertoolsLoggingSerializer.HasContext(customerContext)); } - // [Fact] - // public void Deserialize_ValidJson_ShouldReturnDeserializedObject() - // { - // // Arrange - // var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); - // var json = "{\"Name\":\"John\",\"Age\":30}"; - // var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - // - // // Act - // var result = serializer.Deserialize(stream); - // - // // Assert - // Assert.NotNull(result); - // Assert.Equal("John", result.Name); - // Assert.Equal(30, result.Age); - // } - [Theory] [InlineData(LoggerOutputCase.CamelCase,"{\"fullName\":\"John\",\"age\":30}", "John", 30)] [InlineData(LoggerOutputCase.PascalCase,"{\"FullName\":\"Jane\",\"Age\":25}", "Jane", 25)] @@ -132,10 +130,58 @@ public void Serialize_InvalidType_ShouldThrowInvalidOperationException() } private class UnknownType { } + + [Fact] + public void Deserialize_NonSeekableStream_ShouldDeserializeCorrectly() + { + // Arrange + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var json = "{\"full_name\":\"John\",\"age\":30}"; + var jsonBytes = Encoding.UTF8.GetBytes(json); + var nonSeekableStream = new NonSeekableStream(jsonBytes); + + // Act + var result = serializer.Deserialize(nonSeekableStream); + + // Assert + Assert.NotNull(result); + Assert.Equal("John", result.FullName); + Assert.Equal(30, result.Age); + } + + public class NonSeekableStream : Stream + { + private readonly MemoryStream _innerStream; + + public NonSeekableStream(byte[] data) + { + _innerStream = new MemoryStream(data); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => _innerStream.Length; + public override long Position + { + get => _innerStream.Position; + set => throw new NotSupportedException(); + } + + public override void Flush() => _innerStream.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + // Override the Close and Dispose methods to prevent the inner stream from being closed + public override void Close() { } + protected override void Dispose(bool disposing) { } + } public void Dispose() { - PowertoolsLoggingSerializer.ClearContext(); + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); } } #endif 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 ffe264a6..2d489dd9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -1,8 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + using System; using System.Collections.Generic; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Internal.Converters; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; @@ -110,6 +126,27 @@ public void Serialize_ShouldHandleEnumValues() Assert.Contains("\"level\":\"Error\"", json); } +#if NET8_0_OR_GREATER + [Fact] + public void Serialize_UnknownType_ThrowsInvalidOperationException() + { + // Arrange + var unknownObject = new UnknownType(); + + // Act & Assert + var exception = Assert.Throws(() => + PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType))); + + Assert.Contains("is not known to the serializer", exception.Message); + Assert.Contains(typeof(UnknownType).ToString(), exception.Message); + } + + private class UnknownType + { + public string SomeProperty { get; set; } + } +#endif + private string SerializeTestObject(LoggerOutputCase? outputCase) { if (outputCase.HasValue) @@ -123,6 +160,7 @@ private string SerializeTestObject(LoggerOutputCase? outputCase) public void Dispose() { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); #if NET8_0_OR_GREATER PowertoolsLoggingSerializer.ClearContext(); #endif diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs new file mode 100644 index 00000000..ee2a438f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using Xunit; +using NSubstitute; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +public class PowertoolsConfigurationExtensionsTests +{ + [Theory] + [InlineData(LoggerOutputCase.CamelCase, "TestString", "testString")] + [InlineData(LoggerOutputCase.PascalCase, "testString", "TestString")] + [InlineData(LoggerOutputCase.SnakeCase, "TestString", "test_string")] + [InlineData(LoggerOutputCase.SnakeCase, "testString", "test_string")] // Default case + public void ConvertToOutputCase_ShouldConvertCorrectly(LoggerOutputCase outputCase, string input, string expected) + { + // Arrange + var systemWrapper = Substitute.For(); + var configurations = new PowertoolsConfigurations(systemWrapper); + + // Act + var result = configurations.ConvertToOutputCase(input, outputCase); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("TestString", "test_string")] + [InlineData("testString", "test_string")] + [InlineData("Test_String", "test_string")] + [InlineData("TEST_STRING", "test_string")] + [InlineData("test", "test")] + [InlineData("TestStringABC", "test_string_abc")] + [InlineData("TestStringABCTest", "test_string_abc_test")] + [InlineData("Test__String", "test__string")] + [InlineData("TEST", "test")] + [InlineData("ABCTestDEF", "abc_test_def")] + [InlineData("ABC_TEST_DEF", "abc_test_def")] + [InlineData("abcTestDef", "abc_test_def")] + [InlineData("abc_test_def", "abc_test_def")] + [InlineData("Abc_Test_Def", "abc_test_def")] + [InlineData("ABC", "abc")] + [InlineData("A_B_C", "a_b_c")] + [InlineData("ABCDEFG", "abcdefg")] + [InlineData("ABCDefGHI", "abc_def_ghi")] + [InlineData("ABCTestDEFGhi", "abc_test_def_ghi")] + [InlineData("Test___String", "test___string")] + public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) + { + // Act + var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToSnakeCase", input); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("testString", "TestString")] + [InlineData("TestString", "TestString")] + [InlineData("test", "Test")] + [InlineData("test_string", "TestString")] + [InlineData("test_string_abc", "TestStringAbc")] + [InlineData("test_stringABC", "TestStringABC")] + [InlineData("test__string", "TestString")] + [InlineData("TEST_STRING", "TestString")] + [InlineData("t", "T")] + [InlineData("", "")] + [InlineData("abc_def_ghi", "AbcDefGhi")] + [InlineData("ABC_DEF_GHI", "AbcDefGhi")] + [InlineData("abc123_def456", "Abc123Def456")] + [InlineData("_test_string", "TestString")] + [InlineData("test_string_", "TestString")] + [InlineData("__test__string__", "TestString")] + [InlineData("TEST__STRING", "TestString")] + [InlineData("testString123", "TestString123")] + [InlineData("test_string_123", "TestString123")] + [InlineData("123_test_string", "123TestString")] + [InlineData("test_1_string", "Test1String")] + public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) + { + // Act + var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToPascalCase", input); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("test_string", "testString")] + [InlineData("testString", "testString")] + [InlineData("TestString", "testString")] + [InlineData("test_string_abc", "testStringAbc")] + [InlineData("test_stringABC", "testStringABC")] + [InlineData("test__string", "testString")] + [InlineData("TEST_STRING", "testString")] + [InlineData("test", "test")] + [InlineData("T", "t")] + [InlineData("", "")] + [InlineData("abc_def_ghi", "abcDefGhi")] + [InlineData("ABC_DEF_GHI", "abcDefGhi")] + [InlineData("abc123_def456", "abc123Def456")] + [InlineData("_test_string", "testString")] + [InlineData("test_string_", "testString")] + [InlineData("__test__string__", "testString")] + [InlineData("TEST__STRING", "testString")] + [InlineData("testString123", "testString123")] + [InlineData("test_string_123", "testString123")] + [InlineData("123_test_string", "123TestString")] + [InlineData("test_1_string", "test1String")] + [InlineData("Test_string", "testString")] + [InlineData("Test_String", "testString")] + [InlineData("Test_String_Abc", "testStringAbc")] + [InlineData("alreadyCamelCase", "alreadyCamelCase")] + [InlineData("ALLCAPS", "allcaps")] + [InlineData("ALL_CAPS", "allCaps")] + [InlineData("single", "single")] + public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) + { + // Act + var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToCamelCase", input); + + // Assert + Assert.Equal(expected, result); + } +} + +// Helper class to invoke private static methods +public static class PrivateMethod +{ + public static T InvokeStatic(Type type, string methodName, params object[] parameters) + { + var method = type.GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + return (T)method!.Invoke(null, parameters); + } +} \ No newline at end of file From 86e624da6d5f7bc17de405d77ad071c5dda30cc0 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:57:22 +0100 Subject: [PATCH 13/32] refactor and add tests for logging handler. --- .../Core/IPowertoolsConfigurations.cs | 2 +- .../Core/PowertoolsConfigurations.cs | 8 +- .../Internal/LoggerProvider.cs | 4 +- ...ggingAspectHandler.cs => LoggingAspect.cs} | 129 +++-- .../PowertoolsConfigurationsExtension.cs | 24 +- .../Internal/PowertoolsLogger.cs | 4 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 18 +- .../LoggerConfiguration.cs | 4 +- .../LoggerOutputCase.cs | 5 + .../LoggingAttribute.cs | 74 +-- .../PowertoolsLoggingSerializer.cs | 2 + ...AWS.Lambda.Powertools.Logging.Tests.csproj | 1 + .../Attributes/LoggingAttributeTest.cs | 504 ++++++++++++++++++ .../{ => Context}/LambdaContextTest.cs | 2 +- .../{ => Formatter}/LogFormatterTest.cs | 5 +- .../Handlers/ExceptionFunctionHandler.cs | 6 + .../LoggingAttributeTest.cs | 337 ------------ .../PowertoolsLoggerTest.cs | 22 +- .../PowertoolsLambdaSerializerTests.cs | 27 +- .../Utilities/TestJsonContext.cs | 23 + libraries/tests/Directory.Packages.props | 1 + 21 files changed, 693 insertions(+), 509 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Logging/Internal/{LoggingAspectHandler.cs => LoggingAspect.cs} (77%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs rename libraries/tests/AWS.Lambda.Powertools.Logging.Tests/{ => Context}/LambdaContextTest.cs (98%) rename libraries/tests/AWS.Lambda.Powertools.Logging.Tests/{ => Formatter}/LogFormatterTest.cs (98%) delete mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs index ee95b318..4d08c0fd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs @@ -72,7 +72,7 @@ public interface IPowertoolsConfigurations /// Gets the logger sample rate. /// /// The logger sample rate. - double? LoggerSampleRate { get; } + double LoggerSampleRate { get; } /// /// Gets a value indicating whether [logger log event]. diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index f098d426..72a2a2da 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System.Globalization; + namespace AWS.Lambda.Powertools.Common; /// @@ -157,10 +159,10 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// Gets the logger sample rate. /// /// The logger sample rate. - public double? LoggerSampleRate => - double.TryParse(_systemWrapper.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), out var result) + public double LoggerSampleRate => + double.TryParse(_systemWrapper.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) ? result - : null; + : 0; /// /// Gets a value indicating whether [logger log event]. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index e11791c8..0265648d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -90,8 +90,8 @@ public void Dispose() /// The configuration. internal void Configure(IOptions config) { - if (_currentConfig is not null || config is null) - return; + // if (_currentConfig is not null || config is null) + // return; _currentConfig = config.Value; foreach (var logger in _loggers.Values) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs similarity index 77% rename from libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 206f9331..9a4ec075 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -16,21 +16,31 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.ExceptionServices; using System.Text.Json; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal.Helpers; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; +internal class LoggingAspectFactory +{ + public static object GetInstance(Type type) + { + return new LoggingAspect(PowertoolsConfigurations.Instance, SystemWrapper.Instance); + } +} + /// -/// Class LoggingAspectHandler. -/// Implements the +/// Logging Aspect +/// Scope.Global is singleton /// /// -internal class LoggingAspectHandler : IMethodAspectHandler +[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] +public class LoggingAspect { /// /// The is cold start @@ -45,22 +55,22 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// /// Clear state? /// - private readonly bool _clearState; + private bool _clearState; /// /// The correlation identifier path /// - private readonly string _correlationIdPath; + private string _correlationIdPath; /// /// The log event /// - private readonly bool? _logEvent; + private bool? _logEvent; /// /// The log level /// - private readonly LogLevel? _logLevel; + private LogLevel? _logLevel; /// /// The Powertools for AWS Lambda (.NET) configurations @@ -85,51 +95,69 @@ internal class LoggingAspectHandler : IMethodAspectHandler /// /// The configuration /// - private readonly LoggerConfiguration _config; + private LoggerConfiguration _config; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// - /// The log level. - /// if set to true [log event]. - /// The correlation identifier path. - /// if set to true [clear state]. /// The Powertools configurations. /// The system wrapper. - internal LoggingAspectHandler - ( - LoggerConfiguration config, - LogLevel? logLevel, - bool? logEvent, - string correlationIdPath, - bool clearState, - IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper - ) + public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) { - _logLevel = logLevel; - _logEvent = logEvent; - _clearState = clearState; - _correlationIdPath = correlationIdPath; _powertoolsConfigurations = powertoolsConfigurations; _systemWrapper = systemWrapper; - _config = config; } /// - /// Handles the event. + /// Runs before the execution of the method marked with the Logging Attribute /// - /// - /// The instance containing the - /// event data. - /// - public void OnEntry(AspectEventArgs eventArgs) + /// + /// + /// + /// + /// + /// + /// + [Advice(Kind.Before)] + public void OnEntry( + [Argument(Source.Instance)] object instance, + [Argument(Source.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Type)] Type hostType, + [Argument(Source.Metadata)] MethodBase method, + [Argument(Source.ReturnType)] Type returnType, + [Argument(Source.Triggers)] Attribute[] triggers) { + // Called before the method + var trigger = triggers.OfType().First(); + + var eventArgs = new AspectEventArgs + { + Instance = instance, + Type = hostType, + Method = method, + Name = name, + Args = args, + ReturnType = returnType, + Triggers = triggers + }; + + _config = new LoggerConfiguration + { + Service = trigger.Service, + LoggerOutputCase = trigger.LoggerOutputCase, + SamplingRate = trigger.SamplingRate + }; + + _logLevel = trigger.LogLevel; + _logEvent = trigger.LogEvent; + _correlationIdPath = trigger.CorrelationIdPath; + _clearState = trigger.ClearState; + switch (Logger.LoggerProvider) { case null: - Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper ); + Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); break; case LoggerProvider: ((LoggerProvider)Logger.LoggerProvider).Configure(_config); @@ -153,18 +181,6 @@ public void OnEntry(AspectEventArgs eventArgs) LogEvent(eventObject); } - /// - /// Called when [success]. - /// - /// - /// The instance containing the - /// event data. - /// - /// The result. - public void OnSuccess(AspectEventArgs eventArgs, object result) - { - } - /// /// Called when [exception]. /// @@ -181,13 +197,10 @@ public void OnException(AspectEventArgs eventArgs, Exception exception) } /// - /// Handles the event. + /// Handles the Kind.After event. /// - /// - /// The instance containing the - /// event data. - /// - public void OnExit(AspectEventArgs eventArgs) + [Advice(Kind.After)] + public void OnExit() { if (!_isContextInitialized) return; @@ -270,13 +283,14 @@ private void CaptureCorrelationId(object eventArg) var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); #else - var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg)); + var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg)); #endif var element = jsonDoc.RootElement; for (var i = 0; i < correlationIdPaths.Length; i++) { - var pathWithOutputCase = _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i]); + // For casing parsing to be removed from Logging v2 when we get rid of outputcase + var pathWithOutputCase = _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; @@ -346,5 +360,6 @@ internal static void ResetForTest() LoggingLambdaContext.Clear(); Logger.LoggerProvider = null; Logger.RemoveAllKeys(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index c5caa423..a4f41eec 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -71,10 +71,10 @@ internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powert /// Optional explicit logger output case. /// The determined LoggerOutputCase. internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, - LoggerOutputCase? loggerOutputCase = null) + LoggerOutputCase loggerOutputCase) { - if (loggerOutputCase.HasValue) - return loggerOutputCase.Value; + if (loggerOutputCase != LoggerOutputCase.Default) + return loggerOutputCase; if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result)) return result; @@ -99,7 +99,7 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati /// Gets the current configuration. /// /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. - public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, + internal static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper) { config ??= new LoggerConfiguration(); @@ -121,7 +121,7 @@ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfiguration // set output case var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); config.LoggerOutputCase = loggerOutputCase; - PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase ?? LoggingConstants.DefaultLoggerOutputCase); + PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); // log level var minLogLevel = logLevel; @@ -148,19 +148,17 @@ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfiguration private static LoggerConfiguration SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper, LogLevel minLogLevel) { - var samplingRate = config.SamplingRate ?? powertoolsConfigurations.LoggerSampleRate; + var samplingRate = config.SamplingRate == 0 ? powertoolsConfigurations.LoggerSampleRate : config.SamplingRate; config.SamplingRate = samplingRate; switch (samplingRate) { - case null: - return config; case < 0 or > 1: { if (minLogLevel is LogLevel.Debug or LogLevel.Trace) systemWrapper.LogLine( - $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}"); - config.SamplingRate = null; + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + config.SamplingRate = 0; return config; } case 0: @@ -169,10 +167,10 @@ private static LoggerConfiguration SetSamplingRate(IPowertoolsConfigurations pow var sample = systemWrapper.GetRandom(); - if ((samplingRate.Value <= sample)) return config; + if (samplingRate <= sample) return config; systemWrapper.LogLine( - $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}."); + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); config.MinimumLevel = LogLevel.Debug; return config; @@ -197,7 +195,7 @@ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powert /// /// The input string converted to the configured case (camel, pascal, or snake case). /// - internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, string correlationIdPath, LoggerOutputCase? loggerOutputCase = null) + internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, string correlationIdPath, LoggerOutputCase loggerOutputCase) { return powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 82052a60..603dd86d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -253,8 +253,8 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (_currentConfig.SamplingRate.HasValue) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate.Value); + if (_currentConfig.SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate); if (exception != null) logEntry.TryAdd(LoggingConstants.KeyException, exception); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index ad3611cb..472b00bf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -51,7 +51,7 @@ public class Logger /// Gets the scope. /// /// The scope. - private static IDictionary _scope { get; } = new Dictionary(StringComparer.Ordinal); + private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal); /// /// Creates a new instance. @@ -91,13 +91,7 @@ public static void AppendKey(string key, object value) if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key)); - if (value is null) - throw new ArgumentNullException(nameof(value)); - - if (_scope.ContainsKey(key)) - _scope[key] = value; - else - _scope.Add(key, value); + Scope[key] = value ?? throw new ArgumentNullException(nameof(value)); } /// @@ -128,8 +122,8 @@ public static void RemoveKeys(params string[] keys) { if (keys == null) return; foreach (var key in keys) - if (_scope.ContainsKey(key)) - _scope.Remove(key); + if (Scope.ContainsKey(key)) + Scope.Remove(key); } /// @@ -138,7 +132,7 @@ public static void RemoveKeys(params string[] keys) /// IEnumerable<KeyValuePair<System.String, System.Object>>. public static IEnumerable> GetAllKeys() { - return _scope.AsEnumerable(); + return Scope.AsEnumerable(); } /// @@ -146,7 +140,7 @@ public static IEnumerable> GetAllKeys() /// internal static void RemoveAllKeys() { - _scope.Clear(); + Scope.Clear(); } #endregion diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs index 4d8ec0c8..30cad5ea 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs @@ -45,7 +45,7 @@ public class LoggerConfiguration : IOptions /// This can be also set using the environment variable POWERTOOLS_LOGGER_SAMPLE_RATE. /// /// The sampling rate. - public double? SamplingRate { get; set; } + public double SamplingRate { get; set; } /// /// The default configured options instance @@ -58,5 +58,5 @@ public class LoggerConfiguration : IOptions /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. /// /// The logger output case. - public LoggerOutputCase? LoggerOutputCase { get; set; } + public LoggerOutputCase LoggerOutputCase { get; set; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerOutputCase.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerOutputCase.cs index 6c64e2f0..1ca6a722 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerOutputCase.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerOutputCase.cs @@ -22,6 +22,11 @@ namespace AWS.Lambda.Powertools.Logging; /// public enum LoggerOutputCase { + /// + /// Default value when Not Set - must be first element in Enum + /// + [EnumMember(Value = "Default")] Default, + /// /// Camel Case /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 67aa49aa..9fdd2511 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -14,6 +14,7 @@ */ using System; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -116,7 +117,8 @@ namespace AWS.Lambda.Powertools.Logging; /// /// [AttributeUsage(AttributeTargets.Method)] -public class LoggingAttribute : MethodAspectAttribute +[Injection(typeof(LoggingAspect))] +public class LoggingAttribute : Attribute { /// /// The log event @@ -127,17 +129,6 @@ public class LoggingAttribute : MethodAspectAttribute /// The log level /// private LogLevel? _logLevel; - - - /// - /// The sampling rate - /// - private double? _samplingRate; - - /// - /// The logger output case - /// - private LoggerOutputCase? _loggerOutputCase; /// /// Service name is used for logging. @@ -162,11 +153,7 @@ public LogLevel LogLevel /// This can be also set using the environment variable POWERTOOLS_LOGGER_SAMPLE_RATE. /// /// The sampling rate. - public double SamplingRate - { - get => _samplingRate.GetValueOrDefault(); - set => _samplingRate = value; - } + public double SamplingRate { get; set; } /// /// Explicitly log any incoming event, The first handler parameter is the input to the handler, @@ -202,33 +189,30 @@ public bool LogEvent /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. /// /// The log level. - public LoggerOutputCase LoggerOutputCase { - get => _loggerOutputCase ?? LoggingConstants.DefaultLoggerOutputCase; - set => _loggerOutputCase = value; - } + public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; - /// - /// Creates the handler. - /// - /// IMethodAspectHandler. - protected override IMethodAspectHandler CreateHandler() - { - var config = new LoggerConfiguration - { - Service = Service, - LoggerOutputCase = LoggerOutputCase, - SamplingRate = SamplingRate, - }; - - return new LoggingAspectHandler - ( - config, - LogLevel, - LogEvent, - CorrelationIdPath, - ClearState, - PowertoolsConfigurations.Instance, - SystemWrapper.Instance - ); - } + // /// + // /// Creates the handler. + // /// + // /// IMethodAspectHandler. + // protected override IMethodAspectHandler CreateHandler() + // { + // var config = new LoggerConfiguration + // { + // Service = Service, + // LoggerOutputCase = LoggerOutputCase, + // SamplingRate = SamplingRate, + // }; + // + // return new LoggingAspect + // ( + // config, + // LogLevel, + // LogEvent, + // CorrelationIdPath, + // ClearState, + // PowertoolsConfigurations.Instance, + // SystemWrapper.Instance + // ); + // } } \ 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 2e290fa5..5d0d776c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -159,6 +159,7 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() #endif jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + jsonOptions.PropertyNameCaseInsensitive = true; #if NET8_0_OR_GREATER jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); @@ -175,6 +176,7 @@ internal static bool HasContext(JsonSerializerContext customContext) { return AdditionalContexts.Contains(customContext); } + internal static void ClearContext() { AdditionalContexts.Clear(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj index f98e40b6..352e6707 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs new file mode 100644 index 00000000..c790d76f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -0,0 +1,504 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.CloudWatchEvents; +using Amazon.Lambda.CloudWatchEvents.S3Events; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace AWS.Lambda.Powertools.Logging.Tests.Attributes +{ + class TestClass + { + [Logging] + public void TestMethod() + { + } + + [Logging(LogLevel = LogLevel.Debug)] + public void TestMethodDebug() + { + } + + [Logging(LogEvent = true)] + public void LogEvent() + { + } + + [Logging(LogEvent = true, LogLevel = LogLevel.Debug)] + public void LogEventDebug() + { + } + + [Logging(ClearState = true)] + public void ClearState() + { + } + + [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public void CorrelationApiGatewayProxyRequest(APIGatewayProxyRequest apiGatewayProxyRequest) + { + } + + [Logging(CorrelationIdPath = CorrelationIdPaths.ApplicationLoadBalancer)] + public void CorrelationApplicationLoadBalancerRequest( + ApplicationLoadBalancerRequest applicationLoadBalancerRequest) + { + } + + [Logging(CorrelationIdPath = CorrelationIdPaths.EventBridge)] + public void CorrelationCloudWatchEvent(CloudWatchEvent cwEvent) + { + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public void CorrelationIdFromString(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public void CorrelationIdFromStringSnake(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/Headers/MyRequestIdHeader", LoggerOutputCase = LoggerOutputCase.PascalCase)] + public void CorrelationIdFromStringPascal(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/myRequestIdHeader", LoggerOutputCase = LoggerOutputCase.CamelCase)] + public void CorrelationIdFromStringCamel(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public void CorrelationIdFromStringSnakeEnv(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/Headers/MyRequestIdHeader")] + public void CorrelationIdFromStringPascalEnv(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/myRequestIdHeader")] + public void CorrelationIdFromStringCamelEnv(TestObject testObject) + { + } + + [Logging(Service = "test")] + public void HandlerService() + { + Logger.LogInformation("test"); + } + + [Logging] + public void HandlerServiceEnv() + { + Logger.LogInformation("test"); + } + + [Logging(SamplingRate = 0.5)] + public void HandlerSamplingRate() + { + Logger.LogInformation("test"); + } + + [Logging] + public void HandlerSamplingRateEnv() + { + Logger.LogInformation("test"); + } + } + + + // [Collection("Sequential")] + public class LoggingAttributeTestWithoutLambdaContext : IDisposable + { + private TestClass _testClass; + + public LoggingAttributeTestWithoutLambdaContext() + { + _testClass = new TestClass(); + } + + [Fact] + public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.TestMethod(); + + // Assert + var allKeys = Logger.GetAllKeys() + .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); + + Assert.NotNull(Logger.LoggerProvider); + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); + Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); + + consoleOut.DidNotReceive().WriteLine(Arg.Any()); + } + + [Fact] + public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebug() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.TestMethodDebug(); + + // Assert + var allKeys = Logger.GetAllKeys() + .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); + + Assert.NotNull(Logger.LoggerProvider); + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); + Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); + Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); + + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i == $"Skipping Lambda Context injection because ILambdaContext context parameter not found.") + ); + } + + [Fact] + public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.LogEvent(); + + consoleOut.DidNotReceive().WriteLine( + Arg.Any() + ); + } + + [Fact] + public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.LogEventDebug(); + + consoleOut.Received(1).WriteLine( + Arg.Is(i => i == "Skipping Event Log because event parameter not found.") + ); + } + + [Fact] + public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.ClearState(); + + Assert.NotNull(Logger.LoggerProvider); + Assert.False(Logger.GetAllKeys().Any()); + } + + [Theory] + [InlineData(CorrelationIdPaths.ApiGatewayRest)] + [InlineData(CorrelationIdPaths.ApplicationLoadBalancer)] + [InlineData(CorrelationIdPaths.EventBridge)] + [InlineData("/headers/my_request_id_header")] + public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationIdPath) + { + // Arrange + var correlationId = Guid.NewGuid().ToString(); + +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + + // Act + switch (correlationIdPath) + { + case CorrelationIdPaths.ApiGatewayRest: + _testClass.CorrelationApiGatewayProxyRequest(new APIGatewayProxyRequest + { + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + RequestId = correlationId + } + }); + break; + case CorrelationIdPaths.ApplicationLoadBalancer: + _testClass.CorrelationApplicationLoadBalancerRequest(new ApplicationLoadBalancerRequest + { + Headers = new Dictionary + { + { "x-amzn-trace-id", correlationId } + } + }); + break; + case CorrelationIdPaths.EventBridge: + _testClass.CorrelationCloudWatchEvent(new S3ObjectCreateEvent + { + Id = correlationId + }); + break; + case "/headers/my_request_id_header": + _testClass.CorrelationIdFromString(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + } + + // Assert + var allKeys = Logger.GetAllKeys() + .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); + + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); + Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); + } + + [Theory] + [InlineData(LoggerOutputCase.SnakeCase)] + [InlineData(LoggerOutputCase.PascalCase)] + [InlineData(LoggerOutputCase.CamelCase)] + public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outputCase) + { + // Arrange + var correlationId = Guid.NewGuid().ToString(); + +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + + // Act + switch (outputCase) + { + case LoggerOutputCase.CamelCase: + _testClass.CorrelationIdFromStringCamel(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.PascalCase: + _testClass.CorrelationIdFromStringPascal(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.SnakeCase: + _testClass.CorrelationIdFromStringSnake(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + } + + // Assert + var allKeys = Logger.GetAllKeys() + .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); + + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); + Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); + } + + [Theory] + [InlineData(LoggerOutputCase.SnakeCase)] + [InlineData(LoggerOutputCase.PascalCase)] + [InlineData(LoggerOutputCase.CamelCase)] + public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(LoggerOutputCase outputCase) + { + // Arrange + var correlationId = Guid.NewGuid().ToString(); + +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + + // Act + switch (outputCase) + { + case LoggerOutputCase.CamelCase: + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + _testClass.CorrelationIdFromStringCamelEnv(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.PascalCase: + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "PascalCase"); + _testClass.CorrelationIdFromStringPascalEnv(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.SnakeCase: + _testClass.CorrelationIdFromStringSnakeEnv(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + } + + // Assert + var allKeys = Logger.GetAllKeys() + .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); + + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); + Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); + } + + [Fact] + public void When_Setting_SamplingRate_Should_Add_Key() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + + // Act + _testClass.HandlerSamplingRate(); + + // Assert + + var st = consoleOut.ToString(); + Assert.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\",\"samplingRate\":0.5", st); + } + + [Fact] + public void When_Setting_Env_SamplingRate_Should_Add_Key() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", "0.5"); + + // Act + _testClass.HandlerSamplingRateEnv(); + + // Assert + + var st = consoleOut.ToString(); + Assert.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\",\"samplingRate\":0.5", st); + } + + [Fact] + public void When_Setting_Service_Should_Update_Key() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + + // Act + _testClass.HandlerService(); + + // Assert + + var st = consoleOut.ToString(); + Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + } + + [Fact] + public void When_Setting_Env_Service_Should_Update_Key() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "service name"); + + // Act + _testClass.HandlerServiceEnv(); + + // Assert + + var st = consoleOut.ToString(); + Assert.Contains("\"level\":\"Information\",\"service\":\"service name\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); + + LoggingAspect.ResetForTest(); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Context/LambdaContextTest.cs similarity index 98% rename from libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs rename to libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Context/LambdaContextTest.cs index 55a8f928..feb9283e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Context/LambdaContextTest.cs @@ -7,7 +7,7 @@ using NSubstitute; using Xunit; -namespace AWS.Lambda.Powertools.Logging.Tests; +namespace AWS.Lambda.Powertools.Logging.Tests.Context; public class LambdaContextTest { diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs similarity index 98% rename from libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs rename to libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index aadb3a2f..b44ec4e3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -22,14 +22,13 @@ using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Serializers; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; using Xunit; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace AWS.Lambda.Powertools.Logging.Tests +namespace AWS.Lambda.Powertools.Logging.Tests.Formatter { [Collection("Sequential")] public class LogFormatterTest @@ -174,7 +173,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() Logger.UseDefaultFormatter(); Logger.RemoveAllKeys(); LoggingLambdaContext.Clear(); - LoggingAspectHandler.ResetForTest(); + LoggingAspect.ResetForTest(); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandler.cs index 23005388..170f2a92 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandler.cs @@ -52,4 +52,10 @@ public string HandlerLoggerForExceptions(string input, ILambdaContext context) return "OK"; } + + [Logging(LogEvent = true)] + public string HandleOk(string input) + { + return input.ToUpper(CultureInfo.InvariantCulture); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs deleted file mode 100644 index c13c9759..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using Amazon.Lambda.APIGatewayEvents; -using Amazon.Lambda.ApplicationLoadBalancerEvents; -using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Tests.Utilities; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace AWS.Lambda.Powertools.Logging.Tests -{ - [Collection("Sequential")] - public class LoggingAttributeTestWithoutLambdaContext - { - [Fact] - public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() - { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Information; - - var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = Array.Empty() - }; - - var config = new LoggerConfiguration - { - Service = service - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, systemWrapper); - - // Act - handler.OnEntry(eventArgs); - - var allKeys = Logger.GetAllKeys() - .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - - Assert.NotNull(Logger.LoggerProvider); - Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - - systemWrapper.DidNotReceive().LogLine( - Arg.Any() - ); - } - } - - [Collection("Sequential")] - public class LoggingAttributeTestWithoutLambdaContextDebug - { - [Fact] - public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebug() - { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Trace; - - var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = Array.Empty() - }; - - var config = new LoggerConfiguration - { - Service = service - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, - systemWrapper); - - // Act - handler.OnEntry(eventArgs); - - var allKeys = Logger.GetAllKeys() - .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - - Assert.NotNull(Logger.LoggerProvider); - Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - - systemWrapper.Received(1).LogLine( - Arg.Is(i => - i == $"Skipping Lambda Context injection because ILambdaContext context parameter not found.") - ); - } - } - - [Collection("Sequential")] - public class LoggingAttributeTestWithoutEventArg - { - [Fact] - public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() - { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Information; - - var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = Array.Empty() - }; - - var config = new LoggerConfiguration - { - Service = service - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, - systemWrapper); - - // Act - handler.OnEntry(eventArgs); - - systemWrapper.DidNotReceive().LogLine( - Arg.Any() - ); - } - } - - [Collection("Sequential")] - public class LoggingAttributeTestWithoutEventArgDebug - { - [Fact] - public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() - { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Trace; - - var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = Array.Empty() - }; - - var config = new LoggerConfiguration - { - Service = service - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(config, logLevel, true, null, true, configurations, - systemWrapper); - - // Act - handler.OnEntry(eventArgs); - - systemWrapper.Received(1).LogLine( - Arg.Is(i => i == "Skipping Event Log because event parameter not found.") - ); - } - } - - [Collection("Sequential")] - public class LoggingAttributeTestForClearContext - { - [Fact] - public void OnExit_WhenHandler_ClearKeys() - { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Trace; - - var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = Array.Empty() - }; - - var config = new LoggerConfiguration - { - Service = service - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(config, logLevel, null, null, true, configurations, - systemWrapper); - - // Act - handler.OnEntry(eventArgs); - - var allKeys = Logger.GetAllKeys() - .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - - Assert.NotNull(Logger.LoggerProvider); - Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); - - handler.OnExit(eventArgs); - - Assert.NotNull(Logger.LoggerProvider); - Assert.False(Logger.GetAllKeys().Any()); - } - } - - public abstract class LoggingAttributeTestWithEventArgCorrelationId - { - protected void OnEntry_WhenEventArgExists_CapturesCorrelationIdBase(string correlationId, - string correlationIdPath, object eventArg) - { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Information; - - var configurations = new PowertoolsConfigurations(new SystemWrapperMock(new PowertoolsEnvironment())); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = new[] { eventArg } - }; - - var config = new LoggerConfiguration - { - Service = service - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(config, logLevel, null, correlationIdPath, false, - configurations, systemWrapper); - - // Act - handler.OnEntry(eventArgs); - - var allKeys = Logger.GetAllKeys() - .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - - // Assert - Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); - Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); - } - } - - [Collection("Sequential")] - public class LoggingAttributeTestWithEventArgCorrelationIdApiGateway : LoggingAttributeTestWithEventArgCorrelationId - { - [Fact] - public void OnEntry_WhenEventArgExists_CapturesCorrelationId() - { - var correlationId = Guid.NewGuid().ToString(); - OnEntry_WhenEventArgExists_CapturesCorrelationIdBase - ( - correlationId, - CorrelationIdPaths.ApiGatewayRest, - new APIGatewayProxyRequest - { - RequestContext = new APIGatewayProxyRequest.ProxyRequestContext - { - RequestId = correlationId - } - } - ); - } - } - - [Collection("Sequential")] - public class LoggingAttributeTestWithEventArgCorrelationIdApplicationLoadBalancer : LoggingAttributeTestWithEventArgCorrelationId - { - [Fact] - public void OnEntry_WhenEventArgExists_CapturesCorrelationId() - { - var correlationId = Guid.NewGuid().ToString(); - OnEntry_WhenEventArgExists_CapturesCorrelationIdBase - ( - correlationId, - CorrelationIdPaths.ApplicationLoadBalancer, - new ApplicationLoadBalancerRequest - { - Headers = new Dictionary - { - { "x-amzn-trace-id", correlationId } - } - } - ); - } - } -} \ 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 95a6bb96..b99bf129 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -24,8 +24,6 @@ using AWS.Lambda.Powertools.Logging.Tests.Utilities; using Microsoft.Extensions.Logging; using NSubstitute; -using NSubstitute.Extensions; -using NSubstitute.ReceivedExtensions; using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests @@ -260,7 +258,6 @@ public void LogNone_WithAnyMinimumLevel_DoesNotLog(LogLevel minimumLevel) public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() { // Arrange - var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Trace; var loggerSampleRate = 0.7; @@ -280,10 +277,11 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() MinimumLevel = null }; - configurations.SetCurrentConfig(loggerConfiguration, systemWrapper); + // Act - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - loggerConfiguration); + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var logger = provider.CreateLogger("test"); logger.LogInformation("Test"); @@ -300,7 +298,6 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() { // Arrange - var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Trace; var loggerSampleRate = 0.7; @@ -320,14 +317,12 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() MinimumLevel = null }; - //var (_, output) = configurations.SetCurrentConfig(loggerConfiguration); - + // Act + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger("test"); - // var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - // loggerConfiguration); - + logger.LogInformation("Test"); // Assert @@ -361,6 +356,7 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() MinimumLevel = null }; + // Act var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); @@ -398,6 +394,7 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() MinimumLevel = null }; + // Act var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); @@ -440,6 +437,7 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() LoggerOutputCase = LoggerOutputCase.CamelCase }; + // Act var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index 505f6500..cc82efb5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -20,30 +20,19 @@ using System.IO; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; -[JsonSerializable(typeof(TestObject))] -public partial class TestJsonContext : JsonSerializerContext -{ -} - -public class TestObject -{ - public string FullName { get; set; } - public int Age { get; set; } -} - public class PowertoolsLambdaSerializerTests : IDisposable { [Fact] public void Constructor_ShouldNotThrowException() { // Arrange & Act & Assert - var exception = Record.Exception(() => new PowertoolsLambdaSerializer(TestJsonContext.Default)); + var exception = Record.Exception(() => new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default)); Assert.Null(exception); } @@ -51,7 +40,7 @@ public void Constructor_ShouldNotThrowException() public void Constructor_ShouldAddCustomerContext() { // Arrange - var customerContext = new TestJsonContext(); + var customerContext = new Utilities.TestJsonContext(); // Act var serializer = new PowertoolsLambdaSerializer(customerContext); @@ -67,7 +56,7 @@ public void Constructor_ShouldAddCustomerContext() public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCase outputCase,string json, string expectedName, int expectedAge) { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase); var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); @@ -85,7 +74,7 @@ public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCas public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); @@ -100,7 +89,7 @@ public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() public void Serialize_ValidObject_ShouldSerializeToStream() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); @@ -121,7 +110,7 @@ public void Serialize_ValidObject_ShouldSerializeToStream() public void Serialize_InvalidType_ShouldThrowInvalidOperationException() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); var unknownObject = new UnknownType(); var stream = new MemoryStream(); @@ -135,7 +124,7 @@ private class UnknownType { } public void Deserialize_NonSeekableStream_ShouldDeserializeCorrectly() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); var json = "{\"full_name\":\"John\",\"age\":30}"; var jsonBytes = Encoding.UTF8.GetBytes(json); var nonSeekableStream = new NonSeekableStream(jsonBytes); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs new file mode 100644 index 00000000..0d69bda6 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using Amazon.Lambda.CloudWatchEvents.S3Events; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +[JsonSerializable(typeof(S3ObjectCreateEvent))] +[JsonSerializable(typeof(TestObject))] +internal partial class TestJsonContext : JsonSerializerContext +{ +} + +internal class TestObject +{ + public string FullName { get; set; } + public int Age { get; set; } + + public Header Headers { get; set; } +} + +internal class Header +{ + public string MyRequestIdHeader { get; set; } +} \ No newline at end of file diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props index 53241e7b..4e110e46 100644 --- a/libraries/tests/Directory.Packages.props +++ b/libraries/tests/Directory.Packages.props @@ -3,6 +3,7 @@ true + From 80815ea9974f03b9ba355e0defbd6e46354b4746 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:11:16 +0100 Subject: [PATCH 14/32] Refactor Aspect Injection to Factory. Add more tests for full path using the decorator. Refactor serializer for thread safety. --- .../Internal/LoggerProvider.cs | 27 +- .../Internal/LoggingAspect.cs | 18 +- .../Internal/LoggingAspectFactory.cs | 35 ++ .../PowertoolsConfigurationsExtension.cs | 178 +++++----- .../Internal/PowertoolsLogger.cs | 245 +++++++------- .../PowertoolsLoggingSerializer.cs | 40 ++- .../Attributes/LoggingAttributeTest.cs | 316 +++++++++--------- .../Attributes/TestClass.cs | 126 +++++++ .../Formatter/LogFormatterTest.cs | 36 +- .../PowertoolsLoggerTest.cs | 178 ++++++---- .../PowertoolsLoggingSerializerTests.cs | 19 +- .../TestSetup.cs | 18 + .../Utilities/SystemWrapperMock.cs | 2 +- 13 files changed, 727 insertions(+), 511 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/TestSetup.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index 0265648d..1d70b85d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -42,10 +42,6 @@ public sealed class LoggerProvider : ILoggerProvider /// private readonly ConcurrentDictionary _loggers = new(); - /// - /// The current configuration - /// - private LoggerConfiguration _currentConfig; /// /// Initializes a new instance of the class. @@ -57,8 +53,7 @@ public LoggerProvider(IOptions config, IPowertoolsConfigura { _powertoolsConfigurations = powertoolsConfigurations; _systemWrapper = systemWrapper; - - _currentConfig= powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper); + _powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper); } /// @@ -69,10 +64,9 @@ public LoggerProvider(IOptions config, IPowertoolsConfigura public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, - name => new PowertoolsLogger(name, + name => PowertoolsLogger.CreateLogger(name, _powertoolsConfigurations, - _systemWrapper, - _currentConfig)); + _systemWrapper)); } /// @@ -82,19 +76,4 @@ public void Dispose() { _loggers.Clear(); } - - - /// - /// Configures the loggers. - /// - /// The configuration. - internal void Configure(IOptions config) - { - // if (_currentConfig is not null || config is null) - // return; - - _currentConfig = config.Value; - foreach (var logger in _loggers.Values) - logger.ClearConfig(); - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 9a4ec075..c6992dfb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -26,14 +26,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; -internal class LoggingAspectFactory -{ - public static object GetInstance(Type type) - { - return new LoggingAspect(PowertoolsConfigurations.Instance, SystemWrapper.Instance); - } -} - /// /// Logging Aspect /// Scope.Global is singleton @@ -154,15 +146,7 @@ public void OnEntry( _correlationIdPath = trigger.CorrelationIdPath; _clearState = trigger.ClearState; - switch (Logger.LoggerProvider) - { - case null: - Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); - break; - case LoggerProvider: - ((LoggerProvider)Logger.LoggerProvider).Configure(_config); - break; - } + Logger.LoggerProvider ??= new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); if (!_initializeContext) return; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs new file mode 100644 index 00000000..c575e8db --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using AWS.Lambda.Powertools.Common; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Class LoggingAspectFactory. For "dependency inject" Configuration and SystemWrapper to Aspect +/// +internal class LoggingAspectFactory +{ + /// + /// Get an instance of the LoggingAspect class. + /// + /// The type of the class to be logged. + /// An instance of the LoggingAspect class. + public static object GetInstance(Type type) + { + return new LoggingAspect(PowertoolsConfigurations.Instance, SystemWrapper.Instance); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index a4f41eec..58400109 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -28,6 +28,22 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal static class PowertoolsConfigurationsExtension { + private static readonly object _lock = new object(); + private static LoggerConfiguration _config; + + /// + /// Maps AWS log level to .NET log level + /// + private static readonly Dictionary AwsLogLevelMapper = new(StringComparer.OrdinalIgnoreCase) + { + { "TRACE", LogLevel.Trace }, + { "DEBUG", LogLevel.Debug }, + { "INFO", LogLevel.Information }, + { "WARN", LogLevel.Warning }, + { "ERROR", LogLevel.Error }, + { "FATAL", LogLevel.Critical } + }; + /// /// Gets the log level. /// @@ -53,15 +69,9 @@ internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsCo /// internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations) { - AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), - out var awsLogLevel); + var awsLogLevel = (powertoolsConfigurations.AWSLambdaLogLevel ?? string.Empty).Trim().ToUpperInvariant(); - if (Enum.TryParse(awsLogLevel, true, out LogLevel result)) - { - return result; - } - - return LogLevel.None; + return AwsLogLevelMapper.GetValueOrDefault(awsLogLevel, LogLevel.None); } /// @@ -82,59 +92,40 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati return LoggingConstants.DefaultLoggerOutputCase; } - /// - /// Maps AWS log level to .NET log level - /// - private static readonly Dictionary AwsLogLevelMapper = new() - { - { "TRACE", "TRACE" }, - { "DEBUG", "DEBUG" }, - { "INFO", "INFORMATION" }, - { "WARN", "WARNING" }, - { "ERROR", "ERROR" }, - { "FATAL", "CRITICAL" } - }; - /// /// Gets the current configuration. /// /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. - internal static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, - LoggerConfiguration config, ISystemWrapper systemWrapper) + internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper) { - config ??= new LoggerConfiguration(); + lock (_lock) + { + _config = config; - var logLevel = powertoolsConfigurations.GetLogLevel(config.MinimumLevel); - var lambdaLogLevel = powertoolsConfigurations.GetLambdaLogLevel(); - var lambdaLogLevelEnabled = powertoolsConfigurations.LambdaLogLevelEnabled(); + var logLevel = powertoolsConfigurations.GetLogLevel(_config.MinimumLevel); + var lambdaLogLevel = powertoolsConfigurations.GetLambdaLogLevel(); + var lambdaLogLevelEnabled = powertoolsConfigurations.LambdaLogLevelEnabled(); - if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) - { - systemWrapper.LogLine( - $"Current log level ({logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); - } + if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) + { + systemWrapper.LogLine($"Current log level ({logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); + } - // set service - var service = config.Service ?? powertoolsConfigurations.Service; - config.Service = service; - - // set output case - var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); - config.LoggerOutputCase = loggerOutputCase; - PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); - - // log level - var minLogLevel = logLevel; - if (lambdaLogLevelEnabled) - { - minLogLevel = lambdaLogLevel; - } + // Set service + _config.Service = _config.Service ?? powertoolsConfigurations.Service; + + // Set output case + var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(_config.LoggerOutputCase); + _config.LoggerOutputCase = loggerOutputCase; + PowertoolsLoggingSerializer.ConfigureNamingPolicy(loggerOutputCase); + + // Set log level + var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + _config.MinimumLevel = minLogLevel; - config.MinimumLevel = minLogLevel; - - // set sampling rate - config = SetSamplingRate(powertoolsConfigurations, config, systemWrapper, minLogLevel); - return config; + // Set sampling rate + SetSamplingRate(powertoolsConfigurations, systemWrapper, minLogLevel); + } } /// @@ -145,35 +136,44 @@ internal static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfigurati /// /// /// - private static LoggerConfiguration SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, - LoggerConfiguration config, ISystemWrapper systemWrapper, LogLevel minLogLevel) + private static void SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, LogLevel minLogLevel) { - var samplingRate = config.SamplingRate == 0 ? powertoolsConfigurations.LoggerSampleRate : config.SamplingRate; - config.SamplingRate = samplingRate; + double samplingRate = _config.SamplingRate == 0 ? powertoolsConfigurations.LoggerSampleRate : _config.SamplingRate; + samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, systemWrapper); + + _config.SamplingRate = samplingRate; - switch (samplingRate) + if (samplingRate > 0) { - case < 0 or > 1: + double sample = systemWrapper.GetRandom(); + + if (sample <= samplingRate) { - if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - systemWrapper.LogLine( - $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); - config.SamplingRate = 0; - return config; + systemWrapper.LogLine($"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + _config.MinimumLevel = LogLevel.Debug; } - case 0: - return config; + } + } + + /// + /// Validate Sampling rate + /// + /// + /// + /// + /// + private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) + { + if (samplingRate < 0 || samplingRate > 1) + { + if (minLogLevel is LogLevel.Debug or LogLevel.Trace) + { + systemWrapper.LogLine($"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + } + return 0; } - var sample = systemWrapper.GetRandom(); - - if (samplingRate <= sample) return config; - - systemWrapper.LogLine( - $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - config.MinimumLevel = LogLevel.Debug; - - return config; + return samplingRate; } /// @@ -195,7 +195,8 @@ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powert /// /// The input string converted to the configured case (camel, pascal, or snake case). /// - internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, string correlationIdPath, LoggerOutputCase loggerOutputCase) + internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, + string correlationIdPath, LoggerOutputCase loggerOutputCase) { return powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch { @@ -231,11 +232,12 @@ private static string ToSnakeCase(string input) } else if (char.IsUpper(currentChar)) { - if (i > 0 && !lastCharWasUnderscore && + if (i > 0 && !lastCharWasUnderscore && (!lastCharWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) { result.Append('_'); } + result.Append(char.ToLowerInvariant(currentChar)); lastCharWasUnderscore = false; lastCharWasUpper = true; @@ -252,8 +254,6 @@ private static string ToSnakeCase(string input) } - - /// /// Converts a string to PascalCase. /// @@ -273,7 +273,7 @@ private static string ToPascalCase(string input) { // Capitalize the first character of each word result.Append(char.ToUpperInvariant(word[0])); - + // Handle the rest of the characters if (word.Length > 1) { @@ -310,4 +310,24 @@ private static string ToCamelCase(string input) // Then convert the first character to lowercase return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); } + + /// + /// Determines whether [is log level enabled]. + /// + /// The Powertools for AWS Lambda (.NET) configurations. + /// The log level. + /// true if [is log level enabled]; otherwise, false. + internal static bool IsLogLevelEnabled(this IPowertoolsConfigurations powertoolsConfigurations, LogLevel logLevel) + { + return logLevel != LogLevel.None && logLevel >= _config.MinimumLevel; + } + + /// + /// Gets the current configuration. + /// + /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. + internal static LoggerConfiguration CurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations) + { + return _config; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 603dd86d..33abe71c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using AWS.Lambda.Powertools.Logging.Serializers; @@ -34,58 +35,52 @@ internal sealed class PowertoolsLogger : ILogger /// The name /// private readonly string _name; - + /// /// The current configuration /// - private LoggerConfiguration _currentConfig; - - /// - /// The Powertools for AWS Lambda (.NET) configurations - /// private readonly IPowertoolsConfigurations _powertoolsConfigurations; - + /// /// The system wrapper /// private readonly ISystemWrapper _systemWrapper; /// - /// Initializes a new instance of the class. + /// The current scope + /// + internal PowertoolsLoggerScope CurrentScope { get; private set; } + + /// + /// Private constructor - Is initialized on CreateLogger /// /// The name. /// The Powertools for AWS Lambda (.NET) configurations. /// The system wrapper. - /// - public PowertoolsLogger( + private PowertoolsLogger( string name, IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper, - LoggerConfiguration currentConfig) + ISystemWrapper systemWrapper) { - (_name, _powertoolsConfigurations, _systemWrapper, _currentConfig) = (name, - powertoolsConfigurations, systemWrapper, currentConfig); + _name = name; + _powertoolsConfigurations = powertoolsConfigurations; + _systemWrapper = systemWrapper; _powertoolsConfigurations.SetExecutionEnvironment(this); } /// - /// Sets the minimum level. - /// - /// The minimum level. - private LogLevel MinimumLevel => - _currentConfig.MinimumLevel ?? LoggingConstants.DefaultLogLevel; - - /// - /// Sets the service. + /// Initializes a new instance of the class. /// - /// The service. - private string Service => - !string.IsNullOrWhiteSpace(_currentConfig.Service) - ? _currentConfig.Service - : _powertoolsConfigurations.Service; - - internal PowertoolsLoggerScope CurrentScope { get; private set; } + /// The name. + /// The Powertools for AWS Lambda (.NET) configurations. + /// The system wrapper. + internal static PowertoolsLogger CreateLogger(string name, + IPowertoolsConfigurations powertoolsConfigurations, + ISystemWrapper systemWrapper) + { + return new PowertoolsLogger(name, powertoolsConfigurations, systemWrapper); + } /// /// Begins the scope. @@ -107,64 +102,13 @@ internal void EndScope() CurrentScope = null; } - /// - /// Extract provided scope keys - /// - /// The type of the t state. - /// The state. - /// Key/Value pair of provided scope keys - private static Dictionary GetScopeKeys(TState state) - { - var keys = new Dictionary(); - - if (state is null) - return keys; - - switch (state) - { - case IEnumerable> pairs: - { - foreach (var (key, value) in pairs) - { - if (!string.IsNullOrWhiteSpace(key)) - keys.TryAdd(key, value); - } - - break; - } - case IEnumerable> pairs: - { - foreach (var (key, value) in pairs) - { - if (!string.IsNullOrWhiteSpace(key)) - keys.TryAdd(key, value); - } - - break; - } - default: - { - foreach (var property in state.GetType().GetProperties()) - { - keys.TryAdd(property.Name, property.GetValue(state)); - } - - break; - } - } - - return keys; - } - /// /// Determines whether the specified log level is enabled. /// /// The log level. /// bool. - public bool IsEnabled(LogLevel logLevel) - { - return logLevel != LogLevel.None && logLevel >= MinimumLevel; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEnabled(LogLevel logLevel) => _powertoolsConfigurations.IsLogLevelEnabled(logLevel); /// /// Writes a log entry. @@ -194,7 +138,6 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except ? GetLogEntry(logLevel, timestamp, message, exception) : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); - #if NET8_0_OR_GREATER _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); #else @@ -216,16 +159,14 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times // Add Custom Keys foreach (var (key, value) in Logger.GetAllKeys()) + { logEntry.TryAdd(key, value); + } // Add Lambda Context Keys if (LoggingLambdaContext.Instance is not null) { - logEntry.TryAdd(LoggingConstants.KeyFunctionName, LoggingLambdaContext.Instance.FunctionName); - logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, LoggingLambdaContext.Instance.FunctionVersion); - logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, LoggingLambdaContext.Instance.MemoryLimitInMB); - logEntry.TryAdd(LoggingConstants.KeyFunctionArn, LoggingLambdaContext.Instance.InvokedFunctionArn); - logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, LoggingLambdaContext.Instance.AwsRequestId); + AddLambdaContextKeys(logEntry); } // Add Extra Fields @@ -238,31 +179,24 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } - var keyLogLevel = LoggingConstants.KeyLogLevel; - var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); - - // If ALC is enabled and PascalCase we need to convert Level to LogLevel for it to be parsed and sent to CW - if (lambdaLogLevelEnabled && _currentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase) - { - keyLogLevel = "LogLevel"; - } + var keyLogLevel = GetLogLevelKey(); logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o")); logEntry.TryAdd(keyLogLevel, logLevel.ToString()); - logEntry.TryAdd(LoggingConstants.KeyService, Service); + logEntry.TryAdd(LoggingConstants.KeyService, _powertoolsConfigurations.CurrentConfig().Service); logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (_currentConfig.SamplingRate > 0) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate); + if (_powertoolsConfigurations.CurrentConfig().SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, _powertoolsConfigurations.CurrentConfig().SamplingRate); if (exception != null) logEntry.TryAdd(LoggingConstants.KeyException, exception); return logEntry; } - + /// - /// Gets a formatted log entry. + /// Gets a formatted log entry. For custom log formatter /// /// Entry will be written on this level. /// Entry timestamp. @@ -279,11 +213,11 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec { Timestamp = timestamp, Level = logLevel, - Service = Service, + Service = _powertoolsConfigurations.CurrentConfig().Service, Name = _name, Message = message, Exception = exception, - SamplingRate = _currentConfig.SamplingRate, + SamplingRate = _powertoolsConfigurations.CurrentConfig().SamplingRate, }; var extraKeys = new Dictionary(); @@ -324,14 +258,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec // Add Lambda Context Keys if (LoggingLambdaContext.Instance is not null) { - logEntry.LambdaContext = new LogEntryLambdaContext - { - FunctionName = LoggingLambdaContext.Instance.FunctionName, - FunctionVersion = LoggingLambdaContext.Instance.FunctionVersion, - MemoryLimitInMB = LoggingLambdaContext.Instance.MemoryLimitInMB, - InvokedFunctionArn = LoggingLambdaContext.Instance.InvokedFunctionArn, - AwsRequestId = LoggingLambdaContext.Instance.AwsRequestId, - }; + logEntry.LambdaContext = CreateLambdaContext(); } try @@ -352,15 +279,6 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec } } - - /// - /// Clears the configuration. - /// - internal void ClearConfig() - { - _currentConfig = null; - } - /// /// Formats message for a log entry. /// @@ -369,6 +287,7 @@ internal void ClearConfig() /// The exception related to this entry. /// The formatted message /// bool + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CustomFormatter(TState state, Exception exception, out object message) { message = null; @@ -396,4 +315,90 @@ private static bool CustomFormatter(TState state, Exception exception, o return true; } + + /// + /// Gets the log level key. + /// + /// System.String. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetLogLevelKey() + { + return _powertoolsConfigurations.LambdaLogLevelEnabled() && + _powertoolsConfigurations.CurrentConfig().LoggerOutputCase == LoggerOutputCase.PascalCase + ? "LogLevel" + : LoggingConstants.KeyLogLevel; + } + + /// + /// Adds the lambda context keys. + /// + /// The log entry. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddLambdaContextKeys(Dictionary logEntry) + { + var context = LoggingLambdaContext.Instance; + logEntry.TryAdd(LoggingConstants.KeyFunctionName, context.FunctionName); + logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, context.FunctionVersion); + logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, context.MemoryLimitInMB); + logEntry.TryAdd(LoggingConstants.KeyFunctionArn, context.InvokedFunctionArn); + logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, context.AwsRequestId); + } + + /// + /// Creates the lambda context. + /// + /// LogEntryLambdaContext. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LogEntryLambdaContext CreateLambdaContext() + { + var context = LoggingLambdaContext.Instance; + return new LogEntryLambdaContext + { + FunctionName = context.FunctionName, + FunctionVersion = context.FunctionVersion, + MemoryLimitInMB = context.MemoryLimitInMB, + InvokedFunctionArn = context.InvokedFunctionArn, + AwsRequestId = context.AwsRequestId, + }; + } + + /// + /// Gets the scope keys. + /// + /// The type of the state. + /// The state. + /// Dictionary<System.String, System.Object>. + private static Dictionary GetScopeKeys(TState state) + { + var keys = new Dictionary(); + + if (state is null) + return keys; + + switch (state) + { + case IEnumerable> stringPairs: + foreach (var (key, value) in stringPairs) + { + if (!string.IsNullOrWhiteSpace(key)) + keys.TryAdd(key, value); + } + break; + case IEnumerable> objectPairs: + foreach (var (key, value) in objectPairs) + { + if (!string.IsNullOrWhiteSpace(key)) + keys.TryAdd(key, value); + } + break; + default: + foreach (var property in state.GetType().GetProperties()) + { + keys.TryAdd(property.Name, property.GetValue(state)); + } + break; + } + + return keys; + } } \ 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 5d0d776c..2c32ddd4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -14,7 +14,9 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; @@ -30,16 +32,29 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; /// internal static class PowertoolsLoggingSerializer { - private static JsonSerializerOptions _serializerOptions; - private static readonly List AdditionalContexts = new List(); private static LoggerOutputCase _currentOutputCase = LoggerOutputCase.SnakeCase; + private static readonly object _lock = new object(); + private static readonly ConcurrentBag AdditionalContexts = + new ConcurrentBag(); /// /// Gets the JsonSerializerOptions instance. /// - internal static JsonSerializerOptions SerializerOptions + internal static JsonSerializerOptions GetSerializerOptions() { - get { return _serializerOptions ??= BuildJsonSerializerOptions(); } + lock (_lock) + { + var options = BuildJsonSerializerOptions(); + +#if NET8_0_OR_GREATER + foreach (var context in AdditionalContexts) + { + options.TypeInfoResolverChain.Add(context); + } +#endif + + return options; + } } /// @@ -48,17 +63,10 @@ internal static JsonSerializerOptions SerializerOptions /// The case to use for serialization. public static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) { - _currentOutputCase = loggerOutputCase; - var newOptions = BuildJsonSerializerOptions(); - -#if NET8_0_OR_GREATER - foreach (var context in AdditionalContexts) + lock (_lock) { - newOptions.TypeInfoResolverChain.Add(context); + _currentOutputCase = loggerOutputCase; } -#endif - - _serializerOptions = newOptions; } #if NET6_0 @@ -69,7 +77,8 @@ public static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) /// A JSON string representation of the object. public static string Serialize(object value) { - return JsonSerializer.Serialize(value, SerializerOptions); + var options = GetSerializerOptions(); + return JsonSerializer.Serialize(value, options); } #endif @@ -117,7 +126,8 @@ internal static void AddSerializerContext(JsonSerializerContext context) /// The JsonTypeInfo for the specified type, or null if not found. internal static JsonTypeInfo GetTypeInfo(Type type) { - return SerializerOptions.TypeInfoResolver?.GetTypeInfo(type, SerializerOptions); + var options = GetSerializerOptions(); + return options.TypeInfoResolver?.GetTypeInfo(type, options); } #endif diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index c790d76f..7b857523 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -15,136 +15,26 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; -using System.Text.Json; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; -using Amazon.Lambda.CloudWatchEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Utilities; -using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -[assembly: CollectionBehavior(DisableTestParallelization = true)] - namespace AWS.Lambda.Powertools.Logging.Tests.Attributes { - class TestClass - { - [Logging] - public void TestMethod() - { - } - - [Logging(LogLevel = LogLevel.Debug)] - public void TestMethodDebug() - { - } - - [Logging(LogEvent = true)] - public void LogEvent() - { - } - - [Logging(LogEvent = true, LogLevel = LogLevel.Debug)] - public void LogEventDebug() - { - } - - [Logging(ClearState = true)] - public void ClearState() - { - } - - [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] - public void CorrelationApiGatewayProxyRequest(APIGatewayProxyRequest apiGatewayProxyRequest) - { - } - - [Logging(CorrelationIdPath = CorrelationIdPaths.ApplicationLoadBalancer)] - public void CorrelationApplicationLoadBalancerRequest( - ApplicationLoadBalancerRequest applicationLoadBalancerRequest) - { - } - - [Logging(CorrelationIdPath = CorrelationIdPaths.EventBridge)] - public void CorrelationCloudWatchEvent(CloudWatchEvent cwEvent) - { - } - - [Logging(CorrelationIdPath = "/headers/my_request_id_header")] - public void CorrelationIdFromString(TestObject testObject) - { - } - - [Logging(CorrelationIdPath = "/headers/my_request_id_header")] - public void CorrelationIdFromStringSnake(TestObject testObject) - { - } - - [Logging(CorrelationIdPath = "/Headers/MyRequestIdHeader", LoggerOutputCase = LoggerOutputCase.PascalCase)] - public void CorrelationIdFromStringPascal(TestObject testObject) - { - } - - [Logging(CorrelationIdPath = "/headers/myRequestIdHeader", LoggerOutputCase = LoggerOutputCase.CamelCase)] - public void CorrelationIdFromStringCamel(TestObject testObject) - { - } - - [Logging(CorrelationIdPath = "/headers/my_request_id_header")] - public void CorrelationIdFromStringSnakeEnv(TestObject testObject) - { - } - - [Logging(CorrelationIdPath = "/Headers/MyRequestIdHeader")] - public void CorrelationIdFromStringPascalEnv(TestObject testObject) - { - } - - [Logging(CorrelationIdPath = "/headers/myRequestIdHeader")] - public void CorrelationIdFromStringCamelEnv(TestObject testObject) - { - } - - [Logging(Service = "test")] - public void HandlerService() - { - Logger.LogInformation("test"); - } - - [Logging] - public void HandlerServiceEnv() - { - Logger.LogInformation("test"); - } - - [Logging(SamplingRate = 0.5)] - public void HandlerSamplingRate() - { - Logger.LogInformation("test"); - } - - [Logging] - public void HandlerSamplingRateEnv() - { - Logger.LogInformation("test"); - } - } - - - // [Collection("Sequential")] - public class LoggingAttributeTestWithoutLambdaContext : IDisposable + [Collection("Attribute Tests")] + public class LoggingAttributeTests : IDisposable { private TestClass _testClass; - public LoggingAttributeTestWithoutLambdaContext() + public LoggingAttributeTests() { _testClass = new TestClass(); } @@ -428,33 +318,14 @@ public void When_Setting_SamplingRate_Should_Add_Key() // Arrange var consoleOut = new StringWriter(); SystemWrapper.Instance.SetOut(consoleOut); - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - + // Act _testClass.HandlerSamplingRate(); - - // Assert - - var st = consoleOut.ToString(); - Assert.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\",\"samplingRate\":0.5", st); - } - [Fact] - public void When_Setting_Env_SamplingRate_Should_Add_Key() - { - // Arrange - var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", "0.5"); - - // Act - _testClass.HandlerSamplingRateEnv(); - // Assert - - var st = consoleOut.ToString(); - Assert.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\",\"samplingRate\":0.5", st); + + var st = consoleOut.ToString().Split(Environment.NewLine); + Assert.Contains("\"message\":\"test\",\"samplingRate\":0.5", st[st.Length -2]); } [Fact] @@ -463,42 +334,163 @@ public void When_Setting_Service_Should_Update_Key() // Arrange var consoleOut = new StringWriter(); SystemWrapper.Instance.SetOut(consoleOut); - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - + // Act _testClass.HandlerService(); - - // Assert - - var st = consoleOut.ToString(); - Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); - } - [Fact] - public void When_Setting_Env_Service_Should_Update_Key() - { - // Arrange - var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "service name"); - - // Act - _testClass.HandlerServiceEnv(); - // Assert - + var st = consoleOut.ToString(); - Assert.Contains("\"level\":\"Information\",\"service\":\"service name\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); } public void Dispose() { - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); - + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); + LoggingAspect.ResetForTest(); } } + // + // [Collection("Attribute Tests")] + // public class LoggingAttributeTestsEnvironmentVariables : IDisposable + // { + // [Fact] + // public void When_Setting_Env_Service_Should_Update_Key() + // { + // // Arrange + // var consoleOut = new StringWriter(); + // SystemWrapper.Instance.SetOut(consoleOut); + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "service name"); + // + // // Act + // var testClass = new TestClass(); + // testClass.HandlerServiceEnv(); + // + // // Assert + // + // var st = consoleOut.ToString(); + // Assert.Contains("\"level\":\"Information\",\"service\":\"service name\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + // } + // + // // [Fact] + // // public void When_Setting_Env_SamplingRate_Should_Add_Key() + // // { + // // // Arrange + // // var consoleOut = new StringWriter(); + // // SystemWrapper.Instance.SetOut(consoleOut); + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", "0.5"); + // // + // // // Act + // // var testClass = new TestClass(); + // // testClass.HandlerServiceEnv(); + // // + // // // Assert + // // + // // var st = consoleOut.ToString(); + // // Assert.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\",\"samplingRate\":0.5", st); + // // } + // // + // // [Fact] + // // public void When_Setting_SamplingRate_Should_Add_Key() + // // { + // // // Arrange + // // var consoleOut = new StringWriter(); + // // SystemWrapper.Instance.SetOut(consoleOut); + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + // // + // // // Act + // // var testClass = new TestClass(); + // // testClass.HandlerSamplingRate(); + // // + // // // Assert + // // + // // var st = consoleOut.ToString(); + // // Assert.Contains("\"message\":\"test\",\"samplingRate\":0.5", st); + // // } + // + // [Fact] + // public void When_Setting_Service_Should_Update_Key() + // { + // // Arrange + // var consoleOut = new StringWriter(); + // SystemWrapper.Instance.SetOut(consoleOut); + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + // + // // Act + // var testClass = new TestClass(); + // testClass.HandlerService(); + // + // // Assert + // + // var st = consoleOut.ToString(); + // Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + // } + // + // public void Dispose() + // { + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + // // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); + // + // LoggingAspect.ResetForTest(); + // } + // } + // + // [Collection("Attribute Tests")] + // public class LoggingAttributeTestsSampleRating : IDisposable + // { + // [Fact] + // public void When_Setting_Env_SamplingRate_Should_Add_Key() + // { + // // Arrange + // var consoleOut = new StringWriter(); + // SystemWrapper.Instance.SetOut(consoleOut); + // + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", "0.5"); + // + // var x = PowertoolsConfigurations.Instance.GetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE"); + // + // // Act + // var testClass = new TestClass(); + // testClass.HandlerServiceEnv(); + // + // // Assert + // + // var st = consoleOut.ToString(); + // Assert.Contains("\"samplingRate\":0.5", st); + // } + // + // [Fact] + // public void When_Setting_SamplingRate_Should_Add_Key() + // { + // // Arrange + // var consoleOut = new StringWriter(); + // SystemWrapper.Instance.SetOut(consoleOut); + // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + // + // // Act + // var testClass = new TestClass(); + // testClass.HandlerSamplingRate(); + // + // // Assert + // + // var st = consoleOut.ToString(); + // Assert.Contains("\"message\":\"test\",\"samplingRate\":0.5", st); + // } + // + // public void Dispose() + // { + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + // // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); + // + // LoggingAspect.ResetForTest(); + // } + // } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs new file mode 100644 index 00000000..47706964 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs @@ -0,0 +1,126 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.CloudWatchEvents; +using Amazon.Lambda.CloudWatchEvents.S3Events; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; + +class TestClass +{ + [Logging] + public void TestMethod() + { + } + + [Logging(LogLevel = LogLevel.Debug)] + public void TestMethodDebug() + { + } + + [Logging(LogEvent = true)] + public void LogEvent() + { + } + + [Logging(LogEvent = true, LogLevel = LogLevel.Debug)] + public void LogEventDebug() + { + } + + [Logging(ClearState = true)] + public void ClearState() + { + } + + [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public void CorrelationApiGatewayProxyRequest(APIGatewayProxyRequest apiGatewayProxyRequest) + { + } + + [Logging(CorrelationIdPath = CorrelationIdPaths.ApplicationLoadBalancer)] + public void CorrelationApplicationLoadBalancerRequest( + ApplicationLoadBalancerRequest applicationLoadBalancerRequest) + { + } + + [Logging(CorrelationIdPath = CorrelationIdPaths.EventBridge)] + public void CorrelationCloudWatchEvent(CloudWatchEvent cwEvent) + { + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public void CorrelationIdFromString(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public void CorrelationIdFromStringSnake(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/Headers/MyRequestIdHeader", LoggerOutputCase = LoggerOutputCase.PascalCase)] + public void CorrelationIdFromStringPascal(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/myRequestIdHeader", LoggerOutputCase = LoggerOutputCase.CamelCase)] + public void CorrelationIdFromStringCamel(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public void CorrelationIdFromStringSnakeEnv(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/Headers/MyRequestIdHeader")] + public void CorrelationIdFromStringPascalEnv(TestObject testObject) + { + } + + [Logging(CorrelationIdPath = "/headers/myRequestIdHeader")] + public void CorrelationIdFromStringCamelEnv(TestObject testObject) + { + } + + [Logging(Service = "test", LoggerOutputCase = LoggerOutputCase.CamelCase)] + public void HandlerService() + { + Logger.LogInformation("test"); + } + + [Logging] + public void HandlerServiceEnv() + { + Logger.LogInformation("test"); + } + + [Logging(SamplingRate = 0.5, LoggerOutputCase = LoggerOutputCase.CamelCase)] + public void HandlerSamplingRate() + { + Logger.LogInformation("test"); + } + + [Logging] + public void HandlerSamplingRateEnv() + { + Logger.LogInformation("test"); + } +} \ No newline at end of file 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 b44ec4e3..e50cb00e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -190,19 +190,23 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() var configurations = Substitute.For(); configurations.Service.Returns(service); + configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); + configurations.LogLevel.Returns(LogLevel.Information.ToString()); var logFormatter = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ReturnsNullForAnyArgs(); Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = LogLevel.Information, - LoggerOutputCase = LoggerOutputCase.PascalCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.PascalCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act void Act() => logger.LogInformation(message); @@ -231,19 +235,23 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() var configurations = Substitute.For(); configurations.Service.Returns(service); + configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); + configurations.LogLevel.Returns(LogLevel.Information.ToString()); var logFormatter = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ThrowsForAnyArgs(new Exception(errorMessage)); Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = LogLevel.Information, - LoggerOutputCase = LoggerOutputCase.PascalCase - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.PascalCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act void Act() => logger.LogInformation(message); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index b99bf129..688f42c7 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -47,13 +47,17 @@ private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); + configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); + configurations.LogLevel.Returns(minimumLevel.ToString()); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = minimumLevel - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); switch (logLevel) { @@ -99,13 +103,17 @@ private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logL // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); + configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); + configurations.LogLevel.Returns(minimumLevel.ToString()); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = minimumLevel - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = minimumLevel + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); switch (logLevel) { @@ -556,12 +564,14 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -668,12 +678,14 @@ public void BeginScope_WhenScopeIsObject_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = logLevel - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = logLevel + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new { @@ -709,12 +721,14 @@ public void BeginScope_WhenScopeIsObjectDictionary_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = logLevel - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = logLevel + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary { @@ -750,12 +764,14 @@ public void BeginScope_WhenScopeIsStringDictionary_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = service, - MinimumLevel = logLevel - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = service, + MinimumLevel = logLevel + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary { @@ -1046,12 +1062,14 @@ public void Log_WhenException_LogsExceptionDetails() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); try { @@ -1089,12 +1107,14 @@ public void Log_WhenNestedException_LogsExceptionDetails() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); try { @@ -1169,12 +1189,14 @@ public void Log_WhenMemoryStream_LogsBase64String() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); @@ -1209,12 +1231,14 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); @@ -1238,15 +1262,17 @@ public void Log_Set_Execution_Environment_Context() env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); // Act - var wrapper = new SystemWrapper(env); - var conf = new PowertoolsConfigurations(wrapper); + var systemWrapper = new SystemWrapper(env); + var configurations = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, conf, wrapper, - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); // Assert @@ -1509,12 +1535,16 @@ public void Log_CamelCase_Outputs_Level_When_AWS_Lambda_Log_Level_Enabled(Logger environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Info"); var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); - var logger = new PowertoolsLogger(loggerName, configuration, systemWrapper, - new LoggerConfiguration - { - LoggerOutputCase = casing - }); + var configurations = new PowertoolsConfigurations(systemWrapper); + configurations.LoggerOutputCase.Returns(casing.ToString()); + + var loggerConfiguration = new LoggerConfiguration + { + LoggerOutputCase = casing + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { 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 2d489dd9..296e2b81 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -28,17 +28,26 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; public class PowertoolsLoggingSerializerTests : IDisposable { + + public PowertoolsLoggingSerializerTests() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); +#if NET8_0_OR_GREATER + PowertoolsLoggingSerializer.ClearContext(); +#endif + } + [Fact] public void SerializerOptions_ShouldNotBeNull() { - var options = PowertoolsLoggingSerializer.SerializerOptions; + var options = PowertoolsLoggingSerializer.GetSerializerOptions(); Assert.NotNull(options); } [Fact] public void SerializerOptions_ShouldHaveCorrectDefaultSettings() { - var options = PowertoolsLoggingSerializer.SerializerOptions; + var options = PowertoolsLoggingSerializer.GetSerializerOptions(); Assert.Collection(options.Converters, converter => Assert.IsType(converter), @@ -110,7 +119,7 @@ public void Serialize_ShouldHandleNestedObjects() } }; - var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.SerializerOptions); + var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); Assert.Contains("\"cold_start\":true", json); Assert.Contains("\"nested_object\":{\"property_name\":\"Value\"}", json); } @@ -122,7 +131,7 @@ public void Serialize_ShouldHandleEnumValues() { Level = LogLevel.Error }; - var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.SerializerOptions); + var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); Assert.Contains("\"level\":\"Error\"", json); } @@ -155,7 +164,7 @@ private string SerializeTestObject(LoggerOutputCase? outputCase) } LogEntry testObject = new LogEntry { ColdStart = true }; - return JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.SerializerOptions); + return JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); } public void Dispose() diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/TestSetup.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/TestSetup.cs new file mode 100644 index 00000000..26f0e213 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/TestSetup.cs @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs index 9293f6e1..073faa60 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs @@ -49,7 +49,7 @@ public void LogLine(string value) public double GetRandom() { - throw new System.NotImplementedException(); + return 0.1; } public void SetEnvironmentVariable(string variable, string value) From 68395c35590c675deaba00bc9a6f1f122d711ca2 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:18:49 +0100 Subject: [PATCH 15/32] fix sonar and cleanup --- .../Internal/LoggingAspect.cs | 94 +++++++++---------- .../PowertoolsConfigurationsExtension.cs | 2 +- 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index c6992dfb..a322cbf1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -123,61 +123,55 @@ public void OnEntry( // Called before the method var trigger = triggers.OfType().First(); - var eventArgs = new AspectEventArgs - { - Instance = instance, - Type = hostType, - Method = method, - Name = name, - Args = args, - ReturnType = returnType, - Triggers = triggers - }; - - _config = new LoggerConfiguration + try { - Service = trigger.Service, - LoggerOutputCase = trigger.LoggerOutputCase, - SamplingRate = trigger.SamplingRate - }; + var eventArgs = new AspectEventArgs + { + Instance = instance, + Type = hostType, + Method = method, + Name = name, + Args = args, + ReturnType = returnType, + Triggers = triggers + }; + + _config = new LoggerConfiguration + { + Service = trigger.Service, + LoggerOutputCase = trigger.LoggerOutputCase, + SamplingRate = trigger.SamplingRate + }; - _logLevel = trigger.LogLevel; - _logEvent = trigger.LogEvent; - _correlationIdPath = trigger.CorrelationIdPath; - _clearState = trigger.ClearState; + _logLevel = trigger.LogLevel; + _logEvent = trigger.LogEvent; + _correlationIdPath = trigger.CorrelationIdPath; + _clearState = trigger.ClearState; - Logger.LoggerProvider ??= new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); + Logger.LoggerProvider ??= new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); - if (!_initializeContext) - return; + if (!_initializeContext) + return; - Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); - _isColdStart = false; - _initializeContext = false; - _isContextInitialized = true; + _isColdStart = false; + _initializeContext = false; + _isContextInitialized = true; - var eventObject = eventArgs.Args.FirstOrDefault(); - CaptureXrayTraceId(); - CaptureLambdaContext(eventArgs); - CaptureCorrelationId(eventObject); - if (_logEvent ?? _powertoolsConfigurations.LoggerLogEvent) - LogEvent(eventObject); - } - - /// - /// Called when [exception]. - /// - /// - /// The instance containing the - /// event data. - /// - /// The exception. - public void OnException(AspectEventArgs eventArgs, Exception exception) - { - // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: - // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later - ExceptionDispatchInfo.Capture(exception).Throw(); + var eventObject = eventArgs.Args.FirstOrDefault(); + CaptureXrayTraceId(); + CaptureLambdaContext(eventArgs); + CaptureCorrelationId(eventObject); + if (_logEvent ?? _powertoolsConfigurations.LoggerLogEvent) + LogEvent(eventObject); + } + catch (Exception exception) + { + // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: + // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later + ExceptionDispatchInfo.Capture(exception).Throw(); + } } /// @@ -274,7 +268,8 @@ private void CaptureCorrelationId(object eventArg) for (var i = 0; i < correlationIdPaths.Length; i++) { // For casing parsing to be removed from Logging v2 when we get rid of outputcase - var pathWithOutputCase = _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); + var pathWithOutputCase = + _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; @@ -344,6 +339,5 @@ internal static void ResetForTest() LoggingLambdaContext.Clear(); Logger.LoggerProvider = null; Logger.RemoveAllKeys(); - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 58400109..a597a809 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -138,7 +138,7 @@ internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsC /// private static void SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, LogLevel minLogLevel) { - double samplingRate = _config.SamplingRate == 0 ? powertoolsConfigurations.LoggerSampleRate : _config.SamplingRate; + var samplingRate = _config.SamplingRate > 0 ? _config.SamplingRate : powertoolsConfigurations.LoggerSampleRate; samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, systemWrapper); _config.SamplingRate = samplingRate; From 681de150b373e4670de0fa2de6b576a5c0252410 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:36:04 +0100 Subject: [PATCH 16/32] fix sonar issues --- .../Internal/LoggingAspect.cs | 10 +- .../Internal/LoggingAspectFactory.cs | 2 +- .../PowertoolsConfigurationsExtension.cs | 1 - .../LoggingAttribute.cs | 26 --- .../Attributes/LoggingAttributeTest.cs | 148 +----------------- 5 files changed, 6 insertions(+), 181 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index a322cbf1..04f96109 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -37,12 +37,12 @@ public class LoggingAspect /// /// The is cold start /// - private static bool _isColdStart = true; + private bool _isColdStart = true; /// /// The initialize context /// - private static bool _initializeContext = true; + private bool _initializeContext = true; /// /// Clear state? @@ -208,9 +208,7 @@ private void CaptureXrayTraceId() return; xRayTraceId = xRayTraceId - .Split(';', StringSplitOptions.RemoveEmptyEntries) - .First() - .Replace("Root=", ""); + .Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", ""); Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xRayTraceId); } @@ -334,8 +332,6 @@ private void LogEvent(object eventArg) /// internal static void ResetForTest() { - _isColdStart = true; - _initializeContext = true; LoggingLambdaContext.Clear(); Logger.LoggerProvider = null; Logger.RemoveAllKeys(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs index c575e8db..5feae3cf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -21,7 +21,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// Class LoggingAspectFactory. For "dependency inject" Configuration and SystemWrapper to Aspect /// -internal class LoggingAspectFactory +internal static class LoggingAspectFactory { /// /// Get an instance of the LoggingAspect class. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index a597a809..9775897c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -132,7 +132,6 @@ internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsC /// Set sampling rate /// /// - /// /// /// /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 9fdd2511..98897bf3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -15,7 +15,6 @@ using System; using AspectInjector.Broker; -using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -190,29 +189,4 @@ public bool LogEvent /// /// The log level. public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; - - // /// - // /// Creates the handler. - // /// - // /// IMethodAspectHandler. - // protected override IMethodAspectHandler CreateHandler() - // { - // var config = new LoggerConfiguration - // { - // Service = Service, - // LoggerOutputCase = LoggerOutputCase, - // SamplingRate = SamplingRate, - // }; - // - // return new LoggingAspect - // ( - // config, - // LogLevel, - // LogEvent, - // CorrelationIdPath, - // ClearState, - // PowertoolsConfigurations.Instance, - // SystemWrapper.Instance - // ); - // } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 7b857523..cb968fce 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -55,7 +55,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() Assert.NotNull(Logger.LoggerProvider); Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); + //Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); @@ -81,7 +81,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu Assert.NotNull(Logger.LoggerProvider); Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); + //Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); @@ -346,151 +346,7 @@ public void When_Setting_Service_Should_Update_Key() public void Dispose() { - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); - // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); - LoggingAspect.ResetForTest(); } } - // - // [Collection("Attribute Tests")] - // public class LoggingAttributeTestsEnvironmentVariables : IDisposable - // { - // [Fact] - // public void When_Setting_Env_Service_Should_Update_Key() - // { - // // Arrange - // var consoleOut = new StringWriter(); - // SystemWrapper.Instance.SetOut(consoleOut); - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "service name"); - // - // // Act - // var testClass = new TestClass(); - // testClass.HandlerServiceEnv(); - // - // // Assert - // - // var st = consoleOut.ToString(); - // Assert.Contains("\"level\":\"Information\",\"service\":\"service name\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); - // } - // - // // [Fact] - // // public void When_Setting_Env_SamplingRate_Should_Add_Key() - // // { - // // // Arrange - // // var consoleOut = new StringWriter(); - // // SystemWrapper.Instance.SetOut(consoleOut); - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", "0.5"); - // // - // // // Act - // // var testClass = new TestClass(); - // // testClass.HandlerServiceEnv(); - // // - // // // Assert - // // - // // var st = consoleOut.ToString(); - // // Assert.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\",\"samplingRate\":0.5", st); - // // } - // // - // // [Fact] - // // public void When_Setting_SamplingRate_Should_Add_Key() - // // { - // // // Arrange - // // var consoleOut = new StringWriter(); - // // SystemWrapper.Instance.SetOut(consoleOut); - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - // // - // // // Act - // // var testClass = new TestClass(); - // // testClass.HandlerSamplingRate(); - // // - // // // Assert - // // - // // var st = consoleOut.ToString(); - // // Assert.Contains("\"message\":\"test\",\"samplingRate\":0.5", st); - // // } - // - // [Fact] - // public void When_Setting_Service_Should_Update_Key() - // { - // // Arrange - // var consoleOut = new StringWriter(); - // SystemWrapper.Instance.SetOut(consoleOut); - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - // - // // Act - // var testClass = new TestClass(); - // testClass.HandlerService(); - // - // // Assert - // - // var st = consoleOut.ToString(); - // Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); - // } - // - // public void Dispose() - // { - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); - // // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); - // - // LoggingAspect.ResetForTest(); - // } - // } - // - // [Collection("Attribute Tests")] - // public class LoggingAttributeTestsSampleRating : IDisposable - // { - // [Fact] - // public void When_Setting_Env_SamplingRate_Should_Add_Key() - // { - // // Arrange - // var consoleOut = new StringWriter(); - // SystemWrapper.Instance.SetOut(consoleOut); - // - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", "0.5"); - // - // var x = PowertoolsConfigurations.Instance.GetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE"); - // - // // Act - // var testClass = new TestClass(); - // testClass.HandlerServiceEnv(); - // - // // Assert - // - // var st = consoleOut.ToString(); - // Assert.Contains("\"samplingRate\":0.5", st); - // } - // - // [Fact] - // public void When_Setting_SamplingRate_Should_Add_Key() - // { - // // Arrange - // var consoleOut = new StringWriter(); - // SystemWrapper.Instance.SetOut(consoleOut); - // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - // - // // Act - // var testClass = new TestClass(); - // testClass.HandlerSamplingRate(); - // - // // Assert - // - // var st = consoleOut.ToString(); - // Assert.Contains("\"message\":\"test\",\"samplingRate\":0.5", st); - // } - // - // public void Dispose() - // { - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); - // // Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); - // // Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_SAMPLE_RATE", null); - // - // LoggingAspect.ResetForTest(); - // } - // } } \ No newline at end of file From a18f29e6bd4daaf3618d68c8bb0b8f8b1975211e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:48:19 +0100 Subject: [PATCH 17/32] more tests --- .../Internal/LoggingAspect.cs | 10 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 5 + .../LoggingAttribute.cs | 22 +- .../Attributes/LoggerAspectTests.cs | 211 ++++++++++++++++++ .../Attributes/LoggingAttributeTest.cs | 63 +++++- .../Attributes/TestClass.cs | 17 +- .../Utilities/SystemWrapperMock.cs | 2 +- .../Utilities/TestJsonContext.cs | 2 + 8 files changed, 296 insertions(+), 36 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 04f96109..da236776 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -54,11 +54,6 @@ public class LoggingAspect /// private string _correlationIdPath; - /// - /// The log event - /// - private bool? _logEvent; - /// /// The log level /// @@ -144,7 +139,7 @@ public void OnEntry( }; _logLevel = trigger.LogLevel; - _logEvent = trigger.LogEvent; + var logEvent = trigger.LogEvent; _correlationIdPath = trigger.CorrelationIdPath; _clearState = trigger.ClearState; @@ -163,7 +158,7 @@ public void OnEntry( CaptureXrayTraceId(); CaptureLambdaContext(eventArgs); CaptureCorrelationId(eventObject); - if (_logEvent ?? _powertoolsConfigurations.LoggerLogEvent) + if (logEvent || _powertoolsConfigurations.LoggerLogEvent) LogEvent(eventObject); } catch (Exception exception) @@ -335,5 +330,6 @@ internal static void ResetForTest() LoggingLambdaContext.Clear(); Logger.LoggerProvider = null; Logger.RemoveAllKeys(); + Logger.Dispose(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 472b00bf..224ca4c6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -142,6 +142,11 @@ internal static void RemoveAllKeys() { Scope.Clear(); } + + internal static void Dispose() + { + _loggerInstance = null; + } #endregion diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 98897bf3..95fec953 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -119,16 +119,6 @@ namespace AWS.Lambda.Powertools.Logging; [Injection(typeof(LoggingAspect))] public class LoggingAttribute : Attribute { - /// - /// The log event - /// - private bool? _logEvent; - - /// - /// The log level - /// - private LogLevel? _logLevel; - /// /// Service name is used for logging. /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. @@ -141,11 +131,7 @@ public class LoggingAttribute : Attribute /// This can be also set using the environment variable POWERTOOLS_LOG_LEVEL. /// /// The log level. - public LogLevel LogLevel - { - get => _logLevel ?? LoggingConstants.DefaultLogLevel; - set => _logLevel = value; - } + public LogLevel LogLevel{ get; set; } = LoggingConstants.DefaultLogLevel; /// /// Dynamically set a percentage of logs to DEBUG level. @@ -160,11 +146,7 @@ public LogLevel LogLevel /// such as a string or any custom data object. /// /// true if [log event]; otherwise, false. - public bool LogEvent - { - get => _logEvent.GetValueOrDefault(); - set => _logEvent = value; - } + public bool LogEvent { get; set; } /// /// Pointer path to extract correlation id from input parameter. diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs new file mode 100644 index 00000000..2c0de2cd --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -0,0 +1,211 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; + +[Collection("Sequential")] +public class LoggerAspectTests : IDisposable +{ + private ISystemWrapper _mockSystemWrapper; + private readonly IPowertoolsConfigurations _mockPowertoolsConfigurations; + + public LoggerAspectTests() + { + _mockSystemWrapper = Substitute.For(); + _mockPowertoolsConfigurations = Substitute.For(); + } + + [Fact] + public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() + { + // Arrange +#if NET8_0_OR_GREATER + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + + var instance = new object(); + var name = "TestMethod"; + var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; + var hostType = typeof(string); + var method = typeof(TestClass).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + SamplingRate = 0.5, + LogLevel = LogLevel.Information, + LogEvent = true, + CorrelationIdPath = "/age", + ClearState = true + } + }; + + _mockSystemWrapper.GetRandom().Returns(0.7); + + // Act + var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + + // Assert + _mockSystemWrapper.Received().LogLine(Arg.Is(s => + s.Contains( + "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") + && s.Contains("\"CorrelationId\":\"20\"") + )); + } + + [Fact] + public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() + { + // Arrange +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + + var instance = new object(); + var name = "TestMethod"; + var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; + var hostType = typeof(string); + var method = typeof(TestClass).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogLevel = LogLevel.Information, + LogEvent = false, + CorrelationIdPath = "/age", + ClearState = true + } + }; + + // Env returns true + _mockPowertoolsConfigurations.LoggerLogEvent.Returns(true); + + // Act + var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + + // Assert + var config = _mockPowertoolsConfigurations.CurrentConfig(); + Assert.NotNull(Logger.LoggerProvider); + Assert.Equal("TestService", config.Service); + Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); + Assert.Equal(0, config.SamplingRate); + + _mockSystemWrapper.Received().LogLine(Arg.Is(s => + s.Contains( + "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}}") + && s.Contains("\"CorrelationId\":\"20\"") + )); + } + + [Fact] + public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() + { + // Arrange +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + + var instance = new object(); + var name = "TestMethod"; + var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; + var hostType = typeof(string); + var method = typeof(TestClass).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogLevel = LogLevel.Information, + LogEvent = true, + CorrelationIdPath = "/age", + ClearState = true + } + }; + + // Env returns true + _mockPowertoolsConfigurations.LoggerSampleRate.Returns(0.5); + + // Act + var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + + // Assert + var config = _mockPowertoolsConfigurations.CurrentConfig(); + Assert.NotNull(Logger.LoggerProvider); + Assert.Equal("TestService", config.Service); + Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); + Assert.Equal(0.5, config.SamplingRate); + + _mockSystemWrapper.Received().LogLine(Arg.Is(s => + s.Contains( + "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") + && s.Contains("\"CorrelationId\":\"20\"") + )); + } + + [Fact] + public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() + { + // Arrange + var eventObject = new { testData = "test-data" }; + var triggers = new Attribute[] + { + new LoggingAttribute + { + LogEvent = true + } + }; + + // Act + + var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + loggingAspect.OnEntry(null, null, new object[] { eventObject }, null, null, null, triggers); + + // Assert + _mockSystemWrapper.Received().LogLine(Arg.Is(s => + s.Contains( + "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") + )); + } + + public void Dispose() + { + LoggingAspect.ResetForTest(); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index cb968fce..71506913 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -20,6 +20,7 @@ using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; +using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; @@ -29,7 +30,7 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes { - [Collection("Attribute Tests")] + [Collection("Sequential")] public class LoggingAttributeTests : IDisposable { private TestClass _testClass; @@ -102,7 +103,57 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.LogEvent(); + _testClass.LogEventNoArgs(); + + consoleOut.DidNotReceive().WriteLine( + Arg.Any() + ); + } + + [Fact] + public void OnEntry_WhenEventArgExist_LogEvent() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + var context = new TestLambdaContext() + { + FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" + }; + + // Act + _testClass.LogEvent(context); + + consoleOut.Received(1).WriteLine( + Arg.Is(i => i.Contains("FunctionName\":\"PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1")) + ); + } + + [Fact] + public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); +#endif + var context = new TestLambdaContext() + { + FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" + }; + + // Act + _testClass.LogEventFalse(context); consoleOut.DidNotReceive().WriteLine( Arg.Any() @@ -316,7 +367,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L public void When_Setting_SamplingRate_Should_Add_Key() { // Arrange - var consoleOut = new StringWriter(); + var consoleOut = Substitute.For(); SystemWrapper.Instance.SetOut(consoleOut); // Act @@ -324,8 +375,9 @@ public void When_Setting_SamplingRate_Should_Add_Key() // Assert - var st = consoleOut.ToString().Split(Environment.NewLine); - Assert.Contains("\"message\":\"test\",\"samplingRate\":0.5", st[st.Length -2]); + consoleOut.Received().WriteLine( + Arg.Is(i => i.Contains("\"message\":\"test\",\"samplingRate\":0.5")) + ); } [Fact] @@ -346,6 +398,7 @@ public void When_Setting_Service_Should_Update_Key() public void Dispose() { + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); LoggingAspect.ResetForTest(); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs index 47706964..d3355e8b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs @@ -17,8 +17,9 @@ using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; +using Amazon.Lambda.Core; using AWS.Lambda.Powertools.Logging.Tests.Utilities; -using Microsoft.Extensions.Logging; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; @@ -35,7 +36,17 @@ public void TestMethodDebug() } [Logging(LogEvent = true)] - public void LogEvent() + public void LogEventNoArgs() + { + } + + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase)] + public void LogEvent(ILambdaContext context) + { + } + + [Logging(LogEvent = false)] + public void LogEventFalse(ILambdaContext context) { } @@ -112,7 +123,7 @@ public void HandlerServiceEnv() Logger.LogInformation("test"); } - [Logging(SamplingRate = 0.5, LoggerOutputCase = LoggerOutputCase.CamelCase)] + [Logging(SamplingRate = 0.5, LoggerOutputCase = LoggerOutputCase.CamelCase, LogLevel = LogLevel.Information)] public void HandlerSamplingRate() { Logger.LogInformation("test"); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs index 073faa60..1ab2b94e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs @@ -49,7 +49,7 @@ public void LogLine(string value) public double GetRandom() { - return 0.1; + return 0.7; } public void SetEnvironmentVariable(string variable, string value) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs index 0d69bda6..770d9b00 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs @@ -1,10 +1,12 @@ using System.Text.Json.Serialization; using Amazon.Lambda.CloudWatchEvents.S3Events; +using Amazon.Lambda.TestUtilities; namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; [JsonSerializable(typeof(S3ObjectCreateEvent))] [JsonSerializable(typeof(TestObject))] +[JsonSerializable(typeof(TestLambdaContext))] internal partial class TestJsonContext : JsonSerializerContext { } From e9e71b02efff7a22eacec61e1c0f2d0ce6ec8314 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:04:11 +0100 Subject: [PATCH 18/32] fix sonar issues --- .../src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs | 2 +- libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index da236776..242fb24f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -330,6 +330,6 @@ internal static void ResetForTest() LoggingLambdaContext.Clear(); Logger.LoggerProvider = null; Logger.RemoveAllKeys(); - Logger.Dispose(); + Logger.ClearLoggerInstance(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 224ca4c6..1c86a8e5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -143,7 +143,7 @@ internal static void RemoveAllKeys() Scope.Clear(); } - internal static void Dispose() + internal static void ClearLoggerInstance() { _loggerInstance = null; } From e92559b7ad2c120d016000782eb5fbb341601462 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:16:31 +0100 Subject: [PATCH 19/32] remove ApiGateway and LoadBalancer Events from serializer --- .../AWS.Lambda.Powertools.Logging.csproj | 2 -- .../LoggingSerializationContext.cs | 19 ------------------- libraries/src/Directory.Packages.props | 2 -- .../Attributes/LoggerAspectTests.cs | 8 ++++---- .../Attributes/LoggingAttributeTest.cs | 12 ++++++------ .../Attributes/TestClass.cs | 2 +- .../PowertoolsLambdaSerializerTests.cs | 14 +++++++------- .../TestJsonContext.cs | 6 +++++- 8 files changed, 23 insertions(+), 42 deletions(-) rename libraries/tests/AWS.Lambda.Powertools.Logging.Tests/{Utilities => Serializers}/TestJsonContext.cs (67%) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj index 3a21c651..081040ef 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj @@ -12,8 +12,6 @@ - - diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs index 022b0a5d..0391c1fb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -17,8 +17,6 @@ using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; -using Amazon.Lambda.APIGatewayEvents; -using Amazon.Lambda.ApplicationLoadBalancerEvents; namespace AWS.Lambda.Powertools.Logging.Serializers; @@ -43,24 +41,7 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Byte[]))] [JsonSerializable(typeof(MemoryStream))] -[JsonSerializable(typeof(APIGatewayProxyRequest))] -[JsonSerializable(typeof(APIGatewayProxyResponse))] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext), - TypeInfoPropertyName = "APIGatewayProxyRequestContext")] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert), - TypeInfoPropertyName = "APIGatewayProxyRequestProxyRequestClientCert")] -[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity), - TypeInfoPropertyName = "APIGatewayProxyRequestClientCertValidity")] -[JsonSerializable(typeof(ApplicationLoadBalancerRequest))] [JsonSerializable(typeof(LogEntry))] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext), - TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestContext")] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert), - TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestProxyRequestClientCert")] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity), - TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestClientCertValidity")] public partial class PowertoolsLoggingSerializationContext : JsonSerializerContext { } diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index 8b94c1c4..49aa51ca 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -4,8 +4,6 @@ - - diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index 2c0de2cd..f75cb309 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -17,7 +17,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; -using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using AWS.Lambda.Powertools.Logging.Tests.Serializers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -42,7 +42,7 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() // Arrange #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif var instance = new object(); @@ -86,7 +86,7 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif var instance = new object(); @@ -136,7 +136,7 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif var instance = new object(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 71506913..73d688a6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -24,7 +24,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; -using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using AWS.Lambda.Powertools.Logging.Tests.Serializers; using NSubstitute; using Xunit; @@ -120,7 +120,7 @@ public void OnEntry_WhenEventArgExist_LogEvent() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif var context = new TestLambdaContext() { @@ -145,7 +145,7 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif var context = new TestLambdaContext() { @@ -202,7 +202,7 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif // Act @@ -263,7 +263,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif // Act @@ -318,7 +318,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); #endif // Act diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs index d3355e8b..c98b1264 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs @@ -18,7 +18,7 @@ using Amazon.Lambda.CloudWatchEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; using Amazon.Lambda.Core; -using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using AWS.Lambda.Powertools.Logging.Tests.Serializers; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index cc82efb5..521ceda6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -32,7 +32,7 @@ public class PowertoolsLambdaSerializerTests : IDisposable public void Constructor_ShouldNotThrowException() { // Arrange & Act & Assert - var exception = Record.Exception(() => new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default)); + var exception = Record.Exception(() => new PowertoolsLambdaSerializer(TestJsonContext.Default)); Assert.Null(exception); } @@ -40,7 +40,7 @@ public void Constructor_ShouldNotThrowException() public void Constructor_ShouldAddCustomerContext() { // Arrange - var customerContext = new Utilities.TestJsonContext(); + var customerContext = new TestJsonContext(); // Act var serializer = new PowertoolsLambdaSerializer(customerContext); @@ -56,7 +56,7 @@ public void Constructor_ShouldAddCustomerContext() public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCase outputCase,string json, string expectedName, int expectedAge) { // Arrange - var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase); var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); @@ -74,7 +74,7 @@ public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCas public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() { // Arrange - var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); @@ -89,7 +89,7 @@ public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() public void Serialize_ValidObject_ShouldSerializeToStream() { // Arrange - var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); @@ -110,7 +110,7 @@ public void Serialize_ValidObject_ShouldSerializeToStream() public void Serialize_InvalidType_ShouldThrowInvalidOperationException() { // Arrange - var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); var unknownObject = new UnknownType(); var stream = new MemoryStream(); @@ -124,7 +124,7 @@ private class UnknownType { } public void Deserialize_NonSeekableStream_ShouldDeserializeCorrectly() { // Arrange - var serializer = new PowertoolsLambdaSerializer(Utilities.TestJsonContext.Default); + var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); var json = "{\"full_name\":\"John\",\"age\":30}"; var jsonBytes = Encoding.UTF8.GetBytes(json); var nonSeekableStream = new NonSeekableStream(jsonBytes); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs similarity index 67% rename from libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs rename to libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs index 770d9b00..728e1653 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/TestJsonContext.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs @@ -1,12 +1,16 @@ using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; using Amazon.Lambda.TestUtilities; -namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; +namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; [JsonSerializable(typeof(S3ObjectCreateEvent))] [JsonSerializable(typeof(TestObject))] [JsonSerializable(typeof(TestLambdaContext))] +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(ApplicationLoadBalancerRequest))] internal partial class TestJsonContext : JsonSerializerContext { } From 67dc1cf7bec46c55b50d649100b263b82f1a7b44 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:08:55 +0100 Subject: [PATCH 20/32] fix LogLevel and default to None when not set. more tests --- .../Internal/LoggingAspect.cs | 11 ++--- .../PowertoolsConfigurationsExtension.cs | 7 ++- .../LoggerConfiguration.cs | 2 +- .../LoggingAttribute.cs | 2 +- .../Attributes/LoggerAspectTests.cs | 44 +++++++++++++++++ .../Attributes/LoggingAttributeTest.cs | 48 +++++++++++++++++++ .../Attributes/TestClass.cs | 24 ++++++---- .../PowertoolsLoggerTest.cs | 38 +++++++-------- 8 files changed, 133 insertions(+), 43 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 242fb24f..eb87ac2a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -54,11 +54,6 @@ public class LoggingAspect /// private string _correlationIdPath; - /// - /// The log level - /// - private LogLevel? _logLevel; - /// /// The Powertools for AWS Lambda (.NET) configurations /// @@ -135,10 +130,10 @@ public void OnEntry( { Service = trigger.Service, LoggerOutputCase = trigger.LoggerOutputCase, - SamplingRate = trigger.SamplingRate + SamplingRate = trigger.SamplingRate, + MinimumLevel = trigger.LogLevel }; - _logLevel = trigger.LogLevel; var logEvent = trigger.LogEvent; _correlationIdPath = trigger.CorrelationIdPath; _clearState = trigger.ClearState; @@ -190,7 +185,7 @@ public void OnExit() /// true if this instance is debug; otherwise, false. private bool IsDebug() { - return LogLevel.Debug >= _powertoolsConfigurations.GetLogLevel(_logLevel); + return LogLevel.Debug >= _powertoolsConfigurations.GetLogLevel(_config.MinimumLevel); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 9775897c..4e932684 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -50,11 +50,10 @@ internal static class PowertoolsConfigurationsExtension /// The Powertools for AWS Lambda (.NET) configurations. /// The log level. /// LogLevel. - internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations, - LogLevel? logLevel = null) + internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations, LogLevel logLevel = LogLevel.None) { - if (logLevel.HasValue) - return logLevel.Value; + if (logLevel != LogLevel.None) + return logLevel; if (Enum.TryParse((powertoolsConfigurations.LogLevel ?? "").Trim(), true, out LogLevel result)) return result; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs index 30cad5ea..827572d1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs @@ -38,7 +38,7 @@ public class LoggerConfiguration : IOptions /// This can be also set using the environment variable POWERTOOLS_LOG_LEVEL. /// /// The minimum level. - public LogLevel? MinimumLevel { get; set; } + public LogLevel MinimumLevel { get; set; } = LogLevel.None; /// /// Dynamically set a percentage of logs to DEBUG level. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 95fec953..4a5da930 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -131,7 +131,7 @@ public class LoggingAttribute : Attribute /// This can be also set using the environment variable POWERTOOLS_LOG_LEVEL. /// /// The log level. - public LogLevel LogLevel{ get; set; } = LoggingConstants.DefaultLogLevel; + public LogLevel LogLevel{ get; set; } = LogLevel.None; /// /// Dynamically set a percentage of logs to DEBUG level. diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index f75cb309..38224414 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -203,6 +203,50 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") )); } + + [Fact] + public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable() + { + // Arrange +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); +#endif + + var instance = new object(); + var name = "TestMethod"; + var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; + var hostType = typeof(string); + var method = typeof(TestClass).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + //LogLevel = LogLevel.Information, + LogEvent = true, + CorrelationIdPath = "/age" + } + }; + + // Env returns true + _mockPowertoolsConfigurations.LogLevel.Returns(LogLevel.Error.ToString()); + + // Act + var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + + // Assert + var config = _mockPowertoolsConfigurations.CurrentConfig(); + Assert.NotNull(Logger.LoggerProvider); + Assert.Equal("TestService", config.Service); + Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); + + _mockSystemWrapper.DidNotReceive().LogLine(Arg.Any()); + } public void Dispose() { diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 73d688a6..fa2b63e0 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -395,6 +395,54 @@ public void When_Setting_Service_Should_Update_Key() var st = consoleOut.ToString(); Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); } + + [Fact] + public void When_Setting_LogLevel_Should_Update_LogLevel() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.TestLogLevelCritical(); + + // Assert + + var st = consoleOut.ToString(); + Assert.Contains("\"level\":\"Critical\"", st); + } + + [Fact] + public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + var context = new TestLambdaContext() + { + FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" + }; + + // Act + _testClass.TestLogLevelCriticalLogEvent(context); + + // Assert + consoleOut.DidNotReceive().WriteLine(Arg.Any()); + } + + [Fact] + public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_True() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testClass.TestLogEventWithoutContext(); + + // Assert + consoleOut.Received(1).WriteLine(Arg.Is(s => s == "Skipping Event Log because event parameter not found.")); + } public void Dispose() { diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs index c98b1264..beb35c50 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs @@ -117,21 +117,25 @@ public void HandlerService() Logger.LogInformation("test"); } - [Logging] - public void HandlerServiceEnv() - { - Logger.LogInformation("test"); - } - [Logging(SamplingRate = 0.5, LoggerOutputCase = LoggerOutputCase.CamelCase, LogLevel = LogLevel.Information)] public void HandlerSamplingRate() { Logger.LogInformation("test"); } - - [Logging] - public void HandlerSamplingRateEnv() + + [Logging(LogLevel = LogLevel.Critical)] + public void TestLogLevelCritical() + { + Logger.LogCritical("test"); + } + + [Logging(LogLevel = LogLevel.Critical, LogEvent = true)] + public void TestLogLevelCriticalLogEvent(ILambdaContext context) + { + } + + [Logging(LogLevel = LogLevel.Debug, LogEvent = true)] + public void TestLogEventWithoutContext() { - Logger.LogInformation("test"); } } \ 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 688f42c7..b7257d58 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -53,7 +53,7 @@ private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -282,7 +282,7 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; // Act @@ -322,7 +322,7 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; // Act @@ -361,7 +361,7 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; // Act @@ -399,7 +399,7 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; // Act @@ -441,7 +441,7 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null, + MinimumLevel = LogLevel.None, LoggerOutputCase = LoggerOutputCase.CamelCase }; @@ -485,7 +485,7 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -526,7 +526,7 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null, + MinimumLevel = LogLevel.None, LoggerOutputCase = LoggerOutputCase.PascalCase }; @@ -567,7 +567,7 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -606,7 +606,7 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null, + MinimumLevel = LogLevel.None, LoggerOutputCase = LoggerOutputCase.SnakeCase }; @@ -646,7 +646,7 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -1065,7 +1065,7 @@ public void Log_WhenException_LogsExceptionDetails() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -1110,7 +1110,7 @@ public void Log_WhenNestedException_LogsExceptionDetails() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -1153,7 +1153,7 @@ public void Log_WhenByteArray_LogsByteArrayNumbers() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); @@ -1192,7 +1192,7 @@ public void Log_WhenMemoryStream_LogsBase64String() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -1234,7 +1234,7 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -1268,7 +1268,7 @@ public void Log_Set_Execution_Environment_Context() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null + MinimumLevel = LogLevel.None }; var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); @@ -1300,7 +1300,7 @@ public void Log_Should_Serialize_DateOnly() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null, + MinimumLevel = LogLevel.None, LoggerOutputCase = LoggerOutputCase.CamelCase }; @@ -1347,7 +1347,7 @@ public void Log_Should_Serialize_TimeOnly() var loggerConfiguration = new LoggerConfiguration { Service = null, - MinimumLevel = null, + MinimumLevel = LogLevel.None, LoggerOutputCase = LoggerOutputCase.CamelCase }; From 0595e8ef6921ad62b1fa5dd4c94d22459d54d77e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:25:31 +0100 Subject: [PATCH 21/32] fix serialization on first request. the context was not set properly, refactor. fix bug when using logger with no decorator. more tests and new PowertoolsSourceGeneratorSerializer.cs --- .../AWS.Lambda.Powertools.Logging.csproj | 1 + .../Internal/LoggerProvider.cs | 7 ++ .../Internal/LoggingAspect.cs | 1 + .../PowertoolsConfigurationsExtension.cs | 2 +- .../Internal/PowertoolsLogger.cs | 12 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 4 + .../LoggerConfiguration.cs | 2 +- .../LoggingSerializationContext.cs | 2 - .../Serializers/PowertoolsLambdaSerializer.cs | 81 ------------- .../PowertoolsLoggingSerializer.cs | 87 +++++++------ .../PowertoolsSourceGeneratorSerializer.cs | 63 ++++++++++ libraries/src/Directory.Packages.props | 1 + ...AWS.Lambda.Powertools.Logging.Tests.csproj | 1 + .../Attributes/LoggerAspectTests.cs | 85 ++++++++++--- .../Attributes/LoggingAttributeTest.cs | 42 +++++-- .../Attributes/TestClass.cs | 9 +- .../PowertoolsLoggerTest.cs | 8 +- .../PowertoolsLambdaSerializerTests.cs | 114 +++++++++++++----- .../PowertoolsLoggingSerializerTests.cs | 3 +- libraries/tests/Directory.Packages.props | 3 +- 20 files changed, 336 insertions(+), 192 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj index 081040ef..a4a1478f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj @@ -13,6 +13,7 @@ + diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index 1d70b85d..94bb1c0d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -55,6 +55,13 @@ public LoggerProvider(IOptions config, IPowertoolsConfigura _systemWrapper = systemWrapper; _powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper); } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public LoggerProvider(IOptions config) + : this(config, PowertoolsConfigurations.Instance, SystemWrapper.Instance) { } /// /// Creates a new instance. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index eb87ac2a..162c00fd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -256,6 +256,7 @@ private void CaptureCorrelationId(object eventArg) for (var i = 0; i < correlationIdPaths.Length; i++) { // For casing parsing to be removed from Logging v2 when we get rid of outputcase + // without this CorrelationIdPaths.ApiGatewayRest would not work var pathWithOutputCase = _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 4e932684..148bb540 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -99,7 +99,7 @@ internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsC { lock (_lock) { - _config = config; + _config = config ?? new LoggerConfiguration(); var logLevel = powertoolsConfigurations.GetLogLevel(_config.MinimumLevel); var lambdaLogLevel = powertoolsConfigurations.GetLambdaLogLevel(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 33abe71c..8775631e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -35,12 +35,12 @@ internal sealed class PowertoolsLogger : ILogger /// The name /// private readonly string _name; - + /// /// The current configuration /// private readonly IPowertoolsConfigurations _powertoolsConfigurations; - + /// /// The system wrapper /// @@ -155,7 +155,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except private Dictionary GetLogEntry(LogLevel logLevel, DateTime timestamp, object message, Exception exception) { - var logEntry = new Dictionary(StringComparer.Ordinal); + var logEntry = new Dictionary(); // Add Custom Keys foreach (var (key, value) in Logger.GetAllKeys()) @@ -186,7 +186,6 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(LoggingConstants.KeyService, _powertoolsConfigurations.CurrentConfig().Service); logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (_powertoolsConfigurations.CurrentConfig().SamplingRate > 0) logEntry.TryAdd(LoggingConstants.KeySamplingRate, _powertoolsConfigurations.CurrentConfig().SamplingRate); if (exception != null) @@ -194,7 +193,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times return logEntry; } - + /// /// Gets a formatted log entry. For custom log formatter /// @@ -383,6 +382,7 @@ private static Dictionary GetScopeKeys(TState state) if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } + break; case IEnumerable> objectPairs: foreach (var (key, value) in objectPairs) @@ -390,12 +390,14 @@ private static Dictionary GetScopeKeys(TState state) if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } + break; default: foreach (var property in state.GetType().GetProperties()) { keys.TryAdd(property.Name, property.GetValue(state)); } + break; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 1c86a8e5..3127a5e6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -64,6 +65,9 @@ public static ILogger Create(string categoryName) if (string.IsNullOrWhiteSpace(categoryName)) throw new ArgumentNullException(nameof(categoryName)); + // Needed for when using Logger directly with decorator + LoggerProvider ??= new LoggerProvider(null); + return LoggerProvider.CreateLogger(categoryName); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs index 827572d1..aab959af 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs @@ -58,5 +58,5 @@ public class LoggerConfiguration : IOptions /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. /// /// The logger output case. - public LoggerOutputCase LoggerOutputCase { get; set; } + public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs index 0391c1fb..28692b8e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -33,8 +33,6 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; [JsonSerializable(typeof(Double))] [JsonSerializable(typeof(DateOnly))] [JsonSerializable(typeof(TimeOnly))] -[JsonSerializable(typeof(InvalidOperationException))] -[JsonSerializable(typeof(Exception))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(IEnumerable))] diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs deleted file mode 100644 index c1ff586d..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; -using Amazon.Lambda.Core; - -namespace AWS.Lambda.Powertools.Logging.Serializers; - -#if NET8_0_OR_GREATER -/// -/// Provides a custom Lambda serializer that combines multiple JsonSerializerContexts. -/// -public class PowertoolsLambdaSerializer : ILambdaSerializer -{ - /// - /// Initializes a new instance of PowertoolsLambdaSerializer. - /// - /// The customer's JsonSerializerContext. - public PowertoolsLambdaSerializer(JsonSerializerContext customerContext) - { - PowertoolsLoggingSerializer.AddSerializerContext(customerContext); - } - - /// - /// Deserializes the input stream to the specified type. - /// - public T Deserialize(Stream requestStream) - { - Stream streamToUse = requestStream; - if (!requestStream.CanSeek) - { - var ms = new MemoryStream(); - requestStream.CopyTo(ms); - ms.Position = 0; - streamToUse = ms; - } - - var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T)); - if (typeInfo == null) - { - throw new InvalidOperationException( - $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); - } - - return (T)JsonSerializer.Deserialize(streamToUse, typeInfo)!; - } - - - /// - /// Serializes the specified object and writes the result to the output stream. - /// - public void Serialize(T response, Stream responseStream) - { - var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T)); - if (typeInfo == null) - { - throw new InvalidOperationException( - $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); - } - - using var writer = new Utf8JsonWriter(responseStream, new JsonWriterOptions { SkipValidation = true }); - JsonSerializer.Serialize(writer, response, typeInfo); - } -} - -#endif \ 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 2c32ddd4..000e0785 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -21,6 +21,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Amazon.Lambda.Serialization.SystemTextJson; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Converters; using Microsoft.Extensions.Logging; @@ -32,8 +33,9 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; /// internal static class PowertoolsLoggingSerializer { - private static LoggerOutputCase _currentOutputCase = LoggerOutputCase.SnakeCase; - private static readonly object _lock = new object(); + private static LoggerOutputCase _currentOutputCase; + private static JsonSerializerOptions _jsonOptions; + private static readonly ConcurrentBag AdditionalContexts = new ConcurrentBag(); @@ -42,31 +44,16 @@ internal static class PowertoolsLoggingSerializer /// internal static JsonSerializerOptions GetSerializerOptions() { - lock (_lock) - { - var options = BuildJsonSerializerOptions(); - -#if NET8_0_OR_GREATER - foreach (var context in AdditionalContexts) - { - options.TypeInfoResolverChain.Add(context); - } -#endif - - return options; - } + return _jsonOptions ?? BuildJsonSerializerOptions(); } /// /// Configures the naming policy for the serializer. /// /// The case to use for serialization. - public static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) + internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) { - lock (_lock) - { - _currentOutputCase = loggerOutputCase; - } + _currentOutputCase = loggerOutputCase; } #if NET6_0 @@ -75,7 +62,7 @@ public static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) /// /// The object to serialize. /// A JSON string representation of the object. - public static string Serialize(object value) + internal static string Serialize(object value) { var options = GetSerializerOptions(); return JsonSerializer.Serialize(value, options); @@ -91,13 +78,12 @@ public static string Serialize(object value) /// The type of the object to serialize. /// A JSON string representation of the object. /// Thrown when the input type is not known to the serializer. - public static string Serialize(object value, Type inputType) + internal static string Serialize(object value, Type inputType) { var typeInfo = GetTypeInfo(inputType); - if (typeInfo == null) { - throw new InvalidOperationException( + throw new ApplicationException( $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); } @@ -137,48 +123,53 @@ internal static JsonTypeInfo GetTypeInfo(Type type) /// A configured JsonSerializerOptions instance. private static JsonSerializerOptions BuildJsonSerializerOptions() { - var jsonOptions = new JsonSerializerOptions(); + _jsonOptions = new JsonSerializerOptions(); switch (_currentOutputCase) { case LoggerOutputCase.CamelCase: - jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; break; case LoggerOutputCase.PascalCase: - jsonOptions.PropertyNamingPolicy = PascalCaseNamingPolicy.Instance; - jsonOptions.DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance; + _jsonOptions.PropertyNamingPolicy = PascalCaseNamingPolicy.Instance; + _jsonOptions.DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance; break; default: // Snake case - jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; - jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; +#if NET8_0_OR_GREATER + _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; +#else + _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; + _jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; +#endif break; } - jsonOptions.Converters.Add(new ByteArrayConverter()); - jsonOptions.Converters.Add(new ExceptionConverter()); - jsonOptions.Converters.Add(new MemoryStreamConverter()); - jsonOptions.Converters.Add(new ConstantClassConverter()); - jsonOptions.Converters.Add(new DateOnlyConverter()); - jsonOptions.Converters.Add(new TimeOnlyConverter()); + _jsonOptions.Converters.Add(new ByteArrayConverter()); + _jsonOptions.Converters.Add(new ExceptionConverter()); + _jsonOptions.Converters.Add(new MemoryStreamConverter()); + _jsonOptions.Converters.Add(new ConstantClassConverter()); + _jsonOptions.Converters.Add(new DateOnlyConverter()); + _jsonOptions.Converters.Add(new TimeOnlyConverter()); #if NET8_0_OR_GREATER - jsonOptions.Converters.Add(new JsonStringEnumConverter()); + _jsonOptions.Converters.Add(new JsonStringEnumConverter()); #elif NET6_0 - jsonOptions.Converters.Add(new JsonStringEnumConverter()); + _jsonOptions.Converters.Add(new JsonStringEnumConverter()); #endif - jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; - jsonOptions.PropertyNameCaseInsensitive = true; + _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + _jsonOptions.PropertyNameCaseInsensitive = true; #if NET8_0_OR_GREATER - jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); + _jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); foreach (var context in AdditionalContexts) { - jsonOptions.TypeInfoResolverChain.Add(context); + _jsonOptions.TypeInfoResolverChain.Add(context); } #endif - return jsonOptions; + return _jsonOptions; } #if NET8_0_OR_GREATER @@ -192,4 +183,12 @@ internal static void ClearContext() AdditionalContexts.Clear(); } #endif + + /// + /// Clears options for tests + /// + internal static void ClearOptions() + { + _jsonOptions = null; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs new file mode 100644 index 00000000..d780b1a1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#if NET8_0_OR_GREATER + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.Lambda.Serialization.SystemTextJson; + +namespace AWS.Lambda.Powertools.Logging.Serializers; + +/// +public sealed class PowertoolsSourceGeneratorSerializer< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + TSgContext> : SourceGeneratorLambdaJsonSerializer where TSgContext : JsonSerializerContext +{ + /// + /// Constructs instance of serializer. + /// + public PowertoolsSourceGeneratorSerializer() + : this(null) + { + } + + /// + /// Constructs instance of serializer with the option to customize the JsonSerializerOptions after the + /// Amazon.Lambda.Serialization.SystemTextJson's default settings have been applied. + /// + /// + public PowertoolsSourceGeneratorSerializer( + Action customizer) + + { + var options = CreateDefaultJsonSerializationOptions(); + customizer?.Invoke(options); + + var constructor = typeof(TSgContext).GetConstructor(new Type[] { typeof(JsonSerializerOptions) }); + if (constructor == null) + { + throw new ApplicationException( + $"The serializer {typeof(TSgContext).FullName} is missing a constructor that takes in JsonSerializerOptions object"); + } + + var jsonSerializerContext = constructor.Invoke(new object[] { options }) as TSgContext; + PowertoolsLoggingSerializer.AddSerializerContext(jsonSerializerContext); + } +} + +#endif \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index 49aa51ca..56d0fba9 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -4,6 +4,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj index 352e6707..376b4800 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj @@ -13,6 +13,7 @@ + all diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index 38224414..baf00a64 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -33,7 +33,7 @@ public class LoggerAspectTests : IDisposable public LoggerAspectTests() { _mockSystemWrapper = Substitute.For(); - _mockPowertoolsConfigurations = Substitute.For(); + _mockPowertoolsConfigurations = Substitute.For(); } [Fact] @@ -42,7 +42,7 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() // Arrange #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif var instance = new object(); @@ -60,7 +60,7 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() SamplingRate = 0.5, LogLevel = LogLevel.Information, LogEvent = true, - CorrelationIdPath = "/age", + CorrelationIdPath = "/Age", ClearState = true } }; @@ -78,7 +78,7 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() && s.Contains("\"CorrelationId\":\"20\"") )); } - + [Fact] public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() { @@ -86,7 +86,7 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif var instance = new object(); @@ -103,11 +103,11 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() LoggerOutputCase = LoggerOutputCase.PascalCase, LogLevel = LogLevel.Information, LogEvent = false, - CorrelationIdPath = "/age", + CorrelationIdPath = "/Age", ClearState = true } }; - + // Env returns true _mockPowertoolsConfigurations.LoggerLogEvent.Returns(true); @@ -128,7 +128,7 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() && s.Contains("\"CorrelationId\":\"20\"") )); } - + [Fact] public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() { @@ -136,7 +136,7 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif var instance = new object(); @@ -153,7 +153,7 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() LoggerOutputCase = LoggerOutputCase.PascalCase, LogLevel = LogLevel.Information, LogEvent = true, - CorrelationIdPath = "/age", + CorrelationIdPath = "/Age", ClearState = true } }; @@ -191,19 +191,19 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() LogEvent = true } }; - + // Act var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); loggingAspect.OnEntry(null, null, new object[] { eventObject }, null, null, null, triggers); - + // Assert _mockSystemWrapper.Received().LogLine(Arg.Is(s => s.Contains( "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") )); } - + [Fact] public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable() { @@ -211,7 +211,7 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif var instance = new object(); @@ -226,7 +226,7 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( { Service = "TestService", LoggerOutputCase = LoggerOutputCase.PascalCase, - //LogLevel = LogLevel.Information, + LogEvent = true, CorrelationIdPath = "/age" } @@ -248,8 +248,63 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( _mockSystemWrapper.DidNotReceive().LogLine(Arg.Any()); } + [Fact] + public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() + { + // Arrange +#if NET8_0_OR_GREATER + + // Add seriolization context for AOT + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); +#endif + + var instance = new object(); + var name = "TestMethod"; + var args = new object[] + { + new TestObject { FullName = "Powertools", Age = 20, Headers = new Header { MyRequestIdHeader = "test" } } + }; + var hostType = typeof(string); + var method = typeof(TestClass).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogEvent = true, + CorrelationIdPath = "/Headers/MyRequestIdHeader" + } + }; + + // Env returns true + _mockPowertoolsConfigurations.LogLevel.Returns(LogLevel.Debug.ToString()); + + // Act + var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + + // Assert + var config = _mockPowertoolsConfigurations.CurrentConfig(); + Assert.NotNull(Logger.LoggerProvider); + Assert.Equal("TestService", config.Service); + Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); + Assert.Equal(LogLevel.Debug, config.MinimumLevel); + + _mockSystemWrapper.Received(1).LogLine(Arg.Is(s => + s == "Skipping Lambda Context injection because ILambdaContext context parameter not found.")); + + _mockSystemWrapper.Received(1).LogLine(Arg.Is(s => + s.Contains("\"CorrelationId\":\"test\"") && + s.Contains( + "\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":{\"MyRequestIdHeader\":\"test\"}") + )); + } + public void Dispose() { LoggingAspect.ResetForTest(); + PowertoolsLoggingSerializer.ClearOptions(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index fa2b63e0..00f800f4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -116,19 +116,28 @@ public void OnEntry_WhenEventArgExist_LogEvent() // Arrange var consoleOut = Substitute.For(); SystemWrapper.Instance.SetOut(consoleOut); - + var correlationId = Guid.NewGuid().ToString(); + #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" }; + + var testObj = new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }; // Act - _testClass.LogEvent(context); + _testClass.LogEvent(testObj, context); consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("FunctionName\":\"PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1")) @@ -145,7 +154,7 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif var context = new TestLambdaContext() { @@ -202,7 +211,7 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif // Act @@ -263,7 +272,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif // Act @@ -318,7 +327,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L #if NET8_0_OR_GREATER // Add seriolization context for AOT - var _ = new PowertoolsLambdaSerializer(TestJsonContext.Default); + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); #endif // Act @@ -443,11 +452,30 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ // Assert consoleOut.Received(1).WriteLine(Arg.Is(s => s == "Skipping Event Log because event parameter not found.")); } + + [Fact] + public void Should_Log_When_Not_Using_Decorator() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + var test = new TestClass(); + + // Act + test.TestLogNoDecorator(); + + // Assert + consoleOut.Received().WriteLine( + Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"}")) + ); + } public void Dispose() { Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); LoggingAspect.ResetForTest(); + PowertoolsLoggingSerializer.ClearOptions(); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs index beb35c50..5b549c21 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs @@ -40,8 +40,8 @@ public void LogEventNoArgs() { } - [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase)] - public void LogEvent(ILambdaContext context) + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, CorrelationIdPath = "/Headers/MyRequestIdHeader")] + public void LogEvent(TestObject testObject, ILambdaContext context) { } @@ -138,4 +138,9 @@ public void TestLogLevelCriticalLogEvent(ILambdaContext context) public void TestLogEventWithoutContext() { } + + public void TestLogNoDecorator() + { + Logger.LogInformation("test"); + } } \ 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 b7257d58..1267e090 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -21,6 +21,7 @@ using System.Text; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Utilities; using Microsoft.Extensions.Logging; using NSubstitute; @@ -29,7 +30,7 @@ namespace AWS.Lambda.Powertools.Logging.Tests { [Collection("Sequential")] - public class PowertoolsLoggerTest + public class PowertoolsLoggerTest : IDisposable { public PowertoolsLoggerTest() { @@ -1618,5 +1619,10 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel Assert.Equal(logLevel.ToString(), configurations.LogLevel); Assert.Equal(willLog, systemWrapper.LogMethodCalled); } + + public void Dispose() + { + PowertoolsLoggingSerializer.ClearOptions(); + } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index 521ceda6..2ebeedfc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -20,6 +20,9 @@ using System.IO; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Serialization.SystemTextJson; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Tests.Utilities; using Xunit; @@ -32,33 +35,34 @@ public class PowertoolsLambdaSerializerTests : IDisposable public void Constructor_ShouldNotThrowException() { // Arrange & Act & Assert - var exception = Record.Exception(() => new PowertoolsLambdaSerializer(TestJsonContext.Default)); + var exception = + Record.Exception(() => PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default)); Assert.Null(exception); } - + [Fact] public void Constructor_ShouldAddCustomerContext() { // Arrange var customerContext = new TestJsonContext(); - + // Act - var serializer = new PowertoolsLambdaSerializer(customerContext); - + PowertoolsLoggingSerializer.AddSerializerContext(customerContext); + ; + // Assert Assert.True(PowertoolsLoggingSerializer.HasContext(customerContext)); } [Theory] - [InlineData(LoggerOutputCase.CamelCase,"{\"fullName\":\"John\",\"age\":30}", "John", 30)] - [InlineData(LoggerOutputCase.PascalCase,"{\"FullName\":\"Jane\",\"Age\":25}", "Jane", 25)] - [InlineData(LoggerOutputCase.SnakeCase,"{\"full_name\":\"Jane\",\"age\":25}", "Jane", 25)] - public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCase outputCase,string json, string expectedName, int expectedAge) + [InlineData(LoggerOutputCase.CamelCase, "{\"fullName\":\"John\",\"age\":30}", "John", 30)] + [InlineData(LoggerOutputCase.PascalCase, "{\"FullName\":\"Jane\",\"Age\":25}", "Jane", 25)] + public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCase outputCase, string json, + string expectedName, int expectedAge) { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); - PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase); - + var serializer = new PowertoolsSourceGeneratorSerializer(); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); // Act @@ -74,25 +78,23 @@ public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCas public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); - + var serializer = new PowertoolsSourceGeneratorSerializer(); + ; + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); - + var json = "{\"FullName\":\"John\",\"Age\":30}"; var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); // Act & Assert - Assert.Throws(() => serializer.Deserialize(stream)); + Assert.Throws(() => serializer.Deserialize(stream)); } [Fact] public void Serialize_ValidObject_ShouldSerializeToStream() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); - - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); - + var serializer = new PowertoolsSourceGeneratorSerializer(); var testObject = new TestObject { FullName = "Jane", Age = 25 }; var stream = new MemoryStream(); @@ -110,22 +112,26 @@ public void Serialize_ValidObject_ShouldSerializeToStream() public void Serialize_InvalidType_ShouldThrowInvalidOperationException() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); + var serializer = new PowertoolsSourceGeneratorSerializer(); + ; var unknownObject = new UnknownType(); var stream = new MemoryStream(); // Act & Assert - Assert.Throws(() => serializer.Serialize(unknownObject, stream)); + Assert.Throws(() => serializer.Serialize(unknownObject, stream)); + } + + private class UnknownType + { } - private class UnknownType { } - [Fact] public void Deserialize_NonSeekableStream_ShouldDeserializeCorrectly() { // Arrange - var serializer = new PowertoolsLambdaSerializer(TestJsonContext.Default); - var json = "{\"full_name\":\"John\",\"age\":30}"; + var serializer = new PowertoolsSourceGeneratorSerializer(); + ; + var json = "{\"fullName\":\"John\",\"age\":30}"; var jsonBytes = Encoding.UTF8.GetBytes(json); var nonSeekableStream = new NonSeekableStream(jsonBytes); @@ -137,7 +143,7 @@ public void Deserialize_NonSeekableStream_ShouldDeserializeCorrectly() Assert.Equal("John", result.FullName); Assert.Equal(30, result.Age); } - + public class NonSeekableStream : Stream { private readonly MemoryStream _innerStream; @@ -151,6 +157,7 @@ public NonSeekableStream(byte[] data) public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => _innerStream.Length; + public override long Position { get => _innerStream.Position; @@ -164,14 +171,59 @@ public override long Position public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); // Override the Close and Dispose methods to prevent the inner stream from being closed - public override void Close() { } - protected override void Dispose(bool disposing) { } + public override void Close() + { + } + + protected override void Dispose(bool disposing) + { + } + } + + + [Fact] + public void Should_Serialize_Unknown_Type_When_Including_Outside_Context() + { + // Arrange + var serializer = new PowertoolsSourceGeneratorSerializer(); + var testObject = new APIGatewayProxyRequest + { + Path = "asda", + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + RequestId = "asdas" + } + }; + + var log = new LogEntry + { + Name = "dasda", + Message = testObject + }; + + var stream = new MemoryStream(); + + // Act + serializer.Serialize(testObject, stream); + + stream.Position = 0; + var outputExternalSerializer = new StreamReader(stream).ReadToEnd(); + + var outptuMySerializer = PowertoolsLoggingSerializer.Serialize(log, typeof(LogEntry)); + + // Assert + Assert.Equal( + "{\"Path\":\"asda\",\"RequestContext\":{\"RequestId\":\"asdas\",\"ConnectedAt\":0,\"RequestTimeEpoch\":0},\"IsBase64Encoded\":false}", + outputExternalSerializer); + Assert.Equal( + "{\"cold_start\":false,\"x_ray_trace_id\":null,\"correlation_id\":null,\"timestamp\":\"0001-01-01T00:00:00\",\"level\":\"Trace\",\"service\":null,\"name\":\"dasda\",\"message\":{\"resource\":null,\"path\":\"asda\",\"http_method\":null,\"headers\":null,\"multi_value_headers\":null,\"query_string_parameters\":null,\"multi_value_query_string_parameters\":null,\"path_parameters\":null,\"stage_variables\":null,\"request_context\":{\"path\":null,\"account_id\":null,\"resource_id\":null,\"stage\":null,\"request_id\":\"asdas\",\"identity\":null,\"resource_path\":null,\"http_method\":null,\"api_id\":null,\"extended_request_id\":null,\"connection_id\":null,\"connected_at\":0,\"domain_name\":null,\"domain_prefix\":null,\"event_type\":null,\"message_id\":null,\"route_key\":null,\"authorizer\":null,\"operation_name\":null,\"error\":null,\"integration_latency\":null,\"message_direction\":null,\"request_time\":null,\"request_time_epoch\":0,\"status\":null},\"body\":null,\"is_base64_encoded\":false},\"sampling_rate\":null,\"extra_keys\":null,\"exception\":null,\"lambda_context\":null}", + outptuMySerializer); } public void Dispose() { PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + PowertoolsLoggingSerializer.ClearOptions(); } } -#endif - +#endif \ 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 296e2b81..b00c60ac 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -143,7 +143,7 @@ public void Serialize_UnknownType_ThrowsInvalidOperationException() var unknownObject = new UnknownType(); // Act & Assert - var exception = Assert.Throws(() => + var exception = Assert.Throws(() => PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType))); Assert.Contains("is not known to the serializer", exception.Message); @@ -173,5 +173,6 @@ public void Dispose() #if NET8_0_OR_GREATER PowertoolsLoggingSerializer.ClearContext(); #endif + PowertoolsLoggingSerializer.ClearOptions(); } } \ No newline at end of file diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props index 4e110e46..90d9408d 100644 --- a/libraries/tests/Directory.Packages.props +++ b/libraries/tests/Directory.Packages.props @@ -4,6 +4,7 @@ + @@ -11,7 +12,7 @@ - + From 0b4e96b217c3ca4a6d153bd115360ae557192dcc Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:05:39 +0100 Subject: [PATCH 22/32] replace ApplicationException with JsonSerializerException --- .../Serializers/PowertoolsLoggingSerializer.cs | 2 +- .../Serializers/PowertoolsSourceGeneratorSerializer.cs | 2 +- .../Serializers/PowertoolsLoggingSerializerTests.cs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 000e0785..e88f0577 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -83,7 +83,7 @@ internal static string Serialize(object value, Type inputType) var typeInfo = GetTypeInfo(inputType); if (typeInfo == null) { - throw new ApplicationException( + throw new JsonSerializerException( $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs index d780b1a1..52526b48 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -51,7 +51,7 @@ public PowertoolsSourceGeneratorSerializer( var constructor = typeof(TSgContext).GetConstructor(new Type[] { typeof(JsonSerializerOptions) }); if (constructor == null) { - throw new ApplicationException( + throw new JsonSerializerException( $"The serializer {typeof(TSgContext).FullName} is missing a constructor that takes in JsonSerializerOptions object"); } 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 b00c60ac..e26053ac 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -18,6 +18,7 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using Amazon.Lambda.Serialization.SystemTextJson; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Internal.Converters; using AWS.Lambda.Powertools.Logging.Serializers; @@ -143,7 +144,7 @@ public void Serialize_UnknownType_ThrowsInvalidOperationException() var unknownObject = new UnknownType(); // Act & Assert - var exception = Assert.Throws(() => + var exception = Assert.Throws(() => PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType))); Assert.Contains("is not known to the serializer", exception.Message); From 86b8f85693c85ad11920dd14a25152db55358329 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:20:35 +0100 Subject: [PATCH 23/32] addressed issue when anonymous type on AddKeys. Refactored serialization to avoid multiple #if. tests --- .../Internal/LoggingAspect.cs | 5 +- .../Internal/PowertoolsLogger.cs | 4 - .../AWS.Lambda.Powertools.Logging/Logger.cs | 69 ++++-- .../PowertoolsLoggingSerializer.cs | 21 +- .../PowertoolsLambdaSerializerTests.cs | 38 +++- .../PowertoolsConfigurationExtensionsTests.cs | 152 +++++++++++++ .../Utilities/PowertoolsLoggerHelpersTests.cs | 211 ++++++++---------- 7 files changed, 331 insertions(+), 169 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 162c00fd..5a107831 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -245,12 +245,9 @@ private void CaptureCorrelationId(object eventArg) { var correlationId = string.Empty; -#if NET8_0_OR_GREATER var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); -#else - var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg)); -#endif + var element = jsonDoc.RootElement; for (var i = 0; i < correlationIdPaths.Length; i++) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 8775631e..6e72d102 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -138,11 +138,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except ? GetLogEntry(logLevel, timestamp, message, exception) : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); -#if NET8_0_OR_GREATER _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); -#else - _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry)); -#endif } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 3127a5e6..a008d8c2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -67,7 +68,7 @@ public static ILogger Create(string categoryName) // Needed for when using Logger directly with decorator LoggerProvider ??= new LoggerProvider(null); - + return LoggerProvider.CreateLogger(categoryName); } @@ -94,8 +95,13 @@ public static void AppendKey(string key, object value) { if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key)); - + +#if NET8_0_OR_GREATER + Scope[key] = PowertoolsLoggerHelpers.ObjectToDictionary(value) ?? + throw new ArgumentNullException(nameof(value)); +#else Scope[key] = value ?? throw new ArgumentNullException(nameof(value)); +#endif } /// @@ -146,7 +152,7 @@ internal static void RemoveAllKeys() { Scope.Clear(); } - + internal static void ClearLoggerInstance() { _loggerInstance = null; @@ -773,7 +779,8 @@ public static void Log(LogLevel logLevel, Exception exception) /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogDebug(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogDebug(T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void LogDebug(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.LogDebug(extraKeys, eventId, exception, message, args); } @@ -799,7 +806,8 @@ public static void LogDebug(T extraKeys, EventId eventId, string message, par /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogDebug(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogDebug(T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void LogDebug(T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.LogDebug(extraKeys, exception, message, args); } @@ -829,7 +837,8 @@ public static void LogDebug(T extraKeys, string message, params object[] args /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogTrace(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogTrace(T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void LogTrace(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.LogTrace(extraKeys, eventId, exception, message, args); } @@ -855,7 +864,8 @@ public static void LogTrace(T extraKeys, EventId eventId, string message, par /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogTrace(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogTrace(T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void LogTrace(T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.LogTrace(extraKeys, exception, message, args); } @@ -885,7 +895,8 @@ public static void LogTrace(T extraKeys, string message, params object[] args /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogInformation(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogInformation(T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void LogInformation(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.LogInformation(extraKeys, eventId, exception, message, args); } @@ -898,7 +909,8 @@ public static void LogInformation(T extraKeys, EventId eventId, Exception exc /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogInformation(extraKeys, 0, "Processing request from {Address}", address) - public static void LogInformation(T extraKeys, EventId eventId, string message, params object[] args) where T : class + public static void LogInformation(T extraKeys, EventId eventId, string message, params object[] args) + where T : class { LoggerInstance.LogInformation(extraKeys, eventId, message, args); } @@ -911,7 +923,8 @@ public static void LogInformation(T extraKeys, EventId eventId, string messag /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogInformation(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogInformation(T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void LogInformation(T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.LogInformation(extraKeys, exception, message, args); } @@ -941,7 +954,8 @@ public static void LogInformation(T extraKeys, string message, params object[ /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogWarning(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogWarning(T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void LogWarning(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.LogWarning(extraKeys, eventId, exception, message, args); } @@ -967,7 +981,8 @@ public static void LogWarning(T extraKeys, EventId eventId, string message, p /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogWarning(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogWarning(T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void LogWarning(T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.LogWarning(extraKeys, exception, message, args); } @@ -997,7 +1012,8 @@ public static void LogWarning(T extraKeys, string message, params object[] ar /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogError(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogError(T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void LogError(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.LogError(extraKeys, eventId, exception, message, args); } @@ -1023,7 +1039,8 @@ public static void LogError(T extraKeys, EventId eventId, string message, par /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogError(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogError(T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void LogError(T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.LogError(extraKeys, exception, message, args); } @@ -1053,7 +1070,8 @@ public static void LogError(T extraKeys, string message, params object[] args /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogCritical(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogCritical(T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void LogCritical(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.LogCritical(extraKeys, eventId, exception, message, args); } @@ -1066,7 +1084,8 @@ public static void LogCritical(T extraKeys, EventId eventId, Exception except /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogCritical(extraKeys, 0, "Processing request from {Address}", address) - public static void LogCritical(T extraKeys, EventId eventId, string message, params object[] args) where T : class + public static void LogCritical(T extraKeys, EventId eventId, string message, params object[] args) + where T : class { LoggerInstance.LogCritical(extraKeys, eventId, message, args); } @@ -1079,7 +1098,8 @@ public static void LogCritical(T extraKeys, EventId eventId, string message, /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogCritical(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogCritical(T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void LogCritical(T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.LogCritical(extraKeys, exception, message, args); } @@ -1110,7 +1130,8 @@ public static void LogCritical(T extraKeys, string message, params object[] a /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.Log(LogLevel.Information, extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, Exception exception, string message, params object[] args) where T : class + public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class { LoggerInstance.Log(logLevel, extraKeys, eventId, exception, message, args); } @@ -1124,7 +1145,8 @@ public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, Excep /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.Log(LogLevel.Information, extraKeys, 0, "Processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, string message, params object[] args) where T : class + public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, string message, params object[] args) + where T : class { LoggerInstance.Log(logLevel, extraKeys, eventId, message, args); } @@ -1138,7 +1160,8 @@ public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, strin /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.Log(LogLevel.Information, extraKeys, exception, "Error while processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, Exception exception, string message, params object[] args) where T : class + public static void Log(LogLevel logLevel, T extraKeys, Exception exception, string message, params object[] args) + where T : class { LoggerInstance.Log(logLevel, extraKeys, exception, message, args); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index e88f0577..ef6085d9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -56,21 +56,6 @@ internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) _currentOutputCase = loggerOutputCase; } -#if NET6_0 - /// - /// Serializes an object to a JSON string. - /// - /// The object to serialize. - /// A JSON string representation of the object. - internal static string Serialize(object value) - { - var options = GetSerializerOptions(); - return JsonSerializer.Serialize(value, options); - } -#endif - -#if NET8_0_OR_GREATER - /// /// Serializes an object to a JSON string. /// @@ -80,6 +65,10 @@ internal static string Serialize(object value) /// Thrown when the input type is not known to the serializer. internal static string Serialize(object value, Type inputType) { +#if NET6_0 + var options = GetSerializerOptions(); + return JsonSerializer.Serialize(value, options); +#else var typeInfo = GetTypeInfo(inputType); if (typeInfo == null) { @@ -88,8 +77,10 @@ internal static string Serialize(object value, Type inputType) } return JsonSerializer.Serialize(value, typeInfo); +#endif } +#if NET8_0_OR_GREATER /// /// Adds a JsonSerializerContext to the serializer options. /// diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index 2ebeedfc..b522963f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -#if NET8_0_OR_GREATER using AWS.Lambda.Powertools.Logging.Serializers; using System; @@ -31,6 +30,7 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; public class PowertoolsLambdaSerializerTests : IDisposable { +#if NET8_0_OR_GREATER [Fact] public void Constructor_ShouldNotThrowException() { @@ -220,10 +220,42 @@ public void Should_Serialize_Unknown_Type_When_Including_Outside_Context() outptuMySerializer); } + +#endif public void Dispose() { PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); PowertoolsLoggingSerializer.ClearOptions(); } -} -#endif \ No newline at end of file + +#if NET6_0 + + [Fact] + public void Should_Serialize_Net6() + { + // Arrange + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + var testObject = new APIGatewayProxyRequest + { + Path = "asda", + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + RequestId = "asdas" + } + }; + + var log = new LogEntry + { + Name = "dasda", + Message = testObject + }; + + var outptuMySerializer = PowertoolsLoggingSerializer.Serialize(log, null); + + // Assert + Assert.Equal( + "{\"cold_start\":false,\"x_ray_trace_id\":null,\"correlation_id\":null,\"timestamp\":\"0001-01-01T00:00:00\",\"level\":\"Trace\",\"service\":null,\"name\":\"dasda\",\"message\":{\"resource\":null,\"path\":\"asda\",\"http_method\":null,\"headers\":null,\"multi_value_headers\":null,\"query_string_parameters\":null,\"multi_value_query_string_parameters\":null,\"path_parameters\":null,\"stage_variables\":null,\"request_context\":{\"path\":null,\"account_id\":null,\"resource_id\":null,\"stage\":null,\"request_id\":\"asdas\",\"identity\":null,\"resource_path\":null,\"http_method\":null,\"api_id\":null,\"extended_request_id\":null,\"connection_id\":null,\"connected_at\":0,\"domain_name\":null,\"domain_prefix\":null,\"event_type\":null,\"message_id\":null,\"route_key\":null,\"authorizer\":null,\"operation_name\":null,\"error\":null,\"integration_latency\":null,\"message_direction\":null,\"request_time\":null,\"request_time_epoch\":0,\"status\":null},\"body\":null,\"is_base64_encoded\":false},\"sampling_rate\":null,\"extra_keys\":null,\"exception\":null,\"lambda_context\":null}", + outptuMySerializer); + } +#endif +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs new file mode 100644 index 00000000..ee2a438f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using Xunit; +using NSubstitute; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +public class PowertoolsConfigurationExtensionsTests +{ + [Theory] + [InlineData(LoggerOutputCase.CamelCase, "TestString", "testString")] + [InlineData(LoggerOutputCase.PascalCase, "testString", "TestString")] + [InlineData(LoggerOutputCase.SnakeCase, "TestString", "test_string")] + [InlineData(LoggerOutputCase.SnakeCase, "testString", "test_string")] // Default case + public void ConvertToOutputCase_ShouldConvertCorrectly(LoggerOutputCase outputCase, string input, string expected) + { + // Arrange + var systemWrapper = Substitute.For(); + var configurations = new PowertoolsConfigurations(systemWrapper); + + // Act + var result = configurations.ConvertToOutputCase(input, outputCase); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("TestString", "test_string")] + [InlineData("testString", "test_string")] + [InlineData("Test_String", "test_string")] + [InlineData("TEST_STRING", "test_string")] + [InlineData("test", "test")] + [InlineData("TestStringABC", "test_string_abc")] + [InlineData("TestStringABCTest", "test_string_abc_test")] + [InlineData("Test__String", "test__string")] + [InlineData("TEST", "test")] + [InlineData("ABCTestDEF", "abc_test_def")] + [InlineData("ABC_TEST_DEF", "abc_test_def")] + [InlineData("abcTestDef", "abc_test_def")] + [InlineData("abc_test_def", "abc_test_def")] + [InlineData("Abc_Test_Def", "abc_test_def")] + [InlineData("ABC", "abc")] + [InlineData("A_B_C", "a_b_c")] + [InlineData("ABCDEFG", "abcdefg")] + [InlineData("ABCDefGHI", "abc_def_ghi")] + [InlineData("ABCTestDEFGhi", "abc_test_def_ghi")] + [InlineData("Test___String", "test___string")] + public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) + { + // Act + var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToSnakeCase", input); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("testString", "TestString")] + [InlineData("TestString", "TestString")] + [InlineData("test", "Test")] + [InlineData("test_string", "TestString")] + [InlineData("test_string_abc", "TestStringAbc")] + [InlineData("test_stringABC", "TestStringABC")] + [InlineData("test__string", "TestString")] + [InlineData("TEST_STRING", "TestString")] + [InlineData("t", "T")] + [InlineData("", "")] + [InlineData("abc_def_ghi", "AbcDefGhi")] + [InlineData("ABC_DEF_GHI", "AbcDefGhi")] + [InlineData("abc123_def456", "Abc123Def456")] + [InlineData("_test_string", "TestString")] + [InlineData("test_string_", "TestString")] + [InlineData("__test__string__", "TestString")] + [InlineData("TEST__STRING", "TestString")] + [InlineData("testString123", "TestString123")] + [InlineData("test_string_123", "TestString123")] + [InlineData("123_test_string", "123TestString")] + [InlineData("test_1_string", "Test1String")] + public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) + { + // Act + var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToPascalCase", input); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("test_string", "testString")] + [InlineData("testString", "testString")] + [InlineData("TestString", "testString")] + [InlineData("test_string_abc", "testStringAbc")] + [InlineData("test_stringABC", "testStringABC")] + [InlineData("test__string", "testString")] + [InlineData("TEST_STRING", "testString")] + [InlineData("test", "test")] + [InlineData("T", "t")] + [InlineData("", "")] + [InlineData("abc_def_ghi", "abcDefGhi")] + [InlineData("ABC_DEF_GHI", "abcDefGhi")] + [InlineData("abc123_def456", "abc123Def456")] + [InlineData("_test_string", "testString")] + [InlineData("test_string_", "testString")] + [InlineData("__test__string__", "testString")] + [InlineData("TEST__STRING", "testString")] + [InlineData("testString123", "testString123")] + [InlineData("test_string_123", "testString123")] + [InlineData("123_test_string", "123TestString")] + [InlineData("test_1_string", "test1String")] + [InlineData("Test_string", "testString")] + [InlineData("Test_String", "testString")] + [InlineData("Test_String_Abc", "testStringAbc")] + [InlineData("alreadyCamelCase", "alreadyCamelCase")] + [InlineData("ALLCAPS", "allcaps")] + [InlineData("ALL_CAPS", "allCaps")] + [InlineData("single", "single")] + public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) + { + // Act + var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToCamelCase", input); + + // Assert + Assert.Equal(expected, result); + } +} + +// Helper class to invoke private static methods +public static class PrivateMethod +{ + public static T InvokeStatic(Type type, string methodName, params object[] parameters) + { + var method = type.GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + return (T)method!.Invoke(null, parameters); + } +} \ No newline at end of file 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 ee2a438f..4e01dab9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -1,152 +1,123 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ + +#if NET8_0_OR_GREATER using System; -using Xunit; -using NSubstitute; +using System.Collections.Generic; +using System.IO; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using AWS.Lambda.Powertools.Logging.Serializers; +using NSubstitute; +using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; -public class PowertoolsConfigurationExtensionsTests +public class PowertoolsLoggerHelpersTests : IDisposable { - [Theory] - [InlineData(LoggerOutputCase.CamelCase, "TestString", "testString")] - [InlineData(LoggerOutputCase.PascalCase, "testString", "TestString")] - [InlineData(LoggerOutputCase.SnakeCase, "TestString", "test_string")] - [InlineData(LoggerOutputCase.SnakeCase, "testString", "test_string")] // Default case - public void ConvertToOutputCase_ShouldConvertCorrectly(LoggerOutputCase outputCase, string input, string expected) + [Fact] + public void ObjectToDictionary_AnonymousObjectWithSimpleProperties_ReturnsDictionary() { // Arrange - var systemWrapper = Substitute.For(); - var configurations = new PowertoolsConfigurations(systemWrapper); + var anonymousObject = new { name = "test", age = 30 }; // Act - var result = configurations.ConvertToOutputCase(input, outputCase); + var result = PowertoolsLoggerHelpers.ObjectToDictionary(anonymousObject); // Assert - Assert.Equal(expected, result); + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Equal(2, dictionary.Count); + Assert.Equal("test", dictionary["name"]); + Assert.Equal(30, dictionary["age"]); } - [Theory] - [InlineData("TestString", "test_string")] - [InlineData("testString", "test_string")] - [InlineData("Test_String", "test_string")] - [InlineData("TEST_STRING", "test_string")] - [InlineData("test", "test")] - [InlineData("TestStringABC", "test_string_abc")] - [InlineData("TestStringABCTest", "test_string_abc_test")] - [InlineData("Test__String", "test__string")] - [InlineData("TEST", "test")] - [InlineData("ABCTestDEF", "abc_test_def")] - [InlineData("ABC_TEST_DEF", "abc_test_def")] - [InlineData("abcTestDef", "abc_test_def")] - [InlineData("abc_test_def", "abc_test_def")] - [InlineData("Abc_Test_Def", "abc_test_def")] - [InlineData("ABC", "abc")] - [InlineData("A_B_C", "a_b_c")] - [InlineData("ABCDEFG", "abcdefg")] - [InlineData("ABCDefGHI", "abc_def_ghi")] - [InlineData("ABCTestDEFGhi", "abc_test_def_ghi")] - [InlineData("Test___String", "test___string")] - public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) + [Fact] + public void ObjectToDictionary_AnonymousObjectWithNestedObject_ReturnsDictionaryWithNestedDictionary() { + // Arrange + var anonymousObject = new { name = "test", nested = new { id = 1 } }; + // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToSnakeCase", input); + var result = PowertoolsLoggerHelpers.ObjectToDictionary(anonymousObject); // Assert - Assert.Equal(expected, result); + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Equal(2, dictionary.Count); + Assert.Equal("test", dictionary["name"]); + Assert.IsType>(dictionary["nested"]); + var nestedDictionary = (Dictionary)dictionary["nested"]; + Assert.Single(nestedDictionary); + Assert.Equal(1, nestedDictionary["id"]); } - [Theory] - [InlineData("testString", "TestString")] - [InlineData("TestString", "TestString")] - [InlineData("test", "Test")] - [InlineData("test_string", "TestString")] - [InlineData("test_string_abc", "TestStringAbc")] - [InlineData("test_stringABC", "TestStringABC")] - [InlineData("test__string", "TestString")] - [InlineData("TEST_STRING", "TestString")] - [InlineData("t", "T")] - [InlineData("", "")] - [InlineData("abc_def_ghi", "AbcDefGhi")] - [InlineData("ABC_DEF_GHI", "AbcDefGhi")] - [InlineData("abc123_def456", "Abc123Def456")] - [InlineData("_test_string", "TestString")] - [InlineData("test_string_", "TestString")] - [InlineData("__test__string__", "TestString")] - [InlineData("TEST__STRING", "TestString")] - [InlineData("testString123", "TestString123")] - [InlineData("test_string_123", "TestString123")] - [InlineData("123_test_string", "123TestString")] - [InlineData("test_1_string", "Test1String")] - public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) + [Fact] + public void ObjectToDictionary_ObjectWithNamespace_ReturnsOriginalObject() { + // Arrange + var objectWithNamespace = new System.Text.StringBuilder(); + // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToPascalCase", input); + var result = PowertoolsLoggerHelpers.ObjectToDictionary(objectWithNamespace); // Assert - Assert.Equal(expected, result); + Assert.Same(objectWithNamespace, result); } - [Theory] - [InlineData("test_string", "testString")] - [InlineData("testString", "testString")] - [InlineData("TestString", "testString")] - [InlineData("test_string_abc", "testStringAbc")] - [InlineData("test_stringABC", "testStringABC")] - [InlineData("test__string", "testString")] - [InlineData("TEST_STRING", "testString")] - [InlineData("test", "test")] - [InlineData("T", "t")] - [InlineData("", "")] - [InlineData("abc_def_ghi", "abcDefGhi")] - [InlineData("ABC_DEF_GHI", "abcDefGhi")] - [InlineData("abc123_def456", "abc123Def456")] - [InlineData("_test_string", "testString")] - [InlineData("test_string_", "testString")] - [InlineData("__test__string__", "testString")] - [InlineData("TEST__STRING", "testString")] - [InlineData("testString123", "testString123")] - [InlineData("test_string_123", "testString123")] - [InlineData("123_test_string", "123TestString")] - [InlineData("test_1_string", "test1String")] - [InlineData("Test_string", "testString")] - [InlineData("Test_String", "testString")] - [InlineData("Test_String_Abc", "testStringAbc")] - [InlineData("alreadyCamelCase", "alreadyCamelCase")] - [InlineData("ALLCAPS", "allcaps")] - [InlineData("ALL_CAPS", "allCaps")] - [InlineData("single", "single")] - public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) + [Fact] + public void ObjectToDictionary_NullObject_ThrowsArgumentNullException() { - // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToCamelCase", input); - - // Assert - Assert.Equal(expected, result); + // Act & Assert + Assert.Throws(() => PowertoolsLoggerHelpers.ObjectToDictionary(null)); + } + + [Fact] + public void Should_Log_With_Anonymous() + { + // Act & Assert + Logger.AppendKey("asd", new + { + name = "sada" + }); + + Logger.LogInformation("test"); + } + + [Fact] + public void Should_Log_With_Complex_Anonymous() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act & Assert + Logger.AppendKey("newKey", new + { + id = 1, + name = "my name", + Adresses = new { + street = "street 1", + number = 1, + 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\"}")) + ); } -} -// Helper class to invoke private static methods -public static class PrivateMethod -{ - public static T InvokeStatic(Type type, string methodName, params object[] parameters) + public void Dispose() { - var method = type.GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - return (T)method!.Invoke(null, parameters); + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.Default); + PowertoolsLoggingSerializer.ClearOptions(); } -} \ No newline at end of file +} + +#endif \ No newline at end of file From af66bcd3a07df8382c0c6b964c0eefb892931796 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:05:30 +0100 Subject: [PATCH 24/32] Fix custom serializer on AOT. Add tests --- .../Helpers/PowertoolsLoggerHelpers.cs | 15 ++- .../Attributes/LoggerAspectTests.cs | 11 +- .../Attributes/LoggingAttributeTest.cs | 51 ++++---- .../Formatter/CustomLogFormatter.cs | 36 ++++++ .../Formatter/LogFormatterTest.cs | 121 +++++++++++++++++- .../Handlers/ExceptionFunctionHandlerTests.cs | 10 +- .../TestClass.cs => Handlers/TestHandlers.cs} | 19 ++- .../PowertoolsConfigurationExtensionsTests.cs | 9 +- .../Utilities/PowertoolsLoggerHelpersTests.cs | 16 ++- 9 files changed, 243 insertions(+), 45 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/CustomLogFormatter.cs rename libraries/tests/AWS.Lambda.Powertools.Logging.Tests/{Attributes/TestClass.cs => Handlers/TestHandlers.cs} (88%) 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 c0d63d89..4404a3a2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System.Collections.Generic; using System.Linq; namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; @@ -32,12 +33,24 @@ internal static class PowertoolsLoggerHelpers /// internal static object ObjectToDictionary(object anonymousObject) { + if (anonymousObject == null) + { + return new Dictionary(); + } + if (anonymousObject.GetType().Namespace is not null) { return anonymousObject; } return anonymousObject.GetType().GetProperties() - .ToDictionary(prop => prop.Name, prop => ObjectToDictionary(prop.GetValue(anonymousObject, null))); + .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; + } + ); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index baf00a64..ba08453f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -17,6 +17,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; using AWS.Lambda.Powertools.Logging.Tests.Serializers; using Microsoft.Extensions.Logging; using NSubstitute; @@ -49,7 +50,7 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; var hostType = typeof(string); - var method = typeof(TestClass).GetMethod("TestMethod"); + var method = typeof(TestHandlers).GetMethod("TestMethod"); var returnType = typeof(string); var triggers = new Attribute[] { @@ -93,7 +94,7 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; var hostType = typeof(string); - var method = typeof(TestClass).GetMethod("TestMethod"); + var method = typeof(TestHandlers).GetMethod("TestMethod"); var returnType = typeof(string); var triggers = new Attribute[] { @@ -143,7 +144,7 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; var hostType = typeof(string); - var method = typeof(TestClass).GetMethod("TestMethod"); + var method = typeof(TestHandlers).GetMethod("TestMethod"); var returnType = typeof(string); var triggers = new Attribute[] { @@ -218,7 +219,7 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; var hostType = typeof(string); - var method = typeof(TestClass).GetMethod("TestMethod"); + var method = typeof(TestHandlers).GetMethod("TestMethod"); var returnType = typeof(string); var triggers = new Attribute[] { @@ -265,7 +266,7 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() new TestObject { FullName = "Powertools", Age = 20, Headers = new Header { MyRequestIdHeader = "test" } } }; var hostType = typeof(string); - var method = typeof(TestClass).GetMethod("TestMethod"); + var method = typeof(TestHandlers).GetMethod("TestMethod"); var returnType = typeof(string); var triggers = new Attribute[] { diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 00f800f4..76006713 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -24,6 +24,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; using AWS.Lambda.Powertools.Logging.Tests.Serializers; using NSubstitute; using Xunit; @@ -33,11 +34,11 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes [Collection("Sequential")] public class LoggingAttributeTests : IDisposable { - private TestClass _testClass; + private TestHandlers _testHandlers; public LoggingAttributeTests() { - _testClass = new TestClass(); + _testHandlers = new TestHandlers(); } [Fact] @@ -48,7 +49,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.TestMethod(); + _testHandlers.TestMethod(); // Assert var allKeys = Logger.GetAllKeys() @@ -74,7 +75,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.TestMethodDebug(); + _testHandlers.TestMethodDebug(); // Assert var allKeys = Logger.GetAllKeys() @@ -103,7 +104,7 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.LogEventNoArgs(); + _testHandlers.LogEventNoArgs(); consoleOut.DidNotReceive().WriteLine( Arg.Any() @@ -137,7 +138,7 @@ public void OnEntry_WhenEventArgExist_LogEvent() }; // Act - _testClass.LogEvent(testObj, context); + _testHandlers.LogEvent(testObj, context); consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("FunctionName\":\"PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1")) @@ -162,7 +163,7 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() }; // Act - _testClass.LogEventFalse(context); + _testHandlers.LogEventFalse(context); consoleOut.DidNotReceive().WriteLine( Arg.Any() @@ -177,7 +178,7 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.LogEventDebug(); + _testHandlers.LogEventDebug(); consoleOut.Received(1).WriteLine( Arg.Is(i => i == "Skipping Event Log because event parameter not found.") @@ -192,7 +193,7 @@ public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.ClearState(); + _testHandlers.ClearState(); Assert.NotNull(Logger.LoggerProvider); Assert.False(Logger.GetAllKeys().Any()); @@ -218,7 +219,7 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI switch (correlationIdPath) { case CorrelationIdPaths.ApiGatewayRest: - _testClass.CorrelationApiGatewayProxyRequest(new APIGatewayProxyRequest + _testHandlers.CorrelationApiGatewayProxyRequest(new APIGatewayProxyRequest { RequestContext = new APIGatewayProxyRequest.ProxyRequestContext { @@ -227,7 +228,7 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI }); break; case CorrelationIdPaths.ApplicationLoadBalancer: - _testClass.CorrelationApplicationLoadBalancerRequest(new ApplicationLoadBalancerRequest + _testHandlers.CorrelationApplicationLoadBalancerRequest(new ApplicationLoadBalancerRequest { Headers = new Dictionary { @@ -236,13 +237,13 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI }); break; case CorrelationIdPaths.EventBridge: - _testClass.CorrelationCloudWatchEvent(new S3ObjectCreateEvent + _testHandlers.CorrelationCloudWatchEvent(new S3ObjectCreateEvent { Id = correlationId }); break; case "/headers/my_request_id_header": - _testClass.CorrelationIdFromString(new TestObject + _testHandlers.CorrelationIdFromString(new TestObject { Headers = new Header { @@ -279,7 +280,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu switch (outputCase) { case LoggerOutputCase.CamelCase: - _testClass.CorrelationIdFromStringCamel(new TestObject + _testHandlers.CorrelationIdFromStringCamel(new TestObject { Headers = new Header { @@ -288,7 +289,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu }); break; case LoggerOutputCase.PascalCase: - _testClass.CorrelationIdFromStringPascal(new TestObject + _testHandlers.CorrelationIdFromStringPascal(new TestObject { Headers = new Header { @@ -297,7 +298,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu }); break; case LoggerOutputCase.SnakeCase: - _testClass.CorrelationIdFromStringSnake(new TestObject + _testHandlers.CorrelationIdFromStringSnake(new TestObject { Headers = new Header { @@ -335,7 +336,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L { case LoggerOutputCase.CamelCase: Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); - _testClass.CorrelationIdFromStringCamelEnv(new TestObject + _testHandlers.CorrelationIdFromStringCamelEnv(new TestObject { Headers = new Header { @@ -345,7 +346,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L break; case LoggerOutputCase.PascalCase: Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "PascalCase"); - _testClass.CorrelationIdFromStringPascalEnv(new TestObject + _testHandlers.CorrelationIdFromStringPascalEnv(new TestObject { Headers = new Header { @@ -354,7 +355,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L }); break; case LoggerOutputCase.SnakeCase: - _testClass.CorrelationIdFromStringSnakeEnv(new TestObject + _testHandlers.CorrelationIdFromStringSnakeEnv(new TestObject { Headers = new Header { @@ -380,7 +381,7 @@ public void When_Setting_SamplingRate_Should_Add_Key() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.HandlerSamplingRate(); + _testHandlers.HandlerSamplingRate(); // Assert @@ -397,7 +398,7 @@ public void When_Setting_Service_Should_Update_Key() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.HandlerService(); + _testHandlers.HandlerService(); // Assert @@ -413,7 +414,7 @@ public void When_Setting_LogLevel_Should_Update_LogLevel() SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.TestLogLevelCritical(); + _testHandlers.TestLogLevelCritical(); // Assert @@ -433,7 +434,7 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() }; // Act - _testClass.TestLogLevelCriticalLogEvent(context); + _testHandlers.TestLogLevelCriticalLogEvent(context); // Assert consoleOut.DidNotReceive().WriteLine(Arg.Any()); @@ -447,7 +448,7 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ SystemWrapper.Instance.SetOut(consoleOut); // Act - _testClass.TestLogEventWithoutContext(); + _testHandlers.TestLogEventWithoutContext(); // Assert consoleOut.Received(1).WriteLine(Arg.Is(s => s == "Skipping Event Log because event parameter not found.")); @@ -460,7 +461,7 @@ public void Should_Log_When_Not_Using_Decorator() var consoleOut = Substitute.For(); SystemWrapper.Instance.SetOut(consoleOut); - var test = new TestClass(); + var test = new TestHandlers(); // Act test.TestLogNoDecorator(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/CustomLogFormatter.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/CustomLogFormatter.cs new file mode 100644 index 00000000..6f815c0e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/CustomLogFormatter.cs @@ -0,0 +1,36 @@ +using System; + +namespace AWS.Lambda.Powertools.Logging.Tests.Formatter; + +public class CustomLogFormatter : ILogFormatter +{ + public object FormatLogEntry(LogEntry logEntry) + { + return new + { + Message = logEntry.Message, + Service = logEntry.Service, + CorrelationIds = new + { + AwsRequestId = logEntry.LambdaContext?.AwsRequestId, + XRayTraceId = logEntry.XRayTraceId, + CorrelationId = logEntry.CorrelationId + }, + LambdaFunction = new + { + Name = logEntry.LambdaContext?.FunctionName, + Arn = logEntry.LambdaContext?.InvokedFunctionArn, + MemoryLimitInMB = logEntry.LambdaContext?.MemoryLimitInMB, + Version = logEntry.LambdaContext?.FunctionVersion, + ColdStart = true, + }, + Level = logEntry.Level.ToString(), + Timestamp = new DateTime(2024, 1, 1).ToString("o"), + Logger = new + { + Name = logEntry.Name, + SampleRate = logEntry.SamplingRate + }, + }; + } +} \ No newline at end of file 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 e50cb00e..8b65062f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; @@ -22,6 +23,8 @@ using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; @@ -31,8 +34,15 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Formatter { [Collection("Sequential")] - public class LogFormatterTest + public class LogFormatterTest : IDisposable { + private readonly TestHandlers _testHandler; + + public LogFormatterTest() + { + _testHandler = new TestHandlers(); + } + [Fact] public void Log_WhenCustomFormatter_LogsCustomFormat() { @@ -123,7 +133,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); @@ -166,14 +176,113 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() x.LambdaContext.InvokedFunctionArn == lambdaContext.InvokedFunctionArn && x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId )); - + systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); + } - //Clean up + [Fact] + public void Should_Log_CustomFormatter_When_Decorated() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + var lambdaContext = new TestLambdaContext + { + FunctionName = "funtionName", + FunctionVersion = "version", + InvokedFunctionArn = "function::arn", + AwsRequestId = "requestId", + MemoryLimitInMB = 128 + }; + + Logger.UseFormatter(new CustomLogFormatter()); + _testHandler.TestCustomFormatterWithDecorator("test", lambdaContext); + + // serializer works differently in .net 8 and AOT. In .net 6 it writes properties that have null + // in .net 8 it removes null properties + +#if NET8_0_OR_GREATER + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i == + "{\"message\":\"test\",\"service\":\"my_service\",\"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\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") + ); +#else + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i.Contains( + "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{\"aws_request_id\":\"requestId\",\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_m_b\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\"")) + ); +#endif + } + + [Fact] + public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + var lambdaContext = new TestLambdaContext + { + FunctionName = "funtionName", + FunctionVersion = "version", + InvokedFunctionArn = "function::arn", + AwsRequestId = "requestId", + MemoryLimitInMB = 128 + }; + + Logger.UseFormatter(new CustomLogFormatter()); + + _testHandler.TestCustomFormatterNoDecorator("test", lambdaContext); + + // serializer works differently in .net 8 and AOT. In .net 6 it writes properties that have null + // in .net 8 it removes null properties + +#if NET8_0_OR_GREATER + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i == + "{\"message\":\"test\",\"service\":\"service_undefined\",\"correlation_ids\":{},\"lambda_function\":{\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0}}") + ); +#else + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i == + "{\"message\":\"test\",\"service\":\"service_undefined\",\"correlation_ids\":{\"aws_request_id\":null,\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":null,\"arn\":null,\"memory_limit_in_m_b\":null,\"version\":null,\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0}}") + ); +#endif + } + + [Fact] + public void Should_Log_CustomFormatter_When_Decorated_No_Context() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + Logger.UseFormatter(new CustomLogFormatter()); + + _testHandler.TestCustomFormatterWithDecoratorNoContext("test"); + +#if NET8_0_OR_GREATER + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i == + "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{},\"lambda_function\":{\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") + ); +#else + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i == + "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{\"aws_request_id\":null,\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":null,\"arn\":null,\"memory_limit_in_m_b\":null,\"version\":null,\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") + ); +#endif + } + + public void Dispose() + { Logger.UseDefaultFormatter(); Logger.RemoveAllKeys(); LoggingLambdaContext.Clear(); LoggingAspect.ResetForTest(); + PowertoolsLoggingSerializer.ClearOptions(); } } @@ -204,7 +313,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() MinimumLevel = LogLevel.Information, LoggerOutputCase = LoggerOutputCase.PascalCase }; - + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); @@ -249,7 +358,7 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() MinimumLevel = LogLevel.Information, LoggerOutputCase = LoggerOutputCase.PascalCase }; - + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); var logger = provider.CreateLogger(loggerName); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs index dd7a12db..f9ffd5eb 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs @@ -1,11 +1,13 @@ using System; using System.Threading.Tasks; using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; -public sealed class ExceptionFunctionHandlerTests +public sealed class ExceptionFunctionHandlerTests : IDisposable { [Fact] public async Task Stack_Trace_Included_When_Decorator_Present() @@ -36,4 +38,10 @@ public void Utility_Should_Not_Throw_Exceptions_To_Client() // Assert Assert.Equal("OK", res); } + + public void Dispose() + { + LoggingAspect.ResetForTest(); + PowertoolsLoggingSerializer.ClearOptions(); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs similarity index 88% rename from libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs rename to libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs index 5b549c21..990fa103 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/TestClass.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -21,9 +21,9 @@ using AWS.Lambda.Powertools.Logging.Tests.Serializers; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; +namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; -class TestClass +class TestHandlers { [Logging] public void TestMethod() @@ -139,6 +139,21 @@ public void TestLogEventWithoutContext() { } + [Logging(LogEvent = true, SamplingRate = 0.2, Service = "my_service")] + public void TestCustomFormatterWithDecorator(string input, ILambdaContext context) + { + } + + [Logging(LogEvent = true, SamplingRate = 0.2, Service = "my_service")] + public void TestCustomFormatterWithDecoratorNoContext(string input) + { + } + + public void TestCustomFormatterNoDecorator(string input, ILambdaContext context) + { + Logger.LogInformation(input); + } + public void TestLogNoDecorator() { Logger.LogInformation("test"); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs index ee2a438f..6a719d1b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs @@ -18,10 +18,11 @@ using NSubstitute; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; -public class PowertoolsConfigurationExtensionsTests +public class PowertoolsConfigurationExtensionsTests : IDisposable { [Theory] [InlineData(LoggerOutputCase.CamelCase, "TestString", "testString")] @@ -139,6 +140,12 @@ public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) // Assert Assert.Equal(expected, result); } + + public void Dispose() + { + LoggingAspect.ResetForTest(); + PowertoolsLoggingSerializer.ClearOptions(); + } } // Helper class to invoke private static methods 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 4e01dab9..ed29a772 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -65,22 +65,30 @@ public void ObjectToDictionary_ObjectWithNamespace_ReturnsOriginalObject() } [Fact] - public void ObjectToDictionary_NullObject_ThrowsArgumentNullException() + public void ObjectToDictionary_NullObject_Return_New_Dictionary() { // Act & Assert - Assert.Throws(() => PowertoolsLoggerHelpers.ObjectToDictionary(null)); + Assert.NotNull(() => PowertoolsLoggerHelpers.ObjectToDictionary(null)); } [Fact] public void Should_Log_With_Anonymous() { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + // Act & Assert - Logger.AppendKey("asd", new + Logger.AppendKey("newKey", new { - name = "sada" + name = "my name" }); Logger.LogInformation("test"); + + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i.Contains("\"new_key\":{\"name\":\"my name\"}")) + ); } [Fact] From 6446e95e34fd2f754b6226ee1b7a1729ce7eedb8 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:29:46 +0100 Subject: [PATCH 25/32] for aot, ILogFormatter instance needs to be passed to PowertoolsSourceGeneratorSerializer --- .../PowertoolsSourceGeneratorSerializer.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs index 52526b48..01d51317 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -32,9 +32,18 @@ public sealed class PowertoolsSourceGeneratorSerializer< /// Constructs instance of serializer. /// public PowertoolsSourceGeneratorSerializer() - : this(null) + : this((Action)null) { } + + /// + /// Constructs instance of serializer. + /// + public PowertoolsSourceGeneratorSerializer(ILogFormatter logFormatter) + : this((Action)null) + { + Logger.UseFormatter(logFormatter); + } /// /// Constructs instance of serializer with the option to customize the JsonSerializerOptions after the From 52f60665465e7a96999cd321609644e7b2cfa23c Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:41:01 +0100 Subject: [PATCH 26/32] comments --- libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs | 1 + .../Serializers/PowertoolsSourceGeneratorSerializer.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index a008d8c2..4271de83 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -1189,6 +1189,7 @@ public static void Log(LogLevel logLevel, T extraKeys, string message, params /// Set the log formatter. /// /// The log formatter. + /// WARNING: This method should not be called when using AOT. ILogFormatter should be passed to PowertoolsSourceGeneratorSerializer constructor public static void UseFormatter(ILogFormatter logFormatter) { _logFormatter = logFormatter ?? throw new ArgumentNullException(nameof(logFormatter)); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs index 01d51317..b247c4a3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -37,7 +37,11 @@ public PowertoolsSourceGeneratorSerializer() } /// - /// Constructs instance of serializer. + /// Constructs instance of serializer with an ILogFormatter instance + /// The ILogFormatter instance to use for formatting log messages. + /// + /// The ILogFormatter instance is used to format log messages before they are serialized. + /// /// public PowertoolsSourceGeneratorSerializer(ILogFormatter logFormatter) : this((Action)null) @@ -46,7 +50,7 @@ public PowertoolsSourceGeneratorSerializer(ILogFormatter logFormatter) } /// - /// Constructs instance of serializer with the option to customize the JsonSerializerOptions after the + /// Constructs instance of serializer with the option to customize the JsonSerializerOptions after the /// Amazon.Lambda.Serialization.SystemTextJson's default settings have been applied. /// /// From 4d52cc14b12e4f627d0f5a1bba13bc326fa09b62 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:08:20 +0100 Subject: [PATCH 27/32] add documentation. --- docs/core/logging.md | 129 ++++++++++++++++++ .../PowertoolsSourceGeneratorSerializer.cs | 7 +- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index 7439c79d..8d40e994 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -11,6 +11,7 @@ The logging utility provides a Lambda optimized logger with output structured as * Log Lambda event when instructed (disabled by default) * Log sampling enables DEBUG log level for a percentage of requests (disabled by default) * Append additional keys to structured log at any point in time +* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.6.0 ## Installation @@ -22,6 +23,12 @@ Powertools for AWS Lambda (.NET) are available as NuGet packages. You can instal ## Getting started +!!! info + + AOT Support + If loooking for AOT specific configurations navigate to the [AOT section](#aot-support) + + Logging requires two settings: Setting | Description | Environment variable | Attribute parameter @@ -660,3 +667,125 @@ You can customize the structure (keys and values) of your log entries by impleme } } ``` + +## AOT Support + +Logging utility supports native AOT serialization by default without any changes needed. + +In case you want to use the `LogEvent`, `Custom Log Formatter` features or serialize your own types when Logging events it is required +that you do some changes in your Lambda `Main` method. + +### Configure + +The change needed is to replace `SourceGeneratorLambdaJsonSerializer` with `PowertoolsSourceGeneratorSerializer`. + +This change enables Powertools to construct an instance of JsonSerializerOptions that is used to customize the serialization and deserialization of the Lambda JSON events and your own types. + +=== "Before" + + ```csharp + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + ``` + +=== "After" + + ```csharp hl_lines="2" + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new PowertoolsSourceGeneratorSerializer()) + .Build() + .RunAsync(); + ``` + +For example when you have your own Demo type + +```csharp +public class Demo +{ + public string Name { get; set; } + public Headers Headers { get; set; } +} +``` + +To be able to serialize it in AOT you have to have your own `JsonSerializerContext` + +```csharp +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] +[JsonSerializable(typeof(Demo))] +public partial class MyCustomJsonSerializerContext : JsonSerializerContext +{ +} +``` + +When you change to `PowertoolsSourceGeneratorSerializer` we are +combining your `JsonSerializerContext` types with Powertools `JsonSerializerContext`. This allows Powertools to serialize your types and Lambda events. + +### Custom Log Formatter + +To be able to use a custom log formatter with AOT we need to pass an instance of ` ILogFormatter` to `PowertoolsSourceGeneratorSerializer` +instead of using the static `Logger.UseFormatter` in the Function contructor. + +=== "Function Main method" + + ```csharp hl_lines="5" + + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, + new PowertoolsSourceGeneratorSerializer + ( + new CustomLogFormatter() + ) + ) + .Build() + .RunAsync(); + + ``` + +=== "CustomLogFormatter.cs" + + ```csharp + public class CustomLogFormatter : ILogFormatter + { + public object FormatLogEntry(LogEntry logEntry) + { + return new + { + Message = logEntry.Message, + Service = logEntry.Service, + CorrelationIds = new + { + AwsRequestId = logEntry.LambdaContext?.AwsRequestId, + XRayTraceId = logEntry.XRayTraceId, + CorrelationId = logEntry.CorrelationId + }, + LambdaFunction = new + { + Name = logEntry.LambdaContext?.FunctionName, + Arn = logEntry.LambdaContext?.InvokedFunctionArn, + MemoryLimitInMB = logEntry.LambdaContext?.MemoryLimitInMB, + Version = logEntry.LambdaContext?.FunctionVersion, + ColdStart = logEntry.ColdStart, + }, + Level = logEntry.Level.ToString(), + Timestamp = logEntry.Timestamp.ToString("o"), + Logger = new + { + Name = logEntry.Name, + SampleRate = logEntry.SamplingRate + }, + }; + } + } + ``` + +### Anonymous types + +!!! note + + Although we support anonymous type serialization by converting to a `Dictionary`, + this is not a best practice and is not recommendede when using native AOT. + + Recommendation is to use concrete classes and add them to your `JsonSerializerContext`. \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs index b247c4a3..dadec8da 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -23,7 +23,12 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; -/// +/// +/// ILambdaSerializer implementation that supports the source generator support of System.Text.Json. +/// When the class is compiled it will generate all the JSON serialization code to convert between JSON and the list types. This +/// will avoid any reflection based serialization. +/// +/// public sealed class PowertoolsSourceGeneratorSerializer< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TSgContext> : SourceGeneratorLambdaJsonSerializer where TSgContext : JsonSerializerContext From 0ffbbbc20e540abc5db864fd56f0c82ccb462da2 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:11:56 +0100 Subject: [PATCH 28/32] update doc --- docs/core/logging.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index 8d40e994..6e2be26b 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -672,8 +672,10 @@ You can customize the structure (keys and values) of your log entries by impleme Logging utility supports native AOT serialization by default without any changes needed. -In case you want to use the `LogEvent`, `Custom Log Formatter` features or serialize your own types when Logging events it is required -that you do some changes in your Lambda `Main` method. +!!! info + + In case you want to use the `LogEvent`, `Custom Log Formatter` features or serialize your own types when Logging events it is required + that you do some changes in your Lambda `Main` method. ### Configure From b6f7988942f31f34981217d22efcd5404c4aeb53 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:16:27 +0100 Subject: [PATCH 29/32] refactor AOT examples to each folder. Add Logging AOT examples and update Logging version for release --- .../src/AOT_Logging/AOT_Logging.csproj | 25 ++++++ .../AOT_Logging/src/AOT_Logging/Function.cs | 76 +++++++++++++++++ .../AOT/AOT_Logging/src/AOT_Logging/Readme.md | 82 +++++++++++++++++++ .../aws-lambda-tools-defaults.json | 16 ++++ .../AOT_Logging.Tests.csproj | 18 ++++ .../test/AOT_Logging.Tests/FunctionTest.cs | 18 ++++ .../src/AOT_Metrics/AOT_Metrics.csproj | 25 ++++++ .../AOT_Metrics/src/AOT_Metrics/Function.cs | 77 +++++++++++++++++ .../AOT/AOT_Metrics/src/AOT_Metrics/Readme.md | 82 +++++++++++++++++++ .../aws-lambda-tools-defaults.json | 16 ++++ .../AOT_Metrics.Tests.csproj | 18 ++++ .../test/AOT_Metrics.Tests/FunctionTest.cs | 18 ++++ .../src/AOT_Tracing/AOT_Tracing.csproj | 25 ++++++ .../AOT_Tracing/src/AOT_Tracing/Function.cs | 73 +++++++++++++++++ .../AOT/AOT_Tracing/src/AOT_Tracing/Readme.md | 82 +++++++++++++++++++ .../aws-lambda-tools-defaults.json | 16 ++++ .../AOT_Tracing.Tests.csproj | 18 ++++ .../test/AOT_Tracing.Tests/FunctionTest.cs | 18 ++++ 18 files changed, 703 insertions(+) create mode 100644 examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj create mode 100644 examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs create mode 100644 examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md create mode 100644 examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json create mode 100644 examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj create mode 100644 examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs create mode 100644 examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj create mode 100644 examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs create mode 100644 examples/AOT/AOT_Metrics/src/AOT_Metrics/Readme.md create mode 100644 examples/AOT/AOT_Metrics/src/AOT_Metrics/aws-lambda-tools-defaults.json create mode 100644 examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/AOT_Metrics.Tests.csproj create mode 100644 examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/FunctionTest.cs create mode 100644 examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj create mode 100644 examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs create mode 100644 examples/AOT/AOT_Tracing/src/AOT_Tracing/Readme.md create mode 100644 examples/AOT/AOT_Tracing/src/AOT_Tracing/aws-lambda-tools-defaults.json create mode 100644 examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/AOT_Tracing.Tests.csproj create mode 100644 examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/FunctionTest.cs diff --git a/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj b/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj new file mode 100644 index 00000000..b66d1aeb --- /dev/null +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj @@ -0,0 +1,25 @@ + + + Exe + net8.0 + enable + enable + Lambda + + true + + true + + true + + partial + + + + + + + + \ No newline at end of file diff --git a/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs b/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs new file mode 100644 index 00000000..8d39a4b8 --- /dev/null +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs @@ -0,0 +1,76 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AOT_Logging; + +public class Function +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, + new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + /// + /// A simple function that takes a string and does a ToUpper. + /// + /// To use this handler to respond to an AWS event, reference the appropriate package from + /// https://github.com/aws/aws-lambda-dotnet#events + /// and change the string input parameter to the desired event type. When the event type + /// is changed, the handler type registered in the main method needs to be updated and the LambdaFunctionJsonSerializerContext + /// defined below will need the JsonSerializable updated. If the return type and event type are different then the + /// LambdaFunctionJsonSerializerContext must have two JsonSerializable attributes, one for each type. + /// + // When using Native AOT extra testing with the deployed Lambda functions is required to ensure + // the libraries used in the Lambda function work correctly with Native AOT. If a runtime + // error occurs about missing types or methods the most likely solution will be to remove references to trim-unsafe + // code or configure trimming options. This sample defaults to partial TrimMode because currently the AWS + // SDK for .NET does not support trimming. This will result in a larger executable size, and still does not + // guarantee runtime trimming errors won't be hit. + /// + /// The event for the Lambda function handler to process. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// + [Logging(LogEvent = true, Service = "pt_service", LogLevel = LogLevel.Debug)] + public static string FunctionHandler(string input, ILambdaContext context) + { + Logger.LogInformation("FunctionHandler invocation"); + return ToUpper(input); + } + + private static string ToUpper(string input) + { + Logger.LogInformation("ToUpper invocation"); + + var upper = input.ToUpper(); + + Logger.LogInformation("ToUpper result: {Result}", upper); + + return upper; + } +} + +/// +/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. +/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur +/// from the JSON serializer unable to find the serialization information for unknown types. +/// +[JsonSerializable(typeof(string))] +public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext +{ + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation +} \ No newline at end of file diff --git a/examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md b/examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md new file mode 100644 index 00000000..62a1bc67 --- /dev/null +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md @@ -0,0 +1,82 @@ +# AWS Lambda Native AOT Project + +This starter project consists of: +* Function.cs - contains a class with a `Main` method that starts the bootstrap and a single function handler method. +* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS. + +You may also have a test project depending on the options selected. + +The `Main` function is called once during the Lambda init phase. It initializes the .NET Lambda runtime client passing in the function +handler to invoke for each Lambda event and the JSON serializer to use for converting Lambda JSON format to the .NET types. + +The function handler is a simple method accepting a string argument that returns the uppercase equivalent of the input string. Replace the body of this method and its parameters to suit your needs. + +## Native AOT + +Native AOT is a feature that compiles .NET assemblies into a single native executable. By using the native executable the .NET runtime +is not required to be installed on the target platform. Native AOT can significantly improve Lambda cold starts for .NET Lambda functions. +This project enables Native AOT by setting the .NET `PublishAot` property in the .NET project file to `true`. The `StripSymbols` property is also +set to `true` to strip debugging symbols from the deployed executable to reduce the executable's size. + +### Building Native AOT + +When publishing with Native AOT the build OS and Architecture must match the target platform that the application will run. For AWS Lambda that target +platform is Amazon Linux 2023. The AWS tooling for Lambda like the AWS Toolkit for Visual Studio, .NET Global Tool Amazon.Lambda.Tools and SAM CLI will +perform a container build using a .NET 8 Amazon Linux 2023 build image when `PublishAot` is set to `true`. This means **docker is a requirement** +when packaging .NET Native AOT Lambda functions on non-Amazon Linux 2023 build environments. To install docker go to https://www.docker.com/. + +### Trimming + +As part of the Native AOT compilation, .NET assemblies will be trimmed removing types and methods that the compiler does not find a reference to. This is important +to keep the native executable size small. When types are used through reflection this can go undetected by the compiler causing necessary types and methods to +be removed. When testing Native AOT Lambda functions in Lambda if a runtime error occurs about missing types or methods the most likely solution will +be to remove references to trim-unsafe code or configure [trimming options](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options). +This sample defaults to partial TrimMode because currently the AWS SDK for .NET does not support trimming. This will result in a larger executable size, and still does not +guarantee runtime trimming errors won't be hit. + +For information about trimming see the documentation: + +## Docker requirement + +Docker is required to be installed and running when building .NET Native AOT Lambda functions on any platform besides Amazon Linux 2023. Information on how acquire Docker can be found here: https://docs.docker.com/get-docker/ + +## Here are some steps to follow from Visual Studio: + +To deploy your function to AWS Lambda, right click the project in Solution Explorer and select *Publish to AWS Lambda*. + +To view your deployed function open its Function View window by double-clicking the function name shown beneath the AWS Lambda node in the AWS Explorer tree. + +To perform testing against your deployed function use the Test Invoke tab in the opened Function View window. + +To configure event sources for your deployed function, for example to have your function invoked when an object is created in an Amazon S3 bucket, use the Event Sources tab in the opened Function View window. + +To update the runtime configuration of your deployed function use the Configuration tab in the opened Function View window. + +To view execution logs of invocations of your function use the Logs tab in the opened Function View window. + +## Here are some steps to follow to get started from the command line: + +Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. Version 5.6.0 +or later is required to deploy this project. + +Install Amazon.Lambda.Tools Global Tools if not already installed. +``` + dotnet tool install -g Amazon.Lambda.Tools +``` + +If already installed check if new version is available. +``` + dotnet tool update -g Amazon.Lambda.Tools +``` + +Execute unit tests +``` + cd "AOT_Logging/test/AOT_Logging.Tests" + dotnet test +``` + +Deploy function to AWS Lambda +``` + cd "AOT_Logging/src/AOT_Logging" + dotnet lambda deploy-function +``` diff --git a/examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json b/examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..da01f0f5 --- /dev/null +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json @@ -0,0 +1,16 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet8", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "AOT_Logging", + "msbuild-parameters": "--self-contained true" +} \ No newline at end of file diff --git a/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj new file mode 100644 index 00000000..3d996e24 --- /dev/null +++ b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs new file mode 100644 index 00000000..b9b9b4e6 --- /dev/null +++ b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs @@ -0,0 +1,18 @@ +using Xunit; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; + +namespace AOT_Logging.Tests; + +public class FunctionTest +{ + [Fact] + public void TestToUpperFunction() + { + // Invoke the lambda function and confirm the string was upper cased. + var context = new TestLambdaContext(); + var upperCase = Function.FunctionHandler("hello world", context); + + Assert.Equal("HELLO WORLD", upperCase); + } +} \ No newline at end of file diff --git a/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj b/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj new file mode 100644 index 00000000..b038edb3 --- /dev/null +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj @@ -0,0 +1,25 @@ + + + Exe + net8.0 + enable + enable + Lambda + + true + + true + + true + + partial + + + + + + + + \ No newline at end of file diff --git a/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs new file mode 100644 index 00000000..4555552a --- /dev/null +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs @@ -0,0 +1,77 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Metrics; + +namespace AOT_Metrics; + +public class Function +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, + new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + /// + /// A simple function that takes a string and does a ToUpper. + /// + /// To use this handler to respond to an AWS event, reference the appropriate package from + /// https://github.com/aws/aws-lambda-dotnet#events + /// and change the string input parameter to the desired event type. When the event type + /// is changed, the handler type registered in the main method needs to be updated and the LambdaFunctionJsonSerializerContext + /// defined below will need the JsonSerializable updated. If the return type and event type are different then the + /// LambdaFunctionJsonSerializerContext must have two JsonSerializable attributes, one for each type. + /// + // When using Native AOT extra testing with the deployed Lambda functions is required to ensure + // the libraries used in the Lambda function work correctly with Native AOT. If a runtime + // error occurs about missing types or methods the most likely solution will be to remove references to trim-unsafe + // code or configure trimming options. This sample defaults to partial TrimMode because currently the AWS + // SDK for .NET does not support trimming. This will result in a larger executable size, and still does not + // guarantee runtime trimming errors won't be hit. + /// + /// The event for the Lambda function handler to process. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// + [Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true)] + public static string FunctionHandler(string input, ILambdaContext context) + { + Metrics.AddMetric("Handler invocation", 1, MetricUnit.Count); + return ToUpper(input); + } + + private static string ToUpper(string input) + { + Metrics.AddMetric("ToUpper invocation", 1, MetricUnit.Count); + + var upper = input.ToUpper(); + + // You can add high-cardinality data as part of your Metrics log with AddMetadata method. + // This is useful when you want to search highly contextual information along with your metrics in your logs. + Metrics.AddMetadata("Input Uppercase", upper); + + return upper; + } +} + +/// +/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. +/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur +/// from the JSON serializer unable to find the serialization information for unknown types. +/// +[JsonSerializable(typeof(string))] +public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext +{ + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation +} \ No newline at end of file diff --git a/examples/AOT/AOT_Metrics/src/AOT_Metrics/Readme.md b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Readme.md new file mode 100644 index 00000000..9b5076fc --- /dev/null +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Readme.md @@ -0,0 +1,82 @@ +# AWS Lambda Native AOT Project + +This starter project consists of: +* Function.cs - contains a class with a `Main` method that starts the bootstrap and a single function handler method. +* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS. + +You may also have a test project depending on the options selected. + +The `Main` function is called once during the Lambda init phase. It initializes the .NET Lambda runtime client passing in the function +handler to invoke for each Lambda event and the JSON serializer to use for converting Lambda JSON format to the .NET types. + +The function handler is a simple method accepting a string argument that returns the uppercase equivalent of the input string. Replace the body of this method and its parameters to suit your needs. + +## Native AOT + +Native AOT is a feature that compiles .NET assemblies into a single native executable. By using the native executable the .NET runtime +is not required to be installed on the target platform. Native AOT can significantly improve Lambda cold starts for .NET Lambda functions. +This project enables Native AOT by setting the .NET `PublishAot` property in the .NET project file to `true`. The `StripSymbols` property is also +set to `true` to strip debugging symbols from the deployed executable to reduce the executable's size. + +### Building Native AOT + +When publishing with Native AOT the build OS and Architecture must match the target platform that the application will run. For AWS Lambda that target +platform is Amazon Linux 2023. The AWS tooling for Lambda like the AWS Toolkit for Visual Studio, .NET Global Tool Amazon.Lambda.Tools and SAM CLI will +perform a container build using a .NET 8 Amazon Linux 2023 build image when `PublishAot` is set to `true`. This means **docker is a requirement** +when packaging .NET Native AOT Lambda functions on non-Amazon Linux 2023 build environments. To install docker go to https://www.docker.com/. + +### Trimming + +As part of the Native AOT compilation, .NET assemblies will be trimmed removing types and methods that the compiler does not find a reference to. This is important +to keep the native executable size small. When types are used through reflection this can go undetected by the compiler causing necessary types and methods to +be removed. When testing Native AOT Lambda functions in Lambda if a runtime error occurs about missing types or methods the most likely solution will +be to remove references to trim-unsafe code or configure [trimming options](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options). +This sample defaults to partial TrimMode because currently the AWS SDK for .NET does not support trimming. This will result in a larger executable size, and still does not +guarantee runtime trimming errors won't be hit. + +For information about trimming see the documentation: + +## Docker requirement + +Docker is required to be installed and running when building .NET Native AOT Lambda functions on any platform besides Amazon Linux 2023. Information on how acquire Docker can be found here: https://docs.docker.com/get-docker/ + +## Here are some steps to follow from Visual Studio: + +To deploy your function to AWS Lambda, right click the project in Solution Explorer and select *Publish to AWS Lambda*. + +To view your deployed function open its Function View window by double-clicking the function name shown beneath the AWS Lambda node in the AWS Explorer tree. + +To perform testing against your deployed function use the Test Invoke tab in the opened Function View window. + +To configure event sources for your deployed function, for example to have your function invoked when an object is created in an Amazon S3 bucket, use the Event Sources tab in the opened Function View window. + +To update the runtime configuration of your deployed function use the Configuration tab in the opened Function View window. + +To view execution logs of invocations of your function use the Logs tab in the opened Function View window. + +## Here are some steps to follow to get started from the command line: + +Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. Version 5.6.0 +or later is required to deploy this project. + +Install Amazon.Lambda.Tools Global Tools if not already installed. +``` + dotnet tool install -g Amazon.Lambda.Tools +``` + +If already installed check if new version is available. +``` + dotnet tool update -g Amazon.Lambda.Tools +``` + +Execute unit tests +``` + cd "AOT_Metrics/test/AOT_Metrics.Tests" + dotnet test +``` + +Deploy function to AWS Lambda +``` + cd "AOT_Metrics/src/AOT_Metrics" + dotnet lambda deploy-function +``` diff --git a/examples/AOT/AOT_Metrics/src/AOT_Metrics/aws-lambda-tools-defaults.json b/examples/AOT/AOT_Metrics/src/AOT_Metrics/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..57c8cc44 --- /dev/null +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/aws-lambda-tools-defaults.json @@ -0,0 +1,16 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet8", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "AOT_Metrics", + "msbuild-parameters": "--self-contained true" +} \ No newline at end of file diff --git a/examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/AOT_Metrics.Tests.csproj b/examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/AOT_Metrics.Tests.csproj new file mode 100644 index 00000000..34fa6d4c --- /dev/null +++ b/examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/AOT_Metrics.Tests.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/FunctionTest.cs b/examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/FunctionTest.cs new file mode 100644 index 00000000..d87fd40e --- /dev/null +++ b/examples/AOT/AOT_Metrics/test/AOT_Metrics.Tests/FunctionTest.cs @@ -0,0 +1,18 @@ +using Xunit; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; + +namespace AOT_Metrics.Tests; + +public class FunctionTest +{ + [Fact] + public void TestToUpperFunction() + { + // Invoke the lambda function and confirm the string was upper cased. + var context = new TestLambdaContext(); + var upperCase = Function.FunctionHandler("hello world", context); + + Assert.Equal("HELLO WORLD", upperCase); + } +} \ No newline at end of file diff --git a/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj b/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj new file mode 100644 index 00000000..31858475 --- /dev/null +++ b/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj @@ -0,0 +1,25 @@ + + + Exe + net8.0 + enable + enable + Lambda + + true + + true + + true + + partial + + + + + + + + \ No newline at end of file diff --git a/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs b/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs new file mode 100644 index 00000000..059fcb28 --- /dev/null +++ b/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs @@ -0,0 +1,73 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Tracing; + +namespace AOT_Tracing; + +public class Function +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, + new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + /// + /// A simple function that takes a string and does a ToUpper. + /// + /// To use this handler to respond to an AWS event, reference the appropriate package from + /// https://github.com/aws/aws-lambda-dotnet#events + /// and change the string input parameter to the desired event type. When the event type + /// is changed, the handler type registered in the main method needs to be updated and the LambdaFunctionJsonSerializerContext + /// defined below will need the JsonSerializable updated. If the return type and event type are different then the + /// LambdaFunctionJsonSerializerContext must have two JsonSerializable attributes, one for each type. + /// + // When using Native AOT extra testing with the deployed Lambda functions is required to ensure + // the libraries used in the Lambda function work correctly with Native AOT. If a runtime + // error occurs about missing types or methods the most likely solution will be to remove references to trim-unsafe + // code or configure trimming options. This sample defaults to partial TrimMode because currently the AWS + // SDK for .NET does not support trimming. This will result in a larger executable size, and still does not + // guarantee runtime trimming errors won't be hit. + /// + /// The event for the Lambda function handler to process. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// + [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] + public static string FunctionHandler(string input, ILambdaContext context) + { + return ToUpper(input); + } + + [Tracing(SegmentName = "ToUpper Call")] + private static string ToUpper(string input) + { + var upper = input.ToUpper(); + + Tracing.AddAnnotation("Upper text", upper); + + return upper; + } +} + +/// +/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. +/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur +/// from the JSON serializer unable to find the serialization information for unknown types. +/// +[JsonSerializable(typeof(string))] +public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext +{ + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation +} \ No newline at end of file diff --git a/examples/AOT/AOT_Tracing/src/AOT_Tracing/Readme.md b/examples/AOT/AOT_Tracing/src/AOT_Tracing/Readme.md new file mode 100644 index 00000000..6737b3ad --- /dev/null +++ b/examples/AOT/AOT_Tracing/src/AOT_Tracing/Readme.md @@ -0,0 +1,82 @@ +# AWS Lambda Native AOT Project + +This starter project consists of: +* Function.cs - contains a class with a `Main` method that starts the bootstrap and a single function handler method. +* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS. + +You may also have a test project depending on the options selected. + +The `Main` function is called once during the Lambda init phase. It initializes the .NET Lambda runtime client passing in the function +handler to invoke for each Lambda event and the JSON serializer to use for converting Lambda JSON format to the .NET types. + +The function handler is a simple method accepting a string argument that returns the uppercase equivalent of the input string. Replace the body of this method and its parameters to suit your needs. + +## Native AOT + +Native AOT is a feature that compiles .NET assemblies into a single native executable. By using the native executable the .NET runtime +is not required to be installed on the target platform. Native AOT can significantly improve Lambda cold starts for .NET Lambda functions. +This project enables Native AOT by setting the .NET `PublishAot` property in the .NET project file to `true`. The `StripSymbols` property is also +set to `true` to strip debugging symbols from the deployed executable to reduce the executable's size. + +### Building Native AOT + +When publishing with Native AOT the build OS and Architecture must match the target platform that the application will run. For AWS Lambda that target +platform is Amazon Linux 2023. The AWS tooling for Lambda like the AWS Toolkit for Visual Studio, .NET Global Tool Amazon.Lambda.Tools and SAM CLI will +perform a container build using a .NET 8 Amazon Linux 2023 build image when `PublishAot` is set to `true`. This means **docker is a requirement** +when packaging .NET Native AOT Lambda functions on non-Amazon Linux 2023 build environments. To install docker go to https://www.docker.com/. + +### Trimming + +As part of the Native AOT compilation, .NET assemblies will be trimmed removing types and methods that the compiler does not find a reference to. This is important +to keep the native executable size small. When types are used through reflection this can go undetected by the compiler causing necessary types and methods to +be removed. When testing Native AOT Lambda functions in Lambda if a runtime error occurs about missing types or methods the most likely solution will +be to remove references to trim-unsafe code or configure [trimming options](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options). +This sample defaults to partial TrimMode because currently the AWS SDK for .NET does not support trimming. This will result in a larger executable size, and still does not +guarantee runtime trimming errors won't be hit. + +For information about trimming see the documentation: + +## Docker requirement + +Docker is required to be installed and running when building .NET Native AOT Lambda functions on any platform besides Amazon Linux 2023. Information on how acquire Docker can be found here: https://docs.docker.com/get-docker/ + +## Here are some steps to follow from Visual Studio: + +To deploy your function to AWS Lambda, right click the project in Solution Explorer and select *Publish to AWS Lambda*. + +To view your deployed function open its Function View window by double-clicking the function name shown beneath the AWS Lambda node in the AWS Explorer tree. + +To perform testing against your deployed function use the Test Invoke tab in the opened Function View window. + +To configure event sources for your deployed function, for example to have your function invoked when an object is created in an Amazon S3 bucket, use the Event Sources tab in the opened Function View window. + +To update the runtime configuration of your deployed function use the Configuration tab in the opened Function View window. + +To view execution logs of invocations of your function use the Logs tab in the opened Function View window. + +## Here are some steps to follow to get started from the command line: + +Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. Version 5.6.0 +or later is required to deploy this project. + +Install Amazon.Lambda.Tools Global Tools if not already installed. +``` + dotnet tool install -g Amazon.Lambda.Tools +``` + +If already installed check if new version is available. +``` + dotnet tool update -g Amazon.Lambda.Tools +``` + +Execute unit tests +``` + cd "AOT_Tracing/test/AOT_Tracing.Tests" + dotnet test +``` + +Deploy function to AWS Lambda +``` + cd "AOT_Tracing/src/AOT_Tracing" + dotnet lambda deploy-function +``` diff --git a/examples/AOT/AOT_Tracing/src/AOT_Tracing/aws-lambda-tools-defaults.json b/examples/AOT/AOT_Tracing/src/AOT_Tracing/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..840bee55 --- /dev/null +++ b/examples/AOT/AOT_Tracing/src/AOT_Tracing/aws-lambda-tools-defaults.json @@ -0,0 +1,16 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet8", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "AOT_Tracing", + "msbuild-parameters": "--self-contained true" +} \ No newline at end of file diff --git a/examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/AOT_Tracing.Tests.csproj b/examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/AOT_Tracing.Tests.csproj new file mode 100644 index 00000000..2bdc9557 --- /dev/null +++ b/examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/AOT_Tracing.Tests.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/FunctionTest.cs b/examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/FunctionTest.cs new file mode 100644 index 00000000..8b890492 --- /dev/null +++ b/examples/AOT/AOT_Tracing/test/AOT_Tracing.Tests/FunctionTest.cs @@ -0,0 +1,18 @@ +using Xunit; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; + +namespace AOT_Tracing.Tests; + +public class FunctionTest +{ + [Fact] + public void TestToUpperFunction() + { + // Invoke the lambda function and confirm the string was upper cased. + var context = new TestLambdaContext(); + var upperCase = Function.FunctionHandler("hello world", context); + + Assert.Equal("HELLO WORLD", upperCase); + } +} \ No newline at end of file From cbb78fad6cbc2ba4d769c2f9e1ba0d8943adf47f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:17:24 +0100 Subject: [PATCH 30/32] update examples --- examples/AOT/src/AOT/AOT.csproj | 26 ------ examples/AOT/src/AOT/Function.cs | 86 ------------------- examples/AOT/src/AOT/Readme.md | 82 ------------------ .../src/AOT/aws-lambda-tools-defaults.json | 16 ---- examples/AOT/test/AOT.Tests/AOT.Tests.csproj | 18 ---- examples/AOT/test/AOT.Tests/FunctionTest.cs | 18 ---- examples/examples.sln | 81 +++++++++++++---- version.json | 2 +- 8 files changed, 66 insertions(+), 263 deletions(-) delete mode 100644 examples/AOT/src/AOT/AOT.csproj delete mode 100644 examples/AOT/src/AOT/Function.cs delete mode 100644 examples/AOT/src/AOT/Readme.md delete mode 100644 examples/AOT/src/AOT/aws-lambda-tools-defaults.json delete mode 100644 examples/AOT/test/AOT.Tests/AOT.Tests.csproj delete mode 100644 examples/AOT/test/AOT.Tests/FunctionTest.cs diff --git a/examples/AOT/src/AOT/AOT.csproj b/examples/AOT/src/AOT/AOT.csproj deleted file mode 100644 index 4905165b..00000000 --- a/examples/AOT/src/AOT/AOT.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - Exe - net8.0 - enable - enable - Lambda - - true - - true - - true - - partial - - - - - - - - - diff --git a/examples/AOT/src/AOT/Function.cs b/examples/AOT/src/AOT/Function.cs deleted file mode 100644 index 823b9d15..00000000 --- a/examples/AOT/src/AOT/Function.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Amazon.Lambda.Core; -using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.Serialization.SystemTextJson; -using System.Text.Json.Serialization; -using AWS.Lambda.Powertools.Metrics; -using AWS.Lambda.Powertools.Tracing; - -namespace AOT; - -public class Function -{ - /// - /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It - /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and - /// the JSON serializer to use for converting Lambda JSON format to the .NET types. - /// - private static async Task Main() - { - Func handler = FunctionHandler; - await LambdaBootstrapBuilder.Create(handler, - new SourceGeneratorLambdaJsonSerializer()) - .Build() - .RunAsync(); - } - - /// - /// A simple function that takes a string and does a ToUpper. - /// - /// To use this handler to respond to an AWS event, reference the appropriate package from - /// https://github.com/aws/aws-lambda-dotnet#events - /// and change the string input parameter to the desired event type. When the event type - /// is changed, the handler type registered in the main method needs to be updated and the LambdaFunctionJsonSerializerContext - /// defined below will need the JsonSerializable updated. If the return type and event type are different then the - /// LambdaFunctionJsonSerializerContext must have two JsonSerializable attributes, one for each type. - /// - // When using Native AOT extra testing with the deployed Lambda functions is required to ensure - // the libraries used in the Lambda function work correctly with Native AOT. If a runtime - // error occurs about missing types or methods the most likely solution will be to remove references to trim-unsafe - // code or configure trimming options. This sample defaults to partial TrimMode because currently the AWS - // SDK for .NET does not support trimming. This will result in a larger executable size, and still does not - // guarantee runtime trimming errors won't be hit. - /// - /// The event for the Lambda function handler to process. - /// The ILambdaContext that provides methods for logging and describing the Lambda environment. - /// - - // You can optionally capture cold start metrics by setting CaptureColdStart parameter to true. - [Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true)] - [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] - public static string FunctionHandler(string input, ILambdaContext context) - { - // You can create metrics using AddMetric - // MetricUnit enum facilitates finding a supported metric unit by CloudWatch. - Metrics.AddMetric("Handler invocation", 1, MetricUnit.Count); - return ToUpper(input); - } - - [Tracing(SegmentName = "ToUpper Call")] - private static string ToUpper(string input) - { - Metrics.AddMetric("ToUpper invocation", 1, MetricUnit.Count); - - var upper = input.ToUpper(); - - Tracing.AddAnnotation("Upper text", upper); - - // You can add high-cardinality data as part of your Metrics log with AddMetadata method. - // This is useful when you want to search highly contextual information along with your metrics in your logs. - Metrics.AddMetadata("Input Uppercase", upper); - - return upper; - } -} - -/// -/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. -/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur -/// from the JSON serializer unable to find the serialization information for unknown types. -/// -[JsonSerializable(typeof(string))] -public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext -{ - // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time - // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. - // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation -} \ No newline at end of file diff --git a/examples/AOT/src/AOT/Readme.md b/examples/AOT/src/AOT/Readme.md deleted file mode 100644 index 4cb2a367..00000000 --- a/examples/AOT/src/AOT/Readme.md +++ /dev/null @@ -1,82 +0,0 @@ -# AWS Lambda Native AOT Project with Powertools for AWS Lambda (.NET) - -This starter project consists of: -* Function.cs - contains a class with a `Main` method that starts the bootstrap and a single function handler method. -* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS. - -You may also have a test project depending on the options selected. - -The `Main` function is called once during the Lambda init phase. It initializes the .NET Lambda runtime client passing in the function -handler to invoke for each Lambda event and the JSON serializer to use for converting Lambda JSON format to the .NET types. - -The function handler is a simple method accepting a string argument that returns the uppercase equivalent of the input string. Replace the body of this method and its parameters to suit your needs. - -## Native AOT - -Native AOT is a feature that compiles .NET assemblies into a single native executable. By using the native executable the .NET runtime -is not required to be installed on the target platform. Native AOT can significantly improve Lambda cold starts for .NET Lambda functions. -This project enables Native AOT by setting the .NET `PublishAot` property in the .NET project file to `true`. The `StripSymbols` property is also -set to `true` to strip debugging symbols from the deployed executable to reduce the executable's size. - -### Building Native AOT - -When publishing with Native AOT the build OS and Architecture must match the target platform that the application will run. For AWS Lambda that target -platform is Amazon Linux 2023. The AWS tooling for Lambda like the AWS Toolkit for Visual Studio, .NET Global Tool Amazon.Lambda.Tools and SAM CLI will -perform a container build using a .NET 8 Amazon Linux 2023 build image when `PublishAot` is set to `true`. This means **docker is a requirement** -when packaging .NET Native AOT Lambda functions on non-Amazon Linux 2023 build environments. To install docker go to https://www.docker.com/. - -### Trimming - -As part of the Native AOT compilation, .NET assemblies will be trimmed removing types and methods that the compiler does not find a reference to. This is important -to keep the native executable size small. When types are used through reflection this can go undetected by the compiler causing necessary types and methods to -be removed. When testing Native AOT Lambda functions in Lambda if a runtime error occurs about missing types or methods the most likely solution will -be to remove references to trim-unsafe code or configure [trimming options](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options). -This sample defaults to partial TrimMode because currently the AWS SDK for .NET does not support trimming. This will result in a larger executable size, and still does not -guarantee runtime trimming errors won't be hit. - -For information about trimming see the documentation: - -## Docker requirement - -Docker is required to be installed and running when building .NET Native AOT Lambda functions on any platform besides Amazon Linux 2023. Information on how acquire Docker can be found here: https://docs.docker.com/get-docker/ - -## Here are some steps to follow from Visual Studio: - -To deploy your function to AWS Lambda, right click the project in Solution Explorer and select *Publish to AWS Lambda*. - -To view your deployed function open its Function View window by double-clicking the function name shown beneath the AWS Lambda node in the AWS Explorer tree. - -To perform testing against your deployed function use the Test Invoke tab in the opened Function View window. - -To configure event sources for your deployed function, for example to have your function invoked when an object is created in an Amazon S3 bucket, use the Event Sources tab in the opened Function View window. - -To update the runtime configuration of your deployed function use the Configuration tab in the opened Function View window. - -To view execution logs of invocations of your function use the Logs tab in the opened Function View window. - -## Here are some steps to follow to get started from the command line: - -Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. Version 5.6.0 -or later is required to deploy this project. - -Install Amazon.Lambda.Tools Global Tools if not already installed. -``` - dotnet tool install -g Amazon.Lambda.Tools -``` - -If already installed check if new version is available. -``` - dotnet tool update -g Amazon.Lambda.Tools -``` - -Execute unit tests -``` - cd "AOT/test/AOT.Tests" - dotnet test -``` - -Deploy function to AWS Lambda -``` - cd "AOT/src/AOT" - dotnet lambda deploy-function -``` diff --git a/examples/AOT/src/AOT/aws-lambda-tools-defaults.json b/examples/AOT/src/AOT/aws-lambda-tools-defaults.json deleted file mode 100644 index 2c40112f..00000000 --- a/examples/AOT/src/AOT/aws-lambda-tools-defaults.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "Information": [ - "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", - "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", - "dotnet lambda help", - "All the command line options for the Lambda command can be specified in this file." - ], - "profile": "", - "region": "", - "configuration": "Release", - "function-runtime": "dotnet8", - "function-memory-size": 512, - "function-timeout": 30, - "function-handler": "AOT", - "msbuild-parameters": "--self-contained true" -} \ No newline at end of file diff --git a/examples/AOT/test/AOT.Tests/AOT.Tests.csproj b/examples/AOT/test/AOT.Tests/AOT.Tests.csproj deleted file mode 100644 index 55ab099b..00000000 --- a/examples/AOT/test/AOT.Tests/AOT.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - net8.0 - enable - enable - true - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/AOT/test/AOT.Tests/FunctionTest.cs b/examples/AOT/test/AOT.Tests/FunctionTest.cs deleted file mode 100644 index 04bfbba7..00000000 --- a/examples/AOT/test/AOT.Tests/FunctionTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Xunit; -using Amazon.Lambda.Core; -using Amazon.Lambda.TestUtilities; - -namespace AOT.Tests; - -public class FunctionTest -{ - [Fact] - public void TestToUpperFunction() - { - // Invoke the lambda function and confirm the string was upper cased. - var context = new TestLambdaContext(); - var upperCase = Function.FunctionHandler("hello world", context); - - Assert.Equal("HELLO WORLD", upperCase); - } -} \ No newline at end of file diff --git a/examples/examples.sln b/examples/examples.sln index a6dc9358..10ec4850 100644 --- a/examples/examples.sln +++ b/examples/examples.sln @@ -79,13 +79,35 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld.Tests", "BatchPr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AOT", "AOT", "{F622EDE4-15EB-4F30-AC63-68E848377F1D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C765423A-C454-4ABA-B39D-0B527F9BA09A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logging", "Logging", "{C757C0D9-E9FF-41AA-872C-B85595E20E56}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E04644BA-719E-40D9-AF91-DA6D412059C7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FEE72EAB-494F-403B-A75A-825E713C3D43}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT", "AOT\src\AOT\AOT.csproj", "{0E9D6881-9B32-47C5-89CC-299754D3FD88}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F3480212-EE7F-46FE-9ED5-24ACAB5B681D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT.Tests", "AOT\test\AOT.Tests\AOT.Tests.csproj", "{489F6927-B761-4F11-B8A6-BBD848281698}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{5C8EDFEE-9BE9-41B9-A308-7B96C9E40DEB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tracing", "Tracing", "{68889B72-3C8A-4725-9384-578D0C3F5D00}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4297C676-EF52-4FA7-B16C-21D7074AA738}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{FE1CAA26-87E9-4B71-800E-81D2997A7B53}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{47A5118A-1511-46D4-84D2-57ECD9A1DB39}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{355D2932-13F0-4F26-A7A5-17A83F60BA0F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT_Tracing", "AOT\AOT_Tracing\src\AOT_Tracing\AOT_Tracing.csproj", "{EAE18C5F-57FD-46BC-946F-41E9E6E7E825}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT_Tracing.Tests", "AOT\AOT_Tracing\test\AOT_Tracing.Tests\AOT_Tracing.Tests.csproj", "{D627AAB8-813D-4B10-98A5-722095F73E00}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT_Metrics", "AOT\AOT_Metrics\src\AOT_Metrics\AOT_Metrics.csproj", "{FB0D0A21-FB50-4D8D-9DAD-563BBE87A2B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT_Metrics.Tests", "AOT\AOT_Metrics\test\AOT_Metrics.Tests\AOT_Metrics.Tests.csproj", "{343CF6B9-C006-43F8-924C-BF5BF5B6D051}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT_Logging", "AOT\AOT_Logging\src\AOT_Logging\AOT_Logging.csproj", "{FC02CF45-DE15-4413-958A-D86808B99146}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT_Logging.Tests", "AOT\AOT_Logging\test\AOT_Logging.Tests\AOT_Logging.Tests.csproj", "{FC010A0E-64A9-4440-97FE-DEDA8CEE0BE5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -156,14 +178,30 @@ Global {AAE50681-1FEF-4D9E-9FEA-5406320BDB88}.Debug|Any CPU.Build.0 = Debug|Any CPU {AAE50681-1FEF-4D9E-9FEA-5406320BDB88}.Release|Any CPU.ActiveCfg = Release|Any CPU {AAE50681-1FEF-4D9E-9FEA-5406320BDB88}.Release|Any CPU.Build.0 = Release|Any CPU - {0E9D6881-9B32-47C5-89CC-299754D3FD88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E9D6881-9B32-47C5-89CC-299754D3FD88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E9D6881-9B32-47C5-89CC-299754D3FD88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E9D6881-9B32-47C5-89CC-299754D3FD88}.Release|Any CPU.Build.0 = Release|Any CPU - {489F6927-B761-4F11-B8A6-BBD848281698}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {489F6927-B761-4F11-B8A6-BBD848281698}.Debug|Any CPU.Build.0 = Debug|Any CPU - {489F6927-B761-4F11-B8A6-BBD848281698}.Release|Any CPU.ActiveCfg = Release|Any CPU - {489F6927-B761-4F11-B8A6-BBD848281698}.Release|Any CPU.Build.0 = Release|Any CPU + {EAE18C5F-57FD-46BC-946F-41E9E6E7E825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAE18C5F-57FD-46BC-946F-41E9E6E7E825}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAE18C5F-57FD-46BC-946F-41E9E6E7E825}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAE18C5F-57FD-46BC-946F-41E9E6E7E825}.Release|Any CPU.Build.0 = Release|Any CPU + {D627AAB8-813D-4B10-98A5-722095F73E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D627AAB8-813D-4B10-98A5-722095F73E00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D627AAB8-813D-4B10-98A5-722095F73E00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D627AAB8-813D-4B10-98A5-722095F73E00}.Release|Any CPU.Build.0 = Release|Any CPU + {FB0D0A21-FB50-4D8D-9DAD-563BBE87A2B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB0D0A21-FB50-4D8D-9DAD-563BBE87A2B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB0D0A21-FB50-4D8D-9DAD-563BBE87A2B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB0D0A21-FB50-4D8D-9DAD-563BBE87A2B5}.Release|Any CPU.Build.0 = Release|Any CPU + {343CF6B9-C006-43F8-924C-BF5BF5B6D051}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {343CF6B9-C006-43F8-924C-BF5BF5B6D051}.Debug|Any CPU.Build.0 = Debug|Any CPU + {343CF6B9-C006-43F8-924C-BF5BF5B6D051}.Release|Any CPU.ActiveCfg = Release|Any CPU + {343CF6B9-C006-43F8-924C-BF5BF5B6D051}.Release|Any CPU.Build.0 = Release|Any CPU + {FC02CF45-DE15-4413-958A-D86808B99146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC02CF45-DE15-4413-958A-D86808B99146}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC02CF45-DE15-4413-958A-D86808B99146}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC02CF45-DE15-4413-958A-D86808B99146}.Release|Any CPU.Build.0 = Release|Any CPU + {FC010A0E-64A9-4440-97FE-DEDA8CEE0BE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC010A0E-64A9-4440-97FE-DEDA8CEE0BE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC010A0E-64A9-4440-97FE-DEDA8CEE0BE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC010A0E-64A9-4440-97FE-DEDA8CEE0BE5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {0CC66DBC-C1DF-4AF6-8EEB-FFED6C578BF4} = {526F1EF7-5A9C-4BFF-ABAE-75992ACD8F78} @@ -196,9 +234,20 @@ Global {F33D0918-452F-4AB0-B842-E43AFE6F948D} = {B95EAACA-FBE4-4CC0-B155-D0AD9BCDEE24} {CE5C821F-5610-490F-B096-EE91F0E34C10} = {2B5E8DE7-8DA4-47B8-81B7-9E269CC77619} {AAE50681-1FEF-4D9E-9FEA-5406320BDB88} = {CE5C821F-5610-490F-B096-EE91F0E34C10} - {C765423A-C454-4ABA-B39D-0B527F9BA09A} = {F622EDE4-15EB-4F30-AC63-68E848377F1D} - {E04644BA-719E-40D9-AF91-DA6D412059C7} = {F622EDE4-15EB-4F30-AC63-68E848377F1D} - {0E9D6881-9B32-47C5-89CC-299754D3FD88} = {C765423A-C454-4ABA-B39D-0B527F9BA09A} - {489F6927-B761-4F11-B8A6-BBD848281698} = {E04644BA-719E-40D9-AF91-DA6D412059C7} + {C757C0D9-E9FF-41AA-872C-B85595E20E56} = {F622EDE4-15EB-4F30-AC63-68E848377F1D} + {FEE72EAB-494F-403B-A75A-825E713C3D43} = {C757C0D9-E9FF-41AA-872C-B85595E20E56} + {F3480212-EE7F-46FE-9ED5-24ACAB5B681D} = {C757C0D9-E9FF-41AA-872C-B85595E20E56} + {5C8EDFEE-9BE9-41B9-A308-7B96C9E40DEB} = {F622EDE4-15EB-4F30-AC63-68E848377F1D} + {68889B72-3C8A-4725-9384-578D0C3F5D00} = {F622EDE4-15EB-4F30-AC63-68E848377F1D} + {4297C676-EF52-4FA7-B16C-21D7074AA738} = {5C8EDFEE-9BE9-41B9-A308-7B96C9E40DEB} + {FE1CAA26-87E9-4B71-800E-81D2997A7B53} = {5C8EDFEE-9BE9-41B9-A308-7B96C9E40DEB} + {47A5118A-1511-46D4-84D2-57ECD9A1DB39} = {68889B72-3C8A-4725-9384-578D0C3F5D00} + {355D2932-13F0-4F26-A7A5-17A83F60BA0F} = {68889B72-3C8A-4725-9384-578D0C3F5D00} + {EAE18C5F-57FD-46BC-946F-41E9E6E7E825} = {47A5118A-1511-46D4-84D2-57ECD9A1DB39} + {D627AAB8-813D-4B10-98A5-722095F73E00} = {355D2932-13F0-4F26-A7A5-17A83F60BA0F} + {FB0D0A21-FB50-4D8D-9DAD-563BBE87A2B5} = {4297C676-EF52-4FA7-B16C-21D7074AA738} + {343CF6B9-C006-43F8-924C-BF5BF5B6D051} = {FE1CAA26-87E9-4B71-800E-81D2997A7B53} + {FC02CF45-DE15-4413-958A-D86808B99146} = {FEE72EAB-494F-403B-A75A-825E713C3D43} + {FC010A0E-64A9-4440-97FE-DEDA8CEE0BE5} = {F3480212-EE7F-46FE-9ED5-24ACAB5B681D} EndGlobalSection EndGlobal diff --git a/version.json b/version.json index 8d794b87..bad6b158 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "Core": { - "Logging": "1.5.1", + "Logging": "1.6.0", "Metrics": "1.7.1", "Tracing": "1.5.1" }, From eabc183a6ddd48bd37bcc3f797c5f978e5ef4843 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:44:32 +0100 Subject: [PATCH 31/32] update doc and examples --- docs/core/logging.md | 4 ++++ examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj | 2 +- examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj | 2 +- examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index 6e2be26b..b2847348 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -677,6 +677,10 @@ Logging utility supports native AOT serialization by default without any changes In case you want to use the `LogEvent`, `Custom Log Formatter` features or serialize your own types when Logging events it is required that you do some changes in your Lambda `Main` method. +!!! info + + Starting from version 1.6.0 it is required to update `Amazon.Lambda.Serialization.SystemTextJson` to `version 2.4.3` in your `csproj`. + ### Configure The change needed is to replace `SourceGeneratorLambdaJsonSerializer` with `PowertoolsSourceGeneratorSerializer`. diff --git a/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj b/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj index b66d1aeb..f4b9fa5a 100644 --- a/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/AOT_Logging.csproj @@ -19,7 +19,7 @@ - + \ No newline at end of file diff --git a/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj b/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj index b038edb3..ec08ac52 100644 --- a/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj @@ -19,7 +19,7 @@ - + \ No newline at end of file diff --git a/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj b/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj index 31858475..be83d3c1 100644 --- a/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj +++ b/examples/AOT/AOT_Tracing/src/AOT_Tracing/AOT_Tracing.csproj @@ -19,7 +19,7 @@ - + \ No newline at end of file From e1d11816ae495e6f3b35e5c41b5ea84a243a7f71 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:48:44 +0100 Subject: [PATCH 32/32] fix sonar issues --- examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs | 2 +- examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs | 2 +- examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs b/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs index 8d39a4b8..3e153faa 100644 --- a/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/Function.cs @@ -7,7 +7,7 @@ namespace AOT_Logging; -public class Function +public static class Function { /// /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It diff --git a/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs index 4555552a..be669ed6 100644 --- a/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs @@ -6,7 +6,7 @@ namespace AOT_Metrics; -public class Function +public static class Function { /// /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It diff --git a/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs b/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs index 059fcb28..8fa435da 100644 --- a/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs +++ b/examples/AOT/AOT_Tracing/src/AOT_Tracing/Function.cs @@ -6,7 +6,7 @@ namespace AOT_Tracing; -public class Function +public static class Function { /// /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It