diff --git a/docs/core/logging-v2.md b/docs/core/logging-v2.md new file mode 100644 index 00000000..93b6d51b --- /dev/null +++ b/docs/core/logging-v2.md @@ -0,0 +1,1529 @@ +--- +title: Logging V2 +description: Core utility +--- + +The logging utility provides a Lambda optimized logger with output structured as JSON. + +## Key features + +* Capture key fields from Lambda context, cold start and structures logging output as JSON +* 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) +* Custom log formatter to override default log structure +* Support + for [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) + {target="_blank"} +* Support for Microsoft.Extensions.Logging + and [ILogger](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger?view=dotnet-plat-ext-7.0) + interface +* Support + for [ILoggerFactory](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory?view=dotnet-plat-ext-7.0) + interface +* Support for message templates `{}` and `{@}` for structured logging + +## Installation + +Powertools for AWS Lambda (.NET) are available as NuGet packages. You can install the packages +from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio +editor by searching `AWS.Lambda.Powertools*` to see various utilities available. + +* [AWS.Lambda.Powertools.Logging](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Logging): + + `dotnet add package AWS.Lambda.Powertools.Logging` + +## 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 +-------------------|---------------------------------------------------------------------|---------------------------|--------------------- + **Service** | Sets **Service** key that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `Service` + **Logging level** | Sets how verbose Logger should be (Information, by default) | `POWERTOOLS_LOG_LEVEL` | `LogLevel` + +### Full list of environment variables + +| Environment variable | Description | Default | +|-----------------------------------|----------------------------------------------------------------------------------------|-----------------------| +| **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | `"service_undefined"` | +| **POWERTOOLS_LOG_LEVEL** | Sets logging level | `Information` | +| **POWERTOOLS_LOGGER_CASE** | Override the default casing for log keys | `SnakeCase` | +| **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | `false` | +| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | `0` | + +### Setting up the logger + +You can set up the logger in different ways. The most common way is to use the `Logging` attribute on your Lambda. +You can also use the `ILogger` interface to log messages. This interface is part of the Microsoft.Extensions.Logging. + +=== "Using decorator" + + ```c# hl_lines="6 10" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(Service = "payment", LogLevel = LogLevel.Debug)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +=== "Logger Factory" + + ```c# hl_lines="6 10-17 23" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + private readonly ILogger _logger; + + public Function(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +=== "With Builder" + + ```c# hl_lines="6 10-13 19" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + private readonly ILogger _logger; + + public Function(ILogger logger) + { + _logger = logger ?? new PowertoolsLoggerBuilder() + .WithService("TestService") + .WithOutputCase(LoggerOutputCase.PascalCase) + .Build(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +### Customizing the logger + +You can customize the logger by setting the following properties in the `Logger.Configure` method: + +| Property | Description | +|:----------------------|--------------------------------------------------------------------------------------------------| +| `Service` | The name of the service. This is used to identify the service in the logs. | +| `MinimumLogLevel` | The minimum log level to log. This is used to filter out logs below the specified level. | +| `LogFormatter` | The log formatter to use. This is used to customize the structure of the log entries. | +| `JsonOptions` | The JSON options to use. This is used to customize the serialization of logs.| +| `LogBuffering` | The log buffering options. This is used to configure log buffering. | +| `TimestampFormat` | The format of the timestamp. This is used to customize the format of the timestamp in the logs.| +| `SamplingRate` | Sets a percentage (0.0 to 1.0) of logs that will be dynamically elevated to DEBUG level | +| `LoggerOutputCase` | The output casing of the logger. This is used to customize the casing of the log entries. | +| `LogOutput` | Specifies the console output wrapper used for writing logs. This property allows redirecting log output for testing or specialized handling scenarios. | + + +### Configuration + +You can configure Powertools Logger using the static `Logger` class. This class is a singleton and is created when the +Lambda function is initialized. You can configure the logger using the `Logger.Configure` method. + +=== "Configure static Logger" + +```c# hl_lines="5-9" + public class Function + { + public Function() + { + Logger.Configure(options => + { + options.MinimumLogLevel = LogLevel.Information; + options.LoggerOutputCase = LoggerOutputCase.CamelCase; + }); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Collecting payment"); + ... + } + } +``` + +### ILogger +You can also use the `ILogger` interface to log messages. This interface is part of the Microsoft.Extensions.Logging. +With this approach you get more flexibility and testability using dependency injection (DI). + +=== "Configure with LoggerFactory or Builder" + + ```c# hl_lines="5-12" + public class Function + { + public Function(ILogger logger) + { + _logger = logger ?? LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +## Standard structured keys + +Your logs will always include the following keys to your structured logging: + + Key | Type | Example | Description +------------------------|--------|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------ + **Level** | string | "Information" | Logging level + **Message** | string | "Collecting payment" | Log statement value. Unserializable JSON values will be cast to string + **Timestamp** | string | "2020-05-24 18:17:33,774" | Timestamp of actual log statement + **Service** | string | "payment" | Service name defined. "service_undefined" will be used if unknown + **ColdStart** | bool | true | ColdStart value. + **FunctionName** | string | "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + **FunctionMemorySize** | string | "128" + **FunctionArn** | string | "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + **FunctionRequestId** | string | "899856cb-83d1-40d7-8611-9e78f15f32f4" | AWS Request ID from lambda context + **FunctionVersion** | string | "12" + **XRayTraceId** | string | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when Lambda function has enabled Tracing + **Name** | string | "Powertools for AWS Lambda (.NET) Logger" | Logger name + **SamplingRate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case + **Customer Keys** | | | + +## Message templates + +You can use message templates to extract properties from your objects and log them as structured data. + +!!! info + + Override the `ToString()` method of your object to return a meaningful string representation of the object. + + This is especially important when using `{}` to log the object as a string. + + ```csharp + public class User + { + public string FirstName { get; set; } + public string LastName { get; set; } + public int Age { get; set; } + + public override string ToString() + { + return $"{LastName}, {FirstName} ({Age})"; + } + } + ``` + +If you want to log the object as a JSON object, use `{@}`. This will serialize the object and log it as a JSON object. + +=== "Message template {@}" + + ```c# hl_lines="7-14" + public class Function + { + [Logging(Service = "user-service", LogLevel = LogLevel.Information)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + logger.LogInformation("User object: {@user}", user); + ... + } + } + ``` + +=== "{@} Output" + + ```json hl_lines="3 8-12" + { + "level": "Information", + "message": "User object: Doe, John (42)", + "timestamp": "2025-04-07 09:06:30.708", + "service": "user-service", + "coldStart": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "user": { + "firstName": "John", + "lastName": "Doe", + "age": 42 + }, + ... + } + ``` + +If you want to log the object as a string, use `{}`. This will call the `ToString()` method of the object and log it as +a string. + +=== "Message template {} ToString" + + ```c# hl_lines="7-12 14 18 19" + public class Function + { + [Logging(Service = "user", LogLevel = LogLevel.Information)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + logger.LogInformation("User data: {user}", user); + + // Also works with numbers, dates, etc. + + logger.LogInformation("Price: {price:0.00}", 123.4567); // will respect decimal places + logger.LogInformation("Percentage: {percent:0.0%}", 0.1234); + ... + } + } + ``` + +=== "Output {} ToString" + + ```json hl_lines="3 8 12 17 21 26" + { + "level": "Information", + "message": "User data: Doe, John (42)", + "timestamp": "2025-04-07 09:06:30.689", + "service": "user-servoice", + "coldStart": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "user": "Doe, John (42)" + } + { + "level": "Information", + "message": "Price: 123.46", + "timestamp": "2025-04-07 09:23:01.235", + "service": "user-servoice", + "cold_start": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "price": 123.46 + } + { + "level": "Information", + "message": "Percentage: 12.3%", + "timestamp": "2025-04-07 09:23:01.260", + "service": "user-servoice", + "cold_start": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "percent": "12.3%" + } + ``` + + +## Logging incoming event + +When debugging in non-production environments, you can instruct Logger to log the incoming event with `LogEvent` +parameter or via `POWERTOOLS_LOGGER_LOG_EVENT` environment variable. + +!!! warning +Log event is disabled by default to prevent sensitive info being logged. + +=== "Function.cs" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +## Setting a Correlation ID + +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" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "Example Event" + + ```json hl_lines="3" + { + "headers": { + "my_request_id_header": "correlation_id_value" + } + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="15" + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "lambda-example", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "function_version": "$LATEST", + "xray_trace_id": "1-61b7add4-66532bb81441e1b060389429", + "name": "AWS.Lambda.Powertools.Logging.Logger", + "sampling_rate": 0.7, + "correlation_id": "correlation_id_value", + } + ``` + +We provide [built-in JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03) +{target="_blank"} +for known event sources, where either a request ID or X-Ray Trace ID are present. + +=== "Function.cs" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "Example Event" + + ```json hl_lines="3" + { + "RequestContext": { + "RequestId": "correlation_id_value" + } + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="15" + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "lambda-example", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "function_version": "$LATEST", + "xray_trace_id": "1-61b7add4-66532bb81441e1b060389429", + "name": "AWS.Lambda.Powertools.Logging.Logger", + "sampling_rate": 0.7, + "correlation_id": "correlation_id_value", + } + ``` + +## Appending additional keys + +!!! info "Custom keys are persisted across warm invocations" +Always set additional keys as part of your handler to ensure they have the latest value, or explicitly clear them with [ +`ClearState=true`](#clearing-all-state). + +You can append your own keys to your existing logs via `AppendKey`. Typically this value would be passed into the +function via the event. Appended keys are added to all subsequent log entries in the current execution from the point +the logger method is called. To ensure the key is added to all log entries, call this method as early as possible in the +Lambda handler. + +=== "Function.cs" + + ```c# hl_lines="21" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, + ILambdaContext context) + { + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + // Appended keys are added to all subsequent log entries in the current execution. + // Call this method as early as possible in the Lambda handler. + // Typically this is value would be passed into the function via the event. + // Set the ClearState = true to force the removal of keys across invocations, + Logger.AppendKeys(lookupInfo); + + Logger.LogInformation("Getting ip address from external service"); + + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="4 5 6" + { + "level": "Information", + "message": "Getting ip address from external service" + "timestamp": "2022-03-14T07:25:20.9418065Z", + "service": "powertools-dotnet-logging-sample", + "cold_start": false, + "function_name": "PowertoolsLoggingSample-HelloWorldFunction-hm1r10VT3lCy", + "function_memory_size": 256, + "function_arn": "arn:aws:lambda:function:PowertoolsLoggingSample-HelloWorldFunction-hm1r10VT3lCy", + "function_request_id": "96570b2c-f00e-471c-94ad-b25e95ba7347", + "function_version": "$LATEST", + "xray_trace_id": "1-622eede0-647960c56a91f3b071a9fff1", + "name": "AWS.Lambda.Powertools.Logging.Logger", + "lookup_info": { + "lookup_id": "4c50eace-8b1e-43d3-92ba-0efacf5d1625" + }, + } + ``` + +### Removing additional keys + +You can remove any additional key from entry using `Logger.RemoveKeys()`. + +=== "Function.cs" + + ```c# hl_lines="21 22" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + Logger.AppendKey("test", "willBeLogged"); + ... + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + Logger.AppendKeys(customKeys); + ... + Logger.RemoveKeys("test"); + Logger.RemoveKeys("test1", "test2"); + ... + } + } + ``` + +## Extra Keys + +Extra keys allow you to append additional keys to a log entry. Unlike `AppendKey`, extra keys will only apply to the +current log entry. + +Extra keys argument is available for all log levels' methods, as implemented in the standard logging library - e.g. +Logger.Information, Logger.Warning. + +It accepts any dictionary, and all keyword arguments will be added as part of the root structure of the logs for that +log statement. + +!!! info +Any keyword argument added using extra keys will not be persisted for subsequent messages. + +=== "Function.cs" + + ```c# hl_lines="16" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, + ILambdaContext context) + { + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + + var lookupId = new Dictionary() + { + { "LookupId", requestContextRequestId } + }; + + // Appended keys are added to all subsequent log entries in the current execution. + // Call this method as early as possible in the Lambda handler. + // Typically this is value would be passed into the function via the event. + // Set the ClearState = true to force the removal of keys across invocations, + Logger.AppendKeys(lookupId); + } + ``` + +### Clearing all state + +Logger is commonly initialized in the global scope. Due +to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), this means that +custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use +`ClearState=true` attribute on `[Logging]` attribute. + +=== "Function.cs" + + ```cs hl_lines="6 13" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(ClearState = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + if (apigProxyEvent.Headers.ContainsKey("SomeSpecialHeader")) + { + Logger.AppendKey("SpecialKey", "value"); + } + + Logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +=== "#1 Request" + + ```json hl_lines="11" + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "special_key": "value" + } + ``` + +=== "#2 Request" + + ```json + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + +## Sampling debug logs + +You can dynamically set a percentage of your logs to **DEBUG** level via env var `POWERTOOLS_LOGGER_SAMPLE_RATE` or +via `SamplingRate` parameter on attribute. + +!!! info +Configuration on environment variable is given precedence over sampling rate configuration on attribute, provided it's +in valid value range. + +=== "Sampling via attribute parameter" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(SamplingRate = 0.5)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "Sampling via environment variable" + + ```yaml hl_lines="8" + + Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + ... + Environment: + Variables: + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 + ``` + +## Configure Log Output Casing + +By definition Powertools for AWS Lambda (.NET) outputs logging keys using **snake case** (e.g. *"function_memory_size": +128*). This allows developers using different Powertools for AWS Lambda (.NET) runtimes, to search logs across services +written in languages such as Python or TypeScript. + +If you want to override the default behavior you can either set the desired casing through attributes, as described in +the example below, or by setting the `POWERTOOLS_LOGGER_CASE` environment variable on your AWS Lambda function. Allowed +values are: `CamelCase`, `PascalCase` and `SnakeCase`. + +=== "Output casing via attribute parameter" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LoggerOutputCase = LoggerOutputCase.CamelCase)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +Below are some output examples for different casing. + +=== "Camel Case" + + ```json + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "coldStart": true, + "functionName": "test", + "functionMemorySize": 128, + "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "functionRequestId": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + +=== "Pascal Case" + + ```json + { + "Level": "Information", + "Message": "Collecting payment", + "Timestamp": "2021-12-13T20:32:22.5774262Z", + "Service": "payment", + "ColdStart": true, + "FunctionName": "test", + "FunctionMemorySize": 128, + "FunctionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "FunctionRequestId": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + +=== "Snake Case" + + ```json + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + + +## Advanced + +### Log Levels + +The default log level is `Information` and can be set using the `MinimumLogLevel` property option or by using the `POWERTOOLS_LOG_LEVEL` environment variable. + +We support the following log levels: + +| Level | Numeric value | Lambda Level | +|---------------|---------------|--------------| +| `Trace` | 0 | `trace` | +| `Debug` | 1 | `debug` | +| `Information` | 2 | `info` | +| `Warning` | 3 | `warn` | +| `Error` | 4 | `error` | +| `Critical` | 5 | `fatal` | +| `None` | 6 | | + +### Using AWS Lambda Advanced Logging Controls (ALC) + +!!! question "When is it useful?" +When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, +regardless of runtime and logger used. + +With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced) +{target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. + +When enabled, you should keep `Logger` and ALC log level in sync to avoid data loss. + +!!! warning "When using AWS Lambda Advanced Logging Controls (ALC)" +- When Powertools Logger output is set to `PascalCase` **`Level`** property name will be replaced by **`LogLevel`** as + a property name. +- ALC takes precedence over **`POWERTOOLS_LOG_LEVEL`** and when setting it in code using **`[Logging(LogLevel = )]`** + +Here's a sequence diagram to demonstrate how ALC will drop both `Information` and `Debug` logs emitted from `Logger`, +when ALC log level is stricter than `Logger`. + +```mermaid +sequenceDiagram + title Lambda ALC allows WARN logs only + participant Lambda service + participant Lambda function + participant Application Logger + + Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN" + Note over Application Logger: POWERTOOLS_LOG_LEVEL="DEBUG" + Lambda service->>Lambda function: Invoke (event) + Lambda function->>Lambda function: Calls handler + Lambda function->>Application Logger: Logger.Warning("Something happened") + Lambda function-->>Application Logger: Logger.Debug("Something happened") + Lambda function-->>Application Logger: Logger.Information("Something happened") + + Lambda service->>Lambda service: DROP INFO and DEBUG logs + + Lambda service->>CloudWatch Logs: Ingest error logs +``` + +**Priority of log level settings in Powertools for AWS Lambda** + +We prioritise log level settings in this order: + +1. AWS_LAMBDA_LOG_LEVEL environment variable +2. Setting the log level in code using `[Logging(LogLevel = )]` +3. POWERTOOLS_LOG_LEVEL environment variable + +If you set `Logger` level lower than ALC, we will emit a warning informing you that your messages will be discarded by +Lambda. + +> **NOTE** +> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment +> variable value, +> see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level) +> {target="_blank"} for more details. + +### Using JsonSerializerOptions + +Powertools supports customizing the serialization and deserialization of Lambda JSON events and your own types using +`JsonSerializerOptions`. +You can do this by creating a custom `JsonSerializerOptions` and passing it to the `JsonOptions` of the Powertools +Logger. + +Supports `TypeInfoResolver` and `DictionaryKeyPolicy` options. These two options are the most common ones used to +customize the serialization of Powertools Logger. + +- `TypeInfoResolver`: This option allows you to specify a custom `JsonSerializerContext` that contains the types you + want to serialize and deserialize. This is especially useful when using AOT compilation, as it allows you to specify + the types that should be included in the generated assembly. +- `DictionaryKeyPolicy`: This option allows you to specify a custom naming policy for the properties in the JSON output. + This is useful when you want to change the casing of the property names or use a different naming convention. + +!!! info +If you want to preserve the original casing of the property names (keys), you can set the `DictionaryKeyPolicy` to +`null`. + +```csharp +builder.Logging.AddPowertoolsLogger(options => +{ + options.JsonOptions = new JsonSerializerOptions + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Override output casing + TypeInfoResolver = MyCustomJsonSerializerContext.Default // Your custom JsonSerializerContext + }; +}); +``` + +### Custom Log formatter (Bring Your Own Formatter) + +You can customize the structure (keys and values) of your log entries by implementing a custom log formatter and +override default log formatter using ``LogFormatter`` property in the `configure` options. + +You can implement a custom log formatter by +inheriting the ``ILogFormatter`` class and implementing the ``object FormatLogEntry(LogEntry logEntry)`` method. + +=== "Function.cs" + + ```c# hl_lines="11" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + /// + /// Function constructor + /// + public Function() + { + Logger.Configure(options => + { + options.LogFormatter = new CustomLogFormatter(); + }); + } + + [Logging(CorrelationIdPath = "/headers/my_request_id_header", SamplingRate = 0.7)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "CustomLogFormatter.cs" + + ```c# + 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 + }, + }; + } + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json + { + "Message": "Test Message", + "Service": "lambda-example", + "CorrelationIds": { + "AwsRequestId": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "XRayTraceId": "1-61b7add4-66532bb81441e1b060389429", + "CorrelationId": "correlation_id_value" + }, + "LambdaFunction": { + "Name": "test", + "Arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "MemorySize": 128, + "Version": "$LATEST", + "ColdStart": true + }, + "Level": "Information", + "Timestamp": "2021-12-13T20:32:22.5774262Z", + "Logger": { + "Name": "AWS.Lambda.Powertools.Logging.Logger", + "SampleRate": 0.7 + } + } + ``` + +### Buffering logs + +Log buffering enables you to buffer logs for a specific request or invocation. Enable log buffering by passing `LogBufferingOptions` when configuring a Logger instance. You can buffer logs at the `Warning`, `Information`, `Debug` or `Trace` level, and flush them automatically on error or manually as needed. + +!!! tip "This is useful when you want to reduce the number of log messages emitted while still having detailed logs when needed, such as when troubleshooting issues." + +=== "LogBufferingOptions" + + ```csharp hl_lines="5-14" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 20480, // Default is 20KB (20480 bytes) + FlushOnErrorLog = true // default true + }; + }); + + Logger.LogDebug('This is a debug message'); // This is NOT buffered + } + + [Logging] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogDebug('This is a debug message'); // This is buffered + Logger.LogInformation('This is an info message'); + + // your business logic here + + Logger.LogError('This is an error message'); // This also flushes the buffer + } + } + + ``` + +#### Configuring the buffer + +When configuring the buffer, you can set the following options to fine-tune how logs are captured, stored, and emitted. You can configure the following options in the `logBufferOptions` constructor parameter: + +| Parameter | Description | Configuration | Default | +|---------------------|------------------------------------------------- |--------------------------------------------|---------| +| `MaxBytes` | Maximum size of the log buffer in bytes | `number` | `20480` | +| `BufferAtLogLevel` | Minimum log level to buffer | `Trace`, `Debug`, `Information`, `Warning` | `Debug` | +| `FlushOnErrorLog` | Automatically flush buffer when logging an error | `True`, `False` | `True` | + +=== "BufferAtLogLevel" + + ```csharp hl_lines="10" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Warning + }; + }); + } + + [Logging] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // All logs below are buffered + Logger.LogDebug('This is a debug message'); + Logger.LogInformation('This is an info message'); + Logger.LogWarning('This is a warn message'); + + Logger.ClearBuffer(); // This will clear the buffer without emitting the logs + } + } + ``` + + 1. Setting `BufferAtLogLevel: 'Warning'` configures log buffering for `Warning` and all lower severity levels like `Information`, `Debug`, and `Trace`. + 2. Calling `Logger.ClearBuffer()` will clear the buffer without emitting the logs. + +=== "FlushOnErrorLog" + + ```csharp hl_lines="10" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + FlushOnErrorLog = false + }; + }); + } + + [Logging] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogDebug('This is a debug message'); // this is buffered + + try + { + throw new Exception(); + } + catch (Exception e) + { + Logger.LogError(e.Message); // this does NOT flush the buffer + } + + Logger.LogDebug("Debug!!"); // this is buffered + + try + { + throw new Exception(); + } + catch (Exception e) + { + Logger.LogError(e.Message); // this does NOT flush the buffer + Logger.FlushBuffer(); // Manually flush + } + } + } + ``` + + 1. Disabling `FlushOnErrorLog` will not flush the buffer when logging an error. This is useful when you want to control when the buffer is flushed by calling the `Logger.FlushBuffer()` method. + +#### Flushing on errors + +When using the `Logger` decorator, you can configure the logger to automatically flush the buffer when an error occurs. This is done by setting the `FlushBufferOnUncaughtError` option to `true` in the decorator. + +=== "FlushBufferOnUncaughtError" + + ```csharp hl_lines="15" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }; + }); + } + + [Logging(FlushBufferOnUncaughtError = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogDebug('This is a debug message'); + + throw new Exception(); // This causes the buffer to be flushed + } + } + ``` + +#### Buffering workflows + +##### Manual flush + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Initialize with DEBUG level buffering + Logger-->>Lambda: Logger buffer ready + Lambda->>Logger: Logger.LogDebug("First debug log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: Logger.LogInformation("Info log") + Logger->>CloudWatch: Directly log info message + Lambda->>Logger: Logger.LogDebug("Second debug log") + Logger-->>Logger: Buffer second debug log + Lambda->>Logger: Logger.FlushBuffer() + Logger->>CloudWatch: Emit buffered logs to stdout + Lambda->>Client: Return execution result +``` +Flushing buffer manually +
+ +##### Flushing when logging an error + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Initialize with DEBUG level buffering + Logger-->>Lambda: Logger buffer ready + Lambda->>Logger: Logger.LogDebug("First log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: Logger.LogDebug("Second log") + Logger-->>Logger: Buffer second debug log + Lambda->>Logger: Logger.LogDebug("Third log") + Logger-->>Logger: Buffer third debug log + Lambda->>Lambda: Exception occurs + Lambda->>Logger: Logger.LogError("Error details") + Logger->>CloudWatch: Emit buffered debug logs + Logger->>CloudWatch: Emit error log + Lambda->>Client: Raise exception +``` +Flushing buffer when an error happens +
+ +##### Flushing on error + +This works only when using the `Logger` decorator. You can configure the logger to automatically flush the buffer when an error occurs by setting the `FlushBufferOnUncaughtError` option to `true` in the decorator. + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Using decorator + Logger-->>Lambda: Logger context injected + Lambda->>Logger: Logger.LogDebug("First log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: Logger.LogDebug("Second log") + Logger-->>Logger: Buffer second debug log + Lambda->>Lambda: Uncaught Exception + Lambda->>CloudWatch: Automatically emit buffered debug logs + Lambda->>Client: Raise uncaught exception +``` +Flushing buffer when an uncaught exception happens +
+ +#### Buffering FAQs + +1. **Does the buffer persist across Lambda invocations?** + No, each Lambda invocation has its own buffer. The buffer is initialized when the Lambda function is invoked and is cleared after the function execution completes or when flushed manually. + +2. **Are my logs buffered during cold starts?** + No, we never buffer logs during cold starts. This is because we want to ensure that logs emitted during this phase are always available for debugging and monitoring purposes. The buffer is only used during the execution of the Lambda function. + +3. **How can I prevent log buffering from consuming excessive memory?** + You can limit the size of the buffer by setting the `MaxBytes` option in the `LogBufferingOptions` constructor parameter. This will ensure that the buffer does not grow indefinitely and consume excessive memory. + +4. **What happens if the log buffer reaches its maximum size?** + Older logs are removed from the buffer to make room for new logs. This means that if the buffer is full, you may lose some logs if they are not flushed before the buffer reaches its maximum size. When this happens, we emit a warning when flushing the buffer to indicate that some logs have been dropped. + +5. **How is the log size of a log line calculated?** + The log size is calculated based on the size of the serialized log line in bytes. This includes the size of the log message, the size of any additional keys, and the size of the timestamp. + +6. **What timestamp is used when I flush the logs?** + The timestamp preserves the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. + +7. **What happens if I try to add a log line that is bigger than max buffer size?** + The log will be emitted directly to standard output and not buffered. When this happens, we emit a warning to indicate that the log line was too big to be buffered. + +8. **What happens if Lambda times out without flushing the buffer?** + Logs that are still in the buffer will be lost. If you are using the log buffer to log asynchronously, you should ensure that the buffer is flushed before the Lambda function times out. You can do this by calling the `Logger.FlushBuffer()` method at the end of your Lambda function. + +### Timestamp formatting + +You can customize the timestamp format by setting the `TimestampFormat` property in the `Logger.Configure` method. The default format is `o`, which is the ISO 8601 format. +You can use any valid [DateTime format string](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings) to customize the timestamp format. +For example, to use the `yyyy-MM-dd HH:mm:ss` format, you can do the following: + +```csharp +Logger.Configure(logger => +{ + logger.TimestampFormat = "yyyy-MM-dd HH:mm:ss"; +}); +``` +This will output the timestamp in the following format: + +```json +{ + "level": "Information", + "message": "Test Message", + "timestamp": "2021-12-13 20:32:22", + "service": "lambda-example", + ... +} +``` + +## AOT Support + +!!! info + + If you want to use the `LogEvent`, `Custom Log Formatter` features, or serialize your own types when Logging events, you need to either pass `JsonSerializerContext` or make changes in your Lambda `Main` method. + +!!! info + + Starting from version 1.6.0, it is required to update the Amazon.Lambda.Serialization.SystemTextJson NuGet package to version 2.4.3 in your csproj. + +### Using JsonSerializerOptions + +To be able to serializer your own types, you need to pass your `JsonSerializerContext` to the `TypeInfoResolver` of the `Logger.Configure` method. + +```csharp +Logger.Configure(logger => +{ + logger.JsonOptions = new JsonSerializerOptions + { + TypeInfoResolver = YourJsonSerializerContext.Default + }; +}); +``` + +### Using PowertoolsSourceGeneratorSerializer + +Replace `SourceGeneratorLambdaJsonSerializer` with `PowertoolsSourceGeneratorSerializer`. + +This change enables Powertools to construct an instance of `JsonSerializerOptions` used to customize the serialization +and deserialization of 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 update your code to use `PowertoolsSourceGeneratorSerializer`, we combine your +`JsonSerializerContext` with Powertools' `JsonSerializerContext`. This allows Powertools to serialize your types and +Lambda events. + +### Custom Log Formatter + +To use a custom log formatter with AOT, pass an instance of `ILogFormatter` to `PowertoolsSourceGeneratorSerializer` +instead of using the static `Logger.UseFormatter` in the Function constructor as you do in non-AOT Lambdas. + +=== "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 + + While we support anonymous type serialization by converting to a `Dictionary`, this is **not** a best practice and is **not recommended** when using native AOT. + + We recommend using concrete classes and adding them to your `JsonSerializerContext`. + +## Testing + +You can change where the `Logger` will output its logs by setting the `LogOutput` property. +We also provide a helper class for tests `TestLoggerOutput` or you can provider your own implementation of `IConsoleWrapper`. + +```csharp +// Using TestLoggerOutput +options.LogOutput = new TestLoggerOutput(); +// Custom console output for testing +options.LogOutput = new TestConsoleWrapper(); + +// Example implementation for testing: +public class TestConsoleWrapper : IConsoleWrapper +{ + public List CapturedOutput { get; } = new(); + + public void WriteLine(string message) + { + CapturedOutput.Add(message); + } +} +``` +### ILogger + +If you are using ILogger interface you can inject the logger in a dedicated constructor for your Lambda function and thus you can mock your ILogger instance. + +```csharp +public class Function +{ + private readonly ILogger _logger; + + public Function() + { + _logger = oggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + // constructor used for tests - pass the mock ILogger + public Function(ILogger logger) + { + _logger = logger ?? loggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Collecting payment"); + ... + } +} +``` + + diff --git a/docs/core/logging.md b/docs/core/logging.md index 7c99d17a..a8a2cfce 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -19,7 +19,7 @@ Powertools for AWS Lambda (.NET) are available as NuGet packages. You can instal * [AWS.Lambda.Powertools.Logging](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Logging): - `dotnet add package AWS.Lambda.Powertools.Logging` + `dotnet add package AWS.Lambda.Powertools.Logging --version 1.6.5` ## Getting started diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index c0dc580f..07122c3a 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metri EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{A566F2D7-F8FE-466A-8306-85F266B7E656}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-Function-ILogger", "tests\e2e\functions\core\logging\AOT-Function-ILogger\src\AOT-Function-ILogger\AOT-Function-ILogger.csproj", "{7FC6DD65-0352-4139-8D08-B25C0A0403E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -548,6 +550,18 @@ Global {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x64.Build.0 = Release|Any CPU {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x86.ActiveCfg = Release|Any CPU {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x86.Build.0 = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x64.Build.0 = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x86.Build.0 = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|Any CPU.Build.0 = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x64.ActiveCfg = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x64.Build.0 = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x86.ActiveCfg = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -596,5 +610,6 @@ Global {A566F2D7-F8FE-466A-8306-85F266B7E656} = {1CFF5568-8486-475F-81F6-06105C437528} {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {A566F2D7-F8FE-466A-8306-85F266B7E656} {A422C742-2CF9-409D-BDAE-15825AB62113} = {A566F2D7-F8FE-466A-8306-85F266B7E656} + {7FC6DD65-0352-4139-8D08-B25C0A0403E3} = {4EAB66F9-C9CB-4E8A-BEE6-A14CD7FDE02F} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj b/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj index 54af1670..366ebe42 100644 --- a/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj +++ b/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj @@ -13,6 +13,6 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index 87321140..c71c05ae 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -14,18 +14,67 @@ */ using System; +using System.IO; namespace AWS.Lambda.Powertools.Common; /// public class ConsoleWrapper : IConsoleWrapper { + private static bool _override; + /// - public void WriteLine(string message) => Console.WriteLine(message); - /// - public void Debug(string message) => System.Diagnostics.Debug.WriteLine(message); + public void WriteLine(string message) + { + OverrideLambdaLogger(); + Console.WriteLine(message); + } + /// - public void Error(string message) => Console.Error.WriteLine(message); + public void Debug(string message) + { + OverrideLambdaLogger(); + System.Diagnostics.Debug.WriteLine(message); + } + /// - public string ReadLine() => Console.ReadLine(); + public void Error(string message) + { + if (!_override) + { + var errordOutput = new StreamWriter(Console.OpenStandardError()); + errordOutput.AutoFlush = true; + Console.SetError(errordOutput); + } + + Console.Error.WriteLine(message); + } + + internal static void SetOut(StringWriter consoleOut) + { + _override = true; + Console.SetOut(consoleOut); + } + + private void OverrideLambdaLogger() + { + if (_override) + { + return; + } + // Force override of LambdaLogger + var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + standardOutput.AutoFlush = true; + Console.SetOut(standardOutput); + } + + internal static void WriteLine(string logLevel, string message) + { + Console.WriteLine($"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}\t{logLevel}\t{message}"); + } + + public static void ResetForTest() + { + _override = false; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs index de75020e..a311507f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs @@ -37,10 +37,4 @@ public interface IConsoleWrapper /// /// The error message to write. void Error(string message); - - /// - /// Reads the next line of characters from the standard input stream. - /// - /// The next line of characters from the input stream, or null if no more lines are available. - string ReadLine(); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs index 059cfb7e..6f57aabb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs @@ -34,4 +34,10 @@ public interface IPowertoolsEnvironment /// /// Assembly Version in the Major.Minor.Build format string GetAssemblyVersion(T type); + + /// + /// Sets the execution Environment Variable (AWS_EXECUTION_ENV) + /// + /// + void SetExecutionEnvironment(T type); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index e57bb42e..5ec587b7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -1,12 +1,12 @@ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at - * + * * http://aws.amazon.com/apache2.0 - * + * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing @@ -25,6 +25,8 @@ namespace AWS.Lambda.Powertools.Common; /// public class PowertoolsConfigurations : IPowertoolsConfigurations { + private readonly IPowertoolsEnvironment _powertoolsEnvironment; + /// /// The maximum dimensions /// @@ -40,18 +42,13 @@ public class PowertoolsConfigurations : IPowertoolsConfigurations /// private static IPowertoolsConfigurations _instance; - /// - /// The system wrapper - /// - private readonly ISystemWrapper _systemWrapper; - /// /// Initializes a new instance of the class. /// - /// The system wrapper. - internal PowertoolsConfigurations(ISystemWrapper systemWrapper) + /// + internal PowertoolsConfigurations(IPowertoolsEnvironment powertoolsEnvironment) { - _systemWrapper = systemWrapper; + _powertoolsEnvironment = powertoolsEnvironment; } /// @@ -59,7 +56,7 @@ internal PowertoolsConfigurations(ISystemWrapper systemWrapper) /// /// The instance. public static IPowertoolsConfigurations Instance => - _instance ??= new PowertoolsConfigurations(SystemWrapper.Instance); + _instance ??= new PowertoolsConfigurations(PowertoolsEnvironment.Instance); /// /// Gets the environment variable. @@ -68,7 +65,7 @@ internal PowertoolsConfigurations(ISystemWrapper systemWrapper) /// System.String. public string GetEnvironmentVariable(string variable) { - return _systemWrapper.GetEnvironmentVariable(variable); + return _powertoolsEnvironment.GetEnvironmentVariable(variable); } /// @@ -79,7 +76,7 @@ public string GetEnvironmentVariable(string variable) /// System.String. public string GetEnvironmentVariableOrDefault(string variable, string defaultValue) { - var result = _systemWrapper.GetEnvironmentVariable(variable); + var result = _powertoolsEnvironment.GetEnvironmentVariable(variable); return string.IsNullOrWhiteSpace(result) ? defaultValue : result; } @@ -91,7 +88,7 @@ public string GetEnvironmentVariableOrDefault(string variable, string defaultVal /// System.Int32. public int GetEnvironmentVariableOrDefault(string variable, int defaultValue) { - var result = _systemWrapper.GetEnvironmentVariable(variable); + var result = _powertoolsEnvironment.GetEnvironmentVariable(variable); return int.TryParse(result, out var parsedValue) ? parsedValue : defaultValue; } @@ -103,7 +100,7 @@ public int GetEnvironmentVariableOrDefault(string variable, int defaultValue) /// true if XXXX, false otherwise. public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) { - return bool.TryParse(_systemWrapper.GetEnvironmentVariable(variable), out var result) + return bool.TryParse(_powertoolsEnvironment.GetEnvironmentVariable(variable), out var result) ? result : defaultValue; } @@ -161,7 +158,8 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// /// The logger sample rate. public double LoggerSampleRate => - double.TryParse(_systemWrapper.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) + double.TryParse(_powertoolsEnvironment.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), + NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) ? result : 0; @@ -191,7 +189,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// /// true if this instance is Lambda; otherwise, false. public bool IsLambdaEnvironment => GetEnvironmentVariable(Constants.LambdaTaskRoot) is not null; - + /// /// Gets a value indicating whether [tracing is disabled]. /// @@ -202,7 +200,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// public void SetExecutionEnvironment(T type) { - _systemWrapper.SetExecutionEnvironment(type); + _powertoolsEnvironment.SetExecutionEnvironment(type); } /// @@ -210,20 +208,24 @@ public void SetExecutionEnvironment(T type) GetEnvironmentVariableOrDefault(Constants.IdempotencyDisabledEnv, false); /// - public string BatchProcessingErrorHandlingPolicy => GetEnvironmentVariableOrDefault(Constants.BatchErrorHandlingPolicyEnv, "DeriveFromEvent"); + public string BatchProcessingErrorHandlingPolicy => + GetEnvironmentVariableOrDefault(Constants.BatchErrorHandlingPolicyEnv, "DeriveFromEvent"); /// - public bool BatchParallelProcessingEnabled => GetEnvironmentVariableOrDefault(Constants.BatchParallelProcessingEnabled, false); + public bool BatchParallelProcessingEnabled => + GetEnvironmentVariableOrDefault(Constants.BatchParallelProcessingEnabled, false); /// - public int BatchProcessingMaxDegreeOfParallelism => GetEnvironmentVariableOrDefault(Constants.BatchMaxDegreeOfParallelismEnv, 1); + public int BatchProcessingMaxDegreeOfParallelism => + GetEnvironmentVariableOrDefault(Constants.BatchMaxDegreeOfParallelismEnv, 1); /// - public bool BatchThrowOnFullBatchFailureEnabled => GetEnvironmentVariableOrDefault(Constants.BatchThrowOnFullBatchFailureEnv, true); + public bool BatchThrowOnFullBatchFailureEnabled => + GetEnvironmentVariableOrDefault(Constants.BatchThrowOnFullBatchFailureEnv, true); /// public bool MetricsDisabled => GetEnvironmentVariableOrDefault(Constants.PowertoolsMetricsDisabledEnv, false); - + /// public bool IsColdStart => LambdaLifecycleTracker.IsColdStart; diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs index 3ad5317c..649418a4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace AWS.Lambda.Powertools.Common; @@ -40,4 +41,52 @@ public string GetAssemblyVersion(T type) var version = type.GetType().Assembly.GetName().Version; return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : string.Empty; } + + /// + public void SetExecutionEnvironment(T type) + { + const string envName = Constants.AwsExecutionEnvironmentVariableName; + var envValue = new StringBuilder(); + var currentEnvValue = GetEnvironmentVariable(envName); + var assemblyName = ParseAssemblyName(GetAssemblyName(type)); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if (!string.IsNullOrEmpty(currentEnvValue)) + { + // Avoid duplication - should not happen since the calling Instances are Singletons - defensive purposes + if (currentEnvValue.Contains(assemblyName)) + { + return; + } + + envValue.Append($"{currentEnvValue} "); + } + + var assemblyVersion = GetAssemblyVersion(type); + + envValue.Append($"{assemblyName}/{assemblyVersion}"); + + SetEnvironmentVariable(envName, envValue.ToString()); + } + + /// + /// Parsing the name to conform with the required naming convention for the UserAgent header (PTFeature/Name/Version) + /// Fallback to Assembly Name on exception + /// + /// + /// + private string ParseAssemblyName(string assemblyName) + { + try + { + var parsedName = assemblyName.Substring(assemblyName.LastIndexOf(".", StringComparison.Ordinal) + 1); + return $"{Constants.FeatureContextIdentifier}/{parsedName}"; + } + catch + { + //NOOP + } + + return $"{Constants.FeatureContextIdentifier}/{assemblyName}"; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs b/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs new file mode 100644 index 00000000..b5dded35 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs @@ -0,0 +1,49 @@ +using System.Text; + +namespace AWS.Lambda.Powertools.Common.Tests; + +/// +/// Test logger output +/// +public class TestLoggerOutput : IConsoleWrapper +{ + /// + /// Buffer for all the log messages written to the logger. + /// + private readonly StringBuilder _outputBuffer = new(); + + /// + /// Cleasr the output buffer. + /// + public void Clear() + { + _outputBuffer.Clear(); + } + + /// + /// Output the contents of the buffer. + /// + /// + public override string ToString() + { + return _outputBuffer.ToString(); + } + + /// + public void WriteLine(string message) + { + _outputBuffer.AppendLine(message); + } + + /// + public void Debug(string message) + { + _outputBuffer.AppendLine(message); + } + + /// + public void Error(string message) + { + _outputBuffer.AppendLine(message); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index 00150b72..dc3d6e0a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -15,7 +15,7 @@ - + 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 a4a1478f..f68c3297 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 @@ -15,7 +15,8 @@ - + + diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.cs new file mode 100644 index 00000000..1cc65897 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.cs @@ -0,0 +1,29 @@ +/* + * 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. + */ + + +namespace AWS.Lambda.Powertools.Logging.Internal; + +internal class BufferedLogEntry +{ + public string Entry { get; } + public int Size { get; } + + public BufferedLogEntry(string entry, int calculatedSize) + { + Entry = entry; + Size = calculatedSize; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs new file mode 100644 index 00000000..7b612a83 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -0,0 +1,98 @@ +/* + * 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.Concurrent; +using AWS.Lambda.Powertools.Common; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Logger provider that supports buffering logs +/// +[ProviderAlias("PowertoolsBuffering")] +internal class BufferingLoggerProvider : PowertoolsLoggerProvider +{ + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + private readonly ConcurrentDictionary _loggers = new(); + + internal BufferingLoggerProvider( + PowertoolsLoggerConfiguration config, + IPowertoolsConfigurations powertoolsConfigurations) + : base(config, powertoolsConfigurations) + { + _powertoolsConfigurations = powertoolsConfigurations; + // Register with the buffer manager + LogBufferManager.RegisterProvider(this); + } + + public override ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd( + categoryName, + name => new PowertoolsBufferingLogger( + base.CreateLogger(name), // Use the parent's logger creation + GetCurrentConfig, + _powertoolsConfigurations)); + } + + /// + /// Flush all buffered logs + /// + internal void FlushBuffers() + { + foreach (var logger in _loggers.Values) + { + logger.FlushBuffer(); + } + } + + /// + /// Clear all buffered logs + /// + internal void ClearBuffers() + { + foreach (var logger in _loggers.Values) + { + logger.ClearBuffer(); + } + } + + /// + /// Clear buffered logs for the current invocation only + /// + internal void ClearCurrentBuffer() + { + foreach (var logger in _loggers.Values) + { + logger.ClearCurrentInvocation(); + } + } + + public override void Dispose() + { + // Flush all buffers before disposing + foreach (var logger in _loggers.Values) + { + logger.FlushBuffer(); + } + + // Unregister from buffer manager + LogBufferManager.UnregisterProvider(this); + + _loggers.Clear(); + base.Dispose(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs new file mode 100644 index 00000000..8bf75312 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs @@ -0,0 +1,78 @@ +/* + * 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; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Buffer for a specific invocation +/// +internal class InvocationBuffer +{ + private readonly ConcurrentQueue _buffer = new(); + private int _currentSize; + + public void Add(string logEntry, int maxBytes, int size) + { + // If entry size exceeds max buffer size, discard the entry completely + if (size > maxBytes) + { + // Entry is too large to ever fit in buffer, discard it + return; + } + + if (_currentSize + size > maxBytes) + { + // Remove oldest entries until we have enough space + while (_currentSize + size > maxBytes && _buffer.TryDequeue(out var removed)) + { + _currentSize -= removed.Size; + HasEvictions = true; + } + + if (_currentSize < 0) _currentSize = 0; + } + + _buffer.Enqueue(new BufferedLogEntry(logEntry, size)); + _currentSize += size; + } + + public IReadOnlyCollection GetAndClear() + { + var entries = new List(); + + try + { + while (_buffer.TryDequeue(out var entry)) + { + entries.Add(entry.Entry); + } + } + catch (Exception) + { + _buffer.Clear(); + } + + _currentSize = 0; + return entries; + } + + public bool HasEntries => !_buffer.IsEmpty; + + public bool HasEvictions; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs new file mode 100644 index 00000000..db19e096 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -0,0 +1,125 @@ +/* + * 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 AWS.Lambda.Powertools.Common; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// A buffer for storing log entries, with isolation per Lambda invocation +/// +internal class LogBuffer +{ + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + +// Dictionary of buffers by invocation ID + private readonly ConcurrentDictionary _buffersByInvocation = new(); + private string _lastInvocationId; + + // Get the current invocation ID or create a fallback + private string CurrentInvocationId => _powertoolsConfigurations.XRayTraceId; + + public LogBuffer(IPowertoolsConfigurations powertoolsConfigurations) + { + _powertoolsConfigurations = powertoolsConfigurations; + } + + /// + /// Add a log entry to the buffer for the current invocation + /// + public void Add(string logEntry, int maxBytes, int size) + { + var invocationId = CurrentInvocationId; + if (string.IsNullOrEmpty(invocationId)) + { + // No invocation ID set, do not buffer + return; + } + + // If this is a new invocation ID, clear previous buffers + if (_lastInvocationId != invocationId) + { + if (_lastInvocationId != null) + _buffersByInvocation.Clear(); + _lastInvocationId = invocationId; + } + + var buffer = _buffersByInvocation.GetOrAdd(invocationId, _ => new InvocationBuffer()); + buffer.Add(logEntry, maxBytes, size); + } + + /// + /// Get all entries for the current invocation and clear that buffer + /// + public IReadOnlyCollection GetAndClear() + { + var invocationId = CurrentInvocationId; + + if (string.IsNullOrEmpty(invocationId)) + { + // No invocation ID set, return empty + return Array.Empty(); + } + + // Try to get and remove the buffer for this invocation + if (_buffersByInvocation.TryRemove(invocationId, out var buffer)) + { + return buffer.GetAndClear(); + } + + return Array.Empty(); + } + + /// + /// Clear all buffers + /// + public void Clear() + { + _buffersByInvocation.Clear(); + } + + /// + /// Clear buffer for the current invocation + /// + public void ClearCurrentInvocation() + { + var invocationId = CurrentInvocationId; + _buffersByInvocation.TryRemove(invocationId, out _); + } + + /// + /// Check if the current invocation has any buffered entries + /// + public bool HasEntries + { + get + { + var invocationId = CurrentInvocationId; + return _buffersByInvocation.TryGetValue(invocationId, out var buffer) && buffer.HasEntries; + } + } + + public bool HasEvictions + { + get + { + var invocationId = CurrentInvocationId; + return _buffersByInvocation.TryGetValue(invocationId, out var buffer) && buffer.HasEvictions; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs new file mode 100644 index 00000000..9e3a3aa8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs @@ -0,0 +1,89 @@ +/* + * 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; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Singleton manager for log buffer operations with invocation context awareness +/// +internal static class LogBufferManager +{ + private static readonly List Providers = new(); + + /// + /// Register a buffering provider with the manager + /// + internal static void RegisterProvider(BufferingLoggerProvider provider) + { + if (!Providers.Contains(provider)) + Providers.Add(provider); + } + + /// + /// Flush buffered logs for the current invocation + /// + internal static void FlushCurrentBuffer() + { + try + { + foreach (var provider in Providers) + { + provider?.FlushBuffers(); + } + } + catch (Exception) + { + // Suppress errors + } + } + + /// + /// Clear buffered logs for the current invocation + /// + internal static void ClearCurrentBuffer() + { + try + { + foreach (var provider in Providers) + { + provider?.ClearCurrentBuffer(); + } + } + catch (Exception) + { + // Suppress errors + } + } + + /// + /// Unregister a buffering provider from the manager + /// + /// + internal static void UnregisterProvider(BufferingLoggerProvider provider) + { + Providers.Remove(provider); + } + + /// + /// Reset the manager state (for testing purposes) + /// + internal static void ResetForTesting() + { + Providers.Clear(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs new file mode 100644 index 00000000..9e715c55 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs @@ -0,0 +1,40 @@ +/* * 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 * .cs +/* + * 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.Internal; + +namespace AWS.Lambda.Powertools.Logging; + +public static partial class Logger +{ + /// + /// Flush any buffered logs + /// + public static void FlushBuffer() + { + // Use the buffer manager directly + LogBufferManager.FlushCurrentBuffer(); + } + + /// + /// Clear any buffered logs without writing them + /// + public static void ClearBuffer() + { + // Use the buffer manager directly + LogBufferManager.ClearCurrentBuffer(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs new file mode 100644 index 00000000..fb2f32c0 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -0,0 +1,164 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Logger implementation that supports buffering +/// +internal class PowertoolsBufferingLogger : ILogger +{ + private readonly ILogger _logger; + private readonly Func _getCurrentConfig; + private readonly LogBuffer _buffer; + + public PowertoolsBufferingLogger( + ILogger logger, + Func getCurrentConfig, + IPowertoolsConfigurations powertoolsConfigurations) + { + _logger = logger; + _getCurrentConfig = getCurrentConfig; + _buffer = new LogBuffer(powertoolsConfigurations); + } + + public IDisposable BeginScope(TState state) + { + return _logger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + var options = _getCurrentConfig(); + var bufferOptions = options.LogBuffering; + + // Check if this log should be buffered + bool shouldBuffer = logLevel <= bufferOptions.BufferAtLogLevel; + + if (shouldBuffer) + { + // Add to buffer instead of logging + try + { + if (_logger is PowertoolsLogger powertoolsLogger) + { + var logEntry = powertoolsLogger.LogEntryString(logLevel, state, exception, formatter); + + // Check the size of the log entry, log it if too large + var size = 100 + (logEntry?.Length ?? 0) * 2; + if (size > bufferOptions.MaxBytes) + { + // log the entry directly if it exceeds the buffer size + powertoolsLogger.LogLine(logEntry); + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Cannot add item to the buffer"); + } + else + { + _buffer.Add(logEntry, bufferOptions.MaxBytes, size); + } + } + } + catch (Exception ex) + { + // If buffering fails, try to log an error about it + try + { + _logger.LogError(ex, "Failed to buffer log entry"); + } + catch + { + // Last resort: if even that fails, just suppress the error + } + } + } + else + { + // If this is an error and we should flush on error + if (bufferOptions.FlushOnErrorLog && + logLevel >= LogLevel.Error) + { + FlushBuffer(); + } + } + } + + /// + /// Flush buffered logs to the inner logger + /// + public void FlushBuffer() + { + try + { + if (_logger is PowertoolsLogger powertoolsLogger) + { + if (_buffer.HasEvictions) + { + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer"); + } + + // Get all buffered entries + var entries = _buffer.GetAndClear(); + + // Log each entry directly + foreach (var entry in entries) + { + powertoolsLogger.LogLine(entry); + } + } + } + catch (Exception ex) + { + // If the entire flush operation fails, try to log an error + try + { + _logger.LogError(ex, "Failed to flush log buffer"); + } + catch + { + // If even that fails, just suppress the error + } + } + } + + /// + /// Clear the buffer without logging + /// + public void ClearBuffer() + { + _buffer.Clear(); + } + + /// + /// Clear buffered logs only for the current invocation + /// + public void ClearCurrentInvocation() + { + _buffer.ClearCurrentInvocation(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs index b6d7120d..b868aa64 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs @@ -34,31 +34,30 @@ internal class ByteArrayConverter : JsonConverter /// public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotSupportedException("Deserializing ByteArray is not allowed"); + if (reader.TokenType == JsonTokenType.Null) + return []; + + if (reader.TokenType == JsonTokenType.String) + return Convert.FromBase64String(reader.GetString()!); + + throw new JsonException("Expected string value for byte array"); } /// /// Write the exception value as JSON. /// /// The unicode JsonWriter. - /// The byte array. + /// /// The JsonSerializer options. - public override void Write(Utf8JsonWriter writer, byte[] values, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) { - if (values == null) + if (value == null) { writer.WriteNullValue(); + return; } - else - { - writer.WriteStartArray(); - - foreach (var value in values) - { - writer.WriteNumberValue(value); - } - - writer.WriteEndArray(); - } + + string base64 = Convert.ToBase64String(value); + writer.WriteStringValue(base64); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs index 1bc0f6e9..e6c3aebb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Converters; /// /// JsonConvert to handle the AWS SDK for .NET custom enum classes that derive from the class called ConstantClass. /// -public class ConstantClassConverter : JsonConverter +internal class ConstantClassConverter : JsonConverter { private static readonly HashSet ConstantClassNames = new() { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs index a6f969e5..ecfd62a4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Converters; /// /// DateOnly JSON converter /// -public class DateOnlyConverter : JsonConverter +internal class DateOnlyConverter : JsonConverter { private const string DateFormat = "yyyy-MM-dd"; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs new file mode 100644 index 00000000..9ce483f8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; + +/// +/// Helper class for creating and configuring logger factories +/// +internal static class LoggerFactoryHelper +{ + /// + /// Creates and configures a logger factory with the provided configuration + /// + /// The Powertools logger configuration to apply + /// The configured logger factory + internal static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfiguration configuration) + { + var factory = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = configuration.Service; + config.TimestampFormat = configuration.TimestampFormat; + config.MinimumLogLevel = configuration.MinimumLogLevel; + config.SamplingRate = configuration.SamplingRate; + config.LoggerOutputCase = configuration.LoggerOutputCase; + config.LogLevelKey = configuration.LogLevelKey; + config.LogFormatter = configuration.LogFormatter; + config.JsonOptions = configuration.JsonOptions; + config.LogBuffering = configuration.LogBuffering; + config.LogOutput = configuration.LogOutput; + config.XRayTraceId = configuration.XRayTraceId; + config.LogEvent = configuration.LogEvent; + }); + + // Use current filter level or level from config + if (configuration.MinimumLogLevel != LogLevel.None) + { + builder.AddFilter(null, configuration.MinimumLogLevel); + builder.SetMinimumLevel(configuration.MinimumLogLevel); + } + }); + + LoggerFactoryHolder.SetFactory(factory); + + return factory; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs new file mode 100644 index 00000000..ce96ea73 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs @@ -0,0 +1,78 @@ +/* + * 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.Logging.Internal.Helpers; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Holds and manages the shared logger factory instance +/// +internal static class LoggerFactoryHolder +{ + private static ILoggerFactory _factory; + private static readonly object _lock = new object(); + + /// + /// Gets or creates the shared logger factory + /// + public static ILoggerFactory GetOrCreateFactory() + { + lock (_lock) + { + if (_factory == null) + { + var config = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + _factory = LoggerFactoryHelper.CreateAndConfigureFactory(config); + } + return _factory; + } + } + + public static void SetFactory(ILoggerFactory factory) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + lock (_lock) + { + _factory = factory; + Logger.ClearInstance(); + } + } + + /// + /// Resets the factory holder for testing + /// + internal static void Reset() + { + lock (_lock) + { + // Dispose the old factory if it exists + if (_factory == null) return; + try + { + _factory.Dispose(); + } + catch + { + // Ignore disposal errors + } + + _factory = null; + } + } +} \ 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 deleted file mode 100644 index 94bb1c0d..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ /dev/null @@ -1,86 +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.Collections.Concurrent; -using AWS.Lambda.Powertools.Common; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace AWS.Lambda.Powertools.Logging.Internal; - -/// -/// Class LoggerProvider. This class cannot be inherited. -/// Implements the -/// -/// -public sealed class LoggerProvider : ILoggerProvider -{ - /// - /// The powertools configurations - /// - private readonly IPowertoolsConfigurations _powertoolsConfigurations; - - /// - /// The system wrapper - /// - private readonly ISystemWrapper _systemWrapper; - - /// - /// The loggers - /// - private readonly ConcurrentDictionary _loggers = new(); - - - /// - /// Initializes a new instance of the class. - /// - /// 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) - : this(config, PowertoolsConfigurations.Instance, SystemWrapper.Instance) { } - - /// - /// Creates a new instance. - /// - /// The category name for messages produced by the logger. - /// The instance of that was created. - public ILogger CreateLogger(string categoryName) - { - return _loggers.GetOrAdd(categoryName, - name => PowertoolsLogger.CreateLogger(name, - _powertoolsConfigurations, - _systemWrapper)); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - _loggers.Clear(); - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 9a444405..75f93dc8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -16,13 +16,10 @@ 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.Common.Core; -using AWS.Lambda.Powertools.Logging.Serializers; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -32,8 +29,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Scope.Global is singleton /// /// -[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] -public class LoggingAspect +public class LoggingAspect : IMethodAspectHandler { /// /// The initialize context @@ -45,21 +41,6 @@ public class LoggingAspect /// 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 /// @@ -70,132 +51,48 @@ public class LoggingAspect /// private bool _clearLambdaContext; - /// - /// The configuration - /// - private LoggerConfiguration _config; + private ILogger _logger; + private bool _isDebug; + private bool _bufferingEnabled; + private PowertoolsLoggerConfiguration _currentConfig; + private bool _flushBufferOnUncaughtError; /// /// Initializes a new instance of the class. /// - /// The Powertools configurations. - /// The system wrapper. - public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) + public LoggingAspect(ILogger logger) { - _powertoolsConfigurations = powertoolsConfigurations; - _systemWrapper = systemWrapper; + _logger = logger ?? LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); } - /// - /// 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) + private void InitializeLogger(LoggingAttribute trigger) { - // Called before the method - var trigger = triggers.OfType().First(); + // Check which settings are explicitly provided in the attribute + var hasLogLevel = trigger.LogLevel != LogLevel.None; + var hasService = !string.IsNullOrEmpty(trigger.Service); + var hasOutputCase = trigger.LoggerOutputCase != LoggerOutputCase.Default; + var hasSamplingRate = trigger.SamplingRate > 0; - 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; + // Only update configuration if any settings were provided + var needsReconfiguration = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; + _currentConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); - Logger.AppendKey(LoggingConstants.KeyColdStart, LambdaLifecycleTracker.IsColdStart); - - _initializeContext = false; - _isContextInitialized = true; - - var eventObject = eventArgs.Args.FirstOrDefault(); - CaptureXrayTraceId(); - CaptureLambdaContext(eventArgs); - CaptureCorrelationId(eventObject); - if (logEvent || _powertoolsConfigurations.LoggerLogEvent) - LogEvent(eventObject); - } - catch (Exception exception) + if (needsReconfiguration) { - // 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(); + // Apply each setting directly using the existing Logger static methods + if (hasLogLevel) _currentConfig.MinimumLogLevel = trigger.LogLevel; + if (hasService) _currentConfig.Service = trigger.Service; + if (hasOutputCase) _currentConfig.LoggerOutputCase = trigger.LoggerOutputCase; + if (hasSamplingRate) _currentConfig.SamplingRate = trigger.SamplingRate; + + // Need to refresh the logger after configuration changes + _logger = LoggerFactoryHelper.CreateAndConfigureFactory(_currentConfig).CreatePowertoolsLogger(); + Logger.ClearInstance(); } - } - - /// - /// 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); + // Set operational flags based on current configuration + _isDebug = _currentConfig.MinimumLogLevel <= LogLevel.Debug; + _bufferingEnabled = _currentConfig.LogBuffering != null; } /// @@ -208,8 +105,8 @@ private void CaptureXrayTraceId() private void CaptureLambdaContext(AspectEventArgs eventArgs) { _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs); - if (LoggingLambdaContext.Instance is null && IsDebug()) - _systemWrapper.LogLine( + if (LoggingLambdaContext.Instance is null && _isDebug) + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Skipping Lambda Context injection because ILambdaContext context parameter not found."); } @@ -217,12 +114,13 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) /// Captures the correlation identifier. /// /// The event argument. - private void CaptureCorrelationId(object eventArg) + /// + private void CaptureCorrelationId(object eventArg, string correlationIdPath) { - if (string.IsNullOrWhiteSpace(_correlationIdPath)) + if (string.IsNullOrWhiteSpace(correlationIdPath)) return; - var correlationIdPaths = _correlationIdPath + var correlationIdPaths = correlationIdPath .Split(CorrelationIdPaths.Separator, StringSplitOptions.RemoveEmptyEntries); if (!correlationIdPaths.Any()) @@ -230,8 +128,8 @@ private void CaptureCorrelationId(object eventArg) if (eventArg is null) { - if (IsDebug()) - _systemWrapper.LogLine( + if (_isDebug) + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Skipping CorrelationId capture because event parameter not found."); return; } @@ -241,16 +139,16 @@ private void CaptureCorrelationId(object eventArg) var correlationId = string.Empty; var jsonDoc = - JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); + JsonDocument.Parse(_currentConfig.Serializer.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); + // TODO: For casing parsing to be removed from Logging v2 when we get rid of outputcase without this CorrelationIdPaths.ApiGatewayRest would not work + // TODO: This will be removed and replaced by JMesPath + + var pathWithOutputCase = correlationIdPaths[i].ToCase(_currentConfig.LoggerOutputCase); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; @@ -260,12 +158,12 @@ private void CaptureCorrelationId(object eventArg) } if (!string.IsNullOrWhiteSpace(correlationId)) - Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); + _logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); } catch (Exception e) { - if (IsDebug()) - _systemWrapper.LogLine( + if (_isDebug) + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), $"Skipping CorrelationId capture because of error caused while parsing the event object {e.Message}."); } } @@ -280,30 +178,30 @@ private void LogEvent(object eventArg) { case null: { - if (IsDebug()) - _systemWrapper.LogLine( + if (_isDebug) + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Skipping Event Log because event parameter not found."); break; } case Stream: try { - Logger.LogInformation(eventArg); + _logger.LogInformation(eventArg); } catch (Exception e) { - Logger.LogError(e, "Failed to log event from supplied input stream."); + _logger.LogError(e, "Failed to log event from supplied input stream."); } break; default: try { - Logger.LogInformation(eventArg); + _logger.LogInformation(eventArg); } catch (Exception e) { - Logger.LogError(e, "Failed to log event from supplied input object."); + _logger.LogError(e, "Failed to log event from supplied input object."); } break; @@ -316,8 +214,95 @@ private void LogEvent(object eventArg) internal static void ResetForTest() { LoggingLambdaContext.Clear(); - Logger.LoggerProvider = null; - Logger.RemoveAllKeys(); - Logger.ClearLoggerInstance(); + } + + /// + /// Entry point for the aspect. + /// + /// + public void OnEntry(AspectEventArgs eventArgs) + { + var trigger = eventArgs.Triggers.OfType().First(); + try + { + _clearState = trigger.ClearState; + + InitializeLogger(trigger); + + if (!_initializeContext) + return; + + _initializeContext = false; + _isContextInitialized = true; + _flushBufferOnUncaughtError = trigger.FlushBufferOnUncaughtError; + + var eventObject = eventArgs.Args.FirstOrDefault(); + CaptureLambdaContext(eventArgs); + CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); + + switch (trigger.IsLogEventSet) + { + case true when trigger.LogEvent: + case false when _currentConfig.LogEvent: + LogEvent(eventObject); + break; + } + } + catch (Exception exception) + { + if (_bufferingEnabled && _flushBufferOnUncaughtError) + { + _logger.FlushBuffer(); + } + + // 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(); + } + } + + /// + /// When the method returns successfully, this method is called. + /// + /// + /// + public void OnSuccess(AspectEventArgs eventArgs, object result) + { + + } + + /// + /// When the method throws an exception, this method is called. + /// + /// + /// + public void OnException(AspectEventArgs eventArgs, Exception exception) + { + if (_bufferingEnabled && _flushBufferOnUncaughtError) + { + _logger.FlushBuffer(); + } + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + /// + /// WHen the method exits, this method is called even if it throws an exception. + /// + /// + public void OnExit(AspectEventArgs eventArgs) + { + if (!_isContextInitialized) + return; + if (_clearLambdaContext) + LoggingLambdaContext.Clear(); + if (_clearState) + _logger.RemoveAllKeys(); + _initializeContext = true; + + if (_bufferingEnabled) + { + // clear the buffer after the handler has finished + _logger.ClearBuffer(); + } } } \ 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 index 5feae3cf..c0a49da8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -19,7 +19,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// -/// Class LoggingAspectFactory. For "dependency inject" Configuration and SystemWrapper to Aspect +/// Class LoggingAspectFactory. For "dependency inject" Aspect /// internal static class LoggingAspectFactory { @@ -30,6 +30,6 @@ internal static class LoggingAspectFactory /// An instance of the LoggingAspect class. public static object GetInstance(Type type) { - return new LoggingAspect(PowertoolsConfigurations.Instance, SystemWrapper.Instance); + return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); } } \ 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 index 17c5a3a8..9732bad0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs @@ -7,7 +7,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// Lambda Context /// -public class LoggingLambdaContext +internal class LoggingLambdaContext { /// /// The AWS request ID associated with the request. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 148bb540..3fedb7e6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -15,22 +15,42 @@ 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; +internal static class LambdaLogLevelMapper +{ + public static string ToLambdaLogLevel(this LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return "trace"; + case LogLevel.Debug: + return "debug"; + case LogLevel.Information: + return "info"; + case LogLevel.Warning: + return "warn"; + case LogLevel.Error: + return "error"; + case LogLevel.Critical: + return "fatal"; + default: + return "info"; + } + } +} + + + /// /// 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 /// @@ -91,88 +111,6 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati 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]. @@ -183,149 +121,4 @@ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powert { return powertoolsConfigurations.GetLambdaLogLevel() != LogLevel.None; } - - /// - /// Converts the input string to the configured output case. - /// - /// - /// The string to convert. - /// - /// - /// The input string converted to the configured case (camel, pascal, or snake case). - /// - internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, - string correlationIdPath, LoggerOutputCase loggerOutputCase) - { - 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 6e72d102..ccbac6c3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -17,9 +17,9 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; -using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -31,20 +31,19 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal sealed class PowertoolsLogger : ILogger { + private static string _originalformat = "{OriginalFormat}"; + /// /// The name /// - private readonly string _name; + private readonly string _categoryName; /// /// The current configuration /// - private readonly IPowertoolsConfigurations _powertoolsConfigurations; + private readonly Func _currentConfig; - /// - /// The system wrapper - /// - private readonly ISystemWrapper _systemWrapper; + private readonly IPowertoolsConfigurations _powertoolsConfigurations; /// /// The current scope @@ -54,32 +53,17 @@ internal sealed class PowertoolsLogger : ILogger /// /// Private constructor - Is initialized on CreateLogger /// - /// The name. - /// The Powertools for AWS Lambda (.NET) configurations. - /// The system wrapper. - private PowertoolsLogger( - string name, - IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper) + /// The name. + /// + /// + public PowertoolsLogger( + string categoryName, + Func getCurrentConfig, + IPowertoolsConfigurations powertoolsConfigurations) { - _name = name; + _categoryName = categoryName; + _currentConfig = getCurrentConfig; _powertoolsConfigurations = powertoolsConfigurations; - _systemWrapper = systemWrapper; - - _powertoolsConfigurations.SetExecutionEnvironment(this); - } - - /// - /// Initializes a new instance of the class. - /// - /// 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); } /// @@ -108,7 +92,32 @@ internal void EndScope() /// The log level. /// bool. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsEnabled(LogLevel logLevel) => _powertoolsConfigurations.IsLogLevelEnabled(logLevel); + public bool IsEnabled(LogLevel logLevel) + { + var config = _currentConfig(); + + //if Buffering is enabled and the log level is below the buffer threshold, skip logging only if bellow error + if (logLevel <= config.LogBuffering?.BufferAtLogLevel + && config.LogBuffering?.BufferAtLogLevel != LogLevel.Error + && config.LogBuffering?.BufferAtLogLevel != LogLevel.Critical) + { + return false; + } + + // If we have no explicit minimum level, use the default + var effectiveMinLevel = config.MinimumLogLevel != LogLevel.None + ? config.MinimumLogLevel + : LoggingConstants.DefaultLogLevel; + + // Log diagnostic info for Debug/Trace levels + if (logLevel <= LogLevel.Debug) + { + return logLevel >= effectiveMinLevel; + } + + // Standard check + return logLevel >= effectiveMinLevel; + } /// /// Writes a log entry. @@ -122,23 +131,48 @@ internal void EndScope() public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (formatter is null) - throw new ArgumentNullException(nameof(formatter)); - if (!IsEnabled(logLevel)) + { return; + } + + _currentConfig().LogOutput.WriteLine(LogEntryString(logLevel, state, exception, formatter)); + } + + internal void LogLine(string message) + { + _currentConfig().LogOutput.WriteLine(message); + } + + internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, + Func formatter) + { + var logEntry = LogEntry(logLevel, state, exception, formatter); + return _currentConfig().Serializer.Serialize(logEntry, typeof(object)); + } + internal object LogEntry(LogLevel logLevel, TState state, Exception exception, + Func formatter) + { var timestamp = DateTime.UtcNow; + + if (formatter is null) + throw new ArgumentNullException(nameof(formatter)); + + // Extract structured parameters for template-style logging + var structuredParameters = ExtractStructuredParameters(state, out _); + + // Format the message var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null ? customMessage : formatter(state, exception); - var logFormatter = Logger.GetFormatter(); + // Get log entry + var logFormatter = _currentConfig().LogFormatter; var logEntry = logFormatter is null - ? GetLogEntry(logLevel, timestamp, message, exception) - : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); - - _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); + ? GetLogEntry(logLevel, timestamp, message, exception, structuredParameters) + : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter, structuredParameters); + return logEntry; } /// @@ -148,16 +182,18 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// Entry timestamp. /// The message to be written. Can be also an object. /// The exception related to this entry. + /// The parameters for structured formatting private Dictionary GetLogEntry(LogLevel logLevel, DateTime timestamp, object message, - Exception exception) + Exception exception, Dictionary structuredParameters = null) { var logEntry = new Dictionary(); - // Add Custom Keys - foreach (var (key, value) in Logger.GetAllKeys()) - { - logEntry.TryAdd(key, value); - } + var config = _currentConfig(); + logEntry.TryAdd(config.LogLevelKey, logLevel.ToString()); + logEntry.TryAdd(LoggingConstants.KeyMessage, message); + logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString(config.TimestampFormat ?? "o")); + logEntry.TryAdd(LoggingConstants.KeyService, config.Service); + logEntry.TryAdd(LoggingConstants.KeyColdStart, _powertoolsConfigurations.IsColdStart); // Add Lambda Context Keys if (LoggingLambdaContext.Instance is not null) @@ -165,31 +201,86 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times AddLambdaContextKeys(logEntry); } + if (!string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) + logEntry.TryAdd(LoggingConstants.KeyXRayTraceId, + _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0] + .Replace("Root=", "")); + logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); + + if (config.SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, config.SamplingRate); + + // Add Custom Keys + foreach (var (key, value) in this.GetAllKeys()) + { + // Skip keys that are already defined in LoggingConstants + if (!IsLogConstantKey(key)) + { + logEntry.TryAdd(key, value); + } + } + // Add Extra Fields if (CurrentScope?.ExtraKeys is not null) { foreach (var (key, value) in CurrentScope.ExtraKeys) { - if (!string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrWhiteSpace(key)) continue; + if (!IsLogConstantKey(key)) + { logEntry.TryAdd(key, value); + } } } - var keyLogLevel = GetLogLevelKey(); + // Add structured parameters + if (structuredParameters != null && structuredParameters.Count > 0) + { + foreach (var (key, value) in structuredParameters) + { + if (string.IsNullOrWhiteSpace(key) || key == "json") continue; + if (!IsLogConstantKey(key)) + { + logEntry.TryAdd(key, value); + } + } + } - logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o")); - logEntry.TryAdd(keyLogLevel, logLevel.ToString()); - logEntry.TryAdd(LoggingConstants.KeyService, _powertoolsConfigurations.CurrentConfig().Service); - logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); - logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (_powertoolsConfigurations.CurrentConfig().SamplingRate > 0) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, _powertoolsConfigurations.CurrentConfig().SamplingRate); + // Use the AddExceptionDetails method instead of adding exception directly if (exception != null) + { logEntry.TryAdd(LoggingConstants.KeyException, exception); + } return logEntry; } + /// + /// Checks if a key is defined in LoggingConstants + /// + /// The key to check + /// true if the key is a LoggingConstants key + private bool IsLogConstantKey(string key) + { + return string.Equals(key.ToPascal(), LoggingConstants.KeyColdStart, StringComparison.OrdinalIgnoreCase) + // || string.Equals(key.ToPascal(), LoggingConstants.KeyCorrelationId, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyException, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionArn, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionMemorySize, + StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionName, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionRequestId, + StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionVersion, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyLoggerName, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyLogLevel, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyMessage, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeySamplingRate, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyService, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyTimestamp, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyXRayTraceId, StringComparison.OrdinalIgnoreCase); + } + /// /// Gets a formatted log entry. For custom log formatter /// @@ -198,27 +289,29 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times /// The message to be written. Can be also an object. /// The exception related to this entry. /// The custom log entry formatter. + /// The structured parameters. private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, object message, - Exception exception, ILogFormatter logFormatter) + Exception exception, ILogFormatter logFormatter, Dictionary structuredParameters) { if (logFormatter is null) return null; + var config = _currentConfig(); var logEntry = new LogEntry { Timestamp = timestamp, Level = logLevel, - Service = _powertoolsConfigurations.CurrentConfig().Service, - Name = _name, + Service = config.Service, + Name = _categoryName, Message = message, - Exception = exception, - SamplingRate = _powertoolsConfigurations.CurrentConfig().SamplingRate, + Exception = exception, // Keep this to maintain compatibility + SamplingRate = config.SamplingRate, }; var extraKeys = new Dictionary(); // Add Custom Keys - foreach (var (key, value) in Logger.GetAllKeys()) + foreach (var (key, value) in this.GetAllKeys()) { switch (key) { @@ -243,7 +336,34 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec foreach (var (key, value) in CurrentScope.ExtraKeys) { if (!string.IsNullOrWhiteSpace(key)) + { + extraKeys.TryAdd(key, value); + } + } + } + + // Add structured parameters + if (structuredParameters != null && structuredParameters.Count > 0) + { + foreach (var (key, value) in structuredParameters) + { + if (!string.IsNullOrWhiteSpace(key) && key != "json") + { extraKeys.TryAdd(key, value); + } + } + } + + // Add detailed exception information + if (exception != null) + { + var exceptionDetails = new Dictionary(); + exceptionDetails.TryAdd(LoggingConstants.KeyException, exception); + + // Add exception details to extra keys + foreach (var (key, value) in exceptionDetails) + { + extraKeys.TryAdd(key, value); } } @@ -261,6 +381,7 @@ 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 @@ -300,30 +421,17 @@ private static bool CustomFormatter(TState state, Exception exception, o if (stateKeys is null || stateKeys.Count != 2) return false; - if (!stateKeys.TryGetValue("{OriginalFormat}", out var originalFormat)) + if (!stateKeys.TryGetValue(_originalformat, out var originalFormat)) return false; if (originalFormat?.ToString() != LoggingConstants.KeyJsonFormatter) return false; - message = stateKeys.First(k => k.Key != "{OriginalFormat}").Value; + 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; - } - /// /// Adds the lambda context keys. /// @@ -333,10 +441,10 @@ private void AddLambdaContextKeys(Dictionary logEntry) { var context = LoggingLambdaContext.Instance; logEntry.TryAdd(LoggingConstants.KeyFunctionName, context.FunctionName); - logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, context.FunctionVersion); logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, context.MemoryLimitInMB); logEntry.TryAdd(LoggingConstants.KeyFunctionArn, context.InvokedFunctionArn); logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, context.AwsRequestId); + logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, context.FunctionVersion); } /// @@ -380,6 +488,7 @@ private static Dictionary GetScopeKeys(TState state) } break; + case IEnumerable> objectPairs: foreach (var (key, value) in objectPairs) { @@ -388,10 +497,28 @@ private static Dictionary GetScopeKeys(TState state) } break; + default: + // Skip property reflection for primitive types, strings and value types + if (state is string || + (state.GetType().IsPrimitive) || + state is ValueType) + { + // Don't extract properties from primitives or strings + break; + } + + // For complex objects, use reflection to get properties foreach (var property in state.GetType().GetProperties()) { - keys.TryAdd(property.Name, property.GetValue(state)); + try + { + keys.TryAdd(property.Name, property.GetValue(state)); + } + catch + { + // Safely ignore reflection exceptions + } } break; @@ -399,4 +526,143 @@ private static Dictionary GetScopeKeys(TState state) return keys; } + + /// + /// Extracts structured parameter key-value pairs from the log state + /// + /// Type of the state being logged + /// The log state containing parameters + /// Output parameter for the message template + /// Dictionary of extracted parameter names and values + private Dictionary ExtractStructuredParameters(TState state, out string messageTemplate) + { + messageTemplate = string.Empty; + var parameters = new Dictionary(); + + if (!(state is IEnumerable> stateProps)) + { + return parameters; + } + + // Dictionary to store format specifiers for each parameter + var formatSpecifiers = new Dictionary(); + var statePropsArray = stateProps.ToArray(); + + // First pass - extract message template and identify format specifiers + ExtractFormatSpecifiers(ref messageTemplate, statePropsArray, formatSpecifiers); + + // Second pass - process values with extracted format specifiers + ProcessValuesWithSpecifiers(statePropsArray, formatSpecifiers, parameters); + + return parameters; + } + + private void ProcessValuesWithSpecifiers(KeyValuePair[] statePropsArray, Dictionary formatSpecifiers, + Dictionary parameters) + { + foreach (var prop in statePropsArray) + { + if (prop.Key == _originalformat) + continue; + + // Extract parameter name without braces + var paramName = ExtractParameterName(prop.Key); + if (string.IsNullOrEmpty(paramName)) + continue; + + // Handle special serialization designators (like @) + var useStructuredSerialization = paramName.StartsWith('@'); + var actualParamName = useStructuredSerialization ? paramName.Substring(1) : paramName; + + if (!useStructuredSerialization && + formatSpecifiers.TryGetValue(paramName, out var format) && + prop.Value is IFormattable formattable) + { + // Format the value using the specified format + var formattedValue = formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); + + // Try to preserve the numeric type if possible + if (double.TryParse(formattedValue, out var numericValue)) + { + parameters[actualParamName] = numericValue; + } + else + { + parameters[actualParamName] = formattedValue; + } + } + else if (useStructuredSerialization) + { + // Serialize the entire object + parameters[actualParamName] = prop.Value; + } + else + { + // Handle regular values appropriately + if (prop.Value != null && + !(prop.Value is string) && + !(prop.Value is ValueType) && + !(prop.Value.GetType().IsPrimitive)) + { + // For complex objects, use ToString() representation + parameters[actualParamName] = prop.Value.ToString(); + } + else + { + // For primitives and other simple types, use the value directly + parameters[actualParamName] = prop.Value; + } + } + } + } + + private static void ExtractFormatSpecifiers(ref string messageTemplate, KeyValuePair[] statePropsArray, + Dictionary formatSpecifiers) + { + foreach (var prop in statePropsArray) + { + // The original message template is stored with key "{OriginalFormat}" + if (prop.Key == _originalformat && prop.Value is string template) + { + messageTemplate = template; + + // Extract format specifiers using regex pattern for parameters + var matches = Regex.Matches( + template, + @"{([@\w]+)(?::([^{}]+))?}", + RegexOptions.None, + TimeSpan.FromSeconds(1)); + + foreach (Match match in matches) + { + var paramName = match.Groups[1].Value; + if (match.Groups.Count > 2 && match.Groups[2].Success) + { + formatSpecifiers[paramName] = match.Groups[2].Value; + } + } + + break; + } + } + } + + /// + /// Extracts the parameter name from a template placeholder (e.g. "{paramName}" or "{paramName:format}") + /// + private string ExtractParameterName(string key) + { + // If it's already a proper parameter name without braces, return it + if (!key.StartsWith('{') || !key.EndsWith('}')) + return key; + + // Remove the braces + var nameWithPossibleFormat = key.Substring(1, key.Length - 2); + + // If there's a format specifier, remove it + var colonIndex = nameWithPossibleFormat.IndexOf(':'); + return colonIndex > 0 + ? nameWithPossibleFormat.Substring(0, colonIndex) + : nameWithPossibleFormat; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs new file mode 100644 index 00000000..d29138e8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -0,0 +1,163 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Concurrent; +using AWS.Lambda.Powertools.Common; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Class LoggerProvider. This class cannot be inherited. +/// Implements the +/// +/// +[ProviderAlias("PowertoolsLogger")] +internal class PowertoolsLoggerProvider : ILoggerProvider +{ + private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + private PowertoolsLoggerConfiguration _currentConfig; + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + private bool _environmentConfigured; + + public PowertoolsLoggerProvider( + PowertoolsLoggerConfiguration config, + IPowertoolsConfigurations powertoolsConfigurations) + { + _powertoolsConfigurations = powertoolsConfigurations; + _currentConfig = config; + + // Set execution environment + _powertoolsConfigurations.SetExecutionEnvironment(this); + + // Apply environment configurations if available + ConfigureFromEnvironment(); + } + + public void ConfigureFromEnvironment() + { + var logLevel = _powertoolsConfigurations.GetLogLevel(_currentConfig.MinimumLogLevel); + var lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); + var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); + + // Warn if Lambda log level doesn't match + if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) + { + _currentConfig.LogOutput.WriteLine( + $"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 from environment if not explicitly set + if (string.IsNullOrEmpty(_currentConfig.Service)) + { + _currentConfig.Service = _powertoolsConfigurations.Service; + } + + // Set output case from environment if not explicitly set + if (_currentConfig.LoggerOutputCase == LoggerOutputCase.Default) + { + var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(_currentConfig.LoggerOutputCase); + _currentConfig.LoggerOutputCase = loggerOutputCase; + } + + // Set log level from environment ONLY if not explicitly set + var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + _currentConfig.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; + _currentConfig.XRayTraceId = _powertoolsConfigurations.XRayTraceId; + _currentConfig.LogEvent = _powertoolsConfigurations.LoggerLogEvent; + + // Configure the log level key based on output case + _currentConfig.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && + _currentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase + ? "LogLevel" + : LoggingConstants.KeyLogLevel; + + ProcessSamplingRate(_currentConfig, _powertoolsConfigurations); + _environmentConfigured = true; + } + + /// + /// Process sampling rate configuration + /// + private void ProcessSamplingRate(PowertoolsLoggerConfiguration config, IPowertoolsConfigurations configurations) + { + var samplingRate = config.SamplingRate > 0 + ? config.SamplingRate + : configurations.LoggerSampleRate; + + samplingRate = ValidateSamplingRate(samplingRate, config); + config.SamplingRate = samplingRate; + + // Only notify if sampling is configured + if (samplingRate > 0) + { + double sample = config.GetRandom(); + + // Instead of changing log level, just indicate sampling status + if (sample <= samplingRate) + { + config.LogOutput.WriteLine( + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + config.MinimumLogLevel = LogLevel.Debug; + } + } + } + + /// + /// Validate sampling rate + /// + private double ValidateSamplingRate(double samplingRate, PowertoolsLoggerConfiguration config) + { + if (samplingRate < 0 || samplingRate > 1) + { + if (config.MinimumLogLevel is LogLevel.Debug or LogLevel.Trace) + { + config.LogOutput.WriteLine( + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + } + + return 0; + } + + return samplingRate; + } + + public virtual ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger( + name, + GetCurrentConfig, + _powertoolsConfigurations)); + } + + internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig; + + public void UpdateConfiguration(PowertoolsLoggerConfiguration config) + { + _currentConfig = config; + + // Apply environment configurations if available + if (_powertoolsConfigurations != null && !_environmentConfigured) + { + ConfigureFromEnvironment(); + } + } + + public virtual void Dispose() + { + _loggers.Clear(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs new file mode 100644 index 00000000..7e7b390a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using System.Text; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Extension methods for string case conversion. +/// +internal static class StringCaseExtensions +{ + /// + /// Converts a string to camelCase. + /// + /// The string to convert. + /// A camelCase formatted string. + public static string ToCamel(this string value) + { + if (string.IsNullOrEmpty(value)) + return value; + + // Convert to PascalCase first to handle potential snake_case or kebab-case + string pascalCase = ToPascal(value); + + // Convert first char to lowercase + return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); + } + + /// + /// Converts a string to PascalCase. + /// + /// The string to convert. + /// A PascalCase formatted string. + public static string ToPascal(this 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 snake_case. + /// + /// The string to convert. + /// A snake_case formatted string. + public static string ToSnake(this 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 the specified case format. + /// + /// The string to convert. + /// The target case format. + /// A formatted string in the specified case. + public static string ToCase(this string value, LoggerOutputCase outputCase) + { + return outputCase switch + { + LoggerOutputCase.CamelCase => value.ToCamel(), + LoggerOutputCase.PascalCase => value.ToPascal(), + LoggerOutputCase.SnakeCase => value.ToSnake(), + _ => value.ToSnake() // Default/unchanged + }; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs new file mode 100644 index 00000000..9d31471d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs @@ -0,0 +1,44 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Configuration options for log buffering +/// +public class LogBufferingOptions +{ + /// + /// Gets or sets the maximum size of the buffer in bytes + /// + /// Default is 20KB (20480 bytes) + /// + public int MaxBytes { get; set; } = 20480; + + /// + /// Gets or sets the minimum log level to buffer + /// Defaults to Debug + /// + /// Valid values are: Trace, Debug, Information, Warning + /// + public LogLevel BufferAtLogLevel { get; set; } = LogLevel.Debug; + + /// + /// Gets or sets whether to flush the buffer when logging an error + /// + public bool FlushOnErrorLog { get; set; } = true; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs new file mode 100644 index 00000000..be00722a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs @@ -0,0 +1,439 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public partial class Logger +{ + #region ExtraKeys Logger Methods + + #region Debug + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.LogDebug(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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, "Processing request from {Address}", address) + public static void LogDebug(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogDebug(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.LogDebug(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void LogDebug(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogDebug(extraKeys, message, args); + } + + #endregion + + #region Trace + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.LogTrace(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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, "Processing request from {Address}", address) + public static void LogTrace(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogTrace(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.LogTrace(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void LogTrace(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogTrace(extraKeys, message, args); + } + + #endregion + + #region Information + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.LogInformation(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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 + { + LoggerInstance.LogInformation(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.LogInformation(extraKeys, exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void LogInformation(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogInformation(extraKeys, message, args); + } + + #endregion + + #region Warning + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.LogWarning(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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, "Processing request from {Address}", address) + public static void LogWarning(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogWarning(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.LogWarning(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void LogWarning(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogWarning(extraKeys, message, args); + } + + #endregion + + #region Error + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.LogError(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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, "Processing request from {Address}", address) + public static void LogError(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogError(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.LogError(extraKeys, exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void LogError(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogError(extraKeys, message, args); + } + + #endregion + + #region Critical + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.LogCritical(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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 + { + LoggerInstance.LogCritical(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.LogCritical(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void LogCritical(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogCritical(extraKeys, message, args); + } + + #endregion + + #region Log + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// 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 + { + LoggerInstance.Log(logLevel, extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// 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 + { + LoggerInstance.Log(logLevel, extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// 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 + { + LoggerInstance.Log(logLevel, extraKeys, exception, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// 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, "Processing request from {Address}", address) + public static void Log(LogLevel logLevel, T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.Log(logLevel, extraKeys, message, args); + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs new file mode 100644 index 00000000..d5f5b5ca --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs @@ -0,0 +1,41 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Logging; + +public static partial class Logger +{ + /// + /// 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) + { + Configure(config => { + config.LogFormatter = logFormatter; + }); + } + + /// + /// Set the log formatter to default. + /// + public static void UseDefaultFormatter() + { + Configure(config => { + config.LogFormatter = null; + }); + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs new file mode 100644 index 00000000..1221a282 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs @@ -0,0 +1,168 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public static partial class Logger +{ + #region JSON Logger Methods + + /// + /// Formats and writes a trace log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogTrace(new {User = user, Address = address}) + public static void LogTrace(object message) + { + LoggerInstance.LogTrace(message); + } + + /// + /// Formats and writes an trace log message. + /// + /// The exception to log. + /// logger.LogTrace(exception) + public static void LogTrace(Exception exception) + { + LoggerInstance.LogTrace(exception); + } + + /// + /// Formats and writes a debug log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogDebug(new {User = user, Address = address}) + public static void LogDebug(object message) + { + LoggerInstance.LogDebug(message); + } + + /// + /// Formats and writes an debug log message. + /// + /// The exception to log. + /// logger.LogDebug(exception) + public static void LogDebug(Exception exception) + { + LoggerInstance.LogDebug(exception); + } + + /// + /// Formats and writes an information log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogInformation(new {User = user, Address = address}) + public static void LogInformation(object message) + { + LoggerInstance.LogInformation(message); + } + + /// + /// Formats and writes an information log message. + /// + /// The exception to log. + /// logger.LogInformation(exception) + public static void LogInformation(Exception exception) + { + LoggerInstance.LogInformation(exception); + } + + /// + /// Formats and writes a warning log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogWarning(new {User = user, Address = address}) + public static void LogWarning(object message) + { + LoggerInstance.LogWarning(message); + } + + /// + /// Formats and writes an warning log message. + /// + /// The exception to log. + /// logger.LogWarning(exception) + public static void LogWarning(Exception exception) + { + LoggerInstance.LogWarning(exception); + } + + /// + /// Formats and writes a error log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogCritical(new {User = user, Address = address}) + public static void LogError(object message) + { + LoggerInstance.LogError(message); + } + + /// + /// Formats and writes an error log message. + /// + /// The exception to log. + /// logger.LogError(exception) + public static void LogError(Exception exception) + { + LoggerInstance.LogError(exception); + } + + /// + /// Formats and writes a critical log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogCritical(new {User = user, Address = address}) + public static void LogCritical(object message) + { + LoggerInstance.LogCritical(message); + } + + /// + /// Formats and writes an critical log message. + /// + /// The exception to log. + /// logger.LogCritical(exception) + public static void LogCritical(Exception exception) + { + LoggerInstance.LogCritical(exception); + } + + /// + /// Formats and writes a log message as JSON at the specified log level. + /// + /// Entry will be written on this level. + /// The object to be serialized as JSON. + /// logger.Log(LogLevel.Information, new {User = user, Address = address}) + public static void Log(LogLevel logLevel, object message) + { + LoggerInstance.Log(logLevel, message); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The exception to log. + /// logger.Log(LogLevel.Information, exception) + public static void Log(LogLevel logLevel, Exception exception) + { + LoggerInstance.Log(logLevel, exception); + } + + #endregion +} diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs new file mode 100644 index 00000000..23526cd5 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs @@ -0,0 +1,108 @@ +/* + * 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 AWS.Lambda.Powertools.Logging.Internal.Helpers; + +namespace AWS.Lambda.Powertools.Logging; + +public static partial class Logger +{ + /// + /// Gets the scope. + /// + /// The scope. + private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal); + + /// + /// Appending additional key to the log context. + /// + /// The key. + /// The value. + /// key + /// value + public static void AppendKey(string key, object value) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + +#if NET8_0_OR_GREATER + Scope[key] = PowertoolsLoggerHelpers.ObjectToDictionary(value) ?? + throw new ArgumentNullException(nameof(value)); +#else + Scope[key] = value ?? throw new ArgumentNullException(nameof(value)); +#endif + } + + /// + /// Appending additional key to the log context. + /// + /// The list of keys. + public static void AppendKeys(IEnumerable> keys) + { + foreach (var (key, value) in keys) + AppendKey(key, value); + } + + /// + /// Appending additional key to the log context. + /// + /// The list of keys. + public static void AppendKeys(IEnumerable> keys) + { + foreach (var (key, value) in keys) + AppendKey(key, value); + } + + /// + /// Remove additional keys from the log context. + /// + /// The list of keys. + public static void RemoveKeys(params string[] keys) + { + if (keys == null) return; + foreach (var key in keys) + if (Scope.ContainsKey(key)) + Scope.Remove(key); + } + + /// + /// Returns all additional keys added to the log context. + /// + /// IEnumerable<KeyValuePair<System.String, System.Object>>. + public static IEnumerable> GetAllKeys() + { + return Scope.AsEnumerable(); + } + + /// + /// Removes all additional keys from the log context. + /// + internal static void RemoveAllKeys() + { + Scope.Clear(); + } + + /// + /// Removes a key from the log context. + /// + public static void RemoveKey(string key) + { + if (Scope.ContainsKey(key)) + Scope.Remove(key); + } +} diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs new file mode 100644 index 00000000..f157c0cb --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs @@ -0,0 +1,432 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public static partial class Logger +{ + /// + /// Formats and writes a debug log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// 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(0, exception, "Error while processing request from {Address}", address) + public static void LogDebug(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogDebug(eventId, exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// The event id associated with the log. + /// + /// 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(0, "Processing request from {Address}", address) + public static void LogDebug(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogDebug(eventId, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// The exception to log. + /// + /// 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(exception, "Error while processing request from {Address}", address) + public static void LogDebug(Exception exception, string message, params object[] args) + { + LoggerInstance.LogDebug(exception, message, args); + } + + /// + /// Formats and writes a debug log 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.LogDebug("Processing request from {Address}", address) + public static void LogDebug(string message, params object[] args) + { + LoggerInstance.LogDebug(message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// 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(0, exception, "Error while processing request from {Address}", address) + public static void LogTrace(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogTrace(eventId, exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// The event id associated with the log. + /// + /// 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(0, "Processing request from {Address}", address) + public static void LogTrace(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogTrace(eventId, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// The exception to log. + /// + /// 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(exception, "Error while processing request from {Address}", address) + public static void LogTrace(Exception exception, string message, params object[] args) + { + LoggerInstance.LogTrace(exception, message, args); + } + + /// + /// Formats and writes a trace log 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.LogTrace("Processing request from {Address}", address) + public static void LogTrace(string message, params object[] args) + { + LoggerInstance.LogTrace(message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// 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(0, exception, "Error while processing request from {Address}", address) + public static void LogInformation(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogInformation(eventId, exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// The event id associated with the log. + /// + /// 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(0, "Processing request from {Address}", address) + public static void LogInformation(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogInformation(eventId, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// The exception to log. + /// + /// 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(exception, "Error while processing request from {Address}", address) + public static void LogInformation(Exception exception, string message, params object[] args) + { + LoggerInstance.LogInformation(exception, message, args); + } + + /// + /// Formats and writes an informational log 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.LogInformation("Processing request from {Address}", address) + public static void LogInformation(string message, params object[] args) + { + LoggerInstance.LogInformation(message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// 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(0, exception, "Error while processing request from {Address}", address) + public static void LogWarning(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogWarning(eventId, exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// The event id associated with the log. + /// + /// 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(0, "Processing request from {Address}", address) + public static void LogWarning(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogWarning(eventId, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// The exception to log. + /// + /// 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(exception, "Error while processing request from {Address}", address) + public static void LogWarning(Exception exception, string message, params object[] args) + { + LoggerInstance.LogWarning(exception, message, args); + } + + /// + /// Formats and writes a warning log 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.LogWarning("Processing request from {Address}", address) + public static void LogWarning(string message, params object[] args) + { + LoggerInstance.LogWarning(message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// 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(0, exception, "Error while processing request from {Address}", address) + public static void LogError(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogError(eventId, exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// The event id associated with the log. + /// + /// 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(0, "Processing request from {Address}", address) + public static void LogError(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogError(eventId, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// The exception to log. + /// + /// 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(exception, "Error while processing request from {Address}", address) + public static void LogError(Exception exception, string message, params object[] args) + { + LoggerInstance.LogError(exception, message, args); + } + + /// + /// Formats and writes an error log 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.LogError("Processing request from {Address}", address) + public static void LogError(string message, params object[] args) + { + LoggerInstance.LogError(message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// 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(0, exception, "Error while processing request from {Address}", address) + public static void LogCritical(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogCritical(eventId, exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// The event id associated with the log. + /// + /// 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(0, "Processing request from {Address}", address) + public static void LogCritical(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogCritical(eventId, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// The exception to log. + /// + /// 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(exception, "Error while processing request from {Address}", address) + public static void LogCritical(Exception exception, string message, params object[] args) + { + LoggerInstance.LogCritical(exception, message, args); + } + + /// + /// Formats and writes a critical log 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("Processing request from {Address}", address) + public static void LogCritical(string message, params object[] args) + { + LoggerInstance.LogCritical(message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, string message, params object[] args) + { + LoggerInstance.Log(logLevel, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The event id associated with the log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, EventId eventId, string message, params object[] args) + { + LoggerInstance.Log(logLevel, eventId, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The exception to log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, Exception exception, string message, params object[] args) + { + LoggerInstance.Log(logLevel, exception, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, EventId eventId, Exception exception, string message, + params object[] args) + { + LoggerInstance.Log(logLevel, eventId, exception, message, args); + } + +} \ 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 4271de83..2cf197b8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -14,8 +14,8 @@ */ 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 AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -25,1188 +25,73 @@ namespace AWS.Lambda.Powertools.Logging; /// /// Class Logger. /// -public class Logger +public static partial class Logger { - /// - /// The logger instance - /// private static ILogger _loggerInstance; + private static readonly object Lock = new object(); - /// - /// Gets the logger instance. - /// - /// The logger instance. - private static ILogger LoggerInstance => _loggerInstance ??= Create(); - - /// - /// Gets or sets the logger provider. - /// - /// The logger provider. - internal static ILoggerProvider LoggerProvider { get; set; } - - /// - /// The logger formatter instance - /// - private static ILogFormatter _logFormatter; - - /// - /// Gets the scope. - /// - /// The scope. - private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal); - - /// - /// Creates a new instance. - /// - /// The category name for messages produced by the logger. - /// The instance of that was created. - /// categoryName - 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); - } - - /// - /// Creates a new instance. - /// - /// - /// The instance of that was created. - public static ILogger Create() - { - return Create(typeof(T).FullName); - } - - #region Scope Variables - - /// - /// Appending additional key to the log context. - /// - /// The key. - /// The value. - /// key - /// value - public static void AppendKey(string key, object value) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentNullException(nameof(key)); - -#if NET8_0_OR_GREATER - Scope[key] = PowertoolsLoggerHelpers.ObjectToDictionary(value) ?? - throw new ArgumentNullException(nameof(value)); -#else - Scope[key] = value ?? throw new ArgumentNullException(nameof(value)); -#endif - } - - /// - /// Appending additional key to the log context. - /// - /// The list of keys. - public static void AppendKeys(IEnumerable> keys) + // Change this to a property with getter that recreates if needed + private static ILogger LoggerInstance { - foreach (var (key, value) in keys) - AppendKey(key, value); + get + { + // If we have no instance or configuration has changed, get a new logger + if (_loggerInstance == null) + { + lock (Lock) + { + if (_loggerInstance == null) + { + _loggerInstance = Initialize(); + } + } + } + return _loggerInstance; + } } - /// - /// Appending additional key to the log context. - /// - /// The list of keys. - public static void AppendKeys(IEnumerable> keys) + private static ILogger Initialize() { - foreach (var (key, value) in keys) - AppendKey(key, value); + return LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); } /// - /// Remove additional keys from the log context. + /// Configure with an existing logger factory /// - /// The list of keys. - public static void RemoveKeys(params string[] keys) + /// The factory to use + internal static void Configure(ILoggerFactory loggerFactory) { - if (keys == null) return; - foreach (var key in keys) - if (Scope.ContainsKey(key)) - Scope.Remove(key); + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + LoggerFactoryHolder.SetFactory(loggerFactory); } /// - /// Returns all additional keys added to the log context. + /// Configure using a configuration action /// - /// IEnumerable<KeyValuePair<System.String, System.Object>>. - public static IEnumerable> GetAllKeys() + /// + public static void Configure(Action configure) { - return Scope.AsEnumerable(); + lock (Lock) + { + var config = new PowertoolsLoggerConfiguration(); + configure(config); + _loggerInstance = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + } } - + + /// - /// Removes all additional keys from the log context. + /// Reset the logger for testing /// - internal static void RemoveAllKeys() - { - Scope.Clear(); - } - - internal static void ClearLoggerInstance() + internal static void Reset() { + LoggerFactoryHolder.Reset(); _loggerInstance = null; + RemoveAllKeys(); } - - #endregion - - #region Core Logger Methods - - #region Debug - - /// - /// Formats and writes a debug log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// 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(0, exception, "Error while processing request from {Address}", address) - public static void LogDebug(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogDebug(eventId, exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// The event id associated with the log. - /// - /// 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(0, "Processing request from {Address}", address) - public static void LogDebug(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogDebug(eventId, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// The exception to log. - /// - /// 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(exception, "Error while processing request from {Address}", address) - public static void LogDebug(Exception exception, string message, params object[] args) - { - LoggerInstance.LogDebug(exception, message, args); - } - - /// - /// Formats and writes a debug log 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.LogDebug("Processing request from {Address}", address) - public static void LogDebug(string message, params object[] args) - { - LoggerInstance.LogDebug(message, args); - } - - #endregion - - #region Trace - - /// - /// Formats and writes a trace log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// 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(0, exception, "Error while processing request from {Address}", address) - public static void LogTrace(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogTrace(eventId, exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// The event id associated with the log. - /// - /// 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(0, "Processing request from {Address}", address) - public static void LogTrace(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogTrace(eventId, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// The exception to log. - /// - /// 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(exception, "Error while processing request from {Address}", address) - public static void LogTrace(Exception exception, string message, params object[] args) - { - LoggerInstance.LogTrace(exception, message, args); - } - - /// - /// Formats and writes a trace log 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.LogTrace("Processing request from {Address}", address) - public static void LogTrace(string message, params object[] args) - { - LoggerInstance.LogTrace(message, args); - } - - #endregion - - #region Information - - /// - /// Formats and writes an informational log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// 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(0, exception, "Error while processing request from {Address}", address) - public static void LogInformation(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogInformation(eventId, exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// The event id associated with the log. - /// - /// 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(0, "Processing request from {Address}", address) - public static void LogInformation(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogInformation(eventId, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// The exception to log. - /// - /// 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(exception, "Error while processing request from {Address}", address) - public static void LogInformation(Exception exception, string message, params object[] args) - { - LoggerInstance.LogInformation(exception, message, args); - } - - /// - /// Formats and writes an informational log 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.LogInformation("Processing request from {Address}", address) - public static void LogInformation(string message, params object[] args) - { - LoggerInstance.LogInformation(message, args); - } - - #endregion - - #region Warning - - /// - /// Formats and writes a warning log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// 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(0, exception, "Error while processing request from {Address}", address) - public static void LogWarning(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogWarning(eventId, exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// The event id associated with the log. - /// - /// 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(0, "Processing request from {Address}", address) - public static void LogWarning(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogWarning(eventId, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// The exception to log. - /// - /// 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(exception, "Error while processing request from {Address}", address) - public static void LogWarning(Exception exception, string message, params object[] args) - { - LoggerInstance.LogWarning(exception, message, args); - } - - /// - /// Formats and writes a warning log 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.LogWarning("Processing request from {Address}", address) - public static void LogWarning(string message, params object[] args) - { - LoggerInstance.LogWarning(message, args); - } - - #endregion - - #region Error - - /// - /// Formats and writes an error log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// 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(0, exception, "Error while processing request from {Address}", address) - public static void LogError(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogError(eventId, exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// The event id associated with the log. - /// - /// 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(0, "Processing request from {Address}", address) - public static void LogError(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogError(eventId, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// The exception to log. - /// - /// 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(exception, "Error while processing request from {Address}", address) - public static void LogError(Exception exception, string message, params object[] args) - { - LoggerInstance.LogError(exception, message, args); - } - - /// - /// Formats and writes an error log 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.LogError("Processing request from {Address}", address) - public static void LogError(string message, params object[] args) - { - LoggerInstance.LogError(message, args); - } - - #endregion - - #region Critical - - /// - /// Formats and writes a critical log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// 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(0, exception, "Error while processing request from {Address}", address) - public static void LogCritical(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogCritical(eventId, exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// The event id associated with the log. - /// - /// 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(0, "Processing request from {Address}", address) - public static void LogCritical(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogCritical(eventId, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// The exception to log. - /// - /// 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(exception, "Error while processing request from {Address}", address) - public static void LogCritical(Exception exception, string message, params object[] args) - { - LoggerInstance.LogCritical(exception, message, args); - } - - /// - /// Formats and writes a critical log 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("Processing request from {Address}", address) - public static void LogCritical(string message, params object[] args) - { - LoggerInstance.LogCritical(message, args); - } - - #endregion - - #region Log - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, string message, params object[] args) - { - LoggerInstance.Log(logLevel, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The event id associated with the log. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, EventId eventId, string message, params object[] args) - { - LoggerInstance.Log(logLevel, eventId, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The exception to log. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, Exception exception, string message, params object[] args) - { - LoggerInstance.Log(logLevel, exception, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, EventId eventId, Exception exception, string message, - params object[] args) - { - LoggerInstance.Log(logLevel, eventId, exception, message, args); - } - - /// - /// Writes a log entry. - /// - /// The type of the object to be written. - /// Entry will be written on this level. - /// Id of the event. - /// The entry to be written. Can be also an object. - /// The exception related to this entry. - /// - /// Function to create a message of the - /// and . - /// - public static void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, - Func formatter) - { - LoggerInstance.Log(logLevel, eventId, state, exception, formatter); - } - - #endregion - - #endregion - - #region JSON Logger Methods - - /// - /// Formats and writes a trace log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogTrace(new {User = user, Address = address}) - public static void LogTrace(object message) - { - LoggerInstance.LogTrace(message); - } - - /// - /// Formats and writes an trace log message. - /// - /// The exception to log. - /// logger.LogTrace(exception) - public static void LogTrace(Exception exception) - { - LoggerInstance.LogTrace(exception); - } - - /// - /// Formats and writes a debug log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogDebug(new {User = user, Address = address}) - public static void LogDebug(object message) - { - LoggerInstance.LogDebug(message); - } - - /// - /// Formats and writes an debug log message. - /// - /// The exception to log. - /// logger.LogDebug(exception) - public static void LogDebug(Exception exception) - { - LoggerInstance.LogDebug(exception); - } - - /// - /// Formats and writes an information log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogInformation(new {User = user, Address = address}) - public static void LogInformation(object message) - { - LoggerInstance.LogInformation(message); - } - - /// - /// Formats and writes an information log message. - /// - /// The exception to log. - /// logger.LogInformation(exception) - public static void LogInformation(Exception exception) - { - LoggerInstance.LogInformation(exception); - } - - /// - /// Formats and writes a warning log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogWarning(new {User = user, Address = address}) - public static void LogWarning(object message) - { - LoggerInstance.LogWarning(message); - } - - /// - /// Formats and writes an warning log message. - /// - /// The exception to log. - /// logger.LogWarning(exception) - public static void LogWarning(Exception exception) - { - LoggerInstance.LogWarning(exception); - } - - /// - /// Formats and writes a error log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogCritical(new {User = user, Address = address}) - public static void LogError(object message) - { - LoggerInstance.LogError(message); - } - - /// - /// Formats and writes an error log message. - /// - /// The exception to log. - /// logger.LogError(exception) - public static void LogError(Exception exception) - { - LoggerInstance.LogError(exception); - } - - /// - /// Formats and writes a critical log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogCritical(new {User = user, Address = address}) - public static void LogCritical(object message) - { - LoggerInstance.LogCritical(message); - } - - /// - /// Formats and writes an critical log message. - /// - /// The exception to log. - /// logger.LogCritical(exception) - public static void LogCritical(Exception exception) - { - LoggerInstance.LogCritical(exception); - } - - /// - /// Formats and writes a log message as JSON at the specified log level. - /// - /// Entry will be written on this level. - /// The object to be serialized as JSON. - /// logger.Log(LogLevel.Information, new {User = user, Address = address}) - public static void Log(LogLevel logLevel, object message) - { - LoggerInstance.Log(logLevel, message); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The exception to log. - /// logger.Log(LogLevel.Information, exception) - public static void Log(LogLevel logLevel, Exception exception) - { - LoggerInstance.Log(logLevel, exception); - } - - #endregion - - #region ExtraKeys Logger Methods - - #region Debug - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 - { - LoggerInstance.LogDebug(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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, "Processing request from {Address}", address) - public static void LogDebug(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogDebug(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.LogDebug(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void LogDebug(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogDebug(extraKeys, message, args); - } - - #endregion - - #region Trace - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 - { - LoggerInstance.LogTrace(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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, "Processing request from {Address}", address) - public static void LogTrace(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogTrace(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.LogTrace(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void LogTrace(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogTrace(extraKeys, message, args); - } - - #endregion - - #region Information - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 - { - LoggerInstance.LogInformation(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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 - { - LoggerInstance.LogInformation(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.LogInformation(extraKeys, exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void LogInformation(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogInformation(extraKeys, message, args); - } - - #endregion - - #region Warning - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 - { - LoggerInstance.LogWarning(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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, "Processing request from {Address}", address) - public static void LogWarning(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogWarning(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.LogWarning(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void LogWarning(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogWarning(extraKeys, message, args); - } - - #endregion - - #region Error - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 - { - LoggerInstance.LogError(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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, "Processing request from {Address}", address) - public static void LogError(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogError(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.LogError(extraKeys, exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void LogError(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogError(extraKeys, message, args); - } - - #endregion - - #region Critical - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 + + internal static void ClearInstance() { - LoggerInstance.LogCritical(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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 - { - LoggerInstance.LogCritical(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.LogCritical(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void LogCritical(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogCritical(extraKeys, message, args); - } - - #endregion - - #region Log - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// 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 - { - LoggerInstance.Log(logLevel, extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// 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 - { - LoggerInstance.Log(logLevel, extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// 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 - { - LoggerInstance.Log(logLevel, extraKeys, exception, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// 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, "Processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.Log(logLevel, extraKeys, message, args); - } - - #endregion - - #endregion - - #region Custom Log Formatter - - /// - /// 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)); - } - - /// - /// Set the log formatter to default. - /// - public static void UseDefaultFormatter() - { - _logFormatter = null; + _loggerInstance = null; } - - /// - /// Returns the log formatter. - /// - internal static ILogFormatter GetFormatter() => _logFormatter; - - #endregion } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs deleted file mode 100644 index aab959af..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ /dev/null @@ -1,62 +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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace AWS.Lambda.Powertools.Logging; - -/// -/// Class LoggerConfiguration. -/// Implements the -/// -/// -/// -public class LoggerConfiguration : IOptions -{ - /// - /// Service name is used for logging. - /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. - /// - /// The service. - public string Service { get; set; } - - /// - /// Specify the minimum log level for logging (Information, by default). - /// This can be also set using the environment variable POWERTOOLS_LOG_LEVEL. - /// - /// The minimum level. - 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; } - - /// - /// The default configured options instance - /// - /// The value. - LoggerConfiguration IOptions.Value => this; - - /// - /// The logger output case. - /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. - /// - /// The logger output case. - public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 4a5da930..747cf7db 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -15,6 +15,7 @@ using System; using AspectInjector.Broker; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -116,8 +117,8 @@ namespace AWS.Lambda.Powertools.Logging; /// /// [AttributeUsage(AttributeTargets.Method)] -[Injection(typeof(LoggingAspect))] -public class LoggingAttribute : Attribute +// [Injection(typeof(LoggingAspect))] +public class LoggingAttribute : MethodAspectAttribute { /// /// Service name is used for logging. @@ -146,7 +147,19 @@ public class LoggingAttribute : Attribute /// such as a string or any custom data object. /// /// true if [log event]; otherwise, false. - public bool LogEvent { get; set; } + public bool LogEvent + { + get => _logEvent; + set + { + _logEvent = value; + _logEventSet = true; + } + } + + private bool _logEventSet; + private bool _logEvent; + internal bool IsLogEventSet => _logEventSet; /// /// Pointer path to extract correlation id from input parameter. @@ -171,4 +184,19 @@ public class LoggingAttribute : Attribute /// /// The log level. public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; + + /// + /// Flush buffer on uncaught error + /// When buffering is enabled, this property will flush the buffer on uncaught exceptions + /// + public bool FlushBufferOnUncaughtError { get; set; } + + /// + /// Creates the aspect with the Logger + /// + /// + protected override IMethodAspectHandler CreateHandler() + { + return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs new file mode 100644 index 00000000..e822b3c5 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -0,0 +1,148 @@ +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Builder class for creating configured PowertoolsLogger instances. +/// Provides a fluent interface for configuring logging options. +/// +public class PowertoolsLoggerBuilder +{ + private readonly PowertoolsLoggerConfiguration _configuration = new(); + + /// + /// Sets the service name for the logger. + /// + /// The service name to be included in logs. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithService(string service) + { + _configuration.Service = service; + return this; + } + + /// + /// Sets the sampling rate for logs. + /// + /// The sampling rate between 0 and 1. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithSamplingRate(double rate) + { + _configuration.SamplingRate = rate; + return this; + } + + /// + /// Sets the minimum log level for the logger. + /// + /// The minimum LogLevel to capture. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithMinimumLogLevel(LogLevel level) + { + _configuration.MinimumLogLevel = level; + return this; + } + + /// + /// Sets custom JSON serialization options. + /// + /// JSON serializer options to use for log formatting. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithJsonOptions(JsonSerializerOptions options) + { + _configuration.JsonOptions = options; + return this; + } + + /// + /// Sets the timestamp format for log entries. + /// + /// The timestamp format string. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithTimestampFormat(string format) + { + _configuration.TimestampFormat = format; + return this; + } + + /// + /// Sets the output casing style for log properties. + /// + /// The casing style to use for log output. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithOutputCase(LoggerOutputCase outputCase) + { + _configuration.LoggerOutputCase = outputCase; + return this; + } + + /// + /// Sets a custom log formatter. + /// + /// The formatter to use for log formatting. + /// The builder instance for method chaining. + /// Thrown when formatter is null. + public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) + { + _configuration.LogFormatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + return this; + } + + /// + /// Configures log buffering with custom options. + /// + /// Action to configure the log buffering options. + /// The builder instance for method chaining. + public PowertoolsLoggerBuilder WithLogBuffering(Action configure) + { + _configuration.LogBuffering = new LogBufferingOptions(); + configure?.Invoke(_configuration.LogBuffering); + return this; + } + + /// + /// Specifies the console output wrapper used for writing logs. This property allows + /// redirecting log output for testing or specialized handling scenarios. + /// Defaults to standard console output via ConsoleWrapper. + /// + /// + /// + /// // Using TestLoggerOutput + /// .WithLogOutput(new TestLoggerOutput()); + /// + /// // Custom console output for testing + /// .WithLogOutput(new TestConsoleWrapper()); + /// + /// // Example implementation for testing: + /// public class TestConsoleWrapper : IConsoleWrapper + /// { + /// public List<string> CapturedOutput { get; } = new(); + /// + /// public void WriteLine(string message) + /// { + /// CapturedOutput.Add(message); + /// } + /// } + /// + /// + public PowertoolsLoggerBuilder WithLogOutput(IConsoleWrapper console) + { + _configuration.LogOutput = console ?? throw new ArgumentNullException(nameof(console)); + return this; + } + + + /// + /// Builds and returns a configured logger instance. + /// + /// An ILogger configured with the specified options. + public ILogger Build() + { + var factory = LoggerFactoryHelper.CreateAndConfigureFactory(_configuration); + return factory.CreatePowertoolsLogger(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs new file mode 100644 index 00000000..9e09fb13 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -0,0 +1,349 @@ +/* + * 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.Security.Cryptography; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Serializers; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Configuration for the Powertools Logger. +/// +/// +/// +/// Basic logging configuration: +/// +/// builder.Logging.AddPowertoolsLogger(options => +/// { +/// options.Service = "OrderService"; +/// options.MinimumLogLevel = LogLevel.Information; +/// options.LoggerOutputCase = LoggerOutputCase.CamelCase; +/// }); +/// +/// +/// Using with log buffering: +/// +/// builder.Logging.AddPowertoolsLogger(options => +/// { +/// options.LogBuffering = new LogBufferingOptions +/// { +/// Enabled = true, +/// BufferAtLogLevel = LogLevel.Debug, +/// FlushOnErrorLog = true +/// }; +/// }); +/// +/// +/// Custom JSON formatting: +/// +/// builder.Logging.AddPowertoolsLogger(options => +/// { +/// options.JsonOptions = new JsonSerializerOptions +/// { +/// PropertyNamingPolicy = JsonNamingPolicy.CamelCase, +/// WriteIndented = true +/// }; +/// }); +/// +/// +public class PowertoolsLoggerConfiguration : IOptions +{ + /// + /// The configuration section name used when retrieving configuration from appsettings.json + /// or other configuration providers. + /// + public const string ConfigurationSectionName = "AWS.Lambda.Powertools.Logging.Logger"; + + /// + /// Specifies the service name that will be added to all logs to improve discoverability. + /// This value can also be set using the environment variable POWERTOOLS_SERVICE_NAME. + /// + /// + /// + /// options.Service = "OrderProcessingService"; + /// + /// + public string Service { get; set; } = null; + + /// + /// Defines the format for timestamps in log entries. Supports standard .NET date format strings. + /// When not specified, the default ISO 8601 format is used. + /// + /// + /// + /// // Use specific format + /// options.TimestampFormat = "yyyy-MM-dd HH:mm:ss"; + /// + /// // Use ISO 8601 with milliseconds + /// options.TimestampFormat = "o"; + /// + /// + public string TimestampFormat { get; set; } + + /// + /// Defines the minimum log level that will be processed by the logger. + /// Messages below this level will be ignored. Defaults to LogLevel.None, which means + /// the minimum level is determined by other configuration mechanisms. + /// This can also be set using the environment variable POWERTOOLS_LOG_LEVEL. + /// + /// + /// + /// // Only log warnings and above + /// options.MinimumLogLevel = LogLevel.Warning; + /// + /// // Log everything including trace messages + /// options.MinimumLogLevel = LogLevel.Trace; + /// + /// + public LogLevel MinimumLogLevel { get; set; } = LogLevel.None; + + /// + /// Sets a percentage (0.0 to 1.0) of logs that will be dynamically elevated to DEBUG level, + /// allowing for production debugging without increasing log verbosity for all requests. + /// This can also be set using the environment variable POWERTOOLS_LOGGER_SAMPLE_RATE. + /// + /// + /// + /// // Sample 10% of logs to DEBUG level + /// options.SamplingRate = 0.1; + /// + /// // Sample 100% (all logs) to DEBUG level + /// options.SamplingRate = 1.0; + /// + /// + public double SamplingRate { get; set; } + + /// + /// Controls the case format used for log field names in the JSON output. + /// Available options are Default, CamelCase, PascalCase, or SnakeCase. + /// This can also be set using the environment variable POWERTOOLS_LOGGER_CASE. + /// + /// + /// + /// // Use camelCase for JSON field names + /// options.LoggerOutputCase = LoggerOutputCase.CamelCase; + /// + /// // Use snake_case for JSON field names + /// options.LoggerOutputCase = LoggerOutputCase.SnakeCase; + /// + /// + public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; + + /// + /// Internal key used for log level in output + /// + internal string LogLevelKey { get; set; } = "level"; + + /// + /// Provides a custom log formatter implementation to control how log entries are formatted. + /// Set this to override the default JSON formatting with your own custom format. + /// + /// + /// + /// // Use a custom formatter implementation + /// options.LogFormatter = new MyCustomLogFormatter(); + /// + /// // Example with a simple custom formatter class this will just return a string: + /// public class MyCustomLogFormatter : ILogFormatter + /// { + /// public object FormatLog(LogEntry entry) + /// { + /// // Custom formatting logic here + /// return $"{logEntry.Timestamp}: [{logEntry.Level}] {logEntry.Message}"; + /// } + /// } + /// // Example with a complete formatter class this will just return a json object: + /// 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 + /// }, + /// }; + /// } + /// + /// + public ILogFormatter LogFormatter { get; set; } + + private JsonSerializerOptions _jsonOptions; + + /// + /// Configures the JSON serialization options used when converting log entries to JSON. + /// This allows customization of property naming, indentation, and other serialization behaviors. + /// Setting this property automatically updates the internal serializer. + /// + /// + /// + /// // DictionaryNamingPolicy allows you to control the naming policy for dictionary keys + /// options.JsonOptions = new JsonSerializerOptions + /// { + /// DictionaryNamingPolicy = JsonNamingPolicy.CamelCase + /// }; + /// // Pretty-print JSON logs with indentation + /// options.JsonOptions = new JsonSerializerOptions + /// { + /// WriteIndented = true, + /// PropertyNamingPolicy = JsonNamingPolicy.CamelCase + /// }; + /// + /// // Configure to ignore null values in output + /// options.JsonOptions = new JsonSerializerOptions + /// { + /// DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + /// }; + /// + /// + public JsonSerializerOptions JsonOptions + { + get => _jsonOptions; + set + { + _jsonOptions = value; + if (_jsonOptions != null && _serializer != null) + { + _serializer.SetOptions(_jsonOptions); + } + } + } + + /// + /// Enables or disables log buffering. Logs below the specified level will be buffered + /// until the buffer is flushed or an error occurs. + /// Buffer logs at the WARNING, INFO, and DEBUG levels and reduce CloudWatch costs by decreasing the number of emitted log messages + /// + /// + /// + /// // Enable buffering for debug logs + /// options.LogBuffering = new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Debug, + /// FlushOnErrorLog = true + /// }; + /// + /// // Buffer all logs below Error level + /// options.LogBuffering = new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Warning, + /// FlushOnErrorLog = true + /// }; + /// + /// + public LogBufferingOptions LogBuffering { get; set; } + + /// + /// Serializer instance for this configuration + /// + private PowertoolsLoggingSerializer _serializer; + + /// + /// Gets the serializer instance for this configuration + /// + internal PowertoolsLoggingSerializer Serializer => _serializer ??= InitializeSerializer(); + + /// + /// Specifies the console output wrapper used for writing logs. This property allows + /// redirecting log output for testing or specialized handling scenarios. + /// Defaults to standard console output via ConsoleWrapper. + /// + /// + /// + /// // Using TestLoggerOutput + /// options.LogOutput = new TestLoggerOutput(); + /// + /// // Custom console output for testing + /// options.LogOutput = new TestConsoleWrapper(); + /// + /// // Example implementation for testing: + /// public class TestConsoleWrapper : IConsoleWrapper + /// { + /// public List<string> CapturedOutput { get; } = new(); + /// + /// public void WriteLine(string message) + /// { + /// CapturedOutput.Add(message); + /// } + /// } + /// + /// + public IConsoleWrapper LogOutput { get; set; } = new ConsoleWrapper(); + + /// + /// Initialize serializer with the current configuration + /// + private PowertoolsLoggingSerializer InitializeSerializer() + { + var serializer = new PowertoolsLoggingSerializer(); + if (_jsonOptions != null) + { + serializer.SetOptions(_jsonOptions); + } + + serializer.ConfigureNamingPolicy(LoggerOutputCase); + return serializer; + } + + // IOptions implementation + PowertoolsLoggerConfiguration IOptions.Value => this; + + internal string XRayTraceId { get; set; } + internal bool LogEvent { get; set; } + + internal double Random { get; set; } = GetSafeRandom(); + + /// + /// Gets random number + /// + /// System.Double. + internal virtual double GetRandom() + { + return Random; + } + + internal static double GetSafeRandom() + { + var randomGenerator = RandomNumberGenerator.Create(); + byte[] data = new byte[16]; + randomGenerator.GetBytes(data); + return BitConverter.ToDouble(data); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs similarity index 92% rename from libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs index 200cf46e..e3ca6780 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -22,7 +23,7 @@ namespace AWS.Lambda.Powertools.Logging; /// /// Class LoggerExtensions. /// -public static class LoggerExtensions +public static class PowertoolsLoggerExtensions { #region JSON Logger Extentions @@ -652,4 +653,93 @@ public static void Log(this ILogger logger, LogLevel logLevel, T extraKeys, s #endregion #endregion + + + /// + /// Appending additional key to the log context. + /// + /// + /// The list of keys. + public static void AppendKeys(this ILogger logger,IEnumerable> keys) + { + Logger.AppendKeys(keys); + } + + /// + /// Appending additional key to the log context. + /// + /// + /// The list of keys. + public static void AppendKeys(this ILogger logger,IEnumerable> keys) + { + Logger.AppendKeys(keys); + } + + /// + /// Appending additional key to the log context. + /// + /// + /// The key. + /// The value. + /// key + /// value + public static void AppendKey(this ILogger logger, string key, object value) + { + Logger.AppendKey(key, value); + } + + /// + /// Returns all additional keys added to the log context. + /// + /// IEnumerable<KeyValuePair<System.String, System.Object>>. + public static IEnumerable> GetAllKeys(this ILogger logger) + { + return Logger.GetAllKeys(); + } + + /// + /// Removes all additional keys from the log context. + /// + internal static void RemoveAllKeys(this ILogger logger) + { + Logger.RemoveAllKeys(); + } + + /// + /// Remove additional keys from the log context. + /// + /// + /// The list of keys. + public static void RemoveKeys(this ILogger logger, params string[] keys) + { + Logger.RemoveKeys(keys); + } + + /// + /// Removes a key from the log context. + /// + public static void RemoveKey(this ILogger logger, string key) + { + Logger.RemoveKey(key); + } + + // Replace the buffer methods with direct calls to the manager + + /// + /// Flush any buffered logs + /// + public static void FlushBuffer(this ILogger logger) + { + // Direct call to the buffer manager to avoid any recursion + LogBufferManager.FlushCurrentBuffer(); + } + + /// + /// Clear any buffered logs without writing them + /// + public static void ClearBuffer(this ILogger logger) + { + // Direct call to the buffer manager to avoid any recursion + LogBufferManager.ClearCurrentBuffer(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs new file mode 100644 index 00000000..062a7c15 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -0,0 +1,55 @@ +using System; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +internal sealed class PowertoolsLoggerFactory : IDisposable +{ + private readonly ILoggerFactory _factory; + + internal PowertoolsLoggerFactory(ILoggerFactory loggerFactory) + { + _factory = loggerFactory; + } + + internal PowertoolsLoggerFactory() : this(LoggerFactory.Create(builder => { builder.AddPowertoolsLogger(); })) + { + } + + internal static PowertoolsLoggerFactory Create(Action configureOptions) + { + var options = new PowertoolsLoggerConfiguration(); + configureOptions(options); + var factory = Create(options); + return new PowertoolsLoggerFactory(factory); + } + + internal static ILoggerFactory Create(PowertoolsLoggerConfiguration options) + { + return LoggerFactoryHelper.CreateAndConfigureFactory(options); + } + + // Add builder pattern support + internal static PowertoolsLoggerBuilder CreateBuilder() + { + return new PowertoolsLoggerBuilder(); + } + + internal ILogger CreateLogger() => CreateLogger(typeof(T).FullName ?? typeof(T).Name); + + internal ILogger CreateLogger(string category) + { + return _factory.CreateLogger(category); + } + + internal ILogger CreatePowertoolsLogger() + { + return _factory.CreatePowertoolsLogger(); + } + + public void Dispose() + { + _factory?.Dispose(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs new file mode 100644 index 00000000..edec07fd --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Extensions for ILoggerFactory +/// +public static class PowertoolsLoggerFactoryExtensions +{ + /// + /// Creates a new Powertools Logger instance using the Powertools full name. + /// + /// The factory. + /// The that was created. + public static ILogger CreatePowertoolsLogger(this ILoggerFactory factory) + { + return new PowertoolsLoggerFactory(factory).CreateLogger(PowertoolsLoggerConfiguration.ConfigurationSectionName); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs new file mode 100644 index 00000000..651e4f25 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Concurrent; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Extension methods to configure and add the Powertools logger to an . +/// +/// +/// This class provides methods to integrate the AWS Lambda Powertools logging capabilities +/// with the standard .NET logging framework. +/// +/// +/// Basic usage: +/// +/// builder.Logging.AddPowertoolsLogger(); +/// +/// +public static class PowertoolsLoggingBuilderExtensions +{ + private static readonly ConcurrentBag AllProviders = new(); + private static readonly object Lock = new(); + private static PowertoolsLoggerConfiguration _currentConfig = new(); + + internal static void UpdateConfiguration(PowertoolsLoggerConfiguration config) + { + lock (Lock) + { + // Update the shared configuration + _currentConfig = config; + + // Notify all providers about the change + foreach (var provider in AllProviders) + { + provider.UpdateConfiguration(config); + } + } + } + + internal static PowertoolsLoggerConfiguration GetCurrentConfiguration() + { + lock (Lock) + { + // Return a copy to prevent external modification + return _currentConfig; + } + } + + /// + /// Adds the Powertools logger to the logging builder with default configuration. + /// + /// The logging builder to configure. + /// The logging builder for further configuration. + /// + /// This method registers the Powertools logger with default settings. The logger will output + /// structured JSON logs that integrate well with AWS CloudWatch and other log analysis tools. + /// + /// + /// Add the Powertools logger to your Lambda function: + /// + /// var builder = new HostBuilder() + /// .ConfigureLogging(logging => + /// { + /// logging.AddPowertoolsLogger(); + /// }); + /// + /// + /// Using with minimal API: + /// + /// var builder = WebApplication.CreateBuilder(args); + /// builder.Logging.AddPowertoolsLogger(); + /// + /// + public static ILoggingBuilder AddPowertoolsLogger( + this ILoggingBuilder builder) + { + builder.AddConfiguration(); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(sp => + new PowertoolsConfigurations(sp.GetRequiredService())); + + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton(provider => + { + var powertoolsConfigurations = provider.GetRequiredService(); + + var loggerProvider = new PowertoolsLoggerProvider( + _currentConfig, + powertoolsConfigurations); + + lock (Lock) + { + AllProviders.Add(loggerProvider); + } + + return loggerProvider; + })); + + return builder; + } + + /// + /// Adds the Powertools logger to the logging builder with default configuration. + /// + /// The logging builder to configure. + /// + /// The logging builder for further configuration. + /// + /// This method registers the Powertools logger with default settings. The logger will output + /// structured JSON logs that integrate well with AWS CloudWatch and other log analysis tools. + /// + /// + /// Add the Powertools logger to your Lambda function: + /// + /// var builder = new HostBuilder() + /// .ConfigureLogging(logging => + /// { + /// logging.AddPowertoolsLogger(); + /// }); + /// + /// + /// Using with minimal API: + /// + /// var builder = WebApplication.CreateBuilder(args); + /// builder.Logging.AddPowertoolsLogger(); + /// + /// With custom configuration: + /// + /// builder.Logging.AddPowertoolsLogger(options => + /// { + /// options.MinimumLogLevel = LogLevel.Information; + /// options.LoggerOutputCase = LoggerOutputCase.PascalCase; + /// options.IncludeLogLevel = true; + /// }); + /// + /// + /// With log buffering: + /// + /// builder.Logging.AddPowertoolsLogger(options => + /// { + /// options.LogBuffering = new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Debug + /// }; + /// }); + /// + /// + public static ILoggingBuilder AddPowertoolsLogger( + this ILoggingBuilder builder, + Action configure) + { + // Add configuration + builder.AddPowertoolsLogger(); + + // Create initial configuration + var options = new PowertoolsLoggerConfiguration(); + configure(options); + + // IMPORTANT: Set the minimum level directly on the builder + if (options.MinimumLogLevel != LogLevel.None) + { + builder.SetMinimumLevel(options.MinimumLogLevel); + } + + builder.Services.Configure(configure); + + UpdateConfiguration(options); + + // If buffering is enabled, register buffer providers + if (options.LogBuffering != null) + { + // Add a filter for the buffer provider + builder.AddFilter( + null, + LogLevel.Trace); + + // Register the buffer provider as an enumerable service + // Using singleton to ensure it's properly tracked + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton(provider => + { + var powertoolsConfigurations = provider.GetRequiredService(); + + var bufferingProvider = new BufferingLoggerProvider( + _currentConfig, powertoolsConfigurations + ); + + lock (Lock) + { + AllProviders.Add(bufferingProvider); + } + + return bufferingProvider; + })); + } + + + return builder; + } + + /// + /// Resets all providers and clears the configuration. + /// This is useful for testing purposes to ensure a clean state. + /// + internal static void ResetAllProviders() + { + lock (Lock) + { + // Clear the provider collection + AllProviders.Clear(); + + // Reset the current configuration to default + _currentConfig = new PowertoolsLoggerConfiguration(); + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs new file mode 100644 index 00000000..c665f960 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs @@ -0,0 +1,59 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace AWS.Lambda.Powertools.Logging.Serializers +{ + /// + /// Combines multiple IJsonTypeInfoResolver instances into one + /// + internal class CompositeJsonTypeInfoResolver : IJsonTypeInfoResolver + { + private readonly IJsonTypeInfoResolver[] _resolvers; + + /// + /// Creates a new composite resolver from multiple resolvers + /// + /// Array of resolvers to use + public CompositeJsonTypeInfoResolver(IJsonTypeInfoResolver[] resolvers) + { + _resolvers = resolvers ?? throw new ArgumentNullException(nameof(resolvers)); + } + + + /// + /// Gets JSON type info by trying each resolver in order (.NET Standard 2.0 version) + /// + public JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + foreach (var resolver in _resolvers) + { + var typeInfo = resolver?.GetTypeInfo(type, options); + if (typeInfo != null) + { + return typeInfo; + } + } + + return null; + } + } +} +#endif \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 97aabc06..9e38dcbf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -25,36 +25,64 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Utils; 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 +internal class PowertoolsLoggingSerializer { - private static LoggerOutputCase _currentOutputCase; - private static JsonSerializerOptions _jsonOptions; + private JsonSerializerOptions _currentOptions; + private LoggerOutputCase _currentOutputCase; + private JsonSerializerOptions _jsonOptions; + private readonly object _lock = new(); - private static readonly ConcurrentBag AdditionalContexts = - new ConcurrentBag(); +#if NET8_0_OR_GREATER + private readonly ConcurrentBag _additionalContexts = new(); + private static JsonSerializerContext _staticAdditionalContexts; + private IJsonTypeInfoResolver _customTypeInfoResolver; +#endif /// /// Gets the JsonSerializerOptions instance. /// - internal static JsonSerializerOptions GetSerializerOptions() + internal JsonSerializerOptions GetSerializerOptions() { - return _jsonOptions ?? BuildJsonSerializerOptions(); + // Double-checked locking pattern for thread safety while ensuring we only build once + if (_jsonOptions == null) + { + lock (_lock) + { + if (_jsonOptions == null) + { + BuildJsonSerializerOptions(_currentOptions); + } + } + } + + return _jsonOptions; } /// /// Configures the naming policy for the serializer. /// /// The case to use for serialization. - internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) + internal void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) { - _currentOutputCase = loggerOutputCase; + if (_currentOutputCase != loggerOutputCase) + { + lock (_lock) + { + _currentOutputCase = loggerOutputCase; + + // Only rebuild options if they already exist + if (_jsonOptions != null) + { + SetOutputCase(); + } + } + } } /// @@ -64,7 +92,7 @@ internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) /// 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) + internal string Serialize(object value, Type inputType) { #if NET6_0 var options = GetSerializerOptions(); @@ -72,11 +100,12 @@ internal static string Serialize(object value, Type inputType) #else if (RuntimeFeatureWrapper.IsDynamicCodeSupported) { - var options = GetSerializerOptions(); + var jsonSerializerOptions = GetSerializerOptions(); #pragma warning disable - return JsonSerializer.Serialize(value, options); + return JsonSerializer.Serialize(value, jsonSerializerOptions); } + // Try to serialize using the configured TypeInfoResolver var typeInfo = GetTypeInfo(inputType); if (typeInfo == null) { @@ -85,23 +114,71 @@ internal static string Serialize(object value, Type inputType) } return JsonSerializer.Serialize(value, typeInfo); + #endif } #if NET8_0_OR_GREATER + /// /// Adds a JsonSerializerContext to the serializer options. /// /// The JsonSerializerContext to add. /// Thrown when the context is null. - internal static void AddSerializerContext(JsonSerializerContext context) + internal void AddSerializerContext(JsonSerializerContext context) + { + ArgumentNullException.ThrowIfNull(context); + + // Don't add duplicates + if (!_additionalContexts.Contains(context)) + { + _additionalContexts.Add(context); + + // If we have existing JSON options, update their type resolver + if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) + { + // Reset the type resolver chain to rebuild it + _jsonOptions.TypeInfoResolver = GetCompositeResolver(); + } + } + } + + internal static void AddStaticSerializerContext(JsonSerializerContext context) { ArgumentNullException.ThrowIfNull(context); - if (!AdditionalContexts.Contains(context)) + _staticAdditionalContexts = context; + } + + /// + /// Get a composite resolver that includes all configured resolvers + /// + private IJsonTypeInfoResolver GetCompositeResolver() + { + var resolvers = new List(); + + // Add custom resolver if provided + if (_customTypeInfoResolver != null) + { + resolvers.Add(_customTypeInfoResolver); + } + + // add any static resolvers + if (_staticAdditionalContexts != null) + { + resolvers.Add(_staticAdditionalContexts); + } + + // Add default context + resolvers.Add(PowertoolsLoggingSerializationContext.Default); + + // Add additional contexts + foreach (var context in _additionalContexts) { - AdditionalContexts.Add(context); + resolvers.Add(context); } + + return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); } /// @@ -109,21 +186,77 @@ internal static void AddSerializerContext(JsonSerializerContext context) /// /// The type to get information for. /// The JsonTypeInfo for the specified type, or null if not found. - internal static JsonTypeInfo GetTypeInfo(Type type) + private 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() + private void BuildJsonSerializerOptions(JsonSerializerOptions options = null) { - _jsonOptions = new JsonSerializerOptions(); + lock (_lock) + { + // Create a completely new options instance regardless + _jsonOptions = new JsonSerializerOptions(); + + // Copy any properties from the original options if provided + if (options != null) + { + // Copy standard properties + _jsonOptions.DefaultIgnoreCondition = options.DefaultIgnoreCondition; + _jsonOptions.PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive; + _jsonOptions.PropertyNamingPolicy = options.PropertyNamingPolicy; + _jsonOptions.DictionaryKeyPolicy = options.DictionaryKeyPolicy; + _jsonOptions.WriteIndented = options.WriteIndented; + _jsonOptions.ReferenceHandler = options.ReferenceHandler; + _jsonOptions.MaxDepth = options.MaxDepth; + _jsonOptions.IgnoreReadOnlyFields = options.IgnoreReadOnlyFields; + _jsonOptions.IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties; + _jsonOptions.IncludeFields = options.IncludeFields; + _jsonOptions.NumberHandling = options.NumberHandling; + _jsonOptions.ReadCommentHandling = options.ReadCommentHandling; + _jsonOptions.UnknownTypeHandling = options.UnknownTypeHandling; + _jsonOptions.AllowTrailingCommas = options.AllowTrailingCommas; + +#if NET8_0_OR_GREATER + // Handle type resolver extraction without setting it yet + if (options.TypeInfoResolver != null) + { + _customTypeInfoResolver = options.TypeInfoResolver; + + // If it's a JsonSerializerContext, also add it to our contexts + if (_customTypeInfoResolver is JsonSerializerContext jsonContext) + { + AddSerializerContext(jsonContext); + } + } +#endif + } + + // Set output case and other properties + SetOutputCase(); + AddConverters(); + _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + _jsonOptions.PropertyNameCaseInsensitive = true; + +#if NET8_0_OR_GREATER + // Set TypeInfoResolver last, as this makes options read-only + if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) + { + _jsonOptions.TypeInfoResolver = GetCompositeResolver(); + } +#endif + } + } + internal void SetOutputCase() + { switch (_currentOutputCase) { case LoggerOutputCase.CamelCase: @@ -136,15 +269,19 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() break; default: // Snake case #if NET8_0_OR_GREATER - _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; + // If is default (Not Set) and JsonOptions provided with DictionaryKeyPolicy or PropertyNamingPolicy, use it + _jsonOptions.DictionaryKeyPolicy ??= JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.PropertyNamingPolicy ??= JsonNamingPolicy.SnakeCaseLower; #else _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; _jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; #endif break; } + } + private void AddConverters() + { _jsonOptions.Converters.Add(new ByteArrayConverter()); _jsonOptions.Converters.Add(new ExceptionConverter()); _jsonOptions.Converters.Add(new MemoryStreamConverter()); @@ -157,42 +294,10 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() #elif NET6_0 _jsonOptions.Converters.Add(new LogLevelJsonConverter()); #endif - - _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; - _jsonOptions.PropertyNameCaseInsensitive = true; - -#if NET8_0_OR_GREATER - - // Only add TypeInfoResolver if AOT mode - if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) - { - _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() + internal void SetOptions(JsonSerializerOptions options) { - _jsonOptions = null; + _currentOptions = options; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs index dadec8da..46c83c49 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -74,7 +74,7 @@ public PowertoolsSourceGeneratorSerializer( } var jsonSerializerContext = constructor.Invoke(new object[] { options }) as TSgContext; - PowertoolsLoggingSerializer.AddSerializerContext(jsonSerializerContext); + PowertoolsLoggingSerializer.AddStaticSerializerContext(jsonSerializerContext); } } diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj index 77ec07a3..8f15d771 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj @@ -9,7 +9,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj index 526f6694..36e6c177 100644 --- a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj @@ -20,7 +20,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj index 550a8e83..499d9d39 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj @@ -17,7 +17,7 @@ - + diff --git a/libraries/src/Directory.Build.props b/libraries/src/Directory.Build.props index 7d1e38a6..127ac19a 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -35,15 +35,20 @@ - - - - - - Common\%(RecursiveDir)%(Filename)%(Extension) - - - + + + + + + Common\Common\ + + + + \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index c5af6311..db4a6a7f 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -4,16 +4,16 @@ - + - + - + @@ -22,5 +22,6 @@ + \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs b/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs index 299956ec..c218e419 100644 --- a/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs @@ -28,25 +28,15 @@ public class BatchProcessingInternalTests public void BatchProcessing_Set_Execution_Environment_Context_SQS() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.BatchProcessing"; - var assemblyVersion = "1.0.0"; - - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).ReturnsForAnyArgs(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var sqsBatchProcessor = new SqsBatchProcessor(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/BatchProcessing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(sqsBatchProcessor); } @@ -55,25 +45,15 @@ public void BatchProcessing_Set_Execution_Environment_Context_SQS() public void BatchProcessing_Set_Execution_Environment_Context_Kinesis() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.BatchProcessing"; - var assemblyVersion = "1.0.0"; - - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).ReturnsForAnyArgs(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var KinesisEventBatchProcessor = new KinesisEventBatchProcessor(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/BatchProcessing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(KinesisEventBatchProcessor); } @@ -82,25 +62,15 @@ public void BatchProcessing_Set_Execution_Environment_Context_Kinesis() public void BatchProcessing_Set_Execution_Environment_Context_DynamoDB() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.BatchProcessing"; - var assemblyVersion = "1.0.0"; - - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).ReturnsForAnyArgs(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var dynamoDbStreamBatchProcessor = new DynamoDbStreamBatchProcessor(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/BatchProcessing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(dynamoDbStreamBatchProcessor); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs index 4da57dc0..fdc79d95 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs @@ -4,7 +4,7 @@ namespace AWS.Lambda.Powertools.Common.Tests; -public class ConsoleWrapperTests +public class ConsoleWrapperTests : IDisposable { [Fact] public void WriteLine_Should_Write_To_Console() @@ -12,7 +12,7 @@ public void WriteLine_Should_Write_To_Console() // Arrange var consoleWrapper = new ConsoleWrapper(); var writer = new StringWriter(); - Console.SetOut(writer); + ConsoleWrapper.SetOut(writer); // Act consoleWrapper.WriteLine("test message"); @@ -27,6 +27,7 @@ public void Error_Should_Write_To_Error_Console() // Arrange var consoleWrapper = new ConsoleWrapper(); var writer = new StringWriter(); + ConsoleWrapper.SetOut(writer); Console.SetError(writer); // Act @@ -38,17 +39,126 @@ public void Error_Should_Write_To_Error_Console() } [Fact] - public void ReadLine_Should_Read_From_Console() + public void SetOut_Should_Override_Console_Output() { // Arrange var consoleWrapper = new ConsoleWrapper(); - var reader = new StringReader("input text"); - Console.SetIn(reader); + var writer = new StringWriter(); + ConsoleWrapper.SetOut(writer); // Act - var result = consoleWrapper.ReadLine(); + consoleWrapper.WriteLine("test message"); // Assert - Assert.Equal("input text", result); + Assert.Equal($"test message{Environment.NewLine}", writer.ToString()); } + + [Fact] + public void OverrideLambdaLogger_Should_Override_Console_Out() + { +// Arrange + var originalOut = Console.Out; + try + { + var consoleWrapper = new ConsoleWrapper(); + + // Act - create a custom StringWriter and set it after constructor + // but before WriteLine (which triggers OverrideLambdaLogger) + var writer = new StringWriter(); + ConsoleWrapper.SetOut(writer); + + consoleWrapper.WriteLine("test message"); + + // Assert + Assert.Equal($"test message{Environment.NewLine}", writer.ToString()); + } + finally + { + // Restore original console out + ConsoleWrapper.ResetForTest(); + } + } + + [Fact] + public void WriteLine_WritesMessageToConsole() + { + // Arrange + var consoleWrapper = new ConsoleWrapper(); + var originalOutput = Console.Out; + using var stringWriter = new StringWriter(); + ConsoleWrapper.SetOut(stringWriter); + + try + { + // Act + consoleWrapper.WriteLine("Test message"); + + // Assert + var output = stringWriter.ToString(); + Assert.Contains("Test message", output); + } + finally + { + // Restore original output + ConsoleWrapper.ResetForTest(); + } + } + + [Fact] + public void SetOut_OverridesConsoleOutput() + { + // Arrange + var originalOutput = Console.Out; + using var stringWriter = new StringWriter(); + + try + { + // Act + typeof(ConsoleWrapper) + .GetMethod("SetOut", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static) + ?.Invoke(null, new object[] { stringWriter }); + + Console.WriteLine("Test override"); + + // Assert + var output = stringWriter.ToString(); + Assert.Contains("Test override", output); + } + finally + { + // Restore original output + ConsoleWrapper.ResetForTest(); + } + } + + [Fact] + public void StaticWriteLine_FormatsLogMessageCorrectly() + { + // Arrange + var originalOutput = Console.Out; + using var stringWriter = new StringWriter(); + ConsoleWrapper.SetOut(stringWriter); + + try + { + // Act + typeof(ConsoleWrapper) + .GetMethod("WriteLine", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, null, new[] { typeof(string), typeof(string) }, null) + ?.Invoke(null, new object[] { "INFO", "Test log message" }); + + // Assert + var output = stringWriter.ToString(); + Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\tINFO\tTest log message", output); + } + finally + { + // Restore original output + ConsoleWrapper.ResetForTest(); + } + } + + public void Dispose() + { + ConsoleWrapper.ResetForTest(); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs index e154acd9..934a162c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs @@ -29,17 +29,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableIsNull_Return // Arrange var key = Guid.NewGuid().ToString(); var defaultValue = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(string.Empty); + environment.GetEnvironmentVariable(key).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, defaultValue); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(key); + environment.Received(1).GetEnvironmentVariable(key); Assert.Equal(result, defaultValue); } @@ -49,17 +49,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableIsNull_Return { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(string.Empty); + environment.GetEnvironmentVariable(key).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, false); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(key); + environment.Received(1).GetEnvironmentVariable(key); Assert.False(result); } @@ -69,17 +69,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableIsNull_Return { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(string.Empty); + environment.GetEnvironmentVariable(key).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, true); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.True(result); } @@ -91,17 +91,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableHasValue_Retu var key = Guid.NewGuid().ToString(); var defaultValue = Guid.NewGuid().ToString(); var value = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(value); + environment.GetEnvironmentVariable(key).Returns(value); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, defaultValue); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.Equal(result, value); } @@ -111,17 +111,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableHasValue_Retu { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns("true"); + environment.GetEnvironmentVariable(key).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, false); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.True(result); } @@ -131,17 +131,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableHasValue_Retu { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns("false"); + environment.GetEnvironmentVariable(key).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, true); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.False(result); } @@ -155,17 +155,17 @@ public void Service_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange var defaultService = "service_undefined"; - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.Service; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.Equal(result, defaultService); } @@ -175,17 +175,17 @@ public void Service_WhenEnvironmentHasValue_ReturnsValue() { // Arrange var service = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.Service; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.Equal(result, service); } @@ -199,17 +199,17 @@ public void IsServiceDefined_WhenEnvironmentHasValue_ReturnsTrue() { // Arrange var service = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsServiceDefined; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.True(result); } @@ -218,17 +218,17 @@ public void IsServiceDefined_WhenEnvironmentHasValue_ReturnsTrue() public void IsServiceDefined_WhenEnvironmentDoesNotHaveValue_ReturnsFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsServiceDefined; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.False(result); } @@ -241,17 +241,17 @@ public void IsServiceDefined_WhenEnvironmentDoesNotHaveValue_ReturnsFalse() public void TracerCaptureResponse_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureResponse; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureResponseEnv)); Assert.True(result); @@ -261,17 +261,17 @@ public void TracerCaptureResponse_WhenEnvironmentIsNull_ReturnsDefaultValue() public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureResponse; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureResponseEnv)); Assert.False(result); @@ -281,17 +281,17 @@ public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueFalse() public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureResponse; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureResponseEnv)); Assert.True(result); @@ -305,17 +305,17 @@ public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueTrue() public void TracerCaptureError_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureError; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureErrorEnv)); Assert.True(result); @@ -325,17 +325,17 @@ public void TracerCaptureError_WhenEnvironmentIsNull_ReturnsDefaultValue() public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureError; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureErrorEnv)); Assert.False(result); @@ -345,17 +345,17 @@ public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueFalse() public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureError; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureErrorEnv)); Assert.True(result); @@ -369,17 +369,17 @@ public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueTrue() public void IsSamLocal_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.SamLocalEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.SamLocalEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsSamLocal; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.SamLocalEnv)); Assert.False(result); @@ -389,17 +389,17 @@ public void IsSamLocal_WhenEnvironmentIsNull_ReturnsDefaultValue() public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsSamLocal; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.SamLocalEnv)); Assert.False(result); @@ -409,17 +409,17 @@ public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueFalse() public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsSamLocal; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.SamLocalEnv)); Assert.True(result); @@ -433,17 +433,17 @@ public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueTrue() public void TracingDisabled_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracingDisabled; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracingDisabledEnv)); Assert.False(result); @@ -453,17 +453,17 @@ public void TracingDisabled_WhenEnvironmentIsNull_ReturnsDefaultValue() public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracingDisabled; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracingDisabledEnv)); Assert.False(result); @@ -473,17 +473,17 @@ public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueFalse() public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracingDisabled; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracingDisabledEnv)); Assert.True(result); @@ -497,17 +497,17 @@ public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueTrue() public void IsLambdaEnvironment_WhenEnvironmentIsNull_ReturnsFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.LambdaTaskRoot).Returns((string)null); + environment.GetEnvironmentVariable(Constants.LambdaTaskRoot).Returns((string)null); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsLambdaEnvironment; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.LambdaTaskRoot)); Assert.False(result); @@ -517,17 +517,17 @@ public void IsLambdaEnvironment_WhenEnvironmentIsNull_ReturnsFalse() public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(Guid.NewGuid().ToString()); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(Guid.NewGuid().ToString()); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsLambdaEnvironment; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.LambdaTaskRoot)); Assert.True(result); @@ -537,20 +537,20 @@ public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue() public void Set_Lambda_Execution_Context() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - // systemWrapper.Setup(c => + // environment.Setup(c => // c.SetExecutionEnvironment(GetType()) // ); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act configurations.SetExecutionEnvironment(typeof(PowertoolsConfigurations)); // Assert // method with correct type was called - systemWrapper.Received(1) + environment.Received(1) .SetExecutionEnvironment(Arg.Is(i => i == typeof(PowertoolsConfigurations))); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs index df41e253..936432ac 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs @@ -118,4 +118,9 @@ public string GetAssemblyVersion(T type) { return "1.0.0"; } + + public void SetExecutionEnvironment(T type) + { + throw new NotImplementedException(); + } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index f83cfe34..be80a4c4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -264,25 +264,16 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction(Type t public void Idempotency_Set_Execution_Environment_Context() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.Idempotency"; - var assemblyVersion = "1.0.0"; - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var xRayRecorder = new Idempotency(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/Idempotency/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Idempotency/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(xRayRecorder); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index ba08453f..82962fcf 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -14,9 +14,9 @@ */ using System; +using System.IO; 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; @@ -28,23 +28,31 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; [Collection("Sequential")] public class LoggerAspectTests : IDisposable { - private ISystemWrapper _mockSystemWrapper; - private readonly IPowertoolsConfigurations _mockPowertoolsConfigurations; - + static LoggerAspectTests() + { + ResetAllState(); + } + public LoggerAspectTests() { - _mockSystemWrapper = Substitute.For(); - _mockPowertoolsConfigurations = Substitute.For(); + // Start each test with clean state + ResetAllState(); } - + [Fact] public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() { // Arrange -#if NET8_0_OR_GREATER - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); var instance = new object(); var name = "TestMethod"; @@ -66,17 +74,29 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() } }; - _mockSystemWrapper.GetRandom().Returns(0.7); + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); // 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\"") + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"Level\":\"Information\"") && + s.Contains("\"Service\":\"TestService\"") && + s.Contains("\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}") && + s.Contains("\"CorrelationId\":\"20\"") && + s.Contains("\"SamplingRate\":0.5") )); } @@ -84,12 +104,86 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() { // Arrange -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif + Environment.SetEnvironmentVariable(Constants.LoggerLogEventNameEnv, "true"); + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogEvent = true, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + + 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, + CorrelationIdPath = "/Age", + ClearState = true + } + }; + + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + // Act + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); + + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + // Assert + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(0, updatedConfig.SamplingRate); + Assert.True(updatedConfig.LogEvent); + + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"Level\":\"Information\"") && + s.Contains("\"Service\":\"TestService\"") && + s.Contains("\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}") && + s.Contains("\"CorrelationId\":\"20\"") + )); + } + + [Fact] + public void OnEntry_Should_NOT_Log_Event_When_EnvironmentVariable_Set_But_Attribute_False() + { + // Arrange + Environment.SetEnvironmentVariable(Constants.LoggerLogEventNameEnv, "true"); + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogEvent = true, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var instance = new object(); var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; @@ -108,38 +202,50 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() 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); + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + // Act + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); + + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + // 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); + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(0, updatedConfig.SamplingRate); + Assert.True(updatedConfig.LogEvent); - _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\"") - )); + consoleOut.DidNotReceive().WriteLine(Arg.Any()); } - + [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 consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + SamplingRate = 0.5, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var instance = new object(); var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; @@ -159,31 +265,54 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() } }; - // Env returns true - _mockPowertoolsConfigurations.LoggerSampleRate.Returns(0.5); - // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + // Act + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); + // 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\"") + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(0.5, updatedConfig.SamplingRate); + + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"Level\":\"Information\"") && + s.Contains("\"Service\":\"TestService\"") && + s.Contains("\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}") && + s.Contains("\"CorrelationId\":\"20\"") && + s.Contains("\"SamplingRate\":0.5") )); } - + [Fact] public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() { // Arrange + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogOutput = consoleOut, + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var eventObject = new { testData = "test-data" }; var triggers = new Attribute[] { @@ -192,29 +321,43 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() LogEvent = true } }; - + // Act + + var aspectArgs = new AspectEventArgs + { + Args = new object[] { eventObject }, + Triggers = triggers + }; - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); - loggingAspect.OnEntry(null, null, new object[] { eventObject }, null, null, null, triggers); - + // Act + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); + // Assert - _mockSystemWrapper.Received().LogLine(Arg.Is(s => - s.Contains( - "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Information\"") && + s.Contains("\"service\":\"TestService\"") && + s.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"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 consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Error, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var instance = new object(); var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; @@ -227,38 +370,49 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( { 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); + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + // Act + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); + + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + // 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()); + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + + consoleOut.DidNotReceive().WriteLine(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 - + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Debug"); + + var consoleOut = Substitute.For(); + var config = new PowertoolsLoggerConfiguration + { + LogOutput = consoleOut + }; + var instance = new object(); var name = "TestMethod"; var args = new object[] @@ -278,25 +432,38 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() CorrelationIdPath = "/Headers/MyRequestIdHeader" } }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); - // 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); + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + // Act + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(aspectArgs); + // 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 => + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(LogLevel.Debug, updatedConfig.MinimumLogLevel); + + string consoleOutput = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found.", consoleOutput); + + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"CorrelationId\":\"test\"") && s.Contains( "\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":{\"MyRequestIdHeader\":\"test\"}") @@ -305,7 +472,28 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() public void Dispose() { + ResetAllState(); + } + + private static void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + LoggerFactoryHolder.Reset(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index dd3cd558..677c3c2c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -22,10 +22,12 @@ using Amazon.Lambda.CloudWatchEvents.S3Events; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; +using AWS.Lambda.Powertools.Common.Tests; 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; @@ -35,77 +37,65 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes public class LoggingAttributeTests : IDisposable { private TestHandlers _testHandlers; - + public LoggingAttributeTests() { _testHandlers = new TestHandlers(); } - + [Fact] - public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() + public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext_No_Debug() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); // 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()); + + Assert.Empty(allKeys); + + var st = stringWriter.ToString(); + Assert.Empty(st); } - + [Fact] public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebug() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + Logger.Configure(options => + { + options.LogOutput = 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.") - ); + + Assert.Empty(allKeys); + + var st = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } - + [Fact] public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = GetConsoleOutput(); + // Act _testHandlers.LogEventNoArgs(); - + consoleOut.DidNotReceive().WriteLine( Arg.Any() ); @@ -115,20 +105,18 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() public void OnEntry_WhenEventArgExist_LogEvent() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + var consoleOut = GetConsoleOutput(); var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" }; - + var testObj = new TestObject { Headers = new Header @@ -139,7 +127,7 @@ public void OnEntry_WhenEventArgExist_LogEvent() // Act _testHandlers.LogEvent(testObj, context); - + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("FunctionName\":\"PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1")) ); @@ -149,14 +137,8 @@ public void OnEntry_WhenEventArgExist_LogEvent() public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif + var consoleOut = GetConsoleOutput(); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -164,41 +146,42 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() // Act _testHandlers.LogEventFalse(context); - + consoleOut.DidNotReceive().WriteLine( Arg.Any() ); } - + [Fact] public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act _testHandlers.LogEventDebug(); - - consoleOut.Received(1).WriteLine( - Arg.Is(i => i == "Skipping Event Log because event parameter not found.") - ); + + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Skipping Event Log because event parameter not found.", st); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } - + [Fact] public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys() { - // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - // Act _testHandlers.ClearState(); - - Assert.NotNull(Logger.LoggerProvider); + Assert.False(Logger.GetAllKeys().Any()); } - + [Theory] [InlineData(CorrelationIdPaths.ApiGatewayRest)] [InlineData(CorrelationIdPaths.ApplicationLoadBalancer)] @@ -208,13 +191,7 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI { // Arrange var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + // Act switch (correlationIdPath) { @@ -252,15 +229,15 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI }); 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)] @@ -269,13 +246,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu { // Arrange var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + // Act switch (outputCase) { @@ -307,11 +278,11 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu }); 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); } @@ -324,13 +295,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L { // Arrange var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + // Act switch (outputCase) { @@ -364,11 +329,11 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L }); 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); } @@ -377,42 +342,58 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L public void When_Setting_SamplingRate_Should_Add_Key() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = GetConsoleOutput(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act _testHandlers.HandlerSamplingRate(); // Assert - - consoleOut.Received().WriteLine( - Arg.Is(i => i.Contains("\"message\":\"test\",\"samplingRate\":0.5")) - ); + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Information\"") && + s.Contains("\"service\":\"service_undefined\"") && + s.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"message\":\"test\"") && + s.Contains("\"samplingRate\":0.5") + )); } [Fact] public void When_Setting_Service_Should_Update_Key() { // Arrange - var consoleOut = new StringWriter(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = new TestLoggerOutput(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act _testHandlers.HandlerService(); // Assert var st = consoleOut.ToString(); - Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + + Assert.Contains("\"level\":\"Information\"", st); + Assert.Contains("\"service\":\"test\"", st); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", st); + Assert.Contains("\"message\":\"test\"", st); } [Fact] public void When_Setting_LogLevel_Should_Update_LogLevel() { // Arrange - var consoleOut = new StringWriter(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = new TestLoggerOutput();; + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act _testHandlers.TestLogLevelCritical(); @@ -426,8 +407,12 @@ public void When_Setting_LogLevel_Should_Update_LogLevel() public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + var consoleOut = GetConsoleOutput(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -444,101 +429,175 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_True() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act _testHandlers.TestLogEventWithoutContext(); - + // Assert - consoleOut.Received(1).WriteLine(Arg.Is(s => s == "Skipping Event Log because event parameter not found.")); + var st = stringWriter.ToString(); + Assert.Contains("Skipping Event Log because event parameter not found.", st); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); + } [Fact] public void Should_Log_When_Not_Using_Decorator() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + var consoleOut = GetConsoleOutput(); + Logger.Configure(options => + { + options.LogOutput = 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\"}")) - ); + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Information\"") && + s.Contains("\"service\":\"service_undefined\"") && + s.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"message\":\"test\"") + )); } - public void Dispose() + [Fact] + public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() { - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); - LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); - } - } + // Arrange + var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.MinimumLogLevel = LogLevel.Warning; + }); - [Collection("A Sequential")] - public class ServiceTests : IDisposable - { - private readonly TestServiceHandler _testHandler; + // Act + _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute + + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); + } - public ServiceTests() + [Fact] + public void LoggingAspect_ShouldCorrectlyResetLogLevelAfterExecution() { - _testHandler = new TestServiceHandler(); + // Arrange + var consoleOut = GetConsoleOutput(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.MinimumLogLevel = LogLevel.Warning; + }); + + // Act - First call with Debug level attribute + _testHandlers.TestMethodDebug(); + consoleOut.ClearReceivedCalls(); + + // Act - Then log directly at Debug level (should still work) + Logger.LogDebug("This should be logged"); + + // Assert + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Debug\"") && + s.Contains("\"message\":\"This should be logged\""))); } [Fact] - public void When_Setting_Service_Should_Override_Env() + public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Error"); + var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act - _testHandler.LogWithEnv(); - _testHandler.Handler(); - + _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute + // Assert - - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Environment Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Environment Service\"")) - ); - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) - ); + var st = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } [Fact] - public void When_Setting_Service_Should_Override_Env_And_Empty() + public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = GetConsoleOutput(); + + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.MinimumLogLevel = LogLevel.Error; + }); + // Act - _testHandler.LogWithAndWithoutEnv(); - _testHandler.Handler(); - + Logger.LogInformation("This should NOT be logged"); + _testHandlers.TestMethodDebug(); // Should change level to Debug + Logger.LogInformation("This should be logged"); + // Assert - consoleOut.Received(2).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: service_undefined\"")) - ); - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) - ); + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"message\":\"This should be logged\""))); + consoleOut.DidNotReceive().WriteLine(Arg.Is(s => + s.Contains("\"message\":\"This should NOT be logged\""))); } - + public void Dispose() { - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + ResetAllState(); + } + + private IConsoleWrapper GetConsoleOutput() + { + // Create a new mock each time + var output = Substitute.For(); + return output; + } + + private void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + LoggerFactoryHolder.Reset(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + LambdaLifecycleTracker.Reset(); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs new file mode 100644 index 00000000..657730e0 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs @@ -0,0 +1,57 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; + +[Collection("A Sequential")] +public class ServiceTests : IDisposable +{ + private readonly TestServiceHandler _testHandler; + + public ServiceTests() + { + _testHandler = new TestServiceHandler(); + } + + [Fact] + public void When_Setting_Service_Should_Override_Env() + { + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); + + var consoleOut = Substitute.For(); + Logger.Configure(options => + options.LogOutput = consoleOut); + + // Act + _testHandler.LogWithEnv(); + _testHandler.Handler(); + + // Assert + + consoleOut.Received(1).WriteLine(Arg.Is(i => + i.Contains("\"level\":\"Information\"") && + i.Contains("\"service\":\"Environment Service\"") && + i.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + i.Contains("\"message\":\"Service: Environment Service\"") + )); + consoleOut.Received(1).WriteLine(Arg.Is(i => + i.Contains("\"level\":\"Information\"") && + i.Contains("\"service\":\"Attribute Service\"") && + i.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + i.Contains("\"message\":\"Service: Attribute Service\"") + )); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + LoggingAspect.ResetForTest(); + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs new file mode 100644 index 00000000..b6172371 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs @@ -0,0 +1,543 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering +{ + [Collection("Sequential")] + public class LambdaContextBufferingTests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestLoggerOutput _consoleOut; + + public LambdaContextBufferingTests(ITestOutputHelper output) + { + _output = output; + _consoleOut = new TestLoggerOutput(); + LogBufferManager.ResetForTesting(); + } + + [Fact] + public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(true); + var handler = new ErrorOnlyHandler(logger); + var context = CreateTestContext("test-request-3"); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + // Act + handler.TestMethod("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); + Assert.Contains("Error triggering flush", output); + } + + [Fact] + public void Decorator_Clears_Buffer_On_Exit() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(false); + var handler = new NoFlushHandler(logger); + var context = CreateTestContext("test-request-3"); + + // Act + handler.TestMethod("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", output); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-request-3"); + Logger.FlushBuffer(); + + var debugNotFlushed = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", debugNotFlushed); + + // second event + handler.TestMethod("Event", context); + + // Assert + var output2 = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", output2); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-request-4"); + Logger.FlushBuffer(); + + var debugNotFlushed2 = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", debugNotFlushed2); + } + + [Fact] + public async Task AsyncOperations_MaintainBufferContext() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); + var handler = new AsyncLambdaHandler(logger); + var context = CreateTestContext("async-test"); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + // Act + await handler.TestMethodAsync("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Async info message", output); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + } + + [Fact] + public async Task Should_Log_All_Levels_Bellow() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Information); + var handler = new AsyncLambdaHandler(logger); + var context = CreateTestContext("async-test"); + + // Act + await handler.TestMethodAsync("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Async info message", output); + Assert.Contains("Async debug message", output); + Assert.Contains("Async trace message", output); + Assert.Contains("Async warning message", output); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + } + + private TestLambdaContext CreateTestContext(string requestId) + { + return new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = requestId, + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }; + } + + private ILogger CreateLogger(LogLevel minimumLevel, LogLevel bufferAtLevel) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = minimumLevel; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = bufferAtLevel + }; + }); + }).CreatePowertoolsLogger(); + } + + private ILogger CreateLoggerWithFlushOnError(bool flushOnError) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = LogLevel.Information; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = flushOnError + }; + }); + }).CreatePowertoolsLogger(); + } + + public void Dispose() + { + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + Logger.Reset(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); + } + } + + + [Collection("Sequential")] + [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] + public class StaticLoggerBufferingTests : IDisposable + { + private readonly TestLoggerOutput _consoleOut; + private readonly ITestOutputHelper _output; + + public StaticLoggerBufferingTests(ITestOutputHelper output) + { + _output = output; + _consoleOut = new TestLoggerOutput(); + + // Configure static Logger with our test output + Logger.Configure(options => + options.LogOutput = _consoleOut); + } + + [Fact] + public void StaticLogger_BasicBufferingBehavior() + { + // Arrange - explicitly configure Logger for this test + // First reset any existing configuration + Logger.Reset(); + + // Configure the logger with the test output + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false // Disable auto-flush to test manual flush + }; + }); + + // Set invocation ID manually + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-1"); + + // Act - log messages + Logger.AppendKey("custom-key", "custom-value"); + Logger.LogInformation("Information message"); + Logger.LogDebug("Debug message"); // Should be buffered + + // Check the internal state before flush + var outputBeforeFlush = _consoleOut.ToString(); + _output.WriteLine($"Before flush: {outputBeforeFlush}"); + Assert.DoesNotContain("Debug message", outputBeforeFlush); + + // Flush the buffer + Logger.FlushBuffer(); + + // Assert after flush + var outputAfterFlush = _consoleOut.ToString(); + _output.WriteLine($"After flush: {outputAfterFlush}"); + Assert.Contains("Debug message", outputAfterFlush); + } + + [Fact] + public void StaticLogger_WithLoggingDecoratedHandler() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.LogBuffering = new LogBufferingOptions + { + + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }; + }); + + var handler = new StaticLambdaHandler(); + var context = new TestLambdaContext + { + AwsRequestId = "test-static-request-2", + FunctionName = "test-function" + }; + + // Act + handler.TestMethod("test-event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); + Assert.Contains("Debug message", output); + Assert.Contains("Error message", output); + Assert.Contains("custom-key", output); + Assert.Contains("custom-value", output); + } + + [Fact] + public void StaticLogger_ClearBufferRemovesLogs() + { + // Arrange + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + + BufferAtLogLevel = LogLevel.Debug + }; + }); + + // Set invocation ID + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-3"); + + // Act - log message and clear buffer + Logger.LogDebug("Debug message before clear"); + Logger.ClearBuffer(); + Logger.LogDebug("Debug message after clear"); + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message before clear", output); + Assert.Contains("Debug message after clear", output); + } + + [Fact] + public void StaticLogger_FlushOnErrorLogEnabled() + { + // Arrange + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }; + }); + + // Set invocation ID + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-4"); + + // Act - log debug then error + Logger.LogDebug("Debug message"); + Logger.LogError("Error message"); + + // Assert - error should trigger flush + var output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); + Assert.Contains("Error message", output); + } + + [Fact] + public void StaticLogger_MultipleInvocationsIsolated_And_Clear() + { + // Arrange + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + + BufferAtLogLevel = LogLevel.Debug + }; + }); + + // Act - first invocation + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5A"); + Logger.LogDebug("Debug from invocation A"); + + // Switch to second invocation + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5B"); + Logger.LogDebug("Debug from invocation B"); + + // Switch back to first invocation and flush + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5C"); + Logger.LogDebug("Debug from invocation C"); + + Logger.FlushBuffer(); + + // Assert - after second flush + var outputAfterSecondFlush = _consoleOut.ToString(); + Assert.DoesNotContain("Debug from invocation A", outputAfterSecondFlush); + Assert.DoesNotContain("Debug from invocation B", outputAfterSecondFlush); + Assert.Contains("Debug from invocation C", outputAfterSecondFlush); + } + + [Fact] + public void StaticLogger_FlushOnErrorDisabled() + { + // Arrange + Logger.Reset(); + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false + }; + }); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-6"); + + // Act - log debug then error + Logger.LogDebug("Debug message with auto-flush disabled"); + Logger.LogError("Error message that should not trigger flush"); + + // Assert - debug message should remain buffered + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message with auto-flush disabled", output); + Assert.Contains("Error message that should not trigger flush", output); + + // Now manually flush and verify debug message appears + Logger.FlushBuffer(); + output = _consoleOut.ToString(); + Assert.Contains("Debug message with auto-flush disabled", output); + } + + [Fact] + public void StaticLogger_AsyncOperationsMaintainContext() + { + // Arrange + // Logger.Reset(); + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false + }; + }); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-8"); + + // Act - simulate async operations + Task.Run(() => { Logger.LogDebug("Debug from task 1"); }).Wait(); + + Task.Run(() => { Logger.LogDebug("Debug from task 2"); }).Wait(); + + Logger.LogInformation("Main thread info message"); + + // Flush buffers + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + Assert.Contains("Main thread info message", output); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + LoggerFactoryHolder.Reset(); + _consoleOut.Clear(); + Logger.Reset(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); + } + } + + public class StaticLambdaHandler + { + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + Logger.AppendKey("custom-key", "custom-value"); + Logger.LogInformation("Information message"); + Logger.LogDebug("Debug message"); + Logger.LogError("Error message"); + Logger.FlushBuffer(); + } + } + + // Lambda handlers for testing + public class LambdaHandler + { + private readonly ILogger _logger; + + public LambdaHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("Debug message"); + _logger.LogError("Error message"); + _logger.FlushBuffer(); + } + } + + public class ErrorOnlyHandler + { + private readonly ILogger _logger; + + public ErrorOnlyHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.LogDebug("Debug message"); + _logger.LogError("Error triggering flush"); + } + } + + public class NoFlushHandler + { + private readonly ILogger _logger; + + public NoFlushHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.LogDebug("Debug message"); + _logger.LogError("Error triggering flush"); + // No flush here - Decorator clears buffer on exit + } + } + + public class AsyncLambdaHandler + { + private readonly ILogger _logger; + + public AsyncLambdaHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public async Task TestMethodAsync(string message, ILambdaContext lambdaContext) + { + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + _logger.LogInformation("Async info message"); + _logger.LogDebug("Async debug message"); + _logger.LogTrace("Async trace message"); + _logger.LogWarning("Async warning message"); + + var task1 = Task.Run(() => { _logger.LogDebug("Debug from task 1"); }); + + var task2 = Task.Run(() => { _logger.LogDebug("Debug from task 2"); }); + + await Task.WhenAll(task1, task2); + _logger.FlushBuffer(); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs new file mode 100644 index 00000000..c0640c92 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs @@ -0,0 +1,270 @@ +using System; +using System.IO; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering; + +public class LogBufferCircularCacheTests : IDisposable +{ + private readonly TestLoggerOutput _consoleOut; + + public LogBufferCircularCacheTests() + { + _consoleOut = new TestLoggerOutput(); + LogBufferManager.ResetForTesting(); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 1200 // Small buffer size to trigger overflow - Needs to be adjusted based on the log message size + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); + + // Act - add many debug logs to fill buffer + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Old debug message {i} that should be removed"); + } + + // Add more logs that should push out the older ones + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"New debug message {i} that should remain"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + + // First entries should be discarded + Assert.DoesNotContain("Old debug message 0", output); + Assert.DoesNotContain("Old debug message 1", output); + + // Later entries should be present + Assert.Contains("New debug message 3", output); + Assert.Contains("New debug message 4", output); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries_Warn() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 1024 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); + + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + // Act - add many debug logs to fill buffer + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Old debug message {i} that should be removed"); + } + + // Add more logs that should push out the older ones + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"New debug message {i} that should remain"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", st); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries_Warn_With_Warning_Level() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Warning, + MaxBytes = 1024 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); + + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + // Act - add many debug logs to fill buffer + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Old debug message {i} that should be removed"); + } + + // Add more logs that should push out the older ones + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"New debug message {i} that should remain"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", st); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WithLargeLogEntry_DiscardsManySmallEntries() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 2048 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "large-entry-test"); + + // Act - add many small entries first + for (int i = 0; i < 10; i++) + { + logger.LogDebug($"Small message {i}"); + } + + // Add one very large entry that should displace many small ones + var largeMessage = new string('X', 80); // Large enough to push out multiple small entries + logger.LogDebug($"Large message: {largeMessage}"); + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + + // Several early small messages should be discarded + for (int i = 0; i < 5; i++) + { + Assert.DoesNotContain($"Small message {i}", output); + } + + // Large message should be present + Assert.Contains("Large message: XXXX", output); + + // Some later small messages should remain + Assert.Contains("Small message 9", output); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WithExtremelyLargeEntry_Logs_Directly_And_Warning() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 5096 // Even with a larger buffer + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "extreme-entry-test"); + + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + // Act - add some small entries first + for (int i = 0; i < 4; i++) + { + logger.LogDebug($"Initial message {i}"); + } + + // Add entry larger than the entire buffer - should displace everything + var hugeMessage = new string('X', 3000); + logger.LogDebug($"Huge message: {hugeMessage}"); + + var st = stringWriter.ToString(); + Assert.Contains("Cannot add item to the buffer", st); + + // Add more entries after + for (int i = 0; i < 4; i++) + { + logger.LogDebug($"Final message {i}"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + + // Initial messages should be discarded + for (int i = 0; i < 4; i++) + { + Assert.Contains($"Initial message {i}", output); + } + + // Some of the final messages should be present + Assert.Contains("Final message 3", output); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + Logger.Reset(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs new file mode 100644 index 00000000..cbe95411 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs @@ -0,0 +1,342 @@ +using System; +using System.Threading.Tasks; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering +{ + [Collection("Sequential")] + public class LogBufferingHandlerTests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestLoggerOutput _consoleOut; + + public LogBufferingHandlerTests(ITestOutputHelper output) + { + _output = output; + _consoleOut = new TestLoggerOutput(); + LogBufferManager.ResetForTesting(); + } + + [Fact] + public void BasicBufferingBehavior_BuffersDebugLogsOnly() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); + var handler = new HandlerWithoutFlush(logger); // Use a handler that doesn't flush + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + // Act - log messages without flushing + handler.TestMethod(); + + // Assert - before flush + var outputBeforeFlush = _consoleOut.ToString(); + Assert.Contains("Information message", outputBeforeFlush); + Assert.Contains("Error message", outputBeforeFlush); + Assert.Contains("custom-key", outputBeforeFlush); + Assert.Contains("custom-value", outputBeforeFlush); + Assert.DoesNotContain("Debug message", outputBeforeFlush); // Debug should be buffered + + // Now flush the buffer + Logger.FlushBuffer(); + + // Assert - after flush + var outputAfterFlush = _consoleOut.ToString(); + Assert.Contains("Debug message", outputAfterFlush); // Debug should now be present + } + + [Fact] + public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(true); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + // Act - with custom handler that doesn't manually flush + var handler = new CustomHandlerWithoutFlush(logger); + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); // Should be flushed by error log + Assert.Contains("Error triggering flush", output); + } + + [Fact] + public void FlushOnErrorDisabled_DoesNotAutomaticallyFlushBuffer() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(false); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + // Act + var handler = new CustomHandlerWithoutFlush(logger); + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", output); // Should remain buffered + Assert.Contains("Error triggering flush", output); + } + + [Fact] + public void ClearingBuffer_RemovesBufferedLogs() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + // Act + var handler = new ClearBufferHandler(logger); + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message before clear", output); + Assert.Contains("Debug message after clear", output); + } + + [Fact] + public void MultipleInvocations_IsolateLogBuffers() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); + var handler = new Handlers(logger); + + // Act + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + handler.TestMethod(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); + // Create a custom handler that logs different messages + var customHandler = new MultipleInvocationHandler(logger); + customHandler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); // From first invocation + Assert.Contains("Second invocation info", output); // From second invocation + } + + [Fact] + public void MultipleProviders_AllProvidersReceiveLogs() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions { BufferAtLogLevel = LogLevel.Debug }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Create two separate providers + var provider1 = new BufferingLoggerProvider(config, powertoolsConfig); + var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); + + var logger1 = provider1.CreateLogger("Provider1"); + var logger2 = provider2.CreateLogger("Provider2"); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "multi-provider-test"); + + // Act + logger1.LogDebug("Debug from provider 1"); + logger2.LogDebug("Debug from provider 2"); + + // Flush logs from all providers + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug from provider 1", output); + Assert.Contains("Debug from provider 2", output); + } + + [Fact] + public async Task AsyncOperations_MaintainBufferContext() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); + var handler = new AsyncHandler(logger); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "async-test"); + + // Act + await handler.TestMethodAsync(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Async info message", output); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + } + + private ILogger CreateLogger(LogLevel minimumLevel, LogLevel bufferAtLevel) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = minimumLevel; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = bufferAtLevel, + FlushOnErrorLog = false + }; + }); + }).CreatePowertoolsLogger(); + } + + private ILogger CreateLoggerWithFlushOnError(bool flushOnError) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = LogLevel.Information; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = flushOnError + }; + }); + }).CreatePowertoolsLogger(); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); + } + } + + // Additional test handlers with specific behavior + public class CustomHandlerWithoutFlush + { + private readonly ILogger _logger; + + public CustomHandlerWithoutFlush(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.LogDebug("Debug message"); + _logger.LogError("Error triggering flush"); + // No manual flush + } + } + + public class ClearBufferHandler + { + private readonly ILogger _logger; + + public ClearBufferHandler(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.LogDebug("Debug message before clear"); + Logger.ClearBuffer(); // Clear the buffer + _logger.LogDebug("Debug message after clear"); + Logger.FlushBuffer(); // Flush only second message + } + } + + public class MultipleInvocationHandler + { + private readonly ILogger _logger; + + public MultipleInvocationHandler(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.LogInformation("Second invocation info"); + _logger.LogDebug("Second invocation debug"); + _logger.FlushBuffer(); + } + } + + public class Handlers + { + private readonly ILogger _logger; + + public Handlers(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("Debug message"); + + _logger.LogError("Error message"); + + _logger.FlushBuffer(); + } + } + + public class HandlerWithoutFlush + { + private readonly ILogger _logger; + + public HandlerWithoutFlush(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("Debug message"); + _logger.LogError("Error message"); + // No flush here + } + } + + public class AsyncHandler + { + private readonly ILogger _logger; + + public AsyncHandler(ILogger logger) + { + _logger = logger; + } + + public async Task TestMethodAsync() + { + _logger.LogInformation("Async info message"); + _logger.LogDebug("Async debug message"); + + var task1 = Task.Run(() => { + _logger.LogDebug("Debug from task 1"); + }); + + var task2 = Task.Run(() => { + _logger.LogDebug("Debug from task 2"); + }); + + await Task.WhenAll(task1, task2); + _logger.FlushBuffer(); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs new file mode 100644 index 00000000..00d1e759 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs @@ -0,0 +1,499 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering +{ + [Collection("Sequential")] + public class LogBufferingTests : IDisposable + { + private readonly TestLoggerOutput _consoleOut; + + public LogBufferingTests() + { + _consoleOut = new TestLoggerOutput(); + } + + [Trait("Category", "BufferManager")] + [Fact] + public void SetInvocationId_IsolatesLogsBetweenInvocations_And_Clear() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions(), + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + logger.LogDebug("Debug message from invocation 1"); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); + logger.LogDebug("Debug message from invocation 2"); + + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message from invocation 1", output); + Assert.Contains("Debug message from invocation 2", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_OnlyBuffersConfiguredLogLevels() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Trace + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogTrace("Trace message"); // should buffer + logger.LogDebug("Debug message"); // Should be buffered + logger.LogInformation("Info message"); // Above minimum, should be logged directly + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Trace message", output); + Assert.DoesNotContain("Debug message", output); // Not flushed yet + Assert.Contains("Info message", output); + + // Flush the buffer + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.Contains("Trace message", output); // Now should be visible + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_Buffer_Takes_Precedence_Same_Level() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Information + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogTrace("Trace message"); // Below buffer threshold, should be ignored + logger.LogDebug("Debug message"); // Should be buffered + logger.LogInformation("Info message"); // Above minimum, should be logged directly + + // Assert + var output = _consoleOut.ToString(); + Assert.Empty(output); + + // Flush the buffer + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.Contains("Info message", output); // Now should be visible + Assert.Contains("Debug message", output); // Now should be visible + Assert.Contains("Trace message", output); // Now should be visible + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_Buffer_Takes_Precedence_Higher_Level() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Warning + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogWarning("Warning message"); // Should be buffered + logger.LogInformation("Info message"); // Should be buffered + logger.LogDebug("Debug message"); + + // Assert + var output = _consoleOut.ToString(); + Assert.Empty(output); + + // Flush the buffer + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.Contains("Info message", output); // Now should be visible + Assert.Contains("Warning message", output); + Assert.Contains("Debug message", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_Buffer_Log_Level_Error_Does_Not_Buffer() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Error + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogError("Error message"); // Should be buffered + logger.LogInformation("Info message"); // Should be buffered + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Error message", output); + Assert.Contains("Info message", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void FlushOnErrorLog_FlushesBufferWhenEnabled() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogDebug("Debug message 1"); // Should be buffered + logger.LogDebug("Debug message 2"); // Should be buffered + logger.LogError("Error message"); // Should trigger flush of buffer + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message 1", output); + Assert.Contains("Debug message 2", output); + Assert.Contains("Error message", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void ClearBuffer_RemovesAllBufferedLogs() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + + // Act + logger.LogDebug("Debug message 1"); // Should be buffered + logger.LogDebug("Debug message 2"); // Should be buffered + + Logger.ClearBuffer(); // Should clear all buffered logs + Logger.FlushBuffer(); // No logs should be output + + logger.LogDebug("Debug message 3"); // Should be buffered + Logger.FlushBuffer(); // Should output debug message 3 + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message 1", output); + Assert.DoesNotContain("Debug message 2", output); + Assert.Contains("Debug message 3", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferSizeLimit_DiscardOldestEntriesWhenExceeded() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 1000 // Small buffer size to force overflow + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + // Add enough logs to exceed buffer size + for (int i = 0; i < 20; i++) + { + logger.LogDebug($"Debug message {i} with enough characters to consume space in the buffer"); + } + + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message 0", output); // Older messages should be discarded + Assert.Contains("Debug message 19", output); // Newest messages should be kept + } + + [Trait("Category", "LoggerLifecycle")] + [Fact] + public void DisposingProvider_FlushesBufferedLogs() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + + var provider = LoggerFactoryHelper.CreateAndConfigureFactory(config); + var logger = provider.CreatePowertoolsLogger(); + + // Act + logger.LogDebug("Debug message before disposal"); // Should be buffered + provider.Dispose(); // Should flush buffer + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message before disposal", output); + } + + [Trait("Category", "LoggerConfiguration")] + [Fact] + public void LoggerInitialization_RegistersWithBufferManager() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-id"); + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions(), + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + logger.LogDebug("Test message"); + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Test message", output); + } + + [Trait("Category", "LoggerOutput")] + [Fact] + public void CustomLogOutput_ReceivesLogs() + { + // Arrange + var customOutput = new TestLoggerOutput(); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Debug, // Set to Debug to ensure we log directly + LogOutput = customOutput + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogDebug("Direct debug message"); + + // Assert + var output = customOutput.ToString(); + Assert.Contains("Direct debug message", output); + } + + [Trait("Category", "LoggerIntegration")] + [Fact] + public void RegisteringMultipleProviders_AllWorkCorrectly() + { + // Arrange - create a clean configuration for this test + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "shared-invocation"); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + + // Create providers using the shared configuration + var env = new PowertoolsEnvironment(); + var powertoolsConfig = new PowertoolsConfigurations(env); + + var provider1 = new BufferingLoggerProvider(config, powertoolsConfig); + var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); + + var logger1 = provider1.CreateLogger("Logger1"); + var logger2 = provider2.CreateLogger("Logger2"); + + // Act + logger1.LogDebug("Debug from logger1"); + logger2.LogDebug("Debug from logger2"); + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug from logger1", output); + Assert.Contains("Debug from logger2", output); + } + + [Trait("Category", "LoggerLifecycle")] + [Fact] + public void RegisteringLogBufferManager_HandlesMultipleProviders() + { + // Ensure we start with clean state + LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + // Arrange + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions(), + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Create and register first provider + var provider1 = new BufferingLoggerProvider(config, powertoolsConfig); + var logger1 = provider1.CreateLogger("Logger1"); + // Explicitly dispose and unregister first provider + provider1.Dispose(); + + // Now create and register a second provider + var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); + var logger2 = provider2.CreateLogger("Logger2"); + + // Act + logger1.LogDebug("Debug from first provider"); + logger2.LogDebug("Debug from second provider"); + + // Only the second provider should be registered with the LogBufferManager + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + // Only the second provider's logs should be flushed + Assert.DoesNotContain("Debug from first provider", output); + Assert.Contains("Debug from second provider", output); + } + + [Trait("Category", "BufferEmpty")] + [Fact] + public void FlushingEmptyBuffer_DoesNotCauseErrors() + { + // Arrange + LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "empty-test"); + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions(), + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + + // Act - flush without any logs + Logger.FlushBuffer(); + + // Assert - should not throw exceptions + Assert.Empty(_consoleOut.ToString()); + } + + [Trait("Category", "LogLevelThreshold")] + [Fact] + public void LogsAtExactBufferThreshold_AreBuffered() + { + // Arrange + LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "threshold-test"); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act + logger.LogDebug("Debug message exactly at threshold"); // Should be buffered + + // Assert before flush + Assert.DoesNotContain("Debug message exactly at threshold", _consoleOut.ToString()); + + // After flush + Logger.FlushBuffer(); + Assert.Contains("Debug message exactly at threshold", _consoleOut.ToString()); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs new file mode 100644 index 00000000..c113ff07 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs @@ -0,0 +1,148 @@ +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests; + +public class LoggingAspectFactoryTests +{ + [Fact] + public void GetInstance_ShouldReturnLoggingAspectInstance() + { + // Act + var result = LoggingAspectFactory.GetInstance(typeof(LoggingAspectFactoryTests)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } +} + +public class PowertoolsLoggerFactoryTests + { + [Fact] + public void Constructor_WithLoggerFactory_CreatesPowertoolsLoggerFactory() + { + // Arrange + var mockFactory = Substitute.For(); + + // Act + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void DefaultConstructor_CreatesPowertoolsLoggerFactory() + { + // Act + var factory = new PowertoolsLoggerFactory(); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void Create_WithConfigAction_ReturnsPowertoolsLoggerFactory() + { + // Act + var factory = PowertoolsLoggerFactory.Create(options => + { + options.Service = "TestService"; + }); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void Create_WithConfiguration_ReturnsLoggerFactory() + { + // Arrange + var configuration = new PowertoolsLoggerConfiguration + { + Service = "TestService" + }; + + // Act + var factory = PowertoolsLoggerFactory.Create(configuration); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void CreateBuilder_ReturnsLoggerBuilder() + { + // Act + var builder = PowertoolsLoggerFactory.CreateBuilder(); + + // Assert + Assert.NotNull(builder); + Assert.IsType(builder); + } + + [Fact] + public void CreateLogger_Generic_ReturnsLogger() + { + // Arrange + var mockFactory = Substitute.For(); + mockFactory.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + var logger = factory.CreateLogger(); + + // Assert + Assert.NotNull(logger); + mockFactory.Received(1).CreateLogger(typeof(PowertoolsLoggerFactoryTests).FullName); + } + + [Fact] + public void CreateLogger_WithCategory_ReturnsLogger() + { + // Arrange + var mockFactory = Substitute.For(); + mockFactory.CreateLogger("TestCategory").Returns(Substitute.For()); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + var logger = factory.CreateLogger("TestCategory"); + + // Assert + Assert.NotNull(logger); + mockFactory.Received(1).CreateLogger("TestCategory"); + } + + [Fact] + public void CreatePowertoolsLogger_ReturnsPowertoolsLogger() + { + // Arrange + var mockFactory = Substitute.For(); + mockFactory.CreatePowertoolsLogger().Returns(Substitute.For()); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + var logger = factory.CreatePowertoolsLogger(); + + // Assert + Assert.NotNull(logger); + mockFactory.Received(1).CreatePowertoolsLogger(); + } + + [Fact] + public void Dispose_DisposesInnerFactory() + { + // Arrange + var mockFactory = Substitute.For(); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + factory.Dispose(); + + // Assert + mockFactory.Received(1).Dispose(); + } + } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index 0bccdf1a..37f0fd83 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; @@ -26,6 +25,7 @@ using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; @@ -47,8 +47,12 @@ public LogFormatterTest() [Fact] public void Serialize_ShouldHandleEnumValues() { - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -68,7 +72,7 @@ public void Serialize_ShouldHandleEnumValues() i.Contains("\"message\":\"Dog\"") )); - var json = JsonSerializer.Serialize(Pet.Dog, PowertoolsLoggingSerializer.GetSerializerOptions()); + var json = JsonSerializer.Serialize(Pet.Dog, new PowertoolsLoggingSerializer().GetSerializerOptions()); Assert.Contains("Dog", json); } @@ -107,13 +111,6 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() var configurations = Substitute.For(); configurations.Service.Returns(service); - var loggerConfiguration = new LoggerConfiguration - { - Service = service, - MinimumLevel = minimumLevel, - LoggerOutputCase = LoggerOutputCase.PascalCase - }; - var globalExtraKeys = new Dictionary { { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, @@ -173,12 +170,20 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() } }; + var systemWrapper = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ReturnsForAnyArgs(formattedLogEntry); - Logger.UseFormatter(logFormatter); + + var config = new PowertoolsLoggerConfiguration + { + Service = service, + MinimumLogLevel = minimumLevel, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogFormatter = logFormatter, + LogOutput = systemWrapper + }; - var systemWrapper = Substitute.For(); - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(config, configurations); var logger = provider.CreateLogger(loggerName); var scopeExtraKeys = new Dictionary @@ -221,14 +226,20 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId )); - systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); + systemWrapper.Received(1).WriteLine(JsonSerializer.Serialize(formattedLogEntry)); } [Fact] public void Should_Log_CustomFormatter_When_Decorated() { - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + ResetAllState(); + var consoleOut = Substitute.For(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.LogFormatter = new CustomLogFormatter(); + }); + var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -238,7 +249,7 @@ public void Should_Log_CustomFormatter_When_Decorated() MemoryLimitInMB = 128 }; - Logger.UseFormatter(new CustomLogFormatter()); + // 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 @@ -262,8 +273,14 @@ public void Should_Log_CustomFormatter_When_Decorated() [Fact] public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() { - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); + ResetAllState(); + var consoleOut = Substitute.For(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.LogFormatter = new CustomLogFormatter(); + }); + var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -273,7 +290,7 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() MemoryLimitInMB = 128 }; - Logger.UseFormatter(new CustomLogFormatter()); + // Logger.UseFormatter(new CustomLogFormatter()); _testHandler.TestCustomFormatterNoDecorator("test", lambdaContext); @@ -298,10 +315,14 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() [Fact] public void Should_Log_CustomFormatter_When_Decorated_No_Context() { - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - - Logger.UseFormatter(new CustomLogFormatter()); + var consoleOut = Substitute.For(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.LogFormatter = new CustomLogFormatter(); + }); + + // Logger.UseFormatter(new CustomLogFormatter()); _testHandler.TestCustomFormatterWithDecoratorNoContext("test"); @@ -322,11 +343,29 @@ public void Should_Log_CustomFormatter_When_Decorated_No_Context() public void Dispose() { - Logger.UseDefaultFormatter(); - Logger.RemoveAllKeys(); - LoggingLambdaContext.Clear(); + ResetAllState(); + } + + private static void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + LoggerFactoryHolder.Reset(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } @@ -350,15 +389,16 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() logFormatter.FormatLogEntry(new LogEntry()).ReturnsNullForAnyArgs(); Logger.UseFormatter(logFormatter); - var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var systemWrapper = Substitute.For(); + var config = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Information, - LoggerOutputCase = LoggerOutputCase.PascalCase + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogFormatter = logFormatter }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(config, configurations); var logger = provider.CreateLogger(loggerName); // Act @@ -367,7 +407,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() // Assert Assert.Throws(Act); logFormatter.Received(1).FormatLogEntry(Arg.Any()); - systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); + systemWrapper.DidNotReceiveWithAnyArgs().WriteLine(Arg.Any()); //Clean up Logger.UseDefaultFormatter(); @@ -393,17 +433,17 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() var logFormatter = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ThrowsForAnyArgs(new Exception(errorMessage)); - Logger.UseFormatter(logFormatter); - var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var systemWrapper = Substitute.For(); + var config = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Information, - LoggerOutputCase = LoggerOutputCase.PascalCase + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogFormatter = logFormatter }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(config, configurations); var logger = provider.CreateLogger(loggerName); // Act @@ -412,7 +452,7 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() // Assert Assert.Throws(Act); logFormatter.Received(1).FormatLogEntry(Arg.Any()); - systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); + systemWrapper.DidNotReceiveWithAnyArgs().WriteLine(Arg.Any()); //Clean up Logger.UseDefaultFormatter(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs new file mode 100644 index 00000000..d5effd4a --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs @@ -0,0 +1,684 @@ +using System; +using System.Collections.Generic; +using AWS.Lambda.Powertools.Common.Core; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.Logging.Tests.Formatter +{ + [Collection("Sequential")] + public class LogFormattingTests + { + private readonly ITestOutputHelper _output; + + public LogFormattingTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void TestNumericFormatting() + { + // Set culture for thread and format provider + var originalCulture = System.Threading.Thread.CurrentThread.CurrentCulture; + System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + + + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "format-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + // Test numeric format specifiers + logger.LogInformation("Price: {price:0.00}", 123.4567); + logger.LogInformation("Percentage: {percent:0.0%}", 0.1234); + // Use explicit dollar sign instead of culture-dependent 'C' + // The logger explicitly uses InvariantCulture when formatting values, which uses "¤" as the currency symbol. + // This is by design to ensure consistent logging output regardless of server culture settings. + // By using $ directly in the format string as shown above, you bypass the culture-specific currency symbol and get the expected output in your tests. + logger.LogInformation("Currency: {amount:$#,##0.00}", 42.5); + + logger.LogInformation("Hex: {hex:X}", 255); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // These should all be properly formatted in the log + Assert.Contains("\"price\":123.46", logOutput); + Assert.Contains("\"percent\":\"12.3%\"", logOutput); + Assert.Contains("\"amount\":\"$42.50\"", logOutput); + Assert.Contains("\"hex\":\"FF\"", logOutput); + } + + [Fact] + public void TestCustomObjectFormatting() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "object-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + // Regular object formatting (uses ToString()) + logger.LogInformation("User data: {user}", user); + + // Object serialization with @ prefix + logger.LogInformation("User object: {@user}", user); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // First log should use ToString() + Assert.Contains("\"message\":\"User data: Doe, John (42)\"", logOutput); + Assert.Contains("\"user\":\"Doe, John (42)\"", logOutput); + + // Second log should serialize the object + Assert.Contains("\"user\":{", logOutput); + Assert.Contains("\"firstName\":\"John\"", logOutput); + Assert.Contains("\"lastName\":\"Doe\"", logOutput); + Assert.Contains("\"age\":42", logOutput); + } + + [Fact] + public void TestComplexObjectWithIgnoredProperties() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "complex-object-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var example = new ExampleClass + { + Name = "test", + Price = 1.999, + ThisIsBig = "big", + ThisIsHidden = "hidden" + }; + + // Test with @ prefix for serialization + logger.LogInformation("Example serialized: {@example}", example); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Should serialize the object properties + Assert.Contains("\"example\":{", logOutput); + Assert.Contains("\"name\":\"test\"", logOutput); + Assert.Contains("\"price\":1.999", logOutput); + Assert.Contains("\"this_is_big\":\"big\"", logOutput); + + // The JsonIgnore property should be excluded + Assert.DoesNotContain("this_is_hidden", logOutput); + } + + [Fact] + public void TestMixedFormatting() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "mixed-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "Jane", + LastName = "Smith", + Age = 35 + }; + + // Mix regular values with formatted values and objects + logger.LogInformation( + "Details: User={@user}, Price={price:$#,##0.00}, Date={date:yyyy-MM-dd}", + user, + 123.45, + new DateTime(2023, 4, 5) + ); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify all formatted parts + Assert.Contains("\"User\":{", logOutput); + Assert.Contains("\"FirstName\":\"Jane\"", logOutput); + Assert.Contains("\"Price\":\"$123.45\"", logOutput); + Assert.Contains("\"Date\":\"2023-04-05\"", logOutput); + } + + [Fact] + public void TestNestedObjectSerialization() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "nested-object-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var parent = new ParentClass + { + Name = "Parent", + Child = new ChildClass { Name = "Child" } + }; + + // Regular object formatting (uses ToString()) + logger.LogInformation("Parent: {parent}", parent); + + // Object serialization with @ prefix + logger.LogInformation("Parent with child: {@parent}", parent); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Regular formatting should use ToString() + Assert.Contains("\"parent\":\"Parent with Child\"", logOutput); + + // Serialized object should include nested structure + Assert.Contains("\"parent\":{", logOutput); + Assert.Contains("\"name\":\"Parent\"", logOutput); + Assert.Contains("\"child\":{", logOutput); + Assert.Contains("\"name\":\"Child\"", logOutput); + } + + [Fact] + public void TestCollectionFormatting() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "collection-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var items = new[] { 1, 2, 3 }; + var dict = new Dictionary { ["key1"] = "value1", ["key2"] = 42 }; + + // Regular array formatting + logger.LogInformation("Array: {items}", items); + + // Serialized array with @ prefix + logger.LogInformation("Array serialized: {@items}", items); + + // Dictionary formatting + logger.LogInformation("Dictionary: {dict}", dict); + + // Serialized dictionary + logger.LogInformation("Dictionary serialized: {@dict}", dict); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Regular array formatting uses ToString() + Assert.Contains("\"items\":\"System.Int32[]\"", logOutput); + + // Serialized array should include all items + Assert.Contains("\"items\":[1,2,3]", logOutput); + + // Dictionary formatting depends on ToString() implementation + Assert.Contains("\"dict\":\"System.Collections.Generic.Dictionary", logOutput); + + // Serialized dictionary should include all key-value pairs + Assert.Contains("\"dict\":{", logOutput); + Assert.Contains("\"key1\":\"value1\"", logOutput); + Assert.Contains("\"key2\":42", logOutput); + } + + [Fact] + public void TestNullAndEdgeCases() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "null-edge-case-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + User user = null; + + // Test null formatting + logger.LogInformation("Null object: {user}", user); + logger.LogInformation("Null serialized: {@user}", user); + + // Extreme values + logger.LogInformation("Max value: {max}", int.MaxValue); + logger.LogInformation("Min value: {min}", int.MinValue); + logger.LogInformation("Max double: {maxDouble}", double.MaxValue); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Null objects should be null in output + Assert.Contains("\"user\":null", logOutput); + + // Extreme values should be preserved + Assert.Contains("\"max\":2147483647", logOutput); + Assert.Contains("\"min\":-2147483648", logOutput); + Assert.Contains("\"max_double\":1.7976931348623157E+308", logOutput); + } + + [Fact] + public void TestDateTimeFormats() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "datetime-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var date = new DateTime(2023, 12, 31, 23, 59, 59); + + // Test different date formats + logger.LogInformation("ISO: {date:o}", date); + logger.LogInformation("Short date: {date:d}", date); + logger.LogInformation("Custom: {date:yyyy-MM-dd'T'HH:mm:ss.fff}", date); + logger.LogInformation("Time only: {date:HH:mm:ss}", date); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify different formats + Assert.Contains("\"date\":\"2023-12-31T23:59:59", logOutput); // ISO format + Assert.Contains("\"date\":\"12/31/2023\"", logOutput); // Short date + Assert.Contains("\"date\":\"2023-12-31T23:59:59.000\"", logOutput); // Custom + Assert.Contains("\"date\":\"23:59:59\"", logOutput); // Time only + } + + [Fact] + public void TestExceptionLogging() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "exception-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + try + { + throw new InvalidOperationException("Test exception"); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred with {data}", "test value"); + + // Test with nested exceptions + var outerEx = new Exception("Outer exception", ex); + logger.LogError(outerEx, "Nested exception test"); + } + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify exception details are included + Assert.Contains("\"message\":\"An error occurred with test value\"", logOutput); + Assert.Contains("\"exception\":{", logOutput); + Assert.Contains("\"type\":\"System.InvalidOperationException\"", logOutput); + Assert.Contains("\"message\":\"Test exception\"", logOutput); + Assert.Contains("\"stack_trace\":", logOutput); + + // Verify nested exception details + Assert.Contains("\"message\":\"Nested exception test\"", logOutput); + Assert.Contains("\"inner_exception\":{", logOutput); + } + + [Fact] + public void TestScopedLogging() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "scope-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + // Log without any scope + logger.LogInformation("Outside any scope"); + + // Create a scope and log within it + using (logger.BeginScope(new { RequestId = "req-123", UserId = "user-456" })) + { + logger.LogInformation("Inside first scope"); + + // Nested scope + using (logger.BeginScope(new { OperationId = "op-789" })) + { + logger.LogInformation("Inside nested scope"); + } + + logger.LogInformation("Back to first scope"); + } + + // Back outside all scopes + logger.LogInformation("Outside all scopes again"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify scope information is included correctly + Assert.Contains("\"message\":\"Inside first scope\"", logOutput); + Assert.Contains("\"request_id\":\"req-123\"", logOutput); + Assert.Contains("\"user_id\":\"user-456\"", logOutput); + + // Nested scope should include both scopes' data + Assert.Contains("\"message\":\"Inside nested scope\"", logOutput); + Assert.Contains("\"operation_id\":\"op-789\"", logOutput); + } + + [Fact] + public void TestDifferentLogLevels() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "log-level-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + logger.LogTrace("This is a trace message"); + logger.LogDebug("This is a debug message"); + logger.LogInformation("This is an info message"); + logger.LogWarning("This is a warning message"); + logger.LogError("This is an error message"); + logger.LogCritical("This is a critical message"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Trace shouldn't be logged (below default) + Assert.DoesNotContain("\"level\":\"Trace\"", logOutput); + + // Debug and above should be logged + Assert.Contains("\"level\":\"Debug\"", logOutput); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"level\":\"Warning\"", logOutput); + Assert.Contains("\"level\":\"Error\"", logOutput); + Assert.Contains("\"level\":\"Critical\"", logOutput); + } + + [Fact] + public void Should_Log_Multiple_Formats_No_Duplicates() + { + var output = new TestLoggerOutput(); + LambdaLifecycleTracker.Reset(); + LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "log-level-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42, + TimeStamp = "FakeTime" + }; + + Logger.LogInformation(user, "{Name} and is {Age} years old", new object[]{user.Name, user.Age}); + Assert.Contains("\"first_name\":\"John\"", output.ToString()); + Assert.Contains("\"last_name\":\"Doe\"", output.ToString()); + Assert.Contains("\"age\":42", output.ToString()); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", output.ToString()); // does not override name + + output.Clear(); + + Logger.LogInformation("{level}", user); + Assert.Contains("\"level\":\"Information\"", output.ToString()); // does not override level + Assert.Contains("\"message\":\"Doe, John (42)\"", output.ToString()); // does not override message + Assert.DoesNotContain("\"timestamp\":\"FakeTime\"", output.ToString()); + + output.Clear(); + + Logger.LogInformation("{coldstart}", user); // still not sure if convert to PascalCase to compare or not + Assert.Contains("\"cold_start\":true", output.ToString()); + + output.Clear(); + + Logger.AppendKey("level", "Override"); + Logger.AppendKey("message", "Override"); + Logger.AppendKey("timestamp", "Override"); + Logger.AppendKey("name", "Override"); + Logger.AppendKey("service", "Override"); + Logger.AppendKey("cold_start", "Override"); + Logger.AppendKey("message2", "Its ok!"); + + Logger.LogInformation("no override"); + Assert.DoesNotContain("\"level\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"message\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"timestamp\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"name\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"service\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"cold_start\":\"Override\"", output.ToString()); + Assert.Contains("\"message2\":\"Its ok!\"", output.ToString()); + Assert.Contains("\"level\":\"Information\"", output.ToString()); + } + + [Fact] + public void Should_Log_Multiple_Formats() + { + LambdaLifecycleTracker.Reset(); + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "log-level-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + Logger.LogInformation(user, "{Name} is {Age} years old", new object[]{user.FirstName, user.Age}); + + var logOutput = output.ToString(); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"John is 42 years old\"", logOutput); + Assert.Contains("\"service\":\"log-level-test-service\"", logOutput); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", logOutput); + Assert.Contains("\"first_name\":\"John\"", logOutput); + Assert.Contains("\"last_name\":\"Doe\"", logOutput); + Assert.Contains("\"age\":42", logOutput); + + output.Clear(); + + // Message template string + Logger.LogInformation("{user}", user); + + logOutput = output.ToString(); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Doe, John (42)\"", logOutput); + Assert.Contains("\"service\":\"log-level-test-service\"", logOutput); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", logOutput); + Assert.Contains("\"user\":\"Doe, John (42)\"", logOutput); + // Verify user properties are NOT included in output (since @ prefix wasn't used) + Assert.DoesNotContain("\"first_name\":", logOutput); + Assert.DoesNotContain("\"last_name\":", logOutput); + Assert.DoesNotContain("\"age\":", logOutput); + + output.Clear(); + + // Object serialization with @ prefix + Logger.LogInformation("{@user}", user); + + logOutput = output.ToString(); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Doe, John (42)\"", logOutput); + Assert.Contains("\"service\":\"log-level-test-service\"", logOutput); + Assert.Contains("\"cold_start\":true", logOutput); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", logOutput); + // Verify serialized user object with all properties + Assert.Contains("\"user\":{", logOutput); + Assert.Contains("\"first_name\":\"John\"", logOutput); + Assert.Contains("\"last_name\":\"Doe\"", logOutput); + Assert.Contains("\"age\":42", logOutput); + Assert.Contains("\"name\":\"John Doe\"", logOutput); + Assert.Contains("\"time_stamp\":null", logOutput); + Assert.Contains("}", logOutput); + + output.Clear(); + + Logger.LogInformation("{cold_start}", false); + + logOutput = output.ToString(); + // Assert that the reserved field wasn't replaced + Assert.Contains("\"cold_start\":true", logOutput); + Assert.DoesNotContain("\"cold_start\":false", logOutput); + + output.Clear(); + + Logger.AppendKey("level", "fakeLevel"); + Logger.LogInformation("no override"); + + logOutput = output.ToString(); + + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.DoesNotContain("\"level\":\"fakeLevel\"", logOutput); + + _output.WriteLine(logOutput); + + } + + public class ParentClass + { + public string Name { get; set; } + public ChildClass Child { get; set; } + + public override string ToString() + { + return $"Parent with Child"; + } + } + + public class ChildClass + { + public string Name { get; set; } + + public override string ToString() + { + return $"Child: {Name}"; + } + } + + public class Node + { + public string Name { get; set; } + public Node Parent { get; set; } + public List Children { get; set; } = new List(); + + public override string ToString() + { + return $"Node: {Name}"; + } + } + + public class User + { + public string FirstName { get; set; } + public string LastName { get; set; } + public int Age { get; set; } + public string Name => $"{FirstName} {LastName}"; + public string TimeStamp { get; set; } + + public override string ToString() + { + return $"{LastName}, {FirstName} ({Age})"; + } + } + } +} \ 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 f9ffd5eb..7622ac23 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs @@ -7,6 +7,7 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; +[Collection("Sequential")] public sealed class ExceptionFunctionHandlerTests : IDisposable { [Fact] @@ -42,6 +43,6 @@ public void Utility_Should_Not_Throw_Exceptions_To_Client() public void Dispose() { LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs new file mode 100644 index 00000000..c58c75ca --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs @@ -0,0 +1,442 @@ +using System.Text.Json.Serialization; +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using AWS.Lambda.Powertools.Logging.Tests.Formatter; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; + +public class Handlers +{ + private readonly ILogger _logger; + + public Handlers(ILogger logger) + { + _logger = logger; + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("debug message"); + + var example = new ExampleClass + { + Name = "test", + Price = 1.999, + ThisIsBig = "big", + ThisIsHidden = "hidden" + }; + + _logger.LogInformation("Example object: {example}", example); + _logger.LogInformation("Another JSON log {d:0.000}", 1.2333); + + _logger.LogDebug(example); + _logger.LogInformation(example); + } + + [Logging(LogEvent = true, CorrelationIdPath = "price")] + public void TestMethodCorrelation(ExampleClass message, ILambdaContext lambdaContext) + { + } +} + +public class StaticHandler +{ + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, Service = "my-service122")] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + Logger.LogInformation("Static method"); + } +} + +public class HandlerTests +{ + private readonly ITestOutputHelper _output; + + public HandlerTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void TestMethod() + { + var output = new TestLoggerOutput(); + + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "my-service122"; + config.SamplingRate = 0.002; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.JsonOptions = new JsonSerializerOptions + { + WriteIndented = true + // PropertyNamingPolicy = null, + // DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance, + }; + config.LogOutput = output; + }); + }).CreateLogger(); + + + var handler = new Handlers(logger); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + handler.TestMethodCorrelation(new ExampleClass + { + Name = "test-function", + Price = 1.999, + ThisIsBig = "big", + }, null); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Check if the output contains newlines and spacing (indentation) + Assert.Contains("\n", logOutput); + Assert.Contains(" ", logOutput); + + // Verify write indented JSON + Assert.Contains("\"Level\": \"Information\"", logOutput); + Assert.Contains("\"Service\": \"my-service122\"", logOutput); + Assert.Contains("\"Message\": \"Information message\"", logOutput); + Assert.Contains("\"Custom-key\": \"custom-value\"", logOutput); + Assert.Contains("\"FunctionName\": \"test-function\"", logOutput); + Assert.Contains("\"SamplingRate\": 0.002", logOutput); + } + + [Fact] + public void TestMethodCustom() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "my-service122"; + config.SamplingRate = 0.002; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.JsonOptions = new JsonSerializerOptions + { + // PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + // DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseLower + }; + + config.LogFormatter = new CustomLogFormatter(); + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var handler = new Handlers(logger); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify CamelCase formatting (custom formatter) + Assert.Contains("\"service\":\"my-service122\"", logOutput); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Information message\"", logOutput); + Assert.Contains("\"correlationIds\":{\"awsRequestId\":\"123\"}", logOutput); + } + + [Fact] + public void TestBuffer() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + // builder.AddFilter("AWS.Lambda.Powertools.Logging.Tests.Handlers.Handlers", LogLevel.Debug); + builder.AddPowertoolsLogger(config => + { + config.Service = "my-service122"; + config.SamplingRate = 0.002; + config.MinimumLogLevel = LogLevel.Information; + config.JsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + // PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper, + DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseUpper + }; + config.LogOutput = output; + config.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }; + }); + }).CreatePowertoolsLogger(); + + var handler = new Handlers(logger); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify buffering behavior - only Information logs or higher should be in output + Assert.Contains("Information message", logOutput); + Assert.DoesNotContain("debug message", logOutput); // Debug should be buffered + + // Verify JSON options with indentation + Assert.Contains("\n", logOutput); + Assert.Contains(" ", logOutput); // Check for indentation + + // Check that kebab-case dictionary keys are working + Assert.Contains("\"CUSTOM-KEY\"", logOutput); + } + + [Fact] + public void TestMethodStatic() + { + var output = new TestLoggerOutput(); + var handler = new StaticHandler(); + + Logger.Configure(options => + { + options.LogOutput = output; + options.LoggerOutputCase = LoggerOutputCase.CamelCase; + }); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify static logger configuration + // Verify override of LoggerOutputCase from attribute + Assert.Contains("\"Service\":\"my-service122\"", logOutput); + Assert.Contains("\"Level\":\"Information\"", logOutput); + Assert.Contains("\"Message\":\"Static method\"", logOutput); + } + + [Fact] + public async Task Should_Log_Properties_Setup_Constructor() + { + var output = new TestLoggerOutput(); + _ = new SimpleFunctionWithStaticConfigure(output); + + await SimpleFunctionWithStaticConfigure.FunctionHandler(); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + + Assert.Contains("\"service\":\"MyServiceName\"", logOutput); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Starting up!\"", logOutput); + Assert.Contains("\"xray_trace_id\"", logOutput); + } + + [Fact] + public async Task Should_Flush_On_Exception_Async() + { + var output = new TestLoggerOutput(); + var handler = new SimpleFunctionWithStaticConfigure(output); + + try + { + await handler.AsyncException(); + } + catch + { + } + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"level\":\"Debug\"", logOutput); + Assert.Contains("\"message\":\"Debug!!\"", logOutput); + Assert.Contains("\"xray_trace_id\"", logOutput); + } + + [Fact] + public void Should_Flush_On_Exception() + { + var output = new TestLoggerOutput(); + var handler = new SimpleFunctionWithStaticConfigure(output); + + try + { + handler.SyncException(); + } + catch + { + } + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"level\":\"Debug\"", logOutput); + Assert.Contains("\"message\":\"Debug!!\"", logOutput); + Assert.Contains("\"xray_trace_id\"", logOutput); + } + + [Fact] + public void TestJsonOptionsPropertyNaming() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "json-options-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = false + }; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var handler = new Handlers(logger); + var example = new ExampleClass + { + Name = "TestValue", + Price = 29.99, + ThisIsBig = "LargeValue" + }; + + logger.LogInformation("Testing JSON options with example: {@example}", example); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify snake_case naming policy is applied + Assert.Contains("\"this_is_big\":\"LargeValue\"", logOutput); + Assert.Contains("\"name\":\"TestValue\"", logOutput); + } + + [Fact] + public void TestJsonOptionsDictionaryKeyPolicy() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "json-dictionary-service"; + config.JsonOptions = new JsonSerializerOptions + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var dictionary = new Dictionary + { + { "UserID", 12345 }, + { "OrderDetails", new { ItemCount = 3, Total = 150.75 } }, + { "ShippingAddress", "123 Main St" } + }; + + logger.LogInformation("Dictionary with custom key policy: {@dictionary}", dictionary); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Fix assertion to match actual camelCase behavior with acronyms + Assert.Contains("\"userID\":12345", logOutput); // ID remains uppercase + Assert.Contains("\"orderDetails\":", logOutput); + Assert.Contains("\"shippingAddress\":", logOutput); + } + + [Fact] + public void TestJsonOptionsWriteIndented() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "json-indented-service"; + config.JsonOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var example = new ExampleClass + { + Name = "IndentedTest", + Price = 59.99, + ThisIsBig = "IndentedValue" + }; + + logger.LogInformation("Testing indented JSON: {@example}", example); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Check if the output contains newlines and spacing (indentation) + Assert.Contains("\n", logOutput); + Assert.Contains(" ", logOutput); + } +} + +#endif + +public class ExampleClass +{ + public string Name { get; set; } + + public double Price { get; set; } + + public string ThisIsBig { get; set; } + + [JsonIgnore] public string ThisIsHidden { get; set; } +} \ 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 index 08fe54d4..0c9de3e1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -15,11 +15,13 @@ using System; using System.Text.Json.Serialization; +using System.Threading.Tasks; 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.Common; using AWS.Lambda.Powertools.Logging.Tests.Serializers; using LogLevel = Microsoft.Extensions.Logging.LogLevel; @@ -189,23 +191,68 @@ public class TestServiceHandler { public void LogWithEnv() { - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); - Logger.LogInformation("Service: Environment Service"); } - - public void LogWithAndWithoutEnv() - { - Logger.LogInformation("Service: service_undefined"); - - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); - - Logger.LogInformation("Service: service_undefined"); - } [Logging(Service = "Attribute Service")] public void Handler() { Logger.LogInformation("Service: Attribute Service"); } +} + +public class SimpleFunctionWithStaticConfigure +{ + public SimpleFunctionWithStaticConfigure(IConsoleWrapper output) + { + // Constructor logic can go here if needed + Logger.Configure(logger => + { + logger.LogOutput = output; + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + }; + }); + } + + [Logging] + public static async Task FunctionHandler() + { + // only set on handler + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + Logger.LogInformation("Starting up!"); + + return new APIGatewayHttpApiV2ProxyResponse + { + Body = "Hello", + StatusCode = 200 + }; + } + + [Logging(FlushBufferOnUncaughtError = true)] + public APIGatewayHttpApiV2ProxyResponse SyncException() + { + // only set on handler + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + Logger.LogDebug("Debug!!"); + Logger.LogInformation("Starting up!"); + + throw new Exception(); + } + + [Logging(FlushBufferOnUncaughtError = true)] + public async Task AsyncException() + { + // only set on handler + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + Logger.LogDebug("Debug!!"); + Logger.LogInformation("Starting up!"); + + throw new Exception(); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs new file mode 100644 index 00000000..bbb1c43b --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Tests.Formatter; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.Logging.Tests; + +public class PowertoolsLoggerBuilderTests +{ + private readonly ITestOutputHelper _output; + + public PowertoolsLoggerBuilderTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void WithService_SetsServiceName() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("test-builder-service") + .Build(); + + logger.LogInformation("Testing service name"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"service\":\"test-builder-service\"", logOutput, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void WithSamplingRate_SetsSamplingRate() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("sampling-test") + .WithSamplingRate(0.5) + .Build(); + + // We can't directly test sampling rate in a deterministic way, + // but we can verify the logger is created successfully + logger.LogInformation("Testing sampling rate"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"message\":\"Testing sampling rate\"", logOutput, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void WithMinimumLogLevel_FiltersLowerLevels() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("log-level-test") + .WithMinimumLogLevel(LogLevel.Warning) + .Build(); + + logger.LogDebug("Debug message"); + logger.LogInformation("Info message"); + logger.LogWarning("Warning message"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.DoesNotContain("Debug message", logOutput); + Assert.DoesNotContain("Info message", logOutput); + Assert.Contains("Warning message", logOutput); + } + +#if NET8_0_OR_GREATER + [Fact] + public void WithJsonOptions_AppliesFormatting() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithService("json-options-test") + .WithLogOutput(output) + .WithJsonOptions(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }) + .Build(); + + var testObject = new ExampleClass + { + Name = "TestName", + ThisIsBig = "BigValue" + }; + + logger.LogInformation("Test object: {@testObject}", testObject); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"this_is_big\":\"BigValue\"", logOutput); + Assert.Contains("\"name\":\"TestName\"", logOutput); + Assert.Contains("\n", logOutput); // Indentation includes newlines + } +#endif + + [Fact] + public void WithTimestampFormat_FormatsTimestamp() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("timestamp-test") + .WithTimestampFormat("yyyy-MM-dd") + .Build(); + + logger.LogInformation("Testing timestamp format"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Should match yyyy-MM-dd format (e.g., "2023-04-25") + Assert.Matches("\"timestamp\":\"\\d{4}-\\d{2}-\\d{2}\"", logOutput); + } + + [Fact] + public void WithOutputCase_ChangesPropertyCasing() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("case-test") + .WithOutputCase(LoggerOutputCase.PascalCase) + .Build(); + + logger.LogInformation("Testing output case"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"Service\":\"case-test\"", logOutput); + Assert.Contains("\"Level\":\"Information\"", logOutput); + Assert.Contains("\"Message\":\"Testing output case\"", logOutput); + } + + [Fact] + public void WithLogBuffering_BuffersLowLevelLogs() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("buffer-test") + .WithLogBuffering(options => + { + options.BufferAtLogLevel = LogLevel.Debug; + }) + .Build(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "config-test"); + logger.LogDebug("Debug buffered message"); + logger.LogInformation("Info message"); + + // Without FlushBuffer(), the debug message should be buffered + var initialOutput = output.ToString(); + _output.WriteLine("Before flush: " + initialOutput); + + Assert.DoesNotContain("Debug buffered message", initialOutput); + Assert.Contains("Info message", initialOutput); + + // After flushing, the debug message should appear + logger.FlushBuffer(); + var afterFlushOutput = output.ToString(); + _output.WriteLine("After flush: " + afterFlushOutput); + + Assert.Contains("Debug buffered message", afterFlushOutput); + } + + [Fact] + public void BuilderChaining_ConfiguresAllProperties() + { + var output = new TestLoggerOutput(); + var customFormatter = new CustomLogFormatter(); + + var logger = new PowertoolsLoggerBuilder() + .WithService("chained-config-service") + .WithSamplingRate(0.1) + .WithMinimumLogLevel(LogLevel.Information) + .WithOutputCase(LoggerOutputCase.SnakeCase) + .WithFormatter(customFormatter) + .WithLogOutput(output) + .Build(); + + logger.LogInformation("Testing fully configured logger"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify multiple configured properties are applied + Assert.Contains("\"service\":\"chained-config-service\"", logOutput); + Assert.Contains("\"message\":\"Testing fully configured logger\"", logOutput); + Assert.Contains("\"sample_rate\":0.1", logOutput); + } +} \ 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 e034ce33..f4ba2a43 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -20,6 +20,8 @@ using System.Linq; using System.Text; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; +using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Utilities; @@ -34,30 +36,32 @@ public class PowertoolsLoggerTest : IDisposable { public PowertoolsLoggerTest() { - Logger.UseDefaultFormatter(); + // Logger.UseDefaultFormatter(); } - private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel minimumLevel) + private static void Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel MinimumLogLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - configurations.LogLevel.Returns(minimumLevel.ToString()); + configurations.LogLevel.Returns(MinimumLogLevel.ToString()); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None + Service = service, + LoggerOutputCase = LoggerOutputCase.PascalCase, + MinimumLogLevel = MinimumLogLevel, + LogOutput = systemWrapper // Set the output directly on configuration }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); switch (logLevel) @@ -88,32 +92,34 @@ private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, } // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains(service)) ); } - private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logLevel, LogLevel minimumLevel) + private static void Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel logLevel, + LogLevel MinimumLogLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - configurations.LogLevel.Returns(minimumLevel.ToString()); + configurations.LogLevel.Returns(MinimumLogLevel.ToString()); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = minimumLevel + MinimumLogLevel = MinimumLogLevel, + LogOutput = systemWrapper // Set the output directly on configuration }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); switch (logLevel) @@ -144,33 +150,33 @@ private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logL } // Assert - systemWrapper.DidNotReceive().LogLine( + systemWrapper.DidNotReceive().WriteLine( Arg.Any() ); } [Theory] [InlineData(LogLevel.Trace)] - public void LogTrace_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogTrace_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Trace, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Trace, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Trace)] [InlineData(LogLevel.Debug)] - public void LogDebug_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogDebug_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Debug, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Debug, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Trace)] [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Information)] - public void LogInformation_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogInformation_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Information, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Information, MinimumLogLevel); } [Theory] @@ -178,9 +184,9 @@ public void LogInformation_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimum [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Information)] [InlineData(LogLevel.Warning)] - public void LogWarning_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogWarning_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Warning, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Warning, MinimumLogLevel); } [Theory] @@ -189,9 +195,9 @@ public void LogWarning_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLeve [InlineData(LogLevel.Information)] [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] - public void LogError_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogError_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Error, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Error, MinimumLogLevel); } [Theory] @@ -201,9 +207,9 @@ public void LogError_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogCritical_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogCritical_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Critical, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Critical, MinimumLogLevel); } [Theory] @@ -212,9 +218,9 @@ public void LogCritical_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLev [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogTrace_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogTrace_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Trace, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Trace, MinimumLogLevel); } [Theory] @@ -222,33 +228,33 @@ public void LogTrace_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimum [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogDebug_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogDebug_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Debug, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Debug, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogInformation_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogInformation_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Information, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Information, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogWarning_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogWarning_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Warning, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Warning, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Critical)] - public void LogError_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogError_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Error, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Error, MinimumLogLevel); } [Theory] @@ -258,9 +264,9 @@ public void LogError_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimum [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogNone_WithAnyMinimumLevel_DoesNotLog(LogLevel minimumLevel) + public void LogNone_WithAnyMinimumLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.None, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.None, MinimumLogLevel); } [Fact] @@ -270,32 +276,29 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Trace; var loggerSampleRate = 0.7; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerSampleRate.Returns(loggerSampleRate); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None + Service = service, + MinimumLogLevel = logLevel, + LogOutput = systemWrapper, + SamplingRate = loggerSampleRate }; - // Act - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); - + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger("test"); logger.LogInformation("Test"); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains(service) && s.Contains(loggerSampleRate.ToString(CultureInfo.InvariantCulture)) @@ -317,25 +320,26 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerSampleRate.Returns(loggerSampleRate); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - - var loggerConfiguration = new LoggerConfiguration + var systemWrapper = Substitute.For(); + + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None + Service = service, + MinimumLogLevel = logLevel, + LogOutput = systemWrapper, + SamplingRate = loggerSampleRate, + Random = randomSampleRate }; - + // Act - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger("test"); - + logger.LogInformation("Test"); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s == $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {loggerSampleRate}, Sampler Value: {randomSampleRate}." @@ -357,22 +361,23 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerSampleRate.Returns(loggerSampleRate); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None - }; - - // Act - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + Service = service, + MinimumLogLevel = logLevel, + LogOutput = systemWrapper, + SamplingRate = loggerSampleRate + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s == $"Skipping sampling rate configuration because of invalid value. Sampling rate: {loggerSampleRate}" @@ -387,24 +392,23 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.CamelCase.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; // Act - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -416,7 +420,7 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\"}") ) @@ -430,24 +434,23 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.CamelCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper }; - + // Act - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -459,7 +462,7 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\"}") ) @@ -473,23 +476,22 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -501,7 +503,7 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"Message\":{\"PropOne\":\"Value 1\",\"PropTwo\":\"Value 2\"}") ) @@ -515,23 +517,22 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.PascalCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -543,7 +544,7 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"Message\":{\"PropOne\":\"Value 1\",\"PropTwo\":\"Value 2\"}") )); } @@ -555,23 +556,22 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.SnakeCase.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -583,7 +583,7 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") )); } @@ -595,23 +595,22 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.SnakeCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.SnakeCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -623,7 +622,7 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") )); } @@ -635,22 +634,21 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None - }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -662,7 +660,7 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}"))); } @@ -677,15 +675,15 @@ public void BeginScope_WhenScopeIsObject_ExtractScopeKeys() var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = logLevel + MinimumLogLevel = logLevel }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new @@ -720,15 +718,15 @@ public void BeginScope_WhenScopeIsObjectDictionary_ExtractScopeKeys() var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = logLevel + MinimumLogLevel = logLevel }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -763,15 +761,15 @@ public void BeginScope_WhenScopeIsStringDictionary_ExtractScopeKeys() var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = logLevel + MinimumLogLevel = logLevel }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -819,14 +817,15 @@ public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLeve configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Trace, + MinimumLogLevel = LogLevel.Trace, + LogOutput = systemWrapper }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -868,7 +867,7 @@ public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLeve } } - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains(scopeKeys.Keys.First()) && s.Contains(scopeKeys.Keys.Last()) && s.Contains(scopeKeys.Values.First().ToString()) && @@ -902,15 +901,16 @@ public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLeve configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Trace, + MinimumLogLevel = LogLevel.Trace, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -952,7 +952,7 @@ public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLeve } } - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains(scopeKeys.Keys.First()) && s.Contains(scopeKeys.Keys.Last()) && s.Contains(scopeKeys.Values.First()) && @@ -986,15 +986,16 @@ public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool lo configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Trace, + MinimumLogLevel = LogLevel.Trace, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new @@ -1036,7 +1037,7 @@ public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool lo } } - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("PropOne") && s.Contains("PropTwo") && s.Contains(scopeKeys.PropOne) && @@ -1054,22 +1055,21 @@ public void Log_WhenException_LogsExceptionDetails() var service = Guid.NewGuid().ToString(); var error = new InvalidOperationException("TestError"); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); try @@ -1082,15 +1082,16 @@ public void Log_WhenException_LogsExceptionDetails() } // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => 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()") + systemWrapper.Received(1).WriteLine(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] public void Log_Inner_Exception() { @@ -1100,40 +1101,48 @@ public void Log_Inner_Exception() var error = new InvalidOperationException("Parent exception message", new ArgumentNullException(nameof(service), "Very important inner exception message")); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); logger.LogError( - error, + error, "Something went wrong and we logged an exception itself with an inner exception. This is a param {arg}", 12345); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); - - systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"level\":\"Error\",\"service\":\"" + service+ "\",\"name\":\"" + loggerName + "\",\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"Very important inner exception message (Parameter 'service')\"}}}") + + systemWrapper.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Error\"") && + s.Contains("\"service\":\"" + service + "\"") && + s.Contains("\"name\":\"" + loggerName + "\"") && + s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\"") && + s.Contains("\"exception\":{") && + s.Contains("\"type\":\"System.InvalidOperationException\"") && + s.Contains("\"message\":\"Parent exception message\"") && + s.Contains("\"inner_exception\":{") && + s.Contains("\"type\":\"System.ArgumentNullException\"") && + s.Contains("\"message\":\"Very important inner exception message (Parameter 'service')\"") )); } - + [Fact] public void Log_Nested_Inner_Exception() { @@ -1143,35 +1152,43 @@ public void Log_Nested_Inner_Exception() var error = new InvalidOperationException("Parent exception message", new ArgumentNullException(nameof(service), new Exception("Very important nested inner exception message"))); - + var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); - + logger.LogError( - error, + error, "Something went wrong and we logged an exception itself with an inner exception. This is a param {arg}", 12345); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"service\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Very important nested inner exception message\"}}}}") + systemWrapper.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\"") && + s.Contains("\"exception\":{") && + s.Contains("\"type\":\"System.InvalidOperationException\"") && + s.Contains("\"message\":\"Parent exception message\"") && + s.Contains("\"inner_exception\":{") && + s.Contains("\"type\":\"System.ArgumentNullException\"") && + s.Contains("\"message\":\"service\"") && + s.Contains("\"inner_exception\":{") && + s.Contains("\"type\":\"System.Exception\"") && + s.Contains("\"message\":\"Very important nested inner exception message\"") )); } @@ -1183,22 +1200,21 @@ public void Log_WhenNestedException_LogsExceptionDetails() var service = Guid.NewGuid().ToString(); var error = new InvalidOperationException("TestError"); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); try @@ -1211,14 +1227,14 @@ public void Log_WhenNestedException_LogsExceptionDetails() } // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"error\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); } [Fact] - public void Log_WhenByteArray_LogsByteArrayNumbers() + public void Log_WhenByteArray_LogsBase64EncodedString() { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -1226,29 +1242,29 @@ public void Log_WhenByteArray_LogsByteArrayNumbers() var bytes = new byte[10]; new Random().NextBytes(bytes); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Bytes = bytes }); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"bytes\":[" + string.Join(",", bytes) + "]") + var base64String = Convert.ToBase64String(bytes); + systemWrapper.Received(1).WriteLine(Arg.Is(s => + s.Contains($"\"bytes\":\"{base64String}\"") )); } @@ -1265,29 +1281,28 @@ public void Log_WhenMemoryStream_LogsBase64String() Position = 0 }; var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"stream\":\"" + Convert.ToBase64String(bytes) + "\"") )); } @@ -1307,29 +1322,28 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() Position = 0 }; var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"stream\":\"" + Convert.ToBase64String(bytes) + "\"") )); } @@ -1337,35 +1351,55 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() [Fact] public void Log_Set_Execution_Environment_Context() { - var _originalValue = Environment.GetEnvironmentVariable("POWERTOOLS_SERVICE_NAME"); - // Arrange var loggerName = Guid.NewGuid().ToString(); - var assemblyName = "AWS.Lambda.Powertools.Logger"; - var assemblyVersion = "1.0.0"; - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); + var env = new PowertoolsEnvironment(); + // Act + var configurations = new PowertoolsConfigurations(env); + + var loggerConfiguration = new PowertoolsLoggerConfiguration + { + Service = null, + MinimumLogLevel = LogLevel.None + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); + var logger = provider.CreateLogger(loggerName); + logger.LogInformation("Test"); + + // Assert + Assert.Equal($"{Constants.FeatureContextIdentifier}/Logging/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); + } + + [Fact] + public void Log_Skip_If_Exists_Execution_Environment_Context() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + + var env = new PowertoolsEnvironment(); + env.SetEnvironmentVariable("AWS_EXECUTION_ENV", + $"{Constants.FeatureContextIdentifier}/Logging/AlreadyThere"); // Act - var systemWrapper = new SystemWrapper(env); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(env); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); // Assert - env.Received(1).SetEnvironmentVariable("AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/Logger/{assemblyVersion}"); - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Logging/AlreadyThere", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); + env.SetEnvironmentVariable("AWS_EXECUTION_ENV", null); } [Fact] @@ -1375,23 +1409,22 @@ public void Log_Should_Serialize_DateOnly() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.CamelCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1408,9 +1441,10 @@ public void Log_Should_Serialize_DateOnly() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => - s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}}") + s.Contains( + "\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}") ) ); } @@ -1428,17 +1462,18 @@ public void Log_Should_Serialize_TimeOnly() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.CamelCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper, + Random = randomSampleRate }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1451,20 +1486,21 @@ public void Log_Should_Serialize_TimeOnly() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"time\":\"12:00:00\"}") ) ); } - + [Theory] - [InlineData(true, "WARN", LogLevel.Warning)] - [InlineData(false, "Fatal", LogLevel.Critical)] - [InlineData(false, "NotValid", LogLevel.Critical)] - [InlineData(true, "NotValid", LogLevel.Warning)] - public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bool willLog, string awsLogLevel, LogLevel logLevel) + [InlineData("WARN", LogLevel.Warning)] + [InlineData("Fatal", LogLevel.Critical)] + [InlineData("NotValid", LogLevel.Critical)] + [InlineData("NotValid", LogLevel.Warning)] + public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(string awsLogLevel, + LogLevel logLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -1474,15 +1510,14 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(logLevel.ToString()); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { LoggerOutputCase = LoggerOutputCase.CamelCase }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1494,17 +1529,17 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo // Act logger.LogWarning(message); - + // Assert Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(logLevel, configurations.GetLogLevel()); - Assert.Equal(willLog, systemWrapper.LogMethodCalled); } - + [Theory] - [InlineData(true, "WARN", LogLevel.Warning)] - [InlineData(true, "Fatal", LogLevel.Critical)] - public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, string awsLogLevel, LogLevel logLevel) + [InlineData("WARN", LogLevel.Warning)] + [InlineData("Fatal", LogLevel.Critical)] + public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(string awsLogLevel, + LogLevel logLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -1514,15 +1549,14 @@ public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, strin environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(string.Empty); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { LoggerOutputCase = LoggerOutputCase.CamelCase, }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1534,14 +1568,13 @@ public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, strin // Act logger.LogWarning(message); - + // Assert Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(LogLevel.Information, configurations.GetLogLevel()); //default Assert.Equal(logLevel, configurations.GetLambdaLogLevel()); - Assert.Equal(willLog, systemWrapper.LogMethodCalled); } - + [Fact] public void Log_Should_Show_Warning_When_AWS_Lambda_Log_Level_Enabled() { @@ -1552,49 +1585,53 @@ public void Log_Should_Show_Warning_When_AWS_Lambda_Log_Level_Enabled() environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns("Debug"); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Warn"); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var systemWrapper = new TestLoggerOutput(); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - LoggerOutputCase = LoggerOutputCase.CamelCase + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); 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.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); + 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.ToString()); } - + [Theory] - [InlineData(true,"LogLevel")] - [InlineData(false,"Level")] - public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Level_Enabled_Or_Disabled(bool alcEnabled, string levelProp) + [InlineData(true, "LogLevel")] + [InlineData(false, "Level")] + public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Level_Enabled_Or_Disabled( + bool alcEnabled, string levelProp) { // Arrange var loggerName = Guid.NewGuid().ToString(); - + var environment = Substitute.For(); environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns("Information"); - if(alcEnabled) + if (alcEnabled) environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Info"); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); - var loggerConfiguration = new LoggerConfiguration + var systemWrapper = new TestLoggerOutput(); + var configurations = new PowertoolsConfigurations(environment); + var loggerConfiguration = new PowertoolsLoggerConfiguration { - LoggerOutputCase = LoggerOutputCase.PascalCase + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogOutput = systemWrapper }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1605,10 +1642,9 @@ public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Le logger.LogInformation(message); // Assert - Assert.True(systemWrapper.LogMethodCalled); - Assert.Contains($"\"{levelProp}\":\"Information\"",systemWrapper.LogMethodCalledWithArgument); + Assert.Contains($"\"{levelProp}\":\"Information\"", systemWrapper.ToString()); } - + [Theory] [InlineData(LoggerOutputCase.CamelCase)] [InlineData(LoggerOutputCase.SnakeCase)] @@ -1616,21 +1652,22 @@ public void Log_CamelCase_Outputs_Level_When_AWS_Lambda_Log_Level_Enabled(Logger { // Arrange var loggerName = Guid.NewGuid().ToString(); - + var environment = Substitute.For(); environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(string.Empty); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Info"); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var systemWrapper = new TestLoggerOutput(); + var configurations = new PowertoolsConfigurations(environment); configurations.LoggerOutputCase.Returns(casing.ToString()); - - var loggerConfiguration = new LoggerConfiguration + + var loggerConfiguration = new PowertoolsLoggerConfiguration { - LoggerOutputCase = casing + LoggerOutputCase = casing, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1641,10 +1678,9 @@ public void Log_CamelCase_Outputs_Level_When_AWS_Lambda_Log_Level_Enabled(Logger logger.LogInformation(message); // Assert - Assert.True(systemWrapper.LogMethodCalled); - Assert.Contains("\"level\":\"Information\"",systemWrapper.LogMethodCalledWithArgument); + Assert.Contains("\"level\":\"Information\"", systemWrapper.ToString()); } - + [Theory] [InlineData("TRACE", LogLevel.Trace)] [InlineData("debug", LogLevel.Debug)] @@ -1659,12 +1695,11 @@ public void Should_Map_AWS_Log_Level_And_Default_To_Information(string awsLogLev var environment = Substitute.For(); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); - var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); + var configuration = new PowertoolsConfigurations(environment); // Act var logLvl = configuration.GetLambdaLogLevel(); - + // Assert Assert.Equal(logLevel, logLvl); } @@ -1679,15 +1714,14 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel var environment = Substitute.For(); environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(logLevel.ToString()); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { LoggerOutputCase = LoggerOutputCase.CamelCase }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1703,13 +1737,39 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel // Assert Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(logLevel.ToString(), configurations.LogLevel); - Assert.Equal(willLog, systemWrapper.LogMethodCalled); + } + + [Theory] + [InlineData(true, "on-demand")] + [InlineData(false, "provisioned-concurrency")] + public void Log_Cold_Start(bool willLog, string awsInitType) + { + // Arrange + var logOutput = new TestLoggerOutput(); + Environment.SetEnvironmentVariable("AWS_LAMBDA_INITIALIZATION_TYPE", awsInitType); + var configurations = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + var loggerConfiguration = new PowertoolsLoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = logOutput + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); + var logger = provider.CreateLogger("temp"); + + // Act + logger.LogInformation("Hello"); + + var outPut = logOutput.ToString(); + // Assert + Assert.Contains($"\"coldStart\":{willLog.ToString().ToLower()}", outPut); } public void Dispose() { - PowertoolsLoggingSerializer.ClearOptions(); - LoggingAspect.ResetForTest(); + // Environment.SetEnvironmentVariable("AWS_LAMBDA_INITIALIZATION_TYPE", null); + LambdaLifecycleTracker.Reset(); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index b522963f..b8df5d4f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -30,30 +30,23 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; public class PowertoolsLambdaSerializerTests : IDisposable { + private readonly PowertoolsLoggingSerializer _serializer; + + public PowertoolsLambdaSerializerTests() + { + _serializer = new PowertoolsLoggingSerializer(); + } + #if NET8_0_OR_GREATER [Fact] public void Constructor_ShouldNotThrowException() { // Arrange & Act & Assert var exception = - Record.Exception(() => PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default)); + Record.Exception(() => _serializer.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)] @@ -81,7 +74,7 @@ public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() var serializer = new PowertoolsSourceGeneratorSerializer(); ; - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); + _serializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); var json = "{\"FullName\":\"John\",\"Age\":30}"; var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); @@ -209,7 +202,7 @@ public void Should_Serialize_Unknown_Type_When_Including_Outside_Context() stream.Position = 0; var outputExternalSerializer = new StreamReader(stream).ReadToEnd(); - var outptuMySerializer = PowertoolsLoggingSerializer.Serialize(log, typeof(LogEntry)); + var outptuMySerializer = _serializer.Serialize(log, typeof(LogEntry)); // Assert Assert.Equal( @@ -224,8 +217,7 @@ public void Should_Serialize_Unknown_Type_When_Including_Outside_Context() #endif public void Dispose() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); - PowertoolsLoggingSerializer.ClearOptions(); + } #if NET6_0 @@ -234,7 +226,7 @@ public void Dispose() public void Should_Serialize_Net6() { // Arrange - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + _serializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); var testObject = new APIGatewayProxyRequest { Path = "asda", @@ -250,7 +242,7 @@ public void Should_Serialize_Net6() Message = testObject }; - var outptuMySerializer = PowertoolsLoggingSerializer.Serialize(log, null); + var outptuMySerializer = _serializer.Serialize(log, null); // Assert Assert.Equal( diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs index f8e1cd48..58b42e3f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -1,25 +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 - * permissions and limitations under the License. - */ - using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.IO; 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.Common.Utils; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Internal.Converters; @@ -31,19 +18,21 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; public class PowertoolsLoggingSerializerTests : IDisposable { + private readonly PowertoolsLoggingSerializer _serializer; public PowertoolsLoggingSerializerTests() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + _serializer = new PowertoolsLoggingSerializer(); + _serializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); #if NET8_0_OR_GREATER - PowertoolsLoggingSerializer.ClearContext(); + ClearContext(); #endif } - + [Fact] public void SerializerOptions_ShouldNotBeNull() { - var options = PowertoolsLoggingSerializer.GetSerializerOptions(); + var options = _serializer.GetSerializerOptions(); Assert.NotNull(options); } @@ -51,9 +40,9 @@ public void SerializerOptions_ShouldNotBeNull() public void SerializerOptions_ShouldHaveCorrectDefaultSettings() { RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); - - var options = PowertoolsLoggingSerializer.GetSerializerOptions(); - + + var options = _serializer.GetSerializerOptions(); + Assert.Collection(options.Converters, converter => Assert.IsType(converter), converter => Assert.IsType(converter), @@ -71,17 +60,17 @@ public void SerializerOptions_ShouldHaveCorrectDefaultSettings() #if NET8_0_OR_GREATER Assert.Collection(options.TypeInfoResolverChain, - resolver => Assert.IsType(resolver)); + resolver => Assert.IsType(resolver)); #endif } - + [Fact] public void SerializerOptions_ShouldHaveCorrectDefaultSettings_WhenDynamic() { RuntimeFeatureWrapper.SetIsDynamicCodeSupported(true); - - var options = PowertoolsLoggingSerializer.GetSerializerOptions(); - + + var options = _serializer.GetSerializerOptions(); + Assert.Collection(options.Converters, converter => Assert.IsType(converter), converter => Assert.IsType(converter), @@ -132,7 +121,7 @@ public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedNull() public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedSameCase() { var originalJson = SerializeTestObject(LoggerOutputCase.SnakeCase); - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + _serializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); var newJson = SerializeTestObject(LoggerOutputCase.SnakeCase); Assert.Equal(originalJson, newJson); } @@ -140,7 +129,7 @@ public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedSameCase() [Fact] public void Serialize_ShouldHandleNestedObjects() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + _serializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); var testObject = new LogEntry { @@ -151,7 +140,7 @@ public void Serialize_ShouldHandleNestedObjects() } }; - var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); Assert.Contains("\"cold_start\":true", json); Assert.Contains("\"nested_object\":{\"property_name\":\"Value\"}", json); } @@ -163,7 +152,7 @@ public void Serialize_ShouldHandleEnumValues() { Level = LogLevel.Error }; - var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); Assert.Contains("\"level\":\"Error\"", json); } @@ -177,50 +166,409 @@ public void Serialize_UnknownType_ThrowsInvalidOperationException() RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); // Act & Assert var exception = Assert.Throws(() => - PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType))); + _serializer.Serialize(unknownObject, typeof(UnknownType))); Assert.Contains("is not known to the serializer", exception.Message); Assert.Contains(typeof(UnknownType).ToString(), exception.Message); } - + [Fact] public void Serialize_UnknownType_Should_Not_Throw_InvalidOperationException_When_Dynamic() { // Arrange - var unknownObject = new UnknownType{ SomeProperty = "Hello"}; + var unknownObject = new UnknownType { SomeProperty = "Hello" }; RuntimeFeatureWrapper.SetIsDynamicCodeSupported(true); // Act & Assert var expected = - PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType)); + _serializer.Serialize(unknownObject, typeof(UnknownType)); Assert.Equal("{\"some_property\":\"Hello\"}", expected); } + [Fact] + public void AddSerializerContext_ShouldUpdateTypeInfoResolver() + { + // Arrange + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); + var testContext = new TestSerializerContext(new JsonSerializerOptions()); + + // Get the initial resolver + var beforeOptions = _serializer.GetSerializerOptions(); + var beforeResolver = beforeOptions.TypeInfoResolver; + + // Act + _serializer.AddSerializerContext(testContext); + + // Get the updated resolver + var afterOptions = _serializer.GetSerializerOptions(); + var afterResolver = afterOptions.TypeInfoResolver; + + // Assert - adding a context should create a new resolver + Assert.NotSame(beforeResolver, afterResolver); + Assert.IsType(afterResolver); + } + private class UnknownType { public string SomeProperty { get; set; } } + + private class TestSerializerContext : JsonSerializerContext + { + private readonly JsonSerializerOptions _options; + + public TestSerializerContext(JsonSerializerOptions options) : base(options) + { + _options = options; + } + + public override JsonTypeInfo? GetTypeInfo(Type type) + { + return null; // For testing purposes only + } + + protected override JsonSerializerOptions? GeneratedSerializerOptions => _options; + } + + private void ClearContext() + { + // Create a new serializer to clear any existing contexts + _serializer.SetOptions(new JsonSerializerOptions()); + } #endif private string SerializeTestObject(LoggerOutputCase? outputCase) { if (outputCase.HasValue) { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase.Value); + _serializer.ConfigureNamingPolicy(outputCase.Value); } LogEntry testObject = new LogEntry { ColdStart = true }; - return JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + return JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + } + + [Fact] + public void ByteArrayConverter_ShouldProduceBase64EncodedString() + { + // Arrange + var testObject = new { BinaryData = new byte[] { 1, 2, 3, 4, 5 } }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"binary_data\":\"AQIDBAU=\"", json); + } + + [Fact] + public void ExceptionConverter_ShouldSerializeExceptionDetails() + { + // Arrange + var exception = new InvalidOperationException("Test error message", new Exception("Inner exception")); + var testObject = new { Error = exception }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Equal("{\"error\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Test error message\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Inner exception\"}}}", json); + } + + [Fact] + public void MemoryStreamConverter_ShouldConvertToBase64() + { + // Arrange + var bytes = new byte[] { 10, 20, 30, 40, 50 }; + var memoryStream = new MemoryStream(bytes); + var testObject = new { Stream = memoryStream }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"stream\":\"ChQeKDI=\"", json); + } + + [Fact] + public void ConstantClassConverter_ShouldSerializeToString() + { + // Arrange + var testObject = new { Level = LogLevel.Warning }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"level\":\"Warning\"", json); + } + +#if NET6_0_OR_GREATER + [Fact] + public void DateOnlyConverter_ShouldSerializeToIsoDate() + { + // Arrange + var date = new DateOnly(2023, 10, 15); + var testObject = new { Date = date }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"date\":\"2023-10-15\"", json); + } + + [Fact] + public void TimeOnlyConverter_ShouldSerializeToIsoTime() + { + // Arrange + var time = new TimeOnly(13, 45, 30); + var testObject = new { Time = time }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"time\":\"13:45:30\"", json); + } +#endif + + [Fact] + public void LogLevelJsonConverter_ShouldSerializeAllLogLevels() + { + // Arrange + var levels = new Dictionary + { + { "trace", LogLevel.Trace }, + { "debug", LogLevel.Debug }, + { "info", LogLevel.Information }, + { "warning", LogLevel.Warning }, + { "error", LogLevel.Error }, + { "critical", LogLevel.Critical } + }; + + // Act + var json = JsonSerializer.Serialize(levels, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"trace\":\"Trace\"", json); + Assert.Contains("\"debug\":\"Debug\"", json); + Assert.Contains("\"info\":\"Information\"", json); + Assert.Contains("\"warning\":\"Warning\"", json); + Assert.Contains("\"error\":\"Error\"", json); + Assert.Contains("\"critical\":\"Critical\"", json); + } + + [Fact] + public void Serialize_ComplexObjectWithMultipleConverters_ShouldConvertAllProperties() + { + // Arrange + var testObject = new ComplexTestObject + { + BinaryData = new byte[] { 1, 2, 3 }, + Exception = new ArgumentException("Test argument"), + Stream = new MemoryStream(new byte[] { 4, 5, 6 }), + Level = LogLevel.Information, +#if NET6_0_OR_GREATER + Date = new DateOnly(2023, 1, 15), + Time = new TimeOnly(14, 30, 0), +#endif + }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"binary_data\":\"AQID\"", json); + Assert.Contains("\"exception\":{\"type\":\"System.ArgumentException\"", json); + Assert.Contains("\"stream\":\"BAUG\"", json); + Assert.Contains("\"level\":\"Information\"", json); +#if NET6_0_OR_GREATER + Assert.Contains("\"date\":\"2023-01-15\"", json); + Assert.Contains("\"time\":\"14:30:00\"", json); +#endif + } + + private class ComplexTestObject + { + public byte[] BinaryData { get; set; } + public Exception Exception { get; set; } + public MemoryStream Stream { get; set; } + public LogLevel Level { get; set; } +#if NET6_0_OR_GREATER + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } +#endif } + + [Fact] + public void ConfigureNamingPolicy_WhenChanged_RebuildsOptions() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + + // Force initialization of _jsonOptions + _ = serializer.GetSerializerOptions(); + + // Act + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy); + Assert.Equal(JsonNamingPolicy.CamelCase, options.DictionaryKeyPolicy); + } + + [Fact] + public void ConfigureNamingPolicy_WhenAlreadySet_DoesNothing() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + + // Get the initial options + var initialOptions = serializer.GetSerializerOptions(); + + // Act - set the same case again + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + var newOptions = serializer.GetSerializerOptions(); + + // Assert - should be the same instance + Assert.Same(initialOptions, newOptions); + } + + [Fact] + public void Serialize_WithValidObject_ReturnsJsonString() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + var testObj = new TestClass { Name = "Test", Value = 123 }; + + // Act + var json = serializer.Serialize(testObj, typeof(TestClass)); + + // Assert + Assert.Contains("\"name\"", json); + Assert.Contains("\"value\"", json); + Assert.Contains("123", json); + Assert.Contains("Test", json); + } + +#if NET8_0_OR_GREATER + + [Fact] + public void SetOptions_WithTypeInfoResolver_SetsCustomResolver() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + + // Explicitly disable dynamic code - important to set before creating options + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); + + var context = new TestJsonContext(new JsonSerializerOptions()); + var options = new JsonSerializerOptions + { + TypeInfoResolver = context + }; + + // Act + serializer.SetOptions(options); + var serializerOptions = serializer.GetSerializerOptions(); + + // Assert - options are properly configured + Assert.NotNull(serializerOptions.TypeInfoResolver); + } +#endif + + [Fact] + public void SetOutputCase_CamelCase_SetsPoliciesCorrectly() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + + // Act + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy); + Assert.Equal(JsonNamingPolicy.CamelCase, options.DictionaryKeyPolicy); + } + + [Fact] + public void SetOutputCase_PascalCase_SetsPoliciesCorrectly() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); + + // Act + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.IsType(options.PropertyNamingPolicy); + Assert.IsType(options.DictionaryKeyPolicy); + } + + [Fact] + public void SetOutputCase_SnakeCase_SetsPoliciesCorrectly() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + + // Act + var options = serializer.GetSerializerOptions(); + +#if NET8_0_OR_GREATER + // Assert - in .NET 8 we use built-in SnakeCaseLower + Assert.Equal(JsonNamingPolicy.SnakeCaseLower, options.PropertyNamingPolicy); + Assert.Equal(JsonNamingPolicy.SnakeCaseLower, options.DictionaryKeyPolicy); +#else + // Assert - in earlier versions, we use custom SnakeCaseNamingPolicy + Assert.IsType(options.PropertyNamingPolicy); + Assert.IsType(options.DictionaryKeyPolicy); +#endif + } + + [Fact] + public void GetSerializerOptions_AddsAllConverters() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + + // Act + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.Contains(options.Converters, c => c is ByteArrayConverter); + Assert.Contains(options.Converters, c => c is ExceptionConverter); + Assert.Contains(options.Converters, c => c is MemoryStreamConverter); + Assert.Contains(options.Converters, c => c is ConstantClassConverter); + Assert.Contains(options.Converters, c => c is DateOnlyConverter); + Assert.Contains(options.Converters, c => c is TimeOnlyConverter); +#if NET8_0_OR_GREATER || NET6_0 + Assert.Contains(options.Converters, c => c is LogLevelJsonConverter); +#endif + } + + // Test class for serialization + private class TestClass + { + public string Name { get; set; } + public int Value { get; set; } + } + + + public void Dispose() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); #if NET8_0_OR_GREATER - PowertoolsLoggingSerializer.ClearContext(); + ClearContext(); #endif - PowertoolsLoggingSerializer.ClearOptions(); + _serializer.SetOptions(null); RuntimeFeatureWrapper.Reset(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs new file mode 100644 index 00000000..b7e975e4 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs @@ -0,0 +1,181 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging.Internal.Converters; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +public class ByteArrayConverterTests +{ + private readonly JsonSerializerOptions _options; + + public ByteArrayConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ByteArrayConverter()); + } + + [Fact] + public void Write_WhenByteArrayIsNull_WritesNullValue() + { + // Arrange + var testObject = new TestClass { Data = null }; + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + + // Assert + Assert.Contains("\"data\":null", json); + } + + [Fact] + public void Write_WithByteArray_WritesBase64String() + { + // Arrange + byte[] testData = { 1, 2, 3, 4, 5 }; + var testObject = new TestClass { Data = testData }; + var expectedBase64 = Convert.ToBase64String(testData); + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + + // Assert + Assert.Contains($"\"data\":\"{expectedBase64}\"", json); + } + + [Fact] + public void Read_WithBase64String_ReturnsByteArray() + { + // Arrange + byte[] expectedData = { 1, 2, 3, 4, 5 }; + var base64 = Convert.ToBase64String(expectedData); + var json = $"{{\"data\":\"{base64}\"}}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(expectedData, result.Data); + } + + [Fact] + public void Read_WithInvalidType_ThrowsJsonException() + { + // Arrange + var json = "{\"data\":123}"; + + // Act & Assert + Assert.Throws(() => + JsonSerializer.Deserialize(json, _options)); + } + + [Fact] + public void Read_WithEmptyString_ReturnsEmptyByteArray() + { + // Arrange + var json = "{\"data\":\"\"}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result.Data); + Assert.Empty(result.Data); + } + + [Fact] + public void WriteAndRead_RoundTrip_PreservesData() + { + // Arrange + byte[] originalData = Encoding.UTF8.GetBytes("Test data with special chars: !@#$%^&*()"); + var testObject = new TestClass { Data = originalData }; + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + var deserializedObject = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(originalData, deserializedObject.Data); + } + + private class TestClass + { + [JsonPropertyName("data")] public byte[] Data { get; set; } + } + + [Fact] + public void ByteArrayConverter_Write_ShouldHandleNullValue() + { + // Arrange + var converter = new ByteArrayConverter(); + var options = new JsonSerializerOptions(); + var testObject = new { Data = (byte[])null }; + + // Act + var json = JsonSerializer.Serialize(testObject, options); + + // Assert + Assert.Contains("\"Data\":null", json); + } + + [Fact] + public void ByteArrayConverter_Read_ShouldHandleNullToken() + { + // Arrange + var converter = new ByteArrayConverter(); + var json = "{\"Data\":null}"; + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); + + // Act + var result = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.Null(result.Data); + } + + [Fact] + public void ByteArrayConverter_Read_ShouldHandleStringToken() + { + // Arrange + var converter = new ByteArrayConverter(); + var expectedBytes = new byte[] { 1, 2, 3, 4 }; + var base64String = Convert.ToBase64String(expectedBytes); + var json = $"{{\"Data\":\"{base64String}\"}}"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); + + // Act + var result = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(result.Data); + Assert.Equal(expectedBytes, result.Data); + } + + [Fact] + public void ByteArrayConverter_Read_ShouldThrowOnInvalidToken() + { + // Arrange + var converter = new ByteArrayConverter(); + var json = "{\"Data\":123}"; // Number instead of string + + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); + + // Act & Assert + var ex = Assert.Throws(() => + JsonSerializer.Deserialize(json, options)); + + Assert.Contains("Expected string value for byte array", ex.Message); + } + +// Helper class for testing byte array deserialization + private class TestByteArrayClass + { + public byte[] Data { get; set; } + } +} \ 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 index 6a719d1b..d10c4a0e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs @@ -15,10 +15,7 @@ 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; @@ -31,12 +28,8 @@ public class PowertoolsConfigurationExtensionsTests : IDisposable [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); + var result = input.ToCase(outputCase); // Assert Assert.Equal(expected, result); @@ -66,7 +59,7 @@ public void ConvertToOutputCase_ShouldConvertCorrectly(LoggerOutputCase outputCa public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) { // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToSnakeCase", input); + var result = input.ToSnake(); // Assert Assert.Equal(expected, result); @@ -97,7 +90,7 @@ public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) { // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToPascalCase", input); + var result = input.ToPascal(); // Assert Assert.Equal(expected, result); @@ -135,7 +128,7 @@ public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) { // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToCamelCase", input); + var result = input.ToCamel(); // Assert Assert.Equal(expected, result); @@ -144,7 +137,6 @@ public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) public void Dispose() { LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index 46e76a2c..f35753f8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -6,13 +6,14 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using AWS.Lambda.Powertools.Logging.Serializers; +using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; public class PowertoolsLoggerHelpersTests : IDisposable -{ +{ [Fact] public void ObjectToDictionary_AnonymousObjectWithSimpleProperties_ReturnsDictionary() { @@ -73,9 +74,12 @@ public void ObjectToDictionary_NullObject_Return_New_Dictionary() [Fact] public void Should_Log_With_Anonymous() { - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act & Assert Logger.AppendKey("newKey", new { @@ -93,9 +97,12 @@ public void Should_Log_With_Anonymous() [Fact] public void Should_Log_With_Complex_Anonymous() { - var consoleOut = Substitute.For(); - SystemWrapper.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act & Assert Logger.AppendKey("newKey", new { @@ -201,8 +208,27 @@ public void ObjectToDictionary_ObjectWithAllNullProperties_ReturnsEmptyDictionar public void Dispose() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.Default); - PowertoolsLoggingSerializer.ClearOptions(); + ResetAllState(); + } + + private static void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs deleted file mode 100644 index 1ab2b94e..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs +++ /dev/null @@ -1,68 +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.IO; -using AWS.Lambda.Powertools.Common; - -namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; - -public class SystemWrapperMock : ISystemWrapper -{ - private readonly IPowertoolsEnvironment _powertoolsEnvironment; - public bool LogMethodCalled { get; private set; } - public string LogMethodCalledWithArgument { get; private set; } - - public SystemWrapperMock(IPowertoolsEnvironment powertoolsEnvironment) - { - _powertoolsEnvironment = powertoolsEnvironment; - } - - public string GetEnvironmentVariable(string variable) - { - return _powertoolsEnvironment.GetEnvironmentVariable(variable); - } - - public void Log(string value) - { - LogMethodCalledWithArgument = value; - LogMethodCalled = true; - } - - public void LogLine(string value) - { - LogMethodCalledWithArgument = value; - LogMethodCalled = true; - } - - - public double GetRandom() - { - return 0.7; - } - - public void SetEnvironmentVariable(string variable, string value) - { - throw new System.NotImplementedException(); - } - - public void SetExecutionEnvironment(T type) - { - } - - public void SetOut(TextWriter writeTo) - { - - } -} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs index 8a2b3c7f..90a3547a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs @@ -13,7 +13,7 @@ public void WhenClearAllDimensions_NoDimensionsInOutput() { // Arrange var consoleOut = new StringWriter(); - SystemWrapper.SetOut(consoleOut); + ConsoleWrapper.SetOut(consoleOut); // Act var handler = new FunctionHandler(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs index cba56806..7d0a3e4a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -35,7 +35,7 @@ public EmfValidationTests() { _handler = new FunctionHandler(); _consoleOut = new CustomConsoleWriter(); - SystemWrapper.SetOut(_consoleOut); + ConsoleWrapper.SetOut(_consoleOut); } [Trait("Category", value: "SchemaValidation")] diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs index d9369bc4..7b053739 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -34,7 +34,7 @@ public FunctionHandlerTests() { _handler = new FunctionHandler(); _consoleOut = new CustomConsoleWriter(); - SystemWrapper.SetOut(_consoleOut); + ConsoleWrapper.SetOut(_consoleOut); } [Fact] @@ -417,5 +417,6 @@ public void Dispose() { Metrics.ResetForTest(); MetricsAspect.ResetForTest(); + ConsoleWrapper.ResetForTest(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs index f9cbb9e5..adcee372 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs @@ -17,23 +17,15 @@ public void Metrics_Set_Execution_Environment_Context() { // Arrange Metrics.ResetForTest(); - var assemblyName = "AWS.Lambda.Powertools.Metrics"; - var assemblyVersion = "1.0.0"; + var env = new PowertoolsEnvironment(); - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var conf = new PowertoolsConfigurations(env); _ = new Metrics(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Metrics/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Metrics/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs index 6a024334..5318b7b3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs @@ -31,27 +31,17 @@ public class XRayRecorderTests public void Tracing_Set_Execution_Environment_Context() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.Tracing"; - var assemblyVersion = "1.0.0"; + var env = new PowertoolsEnvironment(); - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var conf = new PowertoolsConfigurations(env); var awsXray = Substitute.For(); // Act var xRayRecorder = new XRayRecorder(awsXray, conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Tracing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable( - "AWS_EXECUTION_ENV" - ); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Tracing/1.0.0", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(xRayRecorder); } diff --git a/libraries/tests/Directory.Build.props b/libraries/tests/Directory.Build.props index d662fc45..887e46d9 100644 --- a/libraries/tests/Directory.Build.props +++ b/libraries/tests/Directory.Build.props @@ -4,6 +4,6 @@ false - + diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props index 516a0e93..88751caf 100644 --- a/libraries/tests/Directory.Packages.props +++ b/libraries/tests/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -13,13 +13,13 @@ - + - + \ No newline at end of file diff --git a/libraries/tests/e2e/InfraShared/FunctionConstruct.cs b/libraries/tests/e2e/InfraShared/FunctionConstruct.cs index 6dfeb84b..c3bb7d9e 100644 --- a/libraries/tests/e2e/InfraShared/FunctionConstruct.cs +++ b/libraries/tests/e2e/InfraShared/FunctionConstruct.cs @@ -27,6 +27,7 @@ public FunctionConstruct(Construct scope, string id, FunctionConstructProps prop Tracing = Tracing.ACTIVE, Timeout = Duration.Seconds(10), Environment = props.Environment, + LoggingFormat = LoggingFormat.TEXT, Code = Code.FromCustomCommand(distPath, [ command diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj new file mode 100644 index 00000000..8655735e --- /dev/null +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj @@ -0,0 +1,33 @@ + + + Exe + net8.0 + enable + enable + Lambda + + true + + true + + true + + partial + + + + + + + + + + TestHelper.cs + + + + + + \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs new file mode 100644 index 00000000..16234c5b --- /dev/null +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Logging.Serializers; +using Helpers; + +namespace AOT_Function; + +public static class Function +{ + private static async Task Main() + { + Logger.Configure(logger => + { + logger.Service = "TestService"; + logger.LoggerOutputCase = LoggerOutputCase.PascalCase; + logger.JsonOptions = new JsonSerializerOptions + { + TypeInfoResolver = LambdaFunctionJsonSerializerContext.Default + }; + }); + + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public static APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Processing request started"); + + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + Logger.AppendKeys(lookupInfo); + Logger.AppendKeys(customKeys); + + Logger.LogWarning("Warn with additional keys"); + + Logger.RemoveKeys("test1", "test2"); + + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(apigwProxyEvent), + new Exception("Very important nested inner exception message"))); + Logger.LogError(error, "Oops something went wrong"); + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } +} + +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(APIGatewayProxyResponse))] +public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext +{ + +} \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/aws-lambda-tools-defaults.json b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..be3c7ec1 --- /dev/null +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/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-Function", + "msbuild-parameters": "--self-contained true" +} \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj index b2636d6b..8655735e 100644 --- a/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj @@ -17,7 +17,7 @@ partial - + diff --git a/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs b/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs index 8a4d3a8b..958f36ff 100644 --- a/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs +++ b/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs @@ -2,24 +2,191 @@ using Amazon.Lambda.Core; using AWS.Lambda.Powertools.Logging; using Helpers; +using Microsoft.Extensions.Logging; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -namespace Function; - -public class Function +namespace Function { - [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, Service = "TestService", + public class Function + { + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, Service = "TestService", CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] - public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + TestHelper.TestMethod(apigwProxyEvent); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace StaticConfiguration +{ + public class Function + { + public Function() + { + Logger.Configure(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + TestHelper.TestMethod(apigwProxyEvent); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace StaticILoggerConfiguration +{ + public class Function { - TestHelper.TestMethod(apigwProxyEvent); + public Function() + { + LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }); + } - return new APIGatewayProxyResponse() + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) { - StatusCode = 200, - Body = apigwProxyEvent.Body.ToUpper() - }; + TestHelper.TestMethod(apigwProxyEvent); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace ILoggerConfiguration +{ + public class Function + { + private readonly ILogger _logger; + + public Function() + { + _logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Processing request started"); + + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + _logger.AppendKeys(lookupInfo); + _logger.AppendKeys(customKeys); + + _logger.LogWarning("Warn with additional keys"); + + _logger.RemoveKeys("test1", "test2"); + + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(apigwProxyEvent), + new Exception("Very important nested inner exception message"))); + _logger.LogError(error, "Oops something went wrong"); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace ILoggerBuilder +{ + public class Function + { + private readonly ILogger _logger; + + public Function() + { + _logger = new PowertoolsLoggerBuilder() + .WithService("TestService") + .WithOutputCase(LoggerOutputCase.PascalCase) + .Build(); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Processing request started"); + + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + _logger.AppendKeys(lookupInfo); + _logger.AppendKeys(customKeys); + + _logger.LogWarning("Warn with additional keys"); + + _logger.RemoveKeys("test1", "test2"); + + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(apigwProxyEvent), + new Exception("Very important nested inner exception message"))); + _logger.LogError(error, "Oops something went wrong"); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } } } \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs b/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs index ca3a857a..b80be012 100644 --- a/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs +++ b/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs @@ -5,6 +5,7 @@ using Amazon.Lambda.Model; using TestUtils; using Xunit.Abstractions; +using Environment = Amazon.Lambda.Model.Environment; namespace Function.Tests; @@ -22,10 +23,21 @@ public FunctionTests(ITestOutputHelper testOutputHelper) [Trait("Category", "AOT")] [Theory] - [InlineData("E2ETestLambda_X64_AOT_NET8_logging")] - [InlineData("E2ETestLambda_ARM_AOT_NET8_logging")] + [InlineData("E2ETestLambda_X64_AOT_NET8_logging_AOT-Function")] + [InlineData("E2ETestLambda_ARM_AOT_NET8_logging_AOT-Function")] public async Task AotFunctionTest(string functionName) { + // await ResetFunction(functionName); + await TestFunction(functionName); + } + + [Trait("Category", "AOT")] + [Theory] + [InlineData("E2ETestLambda_X64_AOT_NET8_logging_AOT-Function-ILogger")] + [InlineData("E2ETestLambda_ARM_AOT_NET8_logging_AOT-Function-ILogger")] + public async Task AotILoggerFunctionTest(string functionName) + { + // await ResetFunction(functionName); await TestFunction(functionName); } @@ -36,6 +48,51 @@ public async Task AotFunctionTest(string functionName) [InlineData("E2ETestLambda_ARM_NET8_logging")] public async Task FunctionTest(string functionName) { + await UpdateFunctionHandler(functionName, "Function::Function.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task StaticConfigurationFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::StaticConfiguration.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task StaticILoggerConfigurationFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::StaticILoggerConfiguration.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task ILoggerConfigurationFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::ILoggerConfiguration.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task ILoggerBuilderFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::ILoggerBuilder.Function::FunctionHandler"); await TestFunction(functionName); } @@ -243,4 +300,48 @@ private void AssertExceptionLog(string functionName, bool isColdStart, string ou Assert.False(root.TryGetProperty("Test1", out JsonElement _)); Assert.False(root.TryGetProperty("Test2", out JsonElement _)); } + + private async Task UpdateFunctionHandler(string functionName, string handler) + { + var updateRequest = new UpdateFunctionConfigurationRequest + { + FunctionName = functionName, + Handler = handler + }; + + var updateResponse = await _lambdaClient.UpdateFunctionConfigurationAsync(updateRequest); + + if (updateResponse.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + Console.WriteLine($"Successfully updated the handler for function {functionName} to {handler}"); + } + else + { + Assert.Fail( + $"Failed to update the handler for function {functionName}. Status code: {updateResponse.HttpStatusCode}"); + } + + //wait a few seconds for the changes to take effect + await Task.Delay(1000); + } + + private async Task ResetFunction(string functionName) + { + var updateRequest = new UpdateFunctionConfigurationRequest + { + FunctionName = functionName, + Environment = new Environment + { + Variables = + { + {"Updated", DateTime.UtcNow.ToString("G")} + } + } + }; + + await _lambdaClient.UpdateFunctionConfigurationAsync(updateRequest); + + //wait a few seconds for the changes to take effect + await Task.Delay(1000); + } } \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj index 85b41ba2..111b59c2 100644 --- a/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj +++ b/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj @@ -17,7 +17,7 @@ partial - + diff --git a/libraries/tests/e2e/infra-aot/CoreAotStack.cs b/libraries/tests/e2e/infra-aot/CoreAotStack.cs index 4387892c..282fce25 100644 --- a/libraries/tests/e2e/infra-aot/CoreAotStack.cs +++ b/libraries/tests/e2e/infra-aot/CoreAotStack.cs @@ -6,6 +6,18 @@ namespace InfraAot; +public class ConstructArgs +{ + public Construct Scope { get; set; } + public string Id { get; set; } + public Runtime Runtime { get; set; } + public Architecture Architecture { get; set; } + public string Name { get; set; } + public string SourcePath { get; set; } + public string DistPath { get; set; } + public string Handler { get; set; } +} + public class CoreAotStack : Stack { private readonly Architecture _architecture; @@ -15,31 +27,42 @@ internal CoreAotStack(Construct scope, string id, PowertoolsDefaultStackProps pr if (props != null) _architecture = props.ArchitectureString == "arm64" ? Architecture.ARM_64 : Architecture.X86_64; CreateFunctionConstructs("logging"); + CreateFunctionConstructs("logging", "AOT-Function-ILogger"); CreateFunctionConstructs("metrics"); CreateFunctionConstructs("tracing"); } - private void CreateFunctionConstructs(string utility) + private void CreateFunctionConstructs(string utility, string function = "AOT-Function" ) { - var baseAotPath = $"../functions/core/{utility}/AOT-Function/src/AOT-Function"; - var distAotPath = $"../functions/core/{utility}/AOT-Function/dist"; + var baseAotPath = $"../functions/core/{utility}/{function}/src/{function}"; + var distAotPath = $"../functions/core/{utility}/{function}/dist/{function}"; var arch = _architecture == Architecture.X86_64 ? "X64" : "ARM"; - CreateFunctionConstruct(this, $"{utility}_{arch}_aot_net8", Runtime.DOTNET_8, _architecture, - $"E2ETestLambda_{arch}_AOT_NET8_{utility}", baseAotPath, distAotPath); + var construct = new ConstructArgs + { + Scope = this, + Id = $"{utility}_{arch}_aot_net8_{function}", + Runtime = Runtime.DOTNET_8, + Architecture = _architecture, + Name = $"E2ETestLambda_{arch}_AOT_NET8_{utility}_{function}", + SourcePath = baseAotPath, + DistPath = distAotPath, + Handler = $"{function}.Function::AWS.Lambda.Powertools.{utility}.{function}.Function.FunctionHandler" + }; + + CreateFunctionConstruct(construct); } - private void CreateFunctionConstruct(Construct scope, string id, Runtime runtime, Architecture architecture, - string name, string sourcePath, string distPath) + private void CreateFunctionConstruct(ConstructArgs constructArgs) { - _ = new FunctionConstruct(scope, id, new FunctionConstructProps + _ = new FunctionConstruct(constructArgs.Scope, constructArgs.Id, new FunctionConstructProps { - Runtime = runtime, - Architecture = architecture, - Name = name, - Handler = "AOT-Function", - SourcePath = sourcePath, - DistPath = distPath, + Runtime = constructArgs.Runtime, + Architecture = constructArgs.Architecture, + Name = constructArgs.Name, + Handler = constructArgs.Handler, + SourcePath = constructArgs.SourcePath, + DistPath = constructArgs.DistPath, IsAot = true }); } diff --git a/libraries/tests/e2e/infra/CoreStack.cs b/libraries/tests/e2e/infra/CoreStack.cs index d77c725a..15f3fd6d 100644 --- a/libraries/tests/e2e/infra/CoreStack.cs +++ b/libraries/tests/e2e/infra/CoreStack.cs @@ -6,6 +6,28 @@ namespace Infra { + public class ConstructArgs + { + public ConstructArgs(Construct scope, string id, Runtime runtime, Architecture architecture, string name, string sourcePath, string distPath) + { + Scope = scope; + Id = id; + Runtime = runtime; + Architecture = architecture; + Name = name; + SourcePath = sourcePath; + DistPath = distPath; + } + + public Construct Scope { get; private set; } + public string Id { get; private set; } + public Runtime Runtime { get; private set; } + public Architecture Architecture { get; private set; } + public string Name { get; private set; } + public string SourcePath { get; private set; } + public string DistPath { get; private set; } + } + public class CoreStack : Stack { internal CoreStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) @@ -20,27 +42,22 @@ private void CreateFunctionConstructs(string utility) var basePath = $"../functions/core/{utility}/Function/src/Function"; var distPath = $"../functions/core/{utility}/Function/dist"; - CreateFunctionConstruct(this, $"{utility}_X64_net8", Runtime.DOTNET_8, Architecture.X86_64, - $"E2ETestLambda_X64_NET8_{utility}", basePath, distPath); - CreateFunctionConstruct(this, $"{utility}_arm_net8", Runtime.DOTNET_8, Architecture.ARM_64, - $"E2ETestLambda_ARM_NET8_{utility}", basePath, distPath); - CreateFunctionConstruct(this, $"{utility}_X64_net6", Runtime.DOTNET_6, Architecture.X86_64, - $"E2ETestLambda_X64_NET6_{utility}", basePath, distPath); - CreateFunctionConstruct(this, $"{utility}_arm_net6", Runtime.DOTNET_6, Architecture.ARM_64, - $"E2ETestLambda_ARM_NET6_{utility}", basePath, distPath); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_X64_net8", Runtime.DOTNET_8, Architecture.X86_64, $"E2ETestLambda_X64_NET8_{utility}", basePath, distPath)); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_arm_net8", Runtime.DOTNET_8, Architecture.ARM_64, $"E2ETestLambda_ARM_NET8_{utility}", basePath, distPath)); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_X64_net6", Runtime.DOTNET_6, Architecture.X86_64, $"E2ETestLambda_X64_NET6_{utility}", basePath, distPath)); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_arm_net6", Runtime.DOTNET_6, Architecture.ARM_64, $"E2ETestLambda_ARM_NET6_{utility}", basePath, distPath)); } - private void CreateFunctionConstruct(Construct scope, string id, Runtime runtime, Architecture architecture, - string name, string sourcePath, string distPath) + private void CreateFunctionConstruct(ConstructArgs constructArgs) { - _ = new FunctionConstruct(scope, id, new FunctionConstructProps + _ = new FunctionConstruct(constructArgs.Scope, constructArgs.Id, new FunctionConstructProps { - Runtime = runtime, - Architecture = architecture, - Name = name, + Runtime = constructArgs.Runtime, + Architecture = constructArgs.Architecture, + Name = constructArgs.Name, Handler = "Function::Function.Function::FunctionHandler", - SourcePath = sourcePath, - DistPath = distPath, + SourcePath = constructArgs.SourcePath, + DistPath = constructArgs.DistPath, }); } } diff --git a/mkdocs.yml b/mkdocs.yml index 24f86cf6..bd8426e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,9 @@ nav: - We Made This (Community): we_made_this.md - Workshop 🆕: https://s12d.com/powertools-for-aws-lambda-workshop" target="_blank - Core utilities: - - core/logging.md + - Logging: + - core/logging.md + - core/logging-v2.md - Metrics: - core/metrics.md - core/metrics-v2.md diff --git a/version.json b/version.json index 9418e41b..ebe899b4 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "Core": { - "Logging": "1.7.0", + "Logging": "2.0.0-preview.1", "Metrics": "2.0.1", "Tracing": "1.6.2", "Metrics.AspNetCore": "0.1.0"