diff --git a/docs/core/logging.md b/docs/core/logging.md index 1e615362..b2847348 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 @@ -166,6 +173,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" @@ -656,3 +667,131 @@ 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. + +!!! 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. + +!!! 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`. + +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/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..f4b9fa5a --- /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..3e153faa --- /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 static 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/src/AOT/Readme.md b/examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md similarity index 97% rename from examples/AOT/src/AOT/Readme.md rename to examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md index 4cb2a367..62a1bc67 100644 --- a/examples/AOT/src/AOT/Readme.md +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md @@ -1,4 +1,4 @@ -# AWS Lambda Native AOT Project with Powertools for AWS Lambda (.NET) +# 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. @@ -71,12 +71,12 @@ If already installed check if new version is available. Execute unit tests ``` - cd "AOT/test/AOT.Tests" + cd "AOT_Logging/test/AOT_Logging.Tests" dotnet test ``` Deploy function to AWS Lambda ``` - cd "AOT/src/AOT" + cd "AOT_Logging/src/AOT_Logging" dotnet lambda deploy-function ``` diff --git a/examples/AOT/src/AOT/aws-lambda-tools-defaults.json b/examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json similarity index 94% rename from examples/AOT/src/AOT/aws-lambda-tools-defaults.json rename to examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json index 2c40112f..da01f0f5 100644 --- a/examples/AOT/src/AOT/aws-lambda-tools-defaults.json +++ b/examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json @@ -11,6 +11,6 @@ "function-runtime": "dotnet8", "function-memory-size": 512, "function-timeout": 30, - "function-handler": "AOT", + "function-handler": "AOT_Logging", "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/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj similarity index 89% rename from examples/AOT/test/AOT.Tests/AOT.Tests.csproj rename to examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj index 55ab099b..3d996e24 100644 --- a/examples/AOT/test/AOT.Tests/AOT.Tests.csproj +++ b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj @@ -13,6 +13,6 @@ - + \ No newline at end of file diff --git a/examples/AOT/test/AOT.Tests/FunctionTest.cs b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs similarity index 93% rename from examples/AOT/test/AOT.Tests/FunctionTest.cs rename to examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs index 04bfbba7..b9b9b4e6 100644 --- a/examples/AOT/test/AOT.Tests/FunctionTest.cs +++ b/examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs @@ -2,7 +2,7 @@ using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; -namespace AOT.Tests; +namespace AOT_Logging.Tests; public class FunctionTest { diff --git a/examples/AOT/src/AOT/AOT.csproj b/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj similarity index 92% rename from examples/AOT/src/AOT/AOT.csproj rename to examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj index 62f453ff..ec08ac52 100644 --- a/examples/AOT/src/AOT/AOT.csproj +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj @@ -19,8 +19,7 @@ - + - \ No newline at end of file diff --git a/examples/AOT/src/AOT/Function.cs b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs similarity index 89% rename from examples/AOT/src/AOT/Function.cs rename to examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs index 823b9d15..be669ed6 100644 --- a/examples/AOT/src/AOT/Function.cs +++ b/examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs @@ -3,11 +3,10 @@ using Amazon.Lambda.Serialization.SystemTextJson; using System.Text.Json.Serialization; using AWS.Lambda.Powertools.Metrics; -using AWS.Lambda.Powertools.Tracing; -namespace AOT; +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 @@ -43,27 +42,19 @@ await LambdaBootstrapBuilder.Create(handler, /// 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); 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..be83d3c1 --- /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..8fa435da --- /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 static 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 diff --git a/examples/BatchProcessing/src/HelloWorld/HelloWorld.csproj b/examples/BatchProcessing/src/HelloWorld/HelloWorld.csproj index 1d970ca9..90cb91fd 100644 --- a/examples/BatchProcessing/src/HelloWorld/HelloWorld.csproj +++ b/examples/BatchProcessing/src/HelloWorld/HelloWorld.csproj @@ -7,7 +7,7 @@ - + diff --git a/examples/Idempotency/src/HelloWorld/HelloWorld.csproj b/examples/Idempotency/src/HelloWorld/HelloWorld.csproj index 2fb80281..0e30573a 100644 --- a/examples/Idempotency/src/HelloWorld/HelloWorld.csproj +++ b/examples/Idempotency/src/HelloWorld/HelloWorld.csproj @@ -8,7 +8,7 @@ - + diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj index 4b8d299c..fd8e3c25 100644 --- a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj @@ -15,6 +15,6 @@ - + diff --git a/examples/Tracing/src/HelloWorld/HelloWorld.csproj b/examples/Tracing/src/HelloWorld/HelloWorld.csproj index 093f53ae..63343996 100644 --- a/examples/Tracing/src/HelloWorld/HelloWorld.csproj +++ b/examples/Tracing/src/HelloWorld/HelloWorld.csproj @@ -9,7 +9,7 @@ - + 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/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.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 53d03f99..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 @@ -12,8 +12,10 @@ - + + + - + 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..e57e4159 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; @@ -57,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(); @@ -76,17 +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; - default: - writer.WritePropertyName(ApplyPropertyNamingPolicy(prop.Name, options)); - JsonSerializer.Serialize(writer, prop.Value, options); + case string 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..80751d3e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionPropertyExtractor.cs @@ -0,0 +1,44 @@ +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 +{ + /// + /// The property extractors + /// + private static readonly Dictionary>>> PropertyExtractors = new() + { + { typeof(Exception), GetBaseExceptionProperties }, + }; + + /// + /// 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) + { + 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); + } +} \ No newline at end of file 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..4404a3a2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs @@ -0,0 +1,56 @@ +/* + * 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.Collections.Generic; +using System.Linq; + +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 == null) + { + return new Dictionary(); + } + + if (anonymousObject.GetType().Namespace is not null) + { + return anonymousObject; + } + + return anonymousObject.GetType().GetProperties() + .Where(prop => prop.GetValue(anonymousObject, null) != null) + .ToDictionary( + prop => prop.Name, + prop => { + var value = prop.GetValue(anonymousObject, null); + return value != null ? ObjectToDictionary(value) : string.Empty; + } + ); + } +} \ 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..94bb1c0d 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,6 +27,16 @@ 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 /// @@ -34,18 +44,24 @@ public sealed class LoggerProvider : ILoggerProvider /// - /// The current configuration + /// Initializes a new instance of the class. /// - private LoggerConfiguration _currentConfig; - + /// The configuration. + /// + /// + public LoggerProvider(IOptions config, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) + { + _powertoolsConfigurations = powertoolsConfigurations; + _systemWrapper = systemWrapper; + _powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper); + } + /// /// Initializes a new instance of the class. /// /// The configuration. public LoggerProvider(IOptions config) - { - _currentConfig = config?.Value; - } + : this(config, PowertoolsConfigurations.Instance, SystemWrapper.Instance) { } /// /// Creates a new instance. @@ -55,10 +71,9 @@ public LoggerProvider(IOptions config) public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, - name => new PowertoolsLogger(name, - PowertoolsConfigurations.Instance, - SystemWrapper.Instance, - GetCurrentConfig)); + name => PowertoolsLogger.CreateLogger(name, + _powertoolsConfigurations, + _systemWrapper)); } /// @@ -68,27 +83,4 @@ public void Dispose() { _loggers.Clear(); } - - /// - /// Gets the current configuration. - /// - /// LoggerConfiguration. - private LoggerConfiguration GetCurrentConfig() - { - return _currentConfig; - } - - /// - /// 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 new file mode 100644 index 00000000..5a107831 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -0,0 +1,328 @@ +/* + * 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.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.Serializers; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Logging Aspect +/// Scope.Global is singleton +/// +/// +[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] +public class LoggingAspect +{ + /// + /// The is cold start + /// + private bool _isColdStart = true; + + /// + /// The initialize context + /// + private bool _initializeContext = true; + + /// + /// Clear state? + /// + private bool _clearState; + + /// + /// The correlation identifier path + /// + private string _correlationIdPath; + + /// + /// The Powertools for AWS Lambda (.NET) configurations + /// + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + + /// + /// The system wrapper + /// + private readonly ISystemWrapper _systemWrapper; + + /// + /// The is context initialized + /// + private bool _isContextInitialized; + + /// + /// Specify to clear Lambda Context on exit + /// + private bool _clearLambdaContext; + + /// + /// The configuration + /// + private LoggerConfiguration _config; + + /// + /// Initializes a new instance of the class. + /// + /// The Powertools configurations. + /// The system wrapper. + public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) + { + _powertoolsConfigurations = powertoolsConfigurations; + _systemWrapper = systemWrapper; + } + + /// + /// Runs before the execution of the method marked with the Logging Attribute + /// + /// + /// + /// + /// + /// + /// + /// + [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(); + + try + { + 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, + MinimumLevel = trigger.LogLevel + }; + + var logEvent = trigger.LogEvent; + _correlationIdPath = trigger.CorrelationIdPath; + _clearState = trigger.ClearState; + + Logger.LoggerProvider ??= new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); + + if (!_initializeContext) + return; + + Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + + _isColdStart = false; + _initializeContext = false; + _isContextInitialized = true; + + 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(); + } + } + + /// + /// Handles the Kind.After event. + /// + [Advice(Kind.After)] + public void OnExit() + { + if (!_isContextInitialized) + return; + if (_clearLambdaContext) + LoggingLambdaContext.Clear(); + if (_clearState) + Logger.RemoveAllKeys(); + _initializeContext = true; + } + + /// + /// Determines whether this instance is debug. + /// + /// true if this instance is debug; otherwise, false. + private bool IsDebug() + { + return LogLevel.Debug >= _powertoolsConfigurations.GetLogLevel(_config.MinimumLevel); + } + + /// + /// Captures the xray trace identifier. + /// + private void CaptureXrayTraceId() + { + var xRayTraceId = _powertoolsConfigurations.XRayTraceId; + if (string.IsNullOrWhiteSpace(xRayTraceId)) + return; + + xRayTraceId = xRayTraceId + .Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", ""); + + Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xRayTraceId); + } + + /// + /// Captures the lambda context. + /// + /// + /// The instance containing the + /// event data. + /// + private void CaptureLambdaContext(AspectEventArgs eventArgs) + { + _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs); + if (LoggingLambdaContext.Instance is null && IsDebug()) + _systemWrapper.LogLine( + "Skipping Lambda Context injection because ILambdaContext context parameter not found."); + } + + /// + /// Captures the correlation identifier. + /// + /// The event argument. + private void CaptureCorrelationId(object eventArg) + { + if (string.IsNullOrWhiteSpace(_correlationIdPath)) + return; + + var correlationIdPaths = _correlationIdPath + .Split(CorrelationIdPaths.Separator, StringSplitOptions.RemoveEmptyEntries); + + if (!correlationIdPaths.Any()) + return; + + if (eventArg is null) + { + if (IsDebug()) + _systemWrapper.LogLine( + "Skipping CorrelationId capture because event parameter not found."); + return; + } + + try + { + var correlationId = string.Empty; + + var jsonDoc = + JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); + + var element = jsonDoc.RootElement; + + 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)) + break; + + element = childElement; + if (i == correlationIdPaths.Length - 1) + correlationId = element.ToString(); + } + + if (!string.IsNullOrWhiteSpace(correlationId)) + Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); + } + catch (Exception e) + { + if (IsDebug()) + _systemWrapper.LogLine( + $"Skipping CorrelationId capture because of error caused while parsing the event object {e.Message}."); + } + } + + /// + /// Logs the event. + /// + /// The event argument. + private void LogEvent(object eventArg) + { + switch (eventArg) + { + case null: + { + if (IsDebug()) + _systemWrapper.LogLine( + "Skipping Event Log because event parameter not found."); + break; + } + case Stream: + try + { + Logger.LogInformation(eventArg); + } + catch (Exception e) + { + Logger.LogError(e, "Failed to log event from supplied input stream."); + } + + break; + default: + try + { + Logger.LogInformation(eventArg); + } + catch (Exception e) + { + Logger.LogError(e, "Failed to log event from supplied input object."); + } + + break; + } + } + + /// + /// Resets for test. + /// + internal static void ResetForTest() + { + LoggingLambdaContext.Clear(); + Logger.LoggerProvider = null; + Logger.RemoveAllKeys(); + Logger.ClearLoggerInstance(); + } +} \ No newline at end of file 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..5feae3cf --- /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 static 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/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs deleted file mode 100644 index 441cdc22..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ /dev/null @@ -1,397 +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.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 Microsoft.Extensions.Logging; - -namespace AWS.Lambda.Powertools.Logging.Internal; - -/// -/// Class LoggingAspectHandler. -/// Implements the -/// -/// -internal class LoggingAspectHandler : IMethodAspectHandler -{ - /// - /// The is cold start - /// - private static bool _isColdStart = true; - - /// - /// The initialize context - /// - private static bool _initializeContext = true; - - /// - /// Clear state? - /// - private readonly bool _clearState; - - /// - /// The correlation identifier path - /// - private readonly string _correlationIdPath; - - /// - /// The log event - /// - private readonly bool? _logEvent; - - /// - /// The log level - /// - 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 - /// - private readonly ISystemWrapper _systemWrapper; - - /// - /// 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. - /// - /// The current configuration. - private static JsonSerializerOptions JsonSerializerOptions => - _jsonSerializerOptions ??= BuildJsonSerializerOptions(); - - /// - /// 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]. - /// The Powertools configurations. - /// The system wrapper. - internal LoggingAspectHandler - ( - string service, - LogLevel? logLevel, - LoggerOutputCase? loggerOutputCase, - double? samplingRate, - bool? logEvent, - string correlationIdPath, - bool clearState, - IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper - ) - { - _service = service; - _logLevel = logLevel; - _loggerOutputCase = loggerOutputCase; - _samplingRate = samplingRate; - _logEvent = logEvent; - _clearState = clearState; - _correlationIdPath = correlationIdPath; - _powertoolsConfigurations = powertoolsConfigurations; - _systemWrapper = systemWrapper; - } - - /// - /// Handles the event. - /// - /// - /// The instance containing the - /// event data. - /// - 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); - break; - case LoggerProvider: - ((LoggerProvider) Logger.LoggerProvider).Configure(loggerConfig); - break; - } - - if (!_initializeContext) - return; - - Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); - - _isColdStart = false; - _initializeContext = false; - _isContextInitialized = true; - - var eventObject = eventArgs.Args.FirstOrDefault(); - CaptureXrayTraceId(); - CaptureLambdaContext(eventArgs); - CaptureCorrelationId(eventObject); - if (_logEvent ?? _powertoolsConfigurations.LoggerLogEvent) - LogEvent(eventObject); - } - - /// - /// Called when [success]. - /// - /// - /// The instance containing the - /// event data. - /// - /// The result. - public void OnSuccess(AspectEventArgs eventArgs, object result) - { - } - - /// - /// 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(); - } - - /// - /// Handles the event. - /// - /// - /// The instance containing the - /// event data. - /// - public void OnExit(AspectEventArgs eventArgs) - { - if (!_isContextInitialized) - return; - if (_clearLambdaContext) - PowertoolsLambdaContext.Clear(); - if (_clearState) - Logger.RemoveAllKeys(); - _initializeContext = true; - } - - /// - /// Determines whether this instance is debug. - /// - /// true if this instance is debug; otherwise, false. - private bool IsDebug() - { - return LogLevel.Debug >= _powertoolsConfigurations.GetLogLevel(_logLevel); - } - - /// - /// Captures the xray trace identifier. - /// - private void CaptureXrayTraceId() - { - var xRayTraceId = _powertoolsConfigurations.XRayTraceId; - if (string.IsNullOrWhiteSpace(xRayTraceId)) - return; - - xRayTraceId = xRayTraceId - .Split(';', StringSplitOptions.RemoveEmptyEntries) - .First() - .Replace("Root=", ""); - - Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xRayTraceId); - } - - /// - /// Captures the lambda context. - /// - /// - /// The instance containing the - /// event data. - /// - private void CaptureLambdaContext(AspectEventArgs eventArgs) - { - _clearLambdaContext = PowertoolsLambdaContext.Extract(eventArgs); - if (PowertoolsLambdaContext.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()); - return jsonOptions; - } - - /// - /// Captures the correlation identifier. - /// - /// The event argument. - private void CaptureCorrelationId(object eventArg) - { - if (string.IsNullOrWhiteSpace(_correlationIdPath)) - return; - - var correlationIdPaths = _correlationIdPath - .Split(CorrelationIdPaths.Separator, StringSplitOptions.RemoveEmptyEntries); - - if (!correlationIdPaths.Any()) - return; - - if (eventArg is null) - { - if (IsDebug()) - _systemWrapper.LogLine( - "Skipping CorrelationId capture because event parameter not found."); - return; - } - - try - { - var correlationId = string.Empty; - var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions)); - var element = jsonDoc.RootElement; - - for (var i = 0; i < correlationIdPaths.Length; i++) - { - if (!element.TryGetProperty(correlationIdPaths[i], out var childElement)) - break; - - element = childElement; - if (i == correlationIdPaths.Length - 1) - correlationId = element.ToString(); - } - - if (!string.IsNullOrWhiteSpace(correlationId)) - Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); - } - catch (Exception e) - { - if (IsDebug()) - _systemWrapper.LogLine( - $"Skipping CorrelationId capture because of error caused while parsing the event object {e.Message}."); - } - } - - /// - /// Logs the event. - /// - /// The event argument. - private void LogEvent(object eventArg) - { - switch (eventArg) - { - case null: - { - if (IsDebug()) - _systemWrapper.LogLine( - "Skipping Event Log because event parameter not found."); - break; - } - case Stream: - try - { - Logger.LogInformation(eventArg); - } - catch (Exception e) - { - Logger.LogError(e, "Failed to log event from supplied input stream."); - } - - break; - default: - try - { - Logger.LogInformation(eventArg); - } - catch (Exception e) - { - Logger.LogError(e, "Failed to log event from supplied input object."); - } - - break; - } - } - - /// - /// Resets for test. - /// - internal static void ResetForTest() - { - _isColdStart = true; - _initializeContext = true; - PowertoolsLambdaContext.Clear(); - Logger.LoggerProvider = null; - Logger.RemoveAllKeys(); - } -} \ No newline at end of file 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 deleted file mode 100644 index 4d39cb16..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs +++ /dev/null @@ -1,79 +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 AWS.Lambda.Powertools.Common; -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 Dictionary AwsLogLevelMapper = new() - { - { "TRACE", "TRACE" }, - { "DEBUG", "DEBUG" }, - { "INFO", "INFORMATION" }, - { "WARN", "WARNING" }, - { "ERROR", "ERROR" }, - { "FATAL", "CRITICAL" } - }; -} \ 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..148bb540 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -0,0 +1,331 @@ +/* + * 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 System.Text; +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 +{ + 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. + /// + /// The Powertools for AWS Lambda (.NET) configurations. + /// The log level. + /// LogLevel. + internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations, LogLevel logLevel = LogLevel.None) + { + if (logLevel != LogLevel.None) + return logLevel; + + 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) + { + var awsLogLevel = (powertoolsConfigurations.AWSLambdaLogLevel ?? string.Empty).Trim().ToUpperInvariant(); + + return AwsLogLevelMapper.GetValueOrDefault(awsLogLevel, 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) + { + if (loggerOutputCase != LoggerOutputCase.Default) + return loggerOutputCase; + + if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result)) + return result; + + return LoggingConstants.DefaultLoggerOutputCase; + } + + /// + /// Gets the current configuration. + /// + /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. + internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper) + { + lock (_lock) + { + _config = 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."); + } + + // 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; + + // Set sampling rate + SetSamplingRate(powertoolsConfigurations, systemWrapper, minLogLevel); + } + } + + /// + /// Set sampling rate + /// + /// + /// + /// + /// + private static void SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, LogLevel minLogLevel) + { + var samplingRate = _config.SamplingRate > 0 ? _config.SamplingRate : powertoolsConfigurations.LoggerSampleRate; + samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, systemWrapper); + + _config.SamplingRate = samplingRate; + + if (samplingRate > 0) + { + double sample = systemWrapper.GetRandom(); + + if (sample <= samplingRate) + { + systemWrapper.LogLine($"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + _config.MinimumLevel = LogLevel.Debug; + } + } + } + + /// + /// 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; + } + + return samplingRate; + } + + /// + /// Determines whether [is lambda log level enabled]. + /// + /// + /// + internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powertoolsConfigurations) + { + 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) + { + 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); + } + + /// + /// 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 e723ed22..6e72d102 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 @@ -16,10 +16,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Encodings.Web; -using System.Text.Json; +using System.Runtime.CompilerServices; 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; @@ -31,23 +31,13 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal sealed class PowertoolsLogger : ILogger { - /// - /// The get current configuration - /// - private readonly Func _getCurrentConfig; - /// /// The name /// private readonly string _name; - - /// - /// The current configuration - /// - private LoggerConfiguration _currentConfig; /// - /// The Powertools for AWS Lambda (.NET) configurations + /// The current configuration /// private readonly IPowertoolsConfigurations _powertoolsConfigurations; @@ -55,69 +45,42 @@ internal sealed class PowertoolsLogger : ILogger /// The system wrapper /// private readonly ISystemWrapper _systemWrapper; - + /// - /// The JsonSerializer options + /// The current scope /// - private JsonSerializerOptions _jsonSerializerOptions; - - private LogLevel _lambdaLogLevel; - private LogLevel _logLevel; - private bool _lambdaLogLevelEnabled; + internal PowertoolsLoggerScope CurrentScope { get; private set; } /// - /// Initializes a new instance of the class. + /// Private constructor - Is initialized on CreateLogger /// /// The name. /// The Powertools for AWS Lambda (.NET) configurations. /// The system wrapper. - /// The get current configuration. - public PowertoolsLogger( + private PowertoolsLogger( string name, IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper, - Func getCurrentConfig) + ISystemWrapper systemWrapper) { - (_name, _powertoolsConfigurations, _systemWrapper, _getCurrentConfig) = (name, - powertoolsConfigurations, systemWrapper, getCurrentConfig); - + _name = name; + _powertoolsConfigurations = powertoolsConfigurations; + _systemWrapper = systemWrapper; + _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; - - /// - /// Sets the service. - /// - /// The service. - private string Service => - !string.IsNullOrWhiteSpace(CurrentConfig.Service) - ? CurrentConfig.Service - : _powertoolsConfigurations.Service; - /// - /// Get JsonSerializer options. + /// Initializes a new instance of the class. /// - /// The current configuration. - private JsonSerializerOptions JsonSerializerOptions => - _jsonSerializerOptions ??= BuildJsonSerializerOptions(); - - 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. @@ -139,61 +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. @@ -219,11 +134,11 @@ 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); - _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions)); + _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); } /// @@ -236,20 +151,18 @@ 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()) + { 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); + AddLambdaContextKeys(logEntry); } // Add Extra Fields @@ -262,21 +175,15 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } - var keyLogLevel = LoggingConstants.KeyLogLevel; - // 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.HasValue) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value); + if (_powertoolsConfigurations.CurrentConfig().SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, _powertoolsConfigurations.CurrentConfig().SamplingRate); if (exception != null) logEntry.TryAdd(LoggingConstants.KeyException, exception); @@ -284,7 +191,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } /// - /// Gets a formatted log entry. + /// Gets a formatted log entry. For custom log formatter /// /// Entry will be written on this level. /// Entry timestamp. @@ -301,11 +208,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(); @@ -344,16 +251,9 @@ 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, - }; + logEntry.LambdaContext = CreateLambdaContext(); } try @@ -361,7 +261,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) { @@ -370,67 +274,6 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec } } - /// - /// Clears the configuration. - /// - 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. /// @@ -439,14 +282,20 @@ private LoggerConfiguration GetCurrentConfig() /// 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; 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,41 +307,96 @@ private static bool CustomFormatter(TState state, Exception exception, o return false; message = stateKeys.First(k => k.Key != "{OriginalFormat}").Value; + 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; + } + /// - /// Builds JsonSerializer options. + /// Adds the lambda context keys. /// - private JsonSerializerOptions BuildJsonSerializerOptions() + /// The log entry. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddLambdaContextKeys(Dictionary logEntry) { - var jsonOptions = CurrentConfig.LoggerOutputCase switch + 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 { - 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 - } + FunctionName = context.FunctionName, + FunctionVersion = context.FunctionVersion, + MemoryLimitInMB = context.MemoryLimitInMB, + InvokedFunctionArn = context.InvokedFunctionArn, + AwsRequestId = context.AwsRequestId, }; - 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; - - return jsonOptions; + } + + /// + /// 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/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 5b55acc5..4271de83 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; @@ -52,7 +53,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. @@ -65,6 +66,7 @@ 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); @@ -93,14 +95,13 @@ 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); + +#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 } /// @@ -131,8 +132,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); } /// @@ -141,7 +142,7 @@ public static void RemoveKeys(params string[] keys) /// IEnumerable<KeyValuePair<System.String, System.Object>>. public static IEnumerable> GetAllKeys() { - return _scope.AsEnumerable(); + return Scope.AsEnumerable(); } /// @@ -149,7 +150,12 @@ public static IEnumerable> GetAllKeys() /// internal static void RemoveAllKeys() { - _scope.Clear(); + Scope.Clear(); + } + + internal static void ClearLoggerInstance() + { + _loggerInstance = null; } #endregion @@ -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); } @@ -1166,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/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs index 4d8ec0c8..aab959af 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs @@ -38,14 +38,14 @@ 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. /// 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; } = LoggerOutputCase.Default; } \ 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 d60ee045..4a5da930 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -14,7 +14,7 @@ */ using System; -using AWS.Lambda.Powertools.Common; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -116,28 +116,9 @@ namespace AWS.Lambda.Powertools.Logging; /// /// [AttributeUsage(AttributeTargets.Method)] -public class LoggingAttribute : MethodAspectAttribute +[Injection(typeof(LoggingAspect))] +public class LoggingAttribute : Attribute { - /// - /// The log event - /// - private bool? _logEvent; - - /// - /// The log level - /// - private LogLevel? _logLevel; - - /// - /// The logger output case - /// - private LoggerOutputCase? _loggerOutputCase; - - /// - /// The sampling rate - /// - private double? _samplingRate; - /// /// Service name is used for logging. /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. @@ -150,22 +131,14 @@ public class LoggingAttribute : MethodAspectAttribute /// 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; } = LogLevel.None; /// /// Dynamically set a percentage of logs to DEBUG level. /// 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, @@ -173,11 +146,7 @@ public double SamplingRate /// 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. @@ -195,35 +164,11 @@ 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; - } - - /// - /// Creates the handler. - /// - /// IMethodAspectHandler. - protected override IMethodAspectHandler CreateHandler() - { - return new LoggingAspectHandler - ( - Service, - _logLevel, - _loggerOutputCase, - _samplingRate, - _logEvent, - CorrelationIdPath, - ClearState, - PowertoolsConfigurations.Instance, - SystemWrapper.Instance - ); - } + 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 new file mode 100644 index 00000000..28692b8e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs @@ -0,0 +1,48 @@ +/* + * 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; + +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(IEnumerable))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Byte[]))] +[JsonSerializable(typeof(MemoryStream))] +[JsonSerializable(typeof(LogEntry))] +public partial class PowertoolsLoggingSerializationContext : JsonSerializerContext +{ +} + + +#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..ef6085d9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -0,0 +1,185 @@ +/* + * 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.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +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; + +namespace AWS.Lambda.Powertools.Logging.Serializers; + +/// +/// Provides serialization functionality for Powertools logging. +/// +internal static class PowertoolsLoggingSerializer +{ + private static LoggerOutputCase _currentOutputCase; + private static JsonSerializerOptions _jsonOptions; + + private static readonly ConcurrentBag AdditionalContexts = + new ConcurrentBag(); + + /// + /// Gets the JsonSerializerOptions instance. + /// + internal static JsonSerializerOptions GetSerializerOptions() + { + return _jsonOptions ?? BuildJsonSerializerOptions(); + } + + /// + /// Configures the naming policy for the serializer. + /// + /// The case to use for serialization. + internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) + { + _currentOutputCase = loggerOutputCase; + } + + /// + /// 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. + 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) + { + throw new JsonSerializerException( + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); + } + + return JsonSerializer.Serialize(value, typeInfo); +#endif + } + +#if NET8_0_OR_GREATER + /// + /// 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); + } + } + + /// + /// 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) + { + var options = GetSerializerOptions(); + return options.TypeInfoResolver?.GetTypeInfo(type, options); + } +#endif + + /// + /// Builds and configures the JsonSerializerOptions. + /// + /// A configured JsonSerializerOptions instance. + private static JsonSerializerOptions BuildJsonSerializerOptions() + { + _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 +#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()); + +#if NET8_0_OR_GREATER + _jsonOptions.Converters.Add(new JsonStringEnumConverter()); +#elif NET6_0 + _jsonOptions.Converters.Add(new JsonStringEnumConverter()); +#endif + + _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + _jsonOptions.PropertyNameCaseInsensitive = true; + +#if NET8_0_OR_GREATER + _jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); + foreach (var context in AdditionalContexts) + { + _jsonOptions.TypeInfoResolverChain.Add(context); + } +#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 + + /// + /// 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..dadec8da --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -0,0 +1,81 @@ +/* + * 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; + +/// +/// 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 +{ + /// + /// Constructs instance of serializer. + /// + public PowertoolsSourceGeneratorSerializer() + : this((Action)null) + { + } + + /// + /// 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) + { + Logger.UseFormatter(logFormatter); + } + + /// + /// 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 JsonSerializerException( + $"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.Build.props b/libraries/src/Directory.Build.props index 32a59a04..ab02fae1 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -21,7 +21,7 @@ - + true true true 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.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/AWS.Lambda.Powertools.Logging.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/AWS.Lambda.Powertools.Logging.Tests.csproj index f98e40b6..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 @@ -11,7 +11,9 @@ + + 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 new file mode 100644 index 00000000..ba08453f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -0,0 +1,311 @@ +/* + * 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.Handlers; +using AWS.Lambda.Powertools.Logging.Tests.Serializers; +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 + PowertoolsLoggingSerializer.AddSerializerContext(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(TestHandlers).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 + PowertoolsLoggingSerializer.AddSerializerContext(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(TestHandlers).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 + PowertoolsLoggingSerializer.AddSerializerContext(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(TestHandlers).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\"}}") + )); + } + + [Fact] + public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_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 } }; + var hostType = typeof(string); + var method = typeof(TestHandlers).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + + 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()); + } + + [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(TestHandlers).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 new file mode 100644 index 00000000..76006713 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -0,0 +1,482 @@ +/* + * 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.Linq; +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; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using AWS.Lambda.Powertools.Logging.Tests.Serializers; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Attributes +{ + [Collection("Sequential")] + public class LoggingAttributeTests : IDisposable + { + private TestHandlers _testHandlers; + + public LoggingAttributeTests() + { + _testHandlers = new TestHandlers(); + } + + [Fact] + public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testHandlers.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 + _testHandlers.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 + _testHandlers.LogEventNoArgs(); + + consoleOut.DidNotReceive().WriteLine( + Arg.Any() + ); + } + + [Fact] + 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 + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); +#endif + var context = new TestLambdaContext() + { + FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" + }; + + var testObj = new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }; + + // Act + _testHandlers.LogEvent(testObj, 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 + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); +#endif + var context = new TestLambdaContext() + { + FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" + }; + + // Act + _testHandlers.LogEventFalse(context); + + consoleOut.DidNotReceive().WriteLine( + Arg.Any() + ); + } + + [Fact] + public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() + { + // Arrange + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testHandlers.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 + _testHandlers.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 + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); +#endif + + // Act + switch (correlationIdPath) + { + case CorrelationIdPaths.ApiGatewayRest: + _testHandlers.CorrelationApiGatewayProxyRequest(new APIGatewayProxyRequest + { + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + RequestId = correlationId + } + }); + break; + case CorrelationIdPaths.ApplicationLoadBalancer: + _testHandlers.CorrelationApplicationLoadBalancerRequest(new ApplicationLoadBalancerRequest + { + Headers = new Dictionary + { + { "x-amzn-trace-id", correlationId } + } + }); + break; + case CorrelationIdPaths.EventBridge: + _testHandlers.CorrelationCloudWatchEvent(new S3ObjectCreateEvent + { + Id = correlationId + }); + break; + case "/headers/my_request_id_header": + _testHandlers.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 + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); +#endif + + // Act + switch (outputCase) + { + case LoggerOutputCase.CamelCase: + _testHandlers.CorrelationIdFromStringCamel(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.PascalCase: + _testHandlers.CorrelationIdFromStringPascal(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.SnakeCase: + _testHandlers.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 + PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); +#endif + + // Act + switch (outputCase) + { + case LoggerOutputCase.CamelCase: + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "CamelCase"); + _testHandlers.CorrelationIdFromStringCamelEnv(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.PascalCase: + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", "PascalCase"); + _testHandlers.CorrelationIdFromStringPascalEnv(new TestObject + { + Headers = new Header + { + MyRequestIdHeader = correlationId + } + }); + break; + case LoggerOutputCase.SnakeCase: + _testHandlers.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 = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testHandlers.HandlerSamplingRate(); + + // Assert + + consoleOut.Received().WriteLine( + Arg.Is(i => i.Contains("\"message\":\"test\",\"samplingRate\":0.5")) + ); + } + + [Fact] + public void When_Setting_Service_Should_Update_Key() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testHandlers.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_LogLevel_Should_Update_LogLevel() + { + // Arrange + var consoleOut = new StringWriter(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act + _testHandlers.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 + _testHandlers.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 + _testHandlers.TestLogEventWithoutContext(); + + // 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 TestHandlers(); + + // 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/Context/LambdaContextTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Context/LambdaContextTest.cs new file mode 100644 index 00000000..feb9283e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Context/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.Context; + +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/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 new file mode 100644 index 00000000..8b65062f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -0,0 +1,377 @@ +/* + * 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.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 AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NSubstitute.ReturnsExtensions; +using Xunit; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AWS.Lambda.Powertools.Logging.Tests.Formatter +{ + [Collection("Sequential")] + public class LogFormatterTest : IDisposable + { + private readonly TestHandlers _testHandler; + + public LogFormatterTest() + { + _testHandler = new TestHandlers(); + } + + [Fact] + public void Log_WhenCustomFormatter_LogsCustomFormat() + { + // Arrange + const bool coldStart = false; + var xrayTraceId = Guid.NewGuid().ToString(); + var correlationId = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var minimumLevel = LogLevel.Information; + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var message = Guid.NewGuid().ToString(); + + Logger.AppendKey(LoggingConstants.KeyColdStart, coldStart); + Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xrayTraceId); + Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); + + 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() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + Logger.AppendKeys(globalExtraKeys); + + var lambdaContext = new TestLambdaContext + { + FunctionName = Guid.NewGuid().ToString(), + FunctionVersion = Guid.NewGuid().ToString(), + InvokedFunctionArn = Guid.NewGuid().ToString(), + 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)); + + // 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 + { + Message = message, + Service = service, + CorrelationIds = new + { + lambdaContext.AwsRequestId, + XRayTraceId = xrayTraceId + }, + LambdaFunction = new + { + Name = lambdaContext.FunctionName, + Arn = lambdaContext.InvokedFunctionArn, + MemorySize = lambdaContext.MemoryLimitInMB, + Version = lambdaContext.FunctionVersion, + ColdStart = coldStart, + }, + Level = logLevel.ToString(), + Logger = new + { + Name = loggerName + } + }; + + logFormatter.FormatLogEntry(new LogEntry()).ReturnsForAnyArgs(formattedLogEntry); + Logger.UseFormatter(logFormatter); + + var systemWrapper = Substitute.For(); + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); + + var scopeExtraKeys = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + // Act + logger.LogInformation(scopeExtraKeys, message); + + // Assert + logFormatter.Received(1).FormatLogEntry(Arg.Is + ( + x => + x.ColdStart == coldStart && + x.XRayTraceId == xrayTraceId && + x.CorrelationId == correlationId && + x.Service == service && + x.Name == loggerName && + x.Level == logLevel && + 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.LambdaContext != null && + x.LambdaContext.FunctionName == lambdaContext.FunctionName && + x.LambdaContext.FunctionVersion == lambdaContext.FunctionVersion && + x.LambdaContext.MemoryLimitInMB == lambdaContext.MemoryLimitInMB && + x.LambdaContext.InvokedFunctionArn == lambdaContext.InvokedFunctionArn && + x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId + )); + + systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); + } + + [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(); + } + } + + [Collection("Sequential")] + public class LogFormatterNullTest + { + [Fact] + public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var message = Guid.NewGuid().ToString(); + + 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 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); + + // Assert + Assert.Throws(Act); + logFormatter.Received(1).FormatLogEntry(Arg.Any()); + systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); + + //Clean up + Logger.UseDefaultFormatter(); + } + } + + [Collection("Sequential")] + public class LogFormatterExceptionTest + { + [Fact] + public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var message = Guid.NewGuid().ToString(); + var errorMessage = Guid.NewGuid().ToString(); + + 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 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); + + // Assert + Assert.Throws(Act); + logFormatter.Received(1).FormatLogEntry(Arg.Any()); + systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); + + //Clean up + Logger.UseDefaultFormatter(); + } + } +} \ No newline at end of file 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/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/Handlers/TestHandlers.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs new file mode 100644 index 00000000..990fa103 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -0,0 +1,161 @@ +/* + * 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 Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Logging.Tests.Serializers; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; + +class TestHandlers +{ + [Logging] + public void TestMethod() + { + } + + [Logging(LogLevel = LogLevel.Debug)] + public void TestMethodDebug() + { + } + + [Logging(LogEvent = true)] + public void LogEventNoArgs() + { + } + + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, CorrelationIdPath = "/Headers/MyRequestIdHeader")] + public void LogEvent(TestObject testObject, ILambdaContext context) + { + } + + [Logging(LogEvent = false)] + public void LogEventFalse(ILambdaContext context) + { + } + + [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(SamplingRate = 0.5, LoggerOutputCase = LoggerOutputCase.CamelCase, LogLevel = LogLevel.Information)] + public void HandlerSamplingRate() + { + Logger.LogInformation("test"); + } + + [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() + { + } + + [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"); + } +} \ 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 deleted file mode 100644 index 158d8fe8..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs +++ /dev/null @@ -1,249 +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 System.Text.Json; -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; - -namespace AWS.Lambda.Powertools.Logging.Tests -{ - [Collection("Sequential")] - public class LogFormatterTest - { - [Fact] - public void Log_WhenCustomFormatter_LogsCustomFormat() - { - // Arrange - const bool coldStart = false; - var xrayTraceId = Guid.NewGuid().ToString(); - var correlationId = Guid.NewGuid().ToString(); - var logLevel = LogLevel.Information; - var minimumLevel = LogLevel.Information; - var loggerName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var message = Guid.NewGuid().ToString(); - - Logger.AppendKey(LoggingConstants.KeyColdStart, coldStart); - Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xrayTraceId); - Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); - - var configurations = Substitute.For(); - configurations.Service.Returns(service); - - var globalExtraKeys = new Dictionary - { - { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, - { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } - }; - Logger.AppendKeys(globalExtraKeys); - - var lambdaContext = new LogEntryLambdaContext - { - FunctionName = Guid.NewGuid().ToString(), - FunctionVersion = Guid.NewGuid().ToString(), - InvokedFunctionArn = Guid.NewGuid().ToString(), - AwsRequestId = Guid.NewGuid().ToString(), - MemoryLimitInMB = (new Random()).Next() - }; - - var eventArgs = new AspectEventArgs - { - Name = Guid.NewGuid().ToString(), - Args = new object[] - { - new - { - Source = "Test" - }, - lambdaContext - } - }; - PowertoolsLambdaContext.Extract(eventArgs); - - var logFormatter = Substitute.For(); - var formattedLogEntry = new - { - Message = message, - Service = service, - CorrelationIds = new - { - lambdaContext.AwsRequestId, - XRayTraceId = xrayTraceId - }, - LambdaFunction = new - { - Name = lambdaContext.FunctionName, - Arn = lambdaContext.InvokedFunctionArn, - MemorySize = lambdaContext.MemoryLimitInMB, - Version = lambdaContext.FunctionVersion, - ColdStart = coldStart, - }, - Level = logLevel.ToString(), - Logger = new - { - Name = loggerName - } - }; - - logFormatter.FormatLogEntry(new LogEntry()).ReturnsForAnyArgs(formattedLogEntry); - Logger.UseFormatter(logFormatter); - - var systemWrapper = Substitute.For(); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = service, - MinimumLevel = minimumLevel, - LoggerOutputCase = LoggerOutputCase.PascalCase - }); - - var scopeExtraKeys = new Dictionary - { - { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, - { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } - }; - - // Act - logger.LogInformation(scopeExtraKeys, message); - - // Assert - logFormatter.Received(1).FormatLogEntry(Arg.Is - ( - x => - x.ColdStart == coldStart && - x.XRayTraceId == xrayTraceId && - x.CorrelationId == correlationId && - x.Service == service && - x.Name == loggerName && - x.Level == logLevel && - 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.LambdaContext != null && - x.LambdaContext.FunctionName == lambdaContext.FunctionName && - x.LambdaContext.FunctionVersion == lambdaContext.FunctionVersion && - x.LambdaContext.MemoryLimitInMB == lambdaContext.MemoryLimitInMB && - x.LambdaContext.InvokedFunctionArn == lambdaContext.InvokedFunctionArn && - x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId - )); - systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); - - //Clean up - Logger.UseDefaultFormatter(); - Logger.RemoveAllKeys(); - PowertoolsLambdaContext.Clear(); - LoggingAspectHandler.ResetForTest(); - } - } - - [Collection("Sequential")] - public class LogFormatterNullTest - { - [Fact] - public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() - { - // Arrange - var loggerName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var message = Guid.NewGuid().ToString(); - - var configurations = Substitute.For(); - configurations.Service.Returns(service); - - 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 - }); - - // Act - void Act() => logger.LogInformation(message); - - // Assert - Assert.Throws(Act); - logFormatter.Received(1).FormatLogEntry(Arg.Any()); - systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); - - //Clean up - Logger.UseDefaultFormatter(); - } - } - - [Collection("Sequential")] - public class LogFormatterExceptionTest - { - [Fact] - public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() - { - // Arrange - var loggerName = Guid.NewGuid().ToString(); - var service = Guid.NewGuid().ToString(); - var message = Guid.NewGuid().ToString(); - var errorMessage = Guid.NewGuid().ToString(); - - var configurations = Substitute.For(); - configurations.Service.Returns(service); - - 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 - }); - - // Act - void Act() => logger.LogInformation(message); - - // Assert - Assert.Throws(Act); - logFormatter.Received(1).FormatLogEntry(Arg.Any()); - systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); - - //Clean up - Logger.UseDefaultFormatter(); - } - } -} \ 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 29fd9a40..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs +++ /dev/null @@ -1,307 +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 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() - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, 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() - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, 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() - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, 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() - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, 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() - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, true, 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 = Substitute.For(); - var systemWrapper = Substitute.For(); - - var eventArgs = new AspectEventArgs - { - Name = methodName, - Args = new[] { eventArg } - }; - - LoggingAspectHandler.ResetForTest(); - var handler = new LoggingAspectHandler(service, logLevel, null, null, false, correlationIdPath, - true, 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 c49852af..1267e090 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -21,17 +21,16 @@ 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; -using NSubstitute.Extensions; -using NSubstitute.ReceivedExtensions; using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests { [Collection("Sequential")] - public class PowertoolsLoggerTest + public class PowertoolsLoggerTest : IDisposable { public PowertoolsLoggerTest() { @@ -49,13 +48,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); switch (logLevel) { @@ -101,13 +104,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) { @@ -260,7 +267,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; @@ -274,12 +280,17 @@ 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 = LogLevel.None + }; + + // Act + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var logger = provider.CreateLogger("test"); logger.LogInformation("Test"); @@ -296,7 +307,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; @@ -309,14 +319,19 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); + + var loggerConfiguration = new LoggerConfiguration + { + Service = null, + MinimumLevel = LogLevel.None + }; + + // Act + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); - var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () => - new LoggerConfiguration - { - Service = null, - MinimumLevel = null - }); - + var logger = provider.CreateLogger("test"); + logger.LogInformation("Test"); // Assert @@ -344,12 +359,15 @@ 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 = LogLevel.None + }; + + // Act + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); @@ -379,12 +397,15 @@ 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 = LogLevel.None + }; + + // Act + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -418,13 +439,16 @@ 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 = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + // Act + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -459,12 +483,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -498,13 +524,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 = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.PascalCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -537,12 +565,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -574,13 +604,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 = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -612,12 +644,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -645,12 +679,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 { @@ -686,12 +722,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 { @@ -727,12 +765,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 { @@ -781,12 +821,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 +904,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 +988,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,12 +1063,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); try { @@ -1039,6 +1086,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()") + )); } [Fact] @@ -1058,12 +1108,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); try { @@ -1099,12 +1151,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 = LogLevel.None + }; + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Bytes = bytes }); @@ -1137,12 +1190,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); @@ -1177,12 +1232,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); @@ -1206,15 +1263,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 = LogLevel.None + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); // Assert @@ -1239,18 +1298,24 @@ 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 = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { PropOne = "Value 1", PropTwo = "Value 2", + PropThree = new + { + PropFour = 1 + }, Date = new DateOnly(2022, 1, 1) }; @@ -1259,7 +1324,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\"}}") ) ); } @@ -1280,13 +1345,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 = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase + }; + + var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var logger = provider.CreateLogger(loggerName); var message = new { @@ -1322,13 +1389,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 { @@ -1342,13 +1411,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 @@ -1360,13 +1429,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 { @@ -1380,8 +1451,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); } @@ -1396,22 +1467,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); } @@ -1430,12 +1503,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 { @@ -1462,12 +1536,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 { @@ -1516,13 +1594,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 { @@ -1536,8 +1616,13 @@ 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); } + + 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 new file mode 100644 index 00000000..b522963f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -0,0 +1,261 @@ +/* + * 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 AWS.Lambda.Powertools.Logging.Serializers; +using System; +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; + +namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; + +public class PowertoolsLambdaSerializerTests : IDisposable +{ +#if NET8_0_OR_GREATER + [Fact] + public void Constructor_ShouldNotThrowException() + { + // Arrange & Act & Assert + var exception = + Record.Exception(() => PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default)); + Assert.Null(exception); + } + + [Fact] + public void Constructor_ShouldAddCustomerContext() + { + // Arrange + var customerContext = new TestJsonContext(); + + // Act + 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)] + public void Deserialize_ValidJson_ShouldReturnDeserializedObject(LoggerOutputCase outputCase, string json, + string expectedName, int expectedAge) + { + // Arrange + var serializer = new PowertoolsSourceGeneratorSerializer(); + + 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 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)); + } + + [Fact] + public void Serialize_ValidObject_ShouldSerializeToStream() + { + // Arrange + var serializer = new PowertoolsSourceGeneratorSerializer(); + 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 PowertoolsSourceGeneratorSerializer(); + ; + var unknownObject = new UnknownType(); + var stream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => serializer.Serialize(unknownObject, stream)); + } + + private class UnknownType + { + } + + [Fact] + public void Deserialize_NonSeekableStream_ShouldDeserializeCorrectly() + { + // Arrange + var serializer = new PowertoolsSourceGeneratorSerializer(); + ; + var json = "{\"fullName\":\"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) + { + } + } + + + [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); + } + + +#endif + public void Dispose() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + PowertoolsLoggingSerializer.ClearOptions(); + } + +#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/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs new file mode 100644 index 00000000..e26053ac --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -0,0 +1,179 @@ +/* + * 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 Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Logging.Internal; +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 +{ + + public PowertoolsLoggingSerializerTests() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); +#if NET8_0_OR_GREATER + PowertoolsLoggingSerializer.ClearContext(); +#endif + } + + [Fact] + public void SerializerOptions_ShouldNotBeNull() + { + var options = PowertoolsLoggingSerializer.GetSerializerOptions(); + Assert.NotNull(options); + } + + [Fact] + public void SerializerOptions_ShouldHaveCorrectDefaultSettings() + { + var options = PowertoolsLoggingSerializer.GetSerializerOptions(); + + 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.GetSerializerOptions()); + 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.GetSerializerOptions()); + 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) + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase.Value); + } + + LogEntry testObject = new LogEntry { ColdStart = true }; + return JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + } + + public void Dispose() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); +#if NET8_0_OR_GREATER + PowertoolsLoggingSerializer.ClearContext(); +#endif + PowertoolsLoggingSerializer.ClearOptions(); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs new file mode 100644 index 00000000..728e1653 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs @@ -0,0 +1,29 @@ +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.Serializers; + +[JsonSerializable(typeof(S3ObjectCreateEvent))] +[JsonSerializable(typeof(TestObject))] +[JsonSerializable(typeof(TestLambdaContext))] +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(ApplicationLoadBalancerRequest))] +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/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/PowertoolsConfigurationExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs new file mode 100644 index 00000000..6a719d1b --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs @@ -0,0 +1,159 @@ +/* + * 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; +using AWS.Lambda.Powertools.Logging.Serializers; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +public class PowertoolsConfigurationExtensionsTests : 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) + { + // 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); + } + + public void Dispose() + { + LoggingAspect.ResetForTest(); + PowertoolsLoggingSerializer.ClearOptions(); + } +} + +// 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 new file mode 100644 index 00000000..ed29a772 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -0,0 +1,131 @@ + +#if NET8_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.IO; +using AWS.Lambda.Powertools.Common; +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 PowertoolsLoggerHelpersTests : IDisposable +{ + [Fact] + public void ObjectToDictionary_AnonymousObjectWithSimpleProperties_ReturnsDictionary() + { + // Arrange + var anonymousObject = new { name = "test", age = 30 }; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(anonymousObject); + + // Assert + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Equal(2, dictionary.Count); + Assert.Equal("test", dictionary["name"]); + Assert.Equal(30, dictionary["age"]); + } + + [Fact] + public void ObjectToDictionary_AnonymousObjectWithNestedObject_ReturnsDictionaryWithNestedDictionary() + { + // Arrange + var anonymousObject = new { name = "test", nested = new { id = 1 } }; + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(anonymousObject); + + // Assert + 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"]); + } + + [Fact] + public void ObjectToDictionary_ObjectWithNamespace_ReturnsOriginalObject() + { + // Arrange + var objectWithNamespace = new System.Text.StringBuilder(); + + // Act + var result = PowertoolsLoggerHelpers.ObjectToDictionary(objectWithNamespace); + + // Assert + Assert.Same(objectWithNamespace, result); + } + + [Fact] + public void ObjectToDictionary_NullObject_Return_New_Dictionary() + { + // Act & Assert + Assert.NotNull(() => PowertoolsLoggerHelpers.ObjectToDictionary(null)); + } + + [Fact] + public void Should_Log_With_Anonymous() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act & Assert + Logger.AppendKey("newKey", new + { + name = "my name" + }); + + Logger.LogInformation("test"); + + consoleOut.Received(1).WriteLine( + Arg.Is(i => + i.Contains("\"new_key\":{\"name\":\"my name\"}")) + ); + } + + [Fact] + public void Should_Log_With_Complex_Anonymous() + { + var consoleOut = Substitute.For(); + SystemWrapper.Instance.SetOut(consoleOut); + + // Act & Assert + Logger.AppendKey("newKey", new + { + 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\"}")) + ); + } + + public void Dispose() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.Default); + PowertoolsLoggingSerializer.ClearOptions(); + } +} + +#endif \ 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..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() { - throw new System.NotImplementedException(); + return 0.7; } public void SetEnvironmentVariable(string variable, string value) diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props index 53241e7b..90d9408d 100644 --- a/libraries/tests/Directory.Packages.props +++ b/libraries/tests/Directory.Packages.props @@ -3,6 +3,8 @@ true + + @@ -10,7 +12,7 @@ - + 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" },