diff --git a/docs/core/logging.md b/docs/core/logging.md index f0fba760a..5306c55df 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -338,73 +338,6 @@ Your logs will always include the following keys in your structured logging: | **xray_trace_id** | String | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when [Tracing is enabled](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html){target="_blank"} | | **error** | Map | `{ "name": "InvalidAmountException", "message": "Amount must be superior to 0", "stack": "at..." }` | Eventual exception (e.g. when doing `logger.error("Error", new InvalidAmountException("Amount must be superior to 0"));`) | -### Log messages as JSON -By default, `message` is logged as a `String` (e.g `"message": "The message"`). When logging JSON content, -you may want to avoid the escaped String (`"message:"{\"key\":\"value\"}"`) for better readability. -You can use `LoggingUtils.logMessagesAsJson(true)` to enable this programmatically. - -=== "PaymentFunction.java" - - ```java hl_lines="14 15 17-20" - import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; - import software.amazon.lambda.powertools.logging.LoggingUtils; - import software.amazon.lambda.powertools.utilities.JsonConfig; - // ... other imports - - public class PaymentFunction implements RequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); - - @Logging - public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - Order order = extractDataFrom(input).as(Order.class); - - // logged as a String - LOGGER.debug("{}", JsonConfig.get().getObjectMapper().writeValueAsString(order)); - - // Logged as JSON - LoggingUtils.logMessagesAsJson(true); - LOGGER.debug("{}", JsonConfig.get().getObjectMapper().writeValueAsString(order)); - LoggingUtils.logMessagesAsJson(false); - - // ... - } - } - ``` - -=== "Order.java" - - ```java - public class Order { - private String id; - private Date date; - private Double amount; - } - ``` - -=== "Example CloudWatch Logs" - - ```json hl_lines="3 9-13" - { - "level": "DEBUG", - "message": "{\"id\":\"435iuh2j3hb4\", \"date\":\"2023-12-01T14:48:59\", \"amount\":435.5}", - "timestamp": "2023-12-01T14:49:19.293Z", - "service": "payment", - } - { - "level": "DEBUG", - "message": { - "id": "435iuh2j3hb4", - "date": "2023-12-01T14:48:59", - "amount":435.5 - }, - "timestamp": "2023-12-01T14:49:19.312Z", - "service": "payment", - } - ``` - -You can also achieve this more broadly for all JSON messages (see advanced configuration for [log4j](#log-messages-as-json_1) & [logback](#log-messages-as-json_2)). - ## Additional structured keys ### Logging Lambda context information @@ -463,58 +396,6 @@ including our custom [JMESPath Functions](../utilities/serialization.md#built-in "correlation_id": "correlation_id_value" } ``` - -**setCorrelationId method** - -You can also use `LoggingUtils.setCorrelationId()` method to inject it anywhere else in your code. - -=== "AppSetCorrelationId.java" - - ```java hl_lines="8" - public class AppSetCorrelationId implements RequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(AppSetCorrelationId.class); - - @Logging - public String handleRequest(final ScheduledEvent event, final Context context) { - // ... - LoggingUtils.setCorrelationId(event.getId()); - LOGGER.info("Scheduled Event") - // ... - } - } - ``` - -=== "Example Schedule Event" - - ```json hl_lines="2" - { - "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", - "detail-type": "Scheduled Event", - "source": "aws.events", - "account": "123456789012", - "time": "2023-12-01T14:49:19Z", - "region": "us-east-1", - "resources": [ - "arn:aws:events:us-east-1:123456789012:rule/ExampleRule" - ], - "detail": {} - } - ``` - -=== "CloudWatch Logs with correlation id" - - ```json hl_lines="6" - { - "level": "INFO", - "message": "Scheduled Event", - "timestamp": "2023-12-01T14:49:19.293Z", - "service": "payment", - "correlation_id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c" - } - ``` -???+ tip - You can retrieve correlation IDs via `LoggingUtils.getCorrelationId()` method if needed. **Known correlation IDs** @@ -563,14 +444,16 @@ we provide [built-in JMESPath expressions](#built-in-correlation-id-expressions) #### Custom keys -???+ warning "Custom keys are persisted across warm invocations" - Always set additional keys as part of your handler method to ensure they have the latest value, or explicitly clear them with [`clearState=true`](#clearing-state). +** Using StructuredArguments ** -To append an additional key in your logs, you can use the `LoggingUtils.appendKey()` or `LoggingUtils.appendKeys()` for multiple keys: +To append additional keys in your logs, you can use the `StructuredArguments` class: === "PaymentFunction.java" - ```java hl_lines="8 9 15 16" + ```java hl_lines="1 2 11 17" + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entries; + public class PaymentFunction implements RequestHandler { private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); @@ -578,20 +461,18 @@ To append an additional key in your logs, you can use the `LoggingUtils.appendKe @Logging public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { // ... - LoggingUtils.appendKey("orderId", order.getId()); - LOGGER.info("Collecting payment"); + LOGGER.info("Collecting payment", entry("orderId", order.getId())); // ... Map customKeys = new HashMap<>(); customKeys.put("paymentId", payment.getId()); customKeys.put("amount", payment.getAmount); - LoggingUtils.appendKeys(customKeys); - LOGGER.info("Payment successful"); + LOGGER.info("Payment successful", entries(customKeys)); } } ``` -=== "Example CloudWatch Logs" +=== "CloudWatch Logs for PaymentFunction" ```json hl_lines="7 16-18" { @@ -615,73 +496,153 @@ To append an additional key in your logs, you can use the `LoggingUtils.appendKe } ``` -???+ tip "Additional keys are based on the MDC" - Mapped Diagnostic Context (MDC) is essentially a Key-Value store. It is supported by the [SLF4J API](https://www.slf4j.org/manual.html#mdc){target="_blank"}, - [logback](https://logback.qos.ch/manual/mdc.html){target="_blank"} and log4j (known as [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html){target="_blank"}). - - `LoggingUtils.appendKey("key", "value")` is equivalent to `MDC.put("key", "value")`. +`StructuredArguments` provides several options: + - `entry` to add one key and value into the log structure. Note that value can be any object type. + - `entries` to add multiple keys and values (from a Map) into the log structure. Note that values can be any object type. + - `json` to add a key and raw json (string) as value into the log structure. + - `array` to add one key and multiple values into the log structure. Note that values can be any object type. -### Removing additional keys - -You can remove any additional key from entry using `LoggingUtils.removeKey()` or `LoggingUtils.removeKeys()` for multiple keys: +=== "OrderFunction.java" -=== "PaymentFunction.java" + ```java hl_lines="1 2 11 17" + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.array; - ```java hl_lines="19 20" - public class PaymentFunction implements RequestHandler { + public class OrderFunction implements RequestHandler { private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); - @Logging(logResponse = true) + @Logging public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { // ... - LoggingUtils.appendKey("orderId", order.getId()); - LOGGER.info("Collecting payment"); - - // ... - Map customKeys = new HashMap<>(); - customKeys.put("paymentId", payment.getId()); - customKeys.put("amount", payment.getAmount); - LoggingUtils.appendKeys(customKeys); - LOGGER.info("Payment successful"); - + LOGGER.info("Processing order", entry("order", order), array("products", productList)); // ... - LoggingUtils.removeKey("orderId"); - LoggingUtils.removeKeys("paymentId", "amount"); - - return response; } } ``` -=== "Example CloudWatch Logs" - Response is logged (`logResponse=true`) without the additional keys: +=== "CloudWatch Logs for OrderFunction" - ```json - ... + ```json hl_lines="7 13" { "level": "INFO", - "message": { - "statusCode": 200, - "isBase64Encoded": false, - "body": ..., - "headers": ..., - "multiValueHeaders": ... - }, + "message": "Processing order", "service": "payment", - "timestamp": "2023-12-01T14:49:20.118Z", - "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5" + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "order": { + "orderId": 23542, + "amount": 459.99, + "date": "2023-12-01T14:49:19.018Z", + "customerId": 328496 + }, + "products": [ + { + "productId": 764330, + "name": "product1", + "quantity": 1, + "price": 300 + }, + { + "productId": 798034, + "name": "product42", + "quantity": 1, + "price": 159.99 + } + ] } ``` -???+ tip "Additional keys are based on the MDC" - `LoggingUtils.removeKey("key")` is equivalent to `MDC.remove("key")`. +???+ tip "Use arguments without log placeholders" + As shown in the example above, you can use arguments (with `StructuredArguments`) without placeholders (`{}`) in the message. + If you add the placeholders, the arguments will be logged both as an additional field and also as a string in the log message, using the `toString()` method. + + === "Function1.java" + + ```java + LOGGER.info("Processing {}", entry("order", order)); + ``` + + === "Order.java" + + ```java hl_lines="5" + public class Order { + // ... + + @Override + public String toString() { + return "Order{" + + "orderId=" + id + + ", amount=" + amount + + ", date='" + date + '\'' + + ", customerId=" + customerId + + '}'; + } + } + ``` + + === "CloudWatch Logs Function1" + + ```json hl_lines="3 7" + { + "level": "INFO", + "message": "Processing order=Order{orderId=23542, amount=459.99, date='2023-12-01T14:49:19.018Z', customerId=328496}", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "order": { + "orderId": 23542, + "amount": 459.99, + "date": "2023-12-01T14:49:19.018Z", + "customerId": 328496 + } + } + ``` + + You can also combine structured arguments with non structured ones. For example: + + === "Function2.java" + ```java + LOGGER.info("Processing order {}", order.getOrderId(), entry("order", order)); + ``` + + === "CloudWatch Logs Function2" + ```json + { + "level": "INFO", + "message": "Processing order 23542", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "order": { + "orderId": 23542, + "amount": 459.99, + "date": "2023-12-01T14:49:19.018Z", + "customerId": 328496 + } + } + ``` + +** Using MDC ** + +Mapped Diagnostic Context (MDC) is essentially a Key-Value store. It is supported by the [SLF4J API](https://www.slf4j.org/manual.html#mdc){target="_blank"}, +[logback](https://logback.qos.ch/manual/mdc.html){target="_blank"} and log4j (known as [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html){target="_blank"}). You can use the following standard: + +`MDC.put("key", "value");` + +???+ warning "Custom keys stored in the MDC are persisted across warm invocations" + Always set additional keys as part of your handler method to ensure they have the latest value, or explicitly clear them with [`clearState=true`](#clearing-state). + + +### Removing additional keys + +You can remove additional keys added with the MDC using `MDC.remove("key")`. #### Clearing 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){target="_blank"}, -this means that custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use +this means that custom keys, added with the MDC can be persisted across invocations. If you want all custom keys to be deleted, you can use `clearState=true` attribute on the `@Logging` annotation. === "CreditCardFunction.java" @@ -694,7 +655,7 @@ this means that custom keys can be persisted across invocations. If you want all @Logging(clearState = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { // ... - LoggingUtils.appendKey("cardNumber", card.getId()); + MDC.put("cardNumber", card.getId()); LOGGER.info("Updating card information"); // ... } @@ -727,8 +688,7 @@ this means that custom keys can be persisted across invocations. If you want all } ``` -???+ tip "Additional keys are based on the MDC" - `clearState` is based on `MDC.clear()`. State clearing is automatically done at the end of the execution of the handler if set to `true`. +`clearState` is based on `MDC.clear()`. State clearing is automatically done at the end of the execution of the handler if set to `true`. ## Logging incoming event @@ -952,22 +912,6 @@ The `JsonTemplateLayout` is automatically configured with the provided template: You can create your own template and leverage the [PowertoolsResolver](https://github.com/aws-powertools/powertools-lambda-java/tree/v2/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java){target="_blank"} and any other resolver to log the desired fields with the desired format. Some examples of customization are given below: -#### Log messages as JSON -`message` field is not handled with the standard [`MessageResolver`](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html#event-template-resolver-message){target="_blank"} but by the `PowertoolsResolver`. -With this resolver, you can choose to log all the JSON messages as JSON and not as String. - -=== "my-custom-template.json" - - ```json - { - "message": { - "$resolver": "powertools", - "field": "message", - "asJson": true - } - } - ``` - #### Customising date format Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` and in system default timezone. @@ -1015,16 +959,6 @@ Logback configuration is done in _logback.xml_ and the Powertools [`LambdaJsonEn The `LambdaJsonEncoder` can be customized in different ways: -#### Log messages as JSON - -With the following configuration, you choose to log all the JSON messages as JSON and not as String (default is `false`): - -```xml - - true - -``` - #### Customising date format Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` and in system default timezone. If you need to customize format and timezone, you can change use the following: @@ -1071,33 +1005,6 @@ If you need to customize format and timezone, you can change use the following: ``` -## Override default object mapper - -You can optionally choose to override default object mapper which is used to serialize lambda function events. You might -want to supply custom object mapper in order to control how serialisation is done, for example, when you want to log only -specific fields from received event due to security. - -=== "App.java" - - ```java hl_lines="6-10" - public class App implements RequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(App.class); - - static { - ObjectMapper objectMapper = new ObjectMapper() - .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - LoggingUtils.setObjectMapper(objectMapper); - } - - @Logging(logEvent = true) - public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - // ... - } - } - ``` - ## Elastic Common Schema (ECS) Support Utility also supports [Elastic Common Schema(ECS)](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html){target="_blank"} format. diff --git a/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java index 18eea0560..5aa268ffe 100644 --- a/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java @@ -31,10 +31,10 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; @@ -62,7 +62,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); }); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); diff --git a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java index 36ef72ae7..b1a701b8f 100644 --- a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java @@ -14,6 +14,7 @@ package helloworld; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; @@ -31,10 +32,10 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; @@ -63,13 +64,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); }); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); diff --git a/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt index 1c925d4f4..8e8857079 100644 --- a/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt +++ b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt @@ -18,23 +18,21 @@ import com.amazonaws.services.lambda.runtime.RequestHandler import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent import com.amazonaws.xray.entities.Subsegment -import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.slf4j.MDC import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger import software.amazon.cloudwatchlogs.emf.model.DimensionSet import software.amazon.cloudwatchlogs.emf.model.Unit import software.amazon.lambda.powertools.logging.Logging -import software.amazon.lambda.powertools.logging.LoggingUtils +import software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry import software.amazon.lambda.powertools.metrics.Metrics import software.amazon.lambda.powertools.metrics.MetricsUtils import software.amazon.lambda.powertools.tracing.CaptureMode import software.amazon.lambda.powertools.tracing.Tracing import software.amazon.lambda.powertools.tracing.TracingUtils -import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader import java.net.URL -import java.util.stream.Collectors /** * Handler for requests to Lambda function. @@ -44,7 +42,6 @@ class App : RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LogManager.getLogger(App.class); + + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); @@ -60,13 +64,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); }); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); diff --git a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java index dacd7f1d4..e0b1a2979 100644 --- a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java @@ -14,6 +14,7 @@ package helloworld; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; @@ -31,10 +32,10 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.slf4j.MDC; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; @@ -44,14 +45,11 @@ * Handler for requests to Lambda function. */ public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LogManager.getLogger(App.class); - // This is controlled by POWERTOOLS_LOGGER_SAMPLE_RATE environment variable - // @Logging(logEvent = true, samplingRate = 0.7) - // This is controlled by POWERTOOLS_METRICS_NAMESPACE environment variable - // @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - // This is controlled by POWERTOOLS_TRACER_CAPTURE_ERROR environment variable + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); @@ -66,13 +64,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); }); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); diff --git a/pom.xml b/pom.xml index 7b126a426..b9b8da3bc 100644 --- a/pom.xml +++ b/pom.xml @@ -55,12 +55,12 @@ powertools-large-messages powertools-e2e-tests powertools-batch - examples powertools-parameters/powertools-parameters-ssm powertools-parameters/powertools-parameters-secrets powertools-parameters/powertools-parameters-dynamodb powertools-parameters/powertools-parameters-appconfig powertools-parameters/powertools-parameters-tests + examples diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index c2634533d..58492653a 100644 --- a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -18,15 +18,15 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; public class Function implements RequestHandler { private static final Logger LOG = LoggerFactory.getLogger(Function.class); @Logging public String handleRequest(Input input, Context context) { - LoggingUtils.appendKeys(input.getKeys()); + input.getKeys().forEach(MDC::put); LOG.info(input.getMessage()); return "OK"; diff --git a/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java index 41d7e2957..f770679ee 100644 --- a/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java +++ b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java @@ -91,7 +91,7 @@ public void saveInProgress_defaultConfig() { assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(dr.getResponseData()).isNull(); - assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(dr.getPayloadHash()).isEqualTo(""); assertThat(dr.getInProgressExpiryTimestamp()).isEmpty(); assertThat(status).isEqualTo(1); @@ -109,7 +109,7 @@ public void saveInProgress_withRemainingTime() { assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(dr.getResponseData()).isNull(); - assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(dr.getPayloadHash()).isEqualTo(""); assertThat(dr.getInProgressExpiryTimestamp().orElse(-1)).isEqualTo( now.plus(lambdaTimeoutMs, ChronoUnit.MILLIS).toEpochMilli()); @@ -228,7 +228,7 @@ public void saveSuccess_shouldUpdateRecord() throws JsonProcessingException { assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(dr.getResponseData()).isEqualTo(JsonConfig.get().getObjectMapper().writeValueAsString(product)); - assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(dr.getPayloadHash()).isEqualTo(""); assertThat(status).isEqualTo(2); assertThat(cache).isEmpty(); @@ -247,11 +247,11 @@ public void saveSuccess_withCacheEnabled_shouldSaveInCache() throws JsonProcessi assertThat(status).isEqualTo(2); assertThat(cache).hasSize(1); - DataRecord record = cache.get("testFunction#7b40f56c086de5aa91dc467456329ed2"); + DataRecord record = cache.get("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); assertThat(record.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(record.getResponseData()).isEqualTo(JsonConfig.get().getObjectMapper().writeValueAsString(product)); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getPayloadHash()).isEqualTo(""); } @@ -270,7 +270,7 @@ public void getRecord_shouldReturnRecordFromPersistence() Instant now = Instant.now(); DataRecord record = persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), now); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(record.getResponseData()).isEqualTo("Response"); assertThat(status).isEqualTo(0); @@ -286,15 +286,15 @@ public void getRecord_cacheEnabledNotExpired_shouldReturnRecordFromCache() Instant now = Instant.now(); DataRecord dr = new DataRecord( - "testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", + "testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", DataRecord.Status.COMPLETED, now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(), "result of the function", null); - cache.put("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", dr); + cache.put("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", dr); DataRecord record = persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), now); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); assertThat(record.getResponseData()).isEqualTo("result of the function"); assertThat(status).isEqualTo(-1); // getRecord must not be called (retrieve from cache) @@ -310,15 +310,15 @@ public void getRecord_cacheEnabledExpired_shouldReturnRecordFromPersistence() Instant now = Instant.now(); DataRecord dr = new DataRecord( - "testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", + "testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", DataRecord.Status.COMPLETED, now.minus(3, ChronoUnit.SECONDS).getEpochSecond(), "result of the function", null); - cache.put("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", dr); + cache.put("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", dr); DataRecord record = persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), now); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(record.getResponseData()).isEqualTo("Response"); assertThat(status).isEqualTo(0); @@ -363,8 +363,8 @@ public void deleteRecord_cacheEnabled_shouldDeleteRecordFromCache() { persistenceStore.configure(IdempotencyConfig.builder() .withUseLocalCache(true).build(), null, cache); - cache.put("testFunction#7b40f56c086de5aa91dc467456329ed2", - new DataRecord("testFunction#7b40f56c086de5aa91dc467456329ed2", DataRecord.Status.COMPLETED, 123, null, + cache.put("testFunction#8d6a8f173b46479eff55e0997864a514", + new DataRecord("testFunction#8d6a8f173b46479eff55e0997864a514", DataRecord.Status.COMPLETED, 123, null, null)); persistenceStore.deleteRecord(JsonConfig.get().getObjectMapper().valueToTree(event), new ArithmeticException()); assertThat(status).isEqualTo(3); diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java index 1296a75c7..227eea39e 100644 --- a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java @@ -18,26 +18,15 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import java.util.HashMap; +import java.util.Map; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; -import software.amazon.lambda.powertools.utilities.JsonConfig; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; public class IdempotencyFunction implements RequestHandler { - private final static Logger LOG = LogManager.getLogger(IdempotencyFunction.class); - public boolean handlerExecuted = false; public IdempotencyFunction(DynamoDbClient client) { @@ -66,28 +55,10 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); - try { - String address = JsonConfig.get().getObjectMapper().readTree(input.getBody()).get("address").asText(); - final String pageContents = this.getPageContents(address); - String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - LOG.debug("ip is {}", pageContents); return response .withStatusCode(200) - .withBody(output); - - } catch (IOException e) { - return response - .withBody("{}") - .withStatusCode(500); - } - } + .withBody("{ \"message\": \"hello world\"}"); - // we could actually also put the @Idempotent annotation here - private String getPageContents(String address) throws IOException { - URL url = new URL(address); - try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { - return br.lines().collect(Collectors.joining(System.lineSeparator())); - } } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java index 95086a085..c98da7833 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java @@ -14,8 +14,7 @@ package org.apache.logging.log4j.layout.template.json.resolver; -import static java.lang.Boolean.TRUE; -import static software.amazon.lambda.powertools.logging.LoggingUtils.LOG_MESSAGES_AS_JSON; +import static java.util.Arrays.stream; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_ARN; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE; @@ -26,18 +25,18 @@ import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SAMPLING_RATE; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.layout.template.json.util.JsonWriter; -import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.util.ReadOnlyStringMap; import software.amazon.lambda.powertools.common.internal.LambdaConstants; import software.amazon.lambda.powertools.common.internal.SystemWrapper; +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; /** @@ -170,76 +169,42 @@ public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { } }; - /** - * Use a custom message resolver to permit to log json string in json format without escaped quotes. - */ - private static final class MessageResolver implements EventResolver { - private final ObjectMapper mapper = new ObjectMapper() - .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); - private final boolean logMessagesAsJsonGlobal; - - public MessageResolver(boolean logMessagesAsJson) { - this.logMessagesAsJsonGlobal = logMessagesAsJson; - } - - public boolean isValidJson(String json) { - if (!(json.startsWith("{") || json.startsWith("["))) { - return false; - } - try { - mapper.readTree(json); - } catch (JacksonException e) { - return false; - } - return true; - } - - @Override - public boolean isResolvable(LogEvent logEvent) { - final Message msg = logEvent.getMessage(); - return null != msg && null != msg.getFormattedMessage(); - } - - @Override - public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { - String message = logEvent.getMessage().getFormattedMessage(); - - String logMessagesAsJsonLocal = logEvent.getContextData().getValue(LOG_MESSAGES_AS_JSON); - Boolean logMessagesAsJson = null; - if (logMessagesAsJsonLocal != null) { - logMessagesAsJson = Boolean.parseBoolean(logMessagesAsJsonLocal); - } - - if (((logMessagesAsJsonGlobal && logMessagesAsJson == null) || TRUE.equals(logMessagesAsJson)) - && isValidJson(message)) { - jsonWriter.writeRawString(message); - } else { - jsonWriter.writeString(message); - } - } - } - + @SuppressWarnings("java:S106") private static final EventResolver NON_POWERTOOLS_FIELD_RESOLVER = (LogEvent logEvent, JsonWriter jsonWriter) -> { StringBuilder stringBuilder = jsonWriter.getStringBuilder(); - // remove dummy field to kick in powertools resolver - stringBuilder.setLength(stringBuilder.length() - 4); - - // Inject all the context information. - ReadOnlyStringMap contextData = logEvent.getContextData(); - contextData.forEach((key, value) -> { - if (!PowertoolsLoggedFields.stringValues().contains(key) && !LOG_MESSAGES_AS_JSON.equals(key)) { - jsonWriter.writeSeparator(); - jsonWriter.writeString(key); - stringBuilder.append(':'); - jsonWriter.writeValue(value); + try (JsonSerializer serializer = new JsonSerializer(stringBuilder)) { + + // remove dummy field to kick in powertools resolver + stringBuilder.setLength(stringBuilder.length() - 4); + + // log other MDC values + ReadOnlyStringMap contextData = logEvent.getContextData(); + contextData.forEach((key, value) -> { + if (!PowertoolsLoggedFields.stringValues().contains(key)) { + serializer.writeSeparator(); + serializer.writeObjectField(key, value); + } + }); + + // log structured arguments + Object[] arguments = logEvent.getMessage().getParameters(); + if (arguments != null) { + stream(arguments).filter(StructuredArgument.class::isInstance).forEach(argument -> { + serializer.writeRaw(','); + try { + ((StructuredArgument) argument).writeTo(serializer); + } catch (IOException e) { + System.err.printf("Failed to encode log event, error: %s.%n", e.getMessage()); + } + }); } - }); + } }; private final EventResolver internalResolver; - private static final Map eventResolverMap = Stream.of(new Object[][] { + private static final Map eventResolverMap = Collections.unmodifiableMap(Stream.of(new Object[][] { { SERVICE.getName(), SERVICE_RESOLVER }, { FUNCTION_NAME.getName(), FUNCTION_NAME_RESOLVER }, { FUNCTION_VERSION.getName(), FUNCTION_VERSION_RESOLVER }, @@ -251,7 +216,7 @@ && isValidJson(message)) { { SAMPLING_RATE.getName(), SAMPLING_RATE_RESOLVER }, { "region", REGION_RESOLVER }, { "account_id", ACCOUNT_ID_RESOLVER } - }).collect(Collectors.toMap(data -> (String) data[0], data -> (EventResolver) data[1])); + }).collect(Collectors.toMap(data -> (String) data[0], data -> (EventResolver) data[1]))); PowertoolsResolver(final TemplateResolverConfig config) { @@ -259,17 +224,9 @@ && isValidJson(message)) { if (fieldName == null) { internalResolver = NON_POWERTOOLS_FIELD_RESOLVER; } else { - boolean logMessagesAsJson = false; - if (config.exists("asJson")) { - logMessagesAsJson = config.getBoolean("asJson"); - } - if ("message".equals(fieldName)) { - internalResolver = new MessageResolver(logMessagesAsJson); - } else { - internalResolver = eventResolverMap.get(fieldName); - } + internalResolver = eventResolverMap.get(fieldName); if (internalResolver == null) { - throw new IllegalArgumentException("unknown field: " + fieldName); + throw new IllegalArgumentException("Unknown field: " + fieldName); } } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json index d8d8810f6..8b811ee5f 100644 --- a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json @@ -4,8 +4,7 @@ "field": "name" }, "message": { - "$resolver": "powertools", - "field": "message" + "$resolver": "message" }, "error": { "message": { diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsMessageResolverTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverArgumentsTest.java similarity index 70% rename from powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsMessageResolverTest.java rename to powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverArgumentsTest.java index a00b78906..24014a759 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsMessageResolverTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverArgumentsTest.java @@ -35,12 +35,10 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.slf4j.MDC; -import software.amazon.lambda.powertools.logging.LoggingUtils; -import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsJsonMessage; -import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments; @Order(2) -class PowertoolsMessageResolverTest { +class PowertoolsResolverArgumentsTest { @Mock private Context context; @@ -67,9 +65,9 @@ void cleanUp() throws IOException { } @Test - void shouldLogJsonMessageWithoutEscapedStringsWhenSettingLogAsJson() { + void shouldLogArgumentsAsJsonWhenUsingRawJson() { // GIVEN - PowertoolsJsonMessage requestHandler = new PowertoolsJsonMessage(); + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.JSON); SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); msg.setMessageId("1212abcd"); msg.setBody("plop"); @@ -85,24 +83,33 @@ void shouldLogJsonMessageWithoutEscapedStringsWhenSettingLogAsJson() { // THEN File logFile = new File("target/logfile.json"); assertThat(contentOf(logFile)) - .contains("\"message\":{\"messageId\":\"1212abcd\",\"receiptHandle\":null,\"body\":\"plop\",\"md5OfBody\":null,\"md5OfMessageAttributes\":null,\"eventSourceArn\":null,\"eventSource\":\"eb\",\"awsRegion\":\"eu-west-1\",\"attributes\":null,\"messageAttributes\":{\"keyAttribute\":{\"stringValue\":null,\"binaryValue\":null,\"stringListValues\":[\"val1\",\"val2\",\"val3\"],\"binaryListValues\":null,\"dataType\":null}}}") + .contains("\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") .contains("\"message\":\"1212abcd\"") - .contains("\"message\":\"{\\\"key\\\":\\\"value\\\"}\"") - .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") - .doesNotContain(LoggingUtils.LOG_MESSAGES_AS_JSON); + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\""); } @Test - void shouldLogStringMessageWhenNotJson() { + void shouldLogArgumentsAsJsonWhenUsingKeyValue() { // GIVEN - PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.ENTRY); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); // WHEN - requestHandler.handleRequest(null, context); + requestHandler.handleRequest(msg, context); // THEN File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("\"message\":\"Test debug event\""); + assertThat(contentOf(logFile)) + .contains("\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\""); } private void setupContext() { diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java index 1aa98fdef..073cd7026 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java @@ -15,6 +15,7 @@ package org.apache.logging.log4j.layout.template.json.resolver; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mockStatic; import static software.amazon.lambda.powertools.common.internal.SystemWrapper.getenv; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_ARN; @@ -70,6 +71,13 @@ void shouldResolveAccountId() { assertThat(result).isEqualTo("\"123456789012\""); } + @Test + void unknownField_shouldThrowException() { + assertThatThrownBy(() -> resolveField("custom-random-unknown-field", "custom-random-unknown-field", "Once apon a time in Switzerland...")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown field: custom-random-unknown-field"); + } + @Test void shouldResolveRegion() { try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java similarity index 62% rename from powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java rename to powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java index 3d196e5fb..387074590 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java @@ -14,29 +14,38 @@ package software.amazon.lambda.powertools.logging.internal.handler; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.fasterxml.jackson.core.JsonProcessingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.logging.argument.StructuredArguments; import software.amazon.lambda.powertools.utilities.JsonConfig; -public class PowertoolsJsonMessage implements RequestHandler { - private final Logger LOG = LoggerFactory.getLogger(PowertoolsJsonMessage.class); +public class PowertoolsArguments implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsArguments.class); + private final ArgumentFormat argumentFormat; + + public PowertoolsArguments(ArgumentFormat argumentFormat) { + this.argumentFormat = argumentFormat; + } @Override @Logging(clearState = true) public String handleRequest(SQSEvent.SQSMessage input, Context context) { try { - LoggingUtils.logMessagesAsJson(true); - LOG.debug(JsonConfig.get().getObjectMapper().writeValueAsString(input)); - - LoggingUtils.logMessagesAsJson(false); - LOG.debug("{\"key\":\"value\"}"); - + MDC.put(CORRELATION_ID.getName(), input.getMessageId()); + if (argumentFormat == ArgumentFormat.JSON) { + LOG.debug("SQS Event", StructuredArguments.json("input", + JsonConfig.get().getObjectMapper().writeValueAsString(input))); + } else { + LOG.debug("SQS Event", StructuredArguments.entry("input", input)); + } LOG.debug("{}", input.getMessageId()); LOG.warn("Message body = {} and id = \"{}\"", input.getBody(), input.getMessageId()); } catch (JsonProcessingException e) { @@ -44,4 +53,8 @@ public String handleRequest(SQSEvent.SQSMessage input, Context context) { } return input.getMessageId(); } + + public enum ArgumentFormat { + JSON, ENTRY + } } diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java index faa722756..e8c0c5851 100644 --- a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java @@ -18,8 +18,8 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; public class PowertoolsLogEnabled implements RequestHandler { private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); @@ -27,8 +27,8 @@ public class PowertoolsLogEnabled implements RequestHandler { @Override @Logging(clearState = true) public Object handleRequest(Object input, Context context) { - LoggingUtils.appendKey("myKey", "myValue"); + MDC.put("myKey", "myValue"); LOG.debug("Test debug event"); - return null; + return "Bonjour le monde"; } } diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java new file mode 100644 index 000000000..b98a8eada --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java @@ -0,0 +1,139 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.logback; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +/** + * Json tools to serialize common fields + */ +final class JsonUtils { + + private JsonUtils() { + // static utils + } + + static void serializeTimestamp(JsonSerializer generator, long timestamp, String timestampFormat, + String timestampFormatTimezoneId, String timestampAttributeName) { + String formattedTimestamp; + if (timestampFormat == null || timestamp < 0) { + formattedTimestamp = String.valueOf(timestamp); + } else { + Date date = new Date(timestamp); + DateFormat format = new SimpleDateFormat(timestampFormat); + + if (timestampFormatTimezoneId != null) { + TimeZone tz = TimeZone.getTimeZone(timestampFormatTimezoneId); + format.setTimeZone(tz); + } + formattedTimestamp = format.format(date); + } + generator.writeStringField(timestampAttributeName, formattedTimestamp); + } + + static void serializeMDCEntries(Map mdcPropertyMap, JsonSerializer serializer) { + TreeMap sortedMap = new TreeMap<>(mdcPropertyMap); + for (Map.Entry entry : sortedMap.entrySet()) { + if (!PowertoolsLoggedFields.stringValues().contains(entry.getKey())) { + serializeMDCEntry(entry, serializer); + } + } + } + + static void serializeMDCEntry(Map.Entry entry, JsonSerializer serializer) { + serializer.writeRaw(','); + serializer.writeFieldName(entry.getKey()); + if (isString(entry.getValue())) { + serializer.writeString(entry.getValue()); + } else { + serializer.writeRaw(entry.getValue()); + } + } + + static void serializeArguments(ILoggingEvent event, JsonSerializer serializer) throws IOException { + Object[] arguments = event.getArgumentArray(); + if (arguments != null) { + for (Object argument : arguments) { + if (argument instanceof StructuredArgument) { + serializer.writeRaw(','); + ((StructuredArgument) argument).writeTo(serializer); + } + } + } + } + + /** + * As MDC is a {@code Map}, we need to check the type + * to output numbers and booleans correctly (without quotes) + */ + private static boolean isString(String str) { + if (str == null) { + return true; + } + if ("true".equals(str) || "false".equals(str)) { + return false; // boolean + } + return !isNumeric(str); // number + } + + /** + * Taken from commons-lang3 NumberUtils to avoid include the library + */ + private static boolean isNumeric(final String str) { + if (str == null || str.isEmpty()) { + return false; + } + if (str.charAt(str.length() - 1) == '.') { + return false; + } + if (str.charAt(0) == '-') { + if (str.length() == 1) { + return false; + } + return withDecimalsParsing(str, 1); + } + return withDecimalsParsing(str, 0); + } + + /** + * Taken from commons-lang3 NumberUtils + */ + private static boolean withDecimalsParsing(final String str, final int beginIdx) { + int decimalPoints = 0; + for (int i = beginIdx; i < str.length(); i++) { + final boolean isDecimalPoint = str.charAt(i) == '.'; + if (isDecimalPoint) { + decimalPoints++; + } + if (decimalPoints > 1) { + return false; + } + if (!isDecimalPoint && !Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java index 1fc98ec67..a1a7daff1 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java @@ -22,6 +22,9 @@ import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_REQUEST_ID; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_VERSION; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeArguments; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntries; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeTimestamp; import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; @@ -29,9 +32,11 @@ import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxy; import ch.qos.logback.core.encoder.EncoderBase; +import java.io.IOException; +import java.util.Arrays; import java.util.Map; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.logging.logback.internal.LambdaEcsSerializer; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; /** @@ -45,6 +50,29 @@ */ public class LambdaEcsEncoder extends EncoderBase { + protected static final String TIMESTAMP_ATTR_NAME = "@timestamp"; + protected static final String ECS_VERSION_ATTR_NAME = "ecs.version"; + protected static final String LOGGER_ATTR_NAME = "log.logger"; + protected static final String LEVEL_ATTR_NAME = "log.level"; + protected static final String SERVICE_NAME_ATTR_NAME = "service.name"; + protected static final String SERVICE_VERSION_ATTR_NAME = "service.version"; + protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + protected static final String THREAD_ATTR_NAME = "process.thread.name"; + protected static final String EXCEPTION_MSG_ATTR_NAME = "error.message"; + protected static final String EXCEPTION_CLASS_ATTR_NAME = "error.type"; + protected static final String EXCEPTION_STACK_ATTR_NAME = "error.stack_trace"; + protected static final String CLOUD_PROVIDER_ATTR_NAME = "cloud.provider"; + protected static final String CLOUD_REGION_ATTR_NAME = "cloud.region"; + protected static final String CLOUD_ACCOUNT_ATTR_NAME = "cloud.account.id"; + protected static final String CLOUD_SERVICE_ATTR_NAME = "cloud.service.name"; + protected static final String FUNCTION_COLD_START_ATTR_NAME = "faas.coldstart"; + protected static final String FUNCTION_REQUEST_ID_ATTR_NAME = "faas.execution"; + protected static final String FUNCTION_ARN_ATTR_NAME = "faas.id"; + protected static final String FUNCTION_NAME_ATTR_NAME = "faas.name"; + protected static final String FUNCTION_VERSION_ATTR_NAME = "faas.version"; + protected static final String FUNCTION_MEMORY_ATTR_NAME = "faas.memory"; + protected static final String FUNCTION_TRACE_ID_ATTR_NAME = "trace.id"; + protected static final String ECS_VERSION = "1.2.0"; protected static final String CLOUD_PROVIDER = "aws"; protected static final String CLOUD_SERVICE = "lambda"; @@ -56,7 +84,7 @@ public class LambdaEcsEncoder extends EncoderBase { @Override public byte[] headerBytes() { - return null; + return new byte[0]; } /** @@ -65,62 +93,108 @@ public byte[] headerBytes() { * @param event the logging event * @return the encoded bytes */ + + @SuppressWarnings("java:S106") @Override public byte[] encode(ILoggingEvent event) { final Map mdcPropertyMap = event.getMDCPropertyMap(); - StringBuilder builder = new StringBuilder(256); - LambdaEcsSerializer.serializeObjectStart(builder); - LambdaEcsSerializer.serializeTimestamp(builder, event.getTimeStamp(), "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "UTC"); - LambdaEcsSerializer.serializeEcsVersion(builder, ECS_VERSION); - LambdaEcsSerializer.serializeLogLevel(builder, event.getLevel()); - LambdaEcsSerializer.serializeFormattedMessage(builder, event.getFormattedMessage()); - IThrowableProxy throwableProxy = event.getThrowableProxy(); - if (throwableProxy != null) { - if (throwableConverter != null) { - LambdaEcsSerializer.serializeException(builder, throwableProxy.getClassName(), - throwableProxy.getMessage(), throwableConverter.convert(event)); - } else if (throwableProxy instanceof ThrowableProxy) { - LambdaEcsSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable()); - } else { - LambdaEcsSerializer.serializeException(builder, throwableProxy.getClassName(), - throwableProxy.getMessage(), throwableProxyConverter.convert(event)); - } + StringBuilder builder = new StringBuilder(); + try (JsonSerializer serializer = new JsonSerializer(builder)) { + serializer.writeStartObject(); + serializeTimestamp(serializer, event.getTimeStamp(), "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "UTC", TIMESTAMP_ATTR_NAME); + serializer.writeRaw(','); + serializer.writeStringField(ECS_VERSION_ATTR_NAME, ECS_VERSION); + serializer.writeRaw(','); + serializer.writeStringField(LEVEL_ATTR_NAME, event.getLevel().toString()); + serializer.writeRaw(','); + serializer.writeStringField(FORMATTED_MESSAGE_ATTR_NAME, event.getFormattedMessage()); + + serializeException(event, serializer); + + serializer.writeRaw(','); + serializer.writeStringField(SERVICE_NAME_ATTR_NAME, LambdaHandlerProcessor.serviceName()); + serializer.writeRaw(','); + serializer.writeStringField(SERVICE_VERSION_ATTR_NAME, mdcPropertyMap.get(FUNCTION_VERSION.getName())); + serializer.writeRaw(','); + serializer.writeStringField(LOGGER_ATTR_NAME, event.getLoggerName()); + serializer.writeRaw(','); + serializer.writeStringField(THREAD_ATTR_NAME, event.getThreadName()); + + String arn = mdcPropertyMap.get(FUNCTION_ARN.getName()); + + serializeCloudInfo(serializer, arn); + + serializeFunctionInfo(serializer, arn, mdcPropertyMap); + + serializeMDCEntries(mdcPropertyMap, serializer); + + serializeArguments(event, serializer); + + serializer.writeEndObject(); + serializer.writeRaw('\n'); + } catch (IOException e) { + System.err.printf("Failed to encode log event, error: %s.%n", e.getMessage()); } - LambdaEcsSerializer.serializeServiceName(builder, LambdaHandlerProcessor.serviceName()); - LambdaEcsSerializer.serializeServiceVersion(builder, mdcPropertyMap.get(FUNCTION_VERSION.getName())); - LambdaEcsSerializer.serializeLoggerName(builder, event.getLoggerName()); - LambdaEcsSerializer.serializeThreadName(builder, event.getThreadName()); - String arn = mdcPropertyMap.get(FUNCTION_ARN.getName()); + return builder.toString().getBytes(UTF_8); + } + + private void serializeFunctionInfo(JsonSerializer serializer, String arn, Map mdcPropertyMap) { + if (includeFaasInfo) { + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_ARN_ATTR_NAME, arn); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_NAME_ATTR_NAME, mdcPropertyMap.get(FUNCTION_NAME.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_VERSION_ATTR_NAME, mdcPropertyMap.get(FUNCTION_VERSION.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_MEMORY_ATTR_NAME, mdcPropertyMap.get(FUNCTION_MEMORY_SIZE.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_REQUEST_ID_ATTR_NAME, mdcPropertyMap.get(FUNCTION_REQUEST_ID.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_COLD_START_ATTR_NAME, mdcPropertyMap.get(FUNCTION_COLD_START.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_TRACE_ID_ATTR_NAME, mdcPropertyMap.get(FUNCTION_TRACE_ID.getName())); + } + } + private void serializeCloudInfo(JsonSerializer serializer, String arn) { if (includeCloudInfo) { - LambdaEcsSerializer.serializeCloudProvider(builder, CLOUD_PROVIDER); - LambdaEcsSerializer.serializeCloudService(builder, CLOUD_SERVICE); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_PROVIDER_ATTR_NAME, CLOUD_PROVIDER); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_SERVICE_ATTR_NAME, CLOUD_SERVICE); if (arn != null) { String[] arnParts = arn.split(":"); - LambdaEcsSerializer.serializeCloudRegion(builder, arnParts[3]); - LambdaEcsSerializer.serializeCloudAccountId(builder, arnParts[4]); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_REGION_ATTR_NAME, arnParts[3]); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_ACCOUNT_ATTR_NAME, arnParts[4]); } } + } - if (includeFaasInfo) { - LambdaEcsSerializer.serializeFunctionId(builder, arn); - LambdaEcsSerializer.serializeFunctionName(builder, mdcPropertyMap.get(FUNCTION_NAME.getName())); - LambdaEcsSerializer.serializeFunctionVersion(builder, mdcPropertyMap.get(FUNCTION_VERSION.getName())); - LambdaEcsSerializer.serializeFunctionMemory(builder, mdcPropertyMap.get(FUNCTION_MEMORY_SIZE.getName())); - LambdaEcsSerializer.serializeFunctionExecutionId(builder, - mdcPropertyMap.get(FUNCTION_REQUEST_ID.getName())); - LambdaEcsSerializer.serializeColdStart(builder, mdcPropertyMap.get(FUNCTION_COLD_START.getName())); - LambdaEcsSerializer.serializeTraceId(builder, mdcPropertyMap.get(FUNCTION_TRACE_ID.getName())); + private void serializeException(ILoggingEvent event, JsonSerializer serializer) { + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + serializeException(serializer, throwableProxy.getClassName(), + throwableProxy.getMessage(), throwableConverter.convert(event)); + } else if (throwableProxy instanceof ThrowableProxy) { + Throwable throwable = ((ThrowableProxy) throwableProxy).getThrowable(); + serializeException(serializer, throwable.getClass().getName(), throwable.getMessage(), + Arrays.toString(throwable.getStackTrace())); + } else { + serializeException(serializer, throwableProxy.getClassName(), + throwableProxy.getMessage(), throwableProxyConverter.convert(event)); + } } - LambdaEcsSerializer.serializeAdditionalFields(builder, event.getMDCPropertyMap()); - LambdaEcsSerializer.serializeObjectEnd(builder); - return builder.toString().getBytes(UTF_8); } @Override public byte[] footerBytes() { - return null; + return new byte[0]; } /** @@ -196,4 +270,13 @@ public void setIncludeCloudInfo(boolean includeCloudInfo) { public void setIncludeFaasInfo(boolean includeFaasInfo) { this.includeFaasInfo = includeFaasInfo; } + + private void serializeException(JsonSerializer serializer, String className, String message, String stackTrace) { + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_MSG_ATTR_NAME, message); + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_CLASS_ATTR_NAME, className); + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_STACK_ATTR_NAME, stackTrace); + } } diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java index b951e266e..9afaf0ab7 100644 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java @@ -15,7 +15,10 @@ package software.amazon.lambda.powertools.logging.logback; import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.logging.LoggingUtils.LOG_MESSAGES_AS_JSON; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeArguments; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntries; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntry; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeTimestamp; import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; @@ -23,25 +26,40 @@ import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxy; import ch.qos.logback.core.encoder.EncoderBase; -import software.amazon.lambda.powertools.logging.logback.internal.LambdaJsonSerializer; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; /** * Custom encoder for logback that encodes logs in JSON format. - * It does not use a JSON library but a custom serializer ({@link LambdaJsonSerializer}) */ public class LambdaJsonEncoder extends EncoderBase { + protected static final String TIMESTAMP_ATTR_NAME = "timestamp"; + protected static final String LEVEL_ATTR_NAME = "level"; + protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + protected static final String THREAD_ATTR_NAME = "thread"; + protected static final String THREAD_ID_ATTR_NAME = "thread_id"; + protected static final String THREAD_PRIORITY_ATTR_NAME = "thread_priority"; + protected static final String EXCEPTION_MSG_ATTR_NAME = "message"; + protected static final String EXCEPTION_CLASS_ATTR_NAME = "name"; + protected static final String EXCEPTION_STACK_ATTR_NAME = "stack"; + protected static final String EXCEPTION_ATTR_NAME = "error"; + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); protected ThrowableHandlingConverter throwableConverter = null; protected String timestampFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; protected String timestampFormatTimezoneId = null; private boolean includeThreadInfo = false; private boolean includePowertoolsInfo = true; - private boolean logMessagesAsJsonGlobal; @Override public byte[] headerBytes() { - return null; + return new byte[0]; } @Override @@ -53,43 +71,83 @@ public void start() { } } + @SuppressWarnings("java:S106") @Override public byte[] encode(ILoggingEvent event) { - StringBuilder builder = new StringBuilder(256); - LambdaJsonSerializer.serializeObjectStart(builder); - LambdaJsonSerializer.serializeLogLevel(builder, event.getLevel()); - LambdaJsonSerializer.serializeFormattedMessage( - builder, - event.getFormattedMessage(), - logMessagesAsJsonGlobal, - event.getMDCPropertyMap().get(LOG_MESSAGES_AS_JSON)); + StringBuilder builder = new StringBuilder(); + try (JsonSerializer serializer = new JsonSerializer(builder)) { + serializer.writeStartObject(); + serializer.writeStringField(LEVEL_ATTR_NAME, event.getLevel().toString()); + serializer.writeRaw(','); + serializer.writeStringField(FORMATTED_MESSAGE_ATTR_NAME, event.getFormattedMessage()); + + serializeException(event, serializer); + + TreeMap sortedMap = new TreeMap<>(event.getMDCPropertyMap()); + serializePowertools(sortedMap, serializer); + + serializeMDCEntries(sortedMap, serializer); + + serializeArguments(event, serializer); + + serializeThreadInfo(event, serializer); + + serializer.writeRaw(','); + serializeTimestamp(serializer, event.getTimeStamp(), + timestampFormat, timestampFormatTimezoneId, TIMESTAMP_ATTR_NAME); + + serializer.writeEndObject(); + serializer.writeRaw('\n'); + } catch (IOException e) { + System.err.printf("Failed to encode log event, error: %s.%n", e.getMessage()); + } + return builder.toString().getBytes(UTF_8); + } + + private void serializeThreadInfo(ILoggingEvent event, JsonSerializer serializer) { + if (includeThreadInfo) { + if (event.getThreadName() != null) { + serializer.writeRaw(','); + serializer.writeStringField(THREAD_ATTR_NAME, event.getThreadName()); + } + serializer.writeRaw(','); + serializer.writeNumberField(THREAD_ID_ATTR_NAME, Thread.currentThread().getId()); + serializer.writeRaw(','); + serializer.writeNumberField(THREAD_PRIORITY_ATTR_NAME, Thread.currentThread().getPriority()); + } + } + + private void serializePowertools(TreeMap sortedMap, JsonSerializer serializer) { + if (includePowertoolsInfo) { + for (Map.Entry entry : sortedMap.entrySet()) { + if (PowertoolsLoggedFields.stringValues().contains(entry.getKey()) + && !(entry.getKey().equals(PowertoolsLoggedFields.SAMPLING_RATE.getName()) && entry.getValue().equals("0.0"))) { + serializeMDCEntry(entry, serializer); + } + } + } + } + + private void serializeException(ILoggingEvent event, JsonSerializer serializer) { IThrowableProxy throwableProxy = event.getThrowableProxy(); if (throwableProxy != null) { if (throwableConverter != null) { - LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), + serializeException(serializer, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableConverter.convert(event)); } else if (throwableProxy instanceof ThrowableProxy) { - LambdaJsonSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable()); + Throwable throwable = ((ThrowableProxy) throwableProxy).getThrowable(); + serializeException(serializer, throwable.getClass().getName(), throwable.getMessage(), + Arrays.toString(throwable.getStackTrace())); } else { - LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), - throwableProxy.getMessage(), throwableProxyConverter.convert(event)); + serializeException(serializer, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert( + event)); } } - LambdaJsonSerializer.serializePowertools(builder, event.getMDCPropertyMap(), includePowertoolsInfo); - if (includeThreadInfo) { - LambdaJsonSerializer.serializeThreadName(builder, event.getThreadName()); - LambdaJsonSerializer.serializeThreadId(builder, String.valueOf(Thread.currentThread().getId())); - LambdaJsonSerializer.serializeThreadPriority(builder, String.valueOf(Thread.currentThread().getPriority())); - } - LambdaJsonSerializer.serializeTimestamp(builder, event.getTimeStamp(), timestampFormat, - timestampFormatTimezoneId); - LambdaJsonSerializer.serializeObjectEnd(builder); - return builder.toString().getBytes(UTF_8); } @Override public byte[] footerBytes() { - return null; + return new byte[0]; } /** @@ -191,18 +249,12 @@ public void setIncludePowertoolsInfo(boolean includePowertoolsInfo) { this.includePowertoolsInfo = includePowertoolsInfo; } - /** - * Specify if messages should be logged as JSON, without escaping string (default is false): - *
- *
{@code
-     *     
-     *         true
-     *     
-     * }
- * - * @param logMessagesAsJson if messages should be looged as JSON (non escaped quotes) - */ - public void setLogMessagesAsJson(boolean logMessagesAsJson) { - this.logMessagesAsJsonGlobal = logMessagesAsJson; + private void serializeException(JsonSerializer serializer, String className, String message, String stackTrace) { + Map map = new HashMap<>(); + map.put(EXCEPTION_MSG_ATTR_NAME, message); + map.put(EXCEPTION_CLASS_ATTR_NAME, className); + map.put(EXCEPTION_STACK_ATTR_NAME, stackTrace); + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_ATTR_NAME, map); } } diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/JsonUtils.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/JsonUtils.java deleted file mode 100644 index e604d10c7..000000000 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/JsonUtils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - */ - -package software.amazon.lambda.powertools.logging.logback.internal; - -/** - * Json tools to serialize attributes manually, to avoid using further dependencies (jackson, gson...) - */ -public class JsonUtils { - - private JsonUtils() { - // static utils - } - - protected static void serializeAttribute(StringBuilder builder, String attr, String value, boolean notBegin) { - if (value != null) { - if (notBegin) { - builder.append(","); - } - builder.append("\"").append(attr).append("\":"); - boolean isString = isString(value); - if (isString) { - builder.append("\""); - } - builder.append(value); - if (isString) { - builder.append("\""); - } - } - } - - protected static void serializeAttribute(StringBuilder builder, String attr, String value) { - serializeAttribute(builder, attr, value, true); - } - - protected static void serializeMessage(StringBuilder builder, String attr, String value, boolean logAsJson) { - builder.append(","); - builder.append("\"").append(attr).append("\":"); - if (logAsJson) { - builder.append(value); // log JSON without quotes - } else { - builder.append("\""); - builder.append(value.replace("\"", "\\\"")); // escape quotes in string - builder.append("\""); - } - } - - - protected static void serializeAttributeAsString(StringBuilder builder, String attr, String value, - boolean notBegin) { - if (value != null) { - if (notBegin) { - builder.append(","); - } - builder.append("\"") - .append(attr) - .append("\":\"") - .append(value) - .append("\""); - } - } - - protected static void serializeAttributeAsString(StringBuilder builder, String attr, String value) { - serializeAttributeAsString(builder, attr, value, true); - } - - /** - * As MDC is a {@code Map}, we need to check the type - * to output numbers and booleans correctly (without quotes) - */ - private static boolean isString(String str) { - if (str == null) { - return true; - } - if ("true".equals(str) || "false".equals(str)) { - return false; // boolean - } - return !isNumeric(str); // number - } - - /** - * Taken from commons-lang3 NumberUtils to avoid include the library - */ - private static boolean isNumeric(final String str) { - if (str == null || str.isEmpty()) { - return false; - } - if (str.charAt(str.length() - 1) == '.') { - return false; - } - if (str.charAt(0) == '-') { - if (str.length() == 1) { - return false; - } - return withDecimalsParsing(str, 1); - } - return withDecimalsParsing(str, 0); - } - - /** - * Taken from commons-lang3 NumberUtils - */ - private static boolean withDecimalsParsing(final String str, final int beginIdx) { - int decimalPoints = 0; - for (int i = beginIdx; i < str.length(); i++) { - final boolean isDecimalPoint = str.charAt(i) == '.'; - if (isDecimalPoint) { - decimalPoints++; - } - if (decimalPoints > 1) { - return false; - } - if (!isDecimalPoint && !Character.isDigit(str.charAt(i))) { - return false; - } - } - return true; - } -} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaEcsSerializer.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaEcsSerializer.java deleted file mode 100644 index bab1a32fc..000000000 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaEcsSerializer.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - */ - -package software.amazon.lambda.powertools.logging.logback.internal; - -import ch.qos.logback.classic.Level; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.Map; -import java.util.TimeZone; -import java.util.TreeMap; -import java.util.regex.Matcher; -import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; - -/** - * This class will serialize the log events in ecs format (ElasticSearch).
- *

- * Inspired from the ElasticSearch Serializer co.elastic.logging.EcsJsonSerializer - */ -public class LambdaEcsSerializer { - protected static final String TIMESTAMP_ATTR_NAME = "@timestamp"; - protected static final String ECS_VERSION_ATTR_NAME = "ecs.version"; - protected static final String LOGGER_ATTR_NAME = "log.logger"; - protected static final String LEVEL_ATTR_NAME = "log.level"; - protected static final String SERVICE_NAME_ATTR_NAME = "service.name"; - protected static final String SERVICE_VERSION_ATTR_NAME = "service.version"; - protected static final String SERVICE_ENV_ATTR_NAME = "service.environment"; - protected static final String EVENT_DATASET_ATTR_NAME = "event.dataset"; - protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; - protected static final String THREAD_ATTR_NAME = "process.thread.name"; - protected static final String THREAD_ID_ATTR_NAME = "process.thread.id"; - protected static final String EXCEPTION_MSG_ATTR_NAME = "error.message"; - protected static final String EXCEPTION_CLASS_ATTR_NAME = "error.type"; - protected static final String EXCEPTION_STACK_ATTR_NAME = "error.stack_trace"; - protected static final String CLOUD_PROVIDER_ATTR_NAME = "cloud.provider"; - protected static final String CLOUD_REGION_ATTR_NAME = "cloud.region"; - protected static final String CLOUD_ACCOUNT_ATTR_NAME = "cloud.account.id"; - protected static final String CLOUD_SERVICE_ATTR_NAME = "cloud.service.name"; - protected static final String FUNCTION_COLD_START_ATTR_NAME = "faas.coldstart"; - protected static final String FUNCTION_REQUEST_ID_ATTR_NAME = "faas.execution"; - protected static final String FUNCTION_ARN_ATTR_NAME = "faas.id"; - protected static final String FUNCTION_NAME_ATTR_NAME = "faas.name"; - protected static final String FUNCTION_VERSION_ATTR_NAME = "faas.version"; - protected static final String FUNCTION_MEMORY_ATTR_NAME = "faas.memory"; - protected static final String FUNCTION_TRACE_ID_ATTR_NAME = "trace.id"; - - private LambdaEcsSerializer() {} - - public static void serializeObjectStart(StringBuilder builder) { - builder.append('{'); - } - - public static void serializeObjectEnd(StringBuilder builder) { - builder.append("}\n"); - } - - public static void serializeTimestamp(StringBuilder builder, long timestamp, String timestampFormat, - String timestampFormatTimezoneId) { - String formattedTimestamp; - if (timestampFormat == null || timestamp < 0) { - formattedTimestamp = String.valueOf(timestamp); - } else { - Date date = new Date(timestamp); - DateFormat format = new SimpleDateFormat(timestampFormat); - - if (timestampFormatTimezoneId != null) { - TimeZone tz = TimeZone.getTimeZone(timestampFormatTimezoneId); - format.setTimeZone(tz); - } - formattedTimestamp = format.format(date); - } - JsonUtils.serializeAttributeAsString(builder, TIMESTAMP_ATTR_NAME, formattedTimestamp, false); - } - - public static void serializeThreadName(StringBuilder builder, String threadName) { - if (threadName != null) { - JsonUtils.serializeAttributeAsString(builder, THREAD_ATTR_NAME, threadName); - } - } - - public static void serializeLogLevel(StringBuilder builder, Level level) { - JsonUtils.serializeAttributeAsString(builder, LEVEL_ATTR_NAME, level.toString()); - } - - public static void serializeFormattedMessage(StringBuilder builder, String formattedMessage) { - JsonUtils.serializeAttributeAsString(builder, FORMATTED_MESSAGE_ATTR_NAME, - formattedMessage.replace("\"", Matcher.quoteReplacement("\\\""))); - } - - public static void serializeException(StringBuilder builder, String className, String message, String stackTrace) { - JsonUtils.serializeAttributeAsString(builder, EXCEPTION_MSG_ATTR_NAME, message); - JsonUtils.serializeAttributeAsString(builder, EXCEPTION_CLASS_ATTR_NAME, className); - JsonUtils.serializeAttributeAsString(builder, EXCEPTION_STACK_ATTR_NAME, stackTrace); - } - - public static void serializeException(StringBuilder builder, Throwable throwable) { - serializeException(builder, throwable.getClass().getName(), throwable.getMessage(), - Arrays.toString(throwable.getStackTrace())); - } - - public static void serializeThreadId(StringBuilder builder, String threadId) { - JsonUtils.serializeAttributeAsString(builder, THREAD_ID_ATTR_NAME, threadId); - } - - public static void serializeAdditionalFields(StringBuilder builder, Map mdc) { - TreeMap sortedMap = new TreeMap<>(mdc); - - sortedMap.forEach((k, v) -> { - if (!PowertoolsLoggedFields.stringValues().contains(k)) { - JsonUtils.serializeAttributeAsString(builder, k, v); - } - }); - } - - public static void serializeEcsVersion(StringBuilder builder, String ecsVersion) { - JsonUtils.serializeAttributeAsString(builder, ECS_VERSION_ATTR_NAME, ecsVersion); - } - - public static void serializeServiceName(StringBuilder builder, String serviceName) { - JsonUtils.serializeAttributeAsString(builder, SERVICE_NAME_ATTR_NAME, serviceName); - } - - public static void serializeServiceVersion(StringBuilder builder, String serviceVersion) { - JsonUtils.serializeAttributeAsString(builder, SERVICE_VERSION_ATTR_NAME, serviceVersion); - } - - public static void serializeLoggerName(StringBuilder builder, String loggerName) { - JsonUtils.serializeAttributeAsString(builder, LOGGER_ATTR_NAME, loggerName); - } - - public static void serializeCloudProvider(StringBuilder builder, String cloudProvider) { - JsonUtils.serializeAttributeAsString(builder, CLOUD_PROVIDER_ATTR_NAME, cloudProvider); - } - - public static void serializeCloudService(StringBuilder builder, String cloudService) { - JsonUtils.serializeAttributeAsString(builder, CLOUD_SERVICE_ATTR_NAME, cloudService); - } - - public static void serializeCloudRegion(StringBuilder builder, String cloudRegion) { - JsonUtils.serializeAttributeAsString(builder, CLOUD_REGION_ATTR_NAME, cloudRegion); - } - - public static void serializeCloudAccountId(StringBuilder builder, String cloudAccountId) { - JsonUtils.serializeAttributeAsString(builder, CLOUD_ACCOUNT_ATTR_NAME, cloudAccountId); - } - - public static void serializeColdStart(StringBuilder builder, String coldStart) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_COLD_START_ATTR_NAME, coldStart); - } - - public static void serializeFunctionExecutionId(StringBuilder builder, String requestId) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_REQUEST_ID_ATTR_NAME, requestId); - } - - public static void serializeFunctionId(StringBuilder builder, String functionArn) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_ARN_ATTR_NAME, functionArn); - } - - public static void serializeFunctionName(StringBuilder builder, String functionName) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_NAME_ATTR_NAME, functionName); - } - - public static void serializeFunctionVersion(StringBuilder builder, String functionVersion) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_VERSION_ATTR_NAME, functionVersion); - } - - public static void serializeFunctionMemory(StringBuilder builder, String functionMemory) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_MEMORY_ATTR_NAME, functionMemory); - } - - public static void serializeTraceId(StringBuilder builder, String traceId) { - JsonUtils.serializeAttributeAsString(builder, FUNCTION_TRACE_ID_ATTR_NAME, traceId); - } -} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaJsonSerializer.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaJsonSerializer.java deleted file mode 100644 index 7d7b8d0d7..000000000 --- a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaJsonSerializer.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - */ - -package software.amazon.lambda.powertools.logging.logback.internal; - -import static java.lang.Boolean.TRUE; -import static software.amazon.lambda.powertools.logging.LoggingUtils.LOG_MESSAGES_AS_JSON; - -import ch.qos.logback.classic.Level; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.Map; -import java.util.TimeZone; -import java.util.TreeMap; -import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; - -/** - * This class will serialize the log events in json.
- *

- * Inspired from the ElasticSearch Serializer co.elastic.logging.EcsJsonSerializer - */ -public class LambdaJsonSerializer { - protected static final String TIMESTAMP_ATTR_NAME = "timestamp"; - protected static final String LEVEL_ATTR_NAME = "level"; - protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; - protected static final String THREAD_ATTR_NAME = "thread"; - protected static final String THREAD_ID_ATTR_NAME = "thread_id"; - protected static final String THREAD_PRIORITY_ATTR_NAME = "thread_priority"; - protected static final String EXCEPTION_MSG_ATTR_NAME = "message"; - protected static final String EXCEPTION_CLASS_ATTR_NAME = "name"; - protected static final String EXCEPTION_STACK_ATTR_NAME = "stack"; - protected static final String EXCEPTION_ATTR_NAME = "error"; - - private LambdaJsonSerializer() {} - - public static void serializeObjectStart(StringBuilder builder) { - builder.append('{'); - } - - public static void serializeObjectEnd(StringBuilder builder) { - builder.append("}\n"); - } - - public static void serializeTimestamp(StringBuilder builder, long timestamp, String timestampFormat, - String timestampFormatTimezoneId) { - String formattedTimestamp; - if (timestampFormat == null || timestamp < 0) { - formattedTimestamp = String.valueOf(timestamp); - } else { - Date date = new Date(timestamp); - DateFormat format = new SimpleDateFormat(timestampFormat); - - if (timestampFormatTimezoneId != null) { - TimeZone tz = TimeZone.getTimeZone(timestampFormatTimezoneId); - format.setTimeZone(tz); - } - formattedTimestamp = format.format(date); - } - JsonUtils.serializeAttribute(builder, TIMESTAMP_ATTR_NAME, formattedTimestamp); - } - - public static void serializeThreadName(StringBuilder builder, String threadName) { - if (threadName != null) { - JsonUtils.serializeAttribute(builder, THREAD_ATTR_NAME, threadName); - } - } - - public static void serializeLogLevel(StringBuilder builder, Level level) { - JsonUtils.serializeAttribute(builder, LEVEL_ATTR_NAME, level.toString(), false); - } - - public static void serializeFormattedMessage(StringBuilder builder, String message, - boolean logMessagesAsJsonGlobal, String logMessagesAsJsonLocal) { - Boolean logMessagesAsJson = null; - if (logMessagesAsJsonLocal != null) { - logMessagesAsJson = Boolean.parseBoolean(logMessagesAsJsonLocal); - } - - boolean logAsJson = ((logMessagesAsJsonGlobal && logMessagesAsJson == null) || TRUE.equals(logMessagesAsJson)) - && isValidJson(message); - JsonUtils.serializeMessage(builder, FORMATTED_MESSAGE_ATTR_NAME, message, logAsJson); - } - - public static void serializeException(StringBuilder builder, String className, String message, String stackTrace) { - builder.append(",\"").append(EXCEPTION_ATTR_NAME).append("\":{"); - JsonUtils.serializeAttribute(builder, EXCEPTION_MSG_ATTR_NAME, message, false); - JsonUtils.serializeAttribute(builder, EXCEPTION_CLASS_ATTR_NAME, className); - JsonUtils.serializeAttribute(builder, EXCEPTION_STACK_ATTR_NAME, stackTrace); - builder.append("}"); - } - - public static void serializeException(StringBuilder builder, Throwable throwable) { - serializeException(builder, throwable.getClass().getName(), throwable.getMessage(), - Arrays.toString(throwable.getStackTrace())); - } - - public static void serializeThreadId(StringBuilder builder, String threadId) { - JsonUtils.serializeAttribute(builder, THREAD_ID_ATTR_NAME, threadId); - } - - public static void serializeThreadPriority(StringBuilder builder, String threadPriority) { - JsonUtils.serializeAttribute(builder, THREAD_PRIORITY_ATTR_NAME, threadPriority); - } - - public static void serializePowertools(StringBuilder builder, Map mdc, - boolean includePowertoolsInfo) { - TreeMap sortedMap = new TreeMap<>(mdc); - sortedMap.forEach((k, v) -> { - if ((includePowertoolsInfo || !PowertoolsLoggedFields.stringValues().contains(k)) // do not log already logged powertools info - && !(k.equals(PowertoolsLoggedFields.SAMPLING_RATE.getName()) && v.equals("0.0")) // do not log sampling rate when 0 - && !LOG_MESSAGES_AS_JSON.equals(k)) // do not log internal keys - { - JsonUtils.serializeAttribute(builder, k, v); - } - }); - - } - - private static final ObjectMapper mapper = new ObjectMapper() - .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); - - private static boolean isValidJson(String str) { - if (!(str.startsWith("{") || str.startsWith("["))) { - return false; - } - try { - mapper.readTree(str); - } catch (JacksonException e) { - return false; - } - return true; - } - -} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java index dc8ac429b..638857cb3 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java @@ -14,13 +14,13 @@ package software.amazon.lambda.powertools.logging.internal; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; -import static software.amazon.lambda.powertools.logging.LoggingUtils.LOG_MESSAGES_AS_JSON; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; @@ -28,11 +28,14 @@ import ch.qos.logback.classic.spi.LoggingEvent; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -42,7 +45,7 @@ import java.util.Date; import java.util.TimeZone; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -50,10 +53,16 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.logging.logback.LambdaJsonEncoder; -import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsJsonMessage; +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.argument.StructuredArguments; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments; import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; -import software.amazon.lambda.powertools.utilities.JsonConfig; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEvent; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventDisabled; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventForStream; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponse; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponseForStream; +import software.amazon.lambda.powertools.logging.logback.LambdaJsonEncoder; @Order(2) class LambdaJsonEncoderTest { @@ -62,11 +71,6 @@ class LambdaJsonEncoderTest { @Mock private Context context; - @BeforeAll - private static void init() { - JsonConfig.get().getObjectMapper().setSerializationInclusion(NON_NULL); - } - @BeforeEach void setUp() throws IllegalAccessException, IOException { openMocks(this); @@ -97,13 +101,37 @@ void shouldLogInJsonFormat() { // THEN File logFile = new File("target/logfile.json"); assertThat(contentOf(logFile)).contains( - "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"function_memory_size\":1024,\"function_name\":\"testFunction\",\"function_request_id\":\"RequestId\",\"function_version\":1,\"myKey\":\"myValue\",\"service\":\"testLogback\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"timestamp\":"); + "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"function_memory_size\":1024,\"function_name\":\"testFunction\",\"function_request_id\":\"RequestId\",\"function_version\":1,\"service\":\"testLogback\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\",\"timestamp\":"); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingRawJson() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.JSON); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + .contains("\"message\":\"Message body = plop and id = \"1212abcd\"\""); } @Test - void shouldLogJsonMessageWithoutEscapedStrings() { + void shouldLogArgumentsAsJsonWhenUsingKeyValue() { // GIVEN - PowertoolsJsonMessage requestHandler = new PowertoolsJsonMessage(); + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.ENTRY); SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); msg.setMessageId("1212abcd"); msg.setBody("plop"); @@ -119,10 +147,9 @@ void shouldLogJsonMessageWithoutEscapedStrings() { // THEN File logFile = new File("target/logfile.json"); assertThat(contentOf(logFile)) - .contains("\"message\":{\"messageId\":\"1212abcd\",\"body\":\"plop\",\"eventSource\":\"eb\",\"awsRegion\":\"eu-west-1\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}}}") + .contains("\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") .contains("\"message\":\"1212abcd\"") - .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") - .doesNotContain(LOG_MESSAGES_AS_JSON); + .contains("\"message\":\"Message body = plop and id = \"1212abcd\"\""); } private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); @@ -159,10 +186,9 @@ void shouldNotLogPowertoolsInfo() { } @Test - void shouldLogMessagesAsJsonWhenEnabledInLogbackConfig() throws JsonProcessingException { + void shouldLogStructuredArgumentsAsNewEntries() { // GIVEN LambdaJsonEncoder encoder = new LambdaJsonEncoder(); - encoder.setLogMessagesAsJson(true); SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); msg.setMessageId("1212abcd"); @@ -172,24 +198,144 @@ void shouldLogMessagesAsJsonWhenEnabledInLogbackConfig() throws JsonProcessingEx SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + StructuredArgument argument = StructuredArguments.entry("msg", msg); // WHEN - LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, JsonConfig.get().getObjectMapper().writeValueAsString(msg), null, null); + LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "A message", null, new Object[]{argument}); byte[] encoded = encoder.encode(loggingEvent); String result = new String(encoded, StandardCharsets.UTF_8); // THEN (logged as JSON) assertThat(result) - .contains("\"message\":{\"messageId\":\"1212abcd\",\"body\":\"plop\",\"eventSource\":\"eb\",\"awsRegion\":\"eu-west-1\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}}}"); + .contains("\"message\":\"A message\",\"msg\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}"); + } - // WHEN (disabling logging as json) - encoder.setLogMessagesAsJson(false); - encoded = encoder.encode(loggingEvent); - result = new String(encoded, StandardCharsets.UTF_8); + @Test + void shouldLogEventForHandlerWithLogEventAnnotation() { + // GIVEN + PowertoolsLogEvent requestHandler = new PowertoolsLogEvent(); - // THEN (logged as String) - assertThat(result) - .contains("\"message\":\"{\\\"messageId\\\":\\\"1212abcd\\\",\\\"body\\\":\\\"plop\\\",\\\"eventSource\\\":\\\"eb\\\",\\\"awsRegion\\\":\\\"eu-west-1\\\",\\\"messageAttributes\\\":{\\\"keyAttribute\\\":{\\\"stringListValues\\\":[\\\"val1\\\",\\\"val2\\\",\\\"val3\\\"]}}}\""); + // WHEN + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("\"event\":[\"ListOfOneElement\"]"); + } + + @Test + void shouldLogEventForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = true; + + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); + message.setBody("body"); + message.setMessageId("1234abcd"); + message.setAwsRegion("eu-west-1"); + + // WHEN + requestHandler.handleRequest(message, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Event\"") + .contains("\"event\":{\"awsRegion\":\"eu-west-1\",\"body\":\"body\",\"messageId\":\"1234abcd\"}"); + } finally { + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + } + } + + @Test + void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + + // WHEN + PowertoolsLogEventDisabled requestHandler = new PowertoolsLogEventDisabled(); + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + Assertions.assertEquals(0, + Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + } + + @Test + void shouldLogEventAsStringForStreamHandler() throws IOException { + // GIVEN + PowertoolsLogEventForStream requestStreamHandler = new PowertoolsLogEventForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(Collections.singletonMap("key", "value"))), output, context); + + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isNotEmpty(); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Event\"") + .contains("\"event\":\"{\"key\":\"value\"}\""); // logged as String for StreamHandler + } + + @Test + void shouldLogResponseForHandlerWithLogResponseAnnotation() { + // GIVEN + PowertoolsLogResponse requestHandler = new PowertoolsLogResponse(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"Hola mundo\""); + } + + @Test + void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; + + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"Bonjour le monde\""); + } finally { + LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + } + } + + @Test + void shouldLogResponseForStreamHandler() throws IOException { + // GIVEN + PowertoolsLogResponseForStream requestStreamHandler = new PowertoolsLogResponseForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + String input = "BobThe Sponge"; + + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), output, context); + + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isEqualTo(input); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\""+input+"\""); } @Test @@ -238,7 +384,10 @@ void shouldLogException() { String result = new String(encoded, StandardCharsets.UTF_8); // THEN - assertThat(result).contains("\"message\":\"Error\",\"error\":{\"message\":\"Unexpected value\",\"name\":\"java.lang.IllegalStateException\",\"stack\":\"[software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest.shouldLogException"); + assertThat(result).contains("\"message\":\"Error\",\"error\":{") + .contains("\"message\":\"Unexpected value\"") + .contains("\"name\":\"java.lang.IllegalStateException\"") + .contains("\"stack\":\"[software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest.shouldLogException"); // WHEN (configure a custom throwableConverter) encoder = new LambdaJsonEncoder(); @@ -249,7 +398,9 @@ void shouldLogException() { result = new String(encoded, StandardCharsets.UTF_8); // THEN (stack is logged with root cause first) - assertThat(result).contains("\"message\":\"Error\",\"error\":{\"message\":\"Unexpected value\",\"name\":\"java.lang.IllegalStateException\",\"stack\":\"java.lang.IllegalStateException: Unexpected value\n"); + assertThat(result).contains("\"message\":\"Unexpected value\"") + .contains("\"name\":\"java.lang.IllegalStateException\"") + .contains("\"stack\":\"java.lang.IllegalStateException: Unexpected value\n"); } private void setupContext() { diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java similarity index 62% rename from powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java index fdc279319..387074590 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java @@ -14,26 +14,38 @@ package software.amazon.lambda.powertools.logging.internal.handler; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.fasterxml.jackson.core.JsonProcessingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.logging.argument.StructuredArguments; import software.amazon.lambda.powertools.utilities.JsonConfig; -public class PowertoolsJsonMessage implements RequestHandler { - private final Logger LOG = LoggerFactory.getLogger(PowertoolsJsonMessage.class); +public class PowertoolsArguments implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsArguments.class); + private final ArgumentFormat argumentFormat; + + public PowertoolsArguments(ArgumentFormat argumentFormat) { + this.argumentFormat = argumentFormat; + } @Override @Logging(clearState = true) public String handleRequest(SQSEvent.SQSMessage input, Context context) { try { - LoggingUtils.logMessagesAsJson(true); - LoggingUtils.setCorrelationId(input.getMessageId()); - LOG.debug(JsonConfig.get().getObjectMapper().writeValueAsString(input)); + MDC.put(CORRELATION_ID.getName(), input.getMessageId()); + if (argumentFormat == ArgumentFormat.JSON) { + LOG.debug("SQS Event", StructuredArguments.json("input", + JsonConfig.get().getObjectMapper().writeValueAsString(input))); + } else { + LOG.debug("SQS Event", StructuredArguments.entry("input", input)); + } LOG.debug("{}", input.getMessageId()); LOG.warn("Message body = {} and id = \"{}\"", input.getBody(), input.getMessageId()); } catch (JsonProcessingException e) { @@ -41,4 +53,8 @@ public String handleRequest(SQSEvent.SQSMessage input, Context context) { } return input.getMessageId(); } + + public enum ArgumentFormat { + JSON, ENTRY + } } diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java index faa722756..e8c0c5851 100644 --- a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java @@ -18,8 +18,8 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; public class PowertoolsLogEnabled implements RequestHandler { private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); @@ -27,8 +27,8 @@ public class PowertoolsLogEnabled implements RequestHandler { @Override @Logging(clearState = true) public Object handleRequest(Object input, Context context) { - LoggingUtils.appendKey("myKey", "myValue"); + MDC.put("myKey", "myValue"); LOG.debug("Test debug event"); - return null; + return "Bonjour le monde"; } } diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEvent.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEvent.java new file mode 100644 index 000000000..14a7874cb --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEvent implements RequestHandler { + + @Override + @Logging(logEvent = true) + public Object handleRequest(Object input, Context context) { + return null; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventDisabled.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventDisabled.java similarity index 90% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventDisabled.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventDisabled.java index fc1feb52d..8171bee3e 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventDisabled.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventDisabled.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.logging.handlers; +package software.amazon.lambda.powertools.logging.internal.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -20,8 +20,8 @@ public class PowertoolsLogEventDisabled implements RequestHandler { - @Logging @Override + @Logging(logEvent = false) public Object handleRequest(Object input, Context context) { return null; } diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventForStream.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventForStream.java new file mode 100644 index 000000000..443051204 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventForStream.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEventForStream implements RequestStreamHandler { + + @Override + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(outputStream, mapper.readValue(inputStream, Map.class)); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponse.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponse.java new file mode 100644 index 000000000..5cbeef487 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogResponse implements RequestHandler { + + @Override + @Logging(logResponse = true) + public Object handleRequest(Object input, Context context) { + return "Hola mundo"; + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponseForStream.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponseForStream.java new file mode 100644 index 000000000..3378b9421 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponseForStream.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogResponseForStream implements RequestStreamHandler { + + @Override + @Logging(logResponse = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + byte[] buf = new byte[1024]; + int length; + while ((length = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, length); + } + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java index 05a9cfe31..9e5e735d1 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java @@ -57,7 +57,8 @@ *

By default {@code Logging} will not log the event which has trigger the invoke of the Lambda function. * This can be enabled using {@code @Logging(logEvent = true)}.

* - *

To append additional keys to each log entry you can use {@link LoggingUtils#appendKey(String, String)}

+ *

To append additional keys to each log entry you can either use {@link org.slf4j.MDC#put(String, String)} + * or {@link software.amazon.lambda.powertools.logging.argument.StructuredArguments}

*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java deleted file mode 100644 index d7ceb8ccd..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - */ - -package software.amazon.lambda.powertools.logging; - -import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Arrays; -import java.util.Map; -import org.slf4j.MDC; -import software.amazon.lambda.powertools.utilities.JsonConfig; - -/** - * A class of helper functions to add functionality to Logging. - * Adding/removing keys is based on MDC, which is ThreadSafe. - */ -public final class LoggingUtils { - - public static final String LOG_MESSAGES_AS_JSON = "PowertoolsLogMessagesAsJson"; - - private static ObjectMapper objectMapper; - - private LoggingUtils() { - } - - /** - * Appends an additional key and value to each log entry made. Duplicate values - * for the same key will be replaced with the latest. - * - * @param key The name of the key to be logged - * @param value The value to be logged - */ - public static void appendKey(String key, String value) { - MDC.put(key, value); - } - - - /** - * Appends additional key and value to each log entry made. Duplicate values - * for the same key will be replaced with the latest. - * - * @param customKeys Map of custom keys values to be appended to logs - */ - public static void appendKeys(Map customKeys) { - customKeys.forEach(MDC::put); - } - - /** - * Remove an additional key from log entry. - * - * @param customKey The name of the key to be logged - */ - public static void removeKey(String customKey) { - MDC.remove(customKey); - } - - - /** - * Removes additional keys from log entry. - * - * @param keys Map of custom keys values to be appended to logs - */ - public static void removeKeys(String... keys) { - Arrays.stream(keys).forEach(MDC::remove); - } - - /** - * Sets correlation id attribute on the logs. - * - * @param value The value of the correlation id - */ - public static void setCorrelationId(String value) { - MDC.put(CORRELATION_ID.getName(), value); - } - - /** - * Get correlation id attribute. Maybe null. - * @return correlation id set `@Logging(correlationIdPath="JMESPATH Expression")` or `LoggingUtils.setCorrelationId("value")` - */ - public static String getCorrelationId() { - return MDC.get(CORRELATION_ID.getName()); - } - - /** - * When set to true, will log messages as JSON (without escaping string). - * Useful to log events or big JSON objects. - * @param value boolean to specify if yes or no messages should be logged as JSON (default is false) - */ - public static void logMessagesAsJson(boolean value) { - MDC.put(LOG_MESSAGES_AS_JSON, String.valueOf(value)); - } - - /** - * Sets the instance of ObjectMapper object which is used for serialising event when - * {@code @Logging(logEvent = true, logResponse = true)}. - * - * Not Thread Safe, the object mapper is static, changing it in different threads can lead to unexpected behaviour - * - * @param objectMapper Custom implementation of object mapper to be used for logging serialised event - */ - public static void setObjectMapper(ObjectMapper objectMapper) { - LoggingUtils.objectMapper = objectMapper; - } - - public static ObjectMapper getObjectMapper() { - if (LoggingUtils.objectMapper == null) { - LoggingUtils.objectMapper = JsonConfig.get().getObjectMapper(); - } - return LoggingUtils.objectMapper; - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/ArrayArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/ArrayArgument.java new file mode 100644 index 000000000..28b29146e --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/ArrayArgument.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Objects; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#array(String, Object...)} + */ +public class ArrayArgument implements StructuredArgument { + private final String key; + private final Object[] values; + + public ArrayArgument(String key, Object[] values) { + this.key = Objects.requireNonNull(key, "Key must not be null"); + this.values = new Object[values.length]; + System.arraycopy(values, 0, this.values, 0, values.length); + } + + @Override + public void writeTo(JsonSerializer serializer) { + serializer.writeFieldName(key); + serializer.writeObject(values); + } + + @Override + public String toString() { + return key + "=" + StructuredArguments.toString(values); + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/JsonArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/JsonArgument.java new file mode 100644 index 000000000..e14f23788 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/JsonArgument.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Objects; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#json(String, String)} + */ +public class JsonArgument implements StructuredArgument { + private final String key; + private final String rawJson; + + public JsonArgument(String key, String rawJson) { + this.key = Objects.requireNonNull(key, "key must not be null"); + this.rawJson = Objects.requireNonNull(rawJson, "rawJson must not be null"); + } + + @Override + public void writeTo(JsonSerializer serializer) { + serializer.writeFieldName(key); + serializer.writeRaw(rawJson); + } + + @Override + public String toString() { + return key + "=" + rawJson; + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/KeyValueArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/KeyValueArgument.java new file mode 100644 index 000000000..569667419 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/KeyValueArgument.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Objects; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#entry(String, Object)} + */ +public class KeyValueArgument implements StructuredArgument { + private final String key; + private final Object value; + + public KeyValueArgument(String key, Object value) { + this.key = Objects.requireNonNull(key, "Key must not be null"); + this.value = value; + } + + @Override + public void writeTo(JsonSerializer serializer) { + serializer.writeObjectField(key, value); + } + + @Override + public String toString() { + return key + "=" + StructuredArguments.toString(value); + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/MapArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/MapArgument.java new file mode 100644 index 000000000..9a06ea095 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/MapArgument.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.HashMap; +import java.util.Map; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#entries(Map)} + */ +public class MapArgument implements StructuredArgument { + private final Map map; + + public MapArgument(Map map) { + if (map != null) { + this.map = new HashMap<>(map); + } else { + this.map = null; + } + } + + @Override + public void writeTo(JsonSerializer serializer) { + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + serializer.writeObjectField(String.valueOf(entry.getKey()), entry.getValue()); + } + } + } + + @Override + public String toString() { + return String.valueOf(map); + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArgument.java new file mode 100644 index 000000000..21fea068d --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArgument.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.io.IOException; +import org.slf4j.Logger; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * A wrapper for an argument passed to a log method (e.g. {@link Logger#info(String, Object...)}) + * that adds data to the JSON event. + */ +public interface StructuredArgument { + /** + * Writes the data associated with this argument to the given {@link JsonSerializer}. + * + * @param serializer the {@link JsonSerializer} to produce JSON content + * @throws IOException if an I/O error occurs + */ + void writeTo(JsonSerializer serializer) throws IOException; + + /** + * Writes the data associated with this argument to a {@link String} to be + * included in a log event's formatted message (via parameter substitution). + *

+ * Note that this will only be included in the log event's formatted + * message if the message format includes a parameter for this argument (using {}). + * + * @return String representation of the data associated with this argument + */ + String toString(); + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArguments.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArguments.java new file mode 100644 index 000000000..8a75b3118 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArguments.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Arrays; +import java.util.Map; + +/** + * Factory for creating {@link StructuredArgument}s. + * Inspired from the StructuredArgument of logstash-logback-encoder. + */ +public class StructuredArguments { + + private StructuredArguments() { + // nothing to do, use static methods only + } + + /** + * Adds "key": "value" to the JSON structure and "key=value" to the formatted message. + * + * @param key the field name + * @param value the value associated with the key (can be any kind of object) + * @return a {@link StructuredArgument} populated with the data + */ + public static StructuredArgument entry(String key, Object value) { + return new KeyValueArgument(key, value); + } + + /** + * Adds a "key": "value" to the JSON structure for each entry in the map + * and {@code map.toString()} to the formatted message. + * + * @param map {@link Map} holding the key/value pairs + * @return a {@link MapArgument} populated with the data + */ + public static StructuredArgument entries(Map map) { + return new MapArgument(map); + } + + /** + * Adds a field to the JSON structure with key as the key and where value + * is a JSON array of objects AND a string version of the array to the formatted message: + * {@code "key": [value, value]} + * + * @param key the field name + * @param values elements of the array associated with the key + * @return an {@link ArrayArgument} populated with the data + */ + public static StructuredArgument array(String key, Object... values) { + return new ArrayArgument(key, values); + } + + /** + * Adds the {@code rawJson} to the JSON structure and + * the {@code rawJson} to the formatted message. + * + * @param key the field name + * @param rawJson the raw JSON String + * @return a {@link JsonArgument} populated with the data + */ + public static StructuredArgument json(String key, String rawJson) { + return new JsonArgument(key, rawJson); + } + + /** + * Format the argument into a string. + * + * This method mimics the slf4j behavior: + * array objects are formatted as array using {@link Arrays#toString}, + * non array object using {@link String#valueOf}. + * + *

See org.slf4j.helpers.MessageFormatter#deeplyAppendParameter(StringBuilder, Object, Map)} + * + * @param arg the argument to format + * @return formatted string version of the argument + */ + @SuppressWarnings("java:S106") + public static String toString(Object arg) { + + if (arg == null) { + return "null"; + } + + Class argClass = arg.getClass(); + + try { + if (!argClass.isArray()) { + return String.valueOf(arg); + } else { + if (argClass == byte[].class) { + return Arrays.toString((byte[]) arg); + } else if (argClass == short[].class) { + return Arrays.toString((short[]) arg); + } else if (argClass == int[].class) { + return Arrays.toString((int[]) arg); + } else if (argClass == long[].class) { + return Arrays.toString((long[]) arg); + } else if (argClass == char[].class) { + return Arrays.toString((char[]) arg); + } else if (argClass == float[].class) { + return Arrays.toString((float[]) arg); + } else if (argClass == double[].class) { + return Arrays.toString((double[]) arg); + } else if (argClass == boolean[].class) { + return Arrays.toString((boolean[]) arg); + } else { + return Arrays.deepToString((Object[]) arg); + } + } + + } catch (Exception e) { + System.err.println("Failed toString() invocation on an object of type [" + argClass.getName() + "]"); + return "[FAILED toString()]"; + } + } + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonSerializer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonSerializer.java new file mode 100644 index 000000000..0b4359825 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonSerializer.java @@ -0,0 +1,599 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.POJONode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +/** + * A simple JSON serializer. + * Used internally for json serialization, not to be used externally. + * We do not use Jackson as we need to serialize each fields of the log event individually. + * Mainly used by logback as log4j is using its own JsonWriter + */ +public class JsonSerializer implements AutoCloseable { + + private final StringBuilder builder; + + public JsonSerializer(StringBuilder builder) { + super(); + if (builder == null) { + throw new IllegalArgumentException("StringBuilder cannot be null"); + } + this.builder = builder; + } + + public void writeStartArray() { + builder.append('['); + } + + public void writeEndArray() { + builder.append(']'); + } + + public void writeStartObject() { + builder.append('{'); + } + + public void writeEndObject() { + builder.append('}'); + } + + public void writeSeparator() { + writeRaw(','); + } + + public void writeFieldName(String name) { + Objects.requireNonNull(name, "field name must not be null"); + writeString(name); + writeRaw(':'); + } + + public void writeString(String text) { + if (text == null) { + writeNull(); + } else { + builder.append("\"").append(text).append("\""); + } + } + + public void writeRaw(String text) { + builder.append(text); + } + + public void writeRaw(char c) { + builder.append(c); + } + + public void writeNumber(short v) { + builder.append(v); + } + + public void writeNumber(int v) { + builder.append(v); + } + + public void writeNumber(long v) { + builder.append(v); + } + + public void writeNumber(BigInteger v) { + builder.append(v); + } + + public void writeNumber(double v) { + builder.append(v); + } + + public void writeNumber(float v) { + builder.append(v); + } + + public void writeNumber(BigDecimal v) { + builder.append(v.toPlainString()); + } + + public void writeBoolean(boolean state) { + builder.append(state); + } + + public void writeArray(final char[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + builder.append('\''); + builder.append(items[itemIndex]); + builder.append('\''); + } + writeEndArray(); + } + } + + public void writeArray(final boolean[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final boolean item = items[itemIndex]; + writeBoolean(item); + } + writeEndArray(); + } + } + + public void writeArray(final byte[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final byte item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final short[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final short item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final int[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final int item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final long[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final long item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final float[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final float item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final double[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final double item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final Object[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final Object item = items[itemIndex]; + writeObject(item); + } + writeEndArray(); + } + } + + public void writeNull() { + builder.append("null"); + } + + public void writeArray(final List items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final Object item = items.get(itemIndex); + writeObject(item); + } + writeEndArray(); + } + } + + public void writeArray(final Collection items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + Iterator iterator = items.iterator(); + while (iterator.hasNext()) { + writeObject(iterator.next()); + if (iterator.hasNext()) { + writeSeparator(); + } + } + writeEndArray(); + } + } + + public void writeMap(final Map map) { + if (map == null) { + writeNull(); + } else { + writeStartObject(); + for (Iterator> entries = map.entrySet().iterator(); entries.hasNext(); ) { + Map.Entry entry = entries.next(); + writeObjectField(String.valueOf(entry.getKey()), entry.getValue()); + if (entries.hasNext()) { + builder.append(','); + } + } + writeEndObject(); + } + } + + public void writeObject(Object value) { + + // null + if (value == null) { + writeNull(); + } + + else if (value instanceof String) { + writeString((String) value); + } + + // number & boolean + else if (value instanceof Number) { + Number n = (Number) value; + if (n instanceof Integer) { + writeNumber(n.intValue()); + } else if (n instanceof Long) { + writeNumber(n.longValue()); + } else if (n instanceof Double) { + writeNumber(n.doubleValue()); + } else if (n instanceof Float) { + writeNumber(n.floatValue()); + } else if (n instanceof Short) { + writeNumber(n.shortValue()); + } else if (n instanceof Byte) { + writeNumber(n.byteValue()); + } else if (n instanceof BigInteger) { + writeNumber((BigInteger) n); + } else if (n instanceof BigDecimal) { + writeNumber((BigDecimal) n); + } else if (n instanceof AtomicInteger) { + writeNumber(((AtomicInteger) n).get()); + } else if (n instanceof AtomicLong) { + writeNumber(((AtomicLong) n).get()); + } + } else if (value instanceof Boolean) { + writeBoolean((Boolean) value); + } else if (value instanceof AtomicBoolean) { + writeBoolean(((AtomicBoolean) value).get()); + } + + // list & collection + else if (value instanceof List) { + final List list = (List) value; + writeArray(list); + } else if (value instanceof Collection) { + final Collection collection = (Collection) value; + writeArray(collection); + } + + // map + else if (value instanceof Map) { + final Map map = (Map) value; + writeMap(map); + } + + + // arrays + else if (value instanceof char[]) { + final char[] charValues = (char[]) value; + writeArray(charValues); + } else if (value instanceof boolean[]) { + final boolean[] booleanValues = (boolean[]) value; + writeArray(booleanValues); + } else if (value instanceof byte[]) { + final byte[] byteValues = (byte[]) value; + writeArray(byteValues); + } else if (value instanceof short[]) { + final short[] shortValues = (short[]) value; + writeArray(shortValues); + } else if (value instanceof int[]) { + final int[] intValues = (int[]) value; + writeArray(intValues); + } else if (value instanceof long[]) { + final long[] longValues = (long[]) value; + writeArray(longValues); + } else if (value instanceof float[]) { + final float[] floatValues = (float[]) value; + writeArray(floatValues); + } else if (value instanceof double[]) { + final double[] doubleValues = (double[]) value; + writeArray(doubleValues); + } else if (value instanceof Object[]) { + final Object[] values = (Object[]) value; + writeArray(values); + } + + else if (value instanceof JsonNode) { + JsonNode node = (JsonNode) value; + + switch (node.getNodeType()) { + case NULL: + case MISSING: + writeNull(); + break; + + case STRING: + writeString(node.asText()); + break; + + case BOOLEAN: + writeBoolean(node.asBoolean()); + break; + + case NUMBER: + if (node.isInt()) { + writeNumber(node.intValue()); + break; + } + if (node.isLong()) { + writeNumber(node.longValue()); + break; + } + if (node.isShort()) { + writeNumber(node.shortValue()); + break; + } + if (node.isDouble()) { + writeNumber(node.doubleValue()); + break; + } + if (node.isFloat()) { + writeNumber(node.floatValue()); + break; + } + if (node.isBigDecimal()) { + writeNumber(node.decimalValue()); + break; + } + if (node.isBigInteger()) { + writeNumber(node.bigIntegerValue()); + break; + } + break; + case OBJECT: + case POJO: + writeStartObject(); + for (Iterator> entries = node.fields(); entries.hasNext(); ) { + Map.Entry entry = entries.next(); + writeObjectField(entry.getKey(), entry.getValue()); + if (entries.hasNext()) { + builder.append(','); + } + } + writeEndObject(); + return; + + case ARRAY: + ArrayNode arrayNode = (ArrayNode) node; + writeStartArray(); + for (Iterator elements = arrayNode.elements(); elements.hasNext(); ) { + writeObject(elements.next()); + if (elements.hasNext()) { + builder.append(','); + } + } + writeEndArray(); + return; + + default: + break; + } + } else { + try { + // default: try to write object as JSON + writeRaw(JsonConfig.get().getObjectMapper().writeValueAsString(value)); + } catch (Exception e) { + // last chance: toString + writeString(value.toString()); + } + } + } + + public void writeObjectField(String key, Object value) { + writeFieldName(key); + writeObject(value); + } + + public void writeBooleanField(String key, boolean value) { + writeFieldName(key); + writeBoolean(value); + } + + public void writeNullField(String key) { + writeFieldName(key); + writeNull(); + } + + public void writeNumberField(String key, int value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, float value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, short value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, long value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, BigInteger value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, double value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, BigDecimal value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeStringField(String key, String value) { + writeFieldName(key); + writeString(value); + } + + public void writeTree(TreeNode rootNode) { + if (rootNode == null) { + writeNull(); + } else if (rootNode instanceof TextNode) { + writeString(((TextNode) rootNode).asText()); + } else if (rootNode instanceof BooleanNode) { + writeBoolean(((BooleanNode) rootNode).asBoolean()); + } else if (rootNode instanceof NumericNode) { + NumericNode numericNode = (NumericNode) rootNode; + if (numericNode.isInt()) { + writeNumber(numericNode.intValue()); + } else if (numericNode.isLong()) { + writeNumber(numericNode.longValue()); + } else if (numericNode.isShort()) { + writeNumber(numericNode.shortValue()); + } else if (numericNode.isDouble()) { + writeNumber(numericNode.doubleValue()); + } else if (numericNode.isFloat()) { + writeNumber(numericNode.floatValue()); + } else if (numericNode.isBigDecimal()) { + writeNumber(numericNode.decimalValue()); + } else if (numericNode.isBigInteger()) { + writeNumber(numericNode.bigIntegerValue()); + } + } else if (rootNode instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) rootNode; + writeObject(arrayNode); + } else if (rootNode instanceof ObjectNode) { + ObjectNode objectNode = (ObjectNode) rootNode; + writeObject(objectNode); + } else if (rootNode instanceof NullNode || rootNode instanceof MissingNode) { + writeNull(); + } else if (rootNode instanceof POJONode) { + writeObject(((POJONode) rootNode).getPojo()); + } + } + + @Override + public void close() { + // nothing to do + } +} + + diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java index 48f67700d..eccfdae4f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java @@ -15,8 +15,6 @@ package software.amazon.lambda.powertools.logging.internal; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Optional.empty; -import static java.util.Optional.ofNullable; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.extractContext; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; @@ -25,20 +23,19 @@ import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.placedOnRequestHandler; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.placedOnStreamHandler; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey; -import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_LEVEL; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_ERROR; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_EVENT; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_LEVEL; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_RESPONSE; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_SAMPLING_RATE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import io.burt.jmespath.Expression; import java.io.ByteArrayInputStream; @@ -54,7 +51,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.Random; import java.util.ServiceLoader; import org.aspectj.lang.ProceedingJoinPoint; @@ -68,7 +64,6 @@ import org.slf4j.MarkerFactory; import org.slf4j.event.Level; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; import software.amazon.lambda.powertools.utilities.JsonConfig; @@ -188,8 +183,9 @@ public Object around(ProceedingJoinPoint pjp, addLambdaContextToLoggingContext(pjp); - getXrayTraceId().ifPresent(xRayTraceId -> appendKey(FUNCTION_TRACE_ID.getName(), xRayTraceId)); + getXrayTraceId().ifPresent(xRayTraceId -> MDC.put(FUNCTION_TRACE_ID.getName(), xRayTraceId)); + // Log Event Object[] proceedArgs = logEvent(pjp, logging, isOnRequestHandler, isOnRequestStreamHandler); if (!logging.correlationIdPath().isEmpty()) { @@ -210,6 +206,7 @@ public Object around(ProceedingJoinPoint pjp, Object lambdaFunctionResponse; try { + // Call Function Handler lambdaFunctionResponse = pjp.proceed(proceedArgs); } catch (Throwable t) { if (logging.logError() || POWERTOOLS_LOG_ERROR) { @@ -224,6 +221,7 @@ public Object around(ProceedingJoinPoint pjp, coldStartDone(); } + // Log Response if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE)) { if (isOnRequestHandler) { logRequestHandlerResponse(pjp, lambdaFunctionResponse); @@ -255,9 +253,9 @@ private void addLambdaContextToLoggingContext(ProceedingJoinPoint pjp) { Context extractedContext = extractContext(pjp); if (extractedContext != null) { - appendKeys(PowertoolsLoggedFields.setValuesFromLambdaContext(extractedContext)); - appendKey(FUNCTION_COLD_START.getName(), isColdStart() ? "true" : "false"); - appendKey(SERVICE.getName(), serviceName()); + PowertoolsLoggedFields.setValuesFromLambdaContext(extractedContext).forEach(MDC::put); + MDC.put(FUNCTION_COLD_START.getName(), isColdStart() ? "true" : "false"); + MDC.put(SERVICE.getName(), serviceName()); } } @@ -273,7 +271,7 @@ private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, return; } - appendKey(PowertoolsLoggedFields.SAMPLING_RATE.getName(), String.valueOf(samplingRate)); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), String.valueOf(samplingRate)); if (samplingRate == 0) { return; @@ -305,49 +303,45 @@ private double samplingRate(final Logging logging) { return logging.samplingRate(); } + @SuppressWarnings("java:S3457") private void logRequestHandlerEvent(final ProceedingJoinPoint pjp, final Object event) { Logger log = logger(pjp); if (log.isInfoEnabled()) { - LoggingUtils.logMessagesAsJson(true); - asJson(event).ifPresent(log::info); - LoggingUtils.logMessagesAsJson(false); + log.info("Handler Event", entry("event", event)); } } + @SuppressWarnings("java:S3457") private Object[] logRequestStreamHandlerEvent(final ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); Logger log = logger(pjp); if (log.isInfoEnabled()) { - LoggingUtils.logMessagesAsJson(true); try { byte[] bytes = bytesFromInputStreamSafely((InputStream) pjp.getArgs()[0]); args[0] = new ByteArrayInputStream(bytes); // do not log asJson as it can be something else (String, XML...) - log.info("{}", new String(bytes, UTF_8)); + log.info("Handler Event", entry("event", new String(bytes, UTF_8))); } catch (IOException e) { LOG.warn("Failed to log event from supplied input stream.", e); } - LoggingUtils.logMessagesAsJson(false); } return args; } + @SuppressWarnings("java:S3457") private void logRequestHandlerResponse(final ProceedingJoinPoint pjp, final Object response) { Logger log = logger(pjp); if (log.isInfoEnabled()) { - LoggingUtils.logMessagesAsJson(true); - asJson(response).ifPresent(log::info); - LoggingUtils.logMessagesAsJson(false); + log.info("Handler Response", entry("response", response)); } } + @SuppressWarnings("java:S3457") private void logRequestStreamHandlerResponse(final ProceedingJoinPoint pjp, final byte[] bytes) { Logger log = logger(pjp); if (log.isInfoEnabled()) { - LoggingUtils.logMessagesAsJson(true); // we do not log with asJson as it can be something else (String, XML, ...) - log.info("{}", new String(bytes, UTF_8)); - LoggingUtils.logMessagesAsJson(false); + log.info("Handler Response", entry("response", new String(bytes, UTF_8))); } } @@ -356,12 +350,12 @@ private void captureCorrelationId(final String correlationIdPath, final boolean isOnRequestHandler, final boolean isOnRequestStreamHandler) { if (isOnRequestHandler) { - JsonNode jsonNode = LoggingUtils.getObjectMapper().valueToTree(proceedArgs[0]); + JsonNode jsonNode = JsonConfig.get().getObjectMapper().valueToTree(proceedArgs[0]); setCorrelationIdFromNode(correlationIdPath, jsonNode); } else if (isOnRequestStreamHandler) { try { byte[] bytes = bytesFromInputStreamSafely((InputStream) proceedArgs[0]); - JsonNode jsonNode = LoggingUtils.getObjectMapper().readTree(bytes); + JsonNode jsonNode = JsonConfig.get().getObjectMapper().readTree(bytes); proceedArgs[0] = new ByteArrayInputStream(bytes); setCorrelationIdFromNode(correlationIdPath, jsonNode); @@ -377,7 +371,7 @@ private void setCorrelationIdFromNode(String correlationIdPath, JsonNode jsonNod String asText = node.asText(); if (null != asText && !asText.isEmpty()) { - LoggingUtils.setCorrelationId(asText); + MDC.put(CORRELATION_ID.getName(), asText); } else { LOG.warn("Unable to extract any correlation id. Is your function expecting supported event type?"); } @@ -398,15 +392,6 @@ private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws } } - private Optional asJson(final Object target) { - try { - return ofNullable(LoggingUtils.getObjectMapper().writeValueAsString(target)); - } catch (JsonProcessingException e) { - LOG.error("Failed logging object of type {}", target.getClass(), e); - return empty(); - } - } - private Logger logger(final ProceedingJoinPoint pjp) { return LoggerFactory.getLogger(pjp.getSignature().getDeclaringType()); } diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java b/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java index 2a9322592..acc635e75 100644 --- a/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java @@ -185,6 +185,9 @@ public class TestLogger extends LegacyAbstractLogger { /** The short name of this simple log instance */ private transient String shortLogName = null; + // used for test purpose + private Object[] arguments; + /** * Package access allows only {@link TestLoggerFactory} to instantiate * SimpleLogger instances. @@ -368,6 +371,8 @@ protected void handleNormalizedLoggingCall(Level level, Marker marker, String me private void innerHandleNormalizedLoggingCall(Level level, List markers, String messagePattern, Object[] arguments, Throwable t) { + this.arguments = arguments; + StringBuilder buf = new StringBuilder(32); // Append date-time if so configured @@ -449,4 +454,11 @@ protected String getFullyQualifiedCallerName() { return null; } + public Object[] getArguments() { + return arguments; + } + + public void clearArguments() { + arguments = null; + } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java deleted file mode 100644 index 04e977c58..000000000 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - */ - -package software.amazon.lambda.powertools.logging; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.MDC; -import software.amazon.lambda.powertools.utilities.JsonConfig; - - -class LoggingUtilsTest { - - @BeforeEach - void setUp() { - MDC.clear(); - } - - @Test - void shouldSetCustomKeyInLoggingContext() { - LoggingUtils.appendKey("org/slf4j/test", "value"); - - assertThat(MDC.getCopyOfContextMap()) - .hasSize(1) - .containsEntry("org/slf4j/test", "value"); - } - - @Test - void shouldSetCustomKeyAsMapInLoggingContext() { - Map customKeys = new HashMap<>(); - customKeys.put("org/slf4j/test", "value"); - customKeys.put("test1", "value1"); - - LoggingUtils.appendKeys(customKeys); - - assertThat(MDC.getCopyOfContextMap()) - .hasSize(2) - .containsEntry("org/slf4j/test", "value") - .containsEntry("test1", "value1"); - } - - @Test - void shouldRemoveCustomKeyInLoggingContext() { - LoggingUtils.appendKey("org/slf4j/test", "value"); - - assertThat(MDC.getCopyOfContextMap()) - .hasSize(1) - .containsEntry("org/slf4j/test", "value"); - - LoggingUtils.removeKey("org/slf4j/test"); - - assertThat(MDC.getCopyOfContextMap()) - .isEmpty(); - } - - @Test - void shouldRemoveCustomKeysInLoggingContext() { - Map customKeys = new HashMap<>(); - customKeys.put("org/slf4j/test", "value"); - customKeys.put("test1", "value1"); - - LoggingUtils.appendKeys(customKeys); - - assertThat(MDC.getCopyOfContextMap()) - .hasSize(2) - .containsEntry("org/slf4j/test", "value") - .containsEntry("test1", "value1"); - - LoggingUtils.removeKeys("org/slf4j/test", "test1"); - - assertThat(MDC.getCopyOfContextMap()) - .isEmpty(); - } - - @Test - void shouldAddCorrelationIdToLoggingContext() { - String id = "correlationID_12345"; - LoggingUtils.setCorrelationId(id); - - assertThat(MDC.getCopyOfContextMap()) - .hasSize(1) - .containsEntry("correlation_id", id); - - assertThat(LoggingUtils.getCorrelationId()).isEqualTo(id); - } - - @Test - void shouldGetObjectMapper() { - assertThat(LoggingUtils.getObjectMapper()).isNotNull(); - assertThat(LoggingUtils.getObjectMapper()).isEqualTo(JsonConfig.get().getObjectMapper()); - - ObjectMapper mapper = new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); - LoggingUtils.setObjectMapper(mapper); - assertThat(LoggingUtils.getObjectMapper()).isEqualTo(mapper); - - } -} \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/argument/StructuredArgumentsTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/argument/StructuredArgumentsTest.java new file mode 100644 index 000000000..64200e640 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/argument/StructuredArgumentsTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.model.Basket; +import software.amazon.lambda.powertools.logging.model.Product; + +class StructuredArgumentsTest { + private StringBuilder sb; + private JsonSerializer serializer; + + @BeforeEach + void setUp() { + sb = new StringBuilder(); + serializer = new JsonSerializer(sb); + } + + @Test + void keyValueArgument() throws IOException { + // GIVEN + Basket basket = new Basket(); + basket.add(new Product(42, "Nintendo DS", 299.45)); + basket.add(new Product(98, "Playstation 5", 499.99)); + + // WHEN + StructuredArgument argument = StructuredArguments.entry("basket", basket); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).hasToString("\"basket\":{\"products\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]}"); + assertThat(argument.toString()).hasToString("basket=Basket{products=[Product{id=42, name='Nintendo DS', price=299.45}, Product{id=98, name='Playstation 5', price=499.99}]}"); + } + + @Test + void mapArgument() throws IOException { + // GIVEN + Map catalog = new HashMap<>(); + catalog.put("nds", new Product(42, "Nintendo DS", 299.45)); + catalog.put("ps5", new Product(98, "Playstation 5", 499.99)); + + // WHEN + StructuredArgument argument = StructuredArguments.entries(catalog); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()) + .contains("\"nds\":{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}") + .contains("\"ps5\":{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}"); + assertThat(argument.toString()) + .contains("nds=Product{id=42, name='Nintendo DS', price=299.45}") + .contains("ps5=Product{id=98, name='Playstation 5', price=499.99}"); + } + + @Test + void arrayArgument() throws IOException { + // GIVEN + Product[] products = new Product[]{ + new Product(42, "Nintendo DS", 299.45), + new Product(98, "Playstation 5", 499.99) + }; + + // WHEN + StructuredArgument argument = StructuredArguments.array("products", products); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).contains("\"products\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + assertThat(argument.toString()).contains("products=[Product{id=42, name='Nintendo DS', price=299.45}, Product{id=98, name='Playstation 5', price=499.99}]"); + } + + @Test + void jsonArgument() throws IOException { + // GIVEN + String rawJson = "{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"; + + // WHEN + StructuredArgument argument = StructuredArguments.json("product", rawJson); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).contains("\"product\":{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + assertThat(argument.toString()).contains("product={\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + } + +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java index e1829a777..cb7fbb408 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java @@ -19,8 +19,8 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; public class PowertoolsLogClearState implements RequestHandler, Object> { private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogClearState.class); @@ -28,7 +28,7 @@ public class PowertoolsLogClearState implements RequestHandler input, Context context) { - LoggingUtils.appendKey("mySuperSecret", input.get("mySuperSecret")); + MDC.put("mySuperSecret", input.get("mySuperSecret")); LOG.info("Test event"); return null; } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java index d6c79a445..aa0a5942c 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java @@ -21,7 +21,7 @@ import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEnabled implements RequestHandler { - private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); + private static final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); private final boolean throwError; public PowertoolsLogEnabled(boolean throwError) { @@ -49,4 +49,8 @@ public Object handleRequest(Object input, Context context) { public void anotherMethod() { System.out.println("test"); } + + public static Logger getLogger() { + return LOG; + } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java index 87677d601..c83692e95 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java @@ -16,13 +16,21 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEvent implements RequestHandler { + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogEvent.class); + @Override @Logging(logEvent = true) public Object handleRequest(Object input, Context context) { return null; } + + public static Logger getLogger() { + return logger; + } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventEnvVar.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventEnvVar.java new file mode 100644 index 000000000..230394bf7 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventEnvVar.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEventEnvVar implements RequestHandler { + + private final Logger logger = LoggerFactory.getLogger(PowertoolsLogEventEnvVar.class); + + @Override + @Logging + public Object handleRequest(Object input, Context context) { + return null; + } + + public Logger getLogger() { + return logger; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java index 350b29cde..6e27f47ce 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java @@ -21,14 +21,22 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEventForStream implements RequestStreamHandler { + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogEventForStream.class); + @Override @Logging(logEvent = true) public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(outputStream, mapper.readValue(inputStream, Map.class)); } + + public static Logger getLogger() { + return logger; + } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java index 001bde3ed..e7607d3c9 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java @@ -16,9 +16,16 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogResponse implements RequestHandler { + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogResponse.class); + + public static Logger getLogger() { + return logger; + } @Override @Logging(logResponse = true) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java index 38be5c025..fe591627b 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java @@ -19,10 +19,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogResponseForStream implements RequestStreamHandler { + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogResponseForStream.class); + @Override @Logging(logResponse = true) public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { @@ -32,4 +36,8 @@ public void handleRequest(InputStream inputStream, OutputStream outputStream, Co outputStream.write(buf, 0, length); } } + + public static Logger getLogger() { + return logger; + } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/JsonSerializerTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/JsonSerializerTest.java new file mode 100644 index 000000000..8b34d32cb --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/JsonSerializerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.logging.model.Basket; +import software.amazon.lambda.powertools.logging.model.Product; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +class JsonSerializerTest { + private StringBuilder sb; + private JsonSerializer generator; + + @BeforeEach + void setUp() { + sb = new StringBuilder(); + generator = new JsonSerializer(sb); + } + + @Test + void writeString_shouldWriteStringWithQuotes() throws IOException { + generator.writeStringField("key", "StringValue"); + assertThat(sb.toString()).hasToString("\"key\":\"StringValue\""); + } + + @Test + void writeBoolean_shouldWriteBooleanValue() throws IOException { + generator.writeBooleanField("key", true); + assertThat(sb.toString()).hasToString("\"key\":true"); + } + + @Test + void writeNull_shouldWriteNullValue() throws IOException { + generator.writeNullField("key"); + assertThat(sb.toString()).hasToString("\"key\":null"); + } + + @Test + void writeInt_shouldWriteIntValue() throws IOException { + generator.writeNumberField("key", 1); + assertThat(sb.toString()).hasToString("\"key\":1"); + } + + @Test + void writeFloat_shouldWriteFloatValue() throws IOException { + generator.writeNumberField("key", 2.4f); + assertThat(sb.toString()).hasToString("\"key\":2.4"); + assertThat(sb.toString()).doesNotContain("F").doesNotContain("f"); // should not contain the F suffix for floats. + } + + @Test + void writeDouble_shouldWriteDoubleValue() throws IOException { + generator.writeNumberField("key", 4.3); + assertThat(sb.toString()).hasToString("\"key\":4.3"); + } + + @Test + void writeLong_shouldWriteLongValue() throws IOException { + generator.writeNumberField("key", 123456789L); + assertThat(sb.toString()).hasToString("\"key\":123456789"); + assertThat(sb.toString()).doesNotContain("L").doesNotContain("l"); // should not contain the L suffix for longs. + } + + @Test + void writeBigDecimal_shouldWriteBigDecimal() throws IOException { + generator.writeNumberField("key", BigDecimal.valueOf(432.1673254564546)); + assertThat(sb.toString()).hasToString("\"key\":432.1673254564546"); + } + + @Test + void writeStringObject_shouldWriteStringWithQuotes() throws IOException { + generator.writeObjectField("key","StringValue"); + assertThat(sb.toString()).hasToString("\"key\":\"StringValue\""); + } + + @Test + void writeMapObject_shouldWriteMapAsJson() throws IOException { + Map map = new HashMap<>(); + map.put("string","StringValue"); + map.put("number", BigInteger.valueOf(1234567890L)); + generator.writeObjectField("map", map); + assertThat(sb.toString()).hasToString("\"map\":{\"number\":1234567890,\"string\":\"StringValue\"}"); + } + + @Test + void writeListObject_shouldWriteListAsArray() throws IOException { + List list = Arrays.asList("val1", "val2", "val3"); + generator.writeObjectField("list", list); + assertThat(sb.toString()).hasToString("\"list\":[\"val1\",\"val2\",\"val3\"]"); + } + + @Test + void writeCustomObject_shouldWriteObjectAsJson() throws IOException { + Basket basket = new Basket(); + basket.add(new Product(42, "Nintendo DS", 299.45)); + basket.add(new Product(98, "Playstation 5", 499.99)); + generator.writeObjectField("basket", basket); + assertThat(sb.toString()).hasToString("\"basket\":{\"products\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]}"); + } + + @Test + void writeJsonNodeObject_shouldWriteObjectAsJson() throws IOException { + JsonNode jsonNode = JsonConfig.get().getObjectMapper().readTree( + "[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + generator.writeObjectField("basket", jsonNode); + assertThat(sb.toString()).hasToString("\"basket\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + } + + @Test + void writeTreeNodeArrayObject_shouldWriteObjectAsJson() throws IOException { + TreeNode treeNode = JsonConfig.get().getObjectMapper().readTree( + "[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + generator.writeTree(treeNode); + assertThat(sb.toString()).hasToString("[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + } + + @Test + void writeTreeNodeObject_shouldWriteObjectAsJson() throws IOException { + TreeNode treeNode = JsonConfig.get().getObjectMapper().readTree( + "{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + generator.writeTree(treeNode); + assertThat(sb.toString()).hasToString("{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + } + +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java index bc5e53675..28e20aadd 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java @@ -15,7 +15,6 @@ package software.amazon.lambda.powertools.logging.internal; import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.joining; import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; @@ -52,15 +51,14 @@ import java.lang.reflect.Method; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -70,8 +68,10 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.slf4j.event.Level; +import org.slf4j.test.TestLogger; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.common.internal.SystemWrapper; +import software.amazon.lambda.powertools.logging.argument.KeyValueArgument; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId; @@ -84,7 +84,7 @@ import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId; -import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventDisabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventForStream; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream; @@ -466,51 +466,61 @@ void shouldLogxRayTraceIdEnvVarSet() { void shouldLogEventForHandlerWithLogEventAnnotation() { // GIVEN requestHandler = new PowertoolsLogEvent(); + List listOfOneElement = singletonList("ListOfOneElement"); // WHEN - requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + requestHandler.handleRequest(listOfOneElement, context); // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("[\"ListOfOneElement\"]"); + TestLogger logger = (TestLogger) PowertoolsLogEvent.getLogger(); + assertThat(logger.getArguments()).hasSize(1); + KeyValueArgument argument = (KeyValueArgument) logger.getArguments()[0]; + assertThat(argument.getKey()).isEqualTo("event"); + assertThat(argument.getValue()).isEqualTo(listOfOneElement); } @Test - void shouldLogEventForHandlerWhenEnvVariableSetToTrue() throws IllegalAccessException { - try { - // GIVEN - LoggingConstants.POWERTOOLS_LOG_EVENT = true; + void shouldLogEventForHandlerWhenEnvVariableSetToTrue() { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = true; - requestHandler = new PowertoolsLogEnabled(); + requestHandler = new PowertoolsLogEventEnvVar(); - SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); - message.setBody("body"); - message.setMessageId("1234abcd"); - message.setAwsRegion("eu-west-1"); + SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); + message.setBody("body"); + message.setMessageId("1234abcd"); + message.setAwsRegion("eu-west-1"); - // WHEN - requestHandler.handleRequest(message, context); + // WHEN + requestHandler.handleRequest(message, context); - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("\"body\":\"body\"").contains("\"messageId\":\"1234abcd\"").contains("\"awsRegion\":\"eu-west-1\""); + // THEN + TestLogger logger = (TestLogger) ((PowertoolsLogEventEnvVar)requestHandler).getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + KeyValueArgument argument = (KeyValueArgument) logger.getArguments()[0]; + assertThat(argument.getKey()).isEqualTo("event"); + assertThat(argument.getValue()).isEqualTo(message); } finally { LoggingConstants.POWERTOOLS_LOG_EVENT = false; + if (logger != null){ + logger.clearArguments(); + } } } @Test - void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { + void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() { // GIVEN LoggingConstants.POWERTOOLS_LOG_EVENT = false; // WHEN - requestHandler = new PowertoolsLogEventDisabled(); + requestHandler = new PowertoolsLogEventEnvVar(); requestHandler.handleRequest(singletonList("ListOfOneElement"), context); // THEN - Assertions.assertEquals(0, - Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + TestLogger logger = (TestLogger) ((PowertoolsLogEventEnvVar)requestHandler).getLogger(); + assertThat(logger.getArguments()).isNull(); } @Test @@ -518,16 +528,24 @@ void shouldLogEventForStreamHandler() throws IOException { // GIVEN requestStreamHandler = new PowertoolsLogEventForStream(); ByteArrayOutputStream output = new ByteArrayOutputStream(); + Map map = Collections.singletonMap("key", "value"); // WHEN - requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(Collections.singletonMap("key", "value"))), output, context); + requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(map)), output, context); // THEN assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) .isNotEmpty(); - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("{\"key\":\"value\"}"); + TestLogger logger = (TestLogger) PowertoolsLogEventForStream.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + KeyValueArgument argument = (KeyValueArgument) logger.getArguments()[0]; + assertThat(argument.getKey()).isEqualTo("event"); + assertThat(argument.getValue()).isEqualTo("{\"key\":\"value\"}"); + } finally { + logger.clearArguments(); + } } @Test @@ -539,26 +557,37 @@ void shouldLogResponseForHandlerWithLogResponseAnnotation() { requestHandler.handleRequest("input", context); // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("Hola mundo"); + TestLogger logger = (TestLogger) PowertoolsLogResponse.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + KeyValueArgument argument = (KeyValueArgument) logger.getArguments()[0]; + assertThat(argument.getKey()).isEqualTo("response"); + assertThat(argument.getValue()).isEqualTo("Hola mundo"); + } finally { + logger.clearArguments(); + } } @Test - void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() throws IllegalAccessException { - try { - // GIVEN - LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; + void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; - requestHandler = new PowertoolsLogEnabled(); + requestHandler = new PowertoolsLogEnabled(); - // WHEN - requestHandler.handleRequest("input", context); + // WHEN + requestHandler.handleRequest("input", context); - // THEN - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains("Bonjour le monde"); + // THEN + TestLogger logger = (TestLogger) PowertoolsLogEnabled.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + KeyValueArgument argument = (KeyValueArgument) logger.getArguments()[0]; + assertThat(argument.getKey()).isEqualTo("response"); + assertThat(argument.getValue()).isEqualTo("Bonjour le monde"); } finally { LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + logger.clearArguments(); } } @@ -576,8 +605,15 @@ void shouldLogResponseForStreamHandler() throws IOException { assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) .isEqualTo(input); - File logFile = new File("target/logfile.json"); - assertThat(contentOf(logFile)).contains(input); + TestLogger logger = (TestLogger) PowertoolsLogResponseForStream.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + KeyValueArgument argument = (KeyValueArgument) logger.getArguments()[0]; + assertThat(argument.getKey()).isEqualTo("response"); + assertThat(argument.getValue()).isEqualTo(input); + } finally { + logger.clearArguments(); + } } @Test @@ -598,7 +634,7 @@ void shouldLogErrorForHandlerWithLogErrorAnnotation() { } @Test - void shouldLogErrorForHandlerWhenEnvVariableSetToTrue() throws IllegalAccessException { + void shouldLogErrorForHandlerWhenEnvVariableSetToTrue() { try { // GIVEN LoggingConstants.POWERTOOLS_LOG_ERROR = true; diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Basket.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Basket.java new file mode 100644 index 000000000..0fa544cf1 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Basket.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Basket { + private List products = new ArrayList<>(); + + public Basket() { + } + + public Basket(Product... p) { + products.addAll(Arrays.asList(p)); + } + + public List getProducts() { + return products; + } + + public void setProducts(List products) { + this.products = products; + } + + public void add(Product product) { + products.add(product); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Basket basket = (Basket) o; + return products.equals(basket.products); + } + + @Override + public int hashCode() { + return Objects.hash(products); + } + + @Override + public String toString() { + return "Basket{" + + "products=" + products + + '}'; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Product.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Product.java new file mode 100644 index 000000000..fee5bc20d --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Product.java @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.logging.model; + +import java.util.Objects; + +public class Product { + private long id; + + private String name; + + private double price; + + public Product() { + } + + public Product(long id, String name, double price) { + this.id = id; + this.name = name; + this.price = price; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Product product = (Product) o; + return id == product.id && Double.compare(product.price, price) == 0 && Objects.equals(name, product.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, price); + } + + @Override + public String toString() { + return "Product{" + + "id=" + id + + ", name='" + name + '\'' + + ", price=" + price + + '}'; + } +} diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java index e961f21fa..fc0f083e5 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java @@ -14,29 +14,51 @@ package software.amazon.lambda.powertools.utilities; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import io.burt.jmespath.JmesPath; import io.burt.jmespath.RuntimeConfiguration; import io.burt.jmespath.function.BaseFunction; import io.burt.jmespath.function.FunctionRegistry; import io.burt.jmespath.jackson.JacksonRuntime; +import java.util.function.Supplier; import software.amazon.lambda.powertools.utilities.jmespath.Base64Function; import software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction; import software.amazon.lambda.powertools.utilities.jmespath.JsonFunction; -public class JsonConfig { - private static final ThreadLocal om = ThreadLocal.withInitial(ObjectMapper::new); +public final class JsonConfig { + + private static final Supplier objectMapperSupplier = () -> JsonMapper.builder() + // Don't throw an exception when json has extra fields you are not serializing on. + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // Ignore null values when writing json. + .serializationInclusion(JsonInclude.Include.NON_NULL) + // Write times as a String instead of a Long so its human-readable. + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + // Sort fields in alphabetical order + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .build(); + + private static final ThreadLocal om = ThreadLocal.withInitial(objectMapperSupplier); + private final FunctionRegistry defaultFunctions = FunctionRegistry.defaultRegistry(); + private final FunctionRegistry customFunctions = defaultFunctions.extend( new Base64Function(), new Base64GZipFunction(), new JsonFunction() ); + private final RuntimeConfiguration configuration = new RuntimeConfiguration.Builder() .withSilentTypeErrors(true) .withFunctionRegistry(customFunctions) .build(); + private JmesPath jmesPath = new JacksonRuntime(configuration, getObjectMapper()); private JsonConfig() { diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index fcfdb47e3..2914bd286 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -39,7 +39,6 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import software.amazon.lambda.powertools.utilities.model.Basket; import software.amazon.lambda.powertools.utilities.model.Order; import software.amazon.lambda.powertools.utilities.model.Product; @@ -93,14 +92,6 @@ public void testDeserializeAPIGWEventBodyAsObject_shouldReturnObject(APIGatewayP assertProduct(product); } - @ParameterizedTest - @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) - public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException(APIGatewayProxyRequestEvent event) { - assertThatThrownBy(() -> extractDataFrom(event).as(Basket.class)) - .isInstanceOf(EventDeserializationException.class) - .hasMessage("Cannot load the event as Basket"); - } - @ParameterizedTest @Event(value = "sns_event.json", type = SNSEvent.class) public void testDeserializeSNSEventMessageAsObject_shouldReturnObject(SNSEvent event) { @@ -164,14 +155,6 @@ public void testDeserializeEmptyEventAsList_shouldThrowException() { .hasMessage("Event content is null: the event may be malformed (missing fields)"); } - @ParameterizedTest - @Event(value = "sqs_event.json", type = SQSEvent.class) - public void testDeserializeSQSEventBodyAsWrongObjectType_shouldThrowException(SQSEvent event) { - assertThatThrownBy(() -> extractDataFrom(event).asListOf(Basket.class)) - .isInstanceOf(EventDeserializationException.class) - .hasMessage("Cannot load the event as a list of Basket"); - } - @ParameterizedTest @Event(value = "apigw_event_no_body.json", type = APIGatewayProxyRequestEvent.class) public void testDeserializeAPIGatewayNoBody_shouldThrowException(APIGatewayProxyRequestEvent event) { diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index ee39b5d0f..a4eb3756f 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -71,6 +71,9 @@ + + + @@ -185,10 +188,6 @@ - - - - @@ -206,10 +205,6 @@ - - - -